diff --git a/__wasm/wit-bindgen-sample/container/src/main.rs b/__wasm/wit-bindgen-sample/container/src/main.rs index a5c4ce1..660e317 100644 --- a/__wasm/wit-bindgen-sample/container/src/main.rs +++ b/__wasm/wit-bindgen-sample/container/src/main.rs @@ -10,7 +10,10 @@ pub struct MyContainer; impl Container for MyContainer { fn fetch(&mut self, s: &str) -> String { - format!("FETCHED: {}", s) + // format!("FETCHED: {}", s) + let r = "{\"result\":\"{}\"}".into(); + println!(">>>> {}", r); + r } } @@ -25,9 +28,12 @@ fn main() -> Result<()> { |linker| container::add_to_linker(linker, |cx| -> &mut MyContainer { &mut cx.imports }), |store, module, linker| Exports::instantiate(store, module, linker, |cx| &mut cx.exports), )?; - let a = exports.eval_javascript(&mut store, "[script]"); + let a = exports.eval_javascript(&mut store, "let a = [];fetch('aaa')"); - println!("Hello, world! {:?}", a); + match a { + Ok(a) => println!("Hello, world! {:?}", a), + Err(e) => println!("ERROR: {}", e), + } Ok(()) } diff --git a/__wasm/wit-bindgen-sample/engine/Cargo.lock b/__wasm/wit-bindgen-sample/engine/Cargo.lock index 85749f0..06d36a6 100644 --- a/__wasm/wit-bindgen-sample/engine/Cargo.lock +++ b/__wasm/wit-bindgen-sample/engine/Cargo.lock @@ -19,20 +19,152 @@ dependencies = [ "syn", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "bitflags" version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "boa_engine" +version = "0.15.0" +dependencies = [ + "bitflags", + "boa_gc", + "boa_interner", + "boa_profiler", + "boa_unicode", + "chrono", + "dyn-clone", + "fast-float", + "gc", + "indexmap", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rand", + "regress", + "rustc-hash", + "ryu-js", + "serde", + "serde_json", + "tap", + "unicode-normalization", +] + +[[package]] +name = "boa_gc" +version = "0.15.0" +dependencies = [ + "gc", +] + +[[package]] +name = "boa_interner" +version = "0.15.0" +dependencies = [ + "phf", + "rustc-hash", + "static_assertions", +] + +[[package]] +name = "boa_profiler" +version = "0.15.0" + +[[package]] +name = "boa_unicode" +version = "0.15.0" +dependencies = [ + "unicode-general-category", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "dyn-clone" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d07a982d1fb29db01e5a59b1918e03da4df7297eaeee7686ac45542fd4e59c8" + [[package]] name = "engine" version = "0.1.0" dependencies = [ + "boa_engine", + "getrandom", + "serde", + "serde_json", "wit-bindgen-gen-core", "wit-bindgen-rust", ] +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "gc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" +dependencies = [ + "gc_derive", +] + +[[package]] +name = "gc_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "hashbrown" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022" + [[package]] name = "heck" version = "0.3.3" @@ -48,12 +180,127 @@ version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + [[package]] name = "proc-macro2" version = "1.0.40" @@ -83,6 +330,106 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "regress" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a92ff21fe8026ce3f2627faaf43606f0b67b014dbc9ccf027181a804f75d92e" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "syn" version = "1.0.98" @@ -94,6 +441,35 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -118,6 +494,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-general-category" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6" + [[package]] name = "unicode-ident" version = "1.0.2" @@ -151,6 +533,40 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "wit-bindgen-gen-core" version = "0.1.0" diff --git a/__wasm/wit-bindgen-sample/engine/Cargo.toml b/__wasm/wit-bindgen-sample/engine/Cargo.toml index 8ef5aa8..1447308 100644 --- a/__wasm/wit-bindgen-sample/engine/Cargo.toml +++ b/__wasm/wit-bindgen-sample/engine/Cargo.toml @@ -8,6 +8,20 @@ edition = "2021" [lib] crate-type = ['cdylib'] +[profile.release] +codegen-units = 1 +incremental = true +lto = true +opt-level = "z" + [dependencies] wit-bindgen-gen-core = { path = '../wit-bindgen/crates/gen-core' } wit-bindgen-rust = { path = '../wit-bindgen/crates/rust-wasm' } +boa_engine = "0.15.0" +getrandom = { version = "0.2", features = ["js"] } +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +[patch.crates-io] +getrandom = { path = "./getrandom" } +boa_engine = { path = "./boa/boa_engine" } \ No newline at end of file diff --git a/__wasm/wit-bindgen-sample/engine/boa/.editorconfig b/__wasm/wit-bindgen-sample/engine/boa/.editorconfig new file mode 100644 index 0000000..4e2d43c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.editorconfig @@ -0,0 +1,11 @@ +root = true + +[{Makefile,**.mk}] +# Use tabs for indentation (Makefiles require tabs) +indent_style = tab + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true diff --git a/__wasm/wit-bindgen-sample/engine/boa/.gitattributes b/__wasm/wit-bindgen-sample/engine/boa/.gitattributes new file mode 100644 index 0000000..951b3ca --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.gitattributes @@ -0,0 +1,30 @@ +# Handle line endings automatically for files detected as text +# and leave all files detected as binary untouched. +* text=auto + +# +# The above will handle all files NOT found below +# +# These files are text and should be normalized (Convert crlf => lf) +*.css eol=lf +*.htm eol=lf +*.html eol=lf +*.js eol=lf +*.json eol=lf +*.sh eol=lf +*.txt eol=lf +*.yml eol=lf +*.rs eol=lf +*.toml eol=lf +*.lock eol=lf +*.md eol=lf +*.svg eol=lf + +# These files are binary and should be left untouched +# (binary is a macro for -text -diff) +*.gif binary +*.ico binary +*.jar binary +*.jpg binary +*.jpeg binary +*.png binary diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/FUNDING.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/FUNDING.yml new file mode 100644 index 0000000..1cb1d55 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/FUNDING.yml @@ -0,0 +1 @@ +open_collective: boa diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/bug_report.md b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..6e24984 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,52 @@ +--- +name: "\U0001F41B Bug report" +about: Create a report to help us improve +title: "" +labels: bug +assignees: "" +--- + + + +**Describe the bug** +A clear and concise description of what the bug is. + + + +**To Reproduce** +Steps to reproduce the issue, or JavaScript code that causes this failure. + + + +**Expected behavior** +Explain what you expected to happen, and what is happening instead. + + + +**Build environment (please complete the following information):** + +- OS: [e.g. Fedora Linux] +- Version: [e.g. 32] +- Target triple: [e.g. x86_64-unknown-linux-gnu] +- Rustc version: [e.g. rustc 1.43.0 (4fb7144ed 2020-04-20), running `rustc -V`] + +**Additional context** +Add any other context about the problem here. + + diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/config.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d1962be --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Discord channel + url: https://discord.gg/tUFFk9Y + about: Please ask and answer questions here. diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/custom.md b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/custom.md new file mode 100644 index 0000000..56ac092 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/custom.md @@ -0,0 +1,13 @@ +--- +name: Custom +about: Open an issue in the repo that is neither a bug or a feature, such a new idea +title: "" +labels: "" +assignees: "" +--- + + + +E.g.: I think we should improve the way the JavaScript interpreter works by... diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/feature_request.md b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7687b7b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,43 @@ +--- +name: "\U0001F680 Feature request" +about: Suggest a new ECMAScript feature to be implemented, or a new capability of the engine. +title: "" +labels: enhancement +assignees: "" +--- + + + +**ECMASCript feature** +Explain the ECMAScript feature that you'd like to see implemented. + + + +**Example code** +Give a code example that should work after the implementation of this feature. + + diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/PULL_REQUEST_TEMPLATE.md b/__wasm/wit-bindgen-sample/engine/boa/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..333605a --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,12 @@ + + +This Pull Request fixes/closes #{issue_num}. + +It changes the following: + +- +- +- diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/codecov.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/codecov.yml new file mode 100644 index 0000000..e377752 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/codecov.yml @@ -0,0 +1,10 @@ +github_checks: + annotations: false + +coverage: + status: + project: + default: + threshold: 5% # allow 5% coverage variance + + patch: off diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/dependabot.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/dependabot.yml new file mode 100644 index 0000000..9397dd8 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/dependabot.yml @@ -0,0 +1,50 @@ +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: daily + - package-ecosystem: github-actions + directory: / + schedule: + interval: daily + - package-ecosystem: cargo + directory: / + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_cli/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_engine/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_gc/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_interner/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_profiler/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_tester/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_unicode/ + schedule: + interval: daily + - package-ecosystem: cargo + directory: /boa_wasm/ + schedule: + interval: daily + - package-ecosystem: gitsubmodule + directory: / + schedule: + interval: weekly diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/release.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/release.yml new file mode 100644 index 0000000..20e969d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/release.yml @@ -0,0 +1,16 @@ +# .github/release.yml + +changelog: + exclude: + authors: + - dependabot + categories: + - title: Feature Enhancements + labels: + - enhancement + - title: Bug Fixes + labels: + - bug + - title: Internal Improvements + labels: + - Internal diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/bors.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/bors.yml new file mode 100644 index 0000000..af37852 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/bors.yml @@ -0,0 +1,162 @@ +name: bors + +on: + push: + branches: + - staging + - trying + +jobs: + test_on_linux: + name: Tests on Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + - uses: actions-rs/cargo@v1 + with: + command: test + args: -v + + test_on_windows: + name: Tests on Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + - uses: actions-rs/cargo@v1 + with: + command: test + args: -v + + test_on_macos: + name: Tests on MacOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - uses: actions-rs/cargo@v1 + with: + command: test + args: -v + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + components: clippy + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- --verbose + + examples: + name: Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-examples-${{ hashFiles('**/Cargo.lock') }} + - run: cd boa_examples + - name: Build examples + uses: actions-rs/cargo@v1 + with: + command: build + - name: Run example classes + uses: actions-rs/cargo@v1 + with: + command: run + args: --bin classes + + doc: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-doc-${{ hashFiles('**/Cargo.lock') }} + - name: Generate documentation + uses: actions-rs/cargo@v1 + with: + command: doc + args: -v --document-private-items --all-features diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/master.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/master.yml new file mode 100644 index 0000000..f1b4dd5 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/master.yml @@ -0,0 +1,54 @@ +name: Main workflows +on: + push: + branches: + - main + +jobs: + benchmark: + if: ${{ github.actor != 'dependabot[bot]' }} + name: Upload docs and run benchmarks + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + !target/doc_upload + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-doc-bench-${{ hashFiles('**/Cargo.lock') }} + - name: Generate documentation + uses: actions-rs/cargo@v1 + with: + command: doc + args: -v --document-private-items --all-features + - run: echo "" > target/doc/index.html + - run: | + if [ -d target/doc_upload ]; then rm -rf target/doc_upload; fi + mkdir target/doc_upload && mv target/doc target/doc_upload/doc + - name: Upload documentation + uses: crazy-max/ghaction-github-pages@v3.0.0 + with: + target_branch: gh-pages + keep_history: true + build_dir: target/doc_upload + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Run benchmark + run: cargo bench -p boa_engine -- --output-format bencher | tee output.txt + - name: Store benchmark result + uses: benchmark-action/github-action-benchmark@v1.14.0 + with: + name: Boa Benchmarks + tool: "cargo" + output-file-path: output.txt + auto-push: true + github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/pull_request.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/pull_request.yml new file mode 100644 index 0000000..2f008df --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/pull_request.yml @@ -0,0 +1,34 @@ +name: Benchmarks + +on: + pull_request: + branches: + - main + +jobs: + runBenchmark: + if: contains(github.event.pull_request.labels.*.name, 'run-benchmark') + name: run benchmark + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + - uses: boa-dev/criterion-compare-action@v3.2.2 + with: + token: ${{ secrets.GITHUB_TOKEN }} + branchName: ${{ github.base_ref }} + cwd: ./boa_engine diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/release.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/release.yml new file mode 100644 index 0000000..bf19966 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/release.yml @@ -0,0 +1,62 @@ +name: Publish Release +on: + release: + types: [published] + +jobs: + publish: + name: publish + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Install Rust toolchain + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + + - name: Install cargo-workspaces + uses: actions-rs/install@v0.1 + with: + crate: cargo-workspaces + + - name: Release + env: + CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} + PATCH: ${{ github.run_number }} + shell: bash + run: | + git config --global user.email "runner@gha.local" + git config --global user.name "Github Action" + cargo workspaces publish --from-git --yes minor + doc-publish: + # needs: publish + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - uses: actions/setup-node@v3 + with: + node-version: "16" + - run: npm ci + - name: Cache npm build + uses: actions/cache@v3 + with: + path: | + node_modules + target + boa_wasm/pkg + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-npm-build-target-${{ hashFiles('**/package-lock.json') }} + - run: npm run build:prod + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + publish_dir: ./dist + destination_dir: playground + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/rust.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/rust.yml new file mode 100644 index 0000000..e7c170d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/rust.yml @@ -0,0 +1,166 @@ +on: + pull_request: + branches: + - main + push: + branches: + - main + +name: Continuous integration + +jobs: + test_on_linux: + name: Tests on Linux + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + - name: Run cargo-tarpaulin + uses: actions-rs/tarpaulin@v0.1 + with: + args: --features intl --ignore-tests + - name: Upload to codecov.io + uses: codecov/codecov-action@v3 + + test_on_windows: + name: Tests on Windows + runs-on: windows-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.lock') }} + - uses: actions-rs/cargo@v1 + with: + command: test + args: -v --features intl + + test_on_macos: + name: Tests on MacOS + runs-on: macos-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - uses: actions-rs/cargo@v1 + with: + command: test + args: -v --features intl + + fmt: + name: Rustfmt + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + components: rustfmt + - uses: actions-rs/cargo@v1 + with: + command: fmt + args: --all -- --check + + clippy: + name: Clippy + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + components: clippy + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-clippy-${{ hashFiles('**/Cargo.lock') }} + - uses: actions-rs/cargo@v1 + with: + command: clippy + args: -- --verbose + + examples: + name: Examples + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-examples-${{ hashFiles('**/Cargo.lock') }} + - run: cd boa_examples + - name: Build examples + uses: actions-rs/cargo@v1 + with: + command: build + - name: Run example classes + uses: actions-rs/cargo@v1 + with: + command: run + args: --bin classes + + doc: + name: Documentation + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-doc-${{ hashFiles('**/Cargo.lock') }} + - name: Generate documentation + uses: actions-rs/cargo@v1 + with: + command: doc + args: -v --document-private-items --all-features diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/security_audit.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/security_audit.yml new file mode 100644 index 0000000..d54cb3c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/security_audit.yml @@ -0,0 +1,12 @@ +name: Security audit +on: + schedule: + - cron: "0 0 * * *" +jobs: + audit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/audit-check@v1.2.0 + with: + token: ${{ secrets.GITHUB_TOKEN }} diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/test262.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/test262.yml new file mode 100644 index 0000000..8a047b3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/test262.yml @@ -0,0 +1,119 @@ +name: EcmaScript official test suite (test262) +on: + push: + branches: + - main + tags: + - v* + pull_request: + branches: + - main + +jobs: + run_test262: + name: Run the test262 test suite + runs-on: ubuntu-latest + steps: + - name: Checkout the repository + uses: actions/checkout@v3 + with: + submodules: true + path: boa + - name: Install the Rust toolchain + uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Cache cargo + uses: actions/cache@v3 + with: + path: | + target + ~/.cargo/git + ~/.cargo/registry + key: ${{ runner.os }}-cargo-test262-${{ hashFiles('**/Cargo.lock') }} + + # Run the test suite and upload the results + - name: Checkout GitHub pages + uses: actions/checkout@v3 + with: + ref: gh-pages + path: gh-pages + + - name: Run the test262 test suite + run: | + cd boa + mkdir ../results + cargo run --release --bin boa_tester -- run -v -o ../results/test262 + cd .. + + # Run the results comparison + - name: Compare results + if: github.event_name == 'pull_request' + id: compare-non-vm + shell: bash + run: | + cd boa + comment="$(./target/release/boa_tester compare ../gh-pages/test262/refs/heads/main/latest.json ../results/test262/pull/latest.json -m)" + comment="${comment//'%'/'%25'}" + comment="${comment//$'\n'/'%0A'}" + comment="${comment//$'\r'/'%0D'}" + echo "::set-output name=comment::$comment" + + - name: Get the PR number + if: github.event_name == 'pull_request' + id: pr-number + uses: kkak10/pr-number-action@v1.3 + + - name: Find Previous Comment + if: github.event_name == 'pull_request' + uses: peter-evans/find-comment@v2 + id: previous-comment + with: + issue-number: ${{ steps.pr-number.outputs.pr }} + body-includes: Test262 conformance changes + + - name: Update comment + if: github.event_name == 'pull_request' && steps.previous-comment.outputs.comment-id + uses: peter-evans/create-or-update-comment@v2 + continue-on-error: true + with: + comment-id: ${{ steps.previous-comment.outputs.comment-id }} + body: | + ### Test262 conformance changes + + ${{ steps.compare-non-vm.outputs.comment }} + ${{ steps.compare-vm.outputs.comment }} + edit-mode: replace + + - name: Write a new comment + if: github.event_name == 'pull_request' && !steps.previous-comment.outputs.comment-id + uses: peter-evans/create-or-update-comment@v2 + continue-on-error: true + with: + issue-number: ${{ steps.pr-number.outputs.pr }} + body: | + ### Test262 conformance changes + + ${{ steps.compare-non-vm.outputs.comment }} + ${{ steps.compare-vm.outputs.comment }} + + # Commit changes to GitHub pages. + - name: Commit files + if: github.event_name == 'push' + run: | + cp -r ./results/test262/* ./gh-pages/test262/ + cd gh-pages + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + git add test262 + git commit -m "Add new test262 results" -a + cd .. + - name: Upload results + if: github.event_name == 'push' + uses: ad-m/github-push-action@v0.6.0 + with: + directory: gh-pages + branch: gh-pages + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/webassembly.yml b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/webassembly.yml new file mode 100644 index 0000000..6f2c769 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.github/workflows/webassembly.yml @@ -0,0 +1,49 @@ +on: + pull_request: + branches: + - main + push: + branches: + - main + +name: Webassembly demo + +jobs: + check_style: + name: Check webassembly demo style + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v3 + - name: Check code formatting + run: npx prettier --check . + + build: + name: Build webassembly demo + runs-on: ubuntu-latest + env: + WASM_PACK_PATH: ~/.cargo/bin/wasm-pack + steps: + - uses: actions/checkout@v3 + - uses: actions-rs/toolchain@v1.0.7 + with: + toolchain: stable + override: true + profile: minimal + - name: Install wasm-pack + run: curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh + - name: Cache npm build + uses: actions/cache@v3 + with: + path: | + node_modules + target + ~/.cargo/git + ~/.cargo/registry + boa_wasm/pkg + key: ${{ runner.os }}-npm-build-target-${{ hashFiles('**/package-lock.json') }} + - uses: actions/setup-node@v3 + with: + node-version: "16" + - run: npm ci + - run: npm run build diff --git a/__wasm/wit-bindgen-sample/engine/boa/.gitignore b/__wasm/wit-bindgen-sample/engine/boa/.gitignore new file mode 100644 index 0000000..56396d4 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.gitignore @@ -0,0 +1,34 @@ +# IDE +.idea/ +*.iml + +# Vim +*.*.swp +*.*.swo + +# Build +target +dist +**/*.rs.bk +node_modules +.DS_Store +yarn-error.log +.vscode/settings.json + +# tests/js/test.js is used for testing changes locally +tests/js/test.js +.boa_history + +# Profiling +*.string_data +*.string_index +*.events +chrome_profiler.json +*.mm_profdata + +# Logs +*.log + +# Yarn +.yarn +.yarnrc.yml diff --git a/__wasm/wit-bindgen-sample/engine/boa/.gitmodules b/__wasm/wit-bindgen-sample/engine/boa/.gitmodules new file mode 100644 index 0000000..c41542f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.gitmodules @@ -0,0 +1,3 @@ +[submodule "test262"] + path = test262 + url = https://github.com/tc39/test262.git diff --git a/__wasm/wit-bindgen-sample/engine/boa/.prettierignore b/__wasm/wit-bindgen-sample/engine/boa/.prettierignore new file mode 100644 index 0000000..ce0118b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.prettierignore @@ -0,0 +1,10 @@ +# Ignore artifacts: +*.rs +target +node_modules +boa_engine/benches/bench_scripts/mini_js.js +boa_engine/benches/bench_scripts/clean_js.js +boa_wasm/pkg +dist +test262 +tests/js/test.js diff --git a/__wasm/wit-bindgen-sample/engine/boa/.vscode/launch.json b/__wasm/wit-bindgen-sample/engine/boa/.vscode/launch.json new file mode 100644 index 0000000..ce5770f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "lldb", + "request": "launch", + "name": "Launch", + "windows": { + "program": "${workspaceFolder}/target/debug/boa.exe" + }, + "program": "${workspaceFolder}/target/debug/boa", + "args": ["${workspaceFolder}/tests/js/test.js"], + "sourceLanguages": ["rust"] + }, + { + "type": "lldb", + "request": "launch", + "name": "Launch (VM)", + "cargo": { + "args": [ + "run", + "--manifest-path", + "./boa_cli/Cargo.toml", + "--features", + "vm" + ] + }, + "args": ["-t", "${workspaceFolder}/tests/js/test.js"], + "sourceLanguages": ["rust"] + } + ] +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/.vscode/tasks.json b/__wasm/wit-bindgen-sample/engine/boa/.vscode/tasks.json new file mode 100644 index 0000000..71c4d1a --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/.vscode/tasks.json @@ -0,0 +1,129 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=733558 + // for the documentation about the tasks.json format + "version": "2.0.0", + "tasks": [ + { + "type": "process", + "label": "Cargo Run", + "command": "cargo", + "args": ["run", "--bin", "boa", "./tests/js/test.js"], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true + }, + "options": { + "env": { + "RUST_BACKTRACE": "1" + } + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Cargo Run (VM)", + "command": "cargo", + "args": ["run", "--", "-t", "../tests/js/test.js"], + "group": { + "kind": "build", + "isDefault": true + }, + "presentation": { + "clear": true + }, + "options": { + "cwd": "${workspaceFolder}/boa_cli", + "env": { + "RUST_BACKTRACE": "1" + } + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Cargo Run (Profiler)", + "command": "cargo", + "args": ["run", "--features", "Boa/profiler", "../tests/js/test.js"], + "group": "build", + "options": { + "env": { + "RUST_BACKTRACE": "full" + }, + "cwd": "${workspaceFolder}/boa_cli" + }, + "presentation": { + "clear": true + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Cargo Run (Profiler & VM)", + "command": "cargo", + "args": [ + "run", + "--features", + "Boa/profiler", + "--features", + "vm", + "../tests/js/test.js" + ], + "group": "build", + "options": { + "env": { + "RUST_BACKTRACE": "full" + }, + "cwd": "${workspaceFolder}/boa_cli" + }, + "presentation": { + "clear": true + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Get Tokens", + "command": "cargo", + "args": ["run", "--bin", "boa", "--", "-t=Debug", "./tests/js/test.js"], + "group": "build", + "presentation": { + "clear": true + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Get AST", + "command": "cargo", + "args": ["run", "--bin", "boa", "--", "-a=Debug", "./tests/js/test.js"], + "group": "build", + "presentation": { + "clear": true + }, + "problemMatcher": [] + }, + { + "type": "process", + "label": "Cargo Test", + "command": "cargo", + "args": ["test"], + "group": { + "kind": "test", + "isDefault": true + }, + "presentation": { + "clear": true + } + }, + { + "type": "process", + "label": "Cargo Test Build", + "command": "cargo", + "args": ["test", "--no-run"], + "group": "build" + } + ] +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/CHANGELOG.md b/__wasm/wit-bindgen-sample/engine/boa/CHANGELOG.md new file mode 100644 index 0000000..dd7af0b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/CHANGELOG.md @@ -0,0 +1,1209 @@ +# CHANGELOG + +## What's Changed + +# [0.15.0 (2022-06-10)](https://github.com/boa-dev/boa/compare/v0.14...v0.15) + +### Feature Enhancements + +- Deploy playground to custom destination dir by @jedel1043 in [#1943](https://github.com/boa-dev/boa/pull/1943) +- add README for crates.io publish by @superhawk610 in [#1952](https://github.com/boa-dev/boa/pull/1952) +- migrated to clap 3 by @manthanabc in [#1957](https://github.com/boa-dev/boa/pull/1957) +- Implement unscopables for Array.prototype by @NorbertGarfield in [#1963](https://github.com/boa-dev/boa/pull/1963) +- Retrieve feature-based results for Test262 runs by @NorbertGarfield in [#1980](https://github.com/boa-dev/boa/pull/1980) +- Added better error handling for the Boa tester by @Razican in [#1984](https://github.com/boa-dev/boa/pull/1984) +- Add From for JsValue by @lastmjs in [#1990](https://github.com/boa-dev/boa/pull/1990) +- Implement Classes by @raskad in [#1976](https://github.com/boa-dev/boa/pull/1976) +- Allow `PropertyName`s in `BindingProperty`in `ObjectBindingPattern` by @raskad in [#2022](https://github.com/boa-dev/boa/pull/2022) +- Allow `Initializer` after `ArrayBindingPattern` in `FormalParameter` by @raskad in [#2002](https://github.com/boa-dev/boa/pull/2002) +- Allow unicode escaped characters in identifiers that are keywords by @raskad in [#2021](https://github.com/boa-dev/boa/pull/2021) +- Feature `JsTypedArray`s by @HalidOdat in [#2003](https://github.com/boa-dev/boa/pull/2003) +- Allow creating object with true/false property names by @lupd in [#2028](https://github.com/boa-dev/boa/pull/2028) +- Implement `get RegExp.prototype.hasIndices` by @HalidOdat in [#2031](https://github.com/boa-dev/boa/pull/2031) +- Partial implementation for Intl.DateTimeFormat by @NorbertGarfield in [#2025](https://github.com/boa-dev/boa/pull/2025) +- Allow `let` as variable declaration name by @raskad in [#2044](https://github.com/boa-dev/boa/pull/2044) +- cargo workspaces fixes #2001 by @jasonwilliams in [#2026](https://github.com/boa-dev/boa/pull/2026) +- Move redeclaration errors to parser by @raskad in [#2027](https://github.com/boa-dev/boa/pull/2027) +- Feature `JsFunction` by @HalidOdat in [#2015](https://github.com/boa-dev/boa/pull/2015) +- Improve `JsString` performance by @YXL76 in [#2042](https://github.com/boa-dev/boa/pull/2042) +- Implement ResolveLocale helper by @NorbertGarfield in [#2036](https://github.com/boa-dev/boa/pull/2036) +- Refactor `IdentifierReference` parsing by @raskad in [#2055](https://github.com/boa-dev/boa/pull/2055) +- Implement the global `eval()` function by @raskad in [#2041](https://github.com/boa-dev/boa/pull/2041) +- DateTimeFormat helpers by @NorbertGarfield in [#2064](https://github.com/boa-dev/boa/pull/2064) +- Create `Date` standard constructor by @jedel1043 in [#2079](https://github.com/boa-dev/boa/pull/2079) +- Implement `ProxyBuilder` by @jedel1043 in [#2076](https://github.com/boa-dev/boa/pull/2076) +- Remove `strict` flag from `Context` by @raskad in [#2069](https://github.com/boa-dev/boa/pull/2069) +- Integrate ICU4X into `Intl` module by @jedel1043 in [#2083](https://github.com/boa-dev/boa/pull/2083) +- Implement `Function` constructor by @raskad in [#2090](https://github.com/boa-dev/boa/pull/2090) +- Parse private generator methods in classes by @raskad in [#2092](https://github.com/boa-dev/boa/pull/2092) + +### Bug Fixes + +- Fix link to the playground by @raskad in [#1947](https://github.com/boa-dev/boa/pull/1947) +- convert inner datetime to local in `to_date_string` by @superhawk610 in [#1953](https://github.com/boa-dev/boa/pull/1953) +- Fix panic on AST dump in JSON format by @kilotaras in [#1959](https://github.com/boa-dev/boa/pull/1959) +- Fix panic in do while by @pdogr in [#1968](https://github.com/boa-dev/boa/pull/1968) +- Support numbers with multiple leading zeroes by @lupd in [#1979](https://github.com/boa-dev/boa/pull/1979) +- Fix length properties on array methods by @lupd in [#1983](https://github.com/boa-dev/boa/pull/1983) +- Allow boolean/null as property identifier by dot operator assignment by @lupd in [#1985](https://github.com/boa-dev/boa/pull/1985) +- fix(vm): off-by-one in code block stringification. by @tsutton in [#1999](https://github.com/boa-dev/boa/pull/1999) +- Indicate bigint has constructor by @lupd in [#2008](https://github.com/boa-dev/boa/pull/2008) +- Change `ArrayBuffer` `byteLength` to accessor property by @lupd in [#2010](https://github.com/boa-dev/boa/pull/2010) +- Fix `ArrayBuffer.isView()` by @HalidOdat in [#2019](https://github.com/boa-dev/boa/pull/2019) +- Fix casting negative number to usize in `Array.splice` by @lupd in [#2030](https://github.com/boa-dev/boa/pull/2030) +- Fix `Symbol` and `BigInt` constructors by @HalidOdat in [#2032](https://github.com/boa-dev/boa/pull/2032) +- Make `Array.prototype` an array object by @HalidOdat in [#2033](https://github.com/boa-dev/boa/pull/2033) +- Fix early return in `for in loop` head by @raskad in [#2043](https://github.com/boa-dev/boa/pull/2043) + +### Internal Improvements + +- docs: update README by structuring the topics by @ftonato in [#1958](https://github.com/boa-dev/boa/pull/1958) +- Migrate to NPM and cleanup Playground by @jedel1043 in [#1951](https://github.com/boa-dev/boa/pull/1951) +- Fix performance bottleneck in VM by @pdogr in [#1973](https://github.com/boa-dev/boa/pull/1973) +- Remove `git2` and `hex` dependencies by @raskad in [#1992](https://github.com/boa-dev/boa/pull/1992) +- Fix rust 1.60 clippy lints by @raskad in [#2014](https://github.com/boa-dev/boa/pull/2014) +- Refactor `RegExp` constructor methods by @raskad in [#2049](https://github.com/boa-dev/boa/pull/2049) +- Fixing build for changes in clippy for Rust 1.61 by @Razican in [#2082](https://github.com/boa-dev/boa/pull/2082) + +**Full Changelog**: https://github.com/boa-dev/boa/compare/v0.14...v0.15 + +# [0.14.0 (2022-03-15) - Virtual Machine](https://github.com/boa-dev/boa/compare/v0.13...v0.14) + +### Feature Enhancements + +- Implement functions for vm by @HalidOdat in [#1433](https://github.com/boa-dev/boa/pull/1433) +- Implement Object.getOwnPropertyNames and Object.getOwnPropertySymbols by @kevinputera in [#1606](https://github.com/boa-dev/boa/pull/1606) +- Implement `Symbol.prototype.valueOf` by @hle0 in [#1618](https://github.com/boa-dev/boa/pull/1618) +- Implement Array.prototype.at() by @nekevss in [#1613](https://github.com/boa-dev/boa/pull/1613) +- Implement Array.from by @nrabulinski [#1831](https://github.com/boa-dev/boa/pull/1831) +- Implement String.fromCharCode by @hle0 in [#1619](https://github.com/boa-dev/boa/pull/1619) +- Implement `Typed Array` built-in by @Razican in [#1552](https://github.com/boa-dev/boa/pull/1552) +- Implement arguments exotic objects by @jedel1043 in [#1522](https://github.com/boa-dev/boa/pull/1522) +- Allow `BindingPattern`s as `CatchParameter` by @lowr in [#1628](https://github.com/boa-dev/boa/pull/1628) +- Implement `Symbol.prototype[ @@toPrimitive ]` by @Nimpruda in [#1634](https://github.com/boa-dev/boa/pull/1634) +- Implement Generator parsing by @raskad in [#1575](https://github.com/boa-dev/boa/pull/1575) +- Implement Object.hasOwn and improve Object.prototype.hasOwnProperty by @kevinputera in [#1639](https://github.com/boa-dev/boa/pull/1639) +- Hashbang lexer support by @nekevss in [#1631](https://github.com/boa-dev/boa/pull/1631) +- Implement `delete` operator in the vm by @raskad in [#1649](https://github.com/boa-dev/boa/pull/1649) +- Implement Object.fromEntries by @kevinputera in [#1660](https://github.com/boa-dev/boa/pull/1660) +- Initial implementation for increment/decrement in VM by @abhishekc-sharma in [#1621](https://github.com/boa-dev/boa/pull/1621) +- Implement `Proxy` object by @raskad in [#1664](https://github.com/boa-dev/boa/pull/1664) +- Implement object literals for vm by @raskad in [#1668](https://github.com/boa-dev/boa/pull/1668) +- Implement Array findLast and findLastIndex by @bsinky in [#1665](https://github.com/boa-dev/boa/pull/1665) +- Implement `DataView` built-in object by @Nimpruda in [#1662](https://github.com/boa-dev/boa/pull/1662) +- Clean-up contribution guidelines, dependencies, Test262, MSRV by @Razican in [#1683](https://github.com/boa-dev/boa/pull/1683) +- Implement Async Generator Parsing by @nekevss in [#1669](https://github.com/boa-dev/boa/pull/1669) +- Implement prototype of `Intl` built-in by @hle0 in [#1622](https://github.com/boa-dev/boa/pull/1622) +- Add limited console.trace implementation by @osman-turan in [#1623](https://github.com/boa-dev/boa/pull/1623) +- Allow `BindingPattern` in function parameters by @am-a-man in [#1666](https://github.com/boa-dev/boa/pull/1666) +- Small test ux improvements by @orndorffgrant in [#1704](https://github.com/boa-dev/boa/pull/1704) +- Implement missing vm operations by @raskad in [#1697](https://github.com/boa-dev/boa/pull/1697) +- Added fallible allocation to data blocks by @Razican in [#1728](https://github.com/boa-dev/boa/pull/1728) +- Document CodeBlock by @TheDoctor314 in [#1691](https://github.com/boa-dev/boa/pull/1691) +- Generic `JsResult` in `context.throw_` methods by @HalidOdat in [#1734](https://github.com/boa-dev/boa/pull/1734) +- Implement `String.raw( template, ...substitutions )` by @HalidOdat in [#1741](https://github.com/boa-dev/boa/pull/1741) +- Updated test262 suite and dependencies by @Razican in [#1755](https://github.com/boa-dev/boa/pull/1755) +- Lexer string interning by @Razican in [#1758](https://github.com/boa-dev/boa/pull/1758) +- Adjust `compile` and `execute` to avoid clones by @Razican in [#1778](https://github.com/boa-dev/boa/pull/1778) +- Interner support in the parser by @Razican in [#1765](https://github.com/boa-dev/boa/pull/1765) +- Convert `Codeblock` variables to `Sym` by @raskad in [#1798](https://github.com/boa-dev/boa/pull/1798) +- Using production builds for WebAssembly by @Razican in [#1825](https://github.com/boa-dev/boa/pull/1825) +- Give the arrow function its proper name by @rumpl in [#1832](https://github.com/boa-dev/boa/pull/1832) +- Unwrap removal by @Razican in [#1842](https://github.com/boa-dev/boa/pull/1842) +- Feature `JsArray` by @HalidOdat in [#1746](https://github.com/boa-dev/boa/pull/1746) +- Rename "Boa" to boa_engine, moved GC and profiler to their crates by @Razican in [#1844](https://github.com/boa-dev/boa/pull/1844) +- Added conversions from and to serde_json's Value type by @Razican in [#1851](https://github.com/boa-dev/boa/pull/1851) +- Toggleable `JsValue` internals displaying by @HalidOdat in [#1865](https://github.com/boa-dev/boa/pull/1865) +- Implement generator execution by @raskad in [#1790](https://github.com/boa-dev/boa/pull/1790) +- Feature arrays with empty elements by @HalidOdat in [#1870](https://github.com/boa-dev/boa/pull/1870) +- Removed reference counted pointers from `JsValue` variants by @Razican in [#1866](https://github.com/boa-dev/boa/pull/1866) +- Implement `Object.prototype.toLocaleString()` by @HalidOdat in [#1875](https://github.com/boa-dev/boa/pull/1875) +- Implement `AggregateError` by @HalidOdat in [#1888](https://github.com/boa-dev/boa/pull/1888) +- Implement destructing assignments for assignment expressions by @raskad in [#1895](https://github.com/boa-dev/boa/pull/1895) +- Added boa examples by @elasmojs in [#1161](https://github.com/boa-dev/boa/pull/1161) + +### Bug Fixes + +- Fix BigInt and Number comparison by @HalidOdat [#1887](https://github.com/boa-dev/boa/pull/1887) +- Fix broken structure links in the documentation by @abhishekc-sharma in [#1612](https://github.com/boa-dev/boa/pull/1612) +- Use function name from identifiers in assignment expressions by @raskad [#1908](https://github.com/boa-dev/boa/pull/1908) +- Fix integer parsing by @nrabulinski in [#1614](https://github.com/boa-dev/boa/pull/1614) +- Fix `Number.toExponential` and `Number.toFixed` by @nrabulinski in [#1620](https://github.com/boa-dev/boa/pull/1620) +- Badge updates by @atouchet in [#1638](https://github.com/boa-dev/boa/pull/1638) +- refactor: fix construct_error functions by @RageKnify in [#1703](https://github.com/boa-dev/boa/pull/1703) +- Fix internal vm tests by @raskad in [#1718](https://github.com/boa-dev/boa/pull/1718) +- Removed a bunch of warnings and clippy errors by @Razican in [#1754](https://github.com/boa-dev/boa/pull/1754) +- Fix some broken links in the profiler documentation by @Razican in [#1762](https://github.com/boa-dev/boa/pull/1762) +- Add proxy handling in `isArray` method by @raskad in [#1777](https://github.com/boa-dev/boa/pull/1777) +- Copy/paste fix in Proxy error message by @icecream17 in [#1787](https://github.com/boa-dev/boa/pull/1787) +- Fixed #1768 by @Razican in [#1820](https://github.com/boa-dev/boa/pull/1820) +- Fix string.prototype methods and add static string methods by @jevancc in [#1123](https://github.com/boa-dev/boa/pull/1123) +- Handle allocation errors by @y21 in [#1850](https://github.com/boa-dev/boa/pull/1850) +- Fix wasm use outside browsers by @Razican in [#1846](https://github.com/boa-dev/boa/pull/1846) +- Add assertion to check that a break label is identified at compile-time by @VTCAKAVSMoACE in [#1852](https://github.com/boa-dev/boa/pull/1852) +- Correct reference error message by @aaronmunsters in [#1855](https://github.com/boa-dev/boa/pull/1855) +- Fixing main branch workflows by @Razican in [#1858](https://github.com/boa-dev/boa/pull/1858) +- Correct pop_on_return behaviour by @VTCAKAVSMoACE in [#1853](https://github.com/boa-dev/boa/pull/1853) +- Fix equality between objects and `undefined` or `null` by @HalidOdat in [#1872](https://github.com/boa-dev/boa/pull/1872) +- Removing the panic in favour of an error result by @Razican in [#1874](https://github.com/boa-dev/boa/pull/1874) +- Make `Object.getOwnPropertyDescriptors` spec compliant by @HalidOdat in [#1876](https://github.com/boa-dev/boa/pull/1876) +- Make `Error` and `%NativeError%` spec compliant by @HalidOdat in [#1879](https://github.com/boa-dev/boa/pull/1879) +- Fix `Number.prototype.toString` when passing `undefined` as radix by @HalidOdat in [#1877](https://github.com/boa-dev/boa/pull/1877) +- Cleanup vm stack on function return by @raskad in [#1880](https://github.com/boa-dev/boa/pull/1880) +- `%NativeError%.[[prototype]]` should be `Error` constructor by @HalidOdat in [#1883](https://github.com/boa-dev/boa/pull/1883) +- Make `StringToNumber` spec compliant by @HalidOdat in [#1881](https://github.com/boa-dev/boa/pull/1881) +- Fix `PropertyKey` to `JsValue` conversion by @HalidOdat in [#1886](https://github.com/boa-dev/boa/pull/1886) +- Make iterator spec complaint by @HalidOdat in [#1889](https://github.com/boa-dev/boa/pull/1889) +- Implement `Number.parseInt` and `Number.parseFloat` by @HalidOdat in [#1894](https://github.com/boa-dev/boa/pull/1894) +- Fix unreachable panics in compile_access by @VTCAKAVSMoACE in [#1861](https://github.com/boa-dev/boa/pull/1861) +- Continue panic fixes by @VTCAKAVSMoACE in [#1896](https://github.com/boa-dev/boa/pull/1896) +- Deny const declarations without initializer inside for loops by @jedel1043 in [#1903](https://github.com/boa-dev/boa/pull/1903) +- Fix try/catch/finally related bugs and add tests by @jedel1043 in [#1901](https://github.com/boa-dev/boa/pull/1901) +- Compile StatementList after parse passes on negative tests by @raskad in [#1906](https://github.com/boa-dev/boa/pull/1906) +- Prevent breaks without loop or switch from causing panics by @VTCAKAVSMoACE in [#1860](https://github.com/boa-dev/boa/pull/1860) +- Fix postfix increment and decrement return values by @raskad in [#1913](https://github.com/boa-dev/boa/pull/1913) + +### Internal Improvements + +- Rewrite initialization of builtins to use the `BuiltIn` trait by @jedel1043 in [#1586](https://github.com/boa-dev/boa/pull/1586) +- Unify object creation with `empty` and `from_proto_and_data` methods by @jedel1043 in [#1567](https://github.com/boa-dev/boa/pull/1567) +- VM Tidy Up by @jasonwilliams in [#1610](https://github.com/boa-dev/boa/pull/1610) +- Fix master refs to main by @jasonwilliams in [#1637](https://github.com/boa-dev/boa/pull/1637) +- Refresh vm docs and fix bytecode trace output by @raskad [#1921](https://github.com/boa-dev/boa/pull/1921) +- Change type of object prototypes to `Option` by @jedel1043 in [#1640](https://github.com/boa-dev/boa/pull/1640) +- Refactor `Function` internal methods and implement `BoundFunction` objects by @jedel1043 in [#1583](https://github.com/boa-dev/boa/pull/1583) +- change that verbosity comparison to > 2 by @praveenbakkal in [#1680](https://github.com/boa-dev/boa/pull/1680) +- Respect rust 1.56 by @RageKnify in [#1681](https://github.com/boa-dev/boa/pull/1681) +- Add bors to CI by @RageKnify in [#1684](https://github.com/boa-dev/boa/pull/1684) +- Adding VM conformance output to PR checks by @Razican in [#1685](https://github.com/boa-dev/boa/pull/1685) +- Start removing non-VM path by @jasonwilliams in [#1747](https://github.com/boa-dev/boa/pull/1747) +- Using upstream benchmark action by @Razican in [#1753](https://github.com/boa-dev/boa/pull/1753) +- Fix bors hanging by @RageKnify in [#1767](https://github.com/boa-dev/boa/pull/1767) +- add more timers on object functions by @jasonwilliams in [#1775](https://github.com/boa-dev/boa/pull/1775) +- Update the PR benchmarks action by @Razican in [#1774](https://github.com/boa-dev/boa/pull/1774) +- General code clean-up and new lint addition by @Razican in [#1809](https://github.com/boa-dev/boa/pull/1809) +- Reduced the size of AST nodes by @Razican in [#1821](https://github.com/boa-dev/boa/pull/1821) +- Using the new formatting arguments from Rust 1.58 by @Razican in [#1834](https://github.com/boa-dev/boa/pull/1834) +- Rework RegExp struct to include bitflags field by @aaronmunsters in [#1837](https://github.com/boa-dev/boa/pull/1837) +- Ignore wastefull `RegExp` tests by @raskad in [#1840](https://github.com/boa-dev/boa/pull/1840) +- Refactor the environment for runtime performance by @raskad in [#1829](https://github.com/boa-dev/boa/pull/1829) +- Refactor mapped `Arguments` object by @raskad in [#1849](https://github.com/boa-dev/boa/pull/1849) +- Fixed dependabot for submodule by @Razican in [#1856](https://github.com/boa-dev/boa/pull/1856) +- Refactorings for Rust 1.59 by @RageKnify in [#1867](https://github.com/boa-dev/boa/pull/1867) +- Removing internal deprecated functions by @HalidOdat in [#1854](https://github.com/boa-dev/boa/pull/1854) +- Remove `toInteger` and document the `string` builtin by @jedel1043 in [#1884](https://github.com/boa-dev/boa/pull/1884) +- Extract `Intrinsics` struct from `Context` and cleanup names by @jedel1043 in [#1890](https://github.com/boa-dev/boa/pull/1890) + +**Full Changelog**: https://github.com/boa-dev/boa/compare/v0.13...v0.14 + +# [0.13.0 (2021-09-30) - Many new features and refactors](https://github.com/boa-dev/boa/compare/v0.12.0...v0.13.0) + +Feature Enhancements: + +- [FEATURE #1526](https://github.com/boa-dev/boa/pull/1526): Implement ComputedPropertyName for accessor properties in ObjectLiteral (@raskad) +- [FEATURE #1365](https://github.com/boa-dev/boa/pull/1365): Implement splice method (@neeldug) +- [FEATURE #1364](https://github.com/boa-dev/boa/pull/1364): Implement spread for objects (@FrancisMurillo) +- [FEATURE #1525](https://github.com/boa-dev/boa/pull/1525): Implement Object.preventExtensions() and Object.isExtensible() (@HalidOdat) +- [FEATURE #1508](https://github.com/boa-dev/boa/pull/1508): Implement Object.values() (@HalidOdat) +- [FEATURE #1332](https://github.com/boa-dev/boa/pull/1332): Implement Array.prototype.sort (@jedel1043) +- [FEATURE #1417](https://github.com/boa-dev/boa/pull/1471): Implement Object.keys and Object.entries (@skyne98) +- [FEATURE #1406](https://github.com/boa-dev/boa/pull/1406): Implement destructuring assignments (@raskad) +- [FEATURE #1469](https://github.com/boa-dev/boa/pull/1469): Implement String.prototype.replaceAll (@raskad) +- [FEATURE #1442](https://github.com/boa-dev/boa/pull/1442): Implement closure functions (@HalidOdat) +- [FEATURE #1390](https://github.com/boa-dev/boa/pull/1390): Implement RegExp named capture groups (@raskad) +- [FEATURE #1424](https://github.com/boa-dev/boa/pull/1424): Implement Symbol.for and Symbol.keyFor (@HalidOdat) +- [FEATURE #1375](https://github.com/boa-dev/boa/pull/1375): Implement `at` method for string (@neeldug) +- [FEATURE #1369](https://github.com/boa-dev/boa/pull/1369): Implement normalize method (@neeldug) +- [FEATURE #1334](https://github.com/boa-dev/boa/pull/1334): Implement Array.prototype.copyWithin (@jedel1043) +- [FEATURE #1326](https://github.com/boa-dev/boa/pull/1326): Implement get RegExp[@@species] (@raskad) +- [FEATURE #1314](https://github.com/boa-dev/boa/pull/1314): Implement RegExp.prototype [ @@search ] ( string ) (@raskad) +- [FEATURE #1451](https://github.com/boa-dev/boa/pull/1451): Feature prelude module (@HalidOdat) +- [FEATURE #1523](https://github.com/boa-dev/boa/pull/1523): Allow moving NativeObject variables into closures as external captures (@jedel1043) + +Bug Fixes: + +- [BUG #1521](https://github.com/boa-dev/boa/pull/1521): Added "js" feature for getrandom for WebAssembly builds (@Razican) +- [BUG #1528](https://github.com/boa-dev/boa/pull/1528): Always return undefined from functions that do not return (@raskad) +- [BUG #1518](https://github.com/boa-dev/boa/pull/1518): Moving a JsObject inside a closure caused a panic (@jedel1043) +- [BUG #1502](https://github.com/boa-dev/boa/pull/1502): Adjust EnumerableOwnPropertyNames to use all String type property keys (@raskad) +- [BUG #1415](https://github.com/boa-dev/boa/pull/1415): Fix panic on bigint size (@neeldug) +- [BUG #1477](https://github.com/boa-dev/boa/pull/1477): Properly handle NaN in new Date() (@raskad) +- [BUG #1449](https://github.com/boa-dev/boa/pull/1449): Make Array.prototype methods spec compliant (@HalidOdat) +- [BUG #1353](https://github.com/boa-dev/boa/pull/1353): Make Array.prototype.concat spec compliant (@neeldug) +- [BUG #1384](https://github.com/boa-dev/boa/pull/1384): bitwise not operation (spec improvements) (@neeldug) +- [BUG #1374](https://github.com/boa-dev/boa/pull/1374): Match and regexp construct fixes (@neeldug) +- [BUG #1366](https://github.com/boa-dev/boa/pull/1366): Use lock for map iteration (@joshwd36) +- [BUG #1360](https://github.com/boa-dev/boa/pull/1360): Adjust a comment to be next to the correct module (@teymour-aldridge) +- [BUG #1349](https://github.com/boa-dev/boa/pull/1349): Fixes Array.protoype.includes (@neeldug) +- [BUG #1348](https://github.com/boa-dev/boa/pull/1348): Fixes unshift maximum size (@neeldug) +- [BUG #1339](https://github.com/boa-dev/boa/pull/1339): Scripts should not be considered in a block (@macmv) +- [BUG #1312](https://github.com/boa-dev/boa/pull/1312): Fix display for nodes (@macmv) +- [BUG #1347](https://github.com/boa-dev/boa/pull/1347): Fix stringpad abstract operation (@neeldug) +- [BUG #1584](https://github.com/boa-dev/boa/pull/1584): Refactor the Math builtin object (spec compliant) (@jedel1043) +- [BUG #1535](https://github.com/boa-dev/boa/pull/1535): Refactor JSON.parse (@raskad) +- [BUG #1572](https://github.com/boa-dev/boa/pull/1572): Refactor builtin Map intrinsics to follow more closely the spec (@jedel1043) +- [BUG #1445](https://github.com/boa-dev/boa/pull/1445): improve map conformance without losing perf (@neeldug) +- [BUG #1488](https://github.com/boa-dev/boa/pull/1488): Date refactor (@raskad) +- [BUG #1463](https://github.com/boa-dev/boa/pull/1463): Return function execution result from constructor if the function returned (@raskad) +- [BUG #1434](https://github.com/boa-dev/boa/pull/1434): Refactor regexp costructor (@raskad) +- [BUG #1350](https://github.com/boa-dev/boa/pull/1350): Refactor / Implement RegExp functions (@RageKnify) (@raskad) +- [BUG #1331](https://github.com/boa-dev/boa/pull/1331): Implement missing species getters (@raskad) + +Internal Improvements: + +- [INTERNAL #1569](https://github.com/boa-dev/boa/pull/1569): Refactor EnvironmentRecordTrait functions (@raskad) +- [INTERNAL #1464](https://github.com/boa-dev/boa/pull/1464): Optimize integer negation (@HalidOdat) +- [INTERNAL #1550](https://github.com/boa-dev/boa/pull/1550): Add strict mode flag to Context (@raskad) +- [INTERNAL #1561](https://github.com/boa-dev/boa/pull/1561): Implement abstract operation GetPrototypeFromConstructor (@jedel1043) +- [INTERNAL #1309](https://github.com/boa-dev/boa/pull/1309): Implement Display for function objects(@kvnvelasco) +- [INTERNAL #1492](https://github.com/boa-dev/boa/pull/1492): Implement new get_or_undefined method for `JsValue` (@jedel1043) +- [INTERNAL #1553](https://github.com/boa-dev/boa/pull/1553): Fix benchmark action in CI (@jedel1043) +- [INTERNAL #1547](https://github.com/boa-dev/boa/pull/1547): Replace FxHashMap with IndexMap in object properties (@raskad) +- [INTERNAL #1435](https://github.com/boa-dev/boa/pull/1435): Constant JsStrings (@HalidOdat) +- [INTERNAL #1499](https://github.com/boa-dev/boa/pull/1499): Updated the Test262 submodule (@Razican) +- [INTERNAL #1458](https://github.com/boa-dev/boa/pull/1458): Refactor the JS testing system (@bartlomieju) +- [INTERNAL #1485](https://github.com/boa-dev/boa/pull/1485): Implement abstract operation CreateArrayFromList (@jedel1043) +- [INTERNAL #1465](https://github.com/boa-dev/boa/pull/1465): Feature throw Error object (@HalidOdat) +- [INTERNAL #1493](https://github.com/boa-dev/boa/pull/1493): Rename boa::Result to JsResult (@bartlomieju) +- [INTERNAL #1457](https://github.com/boa-dev/boa/pull/1457): Rename Value to JsValue (@HalidOdat) +- [INTERNAL #1460](https://github.com/boa-dev/boa/pull/1460): Change StringGetOwnProperty to produce the same strings that the lexer produces (@raskad) +- [INTERNAL #1425](https://github.com/boa-dev/boa/pull/1425): Extract PropertyMap struct from Object (@jedel1043) +- [INTERNAL #1432](https://github.com/boa-dev/boa/pull/1432): Proposal of new PropertyDescriptor design (@jedel1043) +- [INTERNAL #1383](https://github.com/boa-dev/boa/pull/1383): clippy lints and cleanup of old todos (@neeldug) +- [INTERNAL #1346](https://github.com/boa-dev/boa/pull/1346): Implement gh-page workflow on release (@FrancisMurillo) +- [INTERNAL #1422](https://github.com/boa-dev/boa/pull/1422): Refactor internal methods and make some builtins spec compliant (@HalidOdat) +- [INTERNAL #1419](https://github.com/boa-dev/boa/pull/1419): Fix DataDescriptor Value to possibly be empty (@raskad) +- [INTERNAL #1357](https://github.com/boa-dev/boa/pull/1357): Add Example to Execute a Function of a Script File (@schrieveslaach) +- [INTERNAL #1408](https://github.com/boa-dev/boa/pull/1408): Refactor JavaScript bigint rust type (@HalidOdat) +- [INTERNAL #1380](https://github.com/boa-dev/boa/pull/1380): Custom JavaScript string rust type (@HalidOdat) +- [INTERNAL #1382](https://github.com/boa-dev/boa/pull/1382): Refactor JavaScript symbol rust type (@HalidOdat) +- [INTERNAL #1361](https://github.com/boa-dev/boa/pull/1361): Redesign bytecode virtual machine (@HalidOdat) +- [INTERNAL #1381](https://github.com/boa-dev/boa/pull/1381): Fixed documentation warnings (@Razican) +- [INTERNAL #1352](https://github.com/boa-dev/boa/pull/1352): Respect Rust 1.53 (@RageKnify) +- [INTERNAL #1356](https://github.com/boa-dev/boa/pull/1356): Respect Rust fmt updates (@RageKnify) +- [INTERNAL #1338](https://github.com/boa-dev/boa/pull/1338): Fix cargo check errors (@neeldug) +- [INTERNAL #1329](https://github.com/boa-dev/boa/pull/1329): Allow Value.set_field to throw (@raskad) +- [INTERNAL #1333](https://github.com/boa-dev/boa/pull/1333): adds condition to avoid triggers from dependabot (@neeldug) +- [INTERNAL #1337](https://github.com/boa-dev/boa/pull/1337): Fix github actions (@neeldug) + +# [0.12.0 (2021-06-07) - `Set`, accessors, `@@toStringTag` and no more panics](https://github.com/boa-dev/boa/compare/v0.11.0...v0.12.0) + +Feature Enhancements: + +- [FEATURE #1085](https://github.com/boa-dev/boa/pull/1085): Add primitive promotion for method calls on `GetField` (@RageKnify) +- [FEATURE #1033](https://github.com/boa-dev/boa/pull/1033): Implement `Reflect` built-in object (@tofpie) +- [FEATURE #1151](https://github.com/boa-dev/boa/pull/1151): Fully implement `EmptyStatement` (@SamuelQZQ) +- [FEATURE #1158](https://github.com/boa-dev/boa/pull/1158): Include name in verbose results output of `boa-tester` (@0x7D2B) +- [FEATURE #1225](https://github.com/boa-dev/boa/pull/1225): Implement `Math[ @@toStringTag ]` (@HalidOdat) +- [FEATURE #1224](https://github.com/boa-dev/boa/pull/1224): Implement `JSON[ @@toStringTag ]` (@HalidOdat) +- [FEATURE #1222](https://github.com/boa-dev/boa/pull/1222): Implement `Symbol.prototype.description` accessor (@HalidOdat) +- [FEATURE #1221](https://github.com/boa-dev/boa/pull/1221): Implement `RegExp` flag accessors (@HalidOdat) +- [FEATURE #1240](https://github.com/boa-dev/boa/pull/1240): Stop ignoring a bunch of tests (@Razican) +- [FEATURE #1132](https://github.com/boa-dev/boa/pull/1132): Implement `Array.prototype.flat`/`flatMap` (@davimiku) +- [FEATURE #1235](https://github.com/boa-dev/boa/pull/1235): Implement `Object.assign( target, ...sources )` (@HalidOdat) +- [FEATURE #1243](https://github.com/boa-dev/boa/pull/1243): Cross realm symbols (@HalidOdat) +- [FEATURE #1249](https://github.com/boa-dev/boa/pull/1249): Implement `Map.prototype[ @@toStringTag ]` (@wylie39) +- [FEATURE #1111](https://github.com/boa-dev/boa/pull/1111): Implement `Set` builtin object (@RageKnify) +- [FEATURE #1265](https://github.com/boa-dev/boa/pull/1265): Implement `BigInt.prototype[ @@toStringTag ]` (@n14littl) +- [FEATURE #1102](https://github.com/boa-dev/boa/pull/1102): Support Unicode escape in identifier names (@jevancc) +- [FEATURE #1273](https://github.com/boa-dev/boa/pull/1273): Add default parameter support (@0x7D2B) +- [FEATURE #1292](https://github.com/boa-dev/boa/pull/1292): Implement `symbol.prototype[ @@ToStringTag ]` (@moadmmh) +- [FEATURE #1291](https://github.com/boa-dev/boa/pull/1291): Support `GetOwnProperty` for `string` exotic object (@jarkonik) +- [FEATURE #1296](https://github.com/boa-dev/boa/pull/1296): Added the `$262` object to the Test262 test runner (@Razican) +- [FEATURE #1127](https://github.com/boa-dev/boa/pull/1127): Implement `Array.of` (@camc) + +Bug Fixes: + +- [BUG #1071](https://github.com/boa-dev/boa/pull/1071): Fix attribute configurable of the length property of arguments (@tofpie) +- [BUG #1073](https://github.com/boa-dev/boa/pull/1073): Fixed spelling (@vishalsodani) +- [BUG #1072](https://github.com/boa-dev/boa/pull/1072): Fix `get`/`set` as short method name in `object` (@tofpie) +- [BUG #1077](https://github.com/boa-dev/boa/pull/1077): Fix panics from multiple borrows of `Map` (@joshwd36) +- [BUG #1079](https://github.com/boa-dev/boa/pull/1079): Fix lexing escapes in string literal (@jevancc) +- [BUG #1075](https://github.com/boa-dev/boa/pull/1075): Fix out-of-range panics of `Date` (@jevancc) +- [BUG #1084](https://github.com/boa-dev/boa/pull/1084): Fix line terminator in string literal (@jevancc) +- [BUG #1110](https://github.com/boa-dev/boa/pull/1110): Fix parsing floats panics and bugs (@jevancc) +- [BUG #1202](https://github.com/boa-dev/boa/pull/1202): Fix a typo in `gc.rs` (@teymour-aldridge) +- [BUG #1201](https://github.com/boa-dev/boa/pull/1201): Return optional value in `to_json` functions (@fermian) +- [BUG #1223](https://github.com/boa-dev/boa/pull/1223): Update cli name in Readme (@sphinxc0re) +- [BUG #1175](https://github.com/boa-dev/boa/pull/1175): Handle early errors for declarations in `StatementList` (@0x7D2B) +- [BUG #1270](https://github.com/boa-dev/boa/pull/1270): Fix `Context::register_global_function()` (@HalidOdat) +- [BUG #1135](https://github.com/boa-dev/boa/pull/1135): Fix of instructions.rs comment, to_precision impl and rfc changes (@NathanRoyer) +- [BUG #1272](https://github.com/boa-dev/boa/pull/1272): Fix `Array.prototype.filter` (@tofpie & @Razican) +- [BUG #1280](https://github.com/boa-dev/boa/pull/1280): Fix slice index panic in `add_rest_param` (@0x7D2B) +- [BUG #1284](https://github.com/boa-dev/boa/pull/1284): Fix `GcObject` `to_json` mutable borrow panic (@0x7D2B) +- [BUG #1283](https://github.com/boa-dev/boa/pull/1283): Fix panic in regex execution (@0x7D2B) +- [BUG #1286](https://github.com/boa-dev/boa/pull/1286): Fix construct usage (@0x7D2B) +- [BUG #1288](https://github.com/boa-dev/boa/pull/1288): Fixed `Math.hypot.length` bug (@moadmmh) +- [BUG #1285](https://github.com/boa-dev/boa/pull/1285): Fix environment record panics (@0x7D2B) +- [BUG #1302](https://github.com/boa-dev/boa/pull/1302): Fix VM branch (@jasonwilliams) + +Internal Improvements: + +- [INTERNAL #1067](https://github.com/boa-dev/boa/pull/1067): Change `Realm::global_object` field from `Value` to `GcObject` (@RageKnify) +- [INTERNAL #1048](https://github.com/boa-dev/boa/pull/1048): VM Trace output fixes (@jasonwilliams) +- [INTERNAL #1109](https://github.com/boa-dev/boa/pull/1109): Define all property methods of constructors (@RageKnify) +- [INTERNAL #1126](https://github.com/boa-dev/boa/pull/1126): Remove unnecessary wraps for non built-in functions (@RageKnify) +- [INTERNAL #1044](https://github.com/boa-dev/boa/pull/1044): Removed duplicated code in `vm.run` using macros (@stephanemagnenat) +- [INTERNAL #1103](https://github.com/boa-dev/boa/pull/1103): Lazy evaluation for cooked template string (@jevancc) +- [INTERNAL #1156](https://github.com/boa-dev/boa/pull/1156): Rework environment records (@0x7D2B) +- [INTERNAL #1181](https://github.com/boa-dev/boa/pull/1181): Merge `Const`/`Let`/`Var` `DeclList` into `DeclarationList` (@0x7D2B) +- [INTERNAL #1234](https://github.com/boa-dev/boa/pull/1234): Separate `Symbol` builtin (@HalidOdat) +- [INTERNAL #1131](https://github.com/boa-dev/boa/pull/1131): Make environment methods take `&mut Context` (@HalidOdat) +- [INTERNAL #1271](https://github.com/boa-dev/boa/pull/1271): Make `same_value` and `same_value_zero` static methods (@HalidOdat) +- [INTERNAL #1276](https://github.com/boa-dev/boa/pull/1276): Cleanup (@Razican) +- [INTERNAL #1279](https://github.com/boa-dev/boa/pull/1279): Add test comparison to Test262 result compare (@Razican) +- [INTERNAL #1293](https://github.com/boa-dev/boa/pull/1293): Fix test262 comment formatting (@0x7D2B) +- [INTERNAL #1294](https://github.com/boa-dev/boa/pull/1294): Don't consider panic fixes as "new failures" (@Razican) + +# [0.11.0 (2021-01-14) - Faster Parsing & Better compliance](https://github.com/boa-dev/boa/compare/v0.10.0...v0.11.0) + +Feature Enhancements: + +- [FEATURE #836](https://github.com/boa-dev/boa/pull/836): + Async/Await parse (@Lan2u) +- [FEATURE #704](https://github.com/boa-dev/boa/pull/704): + Implement for...of loops (@joshwd36) +- [FEATURE #770](https://github.com/boa-dev/boa/pull/770): + Support for symbols as property keys for `Object.defineProperty` (@georgeroman) +- [FEATURE #717](https://github.com/boa-dev/boa/pull/717): + Strict Mode Lex/Parse (@Lan2u) +- [FEATURE #800](https://github.com/boa-dev/boa/pull/800): + Implement `console` crate feature - Put `console` object behind a feature flag (@HalidOdat) +- [FEATURE #804](https://github.com/boa-dev/boa/pull/804): + Implement `EvalError` (@HalidOdat) +- [FEATURE #805](https://github.com/boa-dev/boa/pull/805): + Implement `Function.prototype.call` (@RageKnify) +- [FEATURE #806](https://github.com/boa-dev/boa/pull/806): + Implement `URIError` (@HalidOdat) +- [FEATURE #811](https://github.com/boa-dev/boa/pull/811): + Implement spread operator using iterator (@croraf) +- [FEATURE #844](https://github.com/boa-dev/boa/pull/844): + Allow UnaryExpression with prefix increment/decrement (@croraf) +- [FEATURE #798](https://github.com/boa-dev/boa/pull/798): + Implement Object.getOwnPropertyDescriptor() and Object.getOwnPropertyDescriptors() (@JohnDoneth) +- [FEATURE #847](https://github.com/boa-dev/boa/pull/847): + Implement Map.prototype.entries() (@croraf) +- [FEATURE #859](https://github.com/boa-dev/boa/pull/859): + Implement spec compliant Array constructor (@georgeroman) +- [FEATURE #874](https://github.com/boa-dev/boa/pull/874): + Implement Map.prototype.values and Map.prototype.keys (@croraf) +- [FEATURE #877](https://github.com/boa-dev/boa/pull/877): + Implement Function.prototype.apply (@georgeroman) +- [FEATURE #908](https://github.com/boa-dev/boa/pull/908): + Implementation of `instanceof` operator (@morrien) +- [FEATURE #935](https://github.com/boa-dev/boa/pull/935): + Implement String.prototype.codePointAt (@devinus) +- [FEATURE #961](https://github.com/boa-dev/boa/pull/961): + Implement the optional `space` parameter in `JSON.stringify` (@tofpie) +- [FEATURE #962](https://github.com/boa-dev/boa/pull/962): + Implement Number.prototype.toPrecision (@NathanRoyer) +- [FEATURE #983](https://github.com/boa-dev/boa/pull/983): + Implement Object.prototype.isPrototypeOf (@tofpie) +- [FEATURE #995](https://github.com/boa-dev/boa/pull/995): + Support Numeric separators (@tofpie) +- [FEATURE #1013](https://github.com/boa-dev/boa/pull/1013): + Implement nullish coalescing (?? and ??=) (@tofpie) +- [FEATURE #987](https://github.com/boa-dev/boa/pull/987): + Implement property accessors (@tofpie) +- [FEATURE #1018](https://github.com/boa-dev/boa/pull/1018): + Implement logical assignment operators (&&= and ||=) (@tofpie) +- [FEATURE #1019](https://github.com/boa-dev/boa/pull/1019): + Implement early errors for non-assignable nodes in assignment (@tofpie) +- [FEATURE #1020](https://github.com/boa-dev/boa/pull/1020): + Implement Symbol.toPrimitive (@tofpie) +- [FEATURE #976](https://github.com/boa-dev/boa/pull/976): + Implement for..in (@tofpie) +- [FEATURE #1026](https://github.com/boa-dev/boa/pull/1026): + Implement String.prototype.split (@jevancc) +- [FEATURE #1047](https://github.com/boa-dev/boa/pull/1047): + Added syntax highlighting for numbers, identifiers and template literals (@Razican) +- [FEATURE #1003](https://github.com/boa-dev/boa/pull/1003): + Improve Unicode support for identifier names (@jevancc) + +Bug Fixes: + +- [BUG #782](https://github.com/boa-dev/boa/pull/782): + Throw TypeError if regexp is passed to startsWith, endsWith, includes (@pt2121) +- [BUG #788](https://github.com/boa-dev/boa/pull/788): + Fixing a duplicated attribute in test262 results (@Razican) +- [BUG #790](https://github.com/boa-dev/boa/pull/790): + Throw RangeError when BigInt division by zero occurs (@JohnDoneth) +- [BUG #785](https://github.com/boa-dev/boa/pull/785): + Fix zero argument panic in JSON.parse() (@JohnDoneth) +- [BUG #749](https://github.com/boa-dev/boa/pull/749): + Fix Error constructors to return rather than throw (@RageKnify) +- [BUG #777](https://github.com/boa-dev/boa/pull/777): + Fix cyclic JSON.stringify / primitive conversion stack overflows (@vgel) +- [BUG #799](https://github.com/boa-dev/boa/pull/799): + Fix lexer span panic with carriage return (@vgel) +- [BUG #812](https://github.com/boa-dev/boa/pull/812): + Fix 2 bugs that caused Test262 to fail (@RageKnify) +- [BUG #826](https://github.com/boa-dev/boa/pull/826): + Fix tokenizing Unicode escape sequence in string literal (@HalidOdat) +- [BUG #825](https://github.com/boa-dev/boa/pull/825): + calling "new" on a primitive value throw a type error (@dlemel8) +- [BUG #853](https://github.com/boa-dev/boa/pull/853) + Handle invalid Unicode code point in the string literals (@jevancc) +- [BUG #870](https://github.com/boa-dev/boa/pull/870) + Fix JSON stringification for fractional numbers (@georgeroman) +- [BUG #807](https://github.com/boa-dev/boa/pull/807): + Make boa::parse emit error on invalid input, not panic (@georgeroman) +- [BUG #880](https://github.com/boa-dev/boa/pull/880): + Support more number literals in BigInt's from string constructor (@georgeroman) +- [BUG #885](https://github.com/boa-dev/boa/pull/885): + Fix `BigInt.prototype.toString()` radix checks (@georgeroman) +- [BUG #882](https://github.com/boa-dev/boa/pull/882): + Fix (panic) remainder by zero (@georgeroman) +- [BUG #884](https://github.com/boa-dev/boa/pull/884): + Fix some panics related to BigInt operations (@georgeroman) +- [BUG #888](https://github.com/boa-dev/boa/pull/888): + Fix some panics in String.prototype properties (@georgeroman) +- [BUG #902](https://github.com/boa-dev/boa/pull/902): + Fix Accessors panics (@HalidOdat) +- [BUG #959](https://github.com/boa-dev/boa/pull/959): + Fix Unicode character escape sequence parsing (@tofpie) +- [BUG #964](https://github.com/boa-dev/boa/pull/964): + Fix single line comment lexing with CRLF line ending (@tofpie) +- [BUG #919](https://github.com/boa-dev/boa/pull/919): + Reduce the number of `Array`-related panics (@jakubfijalkowski) +- [BUG #968](https://github.com/boa-dev/boa/pull/968): + Fix unit tests that can be failed due to daylight saving time (@tofpie) +- [BUG #972](https://github.com/boa-dev/boa/pull/972): + Fix enumerable attribute on array length property (@tofpie) +- [BUG #974](https://github.com/boa-dev/boa/pull/974): + Fix enumerable attribute on string length property (@tofpie) +- [BUG #981](https://github.com/boa-dev/boa/pull/981): + Fix prototypes for Number, String and Boolean (@tofpie) +- [BUG #999](https://github.com/boa-dev/boa/pull/999): + Fix logical expressions evaluation (@tofpie) +- [BUG #1001](https://github.com/boa-dev/boa/pull/1001): + Fix comparison with infinity (@tofpie) +- [BUG #1004](https://github.com/boa-dev/boa/pull/1004): + Fix panics surrounding `Object.prototype.hasOwnProperty()` (@HalidOdat) +- [BUG #1005](https://github.com/boa-dev/boa/pull/1005): + Fix panics surrounding `Object.defineProperty()` (@HalidOdat) +- [BUG #1021](https://github.com/boa-dev/boa/pull/1021): + Fix spread in new and call expressions (@tofpie) +- [BUG #1023](https://github.com/boa-dev/boa/pull/1023): + Fix attributes on properties of functions and constructors (@tofpie) +- [BUG #1017](https://github.com/boa-dev/boa/pull/1017): + Don't panic when function parameters share names (@AnnikaCodes) +- [BUG #1024](https://github.com/boa-dev/boa/pull/1024): + Fix delete when the property is not configurable (@tofpie) +- [BUG #1027](https://github.com/boa-dev/boa/pull/1027): + Supress regress errors on invalid escapes for regex (@jasonwilliams +- [BUG #1031](https://github.com/boa-dev/boa/pull/1031): + Fixed some extra regex panics (@Razican) +- [BUG #1049](https://github.com/boa-dev/boa/pull/1049): + Support overriding the `arguments` variable (@AnnikaCodes) +- [BUG #1050](https://github.com/boa-dev/boa/pull/1050): + Remove panic on named capture groups (@Razican) +- [BUG #1046](https://github.com/boa-dev/boa/pull/1046): + Remove a few different panics (@Razican) +- [BUG #1051](https://github.com/boa-dev/boa/pull/1051): + Fix parsing of arrow functions with 1 argument (@Lan2u) +- [BUG #1045](https://github.com/boa-dev/boa/pull/1045): + Add newTarget to construct (@tofpie) +- [BUG #659](https://github.com/boa-dev/boa/pull/659): + Error handling in environment (@54k1) + +Internal Improvements: + +- [INTERNAL #735](https://github.com/boa-dev/boa/pull/735): + Move exec implementations together with AST node structs (@georgeroman) +- [INTERNAL #724](https://github.com/boa-dev/boa/pull/724): + Ignore tests for code coverage count (@HalidOdat) +- [INTERNAL #768](https://github.com/boa-dev/boa/pull/768) + Update the benchmark Github action (@Razican) +- [INTERNAL #722](https://github.com/boa-dev/boa/pull/722): + `ConstructorBuilder`, `ObjectInitializer`, cache standard objects and fix global object attributes (@HalidOdat) +- [INTERNAL #783](https://github.com/boa-dev/boa/pull/783): + New test262 results format (This also reduces the payload size for the website) (@Razican) +- [INTERNAL #787](https://github.com/boa-dev/boa/pull/787): + Refactor ast/node/expression into ast/node/call and ast/node/new (@croraf) +- [INTERNAL #802](https://github.com/boa-dev/boa/pull/802): + Make `Function.prototype` a function (@HalidOdat) +- [INTERNAL #746](https://github.com/boa-dev/boa/pull/746): + Add Object.defineProperties and handle props argument in Object.create (@dvtkrlbs) +- [INTERNAL #774](https://github.com/boa-dev/boa/pull/774): + Switch from `regex` to `regress` for ECMA spec-compliant regex implementation (@neeldug) +- [INTERNAL #794](https://github.com/boa-dev/boa/pull/794): + Refactor `PropertyDescriptor` (Improved performance) (@HalidOdat) +- [INTERNAL #824](https://github.com/boa-dev/boa/pull/824): + [parser Expression] minor expression macro simplification (@croraf) +- [INTERNAL #833](https://github.com/boa-dev/boa/pull/833): + Using unstable sort for sorting keys on `to_json()` for GC objects (@Razican) +- [INTERNAL #837](https://github.com/boa-dev/boa/pull/837): + Set default-run to `boa` removing need for `--bin` (@RageKnify) +- [INTERNAL #841](https://github.com/boa-dev/boa/pull/841): + Minor refactor and rename in eval() method (@croraf) +- [INTERNAL #840](https://github.com/boa-dev/boa/pull/840): + fix(profiler): update profiler to match current measureme api (@neeldug) +- [INTERNAL #838](https://github.com/boa-dev/boa/pull/838): + style(boa): minor cleanup (@neeldug) +- [INTERNAL #869](https://github.com/boa-dev/boa/pull/869): + Updated cache in workflows (@Razican) +- [INTERNAL #873](https://github.com/boa-dev/boa/pull/873) + Removed cache from MacOS builds (@Razican) +- [INTERNAL #835](https://github.com/boa-dev/boa/pull/835): + Move `Object` internal object methods to `GcObject` (@HalidOdat) +- [INTERNAL #886](https://github.com/boa-dev/boa/pull/886): + Support running a specific test/suite in boa_tester (@georgeroman) +- [INTERNAL #901](https://github.com/boa-dev/boa/pull/901): + Added "unimplemented" syntax errors (@Razican) +- [INTERNAL #911](https://github.com/boa-dev/boa/pull/911): + Change Symbol hash to `u64` (@HalidOdat) +- [INTERNAL #912](https://github.com/boa-dev/boa/pull/912): + Feature `Context::register_global_property()` (@HalidOdat) +- [INTERNAL #913](https://github.com/boa-dev/boa/pull/913): + Added check to ignore semicolon in parser (@AngelOnFira) +- [INTERNAL #915](https://github.com/boa-dev/boa/pull/915): + Improve lexer by make cursor iterate over bytes (@jevancc) +- [INTERNAL #952](https://github.com/boa-dev/boa/pull/952): + Upgraded rustyline and test262 (@Razican) +- [INTERNAL #960](https://github.com/boa-dev/boa/pull/960): + Fix unresolved links in documentation (@tofpie) +- [INTERNAL #979](https://github.com/boa-dev/boa/pull/979): + Read file input in bytes instead of string (@tofpie) +- [INTERNAL #1014](https://github.com/boa-dev/boa/pull/1014): + StatementList: Rename `statements` to `items` (@AnnikaCodes) +- [INTERNAL #860](https://github.com/boa-dev/boa/pull/860): + Investigation into ByteCode Interpreter (@jasonwilliams) +- [INTERNAL #1042](https://github.com/boa-dev/boa/pull/1042): + Add receiver parameter to object internal methods (@tofpie) +- [INTERNAL #1030](https://github.com/boa-dev/boa/pull/1030): + VM: Implement variable declaration (var, const, and let) (@AnnikaCodes) +- [INTERNAL #1010](https://github.com/boa-dev/boa/pull/1010): + Modify environment binding behaviour of function (@54k1) + +# [0.10.0 (2020-09-29) - New Lexer & Test 262 Harness](https://github.com/boa-dev/boa/compare/v0.9.0...v0.10.0) + +Feature Enhancements: + +- [FEATURE #524](https://github.com/boa-dev/boa/pull/525): + Implement remaining `Math` methods (@mr-rodgers) +- [FEATURE #562](https://github.com/boa-dev/boa/pull/562): + Implement remaining `Number` methods (@joshwd36) +- [FEATURE #536](https://github.com/boa-dev/boa/pull/536): + Implement `SyntaxError` (@HalidOdat) +- [FEATURE #543](https://github.com/boa-dev/boa/pull/543): + Implements `Object.create` builtin method (@croraf) +- [FEATURE #492](https://github.com/boa-dev/boa/pull/492): + Switch to [rustyline](https://github.com/kkawakam/rustyline) for the CLI (@IovoslavIovchev & @Razican) +- [FEATURE #595](https://github.com/boa-dev/boa/pull/595): + Added syntax highlighting for strings in REPL (@HalidOdat) +- [FEATURE #586](https://github.com/boa-dev/boa/pull/586): + Better error formatting and cli color (@HalidOdat) +- [FEATURE #590](https://github.com/boa-dev/boa/pull/590): + Added keyword and operator colors and matching bracket validator to REPL (@HalidOdat) +- [FEATURE #555](https://github.com/boa-dev/boa/pull/555): + Implement Array.prototype.reduce (@benjaminflin) +- [FEATURE #550](https://github.com/boa-dev/boa/pull/550): + Initial implementation of Map() (@joshwd36 & @HalidOdat) +- [FEATURE #579](https://github.com/boa-dev/boa/pull/579): + Implement Array.prototype.reduceRight (@benjaminflin) +- [FEATURE #585](https://github.com/boa-dev/boa/pull/587): + Implement Well-Known Symbols (@joshwd36) +- [FEATURE #589](https://github.com/boa-dev/boa/pull/589): + Implement the comma operator (@KashParty) +- [FEATURE #341](https://github.com/boa-dev/boa/pull/590): + Ability to create multiline blocks in boa shell (@HalidOdat) +- [FEATURE #252](https://github.com/boa-dev/boa/pull/596): + Implement `Date` (@jcdickinson) +- [FEATURE #711](https://github.com/boa-dev/boa/pull/711): + Add support for >>>= (@arpit-saxena) +- [FEATURE #549](https://github.com/boa-dev/boa/pull/549): + Implement label statements (@jasonwilliams) +- [FEATURE #373](https://github.com/boa-dev/boa/pull/373): + Introduce PropertyKey for field acces (@RageKnify) +- [FEATURE #627](https://github.com/boa-dev/boa/pull/627): + Feature native class objects (`NativeObject` and `Class` traits) (@HalidOdat) +- [FEATURE #694](https://github.com/boa-dev/boa/pull/694): + Feature `gc` module (@HalidOdat) +- [FEATURE #656](https://github.com/boa-dev/boa/pull/656): + Feature `Context` (@HalidOdat) +- [FEATURE #673](https://github.com/boa-dev/boa/pull/673): + Add `#[track_caller]` to `GcObject` methods that can panic (@HalidOdat) +- [FEATURE #661](https://github.com/boa-dev/boa/pull/661): + Add documentation to `GcObject` methods (@HalidOdat) +- [FEATURE #662](https://github.com/boa-dev/boa/pull/662): + Implement `std::error::Error` for `GcObject` borrow errors (@HalidOdat) +- [FEATURE #660](https://github.com/boa-dev/boa/pull/660): + Make `GcObject::contruct` not take 'this' (@HalidOdat) +- [FEATURE #654](https://github.com/boa-dev/boa/pull/654): + Move `require_object_coercible` to `Value` (@HalidOdat) +- [FEATURE #603](https://github.com/boa-dev/boa/pull/603): + Index `PropertyKey`, `Object` iterators and symbol support (@HalidOdat) +- [FEATURE #637](https://github.com/boa-dev/boa/pull/637): + Feature `boa::Result` (@HalidOdat) +- [FEATURE #625](https://github.com/boa-dev/boa/pull/625): + Moved value operations from `Interpreter` to `Value` (@HalidOdat) +- [FEATURE #638](https://github.com/boa-dev/boa/pull/638): + Changed to `Value::to_*int32` => `Value::to_*32` (@HalidOdat) + +Bug Fixes: + +- [BUG #405](https://github.com/boa-dev/boa/issues/405): + Fix json.stringify symbol handling (@n14little) +- [BUG #520](https://github.com/boa-dev/boa/pull/520): + Fix all `Value` operations and add unsigned shift right (@HalidOdat) +- [BUG #529](https://github.com/boa-dev/boa/pull/529): + Refactor exec/expression into exec/call and exec/new (@croraf) +- [BUG #510](https://github.com/boa-dev/boa/issues/510): + [[Call]] calling an undefined method does not throw (@joshwd36) +- [BUG #493](https://github.com/boa-dev/boa/pull/493): + Use correct exponential representation for rational values (@Tropid) +- [BUG #572](https://github.com/boa-dev/boa/pull/572): + Spec Compliant `Number.prototype.toString()`, better `Number` object formating and `-0` (@HalidOdat) +- [BUG #599](https://github.com/boa-dev/boa/pull/599): + Fixed `String.prototype.indexOf()` bug, when the search string is empty (@HalidOdat) +- [BUG #615](https://github.com/boa-dev/boa/issues/615): + Fix abstract relational comparison operators (@HalidOdat) +- [BUG #608](https://github.com/boa-dev/boa/issues/608): + `Debug::fmt` Causes Causes a Stack Overflow (@jcdickinson) +- [BUG #532](https://github.com/boa-dev/boa/issues/532) + [builtins - Object] Object.getPrototypeOf returning incorrectly (@54k1) +- [BUG #533](https://github.com/boa-dev/boa/issues/533) + [exec - function] function.prototype doesn't have own constructor property pointing to this function (@54k1) +- [BUG #641](https://github.com/boa-dev/boa/issues/641) + Test new_instance_should_point_to_prototype is not checked correctly (@54k1) +- [BUG #644](https://github.com/boa-dev/boa/pull/645) + `undefined` constants panic on execution (@jcdickinson) +- [BUG #631](https://github.com/boa-dev/boa/pull/645): + Unexpected result when applying typeof to undefined value (@jcdickinson) +- [BUG #667](https://github.com/boa-dev/boa/pull/667): + Fix panic when calling function that mutates itself (@dvtkrlbs) +- [BUG #668](https://github.com/boa-dev/boa/pull/668): + Fix clippy on Nightly (@dvtkrlbs) +- [BUG #582](https://github.com/boa-dev/boa/pull/582): + Make `String.prototype.repeat()` ECMAScript specification compliant (@HalidOdat) +- [BUG #541](https://github.com/boa-dev/boa/pull/541): + Made all `Math` methods spec compliant (@HalidOdat) +- [BUG #597](https://github.com/boa-dev/boa/pull/597): + Made `String.prototype.indexOf` spec compliant. (@HalidOdat) +- [BUG #598](https://github.com/boa-dev/boa/pull/598): + Made `String.prototype.lastIndexOf()` spec compliant (@HalidOdat) +- [BUG #583](https://github.com/boa-dev/boa/pull/583): + Fix string prototype `trim` methods (@HalidOdat) +- [BUG #728](https://github.com/boa-dev/boa/pull/728): + Fix bug when setting the length on String objects (@jasonwilliams) +- [BUG #710](https://github.com/boa-dev/boa/pull/710): + Fix panic when a self mutating function is constructing an object (@HalidOdat) +- [BUG #699](https://github.com/boa-dev/boa/pull/699): + Fix `Value::to_json` order of items in array (@sele9) +- [BUG #610](https://github.com/boa-dev/boa/pull/610): + Fix: `String.prototype.replace` substitutions (@RageKnify) +- [BUG #645](https://github.com/boa-dev/boa/pull/645): + Fix undefined constant expression evaluation (@jcdickinson) +- [BUG #643](https://github.com/boa-dev/boa/pull/643): + Change default return type from null to undefined (@54k1) +- [BUG #642](https://github.com/boa-dev/boa/pull/642): + Missing `constructor` field in ordinary functions (@54k1) +- [BUG #604](https://github.com/boa-dev/boa/pull/604): + Missing `__proto__` field in functions instances (@54k1) +- [BUG #561](https://github.com/boa-dev/boa/pull/561): + Throw a `TypeError` when a non-object is called (@joshwd36) +- [BUG #748](https://github.com/boa-dev/boa/pull/748): + Fix parse error throwing a `TypeError`, instead of `SyntaxError` (@iamsaquib8) +- [BUG #737](https://github.com/boa-dev/boa/pull/737): + Make `Object.toString()` spec compliant (@RageKnify) + +Internal Improvements: + +- [INTERNAL #567](https://github.com/boa-dev/boa/pull/567): + Add ECMAScript test suite (test262) (@Razican) +- [INTERNAL #559](https://github.com/boa-dev/boa/pull/559): + New Lexer (@Lan2u @HalidOdat @Razican) +- [INTERNAL #712](https://github.com/boa-dev/boa/pull/712): + Refactor: `Value::to_object` to return `GcObject` (@RageKnify) +- [INTERNAL #544](https://github.com/boa-dev/boa/pull/544): + Removed `console`s dependency of `InternalState` (@HalidOdat) +- [INTERNAL #556](https://github.com/boa-dev/boa/pull/556): + Added benchmark for goal symbol switching (@Razican) +- [INTERNAL #578](https://github.com/boa-dev/boa/pull/580): + Extract `prototype` from internal slots (@HalidOdat) +- [INTERNAL #553](https://github.com/boa-dev/boa/pull/553): + Refactor Property Descriptor flags (@HalidOdat) +- [INTERNAL #592](https://github.com/boa-dev/boa/pull/592): + `RegExp` specialization (@HalidOdat) +- [INTERNAL #626](https://github.com/boa-dev/boa/pull/626): + Refactor `Function` (@HalidOdat @Razican) +- [INTERNAL #564](https://github.com/boa-dev/boa/pull/581): + Add benchmarks for "uglified" JS (@neeldug) +- [INTERNAL #706](https://github.com/boa-dev/boa/pull/706): + Cache well known symbols (@HalidOdat) +- [INTERNAL #723](https://github.com/boa-dev/boa/pull/723): + Add fast path for string concatenation (@RageKnify) +- [INTERNAL #689](https://github.com/boa-dev/boa/pull/689): + Move `object` module to root (@HalidOdat) +- [INTERNAL #684](https://github.com/boa-dev/boa/pull/684): + Move `property` module to root (@HalidOdat) +- [INTERNAL #674](https://github.com/boa-dev/boa/pull/674): + Move `value` module to root (@HalidOdat) +- [INTERNAL #693](https://github.com/boa-dev/boa/pull/693): + Rename `Object::prototype()` and `Object::set_prototype()` (@RageKnify) +- [INTERNAL #665](https://github.com/boa-dev/boa/pull/665): + `approx_eq!` macro for `expm1` tests. (@neeldung) +- [INTERNAL #581](https://github.com/boa-dev/boa/pull/581): + Added CLEAN_JS and MINI_JS benches (@neeldung) +- [INTERNAL #640](https://github.com/boa-dev/boa/pull/640): + Benchmark refactor (@neeldung) +- [INTERNAL #635](https://github.com/boa-dev/boa/pull/635): + Add missing ops to exec module (@jarredholman) +- [INTERNAL #616](https://github.com/boa-dev/boa/pull/616): + Remove `Value::as_num_to_power()` (@HalidOdat) +- [INTERNAL #601](https://github.com/boa-dev/boa/pull/601): + Removed internal_slots from object (@HalidOdat) +- [INTERNAL #560](https://github.com/boa-dev/boa/pull/560): + Added benchmarks for full program execution (@Razican) +- [INTERNAL #547](https://github.com/boa-dev/boa/pull/547): + Merged `create` into `init` for builtins (@HalidOdat) +- [INTERNAL #538](https://github.com/boa-dev/boa/pull/538): + Cleanup and added test for `String.prototype.concat` (@HalidOdat) +- [INTERNAL #739](https://github.com/boa-dev/boa/pull/739): + Add release action (@jasonwilliams) +- [INTERNAL #744](https://github.com/boa-dev/boa/pull/744): + Add MacOS check and test to CI (@neeldug) + +# [# 0.9.0 (2020-07-03) - Move to Organisation, 78% faster execution time](https://github.com/boa-dev/boa/compare/v0.8.0...v0.9.0) + +Feature Enhancements: + +- [FEATURE #414](https://github.com/boa-dev/boa/issues/414): + Implement `Number` object constants (@Lan2u) (@HalidOdat) +- [FEATURE #345](https://github.com/boa-dev/boa/issues/345): + Implement the optional `replacer` parameter in `JSON.stringify( value[, replacer [, space] ] )` (@n14little) +- [FEATURE #480](https://github.com/boa-dev/boa/issues/480): + Implement global `Infinity` property (@AnirudhKonduru) +- [FEATURE #410](https://github.com/boa-dev/boa/pull/410): + Add support for the reviver function to JSON.parse (@abhijeetbhagat) +- [FEATURE #425](https://github.com/boa-dev/boa/pull/425): + Specification compliant `ToString` (`to_string`) (@HalidOdat) +- [FEATURE #442](https://github.com/boa-dev/boa/pull/442): + Added `TypeError` implementation (@HalidOdat) +- [FEATURE #450](https://github.com/boa-dev/boa/pull/450): + Specification compliant `ToBigInt` (`to_bigint`) (@HalidOdat) +- [FEATURE #455](https://github.com/boa-dev/boa/pull/455): + TemplateLiteral Basic lexer implementation (@croraf) +- [FEATURE #447](https://github.com/boa-dev/boa/issues/447): + parseInt, parseFloat implementation (@Lan2u) +- [FEATURE #468](https://github.com/boa-dev/boa/pull/468): + Add BigInt.asIntN() and BigInt.asUintN() functions (@Tropid) +- [FEATURE #428](https://github.com/boa-dev/boa/issues/428): + [Feature Request] - Create benchmark for Array manipulation (@abhijeetbhagat) +- [FEATURE #439](https://github.com/boa-dev/boa/issues/439): + Implement break handling in switch statements (@Lan2u) +- [FEATURE #301](https://github.com/boa-dev/boa/issues/301): + Implementing the switch statement in the new parser (@Lan2u) +- [FEATURE #120](https://github.com/boa-dev/boa/issues/120): + Implement `globalThis` (@zanayr) +- [FEATURE #513](https://github.com/boa-dev/boa/issues/513): + Implement `Object.is()` method (@tylermorten) +- [FEATURE #481](https://github.com/boa-dev/boa/issues/481): + Implement global `undefined` property (@croraf) + +Bug Fixes: + +- [BUG #412](https://github.com/boa-dev/boa/pull/412): + Fixed parsing if statement without else block preceded by a newline (@HalidOdat) +- [BUG #409](https://github.com/boa-dev/boa/pull/409): + Fix function object constructable/callable (@HalidOdat) +- [BUG #403](https://github.com/boa-dev/boa/issues/403) + `Value::to_json()` does not handle `undefined` correctly (@n14little) +- [BUG #443](https://github.com/boa-dev/boa/issues/443): + HasOwnProperty should call GetOwnProperty and not GetProperty (@n14little) +- [BUG #210](https://github.com/boa-dev/boa/issues/210): + builtinfun.length undefined (@Croraf) +- [BUG #466](https://github.com/boa-dev/boa/issues/466): + Change `ToPrimitive()` (`to_primitive()`) hint to be an enum, instead of string (@HalidOdat) +- [BUG #421](https://github.com/boa-dev/boa/issues/421): + `NaN` is lexed as a number, not as an identifier (@croraf) +- [BUG #454](https://github.com/boa-dev/boa/issues/454): + Function declaration returns the function, it should return `undefined` (@croraf) +- [BUG #482](https://github.com/boa-dev/boa/issues/482): + Field access should propagate the exception (`Err(_)`) (@neeldug) +- [BUG #463](https://github.com/boa-dev/boa/issues/463): + Use of undefined variable should throw an error (@croraf) +- [BUG #502](https://github.com/boa-dev/boa/pull/502): + Fixed global objects initialization order (@HalidOdat) +- [BUG #509](https://github.com/boa-dev/boa/issues/509): + JSON.stringify(undefined) panics (@n14little) +- [BUG #514](https://github.com/boa-dev/boa/issues/514): + Clean up `Math` Methods (@n14little) +- [BUG #511](https://github.com/boa-dev/boa/issues/511): + [Call] Usage of "this" in methods is not supported (@jasonwilliams) + +Internal Improvements + +- [INTERNAL #435](https://github.com/boa-dev/boa/issues/435): + Optimize type comparisons (@Lan2u) +- [INTERNAL #296](https://github.com/boa-dev/boa/issues/296): + using measureme for profiling the interpreter (@jasonwilliams) +- [INTERNAL #419](https://github.com/boa-dev/boa/pull/419): + Object specialization (fast paths for many objects) (@HalidOdat) +- [INTERNAL #392](https://github.com/boa-dev/boa/pull/392): + Execution and Node modulization (@Razican) +- [INTERNAL #465](https://github.com/boa-dev/boa/issues/465): + Refactoring Value (decouple `Gc` from `Value`) (@HalidOdat) +- [INTERNAL #416](https://github.com/boa-dev/boa/pull/416) & [INTERNAL #423](https://github.com/boa-dev/boa/commit/c8218dd91ef3181e048e7a2659a4fbf8d53c7174): + Update links to boa-dev (@pedropaulosuzuki) +- [INTERNAL #378](https://github.com/boa-dev/boa/issues/378): + Code Coverage! (@Lan2u) +- [INTERNAL #431](https://github.com/boa-dev/boa/pull/431): + Updates to PR Benchmarks (@Razican) +- [INTERNAL #427 #429 #430](https://github.com/boa-dev/boa/commit/64dbf13afd15f12f958daa87a3d236dc9af1a9aa): + Added new benchmarks (@Razican) + +# [# 0.8.0 (2020-05-23) - BigInt, Modularized Parser, Faster Hashing](https://github.com/boa-dev/boa/compare/v0.7.0...v0.8.0) + +`v0.8.0` brings more language implementations, such as do..while, function objects and also more recent EcmaScript additions, like BigInt. +We have now moved the Web Assembly build into the `wasm` package, plus added a code of conduct for those contributing. + +The parser has been even more modularized in this release making it easier to add new parsing rules. + +Boa has migrated it's object implemention to FXHash which brings much improved results over the built-in Rust hashmaps (at the cost of less DOS Protection). + +Feature Enhancements: + +- [FEATURE #121](https://github.com/boa-dev/boa/issues/121): + `BigInt` Implemented (@HalidOdat) +- [FEATURE #293](https://github.com/boa-dev/boa/pull/293): + Improved documentation of all modules (@HalidOdat) +- [FEATURE #302](https://github.com/boa-dev/boa/issues/302): + Implement do..while loop (@ptasz3k) +- [FEATURE #318](https://github.com/boa-dev/boa/pull/318): + Added continous integration for windows (@HalidOdat) +- [FEATURE #290](https://github.com/boa-dev/boa/pull/290): + Added more build profiles (@Razican) +- [FEATURE #323](https://github.com/boa-dev/boa/pull/323): + Aded more benchmarks (@Razican) +- [FEATURE #326](https://github.com/boa-dev/boa/pull/326): + Rename Boa CLI (@sphinxc0re) +- [FEATURE #312](https://github.com/boa-dev/boa/pull/312): + Added jemallocator for linux targets (@Razican) +- [FEATURE #339](https://github.com/boa-dev/boa/pull/339): + Improved Method parsing (@muskuloes) +- [FEATURE #352](https://github.com/boa-dev/boa/pull/352): + create boa-wasm package (@muskuloes) +- [FEATURE #304](https://github.com/boa-dev/boa/pull/304): + Modularized parser +- [FEATURE #141](https://github.com/boa-dev/boa/issues/141): + Implement function objects (@jasonwilliams) +- [FEATURE #365](https://github.com/boa-dev/boa/issues/365): + Implement for loop execution (@Razican) +- [FEATURE #356](https://github.com/boa-dev/boa/issues/356): + Use Fx Hash to speed up hash maps in the compiler (@Razican) +- [FEATURE #321](https://github.com/boa-dev/boa/issues/321): + Implement unary operator execution (@akryvomaz) +- [FEATURE #379](https://github.com/boa-dev/boa/issues/379): + Automatic auditing of Boa (@n14little) +- [FEATURE #264](https://github.com/boa-dev/boa/issues/264): + Implement `this` (@jasonwilliams) +- [FEATURE #395](https://github.com/boa-dev/boa/pull/395): + impl abstract-equality-comparison (@hello2dj) +- [FEATURE #359](https://github.com/boa-dev/boa/issues/359): + impl typeof (@RestitutorOrbis) +- [FEATURE #390](https://github.com/boa-dev/boa/pull/390): + Modularize try statement parsing (@abhijeetbhagat) + +Bug fixes: + +- [BUG #308](https://github.com/boa-dev/boa/issues/308): + Assignment operator not working in tests (a = a +1) (@ptasz3k) +- [BUG #322](https://github.com/boa-dev/boa/issues/322): + Benchmarks are failing in master (@Razican) +- [BUG #325](https://github.com/boa-dev/boa/pull/325): + Put JSON functions on the object, not the prototype (@coolreader18) +- [BUG #331](https://github.com/boa-dev/boa/issues/331): + We only get `Const::Num`, never `Const::Int` (@HalidOdat) +- [BUG #209](https://github.com/boa-dev/boa/issues/209): + Calling `new Array` with 1 argument doesn't work properly (@HalidOdat) +- [BUG #266](https://github.com/boa-dev/boa/issues/266): + Panic assigning named function to variable (@Razican) +- [BUG #397](https://github.com/boa-dev/boa/pull/397): + fix `NaN` is lexed as identifier, not as a number (@attliaLin) +- [BUG #362](https://github.com/boa-dev/boa/pull/362): + Remove Monaco Editor Webpack Plugin and Manually Vendor Editor Workers (@subhankar-panda) +- [BUG #406](https://github.com/boa-dev/boa/pull/406): + Dependency Upgrade (@Razican) +- [BUG #407](https://github.com/boa-dev/boa/pull/407): + `String()` wasn't defaulting to empty string on call (@jasonwilliams) +- [BUG #404](https://github.com/boa-dev/boa/pull/404): + Fix for 0 length new String(@tylermorten) + +Code Of Conduct: + +- [COC #384](https://github.com/boa-dev/boa/pull/384): + Code of conduct added (@Razican) + +Security: + +- [SEC #391](https://github.com/boa-dev/boa/pull/391): + run security audit daily at midnight. (@n14little) + +# [# 0.7.0 (2020-04-13) - New Parser is 67% faster](https://github.com/boa-dev/boa/compare/v0.6.0...v0.7.0) + +`v0.7.0` brings a REPL, Improved parser messages and a new parser! +This is now the default behaviour of Boa, so running Boa without a file argument will bring you into a javascript shell. +Tests have also been moved to their own files, we had a lot of tests in some modules so it was time to separate. + +## New Parser + +Most of the work in this release has been on rewriting the parser. A big task taken on by [HalidOdat](https://github.com/HalidOdat), [Razican](https://github.com/Razican) and [myself](https://github.com/jasonwilliams). + +The majority of the old parser was 1 big function (called [`parse`](https://github.com/boa-dev/boa/blob/019033eff066e8c6ba9456139690eb214a0bf61d/boa/src/syntax/parser.rs#L353)) which had some pattern matching on each token coming in.\ +The easy branches could generate expressions (which were basically AST Nodes), the more involved branches would recursively call into the same function, until eventually you had an expression generated. + +This only worked so far, eventually debugging parsing problems were difficult, also more bugs were being raised against the parser which couldn't be fixed. + +We decided to break the parser into more of a state-machine. The initial decision for this was inspired by [Fedor Indutny](https://github.com/indutny) who did a talk at (the last) JSConf EU about how he broke up the old node-parser to make it more maintanable. He goes into more detail here https://www.youtube.com/watch?v=x3k_5Mi66sY&feature=youtu.be&t=530 + +The new parser has functions to match the states of parsing in the spec. For example https://tc39.es/ecma262/#prod-VariableDeclaration has a matching function `read_variable_declaration`. This not only makes it better to maintain but easier for new contributors to get involed, as following the parsing logic of the spec is easier than before. + +Once finished some optimisations were added by [HalidOdat](https://github.com/HalidOdat) to use references to the tokens instead of cloning them each time we take them from the lexer.\ +This works because the tokens live just as long as the parser operations do, so we don't need to copy the tokens.\ +What this brings is a huge performance boost, the parser is 67% faster than before! + +![Parser Improvement](./docs/img/parser-graph.png) + +Feature enhancements: + +- [FEATURE #281](https://github.com/boa-dev/boa/pull/281): + Rebuild the parser (@jasonwilliams, @Razican, @HalidOdat) +- [FEATURE #278](https://github.com/boa-dev/boa/pull/278): + Added the ability to dump the token stream or ast in bin. (@HalidOdat) +- [FEATURE #253](https://github.com/boa-dev/boa/pull/253): + Implement Array.isArray (@cisen) +- [FEATURE](https://github.com/boa-dev/boa/commit/edab5ca6cc10d13265f82fa4bc05d6b432a362fc) + Switch to normal output instead of debugged output (stdout/stdout) (@jasonwilliams) +- [FEATURE #258](https://github.com/boa-dev/boa/pull/258): + Moved test modules to their own files (@Razican) +- [FEATURE #267](https://github.com/boa-dev/boa/pull/267): + Add print & REPL functionality to CLI (@JohnDoneth) +- [FEATURE #268](https://github.com/boa-dev/boa/pull/268): + Addition of forEach() (@jasonwilliams) (@xSke) +- [FEATURE #262](https://github.com/boa-dev/boa/pull/262): + Implement Array.prototype.filter (@Nickforall) +- [FEATURE #261](https://github.com/boa-dev/boa/pull/261): + Improved parser error messages (@Razican) +- [FEATURE #277](https://github.com/boa-dev/boa/pull/277): + Add a logo to the project (@HalidOdat) +- [FEATURE #260](https://github.com/boa-dev/boa/pull/260): + Add methods with f64 std equivelant to Math object (@Nickforall) + +Bug fixes: + +- [BUG #249](https://github.com/boa-dev/boa/pull/249): + fix(parser): handle trailing comma in object literals (@gomesalexandre) +- [BUG #244](https://github.com/boa-dev/boa/pull/244): + Fixed more Lexer Panics (@adumbidiot) +- [BUG #256](https://github.com/boa-dev/boa/pull/256): + Fixed comments lexing (@Razican) +- [BUG #251](https://github.com/boa-dev/boa/issues/251): + Fixed empty returns (@Razican) +- [BUG #272](https://github.com/boa-dev/boa/pull/272): + Fix parsing of floats that start with a zero (@Nickforall) +- [BUG #240](https://github.com/boa-dev/boa/issues/240): + Fix parser panic +- [BUG #273](https://github.com/boa-dev/boa/issues/273): + new Class().method() has incorrect precedence + +Documentation Updates: + +- [DOC #297](https://github.com/boa-dev/boa/pull/297): + Better user contributed documentation + +# [# 0.6.0 (2020-02-14) - Migration to Workspace Architecture + lexer/parser improvements](https://github.com/boa-dev/boa/compare/v0.5.1...v0.6.0) + +The lexer has had several fixes in this release, including how it parses numbers, scientific notation should be improved. +On top of that the lexer no longer panics on errors including Syntax Errors (thanks @adumbidiot), instead you get some output on where the error happened. + +## Moving to a workspace architecture + +Boa offers both a CLI and a library, initially these were all in the same binary. The downside is +those who want to embed boa as-is end up with all of the command-line dependencies. +So the time has come to separate out the two, this is normal procedure, this should be analogous to ripgrep +and the regex crate. +Cargo has great support for workspaces, so this shouldn't be an issue. + +## Benchmarks + +We now have [benchmarks which run against master](https://boa-dev.github.io/boa/dev/bench)! +Thanks to Github Actions these will run automatically a commit is merged. + +Feature enhancements: + +- [FEATURE #218](https://github.com/boa-dev/boa/pull/218): + Implement Array.prototype.toString (@cisen) +- [FEATURE #216](https://github.com/boa-dev/boa/commit/85e9a3526105a600358bd53811e2b022987c6fc8): + Keep accepting new array elements after spread. +- [FEATURE #220](https://github.com/boa-dev/boa/pull/220): + Documentation updates. (@croraf) +- [FEATURE #226](https://github.com/boa-dev/boa/pull/226): + add parser benchmark for expressions. (@jasonwilliams) +- [FEATURE #217](https://github.com/boa-dev/boa/pull/217): + String.prototype.replace() implemented +- [FEATURE #247](https://github.com/boa-dev/boa/pull/247): + Moved to a workspace architecture (@Razican) + +Bug fixes: + +- [BUG #222](https://github.com/boa-dev/boa/pull/222): + Fixed clippy errors (@IovoslavIovchev) +- [BUG #228](https://github.com/boa-dev/boa/pull/228): + [lexer: single-line-comment] Fix bug when single line comment is last line of file (@croraf) +- [BUG #229](https://github.com/boa-dev/boa/pull/229): + Replace error throwing with panic in "Lexer::next()" (@croraf) +- [BUG #232/BUG #238](https://github.com/boa-dev/boa/pull/232): + Clippy checking has been scaled right back to just Perf and Style (@jasonwilliams) +- [BUG #227](https://github.com/boa-dev/boa/pull/227): + Array.prototype.toString should be called by ES value (@cisen) +- [BUG #242](https://github.com/boa-dev/boa/pull/242): + Fixed some panics in the lexer (@adumbidiot) +- [BUG #235](https://github.com/boa-dev/boa/pull/235): + Fixed arithmetic operations with no space (@gomesalexandre) +- [BUG #245](https://github.com/boa-dev/boa/pull/245): + Fixed parsing of floats with scientific notation (@adumbidiot) + +# [# 0.5.1 (2019-12-02) - Rest / Spread (almost)](https://github.com/boa-dev/boa/compare/v0.5.0...v0.5.1) + +Feature enhancements: + +- [FEATURE #151](https://github.com/boa-dev/boa/issues/151): + Implement the Rest/Spread operator (functions and arrays). +- [FEATURE #193](https://github.com/boa-dev/boa/issues/193): + Implement macro for setting builtin functions +- [FEATURE #211](https://github.com/boa-dev/boa/pull/211): + Better Display support for all Objects (pretty printing) + +# [# 0.5.0 (2019-11-06) - Hacktoberfest Release](https://github.com/boa-dev/boa/compare/v0.4.0...v0.5.1) + +Feature enhancements: + +- [FEATURE #119](https://github.com/boa-dev/boa/issues/119): + Introduce realm struct to hold realm context and global object. +- [FEATURE #89](https://github.com/boa-dev/boa/issues/89): + Implement exponentiation operator. Thanks @arbroween +- [FEATURE #47](https://github.com/boa-dev/boa/issues/47): + Add tests for comments in source code. Thanks @Emanon42 +- [FEATURE #137](https://github.com/boa-dev/boa/issues/137): + Use Monaco theme for the demo page +- [FEATURE #114](https://github.com/boa-dev/boa/issues/114): + String.match(regExp) is implemented (@muskuloes) +- [FEATURE #115](https://github.com/boa-dev/boa/issues/115): + String.matchAll(regExp) is implemented (@bojan88) +- [FEATURE #163](https://github.com/boa-dev/boa/issues/163): + Implement Array.prototype.every() (@letmutx) +- [FEATURE #165](https://github.com/boa-dev/boa/issues/165): + Implement Array.prototype.find() (@letmutx) +- [FEATURE #166](https://github.com/boa-dev/boa/issues/166): + Implement Array.prototype.findIndex() (@felipe-fg) +- [FEATURE #39](https://github.com/boa-dev/boa/issues/39): + Implement block scoped variable declarations (@barskern) +- [FEATURE #161](https://github.com/boa-dev/boa/pull/161): + Enable obj[key] = value syntax. +- [FEATURE #179](https://github.com/boa-dev/boa/issues/179): + Implement the Tilde operator (@letmutx) +- [FEATURE #189](https://github.com/boa-dev/boa/pull/189): + Implement Array.prototype.includes (incl tests) (@simonbrahan) +- [FEATURE #180](https://github.com/boa-dev/boa/pull/180): + Implement Array.prototype.slice (@muskuloes @letmutx) +- [FEATURE #152](https://github.com/boa-dev/boa/issues/152): + Short Function syntax (no arguments) +- [FEATURE #164](https://github.com/boa-dev/boa/issues/164): + Implement Array.prototype.fill() (@bojan88) +- Array tests: Tests implemented for shift, unshift and reverse, pop and push (@muskuloes) +- Demo page has been improved, new font plus change on input. Thanks @WofWca +- [FEATURE #182](https://github.com/boa-dev/boa/pull/182): + Implement some Number prototype methods (incl tests) (@pop) +- [FEATURE #34](https://github.com/boa-dev/boa/issues/34): + Number object and Constructore are implemented (including methods) (@pop) +- [FEATURE #194](https://github.com/boa-dev/boa/pull/194): + Array.prototype.map (@IovoslavIovchev) +- [FEATURE #90](https://github.com/boa-dev/boa/issues/90): + Symbol Implementation (@jasonwilliams) + +Bug fixes: + +- [BUG #113](https://github.com/boa-dev/boa/issues/113): + Unassigned variables have default of undefined (@pop) +- [BUG #61](https://github.com/boa-dev/boa/issues/61): + Clippy warnings/errors fixed (@korpen) +- [BUG #147](https://github.com/boa-dev/boa/pull/147): + Updated object global +- [BUG #154](https://github.com/boa-dev/boa/issues/154): + Correctly handle all whitespaces within the lexer +- Tidy up Globals being added to Global Object. Thanks @DomParfitt + +# 0.4.0 (2019-09-25) + +v0.4.0 brings quite a big release. The biggest feature to land is the support of regular expressions. +Functions now have the arguments object supported and we have a [`debugging`](docs/debugging.md) section in the docs. + +Feature enhancements: + +- [FEATURE #6](https://github.com/boa-dev/boa/issues/6): + Support for regex literals. (Big thanks @999eagle) +- [FEATURE #13](https://github.com/boa-dev/boa/issues/13): + toLowerCase, toUpperCase, substring, substr and valueOf implemented (thanks @arbroween) +- Support for `arguments` object within functions +- `StringData` instead of `PrimitieData` to match spec +- Native function signatures changed, operations added to match spec +- Primitives can now be boxed/unboxed when methods are ran on them +- Spelling edits (thanks @someguynamedmatt) +- Ability to set global values before interpreter starts (thanks @999eagle) +- Assign operators implemented (thanks @oll3) +- + +Bug fixes: + +- [BUG #57](https://github.com/boa-dev/boa/issues/57): + Fixed issue with stackoverflow by implementing early returns. +- Allow to re-assign value to an existing binding. (Thanks @oll3) + +# 0.3.0 (2019-07-26) + +- UnexpectedKeyword(Else) bug fixed https://github.com/boa-dev/boa/issues/38 +- Contributing guide added +- Ability to specify file - Thanks @callumquick +- Travis fixes +- Parser Tests - Thanks @Razican +- Migrate to dyn traits - Thanks @Atul9 +- Added implementations for Array.prototype: concat(), push(), pop() and join() - Thanks @callumquick +- Some clippy Issues fixed - Thanks @Razican +- Objects have been refactored to use structs which are more closely aligned with the specification +- Benchmarks have been added +- String and Array specific console.log formats - Thanks @callumquick +- isPropertyKey implementation added - Thanks @KrisChambers +- Unit Tests for Array and Strings - Thanks @GalAster +- typo fix - Thanks @palerdot +- dist cleanup, thanks @zgotsch + +# 0.2.1 (2019-06-30) + +Some String prototype methods are implemented. +Thanks to @lennartbuit we have +trim/trimStart/trimEnd added to the string prototype + +Feature enhancements: + +- [String.prototype.concat ( ...args )](https://tc39.es/ecma262/#sec-string.prototype.slice) +- [String.prototype.endsWith ( searchString [ , endPosition ] )](https://tc39.es/ecma262/#sec-string.prototype.endswith) +- [String.prototype.includes ( searchString [ , position ] )](https://tc39.es/ecma262/#sec-string.prototype.includes) +- [String.prototype.indexOf ( searchString [ , position ] )](https://tc39.es/ecma262/#sec-string.prototype.indexof) +- [String.prototype.lastIndexOf ( searchString [ , position ] )](https://tc39.es/ecma262/#sec-string.prototype.lastindexof) +- [String.prototype.repeat ( count )](https://tc39.es/ecma262/#sec-string.prototype.repeat) +- [String.prototype.slice ( start, end )](https://tc39.es/ecma262/#sec-string.prototype.slice) +- [String.prototype.startsWith ( searchString [ , position ] )](https://tc39.es/ecma262/#sec-string.prototype.startswith) + +Bug fixes: + +- Plenty + +# 0.2.0 (2019-06-10) + +Working state reached + +- Tests on the lexer, conforms with puncturators and keywords from TC39 specification +- wasm-bindgen added with working demo in Web Assembly +- snapshot of boa in a working state for the first time diff --git a/__wasm/wit-bindgen-sample/engine/boa/CODE_OF_CONDUCT.md b/__wasm/wit-bindgen-sample/engine/boa/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..8801a2f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or + advances of any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email + address, without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[discord](https://discord.gg/tUFFk9Y) by contacting _JaseW_. +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/__wasm/wit-bindgen-sample/engine/boa/CONTRIBUTING.md b/__wasm/wit-bindgen-sample/engine/boa/CONTRIBUTING.md new file mode 100644 index 0000000..407431c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/CONTRIBUTING.md @@ -0,0 +1,115 @@ +# Contributing to Boa + +Boa welcomes contribution from everyone. Here are the guidelines if you are +thinking of helping out: + +## Contributions + +Contributions to Boa or its dependencies should be made in the form of GitHub +pull requests. Each pull request will be reviewed by a core contributor +(someone with permission to land patches) and either landed in the main tree or +given feedback for changes that would be required. All contributions should +follow this format. + +Should you wish to work on an issue, please claim it first by commenting on +the GitHub issue that you want to work on it. This is to prevent duplicated +efforts from contributors on the same issue. + +Head over to [issues][issues] and check for "good first issue" labels to find +good tasks to start with. If you come across words or jargon that do not make +sense, please ask! + +If you don't already have Rust installed [_rustup_][rustup] is the recommended +tool to use. It will install Rust and allow you to switch between _nightly_, +_stable_ and _beta_. You can also install additional components. In Linux, you +can run: + +```shell +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Then simply clone this project and `cargo build`. + +### Running the compiler + +You can execute a Boa console by running `cargo run`, and you can compile a list +of JavaScript files by running `cargo run -- file1.js file2.js` and so on. + +### Debugging + +Knowing how to debug the interpreter should help you resolve problems quite quickly. +See [Debugging](./docs/debugging.md). + +### Web Assembly + +If you want to develop on the web assembly side you can run `yarn serve` and then go +to . + +### boa-unicode + +Boa uses the library `boa-unicode` to query Unicode character properties and classes in lexer and parser. See [boa_unicode/README.md](./boa_unicode/README.md) for development and more information. + +### Setup + +#### VSCode Plugins + +Either the [Rust (RLS)][rls_vscode] or the [Rust Analyzer][rust-analyzer_vscode] +extensions are preferred. RLS is easier to set up but some of the development is +moving towards Rust Analyzer. Both of these plugins will help you with your Rust +Development + +#### Tasks + +There are some pre-defined tasks in [tasks.json](.vscode/tasks.json) + +- Build - shift+cmd/ctrl+b should build and run cargo. You should be able to make changes and run this task. +- Test - (there is no shortcut, you'll need to make one) - Runs `Cargo Test`. + I personally set a shortcut of shift+cmd+option+T (or shift+ctrl+alt+T) + +If you don't want to install everything on your machine, you can use the Dockerfile. +Start VSCode in container mode (you may need the docker container plugin) and use the Dockerfile. + +## Testing + +Boa provides its own test suite, and can also run the official ECMAScript test suite. To run the Boa test +suite, you can just run the normal `cargo test`, and to run the full ECMAScript test suite, you can run it +with this command: + +```shell +cargo run --release --bin boa_tester -- run -v 2> error.log +``` + +Note that this requires the `test262` submodule to be checked out, so you will need to run the following first: + +```shell +git submodule init && git submodule update +``` + +This will run the test suite in verbose mode (you can remove the `-v` part to run it in non-verbose mode), +and output nice colorings in the terminal. It will also output any panic information into the `error.log` file. + +You can get some more verbose information that tells you the exact name of each test that is being run, useful +for debugging purposes by setting up the verbose flag twice, for example `-vv`. If you want to know the output of +each test that is executed, you can use the triple verbose (`-vvv`) flag. + +If you want to only run one sub-suite or even one test (to just check if you fixed/broke something specific), +you can do it with the `-s` parameter, and then passing the path to the sub-suite or test that you want to run. Note +that the `-s` parameter value should be a path relative to the `test262` directory. For example, to run the number +type tests, use `-s test/language/types/number`. + +Finally, if you're using the verbose flag and running a sub suite with a small number of tests, then the output will +be more readable if you disable parallelism with the `-d` flag. All together it might look something like: + +```shell +cargo run --release --bin boa_tester -- run -vv -d -s test/language/types/number 2> error.log +``` + +## Communication + +We have a Discord server, feel free to ask questions here: + + +[issues]: https://github.com/boa-dev/boa/issues +[rustup]: https://rustup.rs/ +[rls_vscode]: https://marketplace.visualstudio.com/items?itemName=rust-lang.rust +[rust-analyzer_vscode]: https://marketplace.visualstudio.com/items?itemName=matklad.rust-analyzer diff --git a/__wasm/wit-bindgen-sample/engine/boa/Cargo.lock b/__wasm/wit-bindgen-sample/engine/boa/Cargo.lock new file mode 100644 index 0000000..b6abce1 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/Cargo.lock @@ -0,0 +1,1983 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb07d2053ccdbe10e2af2995a2f116c1330396493dc1269f6a91d0ae82e19704" + +[[package]] +name = "arrayvec" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd9fd44efafa8690358b7408d253adf110036b88f55672a933f01d616ad9b1b9" +dependencies = [ + "nodrop", +] + +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi", + "libc", + "winapi", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "boa_cli" +version = "0.15.0" +dependencies = [ + "boa_engine", + "boa_interner", + "clap 3.2.12", + "colored", + "jemallocator", + "phf", + "regex", + "rustyline", + "rustyline-derive", + "serde_json", +] + +[[package]] +name = "boa_engine" +version = "0.15.0" +dependencies = [ + "bitflags", + "boa_gc", + "boa_interner", + "boa_profiler", + "boa_unicode", + "chrono", + "criterion", + "dyn-clone", + "fast-float", + "float-cmp", + "gc", + "icu_datetime", + "icu_locale_canonicalizer", + "icu_locid", + "icu_plurals", + "icu_provider", + "icu_testdata", + "indexmap", + "jemallocator", + "num-bigint", + "num-integer", + "num-traits", + "once_cell", + "rand", + "regress", + "rustc-hash", + "ryu-js", + "serde", + "serde_json", + "sys-locale", + "tap", + "unicode-normalization", +] + +[[package]] +name = "boa_examples" +version = "0.15.0" +dependencies = [ + "boa_engine", + "boa_gc", + "gc", +] + +[[package]] +name = "boa_gc" +version = "0.15.0" +dependencies = [ + "gc", + "measureme", +] + +[[package]] +name = "boa_interner" +version = "0.15.0" +dependencies = [ + "phf", + "rustc-hash", + "serde", + "static_assertions", +] + +[[package]] +name = "boa_profiler" +version = "0.15.0" +dependencies = [ + "measureme", + "once_cell", +] + +[[package]] +name = "boa_tester" +version = "0.15.0" +dependencies = [ + "anyhow", + "bitflags", + "boa_engine", + "boa_gc", + "boa_interner", + "colored", + "fxhash", + "gc", + "num-format", + "once_cell", + "rayon", + "regex", + "serde", + "serde_json", + "serde_yaml", + "structopt", +] + +[[package]] +name = "boa_unicode" +version = "0.15.0" +dependencies = [ + "unicode-general-category", +] + +[[package]] +name = "boa_wasm" +version = "0.15.0" +dependencies = [ + "boa_engine", + "getrandom", + "wasm-bindgen", +] + +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "cast" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" +dependencies = [ + "rustc_version", +] + +[[package]] +name = "cc" +version = "1.0.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +dependencies = [ + "libc", + "num-integer", + "num-traits", + "time", + "winapi", +] + +[[package]] +name = "clap" +version = "2.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" +dependencies = [ + "ansi_term", + "atty", + "bitflags", + "strsim 0.8.0", + "textwrap 0.11.0", + "unicode-width", + "vec_map", +] + +[[package]] +name = "clap" +version = "3.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab8b79fe3946ceb4a0b1c080b4018992b8d27e9ff363644c1c9b6387c854614d" +dependencies = [ + "atty", + "bitflags", + "clap_derive", + "clap_lex", + "indexmap", + "once_cell", + "strsim 0.10.0", + "termcolor", + "textwrap 0.15.0", +] + +[[package]] +name = "clap_derive" +version = "3.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759bf187376e1afa7b85b959e6a664a3e7a95203415dba952ad19139e798f902" +dependencies = [ + "heck 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "clap_lex" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5538cd660450ebeb4234cfecf8f2284b844ffc4c50531e66d584ad5b91293613" +dependencies = [ + "os_str_bytes", +] + +[[package]] +name = "clipboard-win" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db" +dependencies = [ + "error-code", + "str-buf", + "winapi", +] + +[[package]] +name = "colored" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3616f750b84d8f0de8a58bda93e08e2a81ad3f523089b05f1dffecab48c6cbd" +dependencies = [ + "atty", + "lazy_static", + "winapi", +] + +[[package]] +name = "criterion" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" +dependencies = [ + "atty", + "cast", + "clap 2.34.0", + "criterion-plot", + "csv", + "itertools", + "lazy_static", + "num-traits", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_cbor", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" +dependencies = [ + "cast", + "itertools", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" +dependencies = [ + "cfg-if", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07db9d94cbd326813772c968ccd25999e5f8ae22f4f8d1b11effa37ef6ce281d" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "once_cell", + "scopeguard", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "cstr_core" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "644828c273c063ab0d39486ba42a5d1f3a499d35529c759e763a9c6cb8a0fb08" +dependencies = [ + "cty", + "memchr", +] + +[[package]] +name = "csv" +version = "1.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" +dependencies = [ + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", +] + +[[package]] +name = "cty" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b365fabc795046672053e29c954733ec3b05e4be654ab130fe8f1f94d7051f35" + +[[package]] +name = "dirs-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + +[[package]] +name = "dirs-sys-next" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "displaydoc" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bf95dc3f046b9da4f2d51833c0d3547d8564ef6910f5c1ed130306a75b92886" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "dyn-clone" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435217f1b2d06f00513185fb991cd426271d9e4c3a9d9b25b919b8e5a03b282d" + +[[package]] +name = "either" +version = "1.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" + +[[package]] +name = "endian-type" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c34f04666d835ff5d62e058c3995147c06f42fe86ff053337632bca83e42702d" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "error-code" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21" +dependencies = [ + "libc", + "str-buf", +] + +[[package]] +name = "fast-float" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" + +[[package]] +name = "fd-lock" +version = "3.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46e245f4c8ec30c6415c56cb132c07e69e74f1942f6b4a4061da748b49f486ca" +dependencies = [ + "cfg-if", + "rustix", + "windows-sys", +] + +[[package]] +name = "fixed_decimal" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa523feb405bd9fd25daafb500a48112156a8c15860d18451190e8701500e863" +dependencies = [ + "displaydoc", + "smallvec", + "static_assertions", + "writeable", +] + +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + +[[package]] +name = "fs_extra" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2022715d62ab30faffd124d40b76f4134a550a87792276512b18d63272333394" + +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + +[[package]] +name = "gc" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" +dependencies = [ + "gc_derive", +] + +[[package]] +name = "gc_derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", +] + +[[package]] +name = "half" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" + +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2540771e65fc8cb83cd6e8a237f70c319bd5c29f78ed1084ba5d50eeac86f7f9" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "icu_calendar" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c0d5e4d859bd991761ead59f6c8745d3cf3443c1c6c9bf6c5131b3ac8cf3c09" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider", + "serde", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_datetime" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683f1ef5f2d28919f374030942274e0576e4f09c2d2735092899eb8dc1842c9d" +dependencies = [ + "displaydoc", + "either", + "icu_calendar", + "icu_locid", + "icu_plurals", + "icu_provider", + "litemap", + "serde", + "smallvec", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locale_canonicalizer" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "973942d4e8c01fac1839feb537f4933514236585f418125963ff78d4004eebfd" +dependencies = [ + "icu_locid", + "icu_provider", + "litemap", + "serde", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a47bdfb63c6b49f5c43fb7ba358edcd1422fdf2e8df6fe26ece0df4925333cd" +dependencies = [ + "displaydoc", + "litemap", + "serde", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_plurals" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01a725b9ebe4910eb1bcc65a3a4c3262c6cc2ebd327a63df9f6d18ee53acee5f" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locid", + "icu_provider", + "serde", + "zerovec", +] + +[[package]] +name = "icu_provider" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7fbd7ffd479fdbbc366334a82821dc50d9f80b758389393374e9b36ff159f1a" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "litemap", + "postcard", + "serde", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_blob" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "474b884a565f7ec52a26754a8b57646c128195e7af629caa52317ef6674e3e0d" +dependencies = [ + "icu_provider", + "postcard", + "serde", + "writeable", + "yoke", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e3ab0b492dcc1416f2f16142596467382ed70bdbfaee51c83e086b1ce0e75c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "icu_testdata" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5580eeaa6ea70b94f286120ffcfb70f75ac8d759d95ccf6223a3c479ff99285" +dependencies = [ + "icu_provider", + "icu_provider_blob", +] + +[[package]] +name = "indexmap" +version = "1.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +dependencies = [ + "autocfg", + "hashbrown", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9448015e586b611e5d322f6703812bbca2f1e709d5773ecd38ddb4e3bb649504" + +[[package]] +name = "itertools" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + +[[package]] +name = "jemalloc-sys" +version = "0.5.0+5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f655c3ecfa6b0d03634595b4b54551d4bd5ac208b9e0124873949a7ab168f70b" +dependencies = [ + "cc", + "fs_extra", + "libc", +] + +[[package]] +name = "jemallocator" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16c2514137880c52b0b4822b563fadd38257c1f380858addb74a400889696ea6" +dependencies = [ + "jemalloc-sys", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.126" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" + +[[package]] +name = "linked-hash-map" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fb9b38af92608140b86b693604b9ffcc5824240a484d1ecd4795bacb2fe88f3" + +[[package]] +name = "linux-raw-sys" +version = "0.0.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4d2456c373231a208ad294c33dc5bff30051eafd954cd4caae83a712b12854d" + +[[package]] +name = "litemap" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78d268a51abaaee3b8686e56396eb725b0da510bddd266a52e784aa1029dae73" +dependencies = [ + "serde", + "yoke", +] + +[[package]] +name = "lock_api" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "measureme" +version = "10.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbdc226fa10994e8f66a4d2f6f000148bc563a1c671b6dcd2135737018033d8a" +dependencies = [ + "log", + "memmap2", + "parking_lot", + "perf-event-open-sys", + "rustc-hash", + "smallvec", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memmap2" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "723e3ebdcdc5c023db1df315364573789f8857c11b631a2fdfad7c00f5c046b4" +dependencies = [ + "libc", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "nibble_vec" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43" +dependencies = [ + "smallvec", +] + +[[package]] +name = "nix" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f866317acbd3a240710c63f065ffb1e4fd466259045ccb504130b7f668f35c6" +dependencies = [ + "bitflags", + "cc", + "cfg-if", + "libc", + "memoffset", +] + +[[package]] +name = "nodrop" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-format" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" +dependencies = [ + "arrayvec", + "itoa 0.4.8", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "once_cell" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" + +[[package]] +name = "oorandom" +version = "11.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" + +[[package]] +name = "os_str_bytes" +version = "6.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" + +[[package]] +name = "parking_lot" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" +dependencies = [ + "instant", + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" +dependencies = [ + "cfg-if", + "instant", + "libc", + "redox_syscall", + "smallvec", + "winapi", +] + +[[package]] +name = "perf-event-open-sys" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce9bedf5da2c234fdf2391ede2b90fabf585355f33100689bc364a3ea558561a" +dependencies = [ + "libc", +] + +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + +[[package]] +name = "plotters" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" + +[[package]] +name = "plotters-svg" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "postcard" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a25c0b0ae06fcffe600ad392aabfa535696c8973f2253d9ac83171924c58a858" +dependencies = [ + "postcard-cobs", + "serde", +] + +[[package]] +name = "postcard-cobs" +version = "0.1.5-pre" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c68cb38ed13fd7bc9dd5db8f165b7c8d9c1a315104083a2b10f11354c2af97f" + +[[package]] +name = "ppv-lite86" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-hack" +version = "0.5.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" + +[[package]] +name = "proc-macro2" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radix_trie" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c069c179fcdc6a2fe24d8d18305cf085fdbd4f922c041943e203685d6a1c58fd" +dependencies = [ + "endian-type", + "nibble_vec", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rayon" +version = "1.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd99e5772ead8baa5215278c9b15bf92087709e9c1b2d1f97cdb5a183c933a7d" +dependencies = [ + "autocfg", + "crossbeam-deque", + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "258bcdb5ac6dad48491bb2992db6b7cf74878b0384908af124823d118c99683f" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + +[[package]] +name = "redox_syscall" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +dependencies = [ + "bitflags", +] + +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + +[[package]] +name = "regress" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a92ff21fe8026ce3f2627faaf43606f0b67b014dbc9ccf027181a804f75d92e" +dependencies = [ + "memchr", +] + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.34.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2079c267b8394eb529872c3cf92e181c378b41fea36e68130357b52493701d2e" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "winapi", +] + +[[package]] +name = "rustyline" +version = "9.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db7826789c0e25614b03e5a54a0717a86f9ff6e6e5247f92b369472869320039" +dependencies = [ + "bitflags", + "cfg-if", + "clipboard-win", + "dirs-next", + "fd-lock", + "libc", + "log", + "memchr", + "nix", + "radix_trie", + "scopeguard", + "smallvec", + "unicode-segmentation", + "unicode-width", + "utf8parse", + "winapi", +] + +[[package]] +name = "rustyline-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb35a55ab810b5c0fe31606fe9b47d1354e4dc519bec0a102655f78ea2b38057" +dependencies = [ + "quote", + "syn", +] + +[[package]] +name = "ryu" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" + +[[package]] +name = "ryu-js" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6518fc26bced4d53678a22d6e423e9d8716377def84545fe328236e3af070e7f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "semver" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a41d061efea015927ac527063765e73601444cdc344ba855bc7bd44578b25e1c" + +[[package]] +name = "serde" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_cbor" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" +dependencies = [ + "half", + "serde", +] + +[[package]] +name = "serde_derive" +version = "1.0.139" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.82" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" +dependencies = [ + "itoa 1.0.2", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.8.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec0091e1f5aa338283ce049bd9dfefd55e1f168ac233e85c1ffe0038fb48cbe" +dependencies = [ + "indexmap", + "ryu", + "serde", + "yaml-rust", +] + +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + +[[package]] +name = "smallvec" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +dependencies = [ + "serde", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "str-buf" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e08d8363704e6c71fc928674353e6b7c23dcea9d82d7012c8faf2a3a025f8d0" + +[[package]] +name = "strsim" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "structopt" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" +dependencies = [ + "clap 2.34.0", + "lazy_static", + "structopt-derive", +] + +[[package]] +name = "structopt-derive" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" +dependencies = [ + "heck 0.3.3", + "proc-macro-error", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + +[[package]] +name = "sys-locale" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "658ee915b6c7b73ec4c1ffcd838506b5c5a4087eadc1ec8f862f1066cf2c8132" +dependencies = [ + "cc", + "cstr_core", + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", + "winapi", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "textwrap" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "textwrap" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" + +[[package]] +name = "thiserror" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "time" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" +dependencies = [ + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", + "winapi", +] + +[[package]] +name = "tinystr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a34eb32d8589368c99f8d6c0b9be3d09480d992da08c5990702b3e6b9260af8" +dependencies = [ + "displaydoc", + "serde", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + +[[package]] +name = "unicode-general-category" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6" + +[[package]] +name = "unicode-ident" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" + +[[package]] +name = "unicode-normalization" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" + +[[package]] +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "unicode-xid" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04" + +[[package]] +name = "utf8parse" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" + +[[package]] +name = "vec_map" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.10.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.81" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" + +[[package]] +name = "web-sys" +version = "0.3.58" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030b7ff91626e57a05ca64a07c481973cbb2db774e4852c9c7ca342408c6a99a" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29277a4435d642f775f63c7d1faeb927adba532886ce0287bd985bffb16b6bca" + +[[package]] +name = "windows_i686_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1145e1989da93956c68d1864f32fb97c8f561a8f89a5125f6a2b7ea75524e4b8" + +[[package]] +name = "windows_i686_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4a09e3a0d4753b73019db171c1339cd4362c8c44baf1bcea336235e955954a6" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ca64fcb0220d58db4c119e050e7af03c69e6f4f415ef69ec1773d9aab422d5a" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08cabc9f0066848fef4bc6a1c1668e6efce38b661d2aeec75d18d8617eebb5f1" + +[[package]] +name = "writeable" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae065fb2b24446a45c2bea0fe256b4ca54032661ee72d2500aaadeeec41b8e4e" + +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + +[[package]] +name = "yoke" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a829d54286b35cf07cbf9d8de817387ba4de20286e59214e67eaad5124b620a1" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "768f11e61cfb57f5de25941c877571dde114de48cac204594cc72beb71073d9f" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerofrom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aed578cc7fa1c85290bdaca18fa5ac8a9365ddd9ed54af4380a6c5e13d9fc484" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8785f47d6062c1932866147f91297286a9f350b3070e9d9f0b6078e37d623c1a" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] + +[[package]] +name = "zerovec" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c1b475ff48237bf7281cfa1721a52f0ad7f95ede1a46385e555870a354afc45" +dependencies = [ + "serde", + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd14fd397ea1b593c9c97a35d4da3dfb3a0ac7a1cad0e7f9e1b4bc313d1787e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "synstructure", +] diff --git a/__wasm/wit-bindgen-sample/engine/boa/Cargo.toml b/__wasm/wit-bindgen-sample/engine/boa/Cargo.toml new file mode 100644 index 0000000..20631cf --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/Cargo.toml @@ -0,0 +1,34 @@ +[workspace] +members = [ + "boa_cli", + "boa_engine", + "boa_gc", + "boa_interner", + "boa_profiler", + "boa_tester", + "boa_unicode", + "boa_wasm", + "boa_examples", +] + +[workspace.metadata.workspaces] +allow_branch = "main" + +# The release profile, used for `cargo build --release`. +[profile.release] +# Enables "fat" LTO, for faster release builds +lto = "fat" +# Makes sure that all code is compiled together, for LTO +codegen-units = 1 + +# The test profile, used for `cargo test`. +[profile.test] +# Enables thin local LTO and some optimizations. +opt-level = 1 + +# The benchmark profile, used for `cargo bench`. +[profile.bench] +# Enables "fat" LTO, for faster benchmark builds +lto = "fat" +# Makes sure that all code is compiled together, for LTO +codegen-units = 1 diff --git a/__wasm/wit-bindgen-sample/engine/boa/LICENSE-MIT b/__wasm/wit-bindgen-sample/engine/boa/LICENSE-MIT new file mode 100644 index 0000000..d87a9e8 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/LICENSE-MIT @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 Jason Williams + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/__wasm/wit-bindgen-sample/engine/boa/LICENSE-UNLICENSE b/__wasm/wit-bindgen-sample/engine/boa/LICENSE-UNLICENSE new file mode 100644 index 0000000..cf1ab25 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/LICENSE-UNLICENSE @@ -0,0 +1,24 @@ +This is free and unencumbered software released into the public domain. + +Anyone is free to copy, modify, publish, use, compile, sell, or +distribute this software, either in source code form or as a compiled +binary, for any purpose, commercial or non-commercial, and by any +means. + +In jurisdictions that recognize copyright laws, the author or authors +of this software dedicate any and all copyright interest in the +software to the public domain. We make this dedication for the benefit +of the public at large and to the detriment of our heirs and +successors. We intend this dedication to be an overt act of +relinquishment in perpetuity of all present and future rights to this +software under copyright law. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR +OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, +ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. + +For more information, please refer to diff --git a/__wasm/wit-bindgen-sample/engine/boa/README.md b/__wasm/wit-bindgen-sample/engine/boa/README.md new file mode 100644 index 0000000..e01aa75 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/README.md @@ -0,0 +1,115 @@ +# Boa + +

+ Boa Logo +

+ +This is an experimental Javascript lexer, parser and interpreter written in Rust. +Currently, it has support for some of the language. + +[![Build Status][build_badge]][build_link] +[![codecov](https://codecov.io/gh/boa-dev/boa/branch/main/graph/badge.svg)](https://codecov.io/gh/boa-dev/boa) +[![Crates.io](https://img.shields.io/crates/v/Boa.svg)](https://crates.io/crates/Boa) +[![Docs.rs](https://docs.rs/Boa/badge.svg)](https://docs.rs/Boa/) +[![Discord](https://img.shields.io/discord/595323158140158003?logo=discord)](https://discord.gg/tUFFk9Y) + +[build_badge]: https://github.com/boa-dev/boa/actions/workflows/rust.yml/badge.svg?event=push&branch=main +[build_link]: https://github.com/boa-dev/boa/actions/workflows/rust.yml?query=event%3Apush+branch%3Amain + +## Live Demo (WASM) + + + +You can get more verbose errors when running from the command line. + +## Development documentation + +You can check the internal development docs at . + +## Conformance + +To know how much of the _ECMAScript_ specification does Boa cover, you can check out results running the _ECMASCript Test262_ test suite [here](https://boa-dev.github.io/boa/test262/). + +## Contributing + +Please, check the [CONTRIBUTING.md](CONTRIBUTING.md) file to know how to +contribute in the project. You will need Rust installed and an editor. We have +some configurations ready for VSCode. + +### Debugging + +Check [debugging.md](./docs/debugging.md) for more info on debugging. + +### Web Assembly + +This interpreter can be exposed to JavaScript! +You can build the example locally with: + +```shell +npm run build +``` + +In the console you can use `window.evaluate` to pass JavaScript in. +To develop on the web assembly side you can run: + +```shell +npm run serve +``` + +then go to `http://localhost:8080`. + +## Usage + +- Clone this repo. +- Run with `cargo run -- test.js` where `test.js` is an existing JS file with any JS valid code. +- If any JS doesn't work then it's a bug. Please raise an [issue](https://github.com/boa-dev/boa/issues/)! + +### Example + +![Example](docs/img/latestDemo.gif) + +## Command-line Options + +```shell +USAGE: + boa [OPTIONS] [FILE]... + +FLAGS: + -h, --help Prints help information + -V, --version Prints version information + +OPTIONS: + -a, --dump-ast Dump the abstract syntax tree (ast) to stdout with the given format [possible values: Debug, Json, + JsonPretty] + +ARGS: + ... The JavaScript file(s) to be evaluated +``` + +## Roadmap + +See [Milestones](https://github.com/boa-dev/boa/milestones). + +## Benchmarks + +See [Benchmarks](https://boa-dev.github.io/boa/dev/bench/). + +## Profiling + +See [Profiling](./docs/profiling.md). + +## Changelog + +See [CHANGELOG.md](./CHANGELOG.md). + +## Communication + +Feel free to contact us on [Discord](https://discord.gg/tUFFk9Y). + +## License + +This project is licensed under the [Unlicense](./LICENSE-UNLICENSE) or [MIT](./LICENSE-MIT) licenses, at your option. diff --git a/__wasm/wit-bindgen-sample/engine/boa/assets/01_rust_loves_js.png b/__wasm/wit-bindgen-sample/engine/boa/assets/01_rust_loves_js.png new file mode 100644 index 0000000..145034d Binary files /dev/null and b/__wasm/wit-bindgen-sample/engine/boa/assets/01_rust_loves_js.png differ diff --git a/__wasm/wit-bindgen-sample/engine/boa/assets/logo.svg b/__wasm/wit-bindgen-sample/engine/boa/assets/logo.svg new file mode 100644 index 0000000..d91e1db --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/assets/logo.svg @@ -0,0 +1,184 @@ + + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_cli/Cargo.toml b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/Cargo.toml new file mode 100644 index 0000000..55d2f24 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "boa_cli" +version = "0.15.0" +edition = "2021" +rust-version = "1.60" +authors = ["boa-dev"] +description = "Boa is a Javascript lexer, parser and Just-in-Time compiler written in Rust. Currently, it has support for some of the language." +repository = "https://github.com/boa-dev/boa" +keywords = ["javascript", "compiler", "lexer", "parser", "js", "cli"] +categories = ["command-line-utilities"] +license = "Unlicense/MIT" +default-run = "boa" + +[dependencies] +boa_engine = { path = "../boa_engine", features = ["deser", "console"], version = "0.15.0" } +boa_interner = { path = "../boa_interner", version = "0.15.0" } +rustyline = "9.1.2" +rustyline-derive = "0.6.0" +clap = { version = "3.2.12", features = ["derive"] } +serde_json = "1.0.82" +colored = "2.0.0" +regex = "1.6.0" +phf = { version = "0.10.1", features = ["macros"] } + +[target.x86_64-unknown-linux-gnu.dependencies] +jemallocator = "0.5.0" + +[[bin]] +name = "boa" +doc = false +path = "src/main.rs" diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/helper.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/helper.rs new file mode 100644 index 0000000..cd4f723 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/helper.rs @@ -0,0 +1,173 @@ +use colored::{Color, Colorize}; +use phf::{phf_set, Set}; +use regex::{Captures, Regex}; +use rustyline::{ + error::ReadlineError, + highlight::Highlighter, + validate::{MatchingBracketValidator, ValidationContext, ValidationResult, Validator}, +}; +use rustyline_derive::{Completer, Helper, Hinter}; +use std::borrow::Cow; + +const STRING_COLOR: Color = Color::Green; +const KEYWORD_COLOR: Color = Color::Yellow; +const PROPERTY_COLOR: Color = Color::Magenta; +const OPERATOR_COLOR: Color = Color::TrueColor { + r: 214, + g: 95, + b: 26, +}; +const UNDEFINED_COLOR: Color = Color::TrueColor { + r: 100, + g: 100, + b: 100, +}; +const NUMBER_COLOR: Color = Color::TrueColor { + r: 26, + g: 214, + b: 175, +}; +const IDENTIFIER_COLOR: Color = Color::TrueColor { + r: 26, + g: 160, + b: 214, +}; + +#[allow(clippy::upper_case_acronyms)] +#[derive(Completer, Helper, Hinter)] +pub(crate) struct RLHelper { + highlighter: LineHighlighter, + validator: MatchingBracketValidator, +} + +impl RLHelper { + #[inline] + pub(crate) fn new() -> Self { + Self { + highlighter: LineHighlighter, + validator: MatchingBracketValidator::new(), + } + } +} + +impl Validator for RLHelper { + fn validate( + &self, + context: &mut ValidationContext<'_>, + ) -> Result { + self.validator.validate(context) + } + + fn validate_while_typing(&self) -> bool { + self.validator.validate_while_typing() + } +} + +impl Highlighter for RLHelper { + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + hint.into() + } + + fn highlight<'l>(&self, line: &'l str, pos: usize) -> Cow<'l, str> { + self.highlighter.highlight(line, pos) + } + + fn highlight_candidate<'c>( + &self, + candidate: &'c str, + _completion: rustyline::CompletionType, + ) -> Cow<'c, str> { + self.highlighter.highlight(candidate, 0) + } + + fn highlight_char(&self, line: &str, _: usize) -> bool { + !line.is_empty() + } +} + +static KEYWORDS: Set<&'static str> = phf_set! { + "break", + "case", + "catch", + "class", + "const", + "continue", + "default", + "delete", + "do", + "else", + "export", + "extends", + "finally", + "for", + "function", + "if", + "import", + "instanceof", + "new", + "return", + "super", + "switch", + "this", + "throw", + "try", + "typeof", + "var", + "void", + "while", + "with", + "yield", + "await", + "enum", + "let", +}; + +struct LineHighlighter; + +impl Highlighter for LineHighlighter { + fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { + let mut coloured = line.to_string(); + + let reg = Regex::new( + r#"(?x) + (?P\b[$_\p{ID_Start}][$_\p{ID_Continue}\u{200C}\u{200D}]*\b) | + (?P"([^"\\]|\\.)*") | + (?P'([^'\\]|\\.)*') | + (?P`([^`\\]|\\.)*`) | + (?P[+\-/*%~^!&|=<>;:]) | + (?P0[bB][01](_?[01])*n?|0[oO][0-7](_?[0-7])*n?|0[xX][0-9a-fA-F](_?[0-9a-fA-F])*n?|(([0-9](_?[0-9])*\.([0-9](_?[0-9])*)?)|(([0-9](_?[0-9])*)?\.[0-9](_?[0-9])*)|([0-9](_?[0-9])*))([eE][+-]?[0-9](_?[0-9])*)?n?)"#, + ) + .expect("could not compile regular expression"); + + coloured = reg + .replace_all(&coloured, |caps: &Captures<'_>| { + if let Some(cap) = caps.name("identifier") { + match cap.as_str() { + "true" | "false" | "null" | "Infinity" | "globalThis" => { + cap.as_str().color(PROPERTY_COLOR).to_string() + } + "undefined" => cap.as_str().color(UNDEFINED_COLOR).to_string(), + identifier if KEYWORDS.contains(identifier) => { + cap.as_str().color(KEYWORD_COLOR).bold().to_string() + } + _ => cap.as_str().color(IDENTIFIER_COLOR).to_string(), + } + } else if let Some(cap) = caps.name("string_double_quote") { + cap.as_str().color(STRING_COLOR).to_string() + } else if let Some(cap) = caps.name("string_single_quote") { + cap.as_str().color(STRING_COLOR).to_string() + } else if let Some(cap) = caps.name("template_literal") { + cap.as_str().color(STRING_COLOR).to_string() + } else if let Some(cap) = caps.name("op") { + cap.as_str().color(OPERATOR_COLOR).to_string() + } else if let Some(cap) = caps.name("number") { + cap.as_str().color(NUMBER_COLOR).to_string() + } else { + caps[0].to_string() + } + }) + .to_string(); + + coloured.into() + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/main.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/main.rs new file mode 100644 index 0000000..c72d52b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_cli/src/main.rs @@ -0,0 +1,264 @@ +#![doc( + html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" +)] +#![cfg_attr(not(test), deny(clippy::unwrap_used))] +#![warn( + clippy::perf, + clippy::single_match_else, + clippy::dbg_macro, + clippy::doc_markdown, + clippy::wildcard_imports, + clippy::struct_excessive_bools, + clippy::doc_markdown, + clippy::semicolon_if_nothing_returned, + clippy::pedantic +)] +#![deny( + clippy::all, + clippy::cast_lossless, + clippy::redundant_closure_for_method_calls, + clippy::use_self, + clippy::unnested_or_patterns, + clippy::trivially_copy_pass_by_ref, + clippy::needless_pass_by_value, + clippy::match_wildcard_for_single_variants, + clippy::map_unwrap_or, + unused_qualifications, + unused_import_braces, + unused_lifetimes, + unreachable_pub, + trivial_numeric_casts, + // rustdoc, + missing_debug_implementations, + missing_copy_implementations, + deprecated_in_future, + meta_variable_misuse, + non_ascii_idents, + rust_2018_compatibility, + rust_2018_idioms, + future_incompatible, + nonstandard_style, +)] +#![allow( + clippy::module_name_repetitions, + clippy::cast_possible_truncation, + clippy::cast_sign_loss, + clippy::cast_precision_loss, + clippy::cast_possible_wrap, + clippy::cast_ptr_alignment, + clippy::missing_panics_doc, + clippy::too_many_lines, + clippy::unreadable_literal, + clippy::missing_inline_in_public_items, + clippy::cognitive_complexity, + clippy::must_use_candidate, + clippy::missing_errors_doc, + clippy::as_conversions, + clippy::let_unit_value, + rustdoc::missing_doc_code_examples +)] + +use boa_engine::{syntax::ast::node::StatementList, Context}; +use clap::{ArgEnum, Parser}; +use colored::{Color, Colorize}; +use rustyline::{config::Config, error::ReadlineError, EditMode, Editor}; +use std::{fs::read, io, path::PathBuf}; +mod helper; + +#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] +#[cfg_attr( + all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"), + global_allocator +)] +static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; + +/// CLI configuration for Boa. +static CLI_HISTORY: &str = ".boa_history"; + +const READLINE_COLOR: Color = Color::Cyan; + +// Added #[allow(clippy::option_option)] because to StructOpt an Option> +// is an optional argument that optionally takes a value ([--opt=[val]]). +// https://docs.rs/structopt/0.3.11/structopt/#type-magic +#[allow(clippy::option_option)] +#[derive(Debug, Parser)] +#[clap(author, about, name = "boa")] +struct Opt { + /// The JavaScript file(s) to be evaluated. + #[clap(name = "FILE", parse(from_os_str))] + files: Vec, + + /// Dump the AST to stdout with the given format. + #[clap(long, short = 'a', value_name = "FORMAT", ignore_case = true, arg_enum)] + dump_ast: Option>, + + /// Dump the AST to stdout with the given format. + #[clap(long = "trace", short = 't')] + trace: bool, + + /// Use vi mode in the REPL + #[clap(long = "vi")] + vi_mode: bool, +} + +impl Opt { + /// Returns whether a dump flag has been used. + fn has_dump_flag(&self) -> bool { + self.dump_ast.is_some() + } +} + +#[derive(Debug, Clone, ArgEnum)] +enum DumpFormat { + /// The different types of format available for dumping. + /// + // NOTE: This can easily support other formats just by + // adding a field to this enum and adding the necessary + // implementation. Example: Toml, Html, etc. + // + // NOTE: The fields of this enum are not doc comments because + // arg_enum! macro does not support it. + + // This is the default format that you get from std::fmt::Debug. + Debug, + + // This is a minified json format. + Json, + + // This is a pretty printed json format. + JsonPretty, +} + +/// Parses the the token stream into an AST and returns it. +/// +/// Returns a error of type String with a message, +/// if the token stream has a parsing error. +fn parse_tokens(src: S, context: &mut Context) -> Result +where + S: AsRef<[u8]>, +{ + use boa_engine::syntax::parser::Parser; + + let src_bytes = src.as_ref(); + Parser::new(src_bytes) + .parse_all(context) + .map_err(|e| format!("ParsingError: {e}")) +} + +/// Dumps the AST to stdout with format controlled by the given arguments. +/// +/// Returns a error of type String with a error message, +/// if the source has a syntax or parsing error. +fn dump(src: S, args: &Opt, context: &mut Context) -> Result<(), String> +where + S: AsRef<[u8]>, +{ + if let Some(ref arg) = args.dump_ast { + let ast = parse_tokens(src, context)?; + + match arg { + Some(format) => match format { + DumpFormat::Debug => println!("{ast:#?}"), + DumpFormat::Json => println!( + "{}", + serde_json::to_string(&ast).expect("could not convert AST to a JSON string") + ), + DumpFormat::JsonPretty => { + println!( + "{}", + serde_json::to_string_pretty(&ast) + .expect("could not convert AST to a pretty JSON string") + ); + } + }, + // Default ast dumping format. + None => println!("{ast:#?}"), + } + } + + Ok(()) +} + +pub fn main() -> Result<(), std::io::Error> { + let args = Opt::parse(); + + let mut context = Context::default(); + + // Trace Output + context.set_trace(args.trace); + + for file in &args.files { + let buffer = read(file)?; + + if args.has_dump_flag() { + if let Err(e) = dump(&buffer, &args, &mut context) { + eprintln!("{e}"); + } + } else { + match context.eval(&buffer) { + Ok(v) => println!("{}", v.display()), + Err(v) => eprintln!("Uncaught {}", v.display()), + } + } + } + + if args.files.is_empty() { + let config = Config::builder() + .keyseq_timeout(1) + .edit_mode(if args.vi_mode { + EditMode::Vi + } else { + EditMode::Emacs + }) + .build(); + + let mut editor = Editor::with_config(config); + editor.load_history(CLI_HISTORY).map_err(|err| match err { + ReadlineError::Io(e) => e, + e => io::Error::new(io::ErrorKind::Other, e), + })?; + editor.set_helper(Some(helper::RLHelper::new())); + + let readline = ">> ".color(READLINE_COLOR).bold().to_string(); + + loop { + match editor.readline(&readline) { + Ok(line) if line == ".exit" => break, + Err(ReadlineError::Interrupted | ReadlineError::Eof) => break, + + Ok(line) => { + editor.add_history_entry(&line); + + if args.has_dump_flag() { + if let Err(e) = dump(&line, &args, &mut context) { + eprintln!("{e}"); + } + } else { + match context.eval(line.trim_end()) { + Ok(v) => println!("{}", v.display()), + Err(v) => { + eprintln!( + "{}: {}", + "Uncaught".red(), + v.display().to_string().red() + ); + } + } + } + } + + Err(err) => { + eprintln!("Unknown error: {err:?}"); + break; + } + } + } + + editor + .save_history(CLI_HISTORY) + .expect("could not save CLI history"); + } + + Ok(()) +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/Cargo.toml b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/Cargo.toml new file mode 100644 index 0000000..74ef106 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/Cargo.toml @@ -0,0 +1,75 @@ +[package] +name = "boa_engine" +version = "0.15.0" +edition = "2021" +rust-version = "1.60" +authors = ["boa-dev"] +description = "Boa is a Javascript lexer, parser and Just-in-Time compiler written in Rust. Currently, it has support for some of the language." +repository = "https://github.com/boa-dev/boa" +keywords = ["javascript", "js", "compiler", "lexer", "parser"] +categories = ["parser-implementations", "compilers"] +license = "Unlicense/MIT" +readme = "../README.md" + +[features] +profiler = ["boa_profiler/profiler"] +deser = ["boa_interner/serde"] +intl = [ + "dep:icu_locale_canonicalizer", + "dep:icu_locid", + "dep:icu_datetime", + "dep:icu_plurals", + "dep:icu_provider", + "dep:icu_testdata", + "dep:sys-locale" +] + +# Enable Boa's WHATWG console object implementation. +console = [] + +[dependencies] +boa_unicode = { path = "../boa_unicode", version = "0.15.0" } +boa_interner = { path = "../boa_interner", version = "0.15.0" } +boa_gc = { path = "../boa_gc", version = "0.15.0" } +gc = "0.4.1" +boa_profiler = { path = "../boa_profiler", version = "0.15.0" } +serde = { version = "1.0.139", features = ["derive", "rc"] } +serde_json = "1.0.82" +rand = "0.8.5" +num-traits = "0.2.15" +regress = "0.4.1" +rustc-hash = "1.1.0" +num-bigint = { version = "0.4.3", features = ["serde"] } +num-integer = "0.1.45" +bitflags = "1.3.2" +indexmap = "1.9.1" +ryu-js = "0.2.2" +chrono = "0.4.19" +fast-float = "0.2.0" +unicode-normalization = "0.1.21" +dyn-clone = "1.0.7" +once_cell = "1.13.0" +tap = "1.0.1" +icu_locale_canonicalizer = { version = "0.6.0", features = ["serde"], optional = true } +icu_locid = { version = "0.6.0", features = ["serde"], optional = true } +icu_datetime = { version = "0.6.0", features = ["serde"], optional = true } +icu_plurals = { version = "0.6.0", features = ["serde"], optional = true } +icu_provider = { version = "0.6.0", optional = true } +icu_testdata = { version = "0.6.0", optional = true } +sys-locale = { version = "0.2.1", optional = true } + +[dev-dependencies] +criterion = "0.3.5" +float-cmp = "0.9.0" + +[target.x86_64-unknown-linux-gnu.dev-dependencies] +jemallocator = "0.5.0" + +[lib] +crate-type = ["cdylib", "lib"] +name = "boa_engine" +bench = false + +[[bench]] +name = "full" +harness = false diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/README.md b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/README.md new file mode 100644 index 0000000..a84190f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/README.md @@ -0,0 +1,10 @@ +# Boa Benchmarks + +For each js script in the `bench_scripts` folder, we create three benchmarks: + +- Parser => lexing and parsing of the source code +- Compiler => compilation of the parsed statement list into bytecode +- Execution => execution of the bytecode in the vm + +The idea is to check the performance of Boa in different scenarios. +Different parts of Boa are benchmarked separately to make the impact of local changes visible. diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/arithmetic_operations.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/arithmetic_operations.js new file mode 100644 index 0000000..f711585 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/arithmetic_operations.js @@ -0,0 +1 @@ +((2 + 2) ** 3 / 100 - 5 ** 3 * -1000) ** 2 + 100 - 8; diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_access.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_access.js new file mode 100644 index 0000000..6da5c86 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_access.js @@ -0,0 +1,7 @@ +(function () { + let testArr = [1, 2, 3, 4, 5]; + + let res = testArr[2]; + + return res; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_create.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_create.js new file mode 100644 index 0000000..fddd4ca --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_create.js @@ -0,0 +1,8 @@ +(function () { + let testArr = []; + for (let a = 0; a <= 500; a++) { + testArr[a] = "p" + a; + } + + return testArr; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_pop.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_pop.js new file mode 100644 index 0000000..7613cfc --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/array_pop.js @@ -0,0 +1,22 @@ +(function () { + let testArray = [ + 83, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, + 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, + 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, + 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, + 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, + 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, + 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, + 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, 28, 83, 62, + 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, 45, 93, 17, + 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, 67, 77, 32, + 45, 93, 17, 28, 83, 62, 99, 36, 28, 93, 27, 29, 2828, 234, 23, 56, 32, 56, + 67, 77, 32, 45, 93, 17, 28, 83, 62, 99, 36, 28, + ]; + + while (testArray.length > 0) { + testArray.pop(); + } + + return testArray; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/boolean_object_access.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/boolean_object_access.js new file mode 100644 index 0000000..1451300 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/boolean_object_access.js @@ -0,0 +1,7 @@ +new Boolean( + !new Boolean( + new Boolean( + !new Boolean(false).valueOf() && new Boolean(true).valueOf() + ).valueOf() + ).valueOf() +).valueOf(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/clean_js.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/clean_js.js new file mode 100644 index 0000000..848ee4d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/clean_js.js @@ -0,0 +1,15 @@ +!function () { + var M = new Array(); + for (i = 0; i < 100; i++) { + M.push(Math.floor(Math.random() * 100)); + } + var test = []; + for (i = 0; i < 100; i++) { + if (M[i] > 50) { + test.push(M[i]); + } + } + test.forEach(elem => { + 0 + }); +}(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/fibonacci.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/fibonacci.js new file mode 100644 index 0000000..237f59b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/fibonacci.js @@ -0,0 +1,10 @@ +(function () { + let num = 12; + + function fib(n) { + if (n <= 1) return 1; + return fib(n - 1) + fib(n - 2); + } + + return fib(num); +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/for_loop.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/for_loop.js new file mode 100644 index 0000000..3573e8d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/for_loop.js @@ -0,0 +1,10 @@ +(function () { + let b = "hello"; + for (let a = 10; a < 100; a += 5) { + if (a < 50) { + b += "world"; + } + } + + return b; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/mini_js.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/mini_js.js new file mode 100644 index 0000000..e7a8279 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/mini_js.js @@ -0,0 +1 @@ +!function(){var r=new Array();for(i=0;i<100;i++)r.push(Math.floor(100*Math.random()));var a=[];for(i=0;i<100;i++)r[i]>50&&a.push(r[i]);a.forEach(i=>{0})}(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/number_object_access.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/number_object_access.js new file mode 100644 index 0000000..5af0f6b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/number_object_access.js @@ -0,0 +1,5 @@ +new Number( + new Number( + new Number(new Number(100).valueOf() - 10.5).valueOf() + 100 + ).valueOf() * 1.6 +); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_creation.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_creation.js new file mode 100644 index 0000000..ecaae42 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_creation.js @@ -0,0 +1,8 @@ +(function () { + let test = { + my_prop: "hello", + another: 65, + }; + + return test; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_const.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_const.js new file mode 100644 index 0000000..5be57f3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_const.js @@ -0,0 +1,8 @@ +(function () { + let test = { + my_prop: "hello", + another: 65, + }; + + return test.my_prop; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_dyn.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_dyn.js new file mode 100644 index 0000000..9d62575 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/object_prop_access_dyn.js @@ -0,0 +1,8 @@ +(function () { + let test = { + my_prop: "hello", + another: 65, + }; + + return test["my" + "_prop"]; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp.js new file mode 100644 index 0000000..ce8de85 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp.js @@ -0,0 +1,5 @@ +(function () { + let regExp = new RegExp("hello", "i"); + + return regExp.test("Hello World"); +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_creation.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_creation.js new file mode 100644 index 0000000..9b06e77 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_creation.js @@ -0,0 +1,5 @@ +(function () { + let regExp = new RegExp("hello", "i"); + + return regExp; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal.js new file mode 100644 index 0000000..40e09be --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal.js @@ -0,0 +1,5 @@ +(function () { + let regExp = /hello/i; + + return regExp.test("Hello World"); +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal_creation.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal_creation.js new file mode 100644 index 0000000..aa0400b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/regexp_literal_creation.js @@ -0,0 +1,5 @@ +(function () { + let regExp = /hello/i; + + return regExp; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_compare.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_compare.js new file mode 100644 index 0000000..f7f747c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_compare.js @@ -0,0 +1,9 @@ +(function () { + var a = "hello"; + var b = "world"; + + var c = a == b; + + var d = b; + var e = d == b; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_concat.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_concat.js new file mode 100644 index 0000000..26ade2f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_concat.js @@ -0,0 +1,6 @@ +(function () { + var a = "hello"; + var b = "world"; + + var c = a + b; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_copy.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_copy.js new file mode 100644 index 0000000..5b3c5e1 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_copy.js @@ -0,0 +1,4 @@ +(function () { + var a = "hello"; + var b = a; +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_object_access.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_object_access.js new file mode 100644 index 0000000..45f2ec9 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/string_object_access.js @@ -0,0 +1,7 @@ +new String( + new String( + new String( + new String("Hello").valueOf() + new String(", world").valueOf() + ).valueOf() + "!" + ).valueOf() +).valueOf(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/symbol_creation.js b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/symbol_creation.js new file mode 100644 index 0000000..65a5248 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/bench_scripts/symbol_creation.js @@ -0,0 +1,3 @@ +(function () { + return Symbol(); +})(); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/full.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/full.rs new file mode 100644 index 0000000..ec90998 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/benches/full.rs @@ -0,0 +1,94 @@ +//! Benchmarks of the whole execution engine in Boa. + +use boa_engine::{realm::Realm, Context}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +#[cfg(all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"))] +#[cfg_attr( + all(target_arch = "x86_64", target_os = "linux", target_env = "gnu"), + global_allocator +)] +static ALLOC: jemallocator::Jemalloc = jemallocator::Jemalloc; + +fn create_realm(c: &mut Criterion) { + c.bench_function("Create Realm", move |b| b.iter(Realm::create)); +} + +macro_rules! full_benchmarks { + ($({$id:literal, $name:ident}),*) => { + fn bench_parser(c: &mut Criterion) { + $( + { + static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); + let mut context = Context::default(); + c.bench_function(concat!($id, " (Parser)"), move |b| { + b.iter(|| context.parse(black_box(CODE))) + }); + } + )* + } + fn bench_compile(c: &mut Criterion) { + $( + { + static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); + let mut context = Context::default(); + let statement_list = context.parse(CODE).expect("parsing failed"); + c.bench_function(concat!($id, " (Compiler)"), move |b| { + b.iter(|| { + context.compile(black_box(&statement_list)) + }) + }); + } + )* + } + fn bench_execution(c: &mut Criterion) { + $( + { + static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); + let mut context = Context::default(); + let statement_list = context.parse(CODE).expect("parsing failed"); + let code_block = context.compile(&statement_list).unwrap(); + c.bench_function(concat!($id, " (Execution)"), move |b| { + b.iter(|| { + context.execute(black_box(code_block.clone())).unwrap() + }) + }); + } + )* + } + }; +} + +full_benchmarks!( + {"Symbols", symbol_creation}, + {"For loop", for_loop}, + {"Fibonacci", fibonacci}, + {"Object Creation", object_creation}, + {"Static Object Property Access", object_prop_access_const}, + {"Dynamic Object Property Access", object_prop_access_dyn}, + {"RegExp Literal Creation", regexp_literal_creation}, + {"RegExp Creation", regexp_creation}, + {"RegExp Literal", regexp_literal}, + {"RegExp", regexp}, + {"Array access", array_access}, + {"Array creation", array_create}, + {"Array pop", array_pop}, + {"String concatenation", string_concat}, + {"String comparison", string_compare}, + {"String copy", string_copy}, + {"Number Object Access", number_object_access}, + {"Boolean Object Access", boolean_object_access}, + {"String Object Access", string_object_access}, + {"Arithmetic operations", arithmetic_operations}, + {"Clean js", clean_js}, + {"Mini js", mini_js} +); + +criterion_group!( + benches, + create_realm, + bench_parser, + bench_compile, + bench_execution, +); +criterion_main!(benches); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bigint.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bigint.rs new file mode 100644 index 0000000..727f56f --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bigint.rs @@ -0,0 +1,453 @@ +//! This module implements the JavaScript bigint primitive rust type. + +use crate::{builtins::Number, Context, JsValue}; +use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use num_integer::Integer; +use num_traits::{pow::Pow, FromPrimitive, One, ToPrimitive, Zero}; +use std::{ + fmt::{self, Display}, + ops::{Add, BitAnd, BitOr, BitXor, Div, Mul, Neg, Rem, Shl, Shr, Sub}, + rc::Rc, +}; + +/// The raw bigint type. +pub type RawBigInt = num_bigint::BigInt; + +#[cfg(feature = "deser")] +use serde::{Deserialize, Serialize}; + +/// JavaScript bigint primitive rust type. +#[cfg_attr(feature = "deser", derive(Serialize, Deserialize))] +#[derive(Debug, Finalize, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct JsBigInt { + inner: Rc, +} + +// Safety: BigInt does not contain any objects which needs to be traced, +// so this is safe. +unsafe impl Trace for JsBigInt { + unsafe_empty_trace!(); +} + +impl JsBigInt { + /// Create a new [`JsBigInt`]. + #[inline] + pub fn new>(value: T) -> Self { + value.into() + } + + /// Create a [`JsBigInt`] with value `0`. + #[inline] + pub fn zero() -> Self { + Self { + inner: Rc::new(RawBigInt::zero()), + } + } + + /// Check if is zero. + #[inline] + pub fn is_zero(&self) -> bool { + self.inner.is_zero() + } + + /// Create a [`JsBigInt`] with value `1`. + #[inline] + pub fn one() -> Self { + Self { + inner: Rc::new(RawBigInt::one()), + } + } + + /// Check if is one. + #[inline] + pub fn is_one(&self) -> bool { + self.inner.is_one() + } + + /// Convert bigint to string with radix. + #[inline] + pub fn to_string_radix(&self, radix: u32) -> String { + self.inner.to_str_radix(radix) + } + + /// Converts the `BigInt` to a f64 type. + /// + /// Returns `f64::INFINITY` if the `BigInt` is too big. + #[inline] + pub fn to_f64(&self) -> f64 { + self.inner.to_f64().unwrap_or(f64::INFINITY) + } + + /// Converts a string to a `BigInt` with the specified radix. + #[inline] + pub fn from_string_radix(buf: &str, radix: u32) -> Option { + Some(Self { + inner: Rc::new(RawBigInt::parse_bytes(buf.as_bytes(), radix)?), + }) + } + + /// This function takes a string and converts it to `BigInt` type. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-stringtobigint + #[inline] + pub fn from_string(mut string: &str) -> Option { + string = string.trim(); + + if string.is_empty() { + return Some(Self::zero()); + } + + let mut radix = 10; + if string.starts_with("0b") || string.starts_with("0B") { + radix = 2; + string = &string[2..]; + } else if string.starts_with("0x") || string.starts_with("0X") { + radix = 16; + string = &string[2..]; + } else if string.starts_with("0o") || string.starts_with("0O") { + radix = 8; + string = &string[2..]; + } + + Self::from_string_radix(string, radix) + } + + /// Checks for `SameValueZero` equality. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-equal + #[inline] + pub fn same_value_zero(x: &Self, y: &Self) -> bool { + // Return BigInt::equal(x, y) + Self::equal(x, y) + } + + /// Checks for `SameValue` equality. + /// + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-sameValue + #[inline] + pub fn same_value(x: &Self, y: &Self) -> bool { + // Return BigInt::equal(x, y) + Self::equal(x, y) + } + + /// Checks for mathematical equality. + /// + /// The abstract operation `BigInt::equal` takes arguments x (a `BigInt`) and y (a `BigInt`). + /// It returns `true` if x and y have the same mathematical integer value and false otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-numeric-types-bigint-sameValueZero + #[inline] + pub fn equal(x: &Self, y: &Self) -> bool { + x == y + } + + #[inline] + pub fn pow(x: &Self, y: &Self, context: &mut Context) -> Result { + let y = if let Some(y) = y.inner.to_biguint() { + y + } else { + return context.throw_range_error("BigInt negative exponent"); + }; + + let num_bits = (x.inner.bits() as f64 + * y.to_f64().expect("Unable to convert from BigUInt to f64")) + .floor() + + 1f64; + + if num_bits > 1_000_000_000f64 { + return context.throw_range_error("Maximum BigInt size exceeded"); + } + + Ok(Self::new(x.inner.as_ref().clone().pow(y))) + } + + #[inline] + pub fn shift_right(x: &Self, y: &Self, context: &mut Context) -> Result { + if let Some(n) = y.inner.to_i32() { + let inner = if n > 0 { + x.inner.as_ref().clone().shr(n as usize) + } else { + x.inner.as_ref().clone().shl(n.unsigned_abs()) + }; + + Ok(Self::new(inner)) + } else { + context.throw_range_error("Maximum BigInt size exceeded") + } + } + + #[inline] + pub fn shift_left(x: &Self, y: &Self, context: &mut Context) -> Result { + if let Some(n) = y.inner.to_i32() { + let inner = if n > 0 { + x.inner.as_ref().clone().shl(n as usize) + } else { + x.inner.as_ref().clone().shr(n.unsigned_abs()) + }; + + Ok(Self::new(inner)) + } else { + context.throw_range_error("Maximum BigInt size exceeded") + } + } + + /// Floored integer modulo. + /// + /// # Examples + /// ``` + /// # use num_integer::Integer; + /// assert_eq!((8).mod_floor(&3), 2); + /// assert_eq!((8).mod_floor(&-3), -1); + /// ``` + #[inline] + pub fn mod_floor(x: &Self, y: &Self) -> Self { + Self::new(x.inner.mod_floor(&y.inner)) + } + + #[inline] + pub fn add(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().add(y.inner.as_ref())) + } + + #[inline] + pub fn sub(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().sub(y.inner.as_ref())) + } + + #[inline] + pub fn mul(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().mul(y.inner.as_ref())) + } + + #[inline] + pub fn div(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().div(y.inner.as_ref())) + } + + #[inline] + pub fn rem(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().rem(y.inner.as_ref())) + } + + #[inline] + pub fn bitand(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().bitand(y.inner.as_ref())) + } + + #[inline] + pub fn bitor(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().bitor(y.inner.as_ref())) + } + + #[inline] + pub fn bitxor(x: &Self, y: &Self) -> Self { + Self::new(x.inner.as_ref().clone().bitxor(y.inner.as_ref())) + } + + #[inline] + pub fn neg(x: &Self) -> Self { + Self::new(x.as_inner().neg()) + } + + #[inline] + pub fn not(x: &Self) -> Self { + Self::new(!x.as_inner()) + } + + #[inline] + pub(crate) fn as_inner(&self) -> &RawBigInt { + &self.inner + } +} + +impl Display for JsBigInt { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&self.inner, f) + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: RawBigInt) -> Self { + Self { + inner: Rc::new(value), + } + } +} + +impl From> for JsBigInt { + #[inline] + fn from(value: Box) -> Self { + Self { + inner: value.into(), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: i8) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: u8) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: i16) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: u16) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: i32) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: u32) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: i64) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: u64) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: isize) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +impl From for JsBigInt { + #[inline] + fn from(value: usize) -> Self { + Self { + inner: Rc::new(RawBigInt::from(value)), + } + } +} + +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)] +pub struct TryFromF64Error; + +impl Display for TryFromF64Error { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Could not convert f64 value to a BigInt type") + } +} + +impl TryFrom for JsBigInt { + type Error = TryFromF64Error; + + #[inline] + fn try_from(n: f64) -> Result { + // If the truncated version of the number is not the + // same as the non-truncated version then the floating-point + // number conains a fractional part. + if !Number::equal(n.trunc(), n) { + return Err(TryFromF64Error); + } + match RawBigInt::from_f64(n) { + Some(bigint) => Ok(Self::new(bigint)), + None => Err(TryFromF64Error), + } + } +} + +impl PartialEq for JsBigInt { + #[inline] + fn eq(&self, other: &i32) -> bool { + self.inner.as_ref() == &RawBigInt::from(*other) + } +} + +impl PartialEq for i32 { + #[inline] + fn eq(&self, other: &JsBigInt) -> bool { + &RawBigInt::from(*self) == other.inner.as_ref() + } +} + +impl PartialEq for JsBigInt { + #[inline] + fn eq(&self, other: &f64) -> bool { + if other.fract() != 0.0 { + return false; + } + + self.inner.as_ref() == &RawBigInt::from(*other as i64) + } +} + +impl PartialEq for f64 { + #[inline] + fn eq(&self, other: &JsBigInt) -> bool { + if self.fract() != 0.0 { + return false; + } + + &RawBigInt::from(*self as i64) == other.inner.as_ref() + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/array_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/array_iterator.rs new file mode 100644 index 0000000..2820337 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/array_iterator.rs @@ -0,0 +1,145 @@ +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue}, + object::{JsObject, ObjectData}, + property::{PropertyDescriptor, PropertyNameKind}, + symbol::WellKnownSymbols, + Context, JsResult, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +/// The Array Iterator object represents an iteration over an array. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct ArrayIterator { + array: JsObject, + next_index: usize, + kind: PropertyNameKind, + done: bool, +} + +impl ArrayIterator { + pub(crate) const NAME: &'static str = "ArrayIterator"; + + fn new(array: JsObject, kind: PropertyNameKind) -> Self { + Self { + array, + kind, + next_index: 0, + done: false, + } + } + + /// `CreateArrayIterator( array, kind )` + /// + /// Creates a new iterator over the given array. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createarrayiterator + pub(crate) fn create_array_iterator( + array: JsObject, + kind: PropertyNameKind, + context: &Context, + ) -> JsValue { + let array_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .array_iterator(), + ObjectData::array_iterator(Self::new(array, kind)), + ); + array_iterator.into() + } + + /// %ArrayIteratorPrototype%.next( ) + /// + /// Gets the next result in the array. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%.next + pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut array_iterator = this.as_object().map(JsObject::borrow_mut); + let array_iterator = array_iterator + .as_mut() + .and_then(|obj| obj.as_array_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not an ArrayIterator"))?; + let index = array_iterator.next_index; + if array_iterator.done { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + let len = if let Some(f) = array_iterator.array.borrow().as_typed_array() { + if f.is_detached() { + return context.throw_type_error( + "Cannot get value from typed array that has a detached array buffer", + ); + } + + f.array_length() + } else { + array_iterator.array.length_of_array_like(context)? + }; + + if index >= len { + array_iterator.done = true; + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + array_iterator.next_index = index + 1; + match array_iterator.kind { + PropertyNameKind::Key => Ok(create_iter_result_object(index.into(), false, context)), + PropertyNameKind::Value => { + let element_value = array_iterator.array.get(index, context)?; + Ok(create_iter_result_object(element_value, false, context)) + } + PropertyNameKind::KeyAndValue => { + let element_value = array_iterator.array.get(index, context)?; + let result = Array::create_array_from_list([index.into(), element_value], context); + Ok(create_iter_result_object(result.into(), false, context)) + } + } + } + + /// Create the `%ArrayIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let array_iterator = + JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &array_iterator, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("Array Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + array_iterator.insert(to_string_tag, to_string_tag_property); + array_iterator + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/mod.rs new file mode 100644 index 0000000..6f3ed01 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/mod.rs @@ -0,0 +1,2917 @@ +//! This module implements the global `Array` object. +//! +//! The JavaScript `Array` class is a global object that is used in the construction of arrays; which are high-level, list-like objects. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-array-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array + +pub mod array_iterator; +#[cfg(test)] +mod tests; + +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::JsArgs; +use crate::{ + builtins::array::array_iterator::ArrayIterator, + builtins::iterable::{if_abrupt_close_iterator, IteratorHint}, + builtins::BuiltIn, + builtins::Number, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsFunction, JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptor, PropertyNameKind}, + symbol::WellKnownSymbols, + value::{IntegerOrInfinity, JsValue}, + Context, JsResult, JsString, +}; +use std::cmp::{max, min, Ordering}; + +/// JavaScript `Array` built-in implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Array; + +impl BuiltIn for Array { + const NAME: &'static str = "Array"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let symbol_iterator = WellKnownSymbols::iterator(); + let symbol_unscopables = WellKnownSymbols::unscopables(); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let values_function = Self::values_intrinsic(context); + let unscopables_object = Self::unscopables_intrinsic(context); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().array().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .property( + "length", + 0, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ) + .property( + "values", + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + symbol_iterator, + values_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + symbol_unscopables, + unscopables_object, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .method(Self::at, "at", 1) + .method(Self::concat, "concat", 1) + .method(Self::push, "push", 1) + .method(Self::index_of, "indexOf", 1) + .method(Self::last_index_of, "lastIndexOf", 1) + .method(Self::includes_value, "includes", 1) + .method(Self::map, "map", 1) + .method(Self::fill, "fill", 1) + .method(Self::for_each, "forEach", 1) + .method(Self::filter, "filter", 1) + .method(Self::pop, "pop", 0) + .method(Self::join, "join", 1) + .method(Self::to_string, "toString", 0) + .method(Self::reverse, "reverse", 0) + .method(Self::shift, "shift", 0) + .method(Self::unshift, "unshift", 1) + .method(Self::every, "every", 1) + .method(Self::find, "find", 1) + .method(Self::find_index, "findIndex", 1) + .method(Self::find_last, "findLast", 1) + .method(Self::find_last_index, "findLastIndex", 1) + .method(Self::flat, "flat", 0) + .method(Self::flat_map, "flatMap", 1) + .method(Self::slice, "slice", 2) + .method(Self::some, "some", 1) + .method(Self::sort, "sort", 1) + .method(Self::splice, "splice", 2) + .method(Self::reduce, "reduce", 1) + .method(Self::reduce_right, "reduceRight", 1) + .method(Self::keys, "keys", 0) + .method(Self::entries, "entries", 0) + .method(Self::copy_within, "copyWithin", 2) + // Static Methods + .static_method(Self::from, "from", 1) + .static_method(Self::is_array, "isArray", 1) + .static_method(Self::of, "of", 0) + .build() + .conv::() + .pipe(Some) + } +} + +impl Array { + const LENGTH: usize = 1; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let proto be ? GetPrototypeFromConstructor(newTarget, "%Array.prototype%"). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::array, context)?; + + // 3. Let numberOfArgs be the number of elements in values. + let number_of_args = args.len(); + + // 4. If numberOfArgs = 0, then + if number_of_args == 0 { + // 4.a. Return ! ArrayCreate(0, proto). + Ok(Self::array_create(0, Some(prototype), context) + .expect("this ArrayCreate call must not fail") + .into()) + // 5. Else if numberOfArgs = 1, then + } else if number_of_args == 1 { + // a. Let len be values[0]. + let len = &args[0]; + // b. Let array be ! ArrayCreate(0, proto). + let array = Self::array_create(0, Some(prototype), context) + .expect("this ArrayCreate call must not fail"); + // c. If Type(len) is not Number, then + #[allow(clippy::if_not_else)] + let int_len = if !len.is_number() { + // i. Perform ! CreateDataPropertyOrThrow(array, "0", len). + array + .create_data_property_or_throw(0, len, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + // ii. Let intLen be 1𝔽. + 1 + // d. Else, + } else { + // i. Let intLen be ! ToUint32(len). + let int_len = len + .to_u32(context) + .expect("this ToUint32 call must not fail"); + // ii. If SameValueZero(intLen, len) is false, throw a RangeError exception. + if !JsValue::same_value_zero(&int_len.into(), len) { + return context.throw_range_error("invalid array length"); + } + int_len + }; + // e. Perform ! Set(array, "length", intLen, true). + array + .set("length", int_len, true, context) + .expect("this Set call must not fail"); + // f. Return array. + Ok(array.into()) + // 6. Else, + } else { + // 6.a. Assert: numberOfArgs ≥ 2. + debug_assert!(number_of_args >= 2); + + // b. Let array be ? ArrayCreate(numberOfArgs, proto). + let array = Self::array_create(number_of_args, Some(prototype), context)?; + // c. Let k be 0. + // d. Repeat, while k < numberOfArgs, + for (i, item) in args.iter().cloned().enumerate() { + // i. Let Pk be ! ToString(𝔽(k)). + // ii. Let itemK be values[k]. + // iii. Perform ! CreateDataPropertyOrThrow(array, Pk, itemK). + array + .create_data_property_or_throw(i, item, context) + .expect("this CreateDataPropertyOrThrow must not fail"); + // iv. Set k to k + 1. + } + // e. Assert: The mathematical value of array's "length" property is numberOfArgs. + // f. Return array. + Ok(array.into()) + } + } + + /// Utility for constructing `Array` objects. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-arraycreate + pub(crate) fn array_create( + length: usize, + prototype: Option, + context: &mut Context, + ) -> JsResult { + // 1. If length > 2^32 - 1, throw a RangeError exception. + if length > u32::MAX as usize { //2usize.pow(32) - 1 { + return context.throw_range_error("array exceeded max size"); + } + // 7. Return A. + // 2. If proto is not present, set proto to %Array.prototype%. + // 3. Let A be ! MakeBasicObject(« [[Prototype]], [[Extensible]] »). + // 4. Set A.[[Prototype]] to proto. + // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1. + let prototype = match prototype { + Some(prototype) => prototype, + None => context.intrinsics().constructors().array().prototype(), + }; + let array = JsObject::from_proto_and_data(prototype, ObjectData::array()); + + // 6. Perform ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Value]]: 𝔽(length), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). + crate::object::internal_methods::ordinary_define_own_property( + &array, + "length".into(), + PropertyDescriptor::builder() + .value(length) + .writable(true) + .enumerable(false) + .configurable(false) + .build(), + context, + )?; + + Ok(array) + } + + /// Utility for constructing `Array` objects from an iterator of `JsValue`s. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createarrayfromlist + pub(crate) fn create_array_from_list(elements: I, context: &mut Context) -> JsObject + where + I: IntoIterator, + { + // 1. Assert: elements is a List whose elements are all ECMAScript language values. + // 2. Let array be ! ArrayCreate(0). + let array = Self::array_create(0, None, context) + .expect("creating an empty array with the default prototype must not fail"); + + // 3. Let n be 0. + // 4. For each element e of elements, do + // a. Perform ! CreateDataPropertyOrThrow(array, ! ToString(𝔽(n)), e). + // b. Set n to n + 1. + // + // NOTE: This deviates from the spec, but it should have the same behaviour. + let elements: Vec<_> = elements.into_iter().collect(); + let length = elements.len(); + array + .borrow_mut() + .properties_mut() + .override_indexed_properties(elements); + array + .set("length", length, true, context) + .expect("Should not fail"); + + // 5. Return array. + array + } + + /// Utility function for concatenating array objects. + /// + /// Returns a Boolean valued property that if `true` indicates that + /// an object should be flattened to its array elements + /// by `Array.prototype.concat`. + fn is_concat_spreadable(o: &JsValue, context: &mut Context) -> JsResult { + // 1. If Type(O) is not Object, return false. + let o = if let Some(o) = o.as_object() { + o + } else { + return Ok(false); + }; + + // 2. Let spreadable be ? Get(O, @@isConcatSpreadable). + let spreadable = o.get(WellKnownSymbols::is_concat_spreadable(), context)?; + + // 3. If spreadable is not undefined, return ! ToBoolean(spreadable). + if !spreadable.is_undefined() { + return Ok(spreadable.to_boolean()); + } + + // 4. Return ? IsArray(O). + o.is_array_abstract(context) + } + + /// `get Array [ @@species ]` + /// + /// The `Array [ @@species ]` accessor property returns the Array constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-array-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// Utility function used to specify the creation of a new Array object using a constructor + /// function that is derived from `original_array`. + /// + /// see: + pub(crate) fn array_species_create( + original_array: &JsObject, + length: usize, + context: &mut Context, + ) -> JsResult { + // 1. Let isArray be ? IsArray(originalArray). + // 2. If isArray is false, return ? ArrayCreate(length). + if !original_array.is_array_abstract(context)? { + return Self::array_create(length, None, context); + } + // 3. Let C be ? Get(originalArray, "constructor"). + let c = original_array.get("constructor", context)?; + + // 4. If IsConstructor(C) is true, then + // a. Let thisRealm be the current Realm Record. + // b. Let realmC be ? GetFunctionRealm(C). + // c. If thisRealm and realmC are not the same Realm Record, then + // i. If SameValue(C, realmC.[[Intrinsics]].[[%Array%]]) is true, set C to undefined. + // TODO: Step 4 is ignored, as there are no different realms for now + + // 5. If Type(C) is Object, then + let c = if let Some(c) = c.as_object() { + // 5.a. Set C to ? Get(C, @@species). + let c = c.get(WellKnownSymbols::species(), context)?; + // 5.b. If C is null, set C to undefined. + if c.is_null_or_undefined() { + JsValue::undefined() + } else { + c + } + } else { + c + }; + + // 6. If C is undefined, return ? ArrayCreate(length). + if c.is_undefined() { + return Self::array_create(length, None, context); + } + + // 7. If IsConstructor(C) is false, throw a TypeError exception. + if let Some(c) = c.as_constructor() { + // 8. Return ? Construct(C, « 𝔽(length) »). + c.construct(&[JsValue::new(length)], Some(c), context) + } else { + context.throw_type_error("Symbol.species must be a constructor") + } + } + + /// `Array.from(arrayLike)` + /// + /// The Array.from() static method creates a new, + /// shallow-copied Array instance from an array-like or iterable object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.from + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from + pub(crate) fn from( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let items = args.get_or_undefined(0); + let mapfn = args.get_or_undefined(1); + let this_arg = args.get_or_undefined(2); + + // 2. If mapfn is undefined, let mapping be false + // 3. Else, + // a. If IsCallable(mapfn) is false, throw a TypeError exception. + // b. Let mapping be true. + let mapping = match mapfn { + JsValue::Undefined => None, + JsValue::Object(o) if o.is_callable() => Some(o), + _ => return context.throw_type_error(format!("{} is not a function", mapfn.type_of())), + }; + + // 4. Let usingIterator be ? GetMethod(items, @@iterator). + let using_iterator = items + .get_method(WellKnownSymbols::iterator(), context)? + .map(JsValue::from); + + if let Some(using_iterator) = using_iterator { + // 5. If usingIterator is not undefined, then + + // a. If IsConstructor(C) is true, then + // i. Let A be ? Construct(C). + // b. Else, + // i. Let A be ? ArrayCreate(0en). + let a = match this.as_constructor() { + Some(constructor) => constructor.construct(&[], None, context)?, + _ => Self::array_create(0, None, context)?, + }; + + // c. Let iteratorRecord be ? GetIterator(items, sync, usingIterator). + let iterator_record = + items.get_iterator(context, Some(IteratorHint::Sync), Some(using_iterator))?; + + // d. Let k be 0. + // e. Repeat, + // i. If k ≥ 2^53 - 1 (MAX_SAFE_INTEGER), then + // ... + // x. Set k to k + 1. + for k in 0..9_007_199_254_740_991_u64 { + // iii. Let next be ? IteratorStep(iteratorRecord). + let next = iterator_record.step(context)?; + + // iv. If next is false, then + let next = if let Some(next) = next { + next + } else { + // 1. Perform ? Set(A, "length", 𝔽(k), true). + a.set("length", k, true, context)?; + + // 2. Return A. + return Ok(a.into()); + }; + + // v. Let nextValue be ? IteratorValue(next). + let next_value = next.value(context)?; + + // vi. If mapping is true, then + let mapped_value = if let Some(mapfn) = mapping { + // 1. Let mappedValue be Call(mapfn, thisArg, « nextValue, 𝔽(k) »). + let mapped_value = mapfn.call(this_arg, &[next_value, k.into()], context); + + // 2. IfAbruptCloseIterator(mappedValue, iteratorRecord). + if_abrupt_close_iterator!(mapped_value, iterator_record, context) + } else { + // vii. Else, let mappedValue be nextValue. + next_value + }; + + // viii. Let defineStatus be CreateDataPropertyOrThrow(A, Pk, mappedValue). + let define_status = a.create_data_property_or_throw(k, mapped_value, context); + + // ix. IfAbruptCloseIterator(defineStatus, iteratorRecord). + if_abrupt_close_iterator!(define_status, iterator_record, context); + } + + // NOTE: The loop above has to return before it reaches iteration limit, + // which is why it's safe to have this as the fallback return + // + // 1. Let error be ThrowCompletion(a newly created TypeError object). + let error = context.throw_type_error("Invalid array length"); + + // 2. Return ? IteratorClose(iteratorRecord, error). + iterator_record.close(error, context) + } else { + // 6. NOTE: items is not an Iterable so assume it is an array-like object. + // 7. Let arrayLike be ! ToObject(items). + let array_like = items + .to_object(context) + .expect("should not fail according to spec"); + + // 8. Let len be ? LengthOfArrayLike(arrayLike). + let len = array_like.length_of_array_like(context)?; + + // 9. If IsConstructor(C) is true, then + // a. Let A be ? Construct(C, « 𝔽(len) »). + // 10. Else, + // a. Let A be ? ArrayCreate(len). + let a = match this.as_constructor() { + Some(constructor) => constructor.construct(&[len.into()], None, context)?, + _ => Self::array_create(len, None, context)?, + }; + + // 11. Let k be 0. + // 12. Repeat, while k < len, + // ... + // f. Set k to k + 1. + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(arrayLike, Pk). + let k_value = array_like.get(k, context)?; + + let mapped_value = if let Some(mapfn) = mapping { + // c. If mapping is true, then + // i. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + mapfn.call(this_arg, &[k_value, k.into()], context)? + } else { + // d. Else, let mappedValue be kValue. + k_value + }; + + // e. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). + a.create_data_property_or_throw(k, mapped_value, context)?; + } + + // 13. Perform ? Set(A, "length", 𝔽(len), true). + a.set("length", len, true, context)?; + + // 14. Return A. + Ok(a.into()) + } + } + + /// `Array.isArray( arg )` + /// + /// The isArray function takes one argument arg, and returns the Boolean value true + /// if the argument is an object whose class internal property is "Array"; otherwise it returns false. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.isarray + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray + pub(crate) fn is_array( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? IsArray(arg). + args.get_or_undefined(0).is_array(context).map(Into::into) + } + + /// `Array.of(...items)` + /// + /// The Array.of method creates a new Array instance from a variable number of arguments, + /// regardless of the number or type of arguments. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.of + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/of + pub(crate) fn of(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let len be the number of elements in items. + // 2. Let lenNumber be 𝔽(len). + let len = args.len(); + + // 3. Let C be the this value. + // 4. If IsConstructor(C) is true, then + // a. Let A be ? Construct(C, « lenNumber »). + // 5. Else, + // a. Let A be ? ArrayCreate(len). + let a = match this.as_constructor() { + Some(constructor) => constructor.construct(&[len.into()], None, context)?, + _ => Self::array_create(len, None, context)?, + }; + + // 6. Let k be 0. + // 7. Repeat, while k < len, + for (k, value) in args.iter().enumerate() { + // a. Let kValue be items[k]. + // b. Let Pk be ! ToString(𝔽(k)). + // c. Perform ? CreateDataPropertyOrThrow(A, Pk, kValue). + a.create_data_property_or_throw(k, value, context)?; + // d. Set k to k + 1. + } + + // 8. Perform ? Set(A, "length", lenNumber, true). + a.set("length", len, true, context)?; + + // 9. Return A. + Ok(a.into()) + } + + ///'Array.prototype.at(index)' + /// + /// The at() method takes an integer value and returns the item at that + /// index, allowing for positive and negative integers. Negative integers + /// count back from the last item in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.at + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at + pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + //1. let O be ? ToObject(this value) + let obj = this.to_object(context)?; + //2. let len be ? LengthOfArrayLike(O) + let len = obj.length_of_array_like(context)? as i64; + //3. let relativeIndex be ? ToIntegerOrInfinity(index) + let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?; + let k = match relative_index { + //4. if relativeIndex >= 0, then let k be relativeIndex + //check if positive and if below length of array + IntegerOrInfinity::Integer(i) if i >= 0 && i < len => i, + //5. Else, let k be len + relativeIndex + //integer should be negative, so abs() and check if less than or equal to length of array + IntegerOrInfinity::Integer(i) if i < 0 && i.abs() <= len => len + i, + //handle most likely impossible case of + //IntegerOrInfinity::NegativeInfinity || IntegerOrInfinity::PositiveInfinity + //by returning undefined + _ => return Ok(JsValue::undefined()), + }; + //6. if k < 0 or k >= len, + //handled by the above match guards + //7. Return ? Get(O, !ToString(𝔽(k))) + obj.get(k, context) + } + + /// `Array.prototype.concat(...arguments)` + /// + /// When the concat method is called with zero or more arguments, it returns an + /// array containing the array elements of the object followed by the array + /// elements of each argument in order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.concat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat + pub(crate) fn concat( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let obj = this.to_object(context)?; + // 2. Let A be ? ArraySpeciesCreate(O, 0). + let arr = Self::array_species_create(&obj, 0, context)?; + // 3. Let n be 0. + let mut n = 0; + // 4. Prepend O to items. + // 5. For each element E of items, do + for item in [JsValue::new(obj)].iter().chain(args.iter()) { + // a. Let spreadable be ? IsConcatSpreadable(E). + let spreadable = Self::is_concat_spreadable(item, context)?; + // b. If spreadable is true, then + if spreadable { + // item is guaranteed to be an object since is_concat_spreadable checks it, + // so we can call `.unwrap()` + let item = item.as_object().expect("guaranteed to be an object"); + // i. Let k be 0. + // ii. Let len be ? LengthOfArrayLike(E). + let len = item.length_of_array_like(context)?; + // iii. If n + len > 2^53 - 1, throw a TypeError exception. + if n + len > Number::MAX_SAFE_INTEGER as usize { + return context.throw_type_error( + "length + number of arguments exceeds the max safe integer limit", + ); + } + // iv. Repeat, while k < len, + for k in 0..len { + // 1. Let P be ! ToString(𝔽(k)). + // 2. Let exists be ? HasProperty(E, P). + let exists = item.has_property(k, context)?; + // 3. If exists is true, then + if exists { + // a. Let subElement be ? Get(E, P). + let sub_element = item.get(k, context)?; + // b. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), subElement). + arr.create_data_property_or_throw(n, sub_element, context)?; + } + // 4. Set n to n + 1. + n += 1; + // 5. Set k to k + 1. + } + } + // c. Else, + else { + // i. NOTE: E is added as a single item rather than spread. + // ii. If n ≥ 2^53 - 1, throw a TypeError exception. + if n >= Number::MAX_SAFE_INTEGER as usize { + return context.throw_type_error("length exceeds the max safe integer limit"); + } + // iii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), E). + arr.create_data_property_or_throw(n, item, context)?; + // iv. Set n to n + 1. + n += 1; + } + } + // 6. Perform ? Set(A, "length", 𝔽(n), true). + arr.set("length", n, true, context)?; + + // 7. Return A. + Ok(JsValue::new(arr)) + } + + /// `Array.prototype.push( ...items )` + /// + /// The arguments are appended to the end of the array, in the order in which + /// they appear. The new length of the array is returned as the result of the + /// call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.push + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push + pub(crate) fn push( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let mut len = o.length_of_array_like(context)? as u64; + // 3. Let argCount be the number of elements in items. + let arg_count = args.len() as u64; + // 4. If len + argCount > 2^53 - 1, throw a TypeError exception. + if len + arg_count > 2u64.pow(53) - 1 { + return context.throw_type_error( + "the length + the number of arguments exceed the maximum safe integer limit", + ); + } + // 5. For each element E of items, do + for element in args.iter().cloned() { + // a. Perform ? Set(O, ! ToString(𝔽(len)), E, true). + o.set(len, element, true, context)?; + // b. Set len to len + 1. + len += 1; + } + // 6. Perform ? Set(O, "length", 𝔽(len), true). + o.set("length", len, true, context)?; + // 7. Return 𝔽(len). + Ok(len.into()) + } + + /// `Array.prototype.pop()` + /// + /// The last element of the array is removed from the array and returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.pop + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop + pub(crate) fn pop(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If len = 0, then + if len == 0 { + // a. Perform ? Set(O, "length", +0𝔽, true). + o.set("length", 0, true, context)?; + // b. Return undefined. + Ok(JsValue::undefined()) + // 4. Else, + } else { + // a. Assert: len > 0. + // b. Let newLen be 𝔽(len - 1). + let new_len = len - 1; + // c. Let index be ! ToString(newLen). + let index = new_len; + // d. Let element be ? Get(O, index). + let element = o.get(index, context)?; + // e. Perform ? DeletePropertyOrThrow(O, index). + o.delete_property_or_throw(index, context)?; + // f. Perform ? Set(O, "length", newLen, true). + o.set("length", new_len, true, context)?; + // g. Return element. + Ok(element) + } + } + + /// `Array.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// This method executes the provided callback function for each element in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach + pub(crate) fn for_each( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.forEach: invalid callback function") + })?; + // 4. Let k be 0. + // 5. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kPresent be ? HasProperty(O, Pk). + let present = o.has_property(pk, context)?; + // c. If kPresent is true, then + if present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // ii. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + let this_arg = args.get_or_undefined(1); + callback.call(this_arg, &[k_value, k.into(), o.clone().into()], context)?; + } + // d. Set k to k + 1. + } + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Array.prototype.join( separator )` + /// + /// The elements of the array are converted to Strings, and these Strings are + /// then concatenated, separated by occurrences of the separator. If no + /// separator is provided, a single comma is used as the separator. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.join + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join + pub(crate) fn join( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If separator is undefined, let sep be the single-element String ",". + // 4. Else, let sep be ? ToString(separator). + let separator = args.get_or_undefined(0); + let separator = if separator.is_undefined() { + JsString::new(",") + } else { + separator.to_string(context)? + }; + + // 5. Let R be the empty String. + let mut r = String::new(); + // 6. Let k be 0. + // 7. Repeat, while k < len, + for k in 0..len { + // a. If k > 0, set R to the string-concatenation of R and sep. + if k > 0 { + r.push_str(&separator); + } + // b. Let element be ? Get(O, ! ToString(𝔽(k))). + let element = o.get(k, context)?; + // c. If element is undefined or null, let next be the empty String; otherwise, let next be ? ToString(element). + let next = if element.is_null_or_undefined() { + JsString::new("") + } else { + element.to_string(context)? + }; + // d. Set R to the string-concatenation of R and next. + r.push_str(&next); + // e. Set k to k + 1. + } + // 8. Return R. + Ok(r.into()) + } + + /// `Array.prototype.toString( separator )` + /// + /// The toString function is intentionally generic; it does not require that + /// its this value be an Array object. Therefore it can be transferred to + /// other kinds of objects for use as a method. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let array be ? ToObject(this value). + let array = this.to_object(context)?; + // 2. Let func be ? Get(array, "join"). + let func = array.get("join", context)?; + // 3. If IsCallable(func) is false, set func to the intrinsic function %Object.prototype.toString%. + // 4. Return ? Call(func, array). + if let Some(func) = func.as_callable() { + func.call(&array.into(), &[], context) + } else { + crate::builtins::object::Object::to_string(&array.into(), &[], context) + } + } + + /// `Array.prototype.reverse()` + /// + /// The elements of the array are rearranged so as to reverse their order. + /// The object is returned as the result of the call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reverse + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse + #[allow(clippy::else_if_without_else)] + pub(crate) fn reverse( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. Let middle be floor(len / 2). + let middle = len / 2; + // 4. Let lower be 0. + let mut lower = 0; + // 5. Repeat, while lower ≠ middle, + while lower != middle { + // a. Let upper be len - lower - 1. + let upper = len - lower - 1; + // Skipped: b. Let upperP be ! ToString(𝔽(upper)). + // Skipped: c. Let lowerP be ! ToString(𝔽(lower)). + // d. Let lowerExists be ? HasProperty(O, lowerP). + let lower_exists = o.has_property(lower, context)?; + // e. If lowerExists is true, then + let mut lower_value = JsValue::undefined(); + if lower_exists { + // i. Let lowerValue be ? Get(O, lowerP). + lower_value = o.get(lower, context)?; + } + // f. Let upperExists be ? HasProperty(O, upperP). + let upper_exists = o.has_property(upper, context)?; + // g. If upperExists is true, then + let mut upper_value = JsValue::undefined(); + if upper_exists { + // i. Let upperValue be ? Get(O, upperP). + upper_value = o.get(upper, context)?; + } + match (lower_exists, upper_exists) { + // h. If lowerExists is true and upperExists is true, then + (true, true) => { + // i. Perform ? Set(O, lowerP, upperValue, true). + o.set(lower, upper_value, true, context)?; + // ii. Perform ? Set(O, upperP, lowerValue, true). + o.set(upper, lower_value, true, context)?; + } + // i. Else if lowerExists is false and upperExists is true, then + (false, true) => { + // i. Perform ? Set(O, lowerP, upperValue, true). + o.set(lower, upper_value, true, context)?; + // ii. Perform ? DeletePropertyOrThrow(O, upperP). + o.delete_property_or_throw(upper, context)?; + } + // j. Else if lowerExists is true and upperExists is false, then + (true, false) => { + // i. Perform ? DeletePropertyOrThrow(O, lowerP). + o.delete_property_or_throw(lower, context)?; + // ii. Perform ? Set(O, upperP, lowerValue, true). + o.set(upper, lower_value, true, context)?; + } + // k. Else, + (false, false) => { + // i. Assert: lowerExists and upperExists are both false. + // ii. No action is required. + } + } + + // l. Set lower to lower + 1. + lower += 1; + } + // 6. Return O. + Ok(o.into()) + } + + /// `Array.prototype.shift()` + /// + /// The first element of the array is removed from the array and returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.shift + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift + pub(crate) fn shift(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If len = 0, then + if len == 0 { + // a. Perform ? Set(O, "length", +0𝔽, true). + o.set("length", 0, true, context)?; + // b. Return undefined. + return Ok(JsValue::undefined()); + } + // 4. Let first be ? Get(O, "0"). + let first = o.get(0, context)?; + // 5. Let k be 1. + // 6. Repeat, while k < len, + for k in 1..len { + // a. Let from be ! ToString(𝔽(k)). + let from = k; + // b. Let to be ! ToString(𝔽(k - 1)). + let to = k - 1; + // c. Let fromPresent be ? HasProperty(O, from). + let from_present = o.has_property(from, context)?; + // d. If fromPresent is true, then + if from_present { + // i. Let fromVal be ? Get(O, from). + let from_val = o.get(from, context)?; + // ii. Perform ? Set(O, to, fromVal, true). + o.set(to, from_val, true, context)?; + // e. Else, + } else { + // i. Assert: fromPresent is false. + // ii. Perform ? DeletePropertyOrThrow(O, to). + o.delete_property_or_throw(to, context)?; + } + // f. Set k to k + 1. + } + // 7. Perform ? DeletePropertyOrThrow(O, ! ToString(𝔽(len - 1))). + o.delete_property_or_throw(len - 1, context)?; + // 8. Perform ? Set(O, "length", 𝔽(len - 1), true). + o.set("length", len - 1, true, context)?; + // 9. Return first. + Ok(first) + } + + /// `Array.prototype.unshift( ...items )` + /// + /// The arguments are prepended to the start of the array, such that their order + /// within the array is the same as the order in which they appear in the + /// argument list. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.unshift + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift + pub(crate) fn unshift( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)? as u64; + // 3. Let argCount be the number of elements in items. + let arg_count = args.len() as u64; + // 4. If argCount > 0, then + if arg_count > 0 { + // a. If len + argCount > 2^53 - 1, throw a TypeError exception. + if len + arg_count > 2u64.pow(53) - 1 { + return context.throw_type_error( + "length + number of arguments exceeds the max safe integer limit", + ); + } + // b. Let k be len. + let mut k = len; + // c. Repeat, while k > 0, + while k > 0 { + // i. Let from be ! ToString(𝔽(k - 1)). + let from = k - 1; + // ii. Let to be ! ToString(𝔽(k + argCount - 1)). + let to = k + arg_count - 1; + // iii. Let fromPresent be ? HasProperty(O, from). + let from_present = o.has_property(from, context)?; + // iv. If fromPresent is true, then + if from_present { + // 1. Let fromValue be ? Get(O, from). + let from_value = o.get(from, context)?; + // 2. Perform ? Set(O, to, fromValue, true). + o.set(to, from_value, true, context)?; + // v. Else, + } else { + // 1. Assert: fromPresent is false. + // 2. Perform ? DeletePropertyOrThrow(O, to). + o.delete_property_or_throw(to, context)?; + } + // vi. Set k to k - 1. + k -= 1; + } + // d. Let j be +0𝔽. + // e. For each element E of items, do + for (j, e) in args.iter().enumerate() { + // i. Perform ? Set(O, ! ToString(j), E, true). + o.set(j, e, true, context)?; + // ii. Set j to j + 1𝔽. + } + } + // 5. Perform ? Set(O, "length", 𝔽(len + argCount), true). + o.set("length", len + arg_count, true, context)?; + // 6. Return 𝔽(len + argCount). + Ok((len + arg_count).into()) + } + + /// `Array.prototype.every( callback, [ thisArg ] )` + /// + /// The every method executes the provided callback function once for each + /// element present in the array until it finds the one where callback returns + /// a falsy value. It returns `false` if it finds such element, otherwise it + /// returns `true`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.every + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every + pub(crate) fn every( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.every: callback is not callable") + })?; + + let this_arg = args.get_or_undefined(1); + + // 4. Let k be 0. + // 5. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(k, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // ii. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + let test_result = callback + .call(this_arg, &[k_value, k.into(), o.clone().into()], context)? + .to_boolean(); + // iii. If testResult is false, return false. + if !test_result { + return Ok(JsValue::new(false)); + } + } + // d. Set k to k + 1. + } + // 6. Return true. + Ok(JsValue::new(true)) + } + + /// `Array.prototype.map( callback, [ thisArg ] )` + /// + /// For each element in the array the callback function is called, and a new + /// array is constructed from the return values of these calls. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.map + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map + pub(crate) fn map( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.map: Callbackfn is not callable") + })?; + + // 4. Let A be ? ArraySpeciesCreate(O, len). + let a = Self::array_species_create(&o, len, context)?; + + let this_arg = args.get_or_undefined(1); + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let k_present be ? HasProperty(O, Pk). + let k_present = o.has_property(k, context)?; + // c. If k_present is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // ii. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + let mapped_value = + callback.call(this_arg, &[k_value, k.into(), this.into()], context)?; + // iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). + a.create_data_property_or_throw(k, mapped_value, context)?; + } + // d. Set k to k + 1. + } + // 7. Return A. + Ok(a.into()) + } + + /// `Array.prototype.indexOf( searchElement[, fromIndex ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.indexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf + pub(crate) fn index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)? as i64; + + // 3. If len is 0, return -1𝔽. + if len == 0 { + return Ok(JsValue::new(-1)); + } + + // 4. Let n be ? ToIntegerOrInfinity(fromIndex). + let n = args + .get(1) + .cloned() + .unwrap_or_default() + .to_integer_or_infinity(context)?; + // 5. Assert: If fromIndex is undefined, then n is 0. + let n = match n { + // 6. If n is +∞, return -1𝔽. + IntegerOrInfinity::PositiveInfinity => return Ok(JsValue::new(-1)), + // 7. Else if n is -∞, set n to 0. + IntegerOrInfinity::NegativeInfinity => 0, + IntegerOrInfinity::Integer(value) => value, + }; + + // 8. If n ≥ 0, then + let mut k; + if n >= 0 { + // a. Let k be n. + k = n; + // 9. Else, + } else { + // a. Let k be len + n. + k = len + n; + // b. If k < 0, set k to 0. + if k < 0 { + k = 0; + } + }; + + let search_element = args.get_or_undefined(0); + + // 10. Repeat, while k < len, + while k < len { + // a. Let kPresent be ? HasProperty(O, ! ToString(𝔽(k))). + let k_present = o.has_property(k, context)?; + // b. If kPresent is true, then + if k_present { + // i. Let elementK be ? Get(O, ! ToString(𝔽(k))). + let element_k = o.get(k, context)?; + // ii. Let same be IsStrictlyEqual(searchElement, elementK). + // iii. If same is true, return 𝔽(k). + if search_element.strict_equals(&element_k) { + return Ok(JsValue::new(k)); + } + } + // c. Set k to k + 1. + k += 1; + } + // 11. Return -1𝔽. + Ok(JsValue::new(-1)) + } + + /// `Array.prototype.lastIndexOf( searchElement[, fromIndex ] )` + /// + /// + /// `lastIndexOf` compares searchElement to the elements of the array in descending order + /// using the Strict Equality Comparison algorithm, and if found at one or more indices, + /// returns the largest such index; otherwise, -1 is returned. + /// + /// The optional second argument fromIndex defaults to the array's length minus one + /// (i.e. the whole array is searched). If it is greater than or equal to the length of the array, + /// the whole array will be searched. If it is negative, it is used as the offset from the end + /// of the array to compute fromIndex. If the computed index is less than 0, -1 is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.lastindexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf + pub(crate) fn last_index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)? as i64; + + // 3. If len is 0, return -1𝔽. + if len == 0 { + return Ok(JsValue::new(-1)); + } + + // 4. If fromIndex is present, let n be ? ToIntegerOrInfinity(fromIndex); else let n be len - 1. + let n = if let Some(from_index) = args.get(1) { + from_index.to_integer_or_infinity(context)? + } else { + IntegerOrInfinity::Integer(len - 1) + }; + + let mut k = match n { + // 5. If n is -∞, return -1𝔽. + IntegerOrInfinity::NegativeInfinity => return Ok(JsValue::new(-1)), + // 6. If n ≥ 0, then + // a. Let k be min(n, len - 1). + IntegerOrInfinity::Integer(n) if n >= 0 => min(n, len - 1), + IntegerOrInfinity::PositiveInfinity => len - 1, + // 7. Else, + // a. Let k be len + n. + IntegerOrInfinity::Integer(n) => len + n, + }; + + let search_element = args.get_or_undefined(0); + + // 8. Repeat, while k ≥ 0, + while k >= 0 { + // a. Let kPresent be ? HasProperty(O, ! ToString(𝔽(k))). + let k_present = o.has_property(k, context)?; + // b. If kPresent is true, then + if k_present { + // i. Let elementK be ? Get(O, ! ToString(𝔽(k))). + let element_k = o.get(k, context)?; + // ii. Let same be IsStrictlyEqual(searchElement, elementK). + // iii. If same is true, return 𝔽(k). + if JsValue::strict_equals(search_element, &element_k) { + return Ok(JsValue::new(k)); + } + } + // c. Set k to k - 1. + k -= 1; + } + // 9. Return -1𝔽. + Ok(JsValue::new(-1)) + } + + /// `Array.prototype.find( callback, [thisArg] )` + /// + /// The find method executes the callback function once for each index of the array + /// until the callback returns a truthy value. If so, find immediately returns the value + /// of that element. Otherwise, find returns undefined. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.find + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find + pub(crate) fn find( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.find: predicate is not callable") + })?; + + let this_arg = args.get_or_undefined(1); + + // 4. Let k be 0. + let mut k = 0; + // 5. Repeat, while k < len, + while k < len { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + let test_result = predicate + .call( + this_arg, + &[k_value.clone(), k.into(), o.clone().into()], + context, + )? + .to_boolean(); + // d. If testResult is true, return kValue. + if test_result { + return Ok(k_value); + } + // e. Set k to k + 1. + k += 1; + } + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Array.prototype.findIndex( predicate [ , thisArg ] )` + /// + /// This method executes the provided predicate function for each element of the array. + /// If the predicate function returns `true` for an element, this method returns the index of the element. + /// If all elements return `false`, the value `-1` is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.findindex + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex + pub(crate) fn find_index( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.findIndex: predicate is not callable") + })?; + + let this_arg = args.get_or_undefined(1); + + // 4. Let k be 0. + let mut k = 0; + // 5. Repeat, while k < len, + while k < len { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + let test_result = predicate + .call(this_arg, &[k_value, k.into(), o.clone().into()], context)? + .to_boolean(); + // d. If testResult is true, return 𝔽(k). + if test_result { + return Ok(JsValue::new(k)); + } + // e. Set k to k + 1. + k += 1; + } + // 6. Return -1𝔽. + Ok(JsValue::new(-1)) + } + + /// `Array.prototype.findLast( predicate, [thisArg] )` + /// + /// findLast calls predicate once for each element of the array, in descending order, + /// until it finds one where predicate returns true. If such an element is found, findLast + /// immediately returns that element value. Otherwise, findLast returns undefined. + /// + /// More information: + /// - [ECMAScript proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-array-find-from-last/#sec-array.prototype.findlast + pub(crate) fn find_last( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.findLast: predicate is not callable") + })?; + + let this_arg = args.get_or_undefined(1); + + // 4. Let k be len - 1. (implementation differs slightly from spec because k is unsigned) + // 5. Repeat, while k >= 0, (implementation differs slightly from spec because k is unsigned) + for k in (0..len).rev() { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + let test_result = predicate + .call( + this_arg, + &[k_value.clone(), k.into(), this.clone()], + context, + )? + .to_boolean(); + // d. If testResult is true, return kValue. + if test_result { + return Ok(k_value); + } + // e. Set k to k - 1. + } + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Array.prototype.findLastIndex( predicate [ , thisArg ] )` + /// + /// `findLastIndex` calls predicate once for each element of the array, in descending order, + /// until it finds one where predicate returns true. If such an element is found, `findLastIndex` + /// immediately returns the index of that element value. Otherwise, `findLastIndex` returns -1. + /// + /// More information: + /// - [ECMAScript proposal][spec] + /// + /// [spec]: https://tc39.es/proposal-array-find-from-last/#sec-array.prototype.findlastindex + pub(crate) fn find_last_index( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.findLastIndex: predicate is not callable") + })?; + + let this_arg = args.get_or_undefined(1); + + // 4. Let k be len - 1. (implementation differs slightly from spec because k is unsigned) + // 5. Repeat, while k >= 0, (implementation differs slightly from spec because k is unsigned) + for k in (0..len).rev() { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + let test_result = predicate + .call(this_arg, &[k_value, k.into(), this.clone()], context)? + .to_boolean(); + // d. If testResult is true, return 𝔽(k). + if test_result { + return Ok(JsValue::new(k)); + } + // e. Set k to k - 1. + } + // 6. Return -1𝔽. + Ok(JsValue::new(-1)) + } + + /// `Array.prototype.flat( [depth] )` + /// + /// This method creates a new array with all sub-array elements concatenated into it + /// recursively up to the specified depth. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.flat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat + pub(crate) fn flat( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ToObject(this value) + let o = this.to_object(context)?; + + // 2. Let sourceLen be LengthOfArrayLike(O) + let source_len = o.length_of_array_like(context)?; + + // 3. Let depthNum be 1 + let mut depth_num = 1; + + // 4. If depth is not undefined, then set depthNum to IntegerOrInfinity(depth) + if let Some(depth) = args.get(0) { + // a. Set depthNum to ? ToIntegerOrInfinity(depth). + // b. If depthNum < 0, set depthNum to 0. + match depth.to_integer_or_infinity(context)? { + IntegerOrInfinity::Integer(value) if value >= 0 => depth_num = value as u64, + IntegerOrInfinity::PositiveInfinity => depth_num = u64::MAX, + _ => depth_num = 0, + } + }; + + // 5. Let A be ArraySpeciesCreate(O, 0) + let a = Self::array_species_create(&o, 0, context)?; + + // 6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum) + Self::flatten_into_array( + &a, + &o, + source_len as u64, + 0, + depth_num, + None, + &JsValue::undefined(), + context, + )?; + + Ok(a.into()) + } + + /// `Array.prototype.flatMap( callback, [ thisArg ] )` + /// + /// This method returns a new array formed by applying a given callback function to + /// each element of the array, and then flattening the result by one level. It is + /// identical to a `map()` followed by a `flat()` of depth 1, but slightly more + /// efficient than calling those two methods separately. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.flatMap + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap + pub(crate) fn flat_map( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ToObject(this value) + let o = this.to_object(context)?; + + // 2. Let sourceLen be LengthOfArrayLike(O) + let source_len = o.length_of_array_like(context)?; + + // 3. If ! IsCallable(mapperFunction) is false, throw a TypeError exception. + let mapper_function = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("flatMap mapper function is not callable") + })?; + + // 4. Let A be ? ArraySpeciesCreate(O, 0). + let a = Self::array_species_create(&o, 0, context)?; + + // 5. Perform ? FlattenIntoArray(A, O, sourceLen, 0, 1, mapperFunction, thisArg). + Self::flatten_into_array( + &a, + &o, + source_len as u64, + 0, + 1, + Some(mapper_function), + args.get_or_undefined(1), + context, + )?; + + // 6. Return A + Ok(a.into()) + } + + /// Abstract method `FlattenIntoArray`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-flattenintoarray + #[allow(clippy::too_many_arguments)] + fn flatten_into_array( + target: &JsObject, + source: &JsObject, + source_len: u64, + start: u64, + depth: u64, + mapper_function: Option<&JsObject>, + this_arg: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Assert target is Object + // 2. Assert source is Object + + // 3. Assert if mapper_function is present, then: + // - IsCallable(mapper_function) is true + // - thisArg is present + // - depth is 1 + + // 4. Let targetIndex be start + let mut target_index = start; + + // 5. Let sourceIndex be 0 + let mut source_index = 0; + + // 6. Repeat, while R(sourceIndex) < sourceLen + while source_index < source_len { + // a. Let P be ToString(sourceIndex) + let p = source_index; + + // b. Let exists be ? HasProperty(source, P). + let exists = source.has_property(p, context)?; + // c. If exists is true, then + if exists { + // i. Let element be Get(source, P) + let mut element = source.get(p, context)?; + + // ii. If mapperFunction is present, then + if let Some(mapper_function) = mapper_function { + // 1. Set element to ? Call(mapperFunction, thisArg, <>) + element = mapper_function.call( + this_arg, + &[element, source_index.into(), source.clone().into()], + context, + )?; + } + + // iii. Let shouldFlatten be false + let mut should_flatten = false; + + // iv. If depth > 0, then + if depth > 0 { + // 1. Set shouldFlatten to ? IsArray(element). + should_flatten = element.is_array(context)?; + } + + // v. If shouldFlatten is true + if should_flatten { + // For `should_flatten` to be true, element must be an object. + let element = element.as_object().expect("must be an object"); + + // 1. If depth is +Infinity let newDepth be +Infinity + let new_depth = if depth == u64::MAX { + u64::MAX + // 2. Else, let newDepth be depth - 1 + } else { + depth - 1 + }; + + // 3. Let elementLen be ? LengthOfArrayLike(element) + let element_len = element.length_of_array_like(context)?; + + // 4. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, newDepth) + target_index = Self::flatten_into_array( + target, + element, + element_len as u64, + target_index, + new_depth, + None, + &JsValue::undefined(), + context, + )?; + + // vi. Else + } else { + // 1. If targetIndex >= 2^53 - 1, throw a TypeError exception + if target_index >= Number::MAX_SAFE_INTEGER as u64 { + return context + .throw_type_error("Target index exceeded max safe integer value"); + } + + // 2. Perform ? CreateDataPropertyOrThrow(target, targetIndex, element) + target.create_data_property_or_throw(target_index, element, context)?; + + // 3. Set targetIndex to targetIndex + 1 + target_index += 1; + } + } + // d. Set sourceIndex to sourceIndex + 1 + source_index += 1; + } + + // 7. Return targetIndex + Ok(target_index) + } + + /// `Array.prototype.fill( value[, start[, end]] )` + /// + /// The method fills (modifies) all the elements of an array from start index (default 0) + /// to an end index (default array length) with a static value. It returns the modified array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.fill + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill + pub(crate) fn fill( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. Let relativeStart be ? ToIntegerOrInfinity(start). + // 4. If relativeStart is -∞, let k be 0. + // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0). + // 6. Else, let k be min(relativeStart, len). + let mut k = Self::get_relative_start(context, args.get(1), len)?; + + // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + // 8. If relativeEnd is -∞, let final be 0. + // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + // 10. Else, let final be min(relativeEnd, len). + let final_ = Self::get_relative_end(context, args.get(2), len)?; + + let value = args.get_or_undefined(0); + + // 11. Repeat, while k < final, + while k < final_ { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Perform ? Set(O, Pk, value, true). + o.set(pk, value.clone(), true, context)?; + // c. Set k to k + 1. + k += 1; + } + // 12. Return O. + Ok(o.into()) + } + + /// `Array.prototype.includes( valueToFind [, fromIndex] )` + /// + /// Determines whether an array includes a certain value among its entries, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.includes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes + pub(crate) fn includes_value( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)? as i64; + + // 3. If len is 0, return false. + if len == 0 { + return Ok(JsValue::new(false)); + } + + // 4. Let n be ? ToIntegerOrInfinity(fromIndex). + let n = args + .get(1) + .cloned() + .unwrap_or_default() + .to_integer_or_infinity(context)?; + // 5. Assert: If fromIndex is undefined, then n is 0. + // 6. If n is +∞, return false. + // 7. Else if n is -∞, set n to 0. + let n = match n { + IntegerOrInfinity::PositiveInfinity => return Ok(JsValue::new(false)), + IntegerOrInfinity::NegativeInfinity => 0, + IntegerOrInfinity::Integer(value) => value, + }; + + // 8. If n ≥ 0, then + let mut k; + if n >= 0 { + // a. Let k be n. + k = n; + // 9. Else, + } else { + // a. Let k be len + n. + k = len + n; + // b. If k < 0, set k to 0. + if k < 0 { + k = 0; + } + } + + let search_element = args.get_or_undefined(0); + + // 10. Repeat, while k < len, + while k < len { + // a. Let elementK be ? Get(O, ! ToString(𝔽(k))). + let element_k = o.get(k, context)?; + // b. If SameValueZero(searchElement, elementK) is true, return true. + if JsValue::same_value_zero(search_element, &element_k) { + return Ok(JsValue::new(true)); + } + // c. Set k to k + 1. + k += 1; + } + // 11. Return false. + Ok(JsValue::new(false)) + } + + /// `Array.prototype.slice( [begin[, end]] )` + /// + /// The slice method takes two arguments, start and end, and returns an array containing the + /// elements of the array from element start up to, but not including, element end (or through the + /// end of the array if end is undefined). If start is negative, it is treated as length + start + /// where length is the length of the array. If end is negative, it is treated as length + end where + /// length is the length of the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.slice + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice + pub(crate) fn slice( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. Let relativeStart be ? ToIntegerOrInfinity(start). + // 4. If relativeStart is -∞, let k be 0. + // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0). + // 6. Else, let k be min(relativeStart, len). + let mut k = Self::get_relative_start(context, args.get(0), len)?; + + // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + // 8. If relativeEnd is -∞, let final be 0. + // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + // 10. Else, let final be min(relativeEnd, len). + let final_ = Self::get_relative_end(context, args.get(1), len)?; + + // 11. Let count be max(final - k, 0). + let count = final_.saturating_sub(k); + + // 12. Let A be ? ArraySpeciesCreate(O, count). + let a = Self::array_species_create(&o, count, context)?; + + // 13. Let n be 0. + let mut n: u64 = 0; + // 14. Repeat, while k < final, + while k < final_ { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(pk, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), kValue). + a.create_data_property_or_throw(n, k_value, context)?; + } + // d. Set k to k + 1. + k += 1; + // e. Set n to n + 1. + n += 1; + } + + // 15. Perform ? Set(A, "length", 𝔽(n), true). + a.set("length", n, true, context)?; + + // 16. Return A. + Ok(a.into()) + } + + /// `Array.prototype.splice ( start, [deleteCount[, ...items]] )` + /// + /// Splices an array by following + /// The deleteCount elements of the array starting at integer index start are replaced by the elements of items. + /// An Array object containing the deleted elements (if any) is returned. + pub(crate) fn splice( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + let start = args.get(0); + let delete_count = args.get(1); + let items = args.get(2..).unwrap_or(&[]); + // 3. Let relativeStart be ? ToIntegerOrInfinity(start). + // 4. If relativeStart is -∞, let actualStart be 0. + // 5. Else if relativeStart < 0, let actualStart be max(len + relativeStart, 0). + // 6. Else, let actualStart be min(relativeStart, len). + let actual_start = Self::get_relative_start(context, start, len)?; + // 7. If start is not present, then + let insert_count = if start.is_none() || delete_count.is_none() { + // 7a. Let insertCount be 0. + // 8. Else if deleteCount is not present, then + // a. Let insertCount be 0. + 0 + // 9. Else, + } else { + // 9a. Let insertCount be the number of elements in items. + items.len() + }; + let actual_delete_count = if start.is_none() { + // 7b. Let actualDeleteCount be 0. + 0 + // 8. Else if deleteCount is not present, then + } else if delete_count.is_none() { + // 8b. Let actualDeleteCount be len - actualStart. + len - actual_start + // 9. Else, + } else { + // b. Let dc be ? ToIntegerOrInfinity(deleteCount). + let dc = delete_count + .cloned() + .unwrap_or_default() + .to_integer_or_infinity(context)?; + // c. Let actualDeleteCount be the result of clamping dc between 0 and len - actualStart. + let max = len - actual_start; + match dc { + IntegerOrInfinity::Integer(i) => { + usize::try_from(i).unwrap_or_default().clamp(0, max) + } + IntegerOrInfinity::PositiveInfinity => max, + IntegerOrInfinity::NegativeInfinity => 0, + } + }; + + // 10. If len + insertCount - actualDeleteCount > 2^53 - 1, throw a TypeError exception. + if len + insert_count - actual_delete_count > Number::MAX_SAFE_INTEGER as usize { + return context.throw_type_error("Target splice exceeded max safe integer value"); + } + + // 11. Let A be ? ArraySpeciesCreate(O, actualDeleteCount). + let arr = Self::array_species_create(&o, actual_delete_count, context)?; + // 12. Let k be 0. + // 13. Repeat, while k < actualDeleteCount, + for k in 0..actual_delete_count { + // a. Let from be ! ToString(𝔽(actualStart + k)). + // b. Let fromPresent be ? HasProperty(O, from). + let from_present = o.has_property(actual_start + k, context)?; + // c. If fromPresent is true, then + if from_present { + // i. Let fromValue be ? Get(O, from). + let from_value = o.get(actual_start + k, context)?; + // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(k)), fromValue). + arr.create_data_property_or_throw(k, from_value, context)?; + } + // d. Set k to k + 1. + } + + // 14. Perform ? Set(A, "length", 𝔽(actualDeleteCount), true). + arr.set("length", actual_delete_count, true, context)?; + + // 15. Let itemCount be the number of elements in items. + let item_count = items.len(); + + match item_count.cmp(&actual_delete_count) { + // 16. If itemCount < actualDeleteCount, then + Ordering::Less => { + // a. Set k to actualStart. + // b. Repeat, while k < (len - actualDeleteCount), + for k in actual_start..(len - actual_delete_count) { + // i. Let from be ! ToString(𝔽(k + actualDeleteCount)). + let from = k + actual_delete_count; + // ii. Let to be ! ToString(𝔽(k + itemCount)). + let to = k + item_count; + // iii. Let fromPresent be ? HasProperty(O, from). + let from_present = o.has_property(from, context)?; + // iv. If fromPresent is true, then + if from_present { + // 1. Let fromValue be ? Get(O, from). + let from_value = o.get(from, context)?; + // 2. Perform ? Set(O, to, fromValue, true). + o.set(to, from_value, true, context)?; + // v. Else, + } else { + // 1. Assert: fromPresent is false. + debug_assert!(!from_present); + // 2. Perform ? DeletePropertyOrThrow(O, to). + o.delete_property_or_throw(to, context)?; + } + // vi. Set k to k + 1. + } + // c. Set k to len. + // d. Repeat, while k > (len - actualDeleteCount + itemCount), + for k in ((len - actual_delete_count + item_count)..len).rev() { + // i. Perform ? DeletePropertyOrThrow(O, ! ToString(𝔽(k - 1))). + o.delete_property_or_throw(k, context)?; + // ii. Set k to k - 1. + } + } + // 17. Else if itemCount > actualDeleteCount, then + Ordering::Greater => { + // a. Set k to (len - actualDeleteCount). + // b. Repeat, while k > actualStart, + for k in (actual_start..len - actual_delete_count).rev() { + // i. Let from be ! ToString(𝔽(k + actualDeleteCount - 1)). + let from = k + actual_delete_count; + // ii. Let to be ! ToString(𝔽(k + itemCount - 1)). + let to = k + item_count; + // iii. Let fromPresent be ? HasProperty(O, from). + let from_present = o.has_property(from, context)?; + // iv. If fromPresent is true, then + if from_present { + // 1. Let fromValue be ? Get(O, from). + let from_value = o.get(from, context)?; + // 2. Perform ? Set(O, to, fromValue, true). + o.set(to, from_value, true, context)?; + // v. Else, + } else { + // 1. Assert: fromPresent is false. + debug_assert!(!from_present); + // 2. Perform ? DeletePropertyOrThrow(O, to). + o.delete_property_or_throw(to, context)?; + } + // vi. Set k to k - 1. + } + } + Ordering::Equal => {} + }; + + // 18. Set k to actualStart. + // 19. For each element E of items, do + if item_count > 0 { + for (k, item) in items + .iter() + .enumerate() + .map(|(i, val)| (i + actual_start, val)) + { + // a. Perform ? Set(O, ! ToString(𝔽(k)), E, true). + o.set(k, item, true, context)?; + // b. Set k to k + 1. + } + } + + // 20. Perform ? Set(O, "length", 𝔽(len - actualDeleteCount + itemCount), true). + o.set( + "length", + len - actual_delete_count + item_count, + true, + context, + )?; + + // 21. Return A. + Ok(JsValue::from(arr)) + } + + /// `Array.prototype.filter( callback, [ thisArg ] )` + /// + /// For each element in the array the callback function is called, and a new + /// array is constructed for every value whose callback returned a truthy value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.filter + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter + pub(crate) fn filter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let length = o.length_of_array_like(context)?; + + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.filter: `callback` must be callable") + })?; + let this_arg = args.get_or_undefined(1); + + // 4. Let A be ? ArraySpeciesCreate(O, 0). + let a = Self::array_species_create(&o, 0, context)?; + + // 5. Let k be 0. + // 6. Let to be 0. + let mut to = 0u32; + // 7. Repeat, while k < len, + for idx in 0..length { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kPresent be ? HasProperty(O, Pk). + // c. If kPresent is true, then + if o.has_property(idx, context)? { + // i. Let kValue be ? Get(O, Pk). + let element = o.get(idx, context)?; + + let args = [element.clone(), JsValue::new(idx), JsValue::new(o.clone())]; + + // ii. Let selected be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + let selected = callback.call(this_arg, &args, context)?.to_boolean(); + + // iii. If selected is true, then + if selected { + // 1. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(to)), kValue). + a.create_data_property_or_throw(to, element, context)?; + // 2. Set to to to + 1. + to += 1; + } + } + } + + // 8. Return A. + Ok(a.into()) + } + + /// Array.prototype.some ( callbackfn [ , thisArg ] ) + /// + /// The some method tests whether at least one element in the array passes + /// the test implemented by the provided callback function. It returns a Boolean value, + /// true if the callback function returns a truthy value for at least one element + /// in the array. Otherwise, false. + /// + /// Caution: Calling this method on an empty array returns false for any condition! + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.some + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some + pub(crate) fn some( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error("Array.prototype.some: callback is not callable") + })?; + + // 4. Let k be 0. + // 5. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(k, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // ii. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + let this_arg = args.get_or_undefined(1); + let test_result = callback + .call(this_arg, &[k_value, k.into(), o.clone().into()], context)? + .to_boolean(); + // iii. If testResult is true, return true. + if test_result { + return Ok(JsValue::new(true)); + } + } + // d. Set k to k + 1. + } + // 6. Return false. + Ok(JsValue::new(false)) + } + + /// Array.prototype.sort ( comparefn ) + /// + /// The sort method sorts the elements of an array in place and returns the sorted array. + /// The default sort order is ascending, built upon converting the elements into strings, + /// then comparing their sequences of UTF-16 code units values. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.sort + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort + pub(crate) fn sort( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + let comparefn = match args.get_or_undefined(0) { + JsValue::Object(ref obj) if obj.is_callable() => Some(obj), + JsValue::Undefined => None, + _ => { + return context.throw_type_error( + "The comparison function must be either a function or undefined", + ) + } + }; + + // Abstract method `SortCompare`. + // + // More information: + // - [ECMAScript reference][spec] + // + // [spec]: https://tc39.es/ecma262/#sec-sortcompare + let sort_compare = + |x: &JsValue, y: &JsValue, context: &mut Context| -> JsResult { + match (x.is_undefined(), y.is_undefined()) { + // 1. If x and y are both undefined, return +0𝔽. + (true, true) => return Ok(Ordering::Equal), + // 2. If x is undefined, return 1𝔽. + (true, false) => return Ok(Ordering::Greater), + // 3. If y is undefined, return -1𝔽. + (false, true) => return Ok(Ordering::Less), + _ => {} + } + + // 4. If comparefn is not undefined, then + if let Some(cmp) = comparefn { + let args = [x.clone(), y.clone()]; + // a. Let v be ? ToNumber(? Call(comparefn, undefined, « x, y »)). + let v = cmp + .call(&JsValue::Undefined, &args, context)? + .to_number(context)?; + // b. If v is NaN, return +0𝔽. + // c. Return v. + return Ok(v.partial_cmp(&0.0).unwrap_or(Ordering::Equal)); + } + // 5. Let xString be ? ToString(x). + // 6. Let yString be ? ToString(y). + let x_str = x.to_string(context)?; + let y_str = y.to_string(context)?; + + // 7. Let xSmaller be IsLessThan(xString, yString, true). + // 8. If xSmaller is true, return -1𝔽. + // 9. Let ySmaller be IsLessThan(yString, xString, true). + // 10. If ySmaller is true, return 1𝔽. + // 11. Return +0𝔽. + + // NOTE: skipped IsLessThan because it just makes a lexicographic comparison + // when x and y are strings + Ok(x_str.cmp(&y_str)) + }; + + // 2. Let obj be ? ToObject(this value). + let obj = this.to_object(context)?; + + // 3. Let len be ? LengthOfArrayLike(obj). + let length = obj.length_of_array_like(context)?; + + // 4. Let items be a new empty List. + let mut items = Vec::with_capacity(length); + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..length { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kPresent be ? HasProperty(obj, Pk). + // c. If kPresent is true, then + if obj.has_property(k, context)? { + // i. Let kValue be ? Get(obj, Pk). + let kval = obj.get(k, context)?; + // ii. Append kValue to items. + items.push(kval); + } + // d. Set k to k + 1. + } + + // 7. Let itemCount be the number of elements in items. + let item_count = items.len(); + + // 8. Sort items using an implementation-defined sequence of calls to SortCompare. + // If any such call returns an abrupt completion, stop before performing any further + // calls to SortCompare or steps in this algorithm and return that completion. + let mut sort_err = Ok(()); + items.sort_by(|x, y| { + if sort_err.is_ok() { + sort_compare(x, y, context).unwrap_or_else(|err| { + sort_err = Err(err); + Ordering::Equal + }) + } else { + Ordering::Equal + } + }); + sort_err?; + + // 9. Let j be 0. + // 10. Repeat, while j < itemCount, + for (j, item) in items.into_iter().enumerate() { + // a. Perform ? Set(obj, ! ToString(𝔽(j)), items[j], true). + obj.set(j, item, true, context)?; + // b. Set j to j + 1. + } + + // 11. Repeat, while j < len, + for j in item_count..length { + // a. Perform ? DeletePropertyOrThrow(obj, ! ToString(𝔽(j))). + obj.delete_property_or_throw(j, context)?; + // b. Set j to j + 1. + } + + // 12. Return obj. + Ok(obj.into()) + } + + /// `Array.prototype.reduce( callbackFn [ , initialValue ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduce + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce + pub(crate) fn reduce( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context + .construct_type_error("Array.prototype.reduce: callback function is not callable") + })?; + + // 4. If len = 0 and initialValue is not present, throw a TypeError exception. + if len == 0 && args.get(1).is_none() { + return context.throw_type_error( + "Array.prototype.reduce: called on an empty array and with no initial value", + ); + } + + // 5. Let k be 0. + let mut k = 0; + // 6. Let accumulator be undefined. + let mut accumulator = JsValue::undefined(); + + // 7. If initialValue is present, then + if let Some(initial_value) = args.get(1) { + // a. Set accumulator to initialValue. + accumulator = initial_value.clone(); + // 8. Else, + } else { + // a. Let kPresent be false. + let mut k_present = false; + // b. Repeat, while kPresent is false and k < len, + while !k_present && k < len { + // i. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // ii. Set kPresent to ? HasProperty(O, Pk). + k_present = o.has_property(pk, context)?; + // iii. If kPresent is true, then + if k_present { + // 1. Set accumulator to ? Get(O, Pk). + accumulator = o.get(pk, context)?; + } + // iv. Set k to k + 1. + k += 1; + } + // c. If kPresent is false, throw a TypeError exception. + if !k_present { + return context.throw_type_error( + "Array.prototype.reduce: called on an empty array and with no initial value", + ); + } + } + + // 9. Repeat, while k < len, + while k < len { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(pk, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // ii. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »). + accumulator = callback.call( + &JsValue::undefined(), + &[accumulator, k_value, k.into(), o.clone().into()], + context, + )?; + } + // d. Set k to k + 1. + k += 1; + } + + // 10. Return accumulator. + Ok(accumulator) + } + + /// `Array.prototype.reduceRight( callbackFn [ , initialValue ] )` + /// + /// The reduceRight method traverses right to left starting from the last defined value in the array, + /// accumulating a value using a given callback function. It returns the accumulated value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduceright + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight + pub(crate) fn reduce_right( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| { + context.construct_type_error( + "Array.prototype.reduceRight: callback function is not callable", + ) + })?; + + // 4. If len is 0 and initialValue is not present, throw a TypeError exception. + if len == 0 && args.get(1).is_none() { + return context.throw_type_error( + "Array.prototype.reduceRight: called on an empty array and with no initial value", + ); + } + + // 5. Let k be len - 1. + let mut k = len as i64 - 1; + // 6. Let accumulator be undefined. + let mut accumulator = JsValue::undefined(); + // 7. If initialValue is present, then + if let Some(initial_value) = args.get(1) { + // a. Set accumulator to initialValue. + accumulator = initial_value.clone(); + // 8. Else, + } else { + // a. Let kPresent be false. + let mut k_present = false; + // b. Repeat, while kPresent is false and k ≥ 0, + while !k_present && k >= 0 { + // i. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // ii. Set kPresent to ? HasProperty(O, Pk). + k_present = o.has_property(pk, context)?; + // iii. If kPresent is true, then + if k_present { + // 1. Set accumulator to ? Get(O, Pk). + accumulator = o.get(pk, context)?; + } + // iv. Set k to k - 1. + k -= 1; + } + // c. If kPresent is false, throw a TypeError exception. + if !k_present { + return context.throw_type_error( + "Array.prototype.reduceRight: called on an empty array and with no initial value", + ); + } + } + + // 9. Repeat, while k ≥ 0, + while k >= 0 { + // a. Let Pk be ! ToString(𝔽(k)). + let pk = k; + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(pk, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(pk, context)?; + // ii. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »). + accumulator = callback.call( + &JsValue::undefined(), + &[accumulator.clone(), k_value, k.into(), o.clone().into()], + context, + )?; + } + // d. Set k to k - 1. + k -= 1; + } + + // 10. Return accumulator. + Ok(accumulator) + } + + /// `Array.prototype.copyWithin ( target, start [ , end ] )` + /// + /// The copyWithin() method shallow copies part of an array to another location + /// in the same array and returns it without modifying its length. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.copywithin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin + pub(crate) fn copy_within( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let len be ? LengthOfArrayLike(O). + let len = o.length_of_array_like(context)?; + + // 3. Let relativeTarget be ? ToIntegerOrInfinity(target). + // 4. If relativeTarget is -∞, let to be 0. + // 5. Else if relativeTarget < 0, let to be max(len + relativeTarget, 0). + // 6. Else, let to be min(relativeTarget, len). + let mut to = Self::get_relative_start(context, args.get(0), len)? as i64; + + // 7. Let relativeStart be ? ToIntegerOrInfinity(start). + // 8. If relativeStart is -∞, let from be 0. + // 9. Else if relativeStart < 0, let from be max(len + relativeStart, 0). + // 10. Else, let from be min(relativeStart, len). + let mut from = Self::get_relative_start(context, args.get(1), len)? as i64; + + // 11. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + // 12. If relativeEnd is -∞, let final be 0. + // 13. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + // 14. Else, let final be min(relativeEnd, len). + let final_ = Self::get_relative_end(context, args.get(2), len)? as i64; + + // 15. Let count be min(final - from, len - to). + let mut count = min(final_ - from, len as i64 - to); + + // 16. If from < to and to < from + count, then + let direction = if from < to && to < from + count { + // b. Set from to from + count - 1. + from = from + count - 1; + // c. Set to to to + count - 1. + to = to + count - 1; + + // a. Let direction be -1. + -1 + // 17. Else, + } else { + // a. Let direction be 1. + 1 + }; + + // 18. Repeat, while count > 0, + while count > 0 { + // a. Let fromKey be ! ToString(𝔽(from)). + let from_key = from; + + // b. Let toKey be ! ToString(𝔽(to)). + let to_key = to; + + // c. Let fromPresent be ? HasProperty(O, fromKey). + let from_present = o.has_property(from_key, context)?; + // d. If fromPresent is true, then + if from_present { + // i. Let fromVal be ? Get(O, fromKey). + let from_val = o.get(from_key, context)?; + // ii. Perform ? Set(O, toKey, fromVal, true). + o.set(to_key, from_val, true, context)?; + // e. Else, + } else { + // i. Assert: fromPresent is false. + // ii. Perform ? DeletePropertyOrThrow(O, toKey). + o.delete_property_or_throw(to_key, context)?; + } + // f. Set from to from + direction. + from += direction; + // g. Set to to to + direction. + to += direction; + // h. Set count to count - 1. + count -= 1; + } + // 19. Return O. + Ok(o.into()) + } + + /// `Array.prototype.values( )` + /// + /// The values method returns an iterable that iterates over the values in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn values( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Return CreateArrayIterator(O, value). + Ok(ArrayIterator::create_array_iterator( + o, + PropertyNameKind::Value, + context, + )) + } + + /// `Array.prototype.keys( )` + /// + /// The keys method returns an iterable that iterates over the indexes in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.keys + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Return CreateArrayIterator(O, key). + Ok(ArrayIterator::create_array_iterator( + o, + PropertyNameKind::Key, + context, + )) + } + + /// `Array.prototype.entries( )` + /// + /// The entries method returns an iterable that iterates over the key-value pairs in the array. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values + pub(crate) fn entries( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Return CreateArrayIterator(O, key+value). + Ok(ArrayIterator::create_array_iterator( + o, + PropertyNameKind::KeyAndValue, + context, + )) + } + + /// Represents the algorithm to calculate `relativeStart` (or `k`) in array functions. + pub(super) fn get_relative_start( + context: &mut Context, + arg: Option<&JsValue>, + len: usize, + ) -> JsResult { + // 1. Let relativeStart be ? ToIntegerOrInfinity(start). + let relative_start = arg + .cloned() + .unwrap_or_default() + .to_integer_or_infinity(context)?; + match relative_start { + // 2. If relativeStart is -∞, let k be 0. + IntegerOrInfinity::NegativeInfinity => Ok(0), + // 3. Else if relativeStart < 0, let k be max(len + relativeStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as usize), + // Both `as` casts are safe as both variables are non-negative + // 4. Else, let k be min(relativeStart, len). + IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as usize), + + // Special case - positive infinity. `len` is always smaller than +inf, thus from (4) + IntegerOrInfinity::PositiveInfinity => Ok(len), + } + } + + /// Represents the algorithm to calculate `relativeEnd` (or `final`) in array functions. + pub(super) fn get_relative_end( + context: &mut Context, + arg: Option<&JsValue>, + len: usize, + ) -> JsResult { + let default_value = JsValue::undefined(); + let value = arg.unwrap_or(&default_value); + // 1. If end is undefined, let relativeEnd be len [and return it] + if value.is_undefined() { + Ok(len) + } else { + // 1. cont, else let relativeEnd be ? ToIntegerOrInfinity(end). + let relative_end = value.to_integer_or_infinity(context)?; + match relative_end { + // 2. If relativeEnd is -∞, let final be 0. + IntegerOrInfinity::NegativeInfinity => Ok(0), + // 3. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as usize), + // 4. Else, let final be min(relativeEnd, len). + // Both `as` casts are safe as both variables are non-negative + IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as usize), + + // Special case - positive infinity. `len` is always smaller than +inf, thus from (4) + IntegerOrInfinity::PositiveInfinity => Ok(len), + } + } + } + + pub(crate) fn values_intrinsic(context: &mut Context) -> JsFunction { + FunctionBuilder::native(context, Self::values) + .name("values") + .length(0) + .constructor(false) + .build() + } + + /// `Array.prototype [ @@unscopables ]` + /// + /// The initial value of the 'unscopables' data property is an ordinary object + /// with the following boolean properties set to true: + /// 'at', 'copyWithin', 'entries', 'fill', 'find', 'findIndex', 'flat', + /// 'flatMap', 'includes', 'keys', 'values' + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-array.prototype-@@unscopables + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@unscopables + pub(crate) fn unscopables_intrinsic(context: &mut Context) -> JsObject { + // 1. Let unscopableList be OrdinaryObjectCreate(null). + let unscopable_list = JsObject::empty(); + // 2. Perform ! CreateDataPropertyOrThrow(unscopableList, "at", true). + unscopable_list + .create_data_property_or_throw("at", true, context) + .expect("CreateDataPropertyOrThrow for 'at' must not fail"); + // 3. Perform ! CreateDataPropertyOrThrow(unscopableList, "copyWithin", true). + unscopable_list + .create_data_property_or_throw("copyWithin", true, context) + .expect("CreateDataPropertyOrThrow for 'copyWithin' must not fail"); + // 4. Perform ! CreateDataPropertyOrThrow(unscopableList, "entries", true). + unscopable_list + .create_data_property_or_throw("entries", true, context) + .expect("CreateDataPropertyOrThrow for 'entries' must not fail"); + // 5. Perform ! CreateDataPropertyOrThrow(unscopableList, "fill", true). + unscopable_list + .create_data_property_or_throw("fill", true, context) + .expect("CreateDataPropertyOrThrow for 'fill' must not fail"); + // 6. Perform ! CreateDataPropertyOrThrow(unscopableList, "find", true). + unscopable_list + .create_data_property_or_throw("find", true, context) + .expect("CreateDataPropertyOrThrow for 'find' must not fail"); + // 7. Perform ! CreateDataPropertyOrThrow(unscopableList, "findIndex", true). + unscopable_list + .create_data_property_or_throw("findIndex", true, context) + .expect("CreateDataPropertyOrThrow for 'findIndex' must not fail"); + // 8. Perform ! CreateDataPropertyOrThrow(unscopableList, "flat", true). + unscopable_list + .create_data_property_or_throw("flat", true, context) + .expect("CreateDataPropertyOrThrow for 'flat' must not fail"); + // 9. Perform ! CreateDataPropertyOrThrow(unscopableList, "flatMap", true). + unscopable_list + .create_data_property_or_throw("flatMap", true, context) + .expect("CreateDataPropertyOrThrow for 'flatMap' must not fail"); + // 10. Perform ! CreateDataPropertyOrThrow(unscopableList, "includes", true). + unscopable_list + .create_data_property_or_throw("includes", true, context) + .expect("CreateDataPropertyOrThrow for 'includes' must not fail"); + // 11. Perform ! CreateDataPropertyOrThrow(unscopableList, "keys", true). + unscopable_list + .create_data_property_or_throw("keys", true, context) + .expect("CreateDataPropertyOrThrow for 'keys' must not fail"); + // 12. Perform ! CreateDataPropertyOrThrow(unscopableList, "values", true). + unscopable_list + .create_data_property_or_throw("values", true, context) + .expect("CreateDataPropertyOrThrow for 'values' must not fail"); + + // 13. Return unscopableList. + unscopable_list + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/tests.rs new file mode 100644 index 0000000..5449981 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array/tests.rs @@ -0,0 +1,1574 @@ +use super::Array; +use crate::builtins::Number; +use crate::{forward, Context, JsValue}; + +#[test] +fn is_array() { + let mut context = Context::default(); + let init = r#" + var empty = []; + var new_arr = new Array(); + var many = ["a", "b", "c"]; + "#; + context.eval(init).unwrap(); + assert_eq!( + context.eval("Array.isArray(empty)").unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval("Array.isArray(new_arr)").unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval("Array.isArray(many)").unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval("Array.isArray([1, 2, 3])").unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval("Array.isArray([])").unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval("Array.isArray({})").unwrap(), + JsValue::new(false) + ); + // assert_eq!(context.eval("Array.isArray(new Array)"), "true"); + assert_eq!( + context.eval("Array.isArray()").unwrap(), + JsValue::new(false) + ); + assert_eq!( + context + .eval("Array.isArray({ constructor: Array })") + .unwrap(), + JsValue::new(false) + ); + assert_eq!( + context + .eval("Array.isArray({ push: Array.prototype.push, concat: Array.prototype.concat })") + .unwrap(), + JsValue::new(false) + ); + assert_eq!( + context.eval("Array.isArray(17)").unwrap(), + JsValue::new(false) + ); + assert_eq!( + context + .eval("Array.isArray({ __proto__: Array.prototype })") + .unwrap(), + JsValue::new(false) + ); + assert_eq!( + context.eval("Array.isArray({ length: 0 })").unwrap(), + JsValue::new(false) + ); +} + +#[test] +fn of() { + let mut context = Context::default(); + assert_eq!( + context + .eval("Array.of(1, 2, 3)") + .unwrap() + .to_string(&mut context) + .unwrap(), + context + .eval("[1, 2, 3]") + .unwrap() + .to_string(&mut context) + .unwrap() + ); + assert_eq!( + context + .eval("Array.of(1, 'a', [], undefined, null)") + .unwrap() + .to_string(&mut context) + .unwrap(), + context + .eval("[1, 'a', [], undefined, null]") + .unwrap() + .to_string(&mut context) + .unwrap() + ); + assert_eq!( + context + .eval("Array.of()") + .unwrap() + .to_string(&mut context) + .unwrap(), + context.eval("[]").unwrap().to_string(&mut context).unwrap() + ); + + context + .eval(r#"let a = Array.of.call(Date, "a", undefined, 3);"#) + .unwrap(); + assert_eq!( + context.eval("a instanceof Date").unwrap(), + JsValue::new(true) + ); + assert_eq!(context.eval("a[0]").unwrap(), JsValue::new("a")); + assert_eq!(context.eval("a[1]").unwrap(), JsValue::undefined()); + assert_eq!(context.eval("a[2]").unwrap(), JsValue::new(3)); + assert_eq!(context.eval("a.length").unwrap(), JsValue::new(3)); +} + +#[test] +fn concat() { + let mut context = Context::default(); + let init = r#" + var empty = []; + var one = [1]; + "#; + context.eval(init).unwrap(); + // Empty ++ Empty + let ee = context + .eval("empty.concat(empty)") + .unwrap() + .display() + .to_string(); + assert_eq!(ee, "[]"); + // Empty ++ NonEmpty + let en = context + .eval("empty.concat(one)") + .unwrap() + .display() + .to_string(); + assert_eq!(en, "[ 1 ]"); + // NonEmpty ++ Empty + let ne = context + .eval("one.concat(empty)") + .unwrap() + .display() + .to_string(); + assert_eq!(ne, "[ 1 ]"); + // NonEmpty ++ NonEmpty + let nn = context + .eval("one.concat(one)") + .unwrap() + .display() + .to_string(); + assert_eq!(nn, "[ 1, 1 ]"); +} + +#[test] +fn copy_within() { + let mut context = Context::default(); + + let target = forward(&mut context, "[1,2,3,4,5].copyWithin(-2).join('.')"); + assert_eq!(target, String::from("\"1.2.3.1.2\"")); + + let start = forward(&mut context, "[1,2,3,4,5].copyWithin(0, 3).join('.')"); + assert_eq!(start, String::from("\"4.5.3.4.5\"")); + + let end = forward(&mut context, "[1,2,3,4,5].copyWithin(0, 3, 4).join('.')"); + assert_eq!(end, String::from("\"4.2.3.4.5\"")); + + let negatives = forward(&mut context, "[1,2,3,4,5].copyWithin(-2, -3, -1).join('.')"); + assert_eq!(negatives, String::from("\"1.2.3.3.4\"")); +} + +#[test] +fn join() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = ["a"]; + var many = ["a", "b", "c"]; + "#; + eprintln!("{}", forward(&mut context, init)); + // Empty + let empty = forward(&mut context, "empty.join('.')"); + assert_eq!(empty, String::from("\"\"")); + // One + let one = forward(&mut context, "one.join('.')"); + assert_eq!(one, String::from("\"a\"")); + // Many + let many = forward(&mut context, "many.join('.')"); + assert_eq!(many, String::from("\"a.b.c\"")); +} + +#[test] +fn to_string() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = ["a"]; + var many = ["a", "b", "c"]; + "#; + eprintln!("{}", forward(&mut context, init)); + // Empty + let empty = forward(&mut context, "empty.toString()"); + assert_eq!(empty, String::from("\"\"")); + // One + let one = forward(&mut context, "one.toString()"); + assert_eq!(one, String::from("\"a\"")); + // Many + let many = forward(&mut context, "many.toString()"); + assert_eq!(many, String::from("\"a,b,c\"")); +} + +#[test] +fn every() { + let mut context = Context::default(); + // taken from https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every + let init = r#" + var empty = []; + + var array = [11, 23, 45]; + function callback(element) { + return element > 10; + } + function callback2(element) { + return element < 10; + } + + var appendArray = [1,2,3,4]; + function appendingCallback(elem,index,arr) { + arr.push('new'); + return elem !== "new"; + } + + var delArray = [1,2,3,4]; + function deletingCallback(elem,index,arr) { + arr.pop() + return elem < 3; + } + "#; + eprintln!("{}", forward(&mut context, init)); + let result = forward(&mut context, "array.every(callback);"); + assert_eq!(result, "true"); + + let result = forward(&mut context, "empty.every(callback);"); + assert_eq!(result, "true"); + + let result = forward(&mut context, "array.every(callback2);"); + assert_eq!(result, "false"); + + let result = forward(&mut context, "appendArray.every(appendingCallback);"); + assert_eq!(result, "true"); + + let result = forward(&mut context, "delArray.every(deletingCallback);"); + assert_eq!(result, "true"); +} + +#[test] +fn find() { + let mut context = Context::default(); + let init = r#" + function comp(a) { + return a == "a"; + } + var many = ["a", "b", "c"]; + "#; + eprintln!("{}", forward(&mut context, init)); + let found = forward(&mut context, "many.find(comp)"); + assert_eq!(found, String::from("\"a\"")); +} + +#[test] +fn find_index() { + let mut context = Context::default(); + + let code = r#" + function comp(item) { + return item == 2; + } + var many = [1, 2, 3]; + var empty = []; + var missing = [4, 5, 6]; + "#; + + forward(&mut context, code); + + let many = forward(&mut context, "many.findIndex(comp)"); + assert_eq!(many, String::from("1")); + + let empty = forward(&mut context, "empty.findIndex(comp)"); + assert_eq!(empty, String::from("-1")); + + let missing = forward(&mut context, "missing.findIndex(comp)"); + assert_eq!(missing, String::from("-1")); +} + +#[test] +fn flat() { + let mut context = Context::default(); + + let code = r#" + var depth1 = ['a', ['b', 'c']]; + var flat_depth1 = depth1.flat(); + + var depth2 = ['a', ['b', ['c'], 'd']]; + var flat_depth2 = depth2.flat(2); + "#; + forward(&mut context, code); + + assert_eq!(forward(&mut context, "flat_depth1[0]"), "\"a\""); + assert_eq!(forward(&mut context, "flat_depth1[1]"), "\"b\""); + assert_eq!(forward(&mut context, "flat_depth1[2]"), "\"c\""); + assert_eq!(forward(&mut context, "flat_depth1.length"), "3"); + + assert_eq!(forward(&mut context, "flat_depth2[0]"), "\"a\""); + assert_eq!(forward(&mut context, "flat_depth2[1]"), "\"b\""); + assert_eq!(forward(&mut context, "flat_depth2[2]"), "\"c\""); + assert_eq!(forward(&mut context, "flat_depth2[3]"), "\"d\""); + assert_eq!(forward(&mut context, "flat_depth2.length"), "4"); +} + +#[test] +fn flat_empty() { + let mut context = Context::default(); + + let code = r#" + var empty = [[]]; + var flat_empty = empty.flat(); + "#; + forward(&mut context, code); + + assert_eq!(forward(&mut context, "flat_empty.length"), "0"); +} + +#[test] +fn flat_infinity() { + let mut context = Context::default(); + + let code = r#" + var arr = [[[[[['a']]]]]]; + var flat_arr = arr.flat(Infinity) + "#; + forward(&mut context, code); + + assert_eq!(forward(&mut context, "flat_arr[0]"), "\"a\""); + assert_eq!(forward(&mut context, "flat_arr.length"), "1"); +} + +#[test] +fn flat_map() { + let mut context = Context::default(); + + let code = r#" + var double = [1, 2, 3]; + var double_flatmap = double.flatMap(i => [i * 2]); + + var sentence = ["it's Sunny", "in Cali"]; + var flat_split_sentence = sentence.flatMap(x => x.split(" ")); + "#; + forward(&mut context, code); + + assert_eq!(forward(&mut context, "double_flatmap[0]"), "2"); + assert_eq!(forward(&mut context, "double_flatmap[1]"), "4"); + assert_eq!(forward(&mut context, "double_flatmap[2]"), "6"); + assert_eq!(forward(&mut context, "double_flatmap.length"), "3"); + + assert_eq!(forward(&mut context, "flat_split_sentence[0]"), "\"it's\""); + assert_eq!(forward(&mut context, "flat_split_sentence[1]"), "\"Sunny\""); + assert_eq!(forward(&mut context, "flat_split_sentence[2]"), "\"in\""); + assert_eq!(forward(&mut context, "flat_split_sentence[3]"), "\"Cali\""); + assert_eq!(forward(&mut context, "flat_split_sentence.length"), "4"); +} + +#[test] +fn flat_map_with_hole() { + let mut context = Context::default(); + + let code = r#" + var arr = [0, 1, 2]; + delete arr[1]; + var arr_flattened = arr.flatMap(i => [i * 2]); + "#; + forward(&mut context, code); + + assert_eq!(forward(&mut context, "arr_flattened[0]"), "0"); + assert_eq!(forward(&mut context, "arr_flattened[1]"), "4"); + assert_eq!(forward(&mut context, "arr_flattened.length"), "2"); +} + +#[test] +fn flat_map_not_callable() { + let mut context = Context::default(); + + let code = r#" + try { + var array = [1,2,3]; + array.flatMap("not a function"); + } catch (err) { + err.name === "TypeError" + } + "#; + + assert_eq!(forward(&mut context, code), "true"); +} + +#[test] +fn push() { + let mut context = Context::default(); + let init = r#" + var arr = [1, 2]; + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "arr.push()"), "2"); + assert_eq!(forward(&mut context, "arr.push(3, 4)"), "4"); + assert_eq!(forward(&mut context, "arr[2]"), "3"); + assert_eq!(forward(&mut context, "arr[3]"), "4"); +} + +#[test] +fn pop() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = [1]; + var many = [1, 2, 3, 4]; + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!( + forward(&mut context, "empty.pop()"), + String::from("undefined") + ); + assert_eq!(forward(&mut context, "one.pop()"), "1"); + assert_eq!(forward(&mut context, "one.length"), "0"); + assert_eq!(forward(&mut context, "many.pop()"), "4"); + assert_eq!(forward(&mut context, "many[0]"), "1"); + assert_eq!(forward(&mut context, "many.length"), "3"); +} + +#[test] +fn shift() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = [1]; + var many = [1, 2, 3, 4]; + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!( + forward(&mut context, "empty.shift()"), + String::from("undefined") + ); + assert_eq!(forward(&mut context, "one.shift()"), "1"); + assert_eq!(forward(&mut context, "one.length"), "0"); + assert_eq!(forward(&mut context, "many.shift()"), "1"); + assert_eq!(forward(&mut context, "many[0]"), "2"); + assert_eq!(forward(&mut context, "many.length"), "3"); +} + +#[test] +fn unshift() { + let mut context = Context::default(); + let init = r#" + var arr = [3, 4]; + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "arr.unshift()"), "2"); + assert_eq!(forward(&mut context, "arr.unshift(1, 2)"), "4"); + assert_eq!(forward(&mut context, "arr[0]"), "1"); + assert_eq!(forward(&mut context, "arr[1]"), "2"); +} + +#[test] +fn reverse() { + let mut context = Context::default(); + let init = r#" + var arr = [1, 2]; + var reversed = arr.reverse(); + "#; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "reversed[0]"), "2"); + assert_eq!(forward(&mut context, "reversed[1]"), "1"); + assert_eq!(forward(&mut context, "arr[0]"), "2"); + assert_eq!(forward(&mut context, "arr[1]"), "1"); +} + +#[test] +fn index_of() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = ["a"]; + var many = ["a", "b", "c"]; + var duplicates = ["a", "b", "c", "a", "b"]; + "#; + eprintln!("{}", forward(&mut context, init)); + + // Empty + let empty = forward(&mut context, "empty.indexOf('a')"); + assert_eq!(empty, String::from("-1")); + + // One + let one = forward(&mut context, "one.indexOf('a')"); + assert_eq!(one, String::from("0")); + // Missing from one + let missing_from_one = forward(&mut context, "one.indexOf('b')"); + assert_eq!(missing_from_one, String::from("-1")); + + // First in many + let first_in_many = forward(&mut context, "many.indexOf('a')"); + assert_eq!(first_in_many, String::from("0")); + // Second in many + let second_in_many = forward(&mut context, "many.indexOf('b')"); + assert_eq!(second_in_many, String::from("1")); + + // First in duplicates + let first_in_many = forward(&mut context, "duplicates.indexOf('a')"); + assert_eq!(first_in_many, String::from("0")); + // Second in duplicates + let second_in_many = forward(&mut context, "duplicates.indexOf('b')"); + assert_eq!(second_in_many, String::from("1")); + + // Positive fromIndex greater than array length + let fromindex_greater_than_length = forward(&mut context, "one.indexOf('a', 2)"); + assert_eq!(fromindex_greater_than_length, String::from("-1")); + // Positive fromIndex missed match + let fromindex_misses_match = forward(&mut context, "many.indexOf('a', 1)"); + assert_eq!(fromindex_misses_match, String::from("-1")); + // Positive fromIndex matched + let fromindex_matches = forward(&mut context, "many.indexOf('b', 1)"); + assert_eq!(fromindex_matches, String::from("1")); + // Positive fromIndex with duplicates + let first_in_many = forward(&mut context, "duplicates.indexOf('a', 1)"); + assert_eq!(first_in_many, String::from("3")); + + // Negative fromIndex greater than array length + let fromindex_greater_than_length = forward(&mut context, "one.indexOf('a', -2)"); + assert_eq!(fromindex_greater_than_length, String::from("0")); + // Negative fromIndex missed match + let fromindex_misses_match = forward(&mut context, "many.indexOf('b', -1)"); + assert_eq!(fromindex_misses_match, String::from("-1")); + // Negative fromIndex matched + let fromindex_matches = forward(&mut context, "many.indexOf('c', -1)"); + assert_eq!(fromindex_matches, String::from("2")); + // Negative fromIndex with duplicates + let second_in_many = forward(&mut context, "duplicates.indexOf('b', -2)"); + assert_eq!(second_in_many, String::from("4")); +} + +#[test] +fn last_index_of() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = ["a"]; + var many = ["a", "b", "c"]; + var duplicates = ["a", "b", "c", "a", "b"]; + "#; + eprintln!("{}", forward(&mut context, init)); + + // Empty + let empty = forward(&mut context, "empty.lastIndexOf('a')"); + assert_eq!(empty, String::from("-1")); + + // One + let one = forward(&mut context, "one.lastIndexOf('a')"); + assert_eq!(one, String::from("0")); + // Missing from one + let missing_from_one = forward(&mut context, "one.lastIndexOf('b')"); + assert_eq!(missing_from_one, String::from("-1")); + + // First in many + let first_in_many = forward(&mut context, "many.lastIndexOf('a')"); + assert_eq!(first_in_many, String::from("0")); + // Second in many + let second_in_many = forward(&mut context, "many.lastIndexOf('b')"); + assert_eq!(second_in_many, String::from("1")); + + // 4th in duplicates + let first_in_many = forward(&mut context, "duplicates.lastIndexOf('a')"); + assert_eq!(first_in_many, String::from("3")); + // 5th in duplicates + let second_in_many = forward(&mut context, "duplicates.lastIndexOf('b')"); + assert_eq!(second_in_many, String::from("4")); + + // Positive fromIndex greater than array length + let fromindex_greater_than_length = forward(&mut context, "one.lastIndexOf('a', 2)"); + assert_eq!(fromindex_greater_than_length, String::from("0")); + // Positive fromIndex missed match + let fromindex_misses_match = forward(&mut context, "many.lastIndexOf('c', 1)"); + assert_eq!(fromindex_misses_match, String::from("-1")); + // Positive fromIndex matched + let fromindex_matches = forward(&mut context, "many.lastIndexOf('b', 1)"); + assert_eq!(fromindex_matches, String::from("1")); + // Positive fromIndex with duplicates + let first_in_many = forward(&mut context, "duplicates.lastIndexOf('a', 1)"); + assert_eq!(first_in_many, String::from("0")); + + // Negative fromIndex greater than array length + let fromindex_greater_than_length = forward(&mut context, "one.lastIndexOf('a', -2)"); + assert_eq!(fromindex_greater_than_length, String::from("-1")); + // Negative fromIndex missed match + let fromindex_misses_match = forward(&mut context, "many.lastIndexOf('c', -2)"); + assert_eq!(fromindex_misses_match, String::from("-1")); + // Negative fromIndex matched + let fromindex_matches = forward(&mut context, "many.lastIndexOf('c', -1)"); + assert_eq!(fromindex_matches, String::from("2")); + // Negative fromIndex with duplicates + let second_in_many = forward(&mut context, "duplicates.lastIndexOf('b', -2)"); + assert_eq!(second_in_many, String::from("1")); +} + +#[test] +fn fill_obj_ref() { + let mut context = Context::default(); + + // test object reference + forward(&mut context, "a = (new Array(3)).fill({});"); + forward(&mut context, "a[0].hi = 'hi';"); + assert_eq!(forward(&mut context, "a[0].hi"), "\"hi\""); +} + +#[test] +fn fill() { + let mut context = Context::default(); + + forward(&mut context, "var a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4).join()"), + String::from("\"4,4,4\"") + ); + // make sure the array is modified + assert_eq!(forward(&mut context, "a.join()"), String::from("\"4,4,4\"")); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, '1').join()"), + String::from("\"1,4,4\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 1, 2).join()"), + String::from("\"1,4,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 1, 1).join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 3, 3).join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, -3, -2).join()"), + String::from("\"4,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, NaN, NaN).join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 3, 5).join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, '1.2', '2.5').join()"), + String::from("\"1,4,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 'str').join()"), + String::from("\"4,4,4\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, 'str', 'str').join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, undefined, null).join()"), + String::from("\"1,2,3\"") + ); + + forward(&mut context, "a = [1, 2, 3];"); + assert_eq!( + forward(&mut context, "a.fill(4, undefined, undefined).join()"), + String::from("\"4,4,4\"") + ); + + assert_eq!( + forward(&mut context, "a.fill().join()"), + String::from("\",,\"") + ); + + // test object reference + forward(&mut context, "a = (new Array(3)).fill({});"); + forward(&mut context, "a[0].hi = 'hi';"); + assert_eq!(forward(&mut context, "a[0].hi"), String::from("\"hi\"")); +} + +#[test] +fn includes_value() { + let mut context = Context::default(); + let init = r#" + var empty = [ ]; + var one = ["a"]; + var many = ["a", "b", "c"]; + var duplicates = ["a", "b", "c", "a", "b"]; + var undefined = [undefined]; + "#; + eprintln!("{}", forward(&mut context, init)); + + // Empty + let empty = forward(&mut context, "empty.includes('a')"); + assert_eq!(empty, String::from("false")); + + // One + let one = forward(&mut context, "one.includes('a')"); + assert_eq!(one, String::from("true")); + // Missing from one + let missing_from_one = forward(&mut context, "one.includes('b')"); + assert_eq!(missing_from_one, String::from("false")); + + // In many + let first_in_many = forward(&mut context, "many.includes('c')"); + assert_eq!(first_in_many, String::from("true")); + // Missing from many + let second_in_many = forward(&mut context, "many.includes('d')"); + assert_eq!(second_in_many, String::from("false")); + + // In duplicates + let first_in_many = forward(&mut context, "duplicates.includes('a')"); + assert_eq!(first_in_many, String::from("true")); + // Missing from duplicates + let second_in_many = forward(&mut context, "duplicates.includes('d')"); + assert_eq!(second_in_many, String::from("false")); +} + +#[test] +fn map() { + let mut context = Context::default(); + + let js = r#" + var empty = []; + var one = ["x"]; + var many = ["x", "y", "z"]; + + var _this = { answer: 42 }; + + function callbackThatUsesThis() { + return 'The answer to life is: ' + this.answer; + } + + var empty_mapped = empty.map(v => v + '_'); + var one_mapped = one.map(v => '_' + v); + var many_mapped = many.map(v => '_' + v + '_'); + "#; + + forward(&mut context, js); + + // assert the old arrays have not been modified + assert_eq!(forward(&mut context, "one[0]"), String::from("\"x\"")); + assert_eq!( + forward(&mut context, "many[2] + many[1] + many[0]"), + String::from("\"zyx\"") + ); + + // NB: These tests need to be rewritten once `Display` has been implemented for `Array` + // Empty + assert_eq!( + forward(&mut context, "empty_mapped.length"), + String::from("0") + ); + + // One + assert_eq!( + forward(&mut context, "one_mapped.length"), + String::from("1") + ); + assert_eq!( + forward(&mut context, "one_mapped[0]"), + String::from("\"_x\"") + ); + + // Many + assert_eq!( + forward(&mut context, "many_mapped.length"), + String::from("3") + ); + assert_eq!( + forward( + &mut context, + "many_mapped[0] + many_mapped[1] + many_mapped[2]" + ), + String::from("\"_x__y__z_\"") + ); + + // One but it uses `this` inside the callback + let one_with_this = forward(&mut context, "one.map(callbackThatUsesThis, _this)[0];"); + assert_eq!(one_with_this, String::from("\"The answer to life is: 42\"")); +} + +#[test] +fn slice() { + let mut context = Context::default(); + let init = r#" + var empty = [ ].slice(); + var one = ["a"].slice(); + var many1 = ["a", "b", "c", "d"].slice(1); + var many2 = ["a", "b", "c", "d"].slice(2, 3); + var many3 = ["a", "b", "c", "d"].slice(7); + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "empty.length"), "0"); + assert_eq!(forward(&mut context, "one[0]"), "\"a\""); + assert_eq!(forward(&mut context, "many1[0]"), "\"b\""); + assert_eq!(forward(&mut context, "many1[1]"), "\"c\""); + assert_eq!(forward(&mut context, "many1[2]"), "\"d\""); + assert_eq!(forward(&mut context, "many1.length"), "3"); + assert_eq!(forward(&mut context, "many2[0]"), "\"c\""); + assert_eq!(forward(&mut context, "many2.length"), "1"); + assert_eq!(forward(&mut context, "many3.length"), "0"); +} + +#[test] +fn for_each() { + let mut context = Context::default(); + let init = r#" + var a = [2, 3, 4, 5]; + var sum = 0; + var indexSum = 0; + var listLengthSum = 0; + function callingCallback(item, index, list) { + sum += item; + indexSum += index; + listLengthSum += list.length; + } + a.forEach(callingCallback); + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "sum"), "14"); + assert_eq!(forward(&mut context, "indexSum"), "6"); + assert_eq!(forward(&mut context, "listLengthSum"), "16"); +} + +#[test] +fn for_each_push_value() { + let mut context = Context::default(); + let init = r#" + var a = [1, 2, 3, 4]; + function callingCallback(item, index, list) { + list.push(item * 2); + } + a.forEach(callingCallback); + "#; + eprintln!("{}", forward(&mut context, init)); + + // [ 1, 2, 3, 4, 2, 4, 6, 8 ] + assert_eq!(forward(&mut context, "a.length"), "8"); + assert_eq!(forward(&mut context, "a[4]"), "2"); + assert_eq!(forward(&mut context, "a[5]"), "4"); + assert_eq!(forward(&mut context, "a[6]"), "6"); + assert_eq!(forward(&mut context, "a[7]"), "8"); +} + +#[test] +fn filter() { + let mut context = Context::default(); + + let js = r#" + var empty = []; + var one = ["1"]; + var many = ["1", "0", "1"]; + + var empty_filtered = empty.filter(v => v === "1"); + var one_filtered = one.filter(v => v === "1"); + var zero_filtered = one.filter(v => v === "0"); + var many_one_filtered = many.filter(v => v === "1"); + var many_zero_filtered = many.filter(v => v === "0"); + "#; + + forward(&mut context, js); + + // assert the old arrays have not been modified + assert_eq!(forward(&mut context, "one[0]"), String::from("\"1\"")); + assert_eq!( + forward(&mut context, "many[2] + many[1] + many[0]"), + String::from("\"101\"") + ); + + // NB: These tests need to be rewritten once `Display` has been implemented for `Array` + // Empty + assert_eq!( + forward(&mut context, "empty_filtered.length"), + String::from("0") + ); + + // One filtered on "1" + assert_eq!( + forward(&mut context, "one_filtered.length"), + String::from("1") + ); + assert_eq!( + forward(&mut context, "one_filtered[0]"), + String::from("\"1\"") + ); + + // One filtered on "0" + assert_eq!( + forward(&mut context, "zero_filtered.length"), + String::from("0") + ); + + // Many filtered on "1" + assert_eq!( + forward(&mut context, "many_one_filtered.length"), + String::from("2") + ); + assert_eq!( + forward(&mut context, "many_one_filtered[0] + many_one_filtered[1]"), + String::from("\"11\"") + ); + + // Many filtered on "0" + assert_eq!( + forward(&mut context, "many_zero_filtered.length"), + String::from("1") + ); + assert_eq!( + forward(&mut context, "many_zero_filtered[0]"), + String::from("\"0\"") + ); +} + +#[test] +fn some() { + let mut context = Context::default(); + let init = r#" + var empty = []; + + var array = [11, 23, 45]; + function lessThan10(element) { + return element > 10; + } + function greaterThan10(element) { + return element < 10; + } + + // Cases where callback mutates the array. + var appendArray = [1,2,3,4]; + function appendingCallback(elem,index,arr) { + arr.push('new'); + return elem !== "new"; + } + + var delArray = [1,2,3,4]; + function deletingCallback(elem,index,arr) { + arr.pop() + return elem < 3; + } + "#; + forward(&mut context, init); + let result = forward(&mut context, "array.some(lessThan10);"); + assert_eq!(result, "true"); + + let result = forward(&mut context, "empty.some(lessThan10);"); + assert_eq!(result, "false"); + + let result = forward(&mut context, "array.some(greaterThan10);"); + assert_eq!(result, "false"); + + let result = forward(&mut context, "appendArray.some(appendingCallback);"); + let append_array_length = forward(&mut context, "appendArray.length"); + assert_eq!(append_array_length, "5"); + assert_eq!(result, "true"); + + let result = forward(&mut context, "delArray.some(deletingCallback);"); + let del_array_length = forward(&mut context, "delArray.length"); + assert_eq!(del_array_length, "3"); + assert_eq!(result, "true"); +} + +#[test] +fn reduce() { + let mut context = Context::default(); + + let init = r#" + var arr = [1, 2, 3, 4]; + function add(acc, x) { + return acc + x; + } + + function addIdx(acc, _, idx) { + return acc + idx; + } + + function addLen(acc, _x, _idx, arr) { + return acc + arr.length; + } + + function addResize(acc, x, idx, arr) { + if(idx == 0) { + arr.length = 3; + } + return acc + x; + } + var delArray = [1, 2, 3, 4, 5]; + delete delArray[0]; + delete delArray[1]; + delete delArray[3]; + + "#; + forward(&mut context, init); + + // empty array + let result = forward(&mut context, "[].reduce(add, 0)"); + assert_eq!(result, "0"); + + // simple with initial value + let result = forward(&mut context, "arr.reduce(add, 0)"); + assert_eq!(result, "10"); + + // without initial value + let result = forward(&mut context, "arr.reduce(add)"); + assert_eq!(result, "10"); + + // with some items missing + let result = forward(&mut context, "delArray.reduce(add, 0)"); + assert_eq!(result, "8"); + + // with index + let result = forward(&mut context, "arr.reduce(addIdx, 0)"); + assert_eq!(result, "6"); + + // with array + let result = forward(&mut context, "arr.reduce(addLen, 0)"); + assert_eq!(result, "16"); + + // resizing the array as reduce progresses + let result = forward(&mut context, "arr.reduce(addResize, 0)"); + assert_eq!(result, "6"); + + // Empty array + let result = forward( + &mut context, + r#" + try { + [].reduce((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduce: called on an empty array and with no initial value\"" + ); + + // Array with no defined elements + let result = forward( + &mut context, + r#" + try { + var arr = [0, 1]; + delete arr[0]; + delete arr[1]; + arr.reduce((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduce: called on an empty array and with no initial value\"" + ); + + // No callback + let result = forward( + &mut context, + r#" + try { + arr.reduce(""); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduce: callback function is not callable\"" + ); +} + +#[test] +fn reduce_right() { + let mut context = Context::default(); + + let init = r#" + var arr = [1, 2, 3, 4]; + function sub(acc, x) { + return acc - x; + } + + function subIdx(acc, _, idx) { + return acc - idx; + } + + function subLen(acc, _x, _idx, arr) { + return acc - arr.length; + } + + function subResize(acc, x, idx, arr) { + if(idx == arr.length - 1) { + arr.length = 1; + } + return acc - x; + } + function subResize0(acc, x, idx, arr) { + if(idx == arr.length - 2) { + arr.length = 0; + } + return acc - x; + } + var delArray = [1, 2, 3, 4, 5]; + delete delArray[0]; + delete delArray[1]; + delete delArray[3]; + + "#; + forward(&mut context, init); + + // empty array + let result = forward(&mut context, "[].reduceRight(sub, 0)"); + assert_eq!(result, "0"); + + // simple with initial value + let result = forward(&mut context, "arr.reduceRight(sub, 0)"); + assert_eq!(result, "-10"); + + // without initial value + let result = forward(&mut context, "arr.reduceRight(sub)"); + assert_eq!(result, "-2"); + + // with some items missing + let result = forward(&mut context, "delArray.reduceRight(sub, 0)"); + assert_eq!(result, "-8"); + + // with index + let result = forward(&mut context, "arr.reduceRight(subIdx)"); + assert_eq!(result, "1"); + + // with array + let result = forward(&mut context, "arr.reduceRight(subLen)"); + assert_eq!(result, "-8"); + + // resizing the array as reduce progresses + let result = forward(&mut context, "arr.reduceRight(subResize, 0)"); + assert_eq!(result, "-5"); + + // reset array + forward(&mut context, "arr = [1, 2, 3, 4];"); + + // resizing the array to 0 as reduce progresses + let result = forward(&mut context, "arr.reduceRight(subResize0, 0)"); + assert_eq!(result, "-7"); + + // Empty array + let result = forward( + &mut context, + r#" + try { + [].reduceRight((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduceRight: called on an empty array and with no initial value\"" + ); + + // Array with no defined elements + let result = forward( + &mut context, + r#" + try { + var arr = [0, 1]; + delete arr[0]; + delete arr[1]; + arr.reduceRight((acc, x) => acc + x); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduceRight: called on an empty array and with no initial value\"" + ); + + // No callback + let result = forward( + &mut context, + r#" + try { + arr.reduceRight(""); + } catch(e) { + e.message + } + "#, + ); + assert_eq!( + result, + "\"Array.prototype.reduceRight: callback function is not callable\"" + ); +} + +#[test] +fn call_array_constructor_with_one_argument() { + let mut context = Context::default(); + let init = r#" + var empty = new Array(0); + + var five = new Array(5); + + var one = new Array("Hello, world!"); + "#; + forward(&mut context, init); + // let result = forward(&mut context, "empty.length"); + // assert_eq!(result, "0"); + + // let result = forward(&mut context, "five.length"); + // assert_eq!(result, "5"); + + // let result = forward(&mut context, "one.length"); + // assert_eq!(result, "1"); +} + +#[test] +fn array_values_simple() { + let mut context = Context::default(); + let init = r#" + var iterator = [1, 2, 3].values(); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "1"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "2"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "3"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_keys_simple() { + let mut context = Context::default(); + let init = r#" + var iterator = [1, 2, 3].keys(); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "0"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "1"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "2"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_entries_simple() { + let mut context = Context::default(); + let init = r#" + var iterator = [1, 2, 3].entries(); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "[ 0, 1 ]"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "[ 1, 2 ]"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "[ 2, 3 ]"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_values_empty() { + let mut context = Context::default(); + let init = r#" + var iterator = [].values(); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_values_sparse() { + let mut context = Context::default(); + let init = r#" + var array = Array(); + array[3] = 5; + var iterator = array.values(); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "5"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_symbol_iterator() { + let mut context = Context::default(); + let init = r#" + var iterator = [1, 2, 3][Symbol.iterator](); + var next = iterator.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "1"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "2"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "3"); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iterator.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn array_values_symbol_iterator() { + let mut context = Context::default(); + let init = r#" + var iterator = [1, 2, 3].values(); + iterator === iterator[Symbol.iterator](); + "#; + assert_eq!(forward(&mut context, init), "true"); +} + +#[test] +fn array_spread_arrays() { + let mut context = Context::default(); + let init = r#" + const array1 = [2, 3]; + const array2 = [1, ...array1]; + array2[0] === 1 && array2[1] === 2 && array2[2] === 3; + "#; + assert_eq!(forward(&mut context, init), "true"); +} + +#[test] +fn array_spread_non_iterable() { + let mut context = Context::default(); + let init = r#" + try { + const array2 = [...5]; + } catch (err) { + err.name === "TypeError" && err.message === "Value is not callable" + } + "#; + assert_eq!(forward(&mut context, init), "true"); +} + +#[test] +fn get_relative_start() { + let mut context = Context::default(); + + assert_eq!(Array::get_relative_start(&mut context, None, 10), Ok(0)); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::undefined()), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(f64::NEG_INFINITY)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(f64::INFINITY)), 10), + Ok(10) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(-1)), 10), + Ok(9) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(1)), 10), + Ok(1) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(-11)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(11)), 10), + Ok(10) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(f64::MIN)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_start( + &mut context, + Some(&JsValue::new(Number::MIN_SAFE_INTEGER)), + 10 + ), + Ok(0) + ); + assert_eq!( + Array::get_relative_start(&mut context, Some(&JsValue::new(f64::MAX)), 10), + Ok(10) + ); + + // This test is relevant only on 32-bit archs (where usize == u32 thus `len` is u32) + assert_eq!( + Array::get_relative_start( + &mut context, + Some(&JsValue::new(Number::MAX_SAFE_INTEGER)), + 10 + ), + Ok(10) + ); +} + +#[test] +fn get_relative_end() { + let mut context = Context::default(); + + assert_eq!(Array::get_relative_end(&mut context, None, 10), Ok(10)); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::undefined()), 10), + Ok(10) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(f64::NEG_INFINITY)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(f64::INFINITY)), 10), + Ok(10) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(-1)), 10), + Ok(9) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(1)), 10), + Ok(1) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(-11)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(11)), 10), + Ok(10) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(f64::MIN)), 10), + Ok(0) + ); + assert_eq!( + Array::get_relative_end( + &mut context, + Some(&JsValue::new(Number::MIN_SAFE_INTEGER)), + 10 + ), + Ok(0) + ); + assert_eq!( + Array::get_relative_end(&mut context, Some(&JsValue::new(f64::MAX)), 10), + Ok(10) + ); + + // This test is relevant only on 32-bit archs (where usize == u32 thus `len` is u32) + assert_eq!( + Array::get_relative_end( + &mut context, + Some(&JsValue::new(Number::MAX_SAFE_INTEGER)), + 10 + ), + Ok(10) + ); +} + +#[test] +fn array_length_is_not_enumerable() { + let mut context = Context::default(); + + let array = + Array::array_create(0, None, &mut context).expect("could not create an empty array"); + let desc = array + .__get_own_property__(&"length".into(), &mut context) + .expect("accessing length property on array should not throw") + .expect("there should always be a length property on arrays"); + assert!(!desc.expect_enumerable()); +} + +#[test] +fn array_sort() { + let mut context = Context::default(); + let init = r#" + let arr = ['80', '9', '700', 40, 1, 5, 200]; + + function compareNumbers(a, b) { + return a - b; + } + "#; + forward(&mut context, init); + assert_eq!( + forward(&mut context, "arr.sort().join()"), + "\"1,200,40,5,700,80,9\"" + ); + assert_eq!( + forward(&mut context, "arr.sort(compareNumbers).join()"), + "\"1,5,9,40,80,200,700\"" + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/mod.rs new file mode 100644 index 0000000..c8bf648 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/mod.rs @@ -0,0 +1,818 @@ +#[cfg(test)] +mod tests; + +use crate::{ + builtins::{typed_array::TypedArrayKind, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::Attribute, + symbol::WellKnownSymbols, + value::{IntegerOrInfinity, Numeric}, + Context, JsResult, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; +use num_traits::{Signed, ToPrimitive}; +use tap::{Conv, Pipe}; + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct ArrayBuffer { + pub array_buffer_data: Option>, + pub array_buffer_byte_length: usize, + pub array_buffer_detach_key: JsValue, +} + +impl ArrayBuffer { + pub(crate) fn array_buffer_byte_length(&self) -> usize { + self.array_buffer_byte_length + } +} + +impl BuiltIn for ArrayBuffer { + const NAME: &'static str = "ArrayBuffer"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let get_byte_length = FunctionBuilder::native(context, Self::get_byte_length) + .name("get byteLength") + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().array_buffer().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .accessor("byteLength", Some(get_byte_length), None, flag_attributes) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .static_method(Self::is_view, "isView", 1) + .method(Self::slice, "slice", 2) + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl ArrayBuffer { + const LENGTH: usize = 1; + + /// `25.1.3.1 ArrayBuffer ( length )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer-length + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context + .throw_type_error("ArrayBuffer.constructor called with undefined new target"); + } + + // 2. Let byteLength be ? ToIndex(length). + let byte_length = args.get_or_undefined(0).to_index(context)?; + + // 3. Return ? AllocateArrayBuffer(NewTarget, byteLength). + Ok(Self::allocate(new_target, byte_length, context)?.into()) + } + + /// `25.1.4.3 get ArrayBuffer [ @@species ]` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer-@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// `25.1.4.1 ArrayBuffer.isView ( arg )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.isview + #[allow(clippy::unnecessary_wraps)] + fn is_view(_: &JsValue, args: &[JsValue], _context: &mut Context) -> JsResult { + // 1. If Type(arg) is not Object, return false. + // 2. If arg has a [[ViewedArrayBuffer]] internal slot, return true. + // 3. Return false. + Ok(args + .get_or_undefined(0) + .as_object() + .map(|obj| obj.borrow().has_viewed_array_buffer()) + .unwrap_or_default() + .into()) + } + + /// `25.1.5.1 get ArrayBuffer.prototype.byteLength` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-arraybuffer.prototype.bytelength + pub(crate) fn get_byte_length( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + let obj = if let Some(obj) = this.as_object() { + obj + } else { + return context.throw_type_error("ArrayBuffer.byteLength called with non-object value"); + }; + let obj = obj.borrow(); + let o = if let Some(o) = obj.as_array_buffer() { + o + } else { + return context.throw_type_error("ArrayBuffer.byteLength called with invalid object"); + }; + + // TODO: Shared Array Buffer + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + + // 4. If IsDetachedBuffer(O) is true, return +0𝔽. + if Self::is_detached_buffer(o) { + return Ok(0.into()); + } + + // 5. Let length be O.[[ArrayBufferByteLength]]. + // 6. Return 𝔽(length). + Ok(o.array_buffer_byte_length.into()) + } + + /// `25.1.5.3 ArrayBuffer.prototype.slice ( start, end )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-arraybuffer.prototype.slice + fn slice(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[ArrayBufferData]]). + let obj = if let Some(obj) = this.as_object() { + obj + } else { + return context.throw_type_error("ArrayBuffer.slice called with non-object value"); + }; + let obj_borrow = obj.borrow(); + let o = if let Some(o) = obj_borrow.as_array_buffer() { + o + } else { + return context.throw_type_error("ArrayBuffer.slice called with invalid object"); + }; + + // TODO: Shared Array Buffer + // 3. If IsSharedArrayBuffer(O) is true, throw a TypeError exception. + + // 4. If IsDetachedBuffer(O) is true, throw a TypeError exception. + if Self::is_detached_buffer(o) { + return context.throw_type_error("ArrayBuffer.slice called with detached buffer"); + } + + // 5. Let len be O.[[ArrayBufferByteLength]]. + let len = o.array_buffer_byte_length as i64; + + // 6. Let relativeStart be ? ToIntegerOrInfinity(start). + let relative_start = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + let first = match relative_start { + // 7. If relativeStart is -∞, let first be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 8. Else if relativeStart < 0, let first be max(len + relativeStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 9. Else, let first be min(relativeStart, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + let end = args.get_or_undefined(1); + let relative_end = if end.is_undefined() { + IntegerOrInfinity::Integer(len) + } else { + end.to_integer_or_infinity(context)? + }; + + let r#final = match relative_end { + // 11. If relativeEnd is -∞, let final be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 13. Else, let final be min(relativeEnd, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 14. Let newLen be max(final - first, 0). + let new_len = std::cmp::max(r#final - first, 0) as usize; + + // 15. Let ctor be ? SpeciesConstructor(O, %ArrayBuffer%). + let ctor = obj.species_constructor(StandardConstructors::array_buffer, context)?; + + // 16. Let new be ? Construct(ctor, « 𝔽(newLen) »). + let new = ctor.construct(&[new_len.into()], Some(&ctor), context)?; + + { + let new_obj = new.borrow(); + // 17. Perform ? RequireInternalSlot(new, [[ArrayBufferData]]). + let new_array_buffer = new_obj.as_array_buffer().ok_or_else(|| { + context.construct_type_error("ArrayBuffer constructor returned invalid object") + })?; + + // TODO: Shared Array Buffer + // 18. If IsSharedArrayBuffer(new) is true, throw a TypeError exception. + + // 19. If IsDetachedBuffer(new) is true, throw a TypeError exception. + if new_array_buffer.is_detached_buffer() { + return context + .throw_type_error("ArrayBuffer constructor returned detached ArrayBuffer"); + } + } + // 20. If SameValue(new, O) is true, throw a TypeError exception. + if this + .as_object() + .map(|obj| JsObject::equals(obj, &new)) + .unwrap_or_default() + { + return context.throw_type_error("New ArrayBuffer is the same as this ArrayBuffer"); + } + + { + let mut new_obj_borrow = new.borrow_mut(); + let new_array_buffer = new_obj_borrow + .as_array_buffer_mut() + .expect("Already checked that `new_obj` was an `ArrayBuffer`"); + + // 21. If new.[[ArrayBufferByteLength]] < newLen, throw a TypeError exception. + if new_array_buffer.array_buffer_byte_length < new_len { + return context.throw_type_error("New ArrayBuffer length too small"); + } + + // 22. NOTE: Side-effects of the above steps may have detached O. + // 23. If IsDetachedBuffer(O) is true, throw a TypeError exception. + if Self::is_detached_buffer(o) { + return context + .throw_type_error("ArrayBuffer detached while ArrayBuffer.slice was running"); + } + + // 24. Let fromBuf be O.[[ArrayBufferData]]. + let from_buf = o + .array_buffer_data + .as_ref() + .expect("ArrayBuffer cannot be detached here"); + + // 25. Let toBuf be new.[[ArrayBufferData]]. + let to_buf = new_array_buffer + .array_buffer_data + .as_mut() + .expect("ArrayBuffer cannot be detached here"); + + // 26. Perform CopyDataBlockBytes(toBuf, 0, fromBuf, first, newLen). + copy_data_block_bytes(to_buf, 0, from_buf, first as usize, new_len); + } + + // 27. Return new. + Ok(new.into()) + } + + /// `25.1.2.1 AllocateArrayBuffer ( constructor, byteLength )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-allocatearraybuffer + pub(crate) fn allocate( + constructor: &JsValue, + byte_length: usize, + context: &mut Context, + ) -> JsResult { + // 1. Let obj be ? OrdinaryCreateFromConstructor(constructor, "%ArrayBuffer.prototype%", « [[ArrayBufferData]], [[ArrayBufferByteLength]], [[ArrayBufferDetachKey]] »). + let prototype = get_prototype_from_constructor( + constructor, + StandardConstructors::array_buffer, + context, + )?; + let obj = context.construct_object(); + obj.set_prototype(prototype.into()); + + // 2. Let block be ? CreateByteDataBlock(byteLength). + let block = create_byte_data_block(byte_length, context)?; + + // 3. Set obj.[[ArrayBufferData]] to block. + // 4. Set obj.[[ArrayBufferByteLength]] to byteLength. + obj.borrow_mut().data = ObjectData::array_buffer(Self { + array_buffer_data: Some(block), + array_buffer_byte_length: byte_length, + array_buffer_detach_key: JsValue::Undefined, + }); + + // 5. Return obj. + Ok(obj) + } + + /// `25.1.2.2 IsDetachedBuffer ( arrayBuffer )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer + pub(crate) fn is_detached_buffer(&self) -> bool { + // 1. If arrayBuffer.[[ArrayBufferData]] is null, return true. + // 2. Return false. + self.array_buffer_data.is_none() + } + + /// `25.1.2.4 CloneArrayBuffer ( srcBuffer, srcByteOffset, srcLength, cloneConstructor )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-clonearraybuffer + pub(crate) fn clone_array_buffer( + &self, + src_byte_offset: usize, + src_length: usize, + clone_constructor: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Let targetBuffer be ? AllocateArrayBuffer(cloneConstructor, srcLength). + let target_buffer = Self::allocate(clone_constructor, src_length, context)?; + + // 2. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError exception. + // 3. Let srcBlock be srcBuffer.[[ArrayBufferData]]. + let src_block = if let Some(b) = &self.array_buffer_data { + b + } else { + return context.throw_syntax_error("Cannot clone detached array buffer"); + }; + + { + // 4. Let targetBlock be targetBuffer.[[ArrayBufferData]]. + let mut target_buffer_mut = target_buffer.borrow_mut(); + let target_block = target_buffer_mut + .as_array_buffer_mut() + .expect("This must be an ArrayBuffer"); + + // 5. Perform CopyDataBlockBytes(targetBlock, 0, srcBlock, srcByteOffset, srcLength). + copy_data_block_bytes( + target_block + .array_buffer_data + .as_mut() + .expect("ArrayBuffer cannot me detached here"), + 0, + src_block, + src_byte_offset, + src_length, + ); + } + + // 6. Return targetBuffer. + Ok(target_buffer) + } + + /// `25.1.2.6 IsUnclampedIntegerElementType ( type )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isunclampedintegerelementtype + fn is_unclamped_integer_element_type(t: TypedArrayKind) -> bool { + // 1. If type is Int8, Uint8, Int16, Uint16, Int32, or Uint32, return true. + // 2. Return false. + matches!( + t, + TypedArrayKind::Int8 + | TypedArrayKind::Uint8 + | TypedArrayKind::Int16 + | TypedArrayKind::Uint16 + | TypedArrayKind::Int32 + | TypedArrayKind::Uint32 + ) + } + + /// `25.1.2.7 IsBigIntElementType ( type )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isbigintelementtype + fn is_big_int_element_type(t: TypedArrayKind) -> bool { + // 1. If type is BigUint64 or BigInt64, return true. + // 2. Return false. + matches!(t, TypedArrayKind::BigUint64 | TypedArrayKind::BigInt64) + } + + /// `25.1.2.8 IsNoTearConfiguration ( type, order )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isnotearconfiguration + // TODO: Allow unused function until shared array buffers are implemented. + #[allow(dead_code)] + fn is_no_tear_configuration(t: TypedArrayKind, order: SharedMemoryOrder) -> bool { + // 1. If ! IsUnclampedIntegerElementType(type) is true, return true. + if Self::is_unclamped_integer_element_type(t) { + return true; + } + + // 2. If ! IsBigIntElementType(type) is true and order is not Init or Unordered, return true. + if Self::is_big_int_element_type(t) + && !matches!( + order, + SharedMemoryOrder::Init | SharedMemoryOrder::Unordered + ) + { + return true; + } + + // 3. Return false. + false + } + + /// `25.1.2.9 RawBytesToNumeric ( type, rawBytes, isLittleEndian )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-rawbytestonumeric + fn raw_bytes_to_numeric(t: TypedArrayKind, bytes: &[u8], is_little_endian: bool) -> JsValue { + let n: Numeric = match t { + TypedArrayKind::Int8 => { + if is_little_endian { + i8::from_le_bytes(bytes.try_into().expect("slice with incorrect length")).into() + } else { + i8::from_be_bytes(bytes.try_into().expect("slice with incorrect length")).into() + } + } + TypedArrayKind::Uint8 | TypedArrayKind::Uint8Clamped => { + if is_little_endian { + u8::from_le_bytes(bytes.try_into().expect("slice with incorrect length")).into() + } else { + u8::from_be_bytes(bytes.try_into().expect("slice with incorrect length")).into() + } + } + TypedArrayKind::Int16 => { + if is_little_endian { + i16::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + i16::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::Uint16 => { + if is_little_endian { + u16::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + u16::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::Int32 => { + if is_little_endian { + i32::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + i32::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::Uint32 => { + if is_little_endian { + u32::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + u32::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::BigInt64 => { + if is_little_endian { + i64::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + i64::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::BigUint64 => { + if is_little_endian { + u64::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + u64::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::Float32 => { + if is_little_endian { + f32::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + f32::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + TypedArrayKind::Float64 => { + if is_little_endian { + f64::from_le_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } else { + f64::from_be_bytes(bytes.try_into().expect("slice with incorrect length")) + .into() + } + } + }; + + n.into() + } + + /// `25.1.2.10 GetValueFromBuffer ( arrayBuffer, byteIndex, type, isTypedArray, order [ , isLittleEndian ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-getvaluefrombuffer + pub(crate) fn get_value_from_buffer( + &self, + byte_index: usize, + t: TypedArrayKind, + _is_typed_array: bool, + _order: SharedMemoryOrder, + is_little_endian: Option, + ) -> JsValue { + // 1. Assert: IsDetachedBuffer(arrayBuffer) is false. + // 2. Assert: There are sufficient bytes in arrayBuffer starting at byteIndex to represent a value of type. + // 3. Let block be arrayBuffer.[[ArrayBufferData]]. + let block = self + .array_buffer_data + .as_ref() + .expect("ArrayBuffer cannot be detached here"); + + // 4. Let elementSize be the Element Size value specified in Table 73 for Element Type type. + let element_size = t.element_size(); + + // TODO: Shared Array Buffer + // 5. If IsSharedArrayBuffer(arrayBuffer) is true, then + + // 6. Else, let rawValue be a List whose elements are bytes from block at indices byteIndex (inclusive) through byteIndex + elementSize (exclusive). + // 7. Assert: The number of elements in rawValue is elementSize. + let raw_value = &block[byte_index..byte_index + element_size]; + + // TODO: Agent Record [[LittleEndian]] filed + // 8. If isLittleEndian is not present, set isLittleEndian to the value of the [[LittleEndian]] field of the surrounding agent's Agent Record. + let is_little_endian = is_little_endian.unwrap_or(true); + + // 9. Return RawBytesToNumeric(type, rawValue, isLittleEndian). + Self::raw_bytes_to_numeric(t, raw_value, is_little_endian) + } + + /// `25.1.2.11 NumericToRawBytes ( type, value, isLittleEndian )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-numerictorawbytes + fn numeric_to_raw_bytes( + t: TypedArrayKind, + value: &JsValue, + is_little_endian: bool, + context: &mut Context, + ) -> JsResult> { + Ok(match t { + TypedArrayKind::Int8 if is_little_endian => { + value.to_int8(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Int8 => value.to_int8(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Uint8 if is_little_endian => { + value.to_uint8(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Uint8 => value.to_uint8(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Uint8Clamped if is_little_endian => { + value.to_uint8_clamp(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Uint8Clamped => value.to_uint8_clamp(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Int16 if is_little_endian => { + value.to_int16(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Int16 => value.to_int16(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Uint16 if is_little_endian => { + value.to_uint16(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Uint16 => value.to_uint16(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Int32 if is_little_endian => { + value.to_i32(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Int32 => value.to_i32(context)?.to_be_bytes().to_vec(), + TypedArrayKind::Uint32 if is_little_endian => { + value.to_u32(context)?.to_le_bytes().to_vec() + } + TypedArrayKind::Uint32 => value.to_u32(context)?.to_be_bytes().to_vec(), + TypedArrayKind::BigInt64 if is_little_endian => { + let big_int = value.to_big_int64(context)?; + big_int + .to_i64() + .unwrap_or_else(|| { + if big_int.is_positive() { + i64::MAX + } else { + i64::MIN + } + }) + .to_le_bytes() + .to_vec() + } + TypedArrayKind::BigInt64 => { + let big_int = value.to_big_int64(context)?; + big_int + .to_i64() + .unwrap_or_else(|| { + if big_int.is_positive() { + i64::MAX + } else { + i64::MIN + } + }) + .to_be_bytes() + .to_vec() + } + TypedArrayKind::BigUint64 if is_little_endian => value + .to_big_uint64(context)? + .to_u64() + .unwrap_or(u64::MAX) + .to_le_bytes() + .to_vec(), + TypedArrayKind::BigUint64 => value + .to_big_uint64(context)? + .to_u64() + .unwrap_or(u64::MAX) + .to_be_bytes() + .to_vec(), + TypedArrayKind::Float32 => match value.to_number(context)? { + f if is_little_endian => (f as f32).to_le_bytes().to_vec(), + f => (f as f32).to_be_bytes().to_vec(), + }, + TypedArrayKind::Float64 => match value.to_number(context)? { + f if is_little_endian => f.to_le_bytes().to_vec(), + f => f.to_be_bytes().to_vec(), + }, + }) + } + + /// `25.1.2.12 SetValueInBuffer ( arrayBuffer, byteIndex, type, value, isTypedArray, order [ , isLittleEndian ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-setvalueinbuffer + pub(crate) fn set_value_in_buffer( + &mut self, + byte_index: usize, + t: TypedArrayKind, + value: &JsValue, + _order: SharedMemoryOrder, + is_little_endian: Option, + context: &mut Context, + ) -> JsResult { + // 1. Assert: IsDetachedBuffer(arrayBuffer) is false. + // 2. Assert: There are sufficient bytes in arrayBuffer starting at byteIndex to represent a value of type. + // 3. Assert: Type(value) is BigInt if ! IsBigIntElementType(type) is true; otherwise, Type(value) is Number. + // 4. Let block be arrayBuffer.[[ArrayBufferData]]. + let block = self + .array_buffer_data + .as_mut() + .expect("ArrayBuffer cannot be detached here"); + + // 5. Let elementSize be the Element Size value specified in Table 73 for Element Type type. + + // TODO: Agent Record [[LittleEndian]] filed + // 6. If isLittleEndian is not present, set isLittleEndian to the value of the [[LittleEndian]] field of the surrounding agent's Agent Record. + let is_little_endian = is_little_endian.unwrap_or(true); + + // 7. Let rawBytes be NumericToRawBytes(type, value, isLittleEndian). + let raw_bytes = Self::numeric_to_raw_bytes(t, value, is_little_endian, context)?; + + // TODO: Shared Array Buffer + // 8. If IsSharedArrayBuffer(arrayBuffer) is true, then + + // 9. Else, store the individual bytes of rawBytes into block, starting at block[byteIndex]. + for (i, raw_byte) in raw_bytes.iter().enumerate() { + block[byte_index + i] = *raw_byte; + } + + // 10. Return NormalCompletion(undefined). + Ok(JsValue::undefined()) + } +} + +/// `CreateByteDataBlock ( size )` abstract operation. +/// +/// The abstract operation `CreateByteDataBlock` takes argument `size` (a non-negative +/// integer). For more information, check the [spec][spec]. +/// +/// [spec]: https://tc39.es/ecma262/#sec-createbytedatablock +pub fn create_byte_data_block(size: usize, context: &mut Context) -> JsResult> { + // 1. Let db be a new Data Block value consisting of size bytes. If it is impossible to + // create such a Data Block, throw a RangeError exception. + let mut data_block = Vec::new(); + data_block.try_reserve(size).map_err(|e| { + context.construct_range_error(format!("couldn't allocate the data block: {e}")) + })?; + + // 2. Set all of the bytes of db to 0. + data_block.resize(size, 0); + + // 3. Return db. + Ok(data_block) +} + +/// `6.2.8.3 CopyDataBlockBytes ( toBlock, toIndex, fromBlock, fromIndex, count )` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-copydatablockbytes +fn copy_data_block_bytes( + to_block: &mut [u8], + mut to_index: usize, + from_block: &[u8], + mut from_index: usize, + mut count: usize, +) { + // 1. Assert: fromBlock and toBlock are distinct values. + // 2. Let fromSize be the number of bytes in fromBlock. + let from_size = from_block.len(); + + // 3. Assert: fromIndex + count ≤ fromSize. + assert!(from_index + count <= from_size); + + // 4. Let toSize be the number of bytes in toBlock. + let to_size = to_block.len(); + + // 5. Assert: toIndex + count ≤ toSize. + assert!(to_index + count <= to_size); + + // 6. Repeat, while count > 0, + while count > 0 { + // a. If fromBlock is a Shared Data Block, then + // TODO: Shared Data Block + + // b. Else, + // i. Assert: toBlock is not a Shared Data Block. + // ii. Set toBlock[toIndex] to fromBlock[fromIndex]. + to_block[to_index] = from_block[from_index]; + + // c. Set toIndex to toIndex + 1. + to_index += 1; + + // d. Set fromIndex to fromIndex + 1. + from_index += 1; + + // e. Set count to count - 1. + count -= 1; + } + + // 7. Return NormalCompletion(empty). +} + +// TODO: Allow unused variants until shared array buffers are implemented. +#[allow(dead_code)] +#[derive(Debug, PartialEq, Clone, Copy)] +pub(crate) enum SharedMemoryOrder { + Init, + SeqCst, + Unordered, +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/tests.rs new file mode 100644 index 0000000..a78451b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/array_buffer/tests.rs @@ -0,0 +1,15 @@ +use super::*; + +#[test] +fn ut_sunny_day_create_byte_data_block() { + let mut context = Context::default(); + + assert!(create_byte_data_block(100, &mut context).is_ok()); +} + +#[test] +fn ut_rainy_day_create_byte_data_block() { + let mut context = Context::default(); + + assert!(create_byte_data_block(usize::MAX, &mut context).is_err()); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/async_function/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/async_function/mod.rs new file mode 100644 index 0000000..12ba9e4 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/async_function/mod.rs @@ -0,0 +1,107 @@ +//! This module implements the global `AsyncFunction` object. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-async-function-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AsyncFunction + +use crate::{ + builtins::{ + function::{ConstructorKind, Function}, + BuiltIn, + }, + object::ObjectData, + property::PropertyDescriptor, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; + +#[derive(Debug, Clone, Copy)] +pub struct AsyncFunction; + +impl BuiltIn for AsyncFunction { + const NAME: &'static str = "AsyncFunction"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let prototype = &context + .intrinsics() + .constructors() + .async_function() + .prototype; + let constructor = &context + .intrinsics() + .constructors() + .async_function() + .constructor; + + constructor.set_prototype(Some( + context.intrinsics().constructors().function().constructor(), + )); + let property = PropertyDescriptor::builder() + .value(1) + .writable(false) + .enumerable(false) + .configurable(true); + constructor.borrow_mut().insert("length", property); + let property = PropertyDescriptor::builder() + .value(Self::NAME) + .writable(false) + .enumerable(false) + .configurable(true); + constructor.borrow_mut().insert("name", property); + let property = PropertyDescriptor::builder() + .value(prototype.clone()) + .writable(false) + .enumerable(false) + .configurable(false); + constructor.borrow_mut().insert("prototype", property); + constructor.borrow_mut().data = ObjectData::function(Function::Native { + function: Self::constructor, + constructor: Some(ConstructorKind::Base), + }); + + prototype.set_prototype(Some( + context.intrinsics().constructors().function().prototype(), + )); + let property = PropertyDescriptor::builder() + .value(constructor.clone()) + .writable(false) + .enumerable(false) + .configurable(true); + prototype.borrow_mut().insert("constructor", property); + let property = PropertyDescriptor::builder() + .value(Self::NAME) + .writable(false) + .enumerable(false) + .configurable(true); + prototype + .borrow_mut() + .insert(WellKnownSymbols::to_string_tag(), property); + + None + } +} + +impl AsyncFunction { + /// `AsyncFunction ( p1, p2, … , pn, body )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-async-function-constructor-arguments + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + crate::builtins::function::BuiltInFunctionObject::create_dynamic_function( + new_target, args, true, false, context, + ) + .map(Into::into) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/mod.rs new file mode 100644 index 0000000..dc1ddd3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/mod.rs @@ -0,0 +1,290 @@ +//! This module implements the global `BigInt` object. +//! +//! `BigInt` is a built-in object that provides a way to represent whole numbers larger +//! than the largest number JavaScript can reliably represent with the Number primitive +//! and represented by the `Number.MAX_SAFE_INTEGER` constant. +//! `BigInt` can be used for arbitrarily large integers. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-bigint-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt + +use crate::{ + builtins::{BuiltIn, JsArgs}, + object::ConstructorBuilder, + property::Attribute, + symbol::WellKnownSymbols, + value::{IntegerOrInfinity, PreferredType}, + Context, JsBigInt, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use num_bigint::ToBigInt; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +/// `BigInt` implementation. +#[derive(Debug, Clone, Copy)] +pub struct BigInt; + +impl BuiltIn for BigInt { + const NAME: &'static str = "BigInt"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().bigint_object().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .callable(true) + .constructor(true) + .method(Self::to_string, "toString", 0) + .method(Self::value_of, "valueOf", 0) + .static_method(Self::as_int_n, "asIntN", 2) + .static_method(Self::as_uint_n, "asUintN", 2) + .property( + to_string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl BigInt { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// `BigInt()` + /// + /// The `BigInt()` constructor is used to create `BigInt` objects. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint-objects + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/BigInt + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is not undefined, throw a TypeError exception. + if !new_target.is_undefined() { + return context.throw_type_error("BigInt is not a constructor"); + } + + let value = args.get_or_undefined(0); + + // 2. Let prim be ? ToPrimitive(value, number). + let prim = value.to_primitive(context, PreferredType::Number)?; + + // 3. If Type(prim) is Number, return ? NumberToBigInt(prim). + if let Some(number) = prim.as_number() { + return Self::number_to_bigint(number, context); + } + + // 4. Otherwise, return ? ToBigInt(value). + Ok(value.to_bigint(context)?.into()) + } + + /// `NumberToBigInt ( number )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-numbertobigint + #[inline] + fn number_to_bigint(number: f64, context: &mut Context) -> JsResult { + // 1. If IsIntegralNumber(number) is false, throw a RangeError exception. + if number.is_nan() || number.is_infinite() || number.fract() != 0.0 { + return context.throw_range_error(format!("Cannot convert {number} to BigInt")); + } + + // 2. Return the BigInt value that represents ℝ(number). + Ok(JsBigInt::from(number.to_bigint().expect("This conversion must be safe")).into()) + } + + /// The abstract operation `thisBigIntValue` takes argument value. + /// + /// The phrase “this `BigInt` value” within the specification of a method refers to the + /// result returned by calling the abstract operation `thisBigIntValue` with the `this` value + /// of the method invocation passed as the argument. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-thisbigintvalue + #[inline] + fn this_bigint_value(value: &JsValue, context: &mut Context) -> JsResult { + value + // 1. If Type(value) is BigInt, return value. + .as_bigint() + .cloned() + // 2. If Type(value) is Object and value has a [[BigIntData]] internal slot, then + // a. Assert: Type(value.[[BigIntData]]) is BigInt. + // b. Return value.[[BigIntData]]. + .or_else(|| { + value + .as_object() + .and_then(|obj| obj.borrow().as_bigint().cloned()) + }) + // 3. Throw a TypeError exception. + .ok_or_else(|| context.construct_type_error("'this' is not a BigInt")) + } + + /// `BigInt.prototype.toString( [radix] )` + /// + /// The `toString()` method returns a string representing the specified `BigInt` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let x be ? thisBigIntValue(this value). + let x = Self::this_bigint_value(this, context)?; + + let radix = args.get_or_undefined(0); + + // 2. If radix is undefined, let radixMV be 10. + let radix_mv = if radix.is_undefined() { + // 5. If radixMV = 10, return ! ToString(x). + // Note: early return optimization. + return Ok(x.to_string().into()); + // 3. Else, let radixMV be ? ToIntegerOrInfinity(radix). + } else { + radix.to_integer_or_infinity(context)? + }; + + // 4. If radixMV < 2 or radixMV > 36, throw a RangeError exception. + let radix_mv = match radix_mv { + IntegerOrInfinity::Integer(i) if (2..=36).contains(&i) => i, + _ => { + return context.throw_range_error( + "radix must be an integer at least 2 and no greater than 36", + ) + } + }; + + // 5. If radixMV = 10, return ! ToString(x). + if radix_mv == 10 { + return Ok(x.to_string().into()); + } + + // 1. Let x be ? thisBigIntValue(this value). + // 6. Return the String representation of this Number value using the radix specified by radixMV. + // Letters a-z are used for digits with values 10 through 35. + // The precise algorithm is implementation-defined, however the algorithm should be a generalization of that specified in 6.1.6.2.23. + Ok(JsValue::new(x.to_string_radix(radix_mv as u32))) + } + + /// `BigInt.prototype.valueOf()` + /// + /// The `valueOf()` method returns the wrapped primitive value of a Number object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/valueOf + pub(crate) fn value_of( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Ok(JsValue::new(Self::this_bigint_value(this, context)?)) + } + + /// `BigInt.asIntN()` + /// + /// The `BigInt.asIntN()` method wraps the value of a `BigInt` to a signed integer between `-2**(width - 1)` and `2**(width-1) - 1`. + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.asintn + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asIntN + #[allow(clippy::wrong_self_convention)] + pub(crate) fn as_int_n( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let (modulo, bits) = Self::calculate_as_uint_n(args, context)?; + + if bits > 0 + && modulo + >= JsBigInt::pow( + &JsBigInt::new(2), + &JsBigInt::new(i64::from(bits) - 1), + context, + )? + { + Ok(JsValue::new(JsBigInt::sub( + &modulo, + &JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)), context)?, + ))) + } else { + Ok(JsValue::new(modulo)) + } + } + + /// `BigInt.asUintN()` + /// + /// The `BigInt.asUintN()` method wraps the value of a `BigInt` to an unsigned integer between `0` and `2**(width) - 1`. + /// + /// [spec]: https://tc39.es/ecma262/#sec-bigint.asuintn + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/BigInt/asUintN + #[allow(clippy::wrong_self_convention)] + pub(crate) fn as_uint_n( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let (modulo, _) = Self::calculate_as_uint_n(args, context)?; + + Ok(JsValue::new(modulo)) + } + + /// Helper function to wrap the value of a `BigInt` to an unsigned integer. + /// + /// This function expects the same arguments as `as_uint_n` and wraps the value of a `BigInt`. + /// Additionally to the wrapped unsigned value it returns the converted `bits` argument, so it + /// can be reused from the `as_int_n` method. + fn calculate_as_uint_n(args: &[JsValue], context: &mut Context) -> JsResult<(JsBigInt, u32)> { + let bits_arg = args.get_or_undefined(0); + let bigint_arg = args.get_or_undefined(1); + + let bits = bits_arg.to_index(context)?; + let bits = u32::try_from(bits).unwrap_or(u32::MAX); + + let bigint = bigint_arg.to_bigint(context)?; + + Ok(( + JsBigInt::mod_floor( + &bigint, + &JsBigInt::pow(&JsBigInt::new(2), &JsBigInt::new(i64::from(bits)), context)?, + ), + bits, + )) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/tests.rs new file mode 100644 index 0000000..86a1cb1 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/bigint/tests.rs @@ -0,0 +1,413 @@ +use crate::{forward, Context}; + +#[test] +fn equality() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "0n == 0n"), "true"); + assert_eq!(forward(&mut context, "1n == 0n"), "false"); + assert_eq!( + forward( + &mut context, + "1000000000000000000000000000000000n == 1000000000000000000000000000000000n" + ), + "true" + ); + + assert_eq!(forward(&mut context, "0n == ''"), "true"); + assert_eq!(forward(&mut context, "100n == '100'"), "true"); + assert_eq!(forward(&mut context, "100n == '100.5'"), "false"); + assert_eq!( + forward(&mut context, "10000000000000000n == '10000000000000000'"), + "true" + ); + + assert_eq!(forward(&mut context, "'' == 0n"), "true"); + assert_eq!(forward(&mut context, "'100' == 100n"), "true"); + assert_eq!(forward(&mut context, "'100.5' == 100n"), "false"); + assert_eq!( + forward(&mut context, "'10000000000000000' == 10000000000000000n"), + "true" + ); + + assert_eq!(forward(&mut context, "0n == 0"), "true"); + assert_eq!(forward(&mut context, "0n == 0.0"), "true"); + assert_eq!(forward(&mut context, "100n == 100"), "true"); + assert_eq!(forward(&mut context, "100n == 100.0"), "true"); + assert_eq!(forward(&mut context, "100n == '100.5'"), "false"); + assert_eq!(forward(&mut context, "100n == '1005'"), "false"); + assert_eq!( + forward(&mut context, "10000000000000000n == 10000000000000000"), + "true" + ); + + assert_eq!(forward(&mut context, "0 == 0n"), "true"); + assert_eq!(forward(&mut context, "0.0 == 0n"), "true"); + assert_eq!(forward(&mut context, "100 == 100n"), "true"); + assert_eq!(forward(&mut context, "100.0 == 100n"), "true"); + assert_eq!(forward(&mut context, "100.5 == 100n"), "false"); + assert_eq!(forward(&mut context, "1005 == 100n"), "false"); + assert_eq!( + forward(&mut context, "10000000000000000 == 10000000000000000n"), + "true" + ); +} + +#[test] +fn bigint_function_conversion_from_integer() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "BigInt(1000)"), "1000n"); + assert_eq!( + forward(&mut context, "BigInt(20000000000000000)"), + "20000000000000000n" + ); + assert_eq!( + forward(&mut context, "BigInt(1000000000000000000000000000000000)"), + "999999999999999945575230987042816n" + ); +} + +#[test] +fn bigint_function_conversion_from_rational() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "BigInt(0.0)"), "0n"); + assert_eq!(forward(&mut context, "BigInt(1.0)"), "1n"); + assert_eq!(forward(&mut context, "BigInt(10000.0)"), "10000n"); +} + +#[test] +fn bigint_function_conversion_from_rational_with_fractional_part() { + let mut context = Context::default(); + + let scenario = r#" + try { + BigInt(0.1); + } catch (e) { + e.toString(); + } + "#; + assert_eq!( + forward(&mut context, scenario), + "\"RangeError: Cannot convert 0.1 to BigInt\"" + ); +} + +#[test] +fn bigint_function_conversion_from_null() { + let mut context = Context::default(); + + let scenario = r#" + try { + BigInt(null); + } catch (e) { + e.toString(); + } + "#; + assert_eq!( + forward(&mut context, scenario), + "\"TypeError: cannot convert null to a BigInt\"" + ); +} + +#[test] +fn bigint_function_conversion_from_undefined() { + let mut context = Context::default(); + + let scenario = r#" + try { + BigInt(undefined); + } catch (e) { + e.toString(); + } + "#; + assert_eq!( + forward(&mut context, scenario), + "\"TypeError: cannot convert undefined to a BigInt\"" + ); +} + +#[test] +fn bigint_function_conversion_from_string() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "BigInt('')"), "0n"); + assert_eq!(forward(&mut context, "BigInt(' ')"), "0n"); + assert_eq!( + forward(&mut context, "BigInt('200000000000000000')"), + "200000000000000000n" + ); + assert_eq!( + forward(&mut context, "BigInt('1000000000000000000000000000000000')"), + "1000000000000000000000000000000000n" + ); + assert_eq!(forward(&mut context, "BigInt('0b1111')"), "15n"); + assert_eq!(forward(&mut context, "BigInt('0o70')"), "56n"); + assert_eq!(forward(&mut context, "BigInt('0xFF')"), "255n"); +} + +#[test] +fn add() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "10000n + 1000n"), "11000n"); +} + +#[test] +fn sub() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "10000n - 1000n"), "9000n"); +} + +#[test] +fn mul() { + let mut context = Context::default(); + + assert_eq!( + forward(&mut context, "123456789n * 102030n"), + "12596296181670n" + ); +} + +#[test] +fn div() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "15000n / 50n"), "300n"); +} + +#[test] +fn div_with_truncation() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "15001n / 50n"), "300n"); +} + +#[test] +fn r#mod() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "15007n % 10n"), "7n"); +} + +#[test] +fn pow() { + let mut context = Context::default(); + + assert_eq!( + forward(&mut context, "100n ** 10n"), + "100000000000000000000n" + ); +} + +#[test] +fn pow_negative_exponent() { + let mut context = Context::default(); + + assert_throws(&mut context, "10n ** (-10n)", "RangeError"); +} + +#[test] +fn shl() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "8n << 2n"), "32n"); +} + +#[test] +fn shl_out_of_range() { + let mut context = Context::default(); + + assert_throws(&mut context, "1000n << 1000000000000000n", "RangeError"); +} + +#[test] +fn shr() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "8n >> 2n"), "2n"); +} + +#[test] +fn shr_out_of_range() { + let mut context = Context::default(); + + assert_throws(&mut context, "1000n >> 1000000000000000n", "RangeError"); +} + +#[test] +fn to_string() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "1000n.toString()"), "\"1000\""); + assert_eq!(forward(&mut context, "1000n.toString(2)"), "\"1111101000\""); + assert_eq!(forward(&mut context, "255n.toString(16)"), "\"ff\""); + assert_eq!(forward(&mut context, "1000n.toString(36)"), "\"rs\""); +} + +#[test] +fn to_string_invalid_radix() { + let mut context = Context::default(); + + assert_throws(&mut context, "10n.toString(null)", "RangeError"); + assert_throws(&mut context, "10n.toString(-1)", "RangeError"); + assert_throws(&mut context, "10n.toString(37)", "RangeError"); +} + +#[test] +fn as_int_n() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "BigInt.asIntN(0, 1n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asIntN(1, 1n)"), "-1n"); + assert_eq!(forward(&mut context, "BigInt.asIntN(3, 10n)"), "2n"); + assert_eq!(forward(&mut context, "BigInt.asIntN({}, 1n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asIntN(2, 0n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asIntN(2, -0n)"), "0n"); + + assert_eq!( + forward(&mut context, "BigInt.asIntN(2, -123456789012345678901n)"), + "-1n" + ); + assert_eq!( + forward(&mut context, "BigInt.asIntN(2, -123456789012345678900n)"), + "0n" + ); + + assert_eq!( + forward(&mut context, "BigInt.asIntN(2, 123456789012345678900n)"), + "0n" + ); + assert_eq!( + forward(&mut context, "BigInt.asIntN(2, 123456789012345678901n)"), + "1n" + ); + + assert_eq!( + forward( + &mut context, + "BigInt.asIntN(200, 0xcffffffffffffffffffffffffffffffffffffffffffffffffffn)" + ), + "-1n" + ); + assert_eq!( + forward( + &mut context, + "BigInt.asIntN(201, 0xcffffffffffffffffffffffffffffffffffffffffffffffffffn)" + ), + "1606938044258990275541962092341162602522202993782792835301375n" + ); + + assert_eq!( + forward( + &mut context, + "BigInt.asIntN(200, 0xc89e081df68b65fedb32cffea660e55df9605650a603ad5fc54n)" + ), + "-741470203160010616172516490008037905920749803227695190508460n" + ); + assert_eq!( + forward( + &mut context, + "BigInt.asIntN(201, 0xc89e081df68b65fedb32cffea660e55df9605650a603ad5fc54n)" + ), + "865467841098979659369445602333124696601453190555097644792916n" + ); +} + +#[test] +fn as_int_n_errors() { + let mut context = Context::default(); + + assert_throws(&mut context, "BigInt.asIntN(-1, 0n)", "RangeError"); + assert_throws(&mut context, "BigInt.asIntN(-2.5, 0n)", "RangeError"); + assert_throws( + &mut context, + "BigInt.asIntN(9007199254740992, 0n)", + "RangeError", + ); + assert_throws(&mut context, "BigInt.asIntN(0n, 0n)", "TypeError"); +} + +#[test] +fn as_uint_n() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "BigInt.asUintN(0, -2n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(0, -1n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(0, 0n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(0, 1n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(0, 2n)"), "0n"); + + assert_eq!(forward(&mut context, "BigInt.asUintN(1, -3n)"), "1n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, -2n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, -1n)"), "1n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, 0n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, 1n)"), "1n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, 2n)"), "0n"); + assert_eq!(forward(&mut context, "BigInt.asUintN(1, 3n)"), "1n"); + + assert_eq!( + forward(&mut context, "BigInt.asUintN(1, -123456789012345678901n)"), + "1n" + ); + assert_eq!( + forward(&mut context, "BigInt.asUintN(1, -123456789012345678900n)"), + "0n" + ); + assert_eq!( + forward(&mut context, "BigInt.asUintN(1, 123456789012345678900n)"), + "0n" + ); + assert_eq!( + forward(&mut context, "BigInt.asUintN(1, 123456789012345678901n)"), + "1n" + ); + + assert_eq!( + forward( + &mut context, + "BigInt.asUintN(200, 0xbffffffffffffffffffffffffffffffffffffffffffffffffffn)" + ), + "1606938044258990275541962092341162602522202993782792835301375n" + ); + assert_eq!( + forward( + &mut context, + "BigInt.asUintN(201, 0xbffffffffffffffffffffffffffffffffffffffffffffffffffn)" + ), + "3213876088517980551083924184682325205044405987565585670602751n" + ); +} + +#[test] +fn as_uint_n_errors() { + let mut context = Context::default(); + + assert_throws(&mut context, "BigInt.asUintN(-1, 0n)", "RangeError"); + assert_throws(&mut context, "BigInt.asUintN(-2.5, 0n)", "RangeError"); + assert_throws( + &mut context, + "BigInt.asUintN(9007199254740992, 0n)", + "RangeError", + ); + assert_throws(&mut context, "BigInt.asUintN(0n, 0n)", "TypeError"); +} + +fn assert_throws(context: &mut Context, src: &str, error_type: &str) { + let result = forward(context, src); + assert!(result.contains(error_type)); +} + +#[test] +fn division_by_zero() { + let mut context = Context::default(); + assert_throws(&mut context, "1n/0n", "RangeError"); +} + +#[test] +fn remainder_by_zero() { + let mut context = Context::default(); + assert_throws(&mut context, "1n % 0n", "RangeError"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/mod.rs new file mode 100644 index 0000000..4377cbd --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/mod.rs @@ -0,0 +1,123 @@ +//! This module implements the global `Boolean` object. +//! +//! The `Boolean` object is an object wrapper for a boolean value. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-boolean-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean + +#[cfg(test)] +mod tests; + +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +/// Boolean implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Boolean; + +impl BuiltIn for Boolean { + /// The name of the object. + const NAME: &'static str = "Boolean"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().boolean().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .method(Self::to_string, "toString", 0) + .method(Self::value_of, "valueOf", 0) + .build() + .conv::() + .pipe(Some) + } +} + +impl Boolean { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// `[[Construct]]` Create a new boolean object + /// + /// `[[Call]]` Creates a new boolean primitive + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // Get the argument, if any + let data = args.get(0).map_or(false, JsValue::to_boolean); + if new_target.is_undefined() { + return Ok(JsValue::new(data)); + } + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::boolean, context)?; + let boolean = JsObject::from_proto_and_data(prototype, ObjectData::boolean(data)); + + Ok(boolean.into()) + } + + /// An Utility function used to get the internal `[[BooleanData]]`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-thisbooleanvalue + fn this_boolean_value(value: &JsValue, context: &mut Context) -> JsResult { + value + .as_boolean() + .or_else(|| value.as_object().and_then(|obj| obj.borrow().as_boolean())) + .ok_or_else(|| context.construct_type_error("'this' is not a boolean")) + } + + /// The `toString()` method returns a string representing the specified `Boolean` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boolean-object + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let boolean = Self::this_boolean_value(this, context)?; + Ok(JsValue::new(boolean.to_string())) + } + + /// The valueOf() method returns the primitive value of a `Boolean` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boolean.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean/valueOf + #[inline] + pub(crate) fn value_of( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Ok(JsValue::new(Self::this_boolean_value(this, context)?)) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/tests.rs new file mode 100644 index 0000000..3d6ae4a --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/boolean/tests.rs @@ -0,0 +1,65 @@ +use crate::{forward, forward_val, Context}; + +/// Test the correct type is returned from call and construct +#[allow(clippy::unwrap_used)] +#[test] +fn construct_and_call() { + let mut context = Context::default(); + let init = r#" + var one = new Boolean(1); + var zero = Boolean(0); + "#; + eprintln!("{}", forward(&mut context, init)); + let one = forward_val(&mut context, "one").unwrap(); + let zero = forward_val(&mut context, "zero").unwrap(); + + assert!(one.is_object()); + assert!(zero.is_boolean()); +} + +#[test] +fn constructor_gives_true_instance() { + let mut context = Context::default(); + let init = r#" + var trueVal = new Boolean(true); + var trueNum = new Boolean(1); + var trueString = new Boolean("true"); + var trueBool = new Boolean(trueVal); + "#; + + eprintln!("{}", forward(&mut context, init)); + let true_val = forward_val(&mut context, "trueVal").expect("value expected"); + let true_num = forward_val(&mut context, "trueNum").expect("value expected"); + let true_string = forward_val(&mut context, "trueString").expect("value expected"); + let true_bool = forward_val(&mut context, "trueBool").expect("value expected"); + + // Values should all be objects + assert!(true_val.is_object()); + assert!(true_num.is_object()); + assert!(true_string.is_object()); + assert!(true_bool.is_object()); + + // Values should all be truthy + assert!(true_val.to_boolean()); + assert!(true_num.to_boolean()); + assert!(true_string.to_boolean()); + assert!(true_bool.to_boolean()); +} + +#[test] +fn instances_have_correct_proto_set() { + let mut context = Context::default(); + let init = r#" + var boolInstance = new Boolean(true); + var boolProto = Boolean.prototype; + "#; + + eprintln!("{}", forward(&mut context, init)); + let bool_instance = forward_val(&mut context, "boolInstance").expect("value expected"); + let bool_prototype = forward_val(&mut context, "boolProto").expect("value expected"); + + assert_eq!( + &*bool_instance.as_object().unwrap().prototype(), + &bool_prototype.as_object().cloned() + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/mod.rs new file mode 100644 index 0000000..dd2687c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/mod.rs @@ -0,0 +1,577 @@ +//! This module implements the global `console` object. +//! +//! The `console` object can be accessed from any global object. +//! +//! The specifics of how it works varies from browser to browser, but there is a de facto set of features that are typically provided. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [WHATWG `console` specification][spec] +//! +//! [spec]: https://console.spec.whatwg.org/ +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/Console + +#![allow(clippy::print_stdout)] + +#[cfg(test)] +mod tests; + +use crate::{ + builtins::{BuiltIn, JsArgs}, + object::ObjectInitializer, + value::{display::display_obj, JsValue, Numeric}, + Context, JsResult, JsString, +}; +use boa_profiler::Profiler; +use rustc_hash::FxHashMap; +use std::time::SystemTime; +use tap::{Conv, Pipe}; + +/// This represents the different types of log messages. +#[derive(Debug)] +pub enum LogMessage { + Log(String), + Info(String), + Warn(String), + Error(String), +} + +/// Helper function for logging messages. +pub(crate) fn logger(msg: LogMessage, console_state: &Console) { + let indent = 2 * console_state.groups.len(); + + match msg { + LogMessage::Error(msg) => { + eprintln!("{msg:>indent$}"); + } + LogMessage::Log(msg) | LogMessage::Info(msg) | LogMessage::Warn(msg) => { + println!("{msg:>indent$}"); + } + } +} + +/// This represents the `console` formatter. +pub fn formatter(data: &[JsValue], context: &mut Context) -> JsResult { + let target = data + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + match data.len() { + 0 => Ok(String::new()), + 1 => Ok(target.to_string()), + _ => { + let mut formatted = String::new(); + let mut arg_index = 1; + let mut chars = target.chars(); + while let Some(c) = chars.next() { + if c == '%' { + let fmt = chars.next().unwrap_or('%'); + match fmt { + /* integer */ + 'd' | 'i' => { + let arg = match data.get_or_undefined(arg_index).to_numeric(context)? { + Numeric::Number(r) => (r.floor() + 0.0).to_string(), + Numeric::BigInt(int) => int.to_string(), + }; + formatted.push_str(&arg); + arg_index += 1; + } + /* float */ + 'f' => { + let arg = data.get_or_undefined(arg_index).to_number(context)?; + formatted.push_str(&format!("{arg:.6}")); + arg_index += 1; + } + /* object, FIXME: how to render this properly? */ + 'o' | 'O' => { + let arg = data.get_or_undefined(arg_index); + formatted.push_str(&arg.display().to_string()); + arg_index += 1; + } + /* string */ + 's' => { + let arg = data + .get(arg_index) + .cloned() + .unwrap_or_default() + .to_string(context)?; + formatted.push_str(&arg); + arg_index += 1; + } + '%' => formatted.push('%'), + /* TODO: %c is not implemented */ + c => { + formatted.push('%'); + formatted.push(c); + } + } + } else { + formatted.push(c); + }; + } + + /* unformatted data */ + for rest in data.iter().skip(arg_index) { + formatted.push_str(&format!(" {}", rest.to_string(context)?)); + } + + Ok(formatted) + } + } +} + +/// This is the internal console object state. +#[derive(Debug, Default)] +pub(crate) struct Console { + count_map: FxHashMap, + timer_map: FxHashMap, + groups: Vec, +} + +impl BuiltIn for Console { + const NAME: &'static str = "console"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + ObjectInitializer::new(context) + .function(Self::assert, "assert", 0) + .function(Self::clear, "clear", 0) + .function(Self::debug, "debug", 0) + .function(Self::error, "error", 0) + .function(Self::info, "info", 0) + .function(Self::log, "log", 0) + .function(Self::trace, "trace", 0) + .function(Self::warn, "warn", 0) + .function(Self::error, "exception", 0) + .function(Self::count, "count", 0) + .function(Self::count_reset, "countReset", 0) + .function(Self::group, "group", 0) + .function(Self::group, "groupCollapsed", 0) + .function(Self::group_end, "groupEnd", 0) + .function(Self::time, "time", 0) + .function(Self::time_log, "timeLog", 0) + .function(Self::time_end, "timeEnd", 0) + .function(Self::dir, "dir", 0) + .function(Self::dir, "dirxml", 0) + .build() + .conv::() + .pipe(Some) + } +} + +impl Console { + /// The name of the object. + pub(crate) const NAME: &'static str = "console"; + + /// `console.assert(condition, ...data)` + /// + /// Prints a JavaScript value to the standard error if first argument evaluates to `false` or there + /// were no arguments. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#assert + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert + pub(crate) fn assert( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let assertion = args.get(0).map_or(false, JsValue::to_boolean); + + if !assertion { + let mut args: Vec = args.iter().skip(1).cloned().collect(); + let message = "Assertion failed".to_string(); + if args.is_empty() { + args.push(JsValue::new(message)); + } else if !args[0].is_string() { + args.insert(0, JsValue::new(message)); + } else { + let concat = format!("{message}: {}", args[0].display()); + args[0] = JsValue::new(concat); + } + + logger( + LogMessage::Error(formatter(&args, context)?), + context.console(), + ); + } + + Ok(JsValue::undefined()) + } + + /// `console.clear()` + /// + /// Removes all groups and clears console if possible. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#clear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn clear(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + context.console_mut().groups.clear(); + Ok(JsValue::undefined()) + } + + /// `console.debug(...data)` + /// + /// Prints a JavaScript values with "debug" logLevel. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#debug + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug + pub(crate) fn debug(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Log(formatter(args, context)?), + context.console(), + ); + Ok(JsValue::undefined()) + } + + /// `console.error(...data)` + /// + /// Prints a JavaScript values with "error" logLevel. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#error + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error + pub(crate) fn error(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Error(formatter(args, context)?), + context.console(), + ); + Ok(JsValue::undefined()) + } + + /// `console.info(...data)` + /// + /// Prints a JavaScript values with "info" logLevel. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#info + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info + pub(crate) fn info(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Info(formatter(args, context)?), + context.console(), + ); + Ok(JsValue::undefined()) + } + + /// `console.log(...data)` + /// + /// Prints a JavaScript values with "log" logLevel. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#log + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log + pub(crate) fn log(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Log(formatter(args, context)?), + context.console(), + ); + Ok(JsValue::undefined()) + } + + fn get_stack_trace(context: &mut Context) -> Vec { + let mut stack_trace: Vec = vec![]; + + for frame in context.vm.frames.iter().rev() { + stack_trace.push( + context + .interner() + .resolve_expect(frame.code.name) + .to_owned(), + ); + } + + stack_trace + } + + /// `console.trace(...data)` + /// + /// Prints a stack trace with "trace" logLevel, optionally labelled by data. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#trace + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace + pub(crate) fn trace(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + if !args.is_empty() { + logger( + LogMessage::Log(formatter(args, context)?), + context.console(), + ); + + let stack_trace_dump = Self::get_stack_trace(context).join("\n"); + logger(LogMessage::Log(stack_trace_dump), context.console()); + } + + Ok(JsValue::undefined()) + } + + /// `console.warn(...data)` + /// + /// Prints a JavaScript values with "warn" logLevel. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#warn + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn + pub(crate) fn warn(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Warn(formatter(args, context)?), + context.console(), + ); + Ok(JsValue::undefined()) + } + + /// `console.count(label)` + /// + /// Prints number of times the function was called with that particular label. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#count + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count + pub(crate) fn count(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let label = match args.get(0) { + Some(value) => value.to_string(context)?, + None => "default".into(), + }; + + let msg = format!("count {label}:"); + let c = context.console_mut().count_map.entry(label).or_insert(0); + *c += 1; + + logger(LogMessage::Info(format!("{msg} {c}")), context.console()); + Ok(JsValue::undefined()) + } + + /// `console.countReset(label)` + /// + /// Resets the counter for label. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#countreset + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset + pub(crate) fn count_reset( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let label = match args.get(0) { + Some(value) => value.to_string(context)?, + None => "default".into(), + }; + + context.console_mut().count_map.remove(&label); + + logger( + LogMessage::Warn(format!("countReset {label}")), + context.console(), + ); + + Ok(JsValue::undefined()) + } + + /// Returns current system time in ms. + fn system_time_in_ms() -> u128 { + let now = SystemTime::now(); + now.duration_since(SystemTime::UNIX_EPOCH) + .expect("negative duration") + .as_millis() + } + + /// `console.time(label)` + /// + /// Starts the timer for given label. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#time + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time + pub(crate) fn time(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let label = match args.get(0) { + Some(value) => value.to_string(context)?, + None => "default".into(), + }; + + if context.console().timer_map.get(&label).is_some() { + logger( + LogMessage::Warn(format!("Timer '{label}' already exist")), + context.console(), + ); + } else { + let time = Self::system_time_in_ms(); + context.console_mut().timer_map.insert(label, time); + } + + Ok(JsValue::undefined()) + } + + /// `console.timeLog(label, ...data)` + /// + /// Prints elapsed time for timer with given label. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#timelog + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog + pub(crate) fn time_log( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let label = match args.get(0) { + Some(value) => value.to_string(context)?, + None => "default".into(), + }; + + if let Some(t) = context.console().timer_map.get(&label) { + let time = Self::system_time_in_ms(); + let mut concat = format!("{label}: {} ms", time - t); + for msg in args.iter().skip(1) { + concat = concat + " " + &msg.display().to_string(); + } + logger(LogMessage::Log(concat), context.console()); + } else { + logger( + LogMessage::Warn(format!("Timer '{label}' doesn't exist")), + context.console(), + ); + } + + Ok(JsValue::undefined()) + } + + /// `console.timeEnd(label)` + /// + /// Removes the timer with given label. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#timeend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd + pub(crate) fn time_end( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let label = match args.get(0) { + Some(value) => value.to_string(context)?, + None => "default".into(), + }; + + if let Some(t) = context.console_mut().timer_map.remove(label.as_str()) { + let time = Self::system_time_in_ms(); + logger( + LogMessage::Info(format!("{label}: {} ms - timer removed", time - t)), + context.console(), + ); + } else { + logger( + LogMessage::Warn(format!("Timer '{label}' doesn't exist")), + context.console(), + ); + } + + Ok(JsValue::undefined()) + } + + /// `console.group(...data)` + /// + /// Adds new group with name from formatted data to stack. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#group + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group + pub(crate) fn group(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let group_label = formatter(args, context)?; + + logger( + LogMessage::Info(format!("group: {group_label}")), + context.console(), + ); + context.console_mut().groups.push(group_label); + + Ok(JsValue::undefined()) + } + + /// `console.groupEnd(label)` + /// + /// Removes the last group from the stack. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#groupend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn group_end( + _: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + context.console_mut().groups.pop(); + + Ok(JsValue::undefined()) + } + + /// `console.dir(item, options)` + /// + /// Prints info about item + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [WHATWG `console` specification][spec] + /// + /// [spec]: https://console.spec.whatwg.org/#dir + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn dir(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + logger( + LogMessage::Info(display_obj(args.get_or_undefined(0), true)), + context.console(), + ); + Ok(JsValue::undefined()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/tests.rs new file mode 100644 index 0000000..11f03a3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/console/tests.rs @@ -0,0 +1,68 @@ +use crate::{builtins::console::formatter, Context, JsValue}; + +#[test] +fn formatter_no_args_is_empty_string() { + let mut context = Context::default(); + assert_eq!(formatter(&[], &mut context).unwrap(), ""); +} + +#[test] +fn formatter_empty_format_string_is_empty_string() { + let mut context = Context::default(); + let val = JsValue::new(""); + assert_eq!(formatter(&[val], &mut context).unwrap(), ""); +} + +#[test] +fn formatter_format_without_args_renders_verbatim() { + let mut context = Context::default(); + let val = [JsValue::new("%d %s %% %f")]; + let res = formatter(&val, &mut context).unwrap(); + assert_eq!(res, "%d %s %% %f"); +} + +#[test] +fn formatter_empty_format_string_concatenates_rest_of_args() { + let mut context = Context::default(); + + let val = [ + JsValue::new(""), + JsValue::new("to powinno zostać"), + JsValue::new("połączone"), + ]; + let res = formatter(&val, &mut context).unwrap(); + assert_eq!(res, " to powinno zostać połączone"); +} + +#[test] +fn formatter_utf_8_checks() { + let mut context = Context::default(); + + let val = [ + JsValue::new("Są takie chwile %dą %są tu%sów %привет%ź".to_string()), + JsValue::new(123), + JsValue::new(1.23), + JsValue::new("ł"), + ]; + let res = formatter(&val, &mut context).unwrap(); + assert_eq!(res, "Są takie chwile 123ą 1.23ą tułów %привет%ź"); +} + +#[test] +fn formatter_trailing_format_leader_renders() { + let mut context = Context::default(); + + let val = [JsValue::new("%%%%%"), JsValue::new("|")]; + let res = formatter(&val, &mut context).unwrap(); + assert_eq!(res, "%%% |"); +} + +#[test] +#[allow(clippy::approx_constant)] +fn formatter_float_format_works() { + let mut context = Context::default(); + + let val = [JsValue::new("%f"), JsValue::new(3.1415)]; + let res = formatter(&val, &mut context).unwrap(); + assert_eq!(res, "3.141500"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/dataview/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/dataview/mod.rs new file mode 100644 index 0000000..0bb9b24 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/dataview/mod.rs @@ -0,0 +1,1031 @@ +use crate::{ + builtins::{array_buffer::SharedMemoryOrder, typed_array::TypedArrayKind, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::Attribute, + symbol::WellKnownSymbols, + value::JsValue, + Context, JsResult, +}; +use boa_gc::{Finalize, Trace}; +use tap::{Conv, Pipe}; + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct DataView { + viewed_array_buffer: JsObject, + byte_length: usize, + byte_offset: usize, +} + +impl BuiltIn for DataView { + const NAME: &'static str = "DataView"; + + fn init(context: &mut Context) -> Option { + let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; + + let get_buffer = FunctionBuilder::native(context, Self::get_buffer) + .name("get buffer") + .build(); + + let get_byte_length = FunctionBuilder::native(context, Self::get_byte_length) + .name("get byteLength") + .build(); + + let get_byte_offset = FunctionBuilder::native(context, Self::get_byte_offset) + .name("get byteOffset") + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().data_view().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .accessor("buffer", Some(get_buffer), None, flag_attributes) + .accessor("byteLength", Some(get_byte_length), None, flag_attributes) + .accessor("byteOffset", Some(get_byte_offset), None, flag_attributes) + .method(Self::get_big_int64, "getBigInt64", 1) + .method(Self::get_big_uint64, "getBigUint64", 1) + .method(Self::get_float32, "getFloat32", 1) + .method(Self::get_float64, "getFloat64", 1) + .method(Self::get_int8, "getInt8", 1) + .method(Self::get_int16, "getInt16", 1) + .method(Self::get_int32, "getInt32", 1) + .method(Self::get_uint8, "getUint8", 1) + .method(Self::get_uint16, "getUint16", 1) + .method(Self::get_uint32, "getUint32", 1) + .method(Self::set_big_int64, "setBigInt64", 2) + .method(Self::set_big_uint64, "setBigUint64", 2) + .method(Self::set_float32, "setFloat32", 2) + .method(Self::set_float64, "setFloat64", 2) + .method(Self::set_int8, "setInt8", 2) + .method(Self::set_int16, "setInt16", 2) + .method(Self::set_int32, "setInt32", 2) + .method(Self::set_uint8, "setUint8", 2) + .method(Self::set_uint16, "setUint16", 2) + .method(Self::set_uint32, "setUint32", 2) + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl DataView { + pub(crate) const LENGTH: usize = 1; + + /// `25.3.2.1 DataView ( buffer [ , byteOffset [ , byteLength ] ] )` + /// + /// The `DataView` view provides a low-level interface for reading and writing multiple number + /// types in a binary `ArrayBuffer`, without having to care about the platform's endianness. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview-buffer-byteoffset-bytelength + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/DataView + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_length = args.get_or_undefined(2); + + let buffer_obj = args + .get_or_undefined(0) + .as_object() + .ok_or_else(|| context.construct_type_error("buffer must be an ArrayBuffer"))?; + + // 1. If NewTarget is undefined, throw a TypeError exception. + let (offset, view_byte_length) = { + if new_target.is_undefined() { + return context.throw_type_error("new target is undefined"); + } + // 2. Perform ? RequireInternalSlot(buffer, [[ArrayBufferData]]). + let buffer_borrow = buffer_obj.borrow(); + let buffer = buffer_borrow + .as_array_buffer() + .ok_or_else(|| context.construct_type_error("buffer must be an ArrayBuffer"))?; + + // 3. Let offset be ? ToIndex(byteOffset). + let offset = args.get_or_undefined(1).to_index(context)?; + // 4. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer.is_detached_buffer() { + return context.throw_type_error("ArrayBuffer is detached"); + } + // 5. Let bufferByteLength be buffer.[[ArrayBufferByteLength]]. + let buffer_byte_length = buffer.array_buffer_byte_length(); + // 6. If offset > bufferByteLength, throw a RangeError exception. + if offset > buffer_byte_length { + return context + .throw_range_error("Start offset is outside the bounds of the buffer"); + } + // 7. If byteLength is undefined, then + let view_byte_length = if byte_length.is_undefined() { + // a. Let viewByteLength be bufferByteLength - offset. + buffer_byte_length - offset + } else { + // 8.a. Let viewByteLength be ? ToIndex(byteLength). + let view_byte_length = byte_length.to_index(context)?; + // 8.b. If offset + viewByteLength > bufferByteLength, throw a RangeError exception. + if offset + view_byte_length > buffer_byte_length { + return context.throw_range_error("Invalid data view length"); + } + + view_byte_length + }; + (offset, view_byte_length) + }; + + // 9. Let O be ? OrdinaryCreateFromConstructor(NewTarget, "%DataView.prototype%", « [[DataView]], [[ViewedArrayBuffer]], [[ByteLength]], [[ByteOffset]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::data_view, context)?; + + // 10. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer_obj + .borrow() + .as_array_buffer() + .ok_or_else(|| context.construct_type_error("buffer must be an ArrayBuffer"))? + .is_detached_buffer() + { + return context.throw_type_error("ArrayBuffer can't be detached"); + } + + let obj = JsObject::from_proto_and_data( + prototype, + ObjectData::data_view(Self { + // 11. Set O.[[ViewedArrayBuffer]] to buffer. + viewed_array_buffer: buffer_obj.clone(), + // 12. Set O.[[ByteLength]] to viewByteLength. + byte_length: view_byte_length, + // 13. Set O.[[ByteOffset]] to offset. + byte_offset: offset, + }), + ); + + // 14. Return O. + Ok(obj.into()) + } + + /// `25.3.4.1 get DataView.prototype.buffer` + /// + /// The buffer accessor property represents the `ArrayBuffer` or `SharedArrayBuffer` referenced + /// by the `DataView` at construction time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-dataview.prototype.buffer + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/buffer + pub(crate) fn get_buffer( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[DataView]]). + let dataview = this.as_object().map(JsObject::borrow); + let dataview = dataview + .as_ref() + .and_then(|obj| obj.as_data_view()) + .ok_or_else(|| context.construct_type_error("`this` is not a DataView"))?; + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + let buffer = dataview.viewed_array_buffer.clone(); + // 5. Return buffer. + Ok(buffer.into()) + } + + /// `25.3.4.1 get DataView.prototype.byteLength` + /// + /// The `byteLength` accessor property represents the length (in bytes) of the dataview. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-dataview.prototype.bytelength + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/byteLength + pub(crate) fn get_byte_length( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[DataView]]). + let dataview = this.as_object().map(JsObject::borrow); + let dataview = dataview + .as_ref() + .and_then(|obj| obj.as_data_view()) + .ok_or_else(|| context.construct_type_error("`this` is not a DataView"))?; + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + let buffer_borrow = dataview.viewed_array_buffer.borrow(); + let borrow = buffer_borrow + .as_array_buffer() + .expect("DataView must be constructed with an ArrayBuffer"); + // 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if borrow.is_detached_buffer() { + return context.throw_type_error("ArrayBuffer is detached"); + } + // 6. Let size be O.[[ByteLength]]. + let size = dataview.byte_length; + // 7. Return 𝔽(size). + Ok(size.into()) + } + + /// `25.3.4.1 get DataView.prototype.byteOffset` + /// + /// The `byteOffset` accessor property represents the offset (in bytes) of this view from the + /// start of its `ArrayBuffer` or `SharedArrayBuffer`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-dataview.prototype.byteoffset + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/byteOffset + pub(crate) fn get_byte_offset( + this: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[DataView]]). + let dataview = this.as_object().map(JsObject::borrow); + let dataview = dataview + .as_ref() + .and_then(|obj| obj.as_data_view()) + .ok_or_else(|| context.construct_type_error("`this` is not a DataView"))?; + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + let buffer_borrow = dataview.viewed_array_buffer.borrow(); + let borrow = buffer_borrow + .as_array_buffer() + .expect("DataView must be constructed with an ArrayBuffer"); + // 5. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if borrow.is_detached_buffer() { + return context.throw_type_error("ArrayBuffer is detached"); + } + // 6. Let offset be O.[[ByteOffset]]. + let offset = dataview.byte_offset; + // 7. Return 𝔽(offset). + Ok(offset.into()) + } + + /// `25.3.1.1 GetViewValue ( view, requestIndex, isLittleEndian, type )` + /// + /// The abstract operation `GetViewValue` takes arguments view, requestIndex, `isLittleEndian`, + /// and type. It is used by functions on `DataView` instances to retrieve values from the + /// view's buffer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-getviewvalue + fn get_view_value( + view: &JsValue, + request_index: &JsValue, + is_little_endian: &JsValue, + t: TypedArrayKind, + context: &mut Context, + ) -> JsResult { + // 1. Perform ? RequireInternalSlot(view, [[DataView]]). + // 2. Assert: view has a [[ViewedArrayBuffer]] internal slot. + let view = view.as_object().map(JsObject::borrow); + let view = view + .as_ref() + .and_then(|obj| obj.as_data_view()) + .ok_or_else(|| context.construct_type_error("`this` is not a DataView"))?; + // 3. Let getIndex be ? ToIndex(requestIndex). + let get_index = request_index.to_index(context)?; + + // 4. Set isLittleEndian to ! ToBoolean(isLittleEndian). + let is_little_endian = is_little_endian.to_boolean(); + + // 5. Let buffer be view.[[ViewedArrayBuffer]]. + let buffer = &view.viewed_array_buffer; + let buffer_borrow = buffer.borrow(); + let buffer = buffer_borrow + .as_array_buffer() + .expect("Should be unreachable"); + + // 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer.is_detached_buffer() { + return context.throw_type_error("ArrayBuffer is detached"); + } + // 7. Let viewOffset be view.[[ByteOffset]]. + let view_offset = view.byte_offset; + + // 8. Let viewSize be view.[[ByteLength]]. + let view_size = view.byte_length; + + // 9. Let elementSize be the Element Size value specified in Table 72 for Element Type type. + let element_size = t.element_size(); + + // 10. If getIndex + elementSize > viewSize, throw a RangeError exception. + if get_index + element_size > view_size { + return context.throw_range_error("Offset is outside the bounds of the DataView"); + } + + // 11. Let bufferIndex be getIndex + viewOffset. + let buffer_index = get_index + view_offset; + + // 12. Return GetValueFromBuffer(buffer, bufferIndex, type, false, Unordered, isLittleEndian). + Ok(buffer.get_value_from_buffer( + buffer_index, + t, + false, + SharedMemoryOrder::Unordered, + Some(is_little_endian), + )) + } + + /// `25.3.4.5 DataView.prototype.getBigInt64 ( byteOffset [ , littleEndian ] )` + /// + /// The `getBigInt64()` method gets a signed 64-bit integer (long long) at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getbigint64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigInt64 + pub(crate) fn get_big_int64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::BigInt64, + context, + ) + } + + /// `25.3.4.6 DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )` + /// + /// The `getBigUint64()` method gets an unsigned 64-bit integer (unsigned long long) at the + /// specified byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getbiguint64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getBigUint64 + pub(crate) fn get_big_uint64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::BigUint64, + context, + ) + } + + /// `25.3.4.7 DataView.prototype.getBigUint64 ( byteOffset [ , littleEndian ] )` + /// + /// The `getFloat32()` method gets a signed 32-bit float (float) at the specified byte offset + /// from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getfloat32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat32 + pub(crate) fn get_float32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Float32, + context, + ) + } + + /// `25.3.4.8 DataView.prototype.getFloat64 ( byteOffset [ , littleEndian ] )` + /// + /// The `getFloat64()` method gets a signed 64-bit float (double) at the specified byte offset + /// from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getfloat64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getFloat64 + pub(crate) fn get_float64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Float64, + context, + ) + } + + /// `25.3.4.9 DataView.prototype.getInt8 ( byteOffset [ , littleEndian ] )` + /// + /// The `getInt8()` method gets a signed 8-bit integer (byte) at the specified byte offset + /// from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getint8 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt8 + pub(crate) fn get_int8( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int8, + context, + ) + } + + /// `25.3.4.10 DataView.prototype.getInt16 ( byteOffset [ , littleEndian ] )` + /// + /// The `getInt16()` method gets a signed 16-bit integer (short) at the specified byte offset + /// from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getint16 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt16 + pub(crate) fn get_int16( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int16, + context, + ) + } + + /// `25.3.4.11 DataView.prototype.getInt32 ( byteOffset [ , littleEndian ] )` + /// + /// The `getInt32()` method gets a signed 32-bit integer (long) at the specified byte offset + /// from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getint32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getInt32 + pub(crate) fn get_int32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int32, + context, + ) + } + + /// `25.3.4.12 DataView.prototype.getUint8 ( byteOffset [ , littleEndian ] )` + /// + /// The `getUint8()` method gets an unsigned 8-bit integer (unsigned byte) at the specified + /// byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getuint8 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint8 + pub(crate) fn get_uint8( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint8, + context, + ) + } + + /// `25.3.4.13 DataView.prototype.getUint16 ( byteOffset [ , littleEndian ] )` + /// + /// The `getUint16()` method gets an unsigned 16-bit integer (unsigned short) at the specified + /// byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getuint16 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint16 + pub(crate) fn get_uint16( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint16, + context, + ) + } + + /// `25.3.4.14 DataView.prototype.getUint32 ( byteOffset [ , littleEndian ] )` + /// + /// The `getUint32()` method gets an unsigned 32-bit integer (unsigned long) at the specified + /// byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.getuint32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/getUint32 + pub(crate) fn get_uint32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let is_little_endian = args.get_or_undefined(1); + // 1. Let v be the this value. + // 2. Return ? GetViewValue(v, byteOffset, littleEndian, BigInt64). + Self::get_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint32, + context, + ) + } + + /// `25.3.1.1 SetViewValue ( view, requestIndex, isLittleEndian, type )` + /// + /// The abstract operation `SetViewValue` takes arguments view, requestIndex, `isLittleEndian`, + /// type, and value. It is used by functions on `DataView` instances to store values into the + /// view's buffer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-setviewvalue + fn set_view_value( + view: &JsValue, + request_index: &JsValue, + is_little_endian: &JsValue, + t: TypedArrayKind, + value: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Perform ? RequireInternalSlot(view, [[DataView]]). + // 2. Assert: view has a [[ViewedArrayBuffer]] internal slot. + let view = view.as_object().map(JsObject::borrow); + let view = view + .as_ref() + .and_then(|obj| obj.as_data_view()) + .ok_or_else(|| context.construct_type_error("`this` is not a DataView"))?; + // 3. Let getIndex be ? ToIndex(requestIndex). + let get_index = request_index.to_index(context)?; + + let number_value = if t.is_big_int_element_type() { + // 4. If ! IsBigIntElementType(type) is true, let numberValue be ? ToBigInt(value). + value.to_bigint(context)?.into() + } else { + // 5. Otherwise, let numberValue be ? ToNumber(value). + value.to_number(context)?.into() + }; + + // 6. Set isLittleEndian to ! ToBoolean(isLittleEndian). + let is_little_endian = is_little_endian.to_boolean(); + // 7. Let buffer be view.[[ViewedArrayBuffer]]. + let buffer = &view.viewed_array_buffer; + let mut buffer_borrow = buffer.borrow_mut(); + let buffer = buffer_borrow + .as_array_buffer_mut() + .expect("Should be unreachable"); + + // 8. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer.is_detached_buffer() { + return context.throw_type_error("ArrayBuffer is detached"); + } + + // 9. Let viewOffset be view.[[ByteOffset]]. + let view_offset = view.byte_offset; + + // 10. Let viewSize be view.[[ByteLength]]. + let view_size = view.byte_length; + + // 11. Let elementSize be the Element Size value specified in Table 72 for Element Type type. + let element_size = t.element_size(); + + // 12. If getIndex + elementSize > viewSize, throw a RangeError exception. + if get_index + element_size > view_size { + return context.throw_range_error("Offset is outside the bounds of DataView"); + } + + // 13. Let bufferIndex be getIndex + viewOffset. + let buffer_index = get_index + view_offset; + + // 14. Return SetValueInBuffer(buffer, bufferIndex, type, numberValue, false, Unordered, isLittleEndian). + buffer.set_value_in_buffer( + buffer_index, + t, + &number_value, + SharedMemoryOrder::Unordered, + Some(is_little_endian), + context, + ) + } + + /// `25.3.4.15 DataView.prototype.setBigInt64 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setBigInt64()` method stores a signed 64-bit integer (long long) value at the + /// specified byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setbigint64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigInt64 + pub(crate) fn set_big_int64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, BigUint64, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::BigInt64, + value, + context, + ) + } + + /// `25.3.4.16 DataView.prototype.setBigUint64 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setBigUint64()` method stores an unsigned 64-bit integer (unsigned long long) value at + /// the specified byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setbiguint64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setBigUint64 + pub(crate) fn set_big_uint64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, BigUint64, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::BigUint64, + value, + context, + ) + } + + /// `25.3.4.17 DataView.prototype.setFloat32 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setFloat32()` method stores a signed 32-bit float (float) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setfloat32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat32 + pub(crate) fn set_float32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Float32, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Float32, + value, + context, + ) + } + + /// `25.3.4.18 DataView.prototype.setFloat64 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setFloat64()` method stores a signed 64-bit float (double) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setfloat64 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setFloat64 + pub(crate) fn set_float64( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Float64, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Float64, + value, + context, + ) + } + + /// `25.3.4.19 DataView.prototype.setInt8 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setInt8()` method stores a signed 8-bit integer (byte) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setint8 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt8 + pub(crate) fn set_int8( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Int8, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int8, + value, + context, + ) + } + + /// `25.3.4.20 DataView.prototype.setInt16 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setInt16()` method stores a signed 16-bit integer (short) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setint16 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt16 + pub(crate) fn set_int16( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Int16, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int16, + value, + context, + ) + } + + /// `25.3.4.21 DataView.prototype.setInt32 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setInt32()` method stores a signed 32-bit integer (long) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setint32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setInt32 + pub(crate) fn set_int32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Int32, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Int32, + value, + context, + ) + } + + /// `25.3.4.22 DataView.prototype.setUint8 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setUint8()` method stores an unsigned 8-bit integer (byte) value at the specified byte + /// offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setuint8 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint8 + pub(crate) fn set_uint8( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Uint8, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint8, + value, + context, + ) + } + + /// `25.3.4.23 DataView.prototype.setUint16 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setUint16()` method stores an unsigned 16-bit integer (unsigned short) value at the + /// specified byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setuint16 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint16 + pub(crate) fn set_uint16( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Uint16, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint16, + value, + context, + ) + } + + /// `25.3.4.24 DataView.prototype.setUint32 ( byteOffset, value [ , littleEndian ] )` + /// + /// The `setUint32()` method stores an unsigned 32-bit integer (unsigned long) value at the + /// specified byte offset from the start of the `DataView`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-dataview.prototype.setuint32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DataView/setUint32 + pub(crate) fn set_uint32( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let byte_offset = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + let is_little_endian = args.get_or_undefined(2); + // 1. Let v be the this value. + // 2. Return ? SetViewValue(v, byteOffset, littleEndian, Uint32, value). + Self::set_view_value( + this, + byte_offset, + is_little_endian, + TypedArrayKind::Uint32, + value, + context, + ) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/mod.rs new file mode 100644 index 0000000..f4c9fce --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/mod.rs @@ -0,0 +1,1938 @@ +#[cfg(test)] +mod tests; + +use super::JsArgs; +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + symbol::WellKnownSymbols, + value::{JsValue, PreferredType}, + Context, JsResult, JsString, +}; +use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_profiler::Profiler; +use chrono::{prelude::*, Duration, LocalResult}; +use std::fmt::Display; +use tap::{Conv, Pipe}; + +/// The number of nanoseconds in a millisecond. +const NANOS_PER_MS: i64 = 1_000_000; +/// The number of milliseconds in an hour. +const MILLIS_PER_HOUR: i64 = 3_600_000; +/// The number of milliseconds in a minute. +const MILLIS_PER_MINUTE: i64 = 60_000; +/// The number of milliseconds in a second. +const MILLIS_PER_SECOND: i64 = 1000; + +#[inline] +fn is_zero_or_normal_opt(value: Option) -> bool { + value.map_or(true, |value| value == 0f64 || value.is_normal()) +} + +macro_rules! check_normal_opt { + ($($v:expr),+) => { + $(is_zero_or_normal_opt($v.into()) &&)+ true + }; +} + +#[inline] +fn ignore_ambiguity(result: LocalResult) -> Option { + match result { + LocalResult::Ambiguous(v, _) | LocalResult::Single(v) => Some(v), + LocalResult::None => None, + } +} + +macro_rules! getter_method { + ($name:ident) => {{ + fn get_value(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + Ok(JsValue::new(this_time_value(this, context)?.$name())) + } + get_value + }}; + (Self::$name:ident) => {{ + fn get_value(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Ok(JsValue::new(Date::$name())) + } + get_value + }}; +} + +#[derive(Debug, Finalize, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Date(Option); + +impl Display for Date { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self.to_local() { + Some(v) => write!(f, "{}", v.format("%a %b %d %Y %H:%M:%S GMT%:z")), + _ => write!(f, "Invalid Date"), + } + } +} + +unsafe impl Trace for Date { + // Date is a stack value, it doesn't require tracing. + // only safe if `chrono` never implements `Trace` for `NaiveDateTime` + unsafe_empty_trace!(); +} + +impl Default for Date { + fn default() -> Self { + Self(Some(Utc::now().naive_utc())) + } +} + +impl BuiltIn for Date { + const NAME: &'static str = "Date"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().date().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .method(getter_method!(get_date), "getDate", 0) + .method(getter_method!(get_day), "getDay", 0) + .method(getter_method!(get_full_year), "getFullYear", 0) + .method(getter_method!(get_hours), "getHours", 0) + .method(getter_method!(get_milliseconds), "getMilliseconds", 0) + .method(getter_method!(get_minutes), "getMinutes", 0) + .method(getter_method!(get_month), "getMonth", 0) + .method(getter_method!(get_seconds), "getSeconds", 0) + .method(getter_method!(get_time), "getTime", 0) + .method(getter_method!(get_year), "getYear", 0) + .method(Self::get_timezone_offset, "getTimezoneOffset", 0) + .method(getter_method!(get_utc_date), "getUTCDate", 0) + .method(getter_method!(get_utc_day), "getUTCDay", 0) + .method(getter_method!(get_utc_full_year), "getUTCFullYear", 0) + .method(getter_method!(get_utc_hours), "getUTCHours", 0) + .method( + getter_method!(get_utc_milliseconds), + "getUTCMilliseconds", + 0, + ) + .method(getter_method!(get_utc_minutes), "getUTCMinutes", 0) + .method(getter_method!(get_utc_month), "getUTCMonth", 0) + .method(getter_method!(get_utc_seconds), "getUTCSeconds", 0) + .method(Self::set_date, "setDate", 1) + .method(Self::set_full_year, "setFullYear", 3) + .method(Self::set_hours, "setHours", 4) + .method(Self::set_milliseconds, "setMilliseconds", 1) + .method(Self::set_minutes, "setMinutes", 3) + .method(Self::set_month, "setMonth", 2) + .method(Self::set_seconds, "setSeconds", 2) + .method(Self::set_year, "setYear", 1) + .method(Self::set_time, "setTime", 1) + .method(Self::set_utc_date, "setUTCDate", 1) + .method(Self::set_utc_full_year, "setUTCFullYear", 3) + .method(Self::set_utc_hours, "setUTCHours", 4) + .method(Self::set_utc_milliseconds, "setUTCMilliseconds", 1) + .method(Self::set_utc_minutes, "setUTCMinutes", 3) + .method(Self::set_utc_month, "setUTCMonth", 2) + .method(Self::set_utc_seconds, "setUTCSeconds", 2) + .method(Self::to_date_string, "toDateString", 0) + .method(getter_method!(to_gmt_string), "toGMTString", 0) + .method(Self::to_iso_string, "toISOString", 0) + .method(Self::to_json, "toJSON", 1) + // Locale strings + .method(Self::to_string, "toString", 0) + .method(Self::to_time_string, "toTimeString", 0) + .method(getter_method!(to_utc_string), "toUTCString", 0) + .method(getter_method!(value_of), "valueOf", 0) + .method( + Self::to_primitive, + (WellKnownSymbols::to_primitive(), "[Symbol.toPrimitive]"), + 1, + ) + .static_method(Self::now, "now", 0) + .static_method(Self::parse, "parse", 1) + .static_method(Self::utc, "UTC", 7) + .build() + .conv::() + .pipe(Some) + } +} + +impl Date { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 7; + + /// Check if the time (number of milliseconds) is in the expected range. + /// Returns None if the time is not in the range, otherwise returns the time itself in option. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-timeclip + #[inline] + pub fn time_clip(time: f64) -> Option { + if time.abs() > 8.64e15 { + None + } else { + Some(time) + } + } + + /// Converts the `Date` to a local `DateTime`. + /// + /// If the `Date` is invalid (i.e. NAN), this function will return `None`. + #[inline] + pub fn to_local(self) -> Option> { + self.0 + .map(|utc| Local::now().timezone().from_utc_datetime(&utc)) + } + + /// Converts the `Date` to a UTC `DateTime`. + /// + /// If the `Date` is invalid (i.e. NAN), this function will return `None`. + pub fn to_utc(self) -> Option> { + self.0 + .map(|utc| Utc::now().timezone().from_utc_datetime(&utc)) + } + + /// Optionally sets the individual components of the `Date`. + /// + /// Each component does not have to be within the range of valid values. For example, if `month` is too large + /// then `year` will be incremented by the required amount. + #[allow(clippy::too_many_arguments)] + pub fn set_components( + &mut self, + utc: bool, + year: Option, + month: Option, + day: Option, + hour: Option, + minute: Option, + second: Option, + millisecond: Option, + ) { + #[inline] + fn num_days_in(year: i32, month: u32) -> Option { + let month = month + 1; // zero-based for calculations + + Some( + NaiveDate::from_ymd_opt( + match month { + 12 => year.checked_add(1)?, + _ => year, + }, + match month { + 12 => 1, + _ => month + 1, + }, + 1, + )? + .signed_duration_since(NaiveDate::from_ymd_opt(year, month, 1)?) + .num_days() as u32, + ) + } + + #[inline] + fn fix_month(year: i32, month: i32) -> Option<(i32, u32)> { + let year = year.checked_add(month / 12)?; + + if month < 0 { + let year = year.checked_sub(1)?; + let month = (11 + (month + 1) % 12) as u32; + Some((year, month)) + } else { + let month = (month % 12) as u32; + Some((year, month)) + } + } + + #[inline] + fn fix_day(mut year: i32, mut month: i32, mut day: i32) -> Option<(i32, u32, u32)> { + loop { + if day < 0 { + let (fixed_year, fixed_month) = fix_month(year, month.checked_sub(1)?)?; + + year = fixed_year; + month = fixed_month as i32; + day += num_days_in(fixed_year, fixed_month)? as i32; + } else { + let (fixed_year, fixed_month) = fix_month(year, month)?; + let num_days = num_days_in(fixed_year, fixed_month)? as i32; + + if day >= num_days { + day -= num_days; + month = month.checked_add(1)?; + } else { + break; + } + } + } + + let (fixed_year, fixed_month) = fix_month(year, month)?; + Some((fixed_year, fixed_month, day as u32)) + } + + // If any of the args are infinity or NaN, return an invalid date. + if !check_normal_opt!(year, month, day, hour, minute, second, millisecond) { + self.0 = None; + return; + } + + let naive = if utc { + self.to_utc().map(|dt| dt.naive_utc()) + } else { + self.to_local().map(|dt| dt.naive_local()) + }; + + self.0 = naive.and_then(|naive| { + let year = year.unwrap_or_else(|| f64::from(naive.year())) as i32; + let month = month.unwrap_or_else(|| f64::from(naive.month0())) as i32; + let day = (day.unwrap_or_else(|| f64::from(naive.day())) as i32).checked_sub(1)?; + let hour = hour.unwrap_or_else(|| f64::from(naive.hour())) as i64; + let minute = minute.unwrap_or_else(|| f64::from(naive.minute())) as i64; + let second = second.unwrap_or_else(|| f64::from(naive.second())) as i64; + let millisecond = millisecond + .unwrap_or_else(|| f64::from(naive.nanosecond()) / NANOS_PER_MS as f64) + as i64; + + let (year, month, day) = fix_day(year, month, day)?; + + let duration_hour = Duration::milliseconds(hour.checked_mul(MILLIS_PER_HOUR)?); + let duration_minute = Duration::milliseconds(minute.checked_mul(MILLIS_PER_MINUTE)?); + let duration_second = Duration::milliseconds(second.checked_mul(MILLIS_PER_SECOND)?); + let duration_millisecond = Duration::milliseconds(millisecond); + + let duration = duration_hour + .checked_add(&duration_minute)? + .checked_add(&duration_second)? + .checked_add(&duration_millisecond)?; + + NaiveDate::from_ymd_opt(year, month + 1, day + 1) + .and_then(|dt| dt.and_hms(0, 0, 0).checked_add_signed(duration)) + .and_then(|dt| { + if utc { + Some(Utc.from_utc_datetime(&dt).naive_utc()) + } else { + ignore_ambiguity(Local.from_local_datetime(&dt)).map(|dt| dt.naive_utc()) + } + }) + .filter(|dt| Self::time_clip(dt.timestamp_millis() as f64).is_some()) + }); + } + + /// `Date()` + /// + /// Creates a JavaScript `Date` instance that represents a single moment in time in a platform-independent format. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if new_target.is_undefined() { + Ok(Self::make_date_string()) + } else { + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::date, context)?; + Ok(if args.is_empty() { + Self::make_date_now(prototype) + } else if args.len() == 1 { + Self::make_date_single(prototype, args, context)? + } else { + Self::make_date_multiple(prototype, args, context)? + } + .into()) + } + } + + /// `Date()` + /// + /// The `Date()` function is used to create a string that represent the current date and time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + pub(crate) fn make_date_string() -> JsValue { + JsValue::new(Local::now().to_rfc3339()) + } + + /// `Date()` + /// + /// The newly-created `Date` object represents the current date and time as of the time of instantiation. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + pub(crate) fn make_date_now(prototype: JsObject) -> JsObject { + JsObject::from_proto_and_data(prototype, ObjectData::date(Self::default())) + } + + /// `Date(value)` + /// + /// The newly-created `Date` object represents the value provided to the constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + pub(crate) fn make_date_single( + prototype: JsObject, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let value = &args[0]; + let tv = match this_time_value(value, context) { + Ok(dt) => dt.0, + _ => match value.to_primitive(context, PreferredType::Default)? { + JsValue::String(ref str) => match chrono::DateTime::parse_from_rfc3339(str) { + Ok(dt) => Some(dt.naive_utc()), + _ => None, + }, + tv => { + let tv = tv.to_number(context)?; + if tv.is_nan() { + None + } else { + let secs = (tv / 1_000f64) as i64; + let nano_secs = ((tv % 1_000f64) * 1_000_000f64) as u32; + NaiveDateTime::from_timestamp_opt(secs, nano_secs) + } + } + }, + }; + + let tv = tv.filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + Ok(JsObject::from_proto_and_data( + prototype, + ObjectData::date(Self(tv)), + )) + } + + /// `Date(year, month [ , date [ , hours [ , minutes [ , seconds [ , ms ] ] ] ] ])` + /// + /// The newly-created `Date` object represents the date components provided to the constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date-constructor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date + pub(crate) fn make_date_multiple( + prototype: JsObject, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let mut year = args[0].to_number(context)?; + let month = args[1].to_number(context)?; + let day = args + .get(2) + .map_or(Ok(1f64), |value| value.to_number(context))?; + let hour = args + .get(3) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let min = args + .get(4) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let sec = args + .get(5) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let milli = args + .get(6) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + // If any of the args are infinity or NaN, return an invalid date. + if !check_normal_opt!(year, month, day, hour, min, sec, milli) { + return Ok(JsObject::from_proto_and_data( + prototype, + ObjectData::date(Self(None)), + )); + } + + if (0.0..=99.0).contains(&year) { + year += 1900.0; + } + + let mut date = Self( + NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()), + ); + + date.set_components( + false, + Some(year), + Some(month), + Some(day), + Some(hour), + Some(min), + Some(sec), + Some(milli), + ); + + Ok(JsObject::from_proto_and_data( + prototype, + ObjectData::date(date), + )) + } + + /// `Date.prototype[@@toPrimitive]` + /// + /// The [@@toPrimitive]() method converts a Date object to a primitive value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype-@@toprimitive + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/@@toPrimitive + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_primitive( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. If Type(O) is not Object, throw a TypeError exception. + let o = if let Some(o) = this.as_object() { + o + } else { + return context.throw_type_error("Date.prototype[@@toPrimitive] called on non object"); + }; + + let hint = args.get_or_undefined(0); + + let try_first = match hint.as_string().map(JsString::as_str) { + // 3. If hint is "string" or "default", then + // a. Let tryFirst be string. + Some("string" | "default") => PreferredType::String, + // 4. Else if hint is "number", then + // a. Let tryFirst be number. + Some("number") => PreferredType::Number, + // 5. Else, throw a TypeError exception. + _ => { + return context + .throw_type_error("Date.prototype[@@toPrimitive] called with invalid hint") + } + }; + + // 6. Return ? OrdinaryToPrimitive(O, tryFirst). + o.ordinary_to_primitive(context, try_first) + } + + /// `Date.prototype.getDate()` + /// + /// The `getDate()` method returns the day of the month for the specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getdate + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDate + pub fn get_date(&self) -> f64 { + self.to_local().map_or(f64::NAN, |dt| f64::from(dt.day())) + } + + /// `Date.prototype.getDay()` + /// + /// The `getDay()` method returns the day of the week for the specified date according to local time, where 0 + /// represents Sunday. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getday + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getDay + pub fn get_day(&self) -> f64 { + self.to_local().map_or(f64::NAN, |dt| { + let weekday = dt.weekday() as u32; + let weekday = (weekday + 1) % 7; // 0 represents Monday in Chrono + f64::from(weekday) + }) + } + + /// `Date.prototype.getFullYear()` + /// + /// The `getFullYear()` method returns the year of the specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getfullyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getFullYear + pub fn get_full_year(&self) -> f64 { + self.to_local().map_or(f64::NAN, |dt| f64::from(dt.year())) + } + + /// `Date.prototype.getHours()` + /// + /// The `getHours()` method returns the hour for the specified date, according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.gethours + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getHours + pub fn get_hours(&self) -> f64 { + self.to_local().map_or(f64::NAN, |dt| f64::from(dt.hour())) + } + + /// `Date.prototype.getMilliseconds()` + /// + /// The `getMilliseconds()` method returns the milliseconds in the specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getmilliseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMilliseconds + pub fn get_milliseconds(&self) -> f64 { + self.to_local().map_or(f64::NAN, |dt| { + f64::from(dt.nanosecond()) / NANOS_PER_MS as f64 + }) + } + + /// `Date.prototype.getMinutes()` + /// + /// The `getMinutes()` method returns the minutes in the specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getminutes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMinutes + pub fn get_minutes(&self) -> f64 { + self.to_local() + .map_or(f64::NAN, |dt| f64::from(dt.minute())) + } + + /// `Date.prototype.getMonth()` + /// + /// The `getMonth()` method returns the month in the specified date according to local time, as a zero-based value + /// (where zero indicates the first month of the year). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getmonth + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getMonth + pub fn get_month(&self) -> f64 { + self.to_local() + .map_or(f64::NAN, |dt| f64::from(dt.month0())) + } + + /// `Date.prototype.getSeconds()` + /// + /// The `getSeconds()` method returns the seconds in the specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getSeconds + pub fn get_seconds(&self) -> f64 { + self.to_local() + .map_or(f64::NAN, |dt| f64::from(dt.second())) + } + + /// `Date.prototype.getYear()` + /// + /// The `getYear()` method returns the year in the specified date according to local time. + /// Because `getYear()` does not return full years ("year 2000 problem"), it is no longer used + /// and has been replaced by the `getFullYear()` method. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getYear + pub fn get_year(&self) -> f64 { + self.to_local() + .map_or(f64::NAN, |dt| f64::from(dt.year()) - 1900f64) + } + + /// `Date.prototype.getTime()` + /// + /// The `getTime()` method returns the number of milliseconds since the Unix Epoch. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.gettime + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTime + pub fn get_time(&self) -> f64 { + self.to_utc() + .map_or(f64::NAN, |dt| dt.timestamp_millis() as f64) + } + + /// `Date.prototype.getTimeZoneOffset()` + /// + /// The `getTimezoneOffset()` method returns the time zone difference, in minutes, from current locale (host system + /// settings) to UTC. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.gettimezoneoffset + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getTimezoneOffset + #[inline] + pub fn get_timezone_offset( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let t = this_time_value(this, context)?; + + // 2. If t is NaN, return NaN. + if t.0.is_none() { + return Ok(JsValue::nan()); + } + + // 3. Return (t - LocalTime(t)) / msPerMinute. + Ok(JsValue::new( + f64::from(-Local::now().offset().local_minus_utc()) / 60f64, + )) + } + + /// `Date.prototype.getUTCDate()` + /// + /// The `getUTCDate()` method returns the day (date) of the month in the specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcdate + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCDate + pub fn get_utc_date(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.day())) + } + + /// `Date.prototype.getUTCDay()` + /// + /// The `getUTCDay()` method returns the day of the week in the specified date according to universal time, where 0 + /// represents Sunday. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcday + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCDay + pub fn get_utc_day(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| { + let weekday = dt.weekday() as u32; + let weekday = (weekday + 1) % 7; // 0 represents Monday in Chrono + f64::from(weekday) + }) + } + + /// `Date.prototype.getUTCFullYear()` + /// + /// The `getUTCFullYear()` method returns the year in the specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcfullyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCFullYear + pub fn get_utc_full_year(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.year())) + } + + /// `Date.prototype.getUTCHours()` + /// + /// The `getUTCHours()` method returns the hours in the specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutchours + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCHours + pub fn get_utc_hours(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.hour())) + } + + /// `Date.prototype.getUTCMilliseconds()` + /// + /// The `getUTCMilliseconds()` method returns the milliseconds portion of the time object's value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcmilliseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMilliseconds + pub fn get_utc_milliseconds(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| { + f64::from(dt.nanosecond()) / NANOS_PER_MS as f64 + }) + } + + /// `Date.prototype.getUTCMinutes()` + /// + /// The `getUTCMinutes()` method returns the minutes in the specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcminutes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMinutes + pub fn get_utc_minutes(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.minute())) + } + + /// `Date.prototype.getUTCMonth()` + /// + /// The `getUTCMonth()` returns the month of the specified date according to universal time, as a zero-based value + /// (where zero indicates the first month of the year). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcmonth + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCMonth + pub fn get_utc_month(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.month0())) + } + + /// `Date.prototype.getUTCSeconds()` + /// + /// The `getUTCSeconds()` method returns the seconds in the specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.getutcseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/getUTCSeconds + pub fn get_utc_seconds(&self) -> f64 { + self.to_utc().map_or(f64::NAN, |dt| f64::from(dt.second())) + } + + /// `Date.prototype.setDate()` + /// + /// The `setDate()` method sets the day of the `Date` object relative to the beginning of the currently set + /// month. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setdate + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setDate + pub fn set_date(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let dt be ? ToNumber(date). + let dt = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). + t.set_components(false, None, None, Some(dt), None, None, None, None); + + // 4. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 6. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setFullYear()` + /// + /// The `setFullYear()` method sets the full year for a specified date according to local time. Returns new + /// timestamp. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setfullyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setFullYear + pub fn set_full_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + } + + // 3. Let y be ? ToNumber(year). + let y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 4. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) + } else { + None + }; + + // 5. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(dt) = args.get(2) { + Some(dt.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). + t.set_components(false, Some(y), m, dt, None, None, None, None); + + // 7. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 9. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setHours()` + /// + /// The `setHours()` method sets the hours for a specified date according to local time, and returns the number + /// of milliseconds since January 1, 1970 00:00:00 UTC until the time represented by the updated `Date` + /// instance. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.sethours + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setHours + pub fn set_hours(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let h be ? ToNumber(hour). + let h = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If min is not present, let m be MinFromTime(t); otherwise, let m be ? ToNumber(min). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) + } else { + None + }; + + // 4. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let sec = if let Some(sec) = args.get(2) { + Some(sec.to_number(context)?) + } else { + None + }; + + // 5. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(3) { + Some(milli.to_number(context)?) + } else { + None + }; + + // 6. Let date be MakeDate(Day(t), MakeTime(h, m, s, milli)). + t.set_components(false, None, None, None, Some(h), m, sec, milli); + + // 7. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 9. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setMilliseconds()` + /// + /// The `setMilliseconds()` method sets the milliseconds for a specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setmilliseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMilliseconds + pub fn set_milliseconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Set ms to ? ToNumber(ms). + let ms = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), ms). + t.set_components(false, None, None, None, None, None, None, Some(ms)); + + // 4. Let u be TimeClip(UTC(MakeDate(Day(t), time))). + let u = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 6. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setMinutes()` + /// + /// The `setMinutes()` method sets the minutes for a specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setminutes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMinutes + pub fn set_minutes( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(min). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let s = if let Some(s) = args.get(1) { + Some(s.to_number(context)?) + } else { + None + }; + + // 4. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(2) { + Some(milli.to_number(context)?) + } else { + None + }; + + // 5. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). + t.set_components(false, None, None, None, None, Some(m), s, milli); + + // 6. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 8. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setMonth()` + /// + /// The `setMonth()` method sets the month for a specified date according to the currently set year. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setmonth + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setMonth + pub fn set_month(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(month). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(date) = args.get(1) { + Some(date.to_number(context)?) + } else { + None + }; + + // 4. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). + t.set_components(false, None, Some(m), dt, None, None, None, None); + + // 5. Let u be TimeClip(UTC(newDate)). + let u = t.get_time(); + + // 6. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 7. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setSeconds()` + /// + /// The `setSeconds()` method sets the seconds for a specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setSeconds + pub fn set_seconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be LocalTime(? thisTimeValue(this value)). + let mut t = this_time_value(this, context)?; + + // 2. Let s be ? ToNumber(sec). + let s = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let milli = if let Some(milli) = args.get(1) { + Some(milli.to_number(context)?) + } else { + None + }; + + // 4. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). + t.set_components(false, None, None, None, None, None, Some(s), milli); + + // 5. Let u be TimeClip(UTC(date)). + let u = t.get_time(); + + // 6. Set the [[DateValue]] internal slot of this Date object to u. + this.set_data(ObjectData::date(t)); + + // 7. Return u. + Ok(u.into()) + } + + /// `Date.prototype.setYear()` + /// + /// The `setYear()` method sets the year for a specified date according to local time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setYear + pub fn set_year(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽; otherwise, set t to LocalTime(t). + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + } + + // 3. Let y be ? ToNumber(year). + let mut y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 4. If y is NaN, then + if y.is_nan() { + // a. Set the [[DateValue]] internal slot of this Date object to NaN. + this.set_data(ObjectData::date(Self(None))); + + // b. Return NaN. + return Ok(JsValue::nan()); + } + + // 5. Let yi be ! ToIntegerOrInfinity(y). + // 6. If 0 ≤ yi ≤ 99, let yyyy be 1900𝔽 + 𝔽(yi). + // 7. Else, let yyyy be y. + if (0f64..=99f64).contains(&y) { + y += 1900f64; + } + + // 8. Let d be MakeDay(yyyy, MonthFromTime(t), DateFromTime(t)). + // 9. Let date be UTC(MakeDate(d, TimeWithinDay(t))). + t.set_components(false, Some(y), None, None, None, None, None, None); + + // 10. Set the [[DateValue]] internal slot of this Date object to TimeClip(date). + this.set_data(ObjectData::date(t)); + + // 11. Return the value of the [[DateValue]] internal slot of this Date object. + Ok(t.get_time().into()) + } + + /// `Date.prototype.setTime()` + /// + /// The `setTime()` method sets the Date object to the time represented by a number of milliseconds since + /// January 1, 1970, 00:00:00 UTC. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.settime + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setTime + pub fn set_time(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Perform ? thisTimeValue(this value). + this_time_value(this, context)?; + + // 2. Let t be ? ToNumber(time). + let t = if let Some(t) = args.get(0) { + let t = t.to_number(context)?; + let seconds = (t / 1_000f64) as i64; + let nanoseconds = ((t % 1_000f64) * 1_000_000f64) as u32; + Self( + ignore_ambiguity(Local.timestamp_opt(seconds, nanoseconds)) + .map(|dt| dt.naive_utc()), + ) + } else { + Self(None) + }; + + // 3. Let v be TimeClip(t). + let v = t.get_time(); + + // 4. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 5. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCDate()` + /// + /// The `setUTCDate()` method sets the day of the month for a specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcdate + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCDate + pub fn set_utc_date( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let dt be ? ToNumber(date). + let dt = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let newDate be MakeDate(MakeDay(YearFromTime(t), MonthFromTime(t), dt), TimeWithinDay(t)). + t.set_components(true, None, None, Some(dt), None, None, None, None); + + // 4. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 6. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setFullYear()` + /// + /// The `setFullYear()` method sets the full year for a specified date according to local time. Returns new + /// timestamp. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcfullyear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCFullYear + pub fn set_utc_full_year( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. If t is NaN, set t to +0𝔽. + if t.0.is_none() { + t.0 = NaiveDateTime::from_timestamp_opt(0, 0) + .and_then(|local| ignore_ambiguity(Local.from_local_datetime(&local))) + .map(|local| local.naive_utc()) + .filter(|time| Self::time_clip(time.timestamp_millis() as f64).is_some()); + } + + // 3. Let y be ? ToNumber(year). + let y = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 4. If month is not present, let m be MonthFromTime(t); otherwise, let m be ? ToNumber(month). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) + } else { + None + }; + + // 5. If date is not present, let dt be DateFromTime(t); otherwise, let dt be ? ToNumber(date). + let dt = if let Some(dt) = args.get(2) { + Some(dt.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(MakeDay(y, m, dt), TimeWithinDay(t)). + t.set_components(true, Some(y), m, dt, None, None, None, None); + + // 7. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 9. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCHours()` + /// + /// The `setUTCHours()` method sets the hour for a specified date according to universal time, and returns the + /// number of milliseconds since January 1, 1970 00:00:00 UTC until the time represented by the updated `Date` + /// instance. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutchours + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCHours + pub fn set_utc_hours( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let h be ? ToNumber(hour). + let h = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If min is not present, let m be MinFromTime(t); otherwise, let m be ? ToNumber(min). + let m = if let Some(m) = args.get(1) { + Some(m.to_number(context)?) + } else { + None + }; + + // 4. If sec is not present, let s be SecFromTime(t); otherwise, let s be ? ToNumber(sec). + let sec = if let Some(s) = args.get(2) { + Some(s.to_number(context)?) + } else { + None + }; + + // 5. If ms is not present, let milli be msFromTime(t); otherwise, let milli be ? ToNumber(ms). + let ms = if let Some(ms) = args.get(3) { + Some(ms.to_number(context)?) + } else { + None + }; + + // 6. Let newDate be MakeDate(Day(t), MakeTime(h, m, s, milli)). + t.set_components(true, None, None, None, Some(h), m, sec, ms); + + // 7. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 8. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 9. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCMilliseconds()` + /// + /// The `setUTCMilliseconds()` method sets the milliseconds for a specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcmilliseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMilliseconds + pub fn set_utc_milliseconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let milli be ? ToNumber(ms). + let ms = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. Let time be MakeTime(HourFromTime(t), MinFromTime(t), SecFromTime(t), milli). + t.set_components(true, None, None, None, None, None, None, Some(ms)); + + // 4. Let v be TimeClip(MakeDate(Day(t), time)). + let v = t.get_time(); + + // 5. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 6. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCMinutes()` + /// + /// The `setUTCMinutes()` method sets the minutes for a specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcminutes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMinutes + pub fn set_utc_minutes( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(min). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If sec is not present, let s be SecFromTime(t). + // 4. Else, + let s = if let Some(s) = args.get(1) { + // a. Let s be ? ToNumber(sec). + Some(s.to_number(context)?) + } else { + None + }; + + // 5. If ms is not present, let milli be msFromTime(t). + // 6. Else, + let milli = if let Some(ms) = args.get(2) { + // a. Let milli be ? ToNumber(ms). + Some(ms.to_number(context)?) + } else { + None + }; + + // 7. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), m, s, milli)). + t.set_components(true, None, None, None, None, Some(m), s, milli); + + // 8. Let v be TimeClip(date). + let v = t.get_time(); + + // 9. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 10. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCMonth()` + /// + /// The `setUTCMonth()` method sets the month for a specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcmonth + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCMonth + pub fn set_utc_month( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let m be ? ToNumber(month). + let m = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If date is not present, let dt be DateFromTime(t). + // 4. Else, + let dt = if let Some(dt) = args.get(1) { + // a. Let dt be ? ToNumber(date). + Some(dt.to_number(context)?) + } else { + None + }; + + // 5. Let newDate be MakeDate(MakeDay(YearFromTime(t), m, dt), TimeWithinDay(t)). + t.set_components(true, None, Some(m), dt, None, None, None, None); + + // 6. Let v be TimeClip(newDate). + let v = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 8. Return v. + Ok(v.into()) + } + + /// `Date.prototype.setUTCSeconds()` + /// + /// The `setUTCSeconds()` method sets the seconds for a specified date according to universal time. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.setutcseconds + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/setUTCSeconds + pub fn set_utc_seconds( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let t be ? thisTimeValue(this value). + let mut t = this_time_value(this, context)?; + + // 2. Let s be ? ToNumber(sec). + let s = args + .get(0) + .cloned() + .unwrap_or_default() + .to_number(context)?; + + // 3. If ms is not present, let milli be msFromTime(t). + // 4. Else, + let milli = if let Some(milli) = args.get(1) { + // a. Let milli be ? ToNumber(ms). + Some(milli.to_number(context)?) + } else { + None + }; + + // 5. Let date be MakeDate(Day(t), MakeTime(HourFromTime(t), MinFromTime(t), s, milli)). + t.set_components(true, None, None, None, None, None, Some(s), milli); + + // 6. Let v be TimeClip(date). + let v = t.get_time(); + + // 7. Set the [[DateValue]] internal slot of this Date object to v. + this.set_data(ObjectData::date(t)); + + // 8. Return v. + Ok(v.into()) + } + + /// `Date.prototype.toDateString()` + /// + /// The `toDateString()` method returns the date portion of a Date object in English. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.todatestring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toDateString + #[allow(clippy::wrong_self_convention)] + pub fn to_date_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be this Date object. + // 2. Let tv be ? thisTimeValue(O). + let tv = this_time_value(this, context)?; + + // 3. If tv is NaN, return "Invalid Date". + // 4. Let t be LocalTime(tv). + // 5. Return DateString(t). + if let Some(t) = tv.0 { + Ok(Local::now() + .timezone() + .from_utc_datetime(&t) + .format("%a %b %d %Y") + .to_string() + .into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } + } + + /// `Date.prototype.toGMTString()` + /// + /// The `toGMTString()` method converts a date to a string, using Internet Greenwich Mean Time (GMT) conventions. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.togmtstring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toGMTString + pub fn to_gmt_string(self) -> String { + self.to_utc_string() + } + + /// `Date.prototype.toISOString()` + /// + /// The `toISOString()` method returns a string in simplified extended ISO format (ISO 8601). + /// + /// More information: + /// - [ISO 8601][iso8601] + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [iso8601]: http://en.wikipedia.org/wiki/ISO_8601 + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.toisostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toISOString + #[allow(clippy::wrong_self_convention)] + pub fn to_iso_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(t) = this_time_value(this, context)?.0 { + Ok(Utc::now() + .timezone() + .from_utc_datetime(&t) + .format("%Y-%m-%dT%H:%M:%S.%3fZ") + .to_string() + .into()) + } else { + context.throw_range_error("Invalid time value") + } + } + + /// `Date.prototype.toJSON()` + /// + /// The `toJSON()` method returns a string representation of the `Date` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tojson + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toJSON + #[allow(clippy::wrong_self_convention)] + pub fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be ? ToObject(this value). + let o = this.to_object(context)?; + + // 2. Let tv be ? ToPrimitive(O, number). + let tv = this.to_primitive(context, PreferredType::Number)?; + + // 3. If Type(tv) is Number and tv is not finite, return null. + if let Some(number) = tv.as_number() { + if !number.is_finite() { + return Ok(JsValue::null()); + } + } + + // 4. Return ? Invoke(O, "toISOString"). + let func = o.get("toISOString", context)?; + context.call(&func, &o.into(), &[]) + } + + /// `Date.prototype.toString()` + /// + /// The toString() method returns a string representing the specified Date object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toString + #[allow(clippy::wrong_self_convention)] + pub fn to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let tv be ? thisTimeValue(this value). + let tv = this_time_value(this, context)?; + + // 2. Return ToDateString(tv). + if let Some(t) = tv.0 { + Ok(Local::now() + .timezone() + .from_utc_datetime(&t) + .format("%a %b %d %Y %H:%M:%S GMT%z") + .to_string() + .into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } + } + + /// `Date.prototype.toTimeString()` + /// + /// The `toTimeString()` method returns the time portion of a Date object in human readable form in American + /// English. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.totimestring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toTimeString + #[allow(clippy::wrong_self_convention)] + pub fn to_time_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be this Date object. + // 2. Let tv be ? thisTimeValue(O). + let tv = this_time_value(this, context)?; + + // 3. If tv is NaN, return "Invalid Date". + // 4. Let t be LocalTime(tv). + // 5. Return the string-concatenation of TimeString(t) and TimeZoneString(tv). + if let Some(t) = tv.0 { + Ok(Local::now() + .timezone() + .from_utc_datetime(&t) + .format("%H:%M:%S GMT%z") + .to_string() + .into()) + } else { + Ok(JsString::from("Invalid Date").into()) + } + } + + /// `Date.prototype.toUTCString()` + /// + /// The `toUTCString()` method returns a string representing the specified Date object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.toutcstring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toUTCString + pub fn to_utc_string(self) -> String { + self.to_utc().map_or_else( + || "Invalid Date".to_string(), + |date_time| date_time.format("%a, %d %b %Y %H:%M:%S GMT").to_string(), + ) + } + + /// `Date.prototype.valueOf()` + /// + /// The `valueOf()` method returns the primitive value of a `Date` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/valueOf + pub fn value_of(&self) -> f64 { + self.get_time() + } + + /// `Date.now()` + /// + /// The static `Date.now()` method returns the number of milliseconds elapsed since January 1, 1970 00:00:00 UTC. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.now + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn now(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Ok(JsValue::new(Utc::now().timestamp_millis() as f64)) + } + + /// `Date.parse()` + /// + /// The `Date.parse()` method parses a string representation of a date, and returns the number of milliseconds since + /// January 1, 1970, 00:00:00 UTC or `NaN` if the string is unrecognized or, in some cases, contains illegal date + /// values. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.parse + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse + pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // This method is implementation-defined and discouraged, so we just require the same format as the string + // constructor. + + if args.is_empty() { + return Ok(JsValue::nan()); + } + + match DateTime::parse_from_rfc3339(&args[0].to_string(context)?) { + Ok(v) => Ok(JsValue::new(v.naive_utc().timestamp_millis() as f64)), + _ => Ok(JsValue::new(f64::NAN)), + } + } + + /// `Date.UTC()` + /// + /// The `Date.UTC()` method accepts parameters similar to the `Date` constructor, but treats them as UTC. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-date.utc + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/UTC + pub(crate) fn utc(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let year = args + .get(0) + .map_or(Ok(f64::NAN), |value| value.to_number(context))?; + let month = args + .get(1) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let day = args + .get(2) + .map_or(Ok(1f64), |value| value.to_number(context))?; + let hour = args + .get(3) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let min = args + .get(4) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let sec = args + .get(5) + .map_or(Ok(0f64), |value| value.to_number(context))?; + let milli = args + .get(6) + .map_or(Ok(0f64), |value| value.to_number(context))?; + + if !check_normal_opt!(year, month, day, hour, min, sec, milli) { + return Ok(JsValue::nan()); + } + + let year = year as i32; + let month = month as u32; + let day = day as u32; + let hour = hour as u32; + let min = min as u32; + let sec = sec as u32; + let milli = milli as u32; + + let year = if (0..=99).contains(&year) { + 1900 + year + } else { + year + }; + + NaiveDate::from_ymd_opt(year, month + 1, day) + .and_then(|f| f.and_hms_milli_opt(hour, min, sec, milli)) + .and_then(|f| Self::time_clip(f.timestamp_millis() as f64)) + .map_or(Ok(JsValue::nan()), |time| Ok(JsValue::new(time))) + } +} + +/// The abstract operation `thisTimeValue` takes argument value. +/// +/// In following descriptions of functions that are properties of the Date prototype object, the phrase “this +/// Date object” refers to the object that is the this value for the invocation of the function. If the `Type` of +/// the this value is not `Object`, a `TypeError` exception is thrown. The phrase “this time value” within the +/// specification of a method refers to the result returned by calling the abstract operation `thisTimeValue` with +/// the this value of the method invocation passed as the argument. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-thistimevalue +#[inline] +pub fn this_time_value(value: &JsValue, context: &mut Context) -> JsResult { + value + .as_object() + .and_then(|obj| obj.borrow().as_date().copied()) + .ok_or_else(|| context.construct_type_error("'this' is not a Date")) +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/tests.rs new file mode 100644 index 0000000..057fd57 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/date/tests.rs @@ -0,0 +1,1289 @@ +#![allow(clippy::zero_prefixed_literal)] + +use crate::{forward, forward_val, Context, JsValue}; +use chrono::prelude::*; + +// NOTE: Javascript Uses 0-based months, where chrono uses 1-based months. Many of the assertions look wrong because of +// this. + +fn forward_dt_utc(context: &mut Context, src: &str) -> Option { + let date_time = if let Ok(v) = forward_val(context, src) { + v + } else { + panic!("expected success") + }; + + if let JsValue::Object(ref date_time) = date_time { + if let Some(date_time) = date_time.borrow().as_date() { + date_time.0 + } else { + panic!("expected date") + } + } else { + panic!("expected object") + } +} + +fn forward_dt_local(context: &mut Context, src: &str) -> Option { + let date_time = forward_dt_utc(context, src); + + // The timestamp is converted to UTC for internal representation + date_time.map(|utc| { + Local::now() + .timezone() + .from_utc_datetime(&utc) + .naive_local() + }) +} + +#[test] +fn date_display() { + let dt = super::Date(None); + assert_eq!("[Invalid Date]", format!("[{dt}]")); + + let cd = super::Date::default(); + assert_eq!( + format!( + "[{}]", + cd.to_local().unwrap().format("%a %b %d %Y %H:%M:%S GMT%:z") + ), + format!("[{cd}]") + ); +} + +#[test] +fn date_this_time_value() { + let mut context = Context::default(); + + let error = forward_val( + &mut context, + "({toString: Date.prototype.toString}).toString()", + ) + .expect_err("Expected error"); + let message_property = &error + .get_property("message") + .expect("Expected 'message' property") + .expect_value() + .clone(); + + assert_eq!(JsValue::new("\'this\' is not a Date"), *message_property); +} + +#[test] +fn date_call() { + let mut context = Context::default(); + + let dt1 = forward(&mut context, "Date()"); + + std::thread::sleep(std::time::Duration::from_millis(1)); + + let dt2 = forward(&mut context, "Date()"); + + assert_ne!(dt1, dt2); +} + +#[test] +fn date_ctor_call() { + let mut context = Context::default(); + + let dt1 = forward_dt_local(&mut context, "new Date()"); + + std::thread::sleep(std::time::Duration::from_millis(1)); + + let dt2 = forward_dt_local(&mut context, "new Date()"); + + assert_ne!(dt1, dt2); +} + +#[test] +fn date_ctor_call_string() { + let mut context = Context::default(); + + let date_time = forward_dt_utc(&mut context, "new Date('2020-06-08T09:16:15.779-06:30')"); + + // Internal date is expressed as UTC + assert_eq!( + Some(NaiveDate::from_ymd(2020, 06, 08).and_hms_milli(15, 46, 15, 779)), + date_time + ); +} + +#[test] +fn date_ctor_call_string_invalid() { + let mut context = Context::default(); + + let date_time = forward_dt_local(&mut context, "new Date('nope')"); + assert_eq!(None, date_time); +} + +#[test] +fn date_ctor_call_number() { + let mut context = Context::default(); + + let date_time = forward_dt_utc(&mut context, "new Date(1594199775779)"); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 779)), + date_time + ); +} + +#[test] +fn date_ctor_call_date() { + let mut context = Context::default(); + + let date_time = forward_dt_utc(&mut context, "new Date(new Date(1594199775779))"); + + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 779)), + date_time + ); +} + +#[test] +fn date_ctor_call_multiple() { + let mut context = Context::default(); + + let date_time = forward_dt_local(&mut context, "new Date(2020, 06, 08, 09, 16, 15, 779)"); + + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 779)), + date_time + ); +} + +#[test] +fn date_ctor_call_multiple_90s() { + let mut context = Context::default(); + + let date_time = forward_dt_local(&mut context, "new Date(99, 06, 08, 09, 16, 15, 779)"); + + assert_eq!( + Some(NaiveDate::from_ymd(1999, 07, 08).and_hms_milli(09, 16, 15, 779)), + date_time + ); +} + +#[test] +fn date_ctor_call_multiple_nan() { + fn check(src: &str) { + let mut context = Context::default(); + let date_time = forward_dt_local(&mut context, src); + assert_eq!(None, date_time); + } + + check("new Date(1/0, 06, 08, 09, 16, 15, 779)"); + check("new Date(2020, 1/0, 08, 09, 16, 15, 779)"); + check("new Date(2020, 06, 1/0, 09, 16, 15, 779)"); + check("new Date(2020, 06, 08, 1/0, 16, 15, 779)"); + check("new Date(2020, 06, 08, 09, 1/0, 15, 779)"); + check("new Date(2020, 06, 08, 09, 16, 1/0, 779)"); + check("new Date(2020, 06, 08, 09, 16, 15, 1/0)"); +} + +#[test] +fn date_ctor_now_call() { + let mut context = Context::default(); + + let date_time = forward(&mut context, "Date.now()"); + let dt1 = date_time.parse::().unwrap(); + + std::thread::sleep(std::time::Duration::from_millis(1)); + + let date_time = forward(&mut context, "Date.now()"); + let dt2 = date_time.parse::().unwrap(); + + assert_ne!(dt1, dt2); +} + +#[test] +fn date_ctor_parse_call() { + let mut context = Context::default(); + + let date_time = forward_val(&mut context, "Date.parse('2020-06-08T09:16:15.779-07:30')"); + + assert_eq!(Ok(JsValue::new(1591634775779f64)), date_time); +} + +#[test] +fn date_ctor_utc_call() { + let mut context = Context::default(); + + let date_time = forward_val(&mut context, "Date.UTC(2020, 06, 08, 09, 16, 15, 779)"); + + assert_eq!(Ok(JsValue::new(1594199775779f64)), date_time); +} + +#[test] +fn date_ctor_utc_call_nan() { + fn check(src: &str) { + let mut context = Context::default(); + let date_time = forward_val(&mut context, src).expect("Expected Success"); + assert_eq!(JsValue::nan(), date_time); + } + + check("Date.UTC(1/0, 06, 08, 09, 16, 15, 779)"); + check("Date.UTC(2020, 1/0, 08, 09, 16, 15, 779)"); + check("Date.UTC(2020, 06, 1/0, 09, 16, 15, 779)"); + check("Date.UTC(2020, 06, 08, 1/0, 16, 15, 779)"); + check("Date.UTC(2020, 06, 08, 09, 1/0, 15, 779)"); + check("Date.UTC(2020, 06, 08, 09, 16, 1/0, 779)"); + check("Date.UTC(2020, 06, 08, 09, 16, 15, 1/0)"); +} + +#[test] +fn date_proto_get_date_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getDate()", + ); + assert_eq!(Ok(JsValue::new(08f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getDate()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_day_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getDay()", + ); + assert_eq!(Ok(JsValue::new(3f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getDay()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_full_year_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getFullYear()", + ); + assert_eq!(Ok(JsValue::new(2020f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getFullYear()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_hours_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getHours()", + ); + assert_eq!(Ok(JsValue::new(09f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getHours()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_milliseconds_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getMilliseconds()", + ); + assert_eq!(Ok(JsValue::new(779f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getMilliseconds()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_minutes_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getMinutes()", + ); + assert_eq!(Ok(JsValue::new(16f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getMinutes()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_month() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getMonth()", + ); + assert_eq!(Ok(JsValue::new(06f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getMonth()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_seconds() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getSeconds()", + ); + assert_eq!(Ok(JsValue::new(15f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getSeconds()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_time() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getTime()", + ); + + let ts = Local + .ymd(2020, 07, 08) + .and_hms_milli(09, 16, 15, 779) + .timestamp_millis() as f64; + assert_eq!(Ok(JsValue::new(ts)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getTime()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_year() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(2020, 06, 08, 09, 16, 15, 779).getYear()", + ); + assert_eq!(Ok(JsValue::new(120f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getYear()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_timezone_offset() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset() === new Date('1975-08-19T23:15:30-02:00').getTimezoneOffset()", + ); + + // NB: Host Settings, not TZ specified in the DateTime. + assert_eq!(Ok(JsValue::new(true)), actual); + + let actual = forward_val( + &mut context, + "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset()", + ); + + // The value of now().offset() depends on the host machine, so we have to replicate the method code here. + let offset_seconds = f64::from(chrono::Local::now().offset().local_minus_utc()); + let offset_minutes = -offset_seconds / 60f64; + assert_eq!(Ok(JsValue::new(offset_minutes)), actual); + + let actual = forward_val( + &mut context, + "new Date('1975-08-19T23:15:30+07:00').getTimezoneOffset()", + ); + assert_eq!(Ok(JsValue::new(offset_minutes)), actual); +} + +#[test] +fn date_proto_get_utc_date_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCDate()", + ); + assert_eq!(Ok(JsValue::new(08f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCDate()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_day_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCDay()", + ); + assert_eq!(Ok(JsValue::new(3f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCDay()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_full_year_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCFullYear()", + ); + assert_eq!(Ok(JsValue::new(2020f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCFullYear()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_hours_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCHours()", + ); + assert_eq!(Ok(JsValue::new(09f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCHours()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_milliseconds_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCMilliseconds()", + ); + assert_eq!(Ok(JsValue::new(779f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCMilliseconds()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_minutes_call() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCMinutes()", + ); + assert_eq!(Ok(JsValue::new(16f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCMinutes()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_month() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCMonth()", + ); + assert_eq!(Ok(JsValue::new(06f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCMonth()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_get_utc_seconds() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).getUTCSeconds()", + ); + assert_eq!(Ok(JsValue::new(15f64)), actual); + + let actual = forward_val(&mut context, "new Date(1/0).getUTCSeconds()"); + assert_eq!(Ok(JsValue::nan()), actual); +} + +#[test] +fn date_proto_set_date() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setDate(21); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 21).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Date wraps to previous month for 0. + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setDate(0); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 06, 30).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setDate(1/0); dt", + ); + assert_eq!(None, actual); +} + +#[test] +fn date_proto_set_full_year() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setFullYear(2012); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 07, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setFullYear(2012, 8); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 09, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setFullYear(2012, 8, 10); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 09, 10).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Out-of-bounds + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setFullYear(2012, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2014, 12, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setFullYear(2012, -35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2009, 02, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setFullYear(2012, 9, 950); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2015, 05, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setFullYear(2012, 9, -950); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2010, 02, 23).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_hours() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setHours(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setHours(11, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setHours(11, 35, 23); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 23, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setHours(11, 35, 23, 537); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 23, 537)), + actual + ); + + // Out-of-bounds + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setHours(10000, 20000, 30000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 09, 11).and_hms_milli(21, 40, 40, 123)), + actual + ); +} + +#[test] +fn date_proto_set_milliseconds() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMilliseconds(597); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 597)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHours + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMilliseconds(40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 55, 123)), + actual + ); +} + +#[test] +fn date_proto_set_minutes() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMinutes(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMinutes(11, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 35, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMinutes(11, 35, 537); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 35, 537)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHours + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMinutes(600000, 30000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 08, 29).and_hms_milli(09, 20, 40, 123)), + actual + ); +} + +#[test] +fn date_proto_set_month() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMonth(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 12, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setMonth(11, 16); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 12, 16).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setFullYear + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setMonth(40, 83); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2023, 07, 22).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_seconds() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setSeconds(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 11, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setSeconds(11, 487); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 11, 487)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHour + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 07, 08, 09, 16, 15, 779); dt.setSeconds(40000000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 11, 14).and_hms_milli(08, 23, 20, 123)), + actual + ); +} + +#[test] +fn set_year() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setYear(98); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(1998, 07, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_local( + &mut context, + "dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.setYear(2001); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2001, 07, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_time() { + let mut context = Context::default(); + + let actual = forward_dt_local( + &mut context, + "let dt = new Date(); dt.setTime(new Date(2020, 06, 08, 09, 16, 15, 779).getTime()); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_utc_date() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCDate(21); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 21).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Date wraps to previous month for 0. + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCDate(0); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 06, 30).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCDate(1/0); dt", + ); + assert_eq!(None, actual); +} + +#[test] +fn date_proto_set_utc_full_year() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 07, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, 8); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 09, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, 8, 10); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2012, 09, 10).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Out-of-bounds + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2014, 12, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, -35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2009, 02, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, 9, 950); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2015, 05, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCFullYear(2012, 9, -950); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2010, 02, 23).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_utc_hours() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCHours(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCHours(11, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCHours(11, 35, 23); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 23, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCHours(11, 35, 23, 537); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(11, 35, 23, 537)), + actual + ); + + // Out-of-bounds + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCHours(10000, 20000, 30000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 09, 11).and_hms_milli(21, 40, 40, 123)), + actual + ); +} + +#[test] +fn date_proto_set_utc_milliseconds() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMilliseconds(597); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 15, 597)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHours + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMilliseconds(40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 55, 123)), + actual + ); +} + +#[test] +fn date_proto_set_utc_minutes() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMinutes(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMinutes(11, 35); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 35, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMinutes(11, 35, 537); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 11, 35, 537)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHours + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMinutes(600000, 30000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 08, 29).and_hms_milli(09, 20, 40, 123)), + actual + ); +} + +#[test] +fn date_proto_set_utc_month() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMonth(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 12, 08).and_hms_milli(09, 16, 15, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCMonth(11, 16); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 12, 16).and_hms_milli(09, 16, 15, 779)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setFullYear + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCMonth(40, 83); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2023, 07, 22).and_hms_milli(09, 16, 15, 779)), + actual + ); +} + +#[test] +fn date_proto_set_utc_seconds() { + let mut context = Context::default(); + + let actual = forward_dt_utc( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCSeconds(11); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 11, 779)), + actual + ); + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.setUTCSeconds(11, 487); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2020, 07, 08).and_hms_milli(09, 16, 11, 487)), + actual + ); + + // Out-of-bounds + // Thorough tests are done by setHour + + let actual = forward_dt_utc( + &mut context, + "dt = new Date(Date.UTC(2020, 07, 08, 09, 16, 15, 779)); dt.setUTCSeconds(40000000, 40123); dt", + ); + assert_eq!( + Some(NaiveDate::from_ymd(2021, 11, 14).and_hms_milli(08, 23, 20, 123)), + actual + ); +} + +#[test] +fn date_proto_to_date_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.toDateString()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new("Wed Jul 08 2020"), actual); +} + +#[test] +fn date_proto_to_gmt_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.toGMTString()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new("Wed, 08 Jul 2020 09:16:15 GMT"), actual); +} + +#[test] +fn date_proto_to_iso_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.toISOString()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new("2020-07-08T09:16:15.779Z"), actual); +} + +#[test] +fn date_proto_to_json() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.toJSON()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new("2020-07-08T09:16:15.779Z"), actual); +} + +#[test] +fn date_proto_to_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.toString()", + ) + .ok(); + + assert_eq!( + Some(JsValue::new( + Local + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd(2020, 7, 8), + NaiveTime::from_hms_milli(9, 16, 15, 779) + )) + .earliest() + .unwrap() + .format("Wed Jul 08 2020 09:16:15 GMT%z") + .to_string() + )), + actual + ); +} + +#[test] +fn date_proto_to_time_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(2020, 06, 08, 09, 16, 15, 779); dt.toTimeString()", + ) + .ok(); + + assert_eq!( + Some(JsValue::new( + Local + .from_local_datetime(&NaiveDateTime::new( + NaiveDate::from_ymd(2020, 7, 8), + NaiveTime::from_hms_milli(9, 16, 15, 779) + )) + .earliest() + .unwrap() + .format("09:16:15 GMT%z") + .to_string() + )), + actual + ); +} + +#[test] +fn date_proto_to_utc_string() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "let dt = new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)); dt.toUTCString()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new("Wed, 08 Jul 2020 09:16:15 GMT"), actual); +} + +#[test] +fn date_proto_value_of() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)).valueOf()", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new(1594199775779f64), actual); +} + +#[test] +fn date_neg() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "-new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779))", + ) + .expect("Successful eval"); + assert_eq!(JsValue::new(-1594199775779f64), actual); +} + +#[test] +fn date_json() { + let mut context = Context::default(); + + let actual = forward_val( + &mut context, + "JSON.stringify({ date: new Date(Date.UTC(2020, 06, 08, 09, 16, 15, 779)) })", + ) + .expect("Successful eval"); + assert_eq!( + JsValue::new(r#"{"date":"2020-07-08T09:16:15.779Z"}"#), + actual + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/aggregate.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/aggregate.rs new file mode 100644 index 0000000..5c38417 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/aggregate.rs @@ -0,0 +1,116 @@ +//! This module implements the global `AggregateError` object. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-aggregate-error +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/AggregateError + +use crate::{ + builtins::{iterable::iterable_to_list, Array, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptorBuilder}, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct AggregateError; + +impl BuiltIn for AggregateError { + const NAME: &'static str = "AggregateError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context + .intrinsics() + .constructors() + .aggregate_error() + .clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl AggregateError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 2; + + /// Create a new aggregate error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%AggregateError.prototype%", « [[ErrorData]] »). + let prototype = get_prototype_from_constructor( + new_target, + StandardConstructors::aggregate_error, + context, + )?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(1); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(2), context)?; + + // 5. Let errorsList be ? IterableToList(errors). + let errors = args.get_or_undefined(0); + let errors_list = iterable_to_list(context, errors, None)?; + // 6. Perform ! DefinePropertyOrThrow(O, "errors", + // PropertyDescriptor { + // [[Configurable]]: true, + // [[Enumerable]]: false, + // [[Writable]]: true, + // [[Value]]: CreateArrayFromList(errorsList) + // }). + o.define_property_or_throw( + "errors", + PropertyDescriptorBuilder::new() + .configurable(true) + .enumerable(false) + .writable(true) + .value(Array::create_array_from_list(errors_list, context)) + .build(), + context, + ) + .expect("should not fail according to spec"); + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/eval.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/eval.rs new file mode 100644 index 0000000..5637492 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/eval.rs @@ -0,0 +1,91 @@ +//! This module implements the global `EvalError` object. +//! +//! Indicates an error regarding the global `eval()` function. +//! This exception is not thrown by JavaScript anymore, however +//! the `EvalError` object remains for compatibility. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-evalerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/EvalError + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +/// JavaScript `EvalError` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct EvalError; + +impl BuiltIn for EvalError { + const NAME: &'static str = "EvalError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().eval_error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl EvalError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::eval_error, context)?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/mod.rs new file mode 100644 index 0000000..c68d185 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/mod.rs @@ -0,0 +1,187 @@ +//! This module implements the global `Error` object. +//! +//! Error objects are thrown when runtime errors occur. +//! The Error object can also be used as a base object for user-defined exceptions. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-error-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error + +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsString, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +pub(crate) mod aggregate; +pub(crate) mod eval; +pub(crate) mod range; +pub(crate) mod reference; +pub(crate) mod syntax; +pub(crate) mod r#type; +pub(crate) mod uri; + +#[cfg(test)] +mod tests; + +pub(crate) use self::aggregate::AggregateError; +pub(crate) use self::eval::EvalError; +pub(crate) use self::r#type::TypeError; +pub(crate) use self::range::RangeError; +pub(crate) use self::reference::ReferenceError; +pub(crate) use self::syntax::SyntaxError; +pub(crate) use self::uri::UriError; + +use super::JsArgs; + +/// Built-in `Error` object. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Error; + +impl BuiltIn for Error { + const NAME: &'static str = "Error"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .method(Self::to_string, "toString", 0) + .build() + .conv::() + .pipe(Some) + } +} + +impl Error { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + pub(crate) fn install_error_cause( + o: &JsObject, + options: &JsValue, + context: &mut Context, + ) -> JsResult<()> { + // 1. If Type(options) is Object and ? HasProperty(options, "cause") is true, then + if let Some(options) = options.as_object() { + if options.has_property("cause", context)? { + // a. Let cause be ? Get(options, "cause"). + let cause = options.get("cause", context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "cause", cause). + o.create_non_enumerable_data_property_or_throw("cause", cause, context); + } + } + + // 2. Return unused. + Ok(()) + } + + /// `Error( message [ , options ] )` + /// + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%Error.prototype%", « [[ErrorData]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::error, context)?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Self::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } + + /// `Error.prototype.toString()` + /// + /// The toString() method returns a string representing the specified Error object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-error.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + let o = if let Some(o) = this.as_object() { + o + // 2. If Type(O) is not Object, throw a TypeError exception. + } else { + return context.throw_type_error("'this' is not an Object"); + }; + + // 3. Let name be ? Get(O, "name"). + let name = o.get("name", context)?; + + // 4. If name is undefined, set name to "Error"; otherwise set name to ? ToString(name). + let name = if name.is_undefined() { + JsString::new("Error") + } else { + name.to_string(context)? + }; + + // 5. Let msg be ? Get(O, "message"). + let msg = o.get("message", context)?; + + // 6. If msg is undefined, set msg to the empty String; otherwise set msg to ? ToString(msg). + let msg = if msg.is_undefined() { + JsString::empty() + } else { + msg.to_string(context)? + }; + + // 7. If name is the empty String, return msg. + if name.is_empty() { + return Ok(msg.into()); + } + + // 8. If msg is the empty String, return name. + if msg.is_empty() { + return Ok(name.into()); + } + + // 9. Return the string-concatenation of name, the code unit 0x003A (COLON), + // the code unit 0x0020 (SPACE), and msg. + Ok(format!("{name}: {msg}").into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/range.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/range.rs new file mode 100644 index 0000000..1fa1258 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/range.rs @@ -0,0 +1,89 @@ +//! This module implements the global `RangeError` object. +//! +//! Indicates a value that is not in the set or range of allowable values. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-rangeerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RangeError + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +/// JavaScript `RangeError` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct RangeError; + +impl BuiltIn for RangeError { + const NAME: &'static str = "RangeError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().range_error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl RangeError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::range_error, context)?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/reference.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/reference.rs new file mode 100644 index 0000000..e130678 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/reference.rs @@ -0,0 +1,95 @@ +//! This module implements the global `ReferenceError` object. +//! +//! Indicates an error that occurs when de-referencing an invalid reference +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-referenceerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/ReferenceError + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct ReferenceError; + +impl BuiltIn for ReferenceError { + const NAME: &'static str = "ReferenceError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context + .intrinsics() + .constructors() + .reference_error() + .clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl ReferenceError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = get_prototype_from_constructor( + new_target, + StandardConstructors::reference_error, + context, + )?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/syntax.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/syntax.rs new file mode 100644 index 0000000..c7dfb01 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/syntax.rs @@ -0,0 +1,94 @@ +//! This module implements the global `SyntaxError` object. +//! +//! The `SyntaxError` object represents an error when trying to interpret syntactically invalid code. +//! It is thrown when the JavaScript context encounters tokens or token order that does not conform +//! to the syntax of the language when parsing code. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-syntaxerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/SyntaxError + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +/// JavaScript `SyntaxError` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SyntaxError; + +impl BuiltIn for SyntaxError { + const NAME: &'static str = "SyntaxError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().syntax_error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl SyntaxError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = get_prototype_from_constructor( + new_target, + StandardConstructors::syntax_error, + context, + )?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/tests.rs new file mode 100644 index 0000000..e082ba9 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/tests.rs @@ -0,0 +1,86 @@ +use crate::{forward, Context}; + +#[test] +fn error_to_string() { + let mut context = Context::default(); + let init = r#" + let e = new Error('1'); + let name = new Error(); + let message = new Error('message'); + message.name = ''; + let range_e = new RangeError('2'); + let ref_e = new ReferenceError('3'); + let syntax_e = new SyntaxError('4'); + let type_e = new TypeError('5'); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "e.toString()"), "\"Error: 1\""); + assert_eq!(forward(&mut context, "name.toString()"), "\"Error\""); + assert_eq!(forward(&mut context, "message.toString()"), "\"message\""); + assert_eq!( + forward(&mut context, "range_e.toString()"), + "\"RangeError: 2\"" + ); + assert_eq!( + forward(&mut context, "ref_e.toString()"), + "\"ReferenceError: 3\"" + ); + assert_eq!( + forward(&mut context, "syntax_e.toString()"), + "\"SyntaxError: 4\"" + ); + assert_eq!( + forward(&mut context, "type_e.toString()"), + "\"TypeError: 5\"" + ); +} + +#[test] +fn eval_error_name() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "EvalError.name"), "\"EvalError\""); +} + +#[test] +fn eval_error_length() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "EvalError.length"), "1"); +} + +#[test] +fn eval_error_to_string() { + let mut context = Context::default(); + assert_eq!( + forward(&mut context, "new EvalError('hello').toString()"), + "\"EvalError: hello\"" + ); + assert_eq!( + forward(&mut context, "new EvalError().toString()"), + "\"EvalError\"" + ); +} + +#[test] +fn uri_error_name() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "URIError.name"), "\"URIError\""); +} + +#[test] +fn uri_error_length() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "URIError.length"), "1"); +} + +#[test] +fn uri_error_to_string() { + let mut context = Context::default(); + assert_eq!( + forward(&mut context, "new URIError('hello').toString()"), + "\"URIError: hello\"" + ); + assert_eq!( + forward(&mut context, "new URIError().toString()"), + "\"URIError\"" + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/type.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/type.rs new file mode 100644 index 0000000..1f39077 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/type.rs @@ -0,0 +1,118 @@ +//! This module implements the global `TypeError` object. +//! +//! The `TypeError` object represents an error when an operation could not be performed, +//! typically (but not exclusively) when a value is not of the expected type. +//! +//! A `TypeError` may be thrown when: +//! - an operand or argument passed to a function is incompatible with the type expected by that operator or function. +//! - when attempting to modify a value that cannot be changed. +//! - when attempting to use a value in an inappropriate way. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-typeerror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypeError + +use crate::{ + builtins::{function::Function, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptor}, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +/// JavaScript `TypeError` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct TypeError; + +impl BuiltIn for TypeError { + const NAME: &'static str = "TypeError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().type_error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl TypeError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::type_error, context)?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} + +pub(crate) fn create_throw_type_error(context: &mut Context) -> JsObject { + fn throw_type_error(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + context.throw_type_error("'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them") + } + + let function = JsObject::from_proto_and_data( + context.intrinsics().constructors().function().prototype(), + ObjectData::function(Function::Native { + function: throw_type_error, + constructor: None, + }), + ); + + let property = PropertyDescriptor::builder() + .writable(false) + .enumerable(false) + .configurable(false); + function.insert_property("name", property.clone().value("ThrowTypeError")); + function.insert_property("length", property.value(0)); + + function +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/uri.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/uri.rs new file mode 100644 index 0000000..21469f3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/error/uri.rs @@ -0,0 +1,90 @@ +//! This module implements the global `URIError` object. +//! +//! The `URIError` object represents an error when a global URI handling +//! function was used in a wrong way. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-native-error-types-used-in-this-standard-urierror +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/URIError + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::Attribute, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +use super::Error; + +/// JavaScript `URIError` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct UriError; + +impl BuiltIn for UriError { + const NAME: &'static str = "URIError"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let error_constructor = context.intrinsics().constructors().error().constructor(); + let error_prototype = context.intrinsics().constructors().error().prototype(); + + let attribute = Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().uri_error().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(error_prototype) + .custom_prototype(error_constructor) + .property("name", Self::NAME, attribute) + .property("message", "", attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl UriError { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// Create a new error object. + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget. + // 2. Let O be ? OrdinaryCreateFromConstructor(newTarget, "%NativeError.prototype%", « [[ErrorData]] »). + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::uri_error, context)?; + let o = JsObject::from_proto_and_data(prototype, ObjectData::error()); + + // 3. If message is not undefined, then + let message = args.get_or_undefined(0); + if !message.is_undefined() { + // a. Let msg be ? ToString(message). + let msg = message.to_string(context)?; + + // b. Perform CreateNonEnumerableDataPropertyOrThrow(O, "message", msg). + o.create_non_enumerable_data_property_or_throw("message", msg, context); + } + + // 4. Perform ? InstallErrorCause(O, options). + Error::install_error_cause(&o, args.get_or_undefined(1), context)?; + + // 5. Return O. + Ok(o.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/eval/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/eval/mod.rs new file mode 100644 index 0000000..71b8295 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/eval/mod.rs @@ -0,0 +1,149 @@ +//! This module implements the global `eval` function. +//! +//! The `eval()` function evaluates JavaScript code represented as a string. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-eval-x +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval + +use crate::{ + builtins::{BuiltIn, JsArgs}, + object::FunctionBuilder, + property::Attribute, + Context, JsValue, +}; +use boa_profiler::Profiler; +use rustc_hash::FxHashSet; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct Eval; + +impl BuiltIn for Eval { + const NAME: &'static str = "eval"; + + const ATTRIBUTE: Attribute = Attribute::CONFIGURABLE + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::WRITABLE); + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let object = FunctionBuilder::native(context, Self::eval) + .name("eval") + .length(1) + .constructor(false) + .build(); + + Some(object.into()) + } +} + +impl Eval { + /// `19.2.1 eval ( x )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-eval-x + fn eval(_: &JsValue, args: &[JsValue], context: &mut Context) -> Result { + // 1. Return ? PerformEval(x, false, false). + Self::perform_eval(args.get_or_undefined(0), false, false, context) + } + + /// `19.2.1.1 PerformEval ( x, strictCaller, direct )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performeval + pub(crate) fn perform_eval( + x: &JsValue, + direct: bool, + strict: bool, + context: &mut Context, + ) -> Result { + // 1. Assert: If direct is false, then strictCaller is also false. + if !direct { + debug_assert!(!strict); + } + + // 2. If Type(x) is not String, return x. + let x = if let Some(x) = x.as_string() { + x.clone() + } else { + return Ok(x.clone()); + }; + + // Because of implementation details the following code differs from the spec. + + // Parse the script body and handle early errors (6 - 11) + let body = match context.parse_eval(x.as_bytes(), direct, strict) { + Ok(body) => body, + Err(e) => return context.throw_syntax_error(e.to_string()), + }; + + // 12 - 13 are implicit in the call of `Context::compile_with_new_declarative`. + + // Because our environment model does not map directly to the spec this section looks very different. + // 14 - 33 are in the following section, together with EvalDeclarationInstantiation. + if direct { + // If the call to eval is direct, the code is executed in the current environment. + + // Poison the current environment, because it may contain new declarations after/during eval. + context.realm.environments.poison_current(); + + // Set the compile time environment to the current running environment and save the number of current environments. + context.realm.compile_env = context.realm.environments.current_compile_environment(); + let environments_len = context.realm.environments.len(); + + // Error if any var declaration in the eval code already exists as a let/const declaration in the current running environment. + let mut vars = FxHashSet::default(); + body.var_declared_names_new(&mut vars); + if let Some(name) = context + .realm + .environments + .has_lex_binding_until_function_environment(&vars) + { + let name = context.interner().resolve_expect(name); + let msg = format!("variable declaration {name} in eval function already exists as lexically declaration"); + return context.throw_syntax_error(msg); + } + + // Compile and execute the eval statement list. + let code_block = context.compile_with_new_declarative(&body, strict)?; + context + .realm + .environments + .extend_outer_function_environment(); + let result = context.execute(code_block); + + // Pop any added runtime environments that where not removed during the eval execution. + context.realm.environments.truncate(environments_len); + + result + } else { + // If the call to eval is indirect, the code is executed in the global environment. + + // Poison all environments, because the global environment may contain new declarations after/during eval. + context.realm.environments.poison_all(); + + // Pop all environments before the eval execution. + let environments = context.realm.environments.pop_to_global(); + let environments_len = context.realm.environments.len(); + context.realm.compile_env = context.realm.environments.current_compile_environment(); + + // Compile and execute the eval statement list. + let code_block = context.compile_with_new_declarative(&body, false)?; + let result = context.execute(code_block); + + // Restore all environments to the state from before the eval execution. + context.realm.environments.truncate(environments_len); + context.realm.environments.extend(environments); + + result + } + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/arguments.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/arguments.rs new file mode 100644 index 0000000..cc28c65 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/arguments.rs @@ -0,0 +1,293 @@ +use crate::{ + builtins::Array, + environments::DeclarativeEnvironment, + object::{JsObject, ObjectData}, + property::PropertyDescriptor, + symbol::{self, WellKnownSymbols}, + syntax::ast::node::FormalParameterList, + Context, JsValue, +}; +use boa_gc::{Finalize, Gc, Trace}; +use rustc_hash::FxHashMap; + +/// `ParameterMap` represents the `[[ParameterMap]]` internal slot on a Arguments exotic object. +/// +/// This struct stores all the data to access mapped function parameters in their environment. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct ParameterMap { + binding_indices: Vec>, + environment: Gc, +} + +impl ParameterMap { + /// Deletes the binding with the given index from the parameter map. + pub(crate) fn delete(&mut self, index: usize) { + if let Some(binding) = self.binding_indices.get_mut(index) { + *binding = None; + } + } + + /// Get the value of the binding at the given index from the function environment. + /// + /// Note: This function is the abstract getter closure described in 10.4.4.7.1 `MakeArgGetter ( name, env )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-makearggetter + pub(crate) fn get(&self, index: usize) -> Option { + if let Some(Some(binding_index)) = self.binding_indices.get(index) { + return Some(self.environment.get(*binding_index)); + } + None + } + + /// Set the value of the binding at the given index in the function environment. + /// + /// Note: This function is the abstract setter closure described in 10.4.4.7.2 `MakeArgSetter ( name, env )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-makeargsetter + pub(crate) fn set(&self, index: usize, value: &JsValue) { + if let Some(Some(binding_index)) = self.binding_indices.get(index) { + self.environment.set(*binding_index, value.clone()); + } + } +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub enum Arguments { + Unmapped, + Mapped(ParameterMap), +} + +impl Arguments { + /// Creates a new unmapped Arguments ordinary object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createunmappedargumentsobject + pub(crate) fn create_unmapped_arguments_object( + arguments_list: &[JsValue], + context: &mut Context, + ) -> JsObject { + // 1. Let len be the number of elements in argumentsList. + let len = arguments_list.len(); + + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%, « [[ParameterMap]] »). + let obj = context.construct_object(); + + // 3. Set obj.[[ParameterMap]] to undefined. + // skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]` + obj.borrow_mut().data = ObjectData::arguments(Self::Unmapped); + + // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(len) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // 5. Let index be 0. + // 6. Repeat, while index < len, + for (index, value) in arguments_list.iter().cloned().enumerate() { + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + obj.create_data_property_or_throw(index, value, context) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // c. Set index to index + 1. + } + + // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + obj.define_property_or_throw( + symbol::WellKnownSymbols::iterator(), + PropertyDescriptor::builder() + .value(Array::values_intrinsic(context)) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + let throw_type_error = context.intrinsics().objects().throw_type_error(); + + // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, + // [[Configurable]]: false }). + obj.define_property_or_throw( + "callee", + PropertyDescriptor::builder() + .get(throw_type_error.clone()) + .set(throw_type_error) + .enumerable(false) + .configurable(false), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // 9. Return obj. + obj + } + + /// Creates a new mapped Arguments exotic object. + /// + /// + pub(crate) fn create_mapped_arguments_object( + func: &JsObject, + formals: &FormalParameterList, + arguments_list: &[JsValue], + env: &Gc, + context: &mut Context, + ) -> JsObject { + // 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers. + // It may contain duplicate identifiers. + // 2. Let len be the number of elements in argumentsList. + let len = arguments_list.len(); + + // 3. Let obj be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[ParameterMap]] »). + // 4. Set obj.[[GetOwnProperty]] as specified in 10.4.4.1. + // 5. Set obj.[[DefineOwnProperty]] as specified in 10.4.4.2. + // 6. Set obj.[[Get]] as specified in 10.4.4.3. + // 7. Set obj.[[Set]] as specified in 10.4.4.4. + // 8. Set obj.[[Delete]] as specified in 10.4.4.5. + // 9. Set obj.[[Prototype]] to %Object.prototype%. + + // Section 17-19 are done first, for easier object creation in 11. + // + // The section 17-19 differs from the spec, due to the way the runtime environments work. + // + // This section creates getters and setters for all mapped arguments. + // Getting and setting values on the `arguments` object will actually access the bindings in the environment: + // ``` + // function f(a) {console.log(a); arguments[0] = 1; console.log(a)}; + // f(0) // 0, 1 + // ``` + // + // The spec assumes, that identifiers are used at runtime to reference bindings in the environment. + // We use indices to access environment bindings at runtime. + // To map to function parameters to binding indices, we use the fact, that bindings in a + // function environment start with all of the arguments in order: + // `function f (a,b,c)` + // | binding index | `arguments` property key | identifier | + // | 0 | 0 | a | + // | 1 | 1 | b | + // | 2 | 2 | c | + // + // Notice that the binding index does not correspond to the argument index: + // `function f (a,a,b)` => binding indices 0 (a), 1 (b), 2 (c) + // | binding index | `arguments` property key | identifier | + // | - | 0 | - | + // | 0 | 1 | a | + // | 1 | 2 | b | + // While the `arguments` object contains all arguments, they must not be all bound. + // In the case of duplicate parameter names, the last one is bound as the environment binding. + // + // The following logic implements the steps 17-19 adjusted for our environment structure. + + let mut bindings = FxHashMap::default(); + let mut property_index = 0; + 'outer: for formal in formals.parameters.iter() { + for name in formal.names() { + if property_index >= len { + break 'outer; + } + let binding_index = bindings.len() + 1; + let entry = bindings + .entry(name) + .or_insert((binding_index, property_index)); + entry.1 = property_index; + property_index += 1; + } + } + + let mut map = ParameterMap { + binding_indices: vec![None; property_index], + environment: env.clone(), + }; + + for (binding_index, property_index) in bindings.values() { + map.binding_indices[*property_index] = Some(*binding_index); + } + + // 11. Set obj.[[ParameterMap]] to map. + let obj = JsObject::from_proto_and_data( + context.intrinsics().constructors().object().prototype(), + ObjectData::arguments(Self::Mapped(map)), + ); + + // 14. Let index be 0. + // 15. Repeat, while index < len, + for (index, val) in arguments_list.iter().cloned().enumerate() { + // a. Let val be argumentsList[index]. + // b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val). + // Note: Insert is used here because `CreateDataPropertyOrThrow` would cause a panic while executing + // exotic argument object set methods before the variables in the environment are initialized. + obj.insert( + index, + PropertyDescriptor::builder() + .value(val) + .writable(true) + .enumerable(true) + .configurable(true) + .build(), + ); + // c. Set index to index + 1. + } + + // 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len), + // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(len) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor { + // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false, + // [[Configurable]]: true }). + obj.define_property_or_throw( + WellKnownSymbols::iterator(), + PropertyDescriptor::builder() + .value(Array::values_intrinsic(context)) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { + // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). + obj.define_property_or_throw( + "callee", + PropertyDescriptor::builder() + .value(func.clone()) + .writable(true) + .enumerable(false) + .configurable(true), + context, + ) + .expect("Defining new own properties for a new ordinary object cannot fail"); + + // 22. Return obj. + obj + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/mod.rs new file mode 100644 index 0000000..1e23740 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/mod.rs @@ -0,0 +1,1028 @@ +//! This module implements the global `Function` object as well as creates Native Functions. +//! +//! Objects wrap `Function`s and expose them via call/construct slots. +//! +//! The `Function` object is used for matching text with a pattern. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-function-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function + +use crate::{ + builtins::{BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + environments::DeclarativeEnvironmentStack, + object::{ + internal_methods::get_prototype_from_constructor, JsObject, NativeObject, Object, + ObjectData, + }, + object::{ConstructorBuilder, FunctionBuilder, JsFunction, PrivateElement, Ref, RefMut}, + property::{Attribute, PropertyDescriptor, PropertyKey}, + symbol::WellKnownSymbols, + syntax::{ + ast::node::{FormalParameterList, StatementList}, + Parser, + }, + value::IntegerOrInfinity, + Context, JsResult, JsString, JsValue, +}; +use boa_gc::{self, custom_trace, Finalize, Gc, Trace}; +use boa_interner::Sym; +use boa_profiler::Profiler; +use dyn_clone::DynClone; +use std::{ + any::Any, + borrow::Cow, + fmt, + ops::{Deref, DerefMut}, +}; +use tap::{Conv, Pipe}; + +use super::promise::PromiseCapability; + +pub(crate) mod arguments; +#[cfg(test)] +mod tests; + +/// Type representing a native built-in function a.k.a. function pointer. +/// +/// Native functions need to have this signature in order to +/// be callable from Javascript. +/// +/// # Arguments +/// +/// - The first argument represents the `this` variable of every Javascript function. +/// +/// - The second argument represents a list of all arguments passed to the function. +/// +/// - The last argument is the [`Context`] of the engine. +pub type NativeFunctionSignature = fn(&JsValue, &[JsValue], &mut Context) -> JsResult; + +// Allows restricting closures to only `Copy` ones. +// Used the sealed pattern to disallow external implementations +// of `DynCopy`. +mod sealed { + pub trait Sealed {} + impl Sealed for T {} +} +pub trait DynCopy: sealed::Sealed {} +impl DynCopy for T {} + +/// Trait representing a native built-in closure. +/// +/// Closures need to have this signature in order to +/// be callable from Javascript, but most of the time the compiler +/// is smart enough to correctly infer the types. +pub trait ClosureFunctionSignature: + Fn(&JsValue, &[JsValue], Captures, &mut Context) -> JsResult + DynCopy + DynClone + 'static +{ +} + +impl ClosureFunctionSignature for T where + T: Fn(&JsValue, &[JsValue], Captures, &mut Context) -> JsResult + Copy + 'static +{ +} + +// Allows cloning Box +dyn_clone::clone_trait_object!(ClosureFunctionSignature); + +#[derive(Debug, Trace, Finalize, PartialEq, Clone)] +pub enum ThisMode { + Lexical, + Strict, + Global, +} + +impl ThisMode { + /// Returns `true` if the this mode is `Lexical`. + pub fn is_lexical(&self) -> bool { + matches!(self, Self::Lexical) + } + + /// Returns `true` if the this mode is `Strict`. + pub fn is_strict(&self) -> bool { + matches!(self, Self::Strict) + } + + /// Returns `true` if the this mode is `Global`. + pub fn is_global(&self) -> bool { + matches!(self, Self::Global) + } +} + +/// Represents the `[[ConstructorKind]]` internal slot of function objects. +/// +/// More information: +/// - [ECMAScript specification][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ecmascript-function-objects +#[derive(Clone, Copy, Debug, PartialEq)] +pub enum ConstructorKind { + Base, + Derived, +} + +impl ConstructorKind { + /// Returns `true` if the constructor kind is `Base`. + pub fn is_base(&self) -> bool { + matches!(self, Self::Base) + } + + /// Returns `true` if the constructor kind is `Derived`. + pub fn is_derived(&self) -> bool { + matches!(self, Self::Derived) + } +} + +/// Record containing the field definition of classes. +/// +/// More information: +/// - [ECMAScript specification][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-classfielddefinition-record-specification-type +#[derive(Clone, Debug, Finalize)] +pub enum ClassFieldDefinition { + Public(PropertyKey, JsFunction), + Private(Sym, JsFunction), +} + +unsafe impl Trace for ClassFieldDefinition { + custom_trace! {this, { + match this { + Self::Public(key, func) => { + mark(key); + mark(func); + } + Self::Private(_, func) => { + mark(func); + } + } + }} +} + +/// Wrapper for `Gc>` that allows passing additional +/// captures through a `Copy` closure. +/// +/// Any type implementing `Trace + Any + Debug` +/// can be used as a capture context, so you can pass e.g. a String, +/// a tuple or even a full struct. +/// +/// You can cast to `Any` with `as_any`, `as_mut_any` and downcast +/// with `Any::downcast_ref` and `Any::downcast_mut` to recover the original +/// type. +#[derive(Clone, Debug, Trace, Finalize)] +pub struct Captures(Gc>>); + +impl Captures { + /// Creates a new capture context. + pub(crate) fn new(captures: T) -> Self + where + T: NativeObject, + { + Self(Gc::new(boa_gc::Cell::new(Box::new(captures)))) + } + + /// Casts `Captures` to `Any` + /// + /// # Panics + /// + /// Panics if it's already borrowed as `&mut Any` + pub fn as_any(&self) -> boa_gc::Ref<'_, dyn Any> { + Ref::map(self.0.borrow(), |data| data.deref().as_any()) + } + + /// Mutably casts `Captures` to `Any` + /// + /// # Panics + /// + /// Panics if it's already borrowed as `&mut Any` + pub fn as_mut_any(&self) -> boa_gc::RefMut<'_, Box, dyn Any> { + RefMut::map(self.0.borrow_mut(), |data| data.deref_mut().as_mut_any()) + } +} + +/// Boa representation of a Function Object. +/// +/// `FunctionBody` is specific to this interpreter, it will either be Rust code or JavaScript code +/// (AST Node). +/// +/// +#[derive(Finalize)] +pub enum Function { + Native { + function: NativeFunctionSignature, + constructor: Option, + }, + Closure { + function: Box, + constructor: Option, + captures: Captures, + }, + Ordinary { + code: Gc, + environments: DeclarativeEnvironmentStack, + + /// The `[[ConstructorKind]]` internal slot. + constructor_kind: ConstructorKind, + + /// The `[[HomeObject]]` internal slot. + home_object: Option, + + /// The `[[Fields]]` internal slot. + fields: Vec, + + /// The `[[PrivateMethods]]` internal slot. + private_methods: Vec<(Sym, PrivateElement)>, + }, + Async { + code: Gc, + environments: DeclarativeEnvironmentStack, + promise_capability: PromiseCapability, + }, + Generator { + code: Gc, + environments: DeclarativeEnvironmentStack, + }, +} + +unsafe impl Trace for Function { + custom_trace! {this, { + match this { + Self::Native { .. } => {} + Self::Closure { captures, .. } => mark(captures), + Self::Ordinary { code, environments, home_object, fields, private_methods, .. } => { + mark(code); + mark(environments); + mark(home_object); + mark(fields); + for (_, elem) in private_methods { + mark(elem); + } + } + Self::Async { code, environments, promise_capability } => { + mark(code); + mark(environments); + mark(promise_capability); + } + Self::Generator { code, environments } => { + mark(code); + mark(environments); + } + } + }} +} + +impl fmt::Debug for Function { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Function {{ ... }}") + } +} + +impl Function { + /// Returns true if the function object is a constructor. + pub fn is_constructor(&self) -> bool { + match self { + Self::Native { constructor, .. } | Self::Closure { constructor, .. } => { + constructor.is_some() + } + Self::Generator { .. } | Self::Async { .. } => false, + Self::Ordinary { code, .. } => !(code.this_mode == ThisMode::Lexical), + } + } + + /// Returns true if the function object is a derived constructor. + pub(crate) fn is_derived_constructor(&self) -> bool { + if let Self::Ordinary { + constructor_kind, .. + } = self + { + constructor_kind.is_derived() + } else { + false + } + } + + /// Returns a reference to the function `[[HomeObject]]` slot if present. + pub(crate) fn get_home_object(&self) -> Option<&JsObject> { + if let Self::Ordinary { home_object, .. } = self { + home_object.as_ref() + } else { + None + } + } + + /// Sets the `[[HomeObject]]` slot if present. + pub(crate) fn set_home_object(&mut self, object: JsObject) { + if let Self::Ordinary { home_object, .. } = self { + *home_object = Some(object); + } + } + + /// Returns the values of the `[[Fields]]` internal slot. + pub(crate) fn get_fields(&self) -> &[ClassFieldDefinition] { + if let Self::Ordinary { fields, .. } = self { + fields + } else { + &[] + } + } + + /// Pushes a value to the `[[Fields]]` internal slot if present. + pub(crate) fn push_field(&mut self, key: PropertyKey, value: JsFunction) { + if let Self::Ordinary { fields, .. } = self { + fields.push(ClassFieldDefinition::Public(key, value)); + } + } + + /// Pushes a private value to the `[[Fields]]` internal slot if present. + pub(crate) fn push_field_private(&mut self, key: Sym, value: JsFunction) { + if let Self::Ordinary { fields, .. } = self { + fields.push(ClassFieldDefinition::Private(key, value)); + } + } + + /// Returns the values of the `[[PrivateMethods]]` internal slot. + pub(crate) fn get_private_methods(&self) -> &[(Sym, PrivateElement)] { + if let Self::Ordinary { + private_methods, .. + } = self + { + private_methods + } else { + &[] + } + } + + /// Pushes a private method to the `[[PrivateMethods]]` internal slot if present. + pub(crate) fn push_private_method(&mut self, name: Sym, method: PrivateElement) { + if let Self::Ordinary { + private_methods, .. + } = self + { + private_methods.push((name, method)); + } + } + + /// Returns the promise capability if the function is an async function. + pub(crate) fn get_promise_capability(&self) -> Option<&PromiseCapability> { + if let Self::Async { + promise_capability, .. + } = self + { + Some(promise_capability) + } else { + None + } + } +} + +/// Creates a new member function of a `Object` or `prototype`. +/// +/// A function registered using this macro can then be called from Javascript using: +/// +/// parent.name() +/// +/// See the javascript 'Number.toString()' as an example. +/// +/// # Arguments +/// function: The function to register as a built in function. +/// name: The name of the function (how it will be called but without the ()). +/// parent: The object to register the function on, if the global object is used then the function is instead called as name() +/// without requiring the parent, see parseInt() as an example. +/// length: As described at , The value of the "length" property is an integer that +/// indicates the typical number of arguments expected by the function. However, the language permits the function to be invoked with +/// some other number of arguments. +/// +/// If no length is provided, the length will be set to 0. +// TODO: deprecate/remove this. +pub(crate) fn make_builtin_fn( + function: NativeFunctionSignature, + name: N, + parent: &JsObject, + length: usize, + interpreter: &Context, +) where + N: Into, +{ + let name = name.into(); + let _timer = Profiler::global().start_event(&format!("make_builtin_fn: {name}"), "init"); + + let function = JsObject::from_proto_and_data( + interpreter + .intrinsics() + .constructors() + .function() + .prototype(), + ObjectData::function(Function::Native { + function, + constructor: None, + }), + ); + let attribute = PropertyDescriptor::builder() + .writable(false) + .enumerable(false) + .configurable(true); + function.insert_property("length", attribute.clone().value(length)); + function.insert_property("name", attribute.value(name.as_str())); + + parent.clone().insert_property( + name, + PropertyDescriptor::builder() + .value(function) + .writable(true) + .enumerable(false) + .configurable(true), + ); +} + +#[derive(Debug, Clone, Copy)] +pub struct BuiltInFunctionObject; + +impl BuiltInFunctionObject { + pub const LENGTH: usize = 1; + + /// `Function ( p1, p2, … , pn, body )` + /// + /// The apply() method invokes self with the first argument as the `this` value + /// and the rest of the arguments provided as an array (or an array-like object). + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function-p1-p2-pn-body + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/Function + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::create_dynamic_function(new_target, args, false, false, context).map(Into::into) + } + + /// `CreateDynamicFunction ( constructor, newTarget, kind, args )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createdynamicfunction + pub(crate) fn create_dynamic_function( + new_target: &JsValue, + args: &[JsValue], + r#async: bool, + generator: bool, + context: &mut Context, + ) -> JsResult { + let default = if r#async { + StandardConstructors::async_function + } else if generator { + StandardConstructors::generator_function + } else { + StandardConstructors::function + }; + + let prototype = get_prototype_from_constructor(new_target, default, context)?; + if let Some((body_arg, args)) = args.split_last() { + let parameters = if args.is_empty() { + FormalParameterList::empty() + } else { + let mut parameters = Vec::with_capacity(args.len()); + for arg in args { + parameters.push(arg.to_string(context)?); + } + let mut parameters = parameters.join(","); + parameters.push(')'); + + let parameters = match Parser::new(parameters.as_bytes()).parse_formal_parameters( + context.interner_mut(), + generator, + r#async, + ) { + Ok(parameters) => parameters, + Err(e) => { + return context.throw_syntax_error(format!( + "failed to parse function parameters: {e}" + )) + } + }; + + if generator && parameters.contains_yield_expression() { + return context.throw_syntax_error( + "yield expression is not allowed in formal parameter list of generator function", + ); + } + + parameters + }; + + let body_arg = body_arg.to_string(context)?; + + let body = match Parser::new(body_arg.as_bytes()).parse_function_body( + context.interner_mut(), + generator, + r#async, + ) { + Ok(statement_list) => statement_list, + Err(e) => { + return context + .throw_syntax_error(format!("failed to parse function body: {e}")) + } + }; + + // Early Error: If BindingIdentifier is present and the source text matched by BindingIdentifier is strict mode code, + // it is a Syntax Error if the StringValue of BindingIdentifier is "eval" or "arguments". + if body.strict() { + for parameter in parameters.parameters.iter() { + for name in parameter.names() { + if name == Sym::ARGUMENTS || name == Sym::EVAL { + return context.throw_syntax_error( + " Unexpected 'eval' or 'arguments' in strict mode", + ); + } + } + } + } + + // Early Error: If the source code matching FormalParameters is strict mode code, + // the Early Error rules for UniqueFormalParameters : FormalParameters are applied. + if (body.strict()) && parameters.has_duplicates() { + return context + .throw_syntax_error("Duplicate parameter name not allowed in this context"); + } + + // Early Error: It is a Syntax Error if FunctionBodyContainsUseStrict of GeneratorBody is true + // and IsSimpleParameterList of FormalParameters is false. + if body.strict() && !parameters.is_simple() { + return context.throw_syntax_error( + "Illegal 'use strict' directive in function with non-simple parameter list", + ); + } + + // It is a Syntax Error if any element of the BoundNames of FormalParameters + // also occurs in the LexicallyDeclaredNames of FunctionBody. + // https://tc39.es/ecma262/#sec-function-definitions-static-semantics-early-errors + { + let lexically_declared_names = body.lexically_declared_names(); + for param in parameters.parameters.as_ref() { + for param_name in param.names() { + if lexically_declared_names + .iter() + .any(|(name, _)| *name == param_name) + { + return context.throw_syntax_error(format!( + "Redeclaration of formal parameter `{}`", + context.interner().resolve_expect(param_name) + )); + } + } + } + } + + let code = crate::bytecompiler::ByteCompiler::compile_function_code( + crate::bytecompiler::FunctionKind::Expression, + Some(Sym::ANONYMOUS), + ¶meters, + &body, + generator, + false, + context, + )?; + + let environments = context.realm.environments.pop_to_global(); + + let function_object = if generator { + crate::vm::create_generator_function_object(code, context) + } else { + crate::vm::create_function_object(code, r#async, Some(prototype), context) + }; + + context.realm.environments.extend(environments); + + Ok(function_object) + } else if generator { + let code = crate::bytecompiler::ByteCompiler::compile_function_code( + crate::bytecompiler::FunctionKind::Expression, + Some(Sym::ANONYMOUS), + &FormalParameterList::empty(), + &StatementList::default(), + true, + false, + context, + )?; + + let environments = context.realm.environments.pop_to_global(); + let function_object = crate::vm::create_generator_function_object(code, context); + context.realm.environments.extend(environments); + + Ok(function_object) + } else { + let code = crate::bytecompiler::ByteCompiler::compile_function_code( + crate::bytecompiler::FunctionKind::Expression, + Some(Sym::ANONYMOUS), + &FormalParameterList::empty(), + &StatementList::default(), + false, + false, + context, + )?; + + let environments = context.realm.environments.pop_to_global(); + let function_object = + crate::vm::create_function_object(code, r#async, Some(prototype), context); + context.realm.environments.extend(environments); + + Ok(function_object) + } + } + + /// `Function.prototype.apply ( thisArg, argArray )` + /// + /// The apply() method invokes self with the first argument as the `this` value + /// and the rest of the arguments provided as an array (or an array-like object). + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.apply + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/apply + fn apply(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let func be the this value. + // 2. If IsCallable(func) is false, throw a TypeError exception. + let func = this.as_callable().ok_or_else(|| { + context.construct_type_error(format!("{} is not a function", this.display())) + })?; + + let this_arg = args.get_or_undefined(0); + let arg_array = args.get_or_undefined(1); + // 3. If argArray is undefined or null, then + if arg_array.is_null_or_undefined() { + // a. Perform PrepareForTailCall(). + // TODO?: 3.a. PrepareForTailCall + + // b. Return ? Call(func, thisArg). + return func.call(this_arg, &[], context); + } + + // 4. Let argList be ? CreateListFromArrayLike(argArray). + let arg_list = arg_array.create_list_from_array_like(&[], context)?; + + // 5. Perform PrepareForTailCall(). + // TODO?: 5. PrepareForTailCall + + // 6. Return ? Call(func, thisArg, argList). + func.call(this_arg, &arg_list, context) + } + + /// `Function.prototype.bind ( thisArg, ...args )` + /// + /// The bind() method creates a new function that, when called, has its + /// this keyword set to the provided value, with a given sequence of arguments + /// preceding any provided when the new function is called. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.bind + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_objects/Function/bind + fn bind(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let Target be the this value. + // 2. If IsCallable(Target) is false, throw a TypeError exception. + let target = this.as_callable().ok_or_else(|| { + context.construct_type_error("cannot bind `this` without a `[[Call]]` internal method") + })?; + + let this_arg = args.get_or_undefined(0).clone(); + let bound_args = args.get(1..).unwrap_or(&[]).to_vec(); + let arg_count = bound_args.len() as i64; + + // 3. Let F be ? BoundFunctionCreate(Target, thisArg, args). + let f = BoundFunction::create(target.clone(), this_arg, bound_args, context)?; + + // 4. Let L be 0. + let mut l = JsValue::new(0); + + // 5. Let targetHasLength be ? HasOwnProperty(Target, "length"). + // 6. If targetHasLength is true, then + if target.has_own_property("length", context)? { + // a. Let targetLen be ? Get(Target, "length"). + let target_len = target.get("length", context)?; + // b. If Type(targetLen) is Number, then + if target_len.is_number() { + // 1. Let targetLenAsInt be ! ToIntegerOrInfinity(targetLen). + match target_len + .to_integer_or_infinity(context) + .expect("to_integer_or_infinity cannot fail for a number") + { + // i. If targetLen is +∞𝔽, set L to +∞. + IntegerOrInfinity::PositiveInfinity => l = f64::INFINITY.into(), + // ii. Else if targetLen is -∞𝔽, set L to 0. + IntegerOrInfinity::NegativeInfinity => {} + // iii. Else, + IntegerOrInfinity::Integer(target_len) => { + // 2. Assert: targetLenAsInt is finite. + // 3. Let argCount be the number of elements in args. + // 4. Set L to max(targetLenAsInt - argCount, 0). + l = (target_len - arg_count).max(0).into(); + } + } + } + } + + // 7. Perform ! SetFunctionLength(F, L). + f.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(l) + .writable(false) + .enumerable(false) + .configurable(true), + context, + ) + .expect("defining the `length` property for a new object should not fail"); + + // 8. Let targetName be ? Get(Target, "name"). + let target_name = target.get("name", context)?; + + // 9. If Type(targetName) is not String, set targetName to the empty String. + let target_name = target_name + .as_string() + .map_or(JsString::new(""), Clone::clone); + + // 10. Perform SetFunctionName(F, targetName, "bound"). + set_function_name(&f, &target_name.into(), Some("bound"), context); + + // 11. Return F. + Ok(f.into()) + } + + /// `Function.prototype.call ( thisArg, ...args )` + /// + /// The call() method calls a function with a given this value and arguments provided individually. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype.call + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function/call + fn call(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let func be the this value. + // 2. If IsCallable(func) is false, throw a TypeError exception. + let func = this.as_callable().ok_or_else(|| { + context.construct_type_error(format!("{} is not a function", this.display())) + })?; + let this_arg = args.get_or_undefined(0); + + // 3. Perform PrepareForTailCall(). + // TODO?: 3. Perform PrepareForTailCall + + // 4. Return ? Call(func, thisArg, args). + func.call(this_arg, args.get(1..).unwrap_or(&[]), context) + } + + #[allow(clippy::wrong_self_convention)] + fn to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let object = this.as_object().map(JsObject::borrow); + let function = object + .as_deref() + .and_then(Object::as_function) + .ok_or_else(|| context.construct_type_error("Not a function"))?; + + let name = { + // Is there a case here where if there is no name field on a value + // name should default to None? Do all functions have names set? + let value = this + .as_object() + .expect("checked that `this` was an object above") + .get("name", &mut *context)?; + if value.is_null_or_undefined() { + None + } else { + Some(value.to_string(context)?) + } + }; + + match (function, name) { + ( + Function::Native { + function: _, + constructor: _, + }, + Some(name), + ) => Ok(format!("function {name}() {{\n [native Code]\n}}").into()), + (Function::Ordinary { .. }, Some(name)) if name.is_empty() => { + Ok("[Function (anonymous)]".into()) + } + (Function::Ordinary { .. }, Some(name)) => Ok(format!("[Function: {name}]").into()), + (Function::Ordinary { .. }, None) => Ok("[Function (anonymous)]".into()), + (Function::Generator { .. }, Some(name)) => { + Ok(format!("[Function*: {}]", &name).into()) + } + (Function::Generator { .. }, None) => Ok("[Function* (anonymous)]".into()), + _ => Ok("TODO".into()), + } + } + + /// `Function.prototype [ @@hasInstance ] ( V )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-function.prototype-@@hasinstance + fn has_instance(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let F be the this value. + // 2. Return ? OrdinaryHasInstance(F, V). + Ok(JsValue::ordinary_has_instance(this, args.get_or_undefined(0), context)?.into()) + } + + #[allow(clippy::unnecessary_wraps)] + fn prototype(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Ok(JsValue::undefined()) + } +} + +impl BuiltIn for BuiltInFunctionObject { + const NAME: &'static str = "Function"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event("function", "init"); + + let function_prototype = context.intrinsics().constructors().function().prototype(); + FunctionBuilder::native(context, Self::prototype) + .name("") + .length(0) + .constructor(false) + .build_function_prototype(&function_prototype); + + let symbol_has_instance = WellKnownSymbols::has_instance(); + + let has_instance = FunctionBuilder::native(context, Self::has_instance) + .name("[Symbol.iterator]") + .length(1) + .constructor(false) + .build(); + + let throw_type_error = context.intrinsics().objects().throw_type_error(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().function().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .method(Self::apply, "apply", 2) + .method(Self::bind, "bind", 1) + .method(Self::call, "call", 1) + .method(Self::to_string, "toString", 0) + .property(symbol_has_instance, has_instance, Attribute::default()) + .property_descriptor( + "caller", + PropertyDescriptor::builder() + .get(throw_type_error.clone()) + .set(throw_type_error.clone()) + .enumerable(false) + .configurable(true), + ) + .property_descriptor( + "arguments", + PropertyDescriptor::builder() + .get(throw_type_error.clone()) + .set(throw_type_error) + .enumerable(false) + .configurable(true), + ) + .build() + .conv::() + .pipe(Some) + } +} + +/// Abstract operation `SetFunctionName` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-setfunctionname +fn set_function_name( + function: &JsObject, + name: &PropertyKey, + prefix: Option<&str>, + context: &mut Context, +) { + // 1. Assert: F is an extensible object that does not have a "name" own property. + // 2. If Type(name) is Symbol, then + let mut name = match name { + PropertyKey::Symbol(sym) => { + // a. Let description be name's [[Description]] value. + if let Some(desc) = sym.description() { + // c. Else, set name to the string-concatenation of "[", description, and "]". + Cow::Owned(JsString::concat_array(&["[", &desc, "]"])) + } else { + // b. If description is undefined, set name to the empty String. + Cow::Owned(JsString::new("")) + } + } + PropertyKey::String(string) => Cow::Borrowed(string), + PropertyKey::Index(index) => Cow::Owned(JsString::new(index.to_string())), + }; + + // 3. Else if name is a Private Name, then + // a. Set name to name.[[Description]]. + // todo: implement Private Names + + // 4. If F has an [[InitialName]] internal slot, then + // a. Set F.[[InitialName]] to name. + // todo: implement [[InitialName]] for builtins + + // 5. If prefix is present, then + if let Some(prefix) = prefix { + name = Cow::Owned(JsString::concat_array(&[prefix, " ", &name])); + // b. If F has an [[InitialName]] internal slot, then + // i. Optionally, set F.[[InitialName]] to name. + // todo: implement [[InitialName]] for builtins + } + + // 6. Return ! DefinePropertyOrThrow(F, "name", PropertyDescriptor { [[Value]]: name, + // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: true }). + function + .define_property_or_throw( + "name", + PropertyDescriptor::builder() + .value(name.into_owned()) + .writable(false) + .enumerable(false) + .configurable(true), + context, + ) + .expect("defining the `name` property must not fail per the spec"); +} + +/// Binds a `Function Object` when `bind` is called. +#[derive(Debug, Trace, Finalize)] +pub struct BoundFunction { + target_function: JsObject, + this: JsValue, + args: Vec, +} + +impl BoundFunction { + /// Abstract operation `BoundFunctionCreate` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-boundfunctioncreate + pub fn create( + target_function: JsObject, + this: JsValue, + args: Vec, + context: &mut Context, + ) -> JsResult { + // 1. Let proto be ? targetFunction.[[GetPrototypeOf]](). + let proto = target_function.__get_prototype_of__(context)?; + let is_constructor = target_function.is_constructor(); + + // 2. Let internalSlotsList be the internal slots listed in Table 35, plus [[Prototype]] and [[Extensible]]. + // 3. Let obj be ! MakeBasicObject(internalSlotsList). + // 4. Set obj.[[Prototype]] to proto. + // 5. Set obj.[[Call]] as described in 10.4.1.1. + // 6. If IsConstructor(targetFunction) is true, then + // a. Set obj.[[Construct]] as described in 10.4.1.2. + // 7. Set obj.[[BoundTargetFunction]] to targetFunction. + // 8. Set obj.[[BoundThis]] to boundThis. + // 9. Set obj.[[BoundArguments]] to boundArgs. + // 10. Return obj. + Ok(JsObject::from_proto_and_data( + proto, + ObjectData::bound_function( + Self { + target_function, + this, + args, + }, + is_constructor, + ), + )) + } + + /// Get a reference to the bound function's this. + pub fn this(&self) -> &JsValue { + &self.this + } + + /// Get a reference to the bound function's target function. + pub fn target_function(&self) -> &JsObject { + &self.target_function + } + + /// Get a reference to the bound function's args. + pub fn args(&self) -> &[JsValue] { + self.args.as_slice() + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/tests.rs new file mode 100644 index 0000000..f183343 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/function/tests.rs @@ -0,0 +1,262 @@ +use crate::{ + forward, forward_val, + object::FunctionBuilder, + property::{Attribute, PropertyDescriptor}, + Context, JsString, +}; + +#[allow(clippy::float_cmp)] +#[test] +fn arguments_object() { + let mut context = Context::default(); + + let init = r#" + function jason(a, b) { + return arguments[0]; + } + var val = jason(100, 6); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let return_val = forward_val(&mut context, "val").expect("value expected"); + assert!(return_val.is_integer()); + assert_eq!( + return_val + .to_i32(&mut context) + .expect("Could not convert value to i32"), + 100 + ); +} + +#[test] +fn self_mutating_function_when_calling() { + let mut context = Context::default(); + let func = r#" + function x() { + x.y = 3; + } + x(); + "#; + eprintln!("{}", forward(&mut context, func)); + let y = forward_val(&mut context, "x.y").expect("value expected"); + assert!(y.is_integer()); + assert_eq!( + y.to_i32(&mut context) + .expect("Could not convert value to i32"), + 3 + ); +} + +#[test] +fn self_mutating_function_when_constructing() { + let mut context = Context::default(); + let func = r#" + function x() { + x.y = 3; + } + new x(); + "#; + eprintln!("{}", forward(&mut context, func)); + let y = forward_val(&mut context, "x.y").expect("value expected"); + assert!(y.is_integer()); + assert_eq!( + y.to_i32(&mut context) + .expect("Could not convert value to i32"), + 3 + ); +} + +#[test] +fn call_function_prototype() { + let mut context = Context::default(); + let func = r#" + Function.prototype() + "#; + let value = forward_val(&mut context, func).unwrap(); + assert!(value.is_undefined()); +} + +#[test] +fn call_function_prototype_with_arguments() { + let mut context = Context::default(); + let func = r#" + Function.prototype(1, "", new String("")) + "#; + let value = forward_val(&mut context, func).unwrap(); + assert!(value.is_undefined()); +} + +#[test] +fn call_function_prototype_with_new() { + let mut context = Context::default(); + let func = r#" + new Function.prototype() + "#; + let value = forward_val(&mut context, func); + assert!(value.is_err()); +} + +#[test] +fn function_prototype_name() { + let mut context = Context::default(); + let func = r#" + Function.prototype.name + "#; + let value = forward_val(&mut context, func).unwrap(); + assert!(value.is_string()); + assert!(value.as_string().unwrap().is_empty()); +} + +#[test] +#[allow(clippy::float_cmp)] +fn function_prototype_length() { + let mut context = Context::default(); + let func = r#" + Function.prototype.length + "#; + let value = forward_val(&mut context, func).unwrap(); + assert!(value.is_number()); + assert_eq!(value.as_number().unwrap(), 0.0); +} + +#[test] +fn function_prototype_call() { + let mut context = Context::default(); + let func = r#" + let e = new Error() + Object.prototype.toString.call(e) + "#; + let value = forward_val(&mut context, func).unwrap(); + assert!(value.is_string()); + assert_eq!(value.as_string().unwrap(), "[object Error]"); +} + +#[test] +fn function_prototype_call_throw() { + let mut context = Context::default(); + let throw = r#" + let call = Function.prototype.call; + call(call) + "#; + let value = forward_val(&mut context, throw).unwrap_err(); + assert!(value.is_object()); + let string = value.to_string(&mut context).unwrap(); + assert!(string.starts_with("TypeError")); +} + +#[test] +fn function_prototype_call_multiple_args() { + let mut context = Context::default(); + let init = r#" + function f(a, b) { + this.a = a; + this.b = b; + } + let o = {a: 0, b: 0}; + f.call(o, 1, 2); + "#; + forward_val(&mut context, init).unwrap(); + let boolean = forward_val(&mut context, "o.a == 1") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); + let boolean = forward_val(&mut context, "o.b == 2") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); +} + +#[test] +fn function_prototype_apply() { + let mut context = Context::default(); + let init = r#" + const numbers = [6, 7, 3, 4, 2]; + const max = Math.max.apply(null, numbers); + const min = Math.min.apply(null, numbers); + "#; + forward_val(&mut context, init).unwrap(); + + let boolean = forward_val(&mut context, "max == 7") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); + + let boolean = forward_val(&mut context, "min == 2") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); +} + +#[test] +fn function_prototype_apply_on_object() { + let mut context = Context::default(); + let init = r#" + function f(a, b) { + this.a = a; + this.b = b; + } + let o = {a: 0, b: 0}; + f.apply(o, [1, 2]); + "#; + forward_val(&mut context, init).unwrap(); + + let boolean = forward_val(&mut context, "o.a == 1") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); + + let boolean = forward_val(&mut context, "o.b == 2") + .unwrap() + .as_boolean() + .unwrap(); + assert!(boolean); +} + +#[test] +fn closure_capture_clone() { + let mut context = Context::default(); + + let string = JsString::from("Hello"); + let object = context.construct_object(); + object + .define_property_or_throw( + "key", + PropertyDescriptor::builder() + .value(" world!") + .writable(false) + .enumerable(false) + .configurable(false), + &mut context, + ) + .unwrap(); + + let func = FunctionBuilder::closure_with_captures( + &mut context, + |_, _, captures, context| { + let (string, object) = &captures; + + let hw = JsString::concat( + string, + object + .__get_own_property__(&"key".into(), context)? + .and_then(|prop| prop.value().cloned()) + .and_then(|val| val.as_string().cloned()) + .ok_or_else(|| context.construct_type_error("invalid `key` property"))?, + ); + Ok(hw.into()) + }, + (string, object), + ) + .name("closure") + .build(); + + context.register_global_property("closure", func, Attribute::default()); + + assert_eq!(forward(&mut context, "closure()"), "\"Hello world!\""); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator/mod.rs new file mode 100644 index 0000000..c446078 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator/mod.rs @@ -0,0 +1,419 @@ +//! This module implements the global `Generator` object. +//! +//! A Generator is an instance of a generator function and conforms to both the Iterator and Iterable interfaces. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-generator-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator + +use crate::{ + builtins::{iterable::create_iter_result_object, BuiltIn, JsArgs}, + environments::DeclarativeEnvironmentStack, + object::{ConstructorBuilder, JsObject, ObjectData}, + property::{Attribute, PropertyDescriptor}, + symbol::WellKnownSymbols, + value::JsValue, + vm::{CallFrame, GeneratorResumeKind, ReturnType}, + Context, JsResult, +}; +use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_profiler::Profiler; + +/// Indicates the state of a generator. +#[derive(Debug, Clone, Copy, PartialEq)] +pub(crate) enum GeneratorState { + Undefined, + SuspendedStart, + SuspendedYield, + Executing, + Completed, +} + +/// Holds all information that a generator needs to continue it's execution. +/// +/// All of the fields must be changed with those that are currently present in the +/// context/vm before the generator execution starts/resumes and after it has ended/yielded. +#[derive(Debug, Clone, Finalize, Trace)] +pub(crate) struct GeneratorContext { + pub(crate) environments: DeclarativeEnvironmentStack, + pub(crate) call_frame: CallFrame, + pub(crate) stack: Vec, +} + +/// The internal representation on a `Generator` object. +#[derive(Debug, Clone, Finalize, Trace)] +pub struct Generator { + /// The `[[GeneratorState]]` internal slot. + #[unsafe_ignore_trace] + pub(crate) state: GeneratorState, + + /// The `[[GeneratorContext]]` internal slot. + pub(crate) context: Option>>, +} + +impl BuiltIn for Generator { + const NAME: &'static str = "Generator"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let iterator_prototype = context + .intrinsics() + .objects() + .iterator_prototypes() + .iterator_prototype(); + + let generator_function_prototype = context + .intrinsics() + .constructors() + .generator_function() + .prototype(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().generator().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .method(Self::next, "next", 1) + .method(Self::r#return, "return", 1) + .method(Self::throw, "throw", 1) + .inherit(iterator_prototype) + .build(); + + context + .intrinsics() + .constructors() + .generator() + .prototype + .insert_property( + "constructor", + PropertyDescriptor::builder() + .value(generator_function_prototype) + .writable(false) + .enumerable(false) + .configurable(true), + ); + + None + } +} + +impl Generator { + pub(crate) const LENGTH: usize = 0; + + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn constructor( + _: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let prototype = context.intrinsics().constructors().generator().prototype(); + + let this = JsObject::from_proto_and_data( + prototype, + ObjectData::generator(Self { + state: GeneratorState::Undefined, + context: None, + }), + ); + + Ok(this.into()) + } + + /// `Generator.prototype.next ( value )` + /// + /// The `next()` method returns an object with two properties done and value. + /// You can also provide a parameter to the next method to send a value to the generator. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.next + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/next + pub(crate) fn next( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? GeneratorResume(this value, value, empty). + match this.as_object() { + Some(obj) if obj.is_generator() => { + Self::generator_resume(obj, args.get_or_undefined(0), context) + } + _ => context.throw_type_error("Generator.prototype.next called on non generator"), + } + } + + /// `Generator.prototype.return ( value )` + /// + /// The `return()` method returns the given value and finishes the generator. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.return + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return + pub(crate) fn r#return( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let g be the this value. + // 2. Let C be Completion { [[Type]]: return, [[Value]]: value, [[Target]]: empty }. + // 3. Return ? GeneratorResumeAbrupt(g, C, empty). + Self::generator_resume_abrupt(this, Ok(args.get_or_undefined(0).clone()), context) + } + + /// `Generator.prototype.throw ( exception )` + /// + /// The `throw()` method resumes the execution of a generator by throwing an error into it + /// and returns an object with two properties done and value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generator.prototype.throw + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/throw + pub(crate) fn throw( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let g be the this value. + // 2. Let C be ThrowCompletion(exception). + // 3. Return ? GeneratorResumeAbrupt(g, C, empty). + Self::generator_resume_abrupt(this, Err(args.get_or_undefined(0).clone()), context) + } + + /// `27.5.3.3 GeneratorResume ( generator, value, generatorBrand )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generatorresume + pub(crate) fn generator_resume( + generator_obj: &JsObject, + value: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Let state be ? GeneratorValidate(generator, generatorBrand). + let mut generator_obj_mut = generator_obj.borrow_mut(); + let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| { + context.construct_type_error("generator resumed on non generator object") + })?; + let state = generator.state; + + if state == GeneratorState::Executing { + return Err(context.construct_type_error("Generator should not be executing")); + } + + // 2. If state is completed, return CreateIterResultObject(undefined, true). + if state == GeneratorState::Completed { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + // 3. Assert: state is either suspendedStart or suspendedYield. + assert!(matches!( + state, + GeneratorState::SuspendedStart | GeneratorState::SuspendedYield + )); + + // 4. Let genContext be generator.[[GeneratorContext]]. + // 5. Let methodContext be the running execution context. + // 6. Suspend methodContext. + // 7. Set generator.[[GeneratorState]] to executing. + generator.state = GeneratorState::Executing; + let first_execution = matches!(state, GeneratorState::SuspendedStart); + + let generator_context_cell = generator + .context + .take() + .expect("generator context cannot be empty here"); + let mut generator_context = generator_context_cell.borrow_mut(); + drop(generator_obj_mut); + + std::mem::swap( + &mut context.realm.environments, + &mut generator_context.environments, + ); + std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); + context.vm.push_frame(generator_context.call_frame.clone()); + if !first_execution { + context.vm.push(value); + } + + context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Normal; + + let result = context.run(); + + generator_context.call_frame = context + .vm + .pop_frame() + .expect("generator call frame must exist"); + std::mem::swap( + &mut context.realm.environments, + &mut generator_context.environments, + ); + std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); + + let mut generator_obj_mut = generator_obj.borrow_mut(); + let generator = generator_obj_mut + .as_generator_mut() + .expect("already checked this object type"); + + match result { + Ok((value, ReturnType::Yield)) => { + generator.state = GeneratorState::SuspendedYield; + drop(generator_context); + generator.context = Some(generator_context_cell); + Ok(create_iter_result_object(value, false, context)) + } + Ok((value, _)) => { + generator.state = GeneratorState::Completed; + Ok(create_iter_result_object(value, true, context)) + } + Err(value) => { + generator.state = GeneratorState::Completed; + Err(value) + } + } + + // 8. Push genContext onto the execution context stack; genContext is now the running execution context. + // 9. Resume the suspended evaluation of genContext using NormalCompletion(value) as the result of the operation that suspended it. Let result be the value returned by the resumed computation. + // 10. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. + // 11. Return Completion(result). + } + + /// `27.5.3.4 GeneratorResumeAbrupt ( generator, abruptCompletion, generatorBrand )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generatorresumeabrupt + pub(crate) fn generator_resume_abrupt( + this: &JsValue, + abrupt_completion: JsResult, + context: &mut Context, + ) -> JsResult { + // 1. Let state be ? GeneratorValidate(generator, generatorBrand). + let generator_obj = this.as_object().ok_or_else(|| { + context.construct_type_error("generator resumed on non generator object") + })?; + let mut generator_obj_mut = generator_obj.borrow_mut(); + let generator = generator_obj_mut.as_generator_mut().ok_or_else(|| { + context.construct_type_error("generator resumed on non generator object") + })?; + let mut state = generator.state; + + if state == GeneratorState::Executing { + return Err(context.construct_type_error("Generator should not be executing")); + } + + // 2. If state is suspendedStart, then + if state == GeneratorState::SuspendedStart { + // a. Set generator.[[GeneratorState]] to completed. + generator.state = GeneratorState::Completed; + // b. Once a generator enters the completed state it never leaves it and its associated execution context is never resumed. Any execution state associated with generator can be discarded at this point. + generator.context = None; + // c. Set state to completed. + state = GeneratorState::Completed; + } + + // 3. If state is completed, then + if state == GeneratorState::Completed { + // a. If abruptCompletion.[[Type]] is return, then + if let Ok(value) = abrupt_completion { + // i. Return CreateIterResultObject(abruptCompletion.[[Value]], true). + return Ok(create_iter_result_object(value, true, context)); + } + // b. Return Completion(abruptCompletion). + return abrupt_completion; + } + + // 4. Assert: state is suspendedYield. + // 5. Let genContext be generator.[[GeneratorContext]]. + // 6. Let methodContext be the running execution context. + // 7. Suspend methodContext. + // 8. Set generator.[[GeneratorState]] to executing. + // 9. Push genContext onto the execution context stack; genContext is now the running execution context. + // 10. Resume the suspended evaluation of genContext using abruptCompletion as the result of the operation that suspended it. Let result be the completion record returned by the resumed computation. + // 11. Assert: When we return here, genContext has already been removed from the execution context stack and methodContext is the currently running execution context. + // 12. Return Completion(result). + let generator_context_cell = generator + .context + .take() + .expect("generator context cannot be empty here"); + let mut generator_context = generator_context_cell.borrow_mut(); + + generator.state = GeneratorState::Executing; + drop(generator_obj_mut); + + std::mem::swap( + &mut context.realm.environments, + &mut generator_context.environments, + ); + std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); + context.vm.push_frame(generator_context.call_frame.clone()); + + let result = match abrupt_completion { + Ok(value) => { + context.vm.push(value); + context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Return; + context.run() + } + Err(value) => { + context.vm.push(value); + context.vm.frame_mut().generator_resume_kind = GeneratorResumeKind::Throw; + context.run() + } + }; + generator_context.call_frame = context + .vm + .pop_frame() + .expect("generator call frame must exist"); + std::mem::swap( + &mut context.realm.environments, + &mut generator_context.environments, + ); + std::mem::swap(&mut context.vm.stack, &mut generator_context.stack); + + let mut generator_obj_mut = generator_obj.borrow_mut(); + let generator = generator_obj_mut + .as_generator_mut() + .expect("already checked this object type"); + + match result { + Ok((value, ReturnType::Yield)) => { + generator.state = GeneratorState::SuspendedYield; + drop(generator_context); + generator.context = Some(generator_context_cell); + Ok(create_iter_result_object(value, false, context)) + } + Ok((value, _)) => { + generator.state = GeneratorState::Completed; + Ok(create_iter_result_object(value, true, context)) + } + Err(value) => { + generator.state = GeneratorState::Completed; + Err(value) + } + } + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator_function/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator_function/mod.rs new file mode 100644 index 0000000..637b81e --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/generator_function/mod.rs @@ -0,0 +1,128 @@ +//! This module implements the global `GeneratorFunction` object. +//! +//! The `GeneratorFunction` constructor creates a new generator function object. +//! In JavaScript, every generator function is actually a `GeneratorFunction` object. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-generatorfunction-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/GeneratorFunction + +use crate::{ + builtins::{ + function::{BuiltInFunctionObject, ConstructorKind, Function}, + BuiltIn, + }, + object::ObjectData, + property::PropertyDescriptor, + symbol::WellKnownSymbols, + value::JsValue, + Context, JsResult, +}; +use boa_profiler::Profiler; + +/// The internal representation on a `Generator` object. +#[derive(Debug, Clone, Copy)] +pub struct GeneratorFunction; + +impl BuiltIn for GeneratorFunction { + const NAME: &'static str = "GeneratorFunction"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let prototype = &context + .intrinsics() + .constructors() + .generator_function() + .prototype; + let constructor = &context + .intrinsics() + .constructors() + .generator_function() + .constructor; + + constructor.set_prototype(Some( + context.intrinsics().constructors().function().constructor(), + )); + let property = PropertyDescriptor::builder() + .value(1) + .writable(false) + .enumerable(false) + .configurable(true); + constructor.borrow_mut().insert("length", property); + let property = PropertyDescriptor::builder() + .value("GeneratorFunction") + .writable(false) + .enumerable(false) + .configurable(true); + constructor.borrow_mut().insert("name", property); + let property = PropertyDescriptor::builder() + .value( + context + .intrinsics() + .constructors() + .generator_function() + .prototype(), + ) + .writable(false) + .enumerable(false) + .configurable(false); + constructor.borrow_mut().insert("prototype", property); + constructor.borrow_mut().data = ObjectData::function(Function::Native { + function: Self::constructor, + constructor: Some(ConstructorKind::Base), + }); + + prototype.set_prototype(Some( + context.intrinsics().constructors().function().prototype(), + )); + let property = PropertyDescriptor::builder() + .value( + context + .intrinsics() + .constructors() + .generator_function() + .constructor(), + ) + .writable(false) + .enumerable(false) + .configurable(true); + prototype.borrow_mut().insert("constructor", property); + let property = PropertyDescriptor::builder() + .value(context.intrinsics().constructors().generator().prototype()) + .writable(false) + .enumerable(false) + .configurable(true); + prototype.borrow_mut().insert("prototype", property); + let property = PropertyDescriptor::builder() + .value("GeneratorFunction") + .writable(false) + .enumerable(false) + .configurable(true); + prototype + .borrow_mut() + .insert(WellKnownSymbols::to_string_tag(), property); + + None + } +} + +impl GeneratorFunction { + /// `GeneratorFunction ( p1, p2, … , pn, body )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-generatorfunction + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + BuiltInFunctionObject::create_dynamic_function(new_target, args, false, true, context) + .map(Into::into) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/mod.rs new file mode 100644 index 0000000..ac6f6f2 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/mod.rs @@ -0,0 +1,30 @@ +//! This module implements the global `globalThis` property. +//! +//! The global globalThis property contains the global this value, +//! which is akin to the global object. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-globalthis +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/globalThis + +use crate::{builtins::BuiltIn, Context, JsValue}; +use boa_profiler::Profiler; + +#[cfg(test)] +mod tests; + +/// The JavaScript `globalThis`. +pub(crate) struct GlobalThis; + +impl BuiltIn for GlobalThis { + const NAME: &'static str = "globalThis"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + Some(context.global_object().clone().into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/tests.rs new file mode 100644 index 0000000..719ef51 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/global_this/tests.rs @@ -0,0 +1,10 @@ +use crate::exec; + +#[test] +fn global_this_exists_on_global_object_and_evaluates_to_an_object() { + let scenario = r#" + typeof globalThis; + "#; + + assert_eq!(&exec(scenario), "\"object\""); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/mod.rs new file mode 100644 index 0000000..2800be3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/mod.rs @@ -0,0 +1,34 @@ +//! This module implements the global `Infinity` property. +//! +//! The global property `Infinity` is a numeric value representing infinity. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-value-properties-of-the-global-object-infinity +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Infinity + +#[cfg(test)] +mod tests; + +use crate::{builtins::BuiltIn, property::Attribute, Context, JsValue}; +use boa_profiler::Profiler; + +/// JavaScript global `Infinity` property. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Infinity; + +impl BuiltIn for Infinity { + const NAME: &'static str = "Infinity"; + + const ATTRIBUTE: Attribute = Attribute::READONLY + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::PERMANENT); + + fn init(_: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + Some(f64::INFINITY.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/tests.rs new file mode 100644 index 0000000..f63cbde --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/infinity/tests.rs @@ -0,0 +1,19 @@ +use crate::exec; + +#[test] +fn infinity_exists_on_global_object_and_evaluates_to_infinity_value() { + let scenario = r#" + Infinity; + "#; + + assert_eq!(&exec(scenario), "Infinity"); +} + +#[test] +fn infinity_exists_and_equals_to_number_positive_infinity_value() { + let scenario = r#" + Number.POSITIVE_INFINITY === Infinity; + "#; + + assert_eq!(&exec(scenario), "true"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/date_time_format.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/date_time_format.rs new file mode 100644 index 0000000..333c224 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/date_time_format.rs @@ -0,0 +1,236 @@ +//! This module implements the global `Intl.DateTimeFormat` object. +//! +//! `Intl.DateTimeFormat` is a built-in object that has properties and methods for date and time i18n. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma402/#datetimeformat-objects + +use crate::{ + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsFunction, JsObject, + ObjectData, + }, + Context, JsResult, JsString, JsValue, +}; + +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +/// JavaScript `Intl.DateTimeFormat` object. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct DateTimeFormat { + initialized_date_time_format: bool, + locale: JsString, + calendar: JsString, + numbering_system: JsString, + time_zone: JsString, + weekday: JsString, + era: JsString, + year: JsString, + month: JsString, + day: JsString, + day_period: JsString, + hour: JsString, + minute: JsString, + second: JsString, + fractional_second_digits: JsString, + time_zone_name: JsString, + hour_cycle: JsString, + pattern: JsString, + bound_format: JsString, +} + +impl DateTimeFormat { + const NAME: &'static str = "DateTimeFormat"; + + pub(super) fn init(context: &mut Context) -> JsFunction { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + ConstructorBuilder::new(context, Self::constructor) + .name(Self::NAME) + .length(0) + .build() + } +} + +impl DateTimeFormat { + /// The `Intl.DateTimeFormat` constructor is the `%DateTimeFormat%` intrinsic object and a standard built-in property of the `Intl` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma402/#datetimeformat-objects + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat + pub(crate) fn constructor( + new_target: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, let newTarget be the active function object, else let newTarget be NewTarget. + let prototype = get_prototype_from_constructor( + new_target, + StandardConstructors::date_time_format, + context, + )?; + // 2. Let dateTimeFormat be ? OrdinaryCreateFromConstructor(newTarget, "%DateTimeFormat.prototype%", + // « [[InitializedDateTimeFormat]], [[Locale]], [[Calendar]], [[NumberingSystem]], [[TimeZone]], [[Weekday]], + // [[Era]], [[Year]], [[Month]], [[Day]], [[DayPeriod]], [[Hour]], [[Minute]], [[Second]], + // [[FractionalSecondDigits]], [[TimeZoneName]], [[HourCycle]], [[Pattern]], [[BoundFormat]] »). + let date_time_format = JsObject::from_proto_and_data( + prototype, + ObjectData::date_time_format(Box::new(Self { + initialized_date_time_format: true, + locale: JsString::from("en-US"), + calendar: JsString::from("gregory"), + numbering_system: JsString::from("arab"), + time_zone: JsString::from("UTC"), + weekday: JsString::from("narrow"), + era: JsString::from("narrow"), + year: JsString::from("numeric"), + month: JsString::from("narrow"), + day: JsString::from("numeric"), + day_period: JsString::from("narrow"), + hour: JsString::from("numeric"), + minute: JsString::from("numeric"), + second: JsString::from("numeric"), + fractional_second_digits: JsString::from(""), + time_zone_name: JsString::from(""), + hour_cycle: JsString::from("h24"), + pattern: JsString::from("{hour}:{minute}"), + bound_format: JsString::from("undefined"), + })), + ); + + // TODO 3. Perform ? InitializeDateTimeFormat(dateTimeFormat, locales, options). + // TODO 4. If the implementation supports the normative optional constructor mode of 4.3 Note 1, then + // TODO a. Let this be the this value. + // TODO b. Return ? ChainDateTimeFormat(dateTimeFormat, NewTarget, this). + + // 5. Return dateTimeFormat. + Ok(date_time_format.into()) + } +} + +/// Represents the `required` and `defaults` arguments in the abstract operation +/// `toDateTimeOptions`. +/// +/// Since `required` and `defaults` differ only in the `any` and `all` variants, +/// we combine both in a single variant `AnyAll`. +#[allow(unused)] +#[derive(Debug, PartialEq)] +pub(crate) enum DateTimeReqs { + Date, + Time, + AnyAll, +} + +/// The abstract operation `toDateTimeOptions` is called with arguments `options`, `required` and +/// `defaults`. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-todatetimeoptions +#[allow(unused)] +pub(crate) fn to_date_time_options( + options: &JsValue, + required: &DateTimeReqs, + defaults: &DateTimeReqs, + context: &mut Context, +) -> JsResult { + // 1. If options is undefined, let options be null; + // otherwise let options be ? ToObject(options). + // 2. Let options be ! OrdinaryObjectCreate(options). + let options = if options.is_undefined() { + None + } else { + Some(options.to_object(context)?) + }; + let options = JsObject::from_proto_and_data(options, ObjectData::ordinary()); + + // 3. Let needDefaults be true. + let mut need_defaults = true; + + // 4. If required is "date" or "any", then + if [DateTimeReqs::Date, DateTimeReqs::AnyAll].contains(required) { + // a. For each property name prop of « "weekday", "year", "month", "day" », do + for property in ["weekday", "year", "month", "day"] { + // i. Let value be ? Get(options, prop). + let value = options.get(property, context)?; + + // ii. If value is not undefined, let needDefaults be false. + if !value.is_undefined() { + need_defaults = false; + } + } + } + + // 5. If required is "time" or "any", then + if [DateTimeReqs::Time, DateTimeReqs::AnyAll].contains(required) { + // a. For each property name prop of « "dayPeriod", "hour", "minute", "second", + // "fractionalSecondDigits" », do + for property in [ + "dayPeriod", + "hour", + "minute", + "second", + "fractionalSecondDigits", + ] { + // i. Let value be ? Get(options, prop). + let value = options.get(property, context)?; + + // ii. If value is not undefined, let needDefaults be false. + if !value.is_undefined() { + need_defaults = false; + } + } + } + + // 6. Let dateStyle be ? Get(options, "dateStyle"). + let date_style = options.get("dateStyle", context)?; + + // 7. Let timeStyle be ? Get(options, "timeStyle"). + let time_style = options.get("timeStyle", context)?; + + // 8. If dateStyle is not undefined or timeStyle is not undefined, let needDefaults be false. + if !date_style.is_undefined() || !time_style.is_undefined() { + need_defaults = false; + } + + // 9. If required is "date" and timeStyle is not undefined, then + if required == &DateTimeReqs::Date && !time_style.is_undefined() { + // a. Throw a TypeError exception. + return context.throw_type_error("'date' is required, but timeStyle was defined"); + } + + // 10. If required is "time" and dateStyle is not undefined, then + if required == &DateTimeReqs::Time && !date_style.is_undefined() { + // a. Throw a TypeError exception. + return context.throw_type_error("'time' is required, but dateStyle was defined"); + } + + // 11. If needDefaults is true and defaults is either "date" or "all", then + if need_defaults && [DateTimeReqs::Date, DateTimeReqs::AnyAll].contains(defaults) { + // a. For each property name prop of « "year", "month", "day" », do + for property in ["year", "month", "day"] { + // i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric"). + options.create_data_property_or_throw(property, "numeric", context)?; + } + } + + // 12. If needDefaults is true and defaults is either "time" or "all", then + if need_defaults && [DateTimeReqs::Time, DateTimeReqs::AnyAll].contains(defaults) { + // a. For each property name prop of « "hour", "minute", "second" », do + for property in ["hour", "minute", "second"] { + // i. Perform ? CreateDataPropertyOrThrow(options, prop, "numeric"). + options.create_data_property_or_throw(property, "numeric", context)?; + } + } + + // 13. Return options. + Ok(options) +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/mod.rs new file mode 100644 index 0000000..889d31e --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/mod.rs @@ -0,0 +1,836 @@ +//! This module implements the global `Intl` object. +//! +//! `Intl` is a built-in object that has properties and methods for i18n. It's not a function object. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma402/#intl-object + +use crate::{ + builtins::intl::date_time_format::DateTimeFormat, + builtins::{Array, BuiltIn, JsArgs}, + object::{JsObject, ObjectInitializer}, + property::Attribute, + symbol::WellKnownSymbols, + Context, JsResult, JsString, JsValue, +}; + +pub mod date_time_format; +#[cfg(test)] +mod tests; + +use boa_profiler::Profiler; +use icu_locale_canonicalizer::LocaleCanonicalizer; +use icu_locid::{locale, Locale}; +use indexmap::IndexSet; +use rustc_hash::FxHashMap; +use tap::{Conv, Pipe, TapOptional}; + +/// JavaScript `Intl` object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Intl; + +impl BuiltIn for Intl { + const NAME: &'static str = "Intl"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let string_tag = WellKnownSymbols::to_string_tag(); + let date_time_format = DateTimeFormat::init(context); + ObjectInitializer::new(context) + .function(Self::get_canonical_locales, "getCanonicalLocales", 1) + .property( + string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + "DateTimeFormat", + date_time_format, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl Intl { + /// `Intl.getCanonicalLocales ( locales )` + /// + /// Returns an array containing the canonical locale names. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN docs][mdn] + /// + /// [spec]: https://tc39.es/ecma402/#sec-intl.getcanonicallocales + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/getCanonicalLocales + pub(crate) fn get_canonical_locales( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let ll be ? CanonicalizeLocaleList(locales). + let ll = canonicalize_locale_list(args, context)?; + + // 2. Return CreateArrayFromList(ll). + Ok(JsValue::Object(Array::create_array_from_list( + ll.into_iter().map(|loc| loc.to_string().into()), + context, + ))) + } +} + +/// `MatcherRecord` type aggregates unicode `locale` string and unicode locale `extension`. +/// +/// This is a return value for `lookup_matcher` and `best_fit_matcher` subroutines. +#[derive(Debug)] +struct MatcherRecord { + locale: JsString, + extension: JsString, +} + +/// Abstract operation `DefaultLocale ( )` +/// +/// Returns a String value representing the structurally valid and canonicalized +/// Unicode BCP 47 locale identifier for the host environment's current locale. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-defaultlocale +fn default_locale(canonicalizer: &LocaleCanonicalizer) -> Locale { + sys_locale::get_locale() + .and_then(|loc| loc.parse::().ok()) + .tap_some_mut(|loc| canonicalize_unicode_locale_id(loc, canonicalizer)) + .unwrap_or(locale!("en-US")) +} + +/// Abstract operation `BestAvailableLocale ( availableLocales, locale )` +/// +/// Compares the provided argument `locale`, which must be a String value with a +/// structurally valid and canonicalized Unicode BCP 47 locale identifier, against +/// the locales in `availableLocales` and returns either the longest non-empty prefix +/// of `locale` that is an element of `availableLocales`, or undefined if there is no +/// such element. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-bestavailablelocale +fn best_available_locale(available_locales: &[JsString], locale: &JsString) -> Option { + // 1. Let candidate be locale. + let mut candidate = locale.clone(); + // 2. Repeat + loop { + // a. If availableLocales contains an element equal to candidate, return candidate. + if available_locales.contains(&candidate) { + return Some(candidate); + } + + // b. Let pos be the character index of the last occurrence of "-" (U+002D) within candidate. If that character does not occur, return undefined. + let pos = candidate.rfind('-'); + match pos { + Some(ind) => { + // c. If pos ≥ 2 and the character "-" occurs at index pos-2 of candidate, decrease pos by 2. + let tmp_candidate = candidate[..ind].to_string(); + let prev_dash = tmp_candidate.rfind('-').unwrap_or(ind); + let trim_ind = if ind >= 2 && prev_dash == ind - 2 { + ind - 2 + } else { + ind + }; + // d. Let candidate be the substring of candidate from position 0, inclusive, to position pos, exclusive. + candidate = JsString::new(&candidate[..trim_ind]); + } + None => return None, + } + } +} + +/// Abstract operation `LookupMatcher ( availableLocales, requestedLocales )` +/// +/// Compares `requestedLocales`, which must be a `List` as returned by `CanonicalizeLocaleList`, +/// against the locales in `availableLocales` and determines the best available language to +/// meet the request. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-lookupmatcher +fn lookup_matcher( + available_locales: &[JsString], + requested_locales: &[JsString], + canonicalizer: &LocaleCanonicalizer, +) -> MatcherRecord { + // 1. Let result be a new Record. + // 2. For each element locale of requestedLocales, do + for locale_str in requested_locales { + // a. Let noExtensionsLocale be the String value that is locale with any Unicode locale + // extension sequences removed. + let parsed_locale = + Locale::from_bytes(locale_str.as_bytes()).expect("Locale parsing failed"); + let no_extensions_locale = JsString::new(parsed_locale.id.to_string()); + + // b. Let availableLocale be ! BestAvailableLocale(availableLocales, noExtensionsLocale). + let available_locale = best_available_locale(available_locales, &no_extensions_locale); + + // c. If availableLocale is not undefined, then + if let Some(available_locale) = available_locale { + // i. Set result.[[locale]] to availableLocale. + // Assignment deferred. See return statement below. + // ii. If locale and noExtensionsLocale are not the same String value, then + let maybe_ext = if locale_str.eq(&no_extensions_locale) { + JsString::empty() + } else { + // 1. Let extension be the String value consisting of the substring of the Unicode + // locale extension sequence within locale. + // 2. Set result.[[extension]] to extension. + JsString::new(parsed_locale.extensions.to_string()) + }; + + // iii. Return result. + return MatcherRecord { + locale: available_locale, + extension: maybe_ext, + }; + } + } + + // 3. Let defLocale be ! DefaultLocale(). + // 4. Set result.[[locale]] to defLocale. + // 5. Return result. + MatcherRecord { + locale: default_locale(canonicalizer).to_string().into(), + extension: JsString::empty(), + } +} + +/// Abstract operation `BestFitMatcher ( availableLocales, requestedLocales )` +/// +/// Compares `requestedLocales`, which must be a `List` as returned by `CanonicalizeLocaleList`, +/// against the locales in `availableLocales` and determines the best available language to +/// meet the request. The algorithm is implementation dependent, but should produce results +/// that a typical user of the requested locales would perceive as at least as good as those +/// produced by the `LookupMatcher` abstract operation. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-bestfitmatcher +fn best_fit_matcher( + available_locales: &[JsString], + requested_locales: &[JsString], + canonicalizer: &LocaleCanonicalizer, +) -> MatcherRecord { + lookup_matcher(available_locales, requested_locales, canonicalizer) +} + +/// `Keyword` structure is a pair of keyword key and keyword value. +#[derive(Debug)] +struct Keyword { + key: JsString, + value: JsString, +} + +/// `UniExtRecord` structure represents unicode extension records. +/// +/// It contains the list of unicode `extension` attributes and the list of `keywords`. +/// +/// For example: +/// +/// - `-u-nu-thai` has no attributes and the list of keywords contains `(nu:thai)` pair. +#[allow(dead_code)] +#[derive(Debug)] +struct UniExtRecord { + attributes: Vec, // never read at this point + keywords: Vec, +} + +/// Abstract operation `UnicodeExtensionComponents ( extension )` +/// +/// Returns the attributes and keywords from `extension`, which must be a String +/// value whose contents are a `Unicode locale extension` sequence. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-unicode-extension-components +fn unicode_extension_components(extension: &JsString) -> UniExtRecord { + // 1. Let attributes be a new empty List. + let mut attributes = Vec::::new(); + + // 2. Let keywords be a new empty List. + let mut keywords = Vec::::new(); + + // 3. Let keyword be undefined. + let mut keyword: Option = None; + + // 4. Let size be the length of extension. + let size = extension.len(); + + // 5. Let k be 3. + let mut k = 3; + + // 6. Repeat, while k < size, + while k < size { + // a. Let e be ! StringIndexOf(extension, "-", k). + let e = extension.index_of(&JsString::new("-"), k); + + // b. If e = -1, let len be size - k; else let len be e - k. + let len = match e { + Some(pos) => pos - k, + None => size - k, + }; + + // c. Let subtag be the String value equal to the substring of extension consisting of the + // code units at indices k (inclusive) through k + len (exclusive). + let subtag = JsString::new(&extension[k..k + len]); + + // d. If keyword is undefined and len ≠ 2, then + if keyword.is_none() && len != 2 { + // i. If subtag is not an element of attributes, then + if !attributes.contains(&subtag) { + // 1. Append subtag to attributes. + attributes.push(subtag); + } + // e. Else if len = 2, then + } else if len == 2 { + // i. If keyword is not undefined and keywords does not contain an element + // whose [[Key]] is the same as keyword.[[Key]], then + // 1. Append keyword to keywords. + if let Some(keyword_val) = keyword { + let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key); + if !has_key { + keywords.push(keyword_val); + } + }; + + // ii. Set keyword to the Record { [[Key]]: subtag, [[Value]]: "" }. + keyword = Some(Keyword { + key: subtag, + value: JsString::empty(), + }); + // f. Else, + } else { + // i. If keyword.[[Value]] is the empty String, then + // 1. Set keyword.[[Value]] to subtag. + // ii. Else, + // 1. Set keyword.[[Value]] to the string-concatenation of keyword.[[Value]], "-", and subtag. + if let Some(keyword_val) = keyword { + let new_keyword_val = if keyword_val.value.is_empty() { + subtag + } else { + JsString::new(format!("{}-{subtag}", keyword_val.value)) + }; + + keyword = Some(Keyword { + key: keyword_val.key, + value: new_keyword_val, + }); + }; + } + + // g. Let k be k + len + 1. + k = k + len + 1; + } + + // 7. If keyword is not undefined and keywords does not contain an element whose [[Key]] is + // the same as keyword.[[Key]], then + // a. Append keyword to keywords. + if let Some(keyword_val) = keyword { + let has_key = keywords.iter().any(|elem| elem.key == keyword_val.key); + if !has_key { + keywords.push(keyword_val); + } + }; + + // 8. Return the Record { [[Attributes]]: attributes, [[Keywords]]: keywords }. + UniExtRecord { + attributes, + keywords, + } +} + +/// Abstract operation `InsertUnicodeExtensionAndCanonicalize ( locale, extension )` +/// +/// Inserts `extension`, which must be a Unicode locale extension sequence, into +/// `locale`, which must be a String value with a structurally valid and canonicalized +/// Unicode BCP 47 locale identifier. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-insert-unicode-extension-and-canonicalize +fn insert_unicode_extension_and_canonicalize( + locale: &str, + extension: &str, + canonicalizer: &LocaleCanonicalizer, +) -> JsString { + // TODO 1. Assert: locale does not contain a substring that is a Unicode locale extension sequence. + // TODO 2. Assert: extension is a Unicode locale extension sequence. + // TODO 3. Assert: tag matches the unicode_locale_id production. + // 4. Let privateIndex be ! StringIndexOf(locale, "-x-", 0). + let private_index = locale.find("-x-"); + let new_locale = match private_index { + // 5. If privateIndex = -1, then + None => { + // a. Let locale be the string-concatenation of locale and extension. + locale.to_owned() + extension + } + // 6. Else, + Some(idx) => { + // a. Let preExtension be the substring of locale from position 0, inclusive, + // to position privateIndex, exclusive. + let pre_extension = &locale[0..idx]; + + // b. Let postExtension be the substring of locale from position privateIndex to + // the end of the string. + let post_extension = &locale[idx..]; + + // c. Let locale be the string-concatenation of preExtension, extension, + // and postExtension. + pre_extension.to_owned() + extension + post_extension + } + }; + + // 7. Assert: ! IsStructurallyValidLanguageTag(locale) is true. + let mut new_locale = new_locale + .parse() + .expect("Assert: ! IsStructurallyValidLanguageTag(locale) is true."); + + // 8. Return ! CanonicalizeUnicodeLocaleId(locale). + canonicalize_unicode_locale_id(&mut new_locale, canonicalizer); + new_locale.to_string().into() +} + +/// Abstract operation `CanonicalizeLocaleList ( locales )` +/// +/// Converts an array of [`JsValue`]s containing structurally valid +/// [Unicode BCP 47 locale identifiers][bcp-47] into their [canonical form][canon]. +/// +/// For efficiency, this returns a [`Vec`] of [`Locale`]s instead of a [`Vec`] of +/// [`String`]s, since [`Locale`] allows us to modify individual parts of the locale +/// without scanning the whole string again. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-canonicalizelocalelist +/// [bcp-47]: https://unicode.org/reports/tr35/#Unicode_locale_identifier +/// [canon]: https://unicode.org/reports/tr35/#LocaleId_Canonicalization +fn canonicalize_locale_list(args: &[JsValue], context: &mut Context) -> JsResult> { + // 1. If locales is undefined, then + let locales = args.get_or_undefined(0); + if locales.is_undefined() { + // a. Return a new empty List. + return Ok(Vec::new()); + } + + // 2. Let seen be a new empty List. + let mut seen = IndexSet::new(); + + // 3. If Type(locales) is String or Type(locales) is Object and locales has an [[InitializedLocale]] internal slot, then + // TODO: check if Type(locales) is object and handle the internal slots + let o = if locales.is_string() { + // a. Let O be CreateArrayFromList(« locales »). + Array::create_array_from_list([locales.clone()], context) + } else { + // 4. Else, + // a. Let O be ? ToObject(locales). + locales.to_object(context)? + }; + + // 5. Let len be ? ToLength(? Get(O, "length")). + let len = o.length_of_array_like(context)?; + + // 6 Let k be 0. + // 7. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ToString(k). + // b. Let kPresent be ? HasProperty(O, Pk). + let k_present = o.has_property(k, context)?; + // c. If kPresent is true, then + if k_present { + // i. Let kValue be ? Get(O, Pk). + let k_value = o.get(k, context)?; + // ii. If Type(kValue) is not String or Object, throw a TypeError exception. + if !(k_value.is_object() || k_value.is_string()) { + return context.throw_type_error("locale should be a String or Object"); + } + // iii. If Type(kValue) is Object and kValue has an [[InitializedLocale]] internal slot, then + // TODO: handle checks for InitializedLocale internal slot (there should be an if statement here) + // 1. Let tag be kValue.[[Locale]]. + // iv. Else, + // 1. Let tag be ? ToString(kValue). + let tag = k_value.to_string(context)?; + // v. If IsStructurallyValidLanguageTag(tag) is false, throw a RangeError exception. + let mut tag = tag.parse().map_err(|_| { + context.construct_range_error("locale is not a structurally valid language tag") + })?; + + // vi. Let canonicalizedTag be CanonicalizeUnicodeLocaleId(tag). + canonicalize_unicode_locale_id(&mut tag, &*context.icu().locale_canonicalizer()); + seen.insert(tag); + // vii. If canonicalizedTag is not an element of seen, append canonicalizedTag as the last element of seen. + } + // d. Increase k by 1. + } + + // 8. Return seen. + Ok(seen.into_iter().collect()) +} + +/// `LocaleDataRecord` is the type of `locale_data` argument in `resolve_locale` subroutine. +/// +/// It is an alias for a map where key is a string and value is another map. +/// +/// Value of that inner map is a vector of strings representing locale parameters. +type LocaleDataRecord = FxHashMap>>; + +/// `DateTimeFormatRecord` type aggregates `locale_matcher` selector and `properties` map. +/// +/// It is used as a type of `options` parameter in `resolve_locale` subroutine. +#[derive(Debug)] +struct DateTimeFormatRecord { + pub(crate) locale_matcher: JsString, + pub(crate) properties: FxHashMap, +} + +/// `ResolveLocaleRecord` type consists of unicode `locale` string, `data_locale` string and `properties` map. +/// +/// This is a return value for `resolve_locale` subroutine. +#[derive(Debug)] +struct ResolveLocaleRecord { + pub(crate) locale: JsString, + pub(crate) properties: FxHashMap, + pub(crate) data_locale: JsString, +} + +/// Abstract operation `ResolveLocale ( availableLocales, requestedLocales, options, relevantExtensionKeys, localeData )` +/// +/// Compares a BCP 47 language priority list `requestedLocales` against the locales +/// in `availableLocales` and determines the best available language to meet the request. +/// `availableLocales`, `requestedLocales`, and `relevantExtensionKeys` must be provided as +/// `List` values, options and `localeData` as Records. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-resolvelocale +#[allow(dead_code)] +fn resolve_locale( + available_locales: &[JsString], + requested_locales: &[JsString], + options: &DateTimeFormatRecord, + relevant_extension_keys: &[JsString], + locale_data: &LocaleDataRecord, + context: &mut Context, +) -> ResolveLocaleRecord { + // 1. Let matcher be options.[[localeMatcher]]. + let matcher = &options.locale_matcher; + // 2. If matcher is "lookup", then + // a. Let r be ! LookupMatcher(availableLocales, requestedLocales). + // 3. Else, + // a. Let r be ! BestFitMatcher(availableLocales, requestedLocales). + let r = if matcher.eq(&JsString::new("lookup")) { + lookup_matcher( + available_locales, + requested_locales, + context.icu().locale_canonicalizer(), + ) + } else { + best_fit_matcher( + available_locales, + requested_locales, + context.icu().locale_canonicalizer(), + ) + }; + + // 4. Let foundLocale be r.[[locale]]. + let mut found_locale = r.locale; + + // 5. Let result be a new Record. + let mut result = ResolveLocaleRecord { + locale: JsString::empty(), + properties: FxHashMap::default(), + data_locale: JsString::empty(), + }; + + // 6. Set result.[[dataLocale]] to foundLocale. + result.data_locale = found_locale.clone(); + + // 7. If r has an [[extension]] field, then + let keywords = if r.extension.is_empty() { + Vec::::new() + } else { + // a. Let components be ! UnicodeExtensionComponents(r.[[extension]]). + let components = unicode_extension_components(&r.extension); + // b. Let keywords be components.[[Keywords]]. + components.keywords + }; + + // 8. Let supportedExtension be "-u". + let mut supported_extension = JsString::new("-u"); + + // 9. For each element key of relevantExtensionKeys, do + for key in relevant_extension_keys { + // a. Let foundLocaleData be localeData.[[]]. + // TODO b. Assert: Type(foundLocaleData) is Record. + let found_locale_data = match locale_data.get(&found_locale) { + Some(locale_value) => locale_value.clone(), + None => FxHashMap::default(), + }; + + // c. Let keyLocaleData be foundLocaleData.[[]]. + // TODO d. Assert: Type(keyLocaleData) is List. + let key_locale_data = match found_locale_data.get(key) { + Some(locale_vec) => locale_vec.clone(), + None => Vec::new(), + }; + + // e. Let value be keyLocaleData[0]. + // TODO f. Assert: Type(value) is either String or Null. + let mut value = match key_locale_data.get(0) { + Some(first_elt) => JsValue::String(first_elt.clone()), + None => JsValue::null(), + }; + + // g. Let supportedExtensionAddition be "". + let mut supported_extension_addition = JsString::empty(); + + // h. If r has an [[extension]] field, then + if !r.extension.is_empty() { + // i. If keywords contains an element whose [[Key]] is the same as key, then + // 1. Let entry be the element of keywords whose [[Key]] is the same as key. + let maybe_entry = keywords.iter().find(|elem| key.eq(&elem.key)); + if let Some(entry) = maybe_entry { + // 2. Let requestedValue be entry.[[Value]]. + let requested_value = &entry.value; + + // 3. If requestedValue is not the empty String, then + if !requested_value.is_empty() { + // a. If keyLocaleData contains requestedValue, then + if key_locale_data.contains(requested_value) { + // i. Let value be requestedValue. + value = JsValue::String(JsString::new(requested_value)); + // ii. Let supportedExtensionAddition be the string-concatenation + // of "-", key, "-", and value. + supported_extension_addition = + JsString::concat_array(&["-", key, "-", requested_value]); + } + // 4. Else if keyLocaleData contains "true", then + } else if key_locale_data.contains(&JsString::new("true")) { + // a. Let value be "true". + value = JsValue::String(JsString::new("true")); + // b. Let supportedExtensionAddition be the string-concatenation of "-" and key. + supported_extension_addition = JsString::concat_array(&["-", key]); + } + } + } + + // i. If options has a field [[]], then + if options.properties.contains_key(key) { + // i. Let optionsValue be options.[[]]. + // TODO ii. Assert: Type(optionsValue) is either String, Undefined, or Null. + let mut options_value = options + .properties + .get(key) + .unwrap_or(&JsValue::undefined()) + .clone(); + + // iii. If Type(optionsValue) is String, then + if options_value.is_string() { + // TODO 1. Let optionsValue be the string optionsValue after performing the + // algorithm steps to transform Unicode extension values to canonical syntax + // per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode Locale + // Identifiers, treating key as ukey and optionsValue as uvalue productions. + + // TODO 2. Let optionsValue be the string optionsValue after performing the + // algorithm steps to replace Unicode extension values with their canonical + // form per Unicode Technical Standard #35 LDML § 3.2.1 Canonical Unicode + // Locale Identifiers, treating key as ukey and optionsValue as uvalue + // productions. + + // 3. If optionsValue is the empty String, then + if let Some(options_val_str) = options_value.as_string() { + if options_val_str.is_empty() { + // a. Let optionsValue be "true". + options_value = JsValue::String(JsString::new("true")); + } + } + } + + // iv. If keyLocaleData contains optionsValue, then + let options_val_str = options_value + .to_string(context) + .unwrap_or_else(|_| JsString::empty()); + if key_locale_data.contains(&options_val_str) { + // 1. If SameValue(optionsValue, value) is false, then + if !options_value.eq(&value) { + // a. Let value be optionsValue. + value = options_value; + + // b. Let supportedExtensionAddition be "". + supported_extension_addition = JsString::empty(); + } + } + } + + // j. Set result.[[]] to value. + result.properties.insert(key.clone(), value); + + // k. Append supportedExtensionAddition to supportedExtension. + supported_extension = JsString::concat(supported_extension, &supported_extension_addition); + } + + // 10. If the number of elements in supportedExtension is greater than 2, then + if supported_extension.len() > 2 { + // a. Let foundLocale be InsertUnicodeExtensionAndCanonicalize(foundLocale, supportedExtension). + found_locale = insert_unicode_extension_and_canonicalize( + &found_locale, + &supported_extension, + context.icu().locale_canonicalizer(), + ); + } + + // 11. Set result.[[locale]] to foundLocale. + result.locale = found_locale; + + // 12. Return result. + result +} + +#[allow(unused)] +pub(crate) enum GetOptionType { + String, + Boolean, +} + +/// Abstract operation `GetOption ( options, property, type, values, fallback )` +/// +/// Extracts the value of the property named `property` from the provided `options` object, +/// converts it to the required `type`, checks whether it is one of a `List` of allowed +/// `values`, and fills in a `fallback` value if necessary. If `values` is +/// undefined, there is no fixed set of values and any is permitted. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-getoption +#[allow(unused)] +pub(crate) fn get_option( + options: &JsObject, + property: &str, + r#type: &GetOptionType, + values: &[JsString], + fallback: &JsValue, + context: &mut Context, +) -> JsResult { + // 1. Assert: Type(options) is Object. + // 2. Let value be ? Get(options, property). + let mut value = options.get(property, context)?; + + // 3. If value is undefined, return fallback. + if value.is_undefined() { + return Ok(fallback.clone()); + } + + // 4. Assert: type is "boolean" or "string". + // 5. If type is "boolean", then + // a. Set value to ! ToBoolean(value). + // 6. If type is "string", then + // a. Set value to ? ToString(value). + // 7. If values is not undefined and values does not contain an element equal to value, + // throw a RangeError exception. + value = match r#type { + GetOptionType::Boolean => JsValue::Boolean(value.to_boolean()), + GetOptionType::String => { + let string_value = value.to_string(context)?; + if !values.is_empty() && !values.contains(&string_value) { + return context.throw_range_error("GetOption: values array does not contain value"); + } + JsValue::String(string_value) + } + }; + + // 8. Return value. + Ok(value) +} + +/// Abstract operation `GetNumberOption ( options, property, minimum, maximum, fallback )` +/// +/// Extracts the value of the property named `property` from the provided `options` +/// object, converts it to a `Number value`, checks whether it is in the allowed range, +/// and fills in a `fallback` value if necessary. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-getnumberoption +#[allow(unused)] +pub(crate) fn get_number_option( + options: &JsObject, + property: &str, + minimum: f64, + maximum: f64, + fallback: Option, + context: &mut Context, +) -> JsResult> { + // 1. Assert: Type(options) is Object. + // 2. Let value be ? Get(options, property). + let value = options.get(property, context)?; + + // 3. Return ? DefaultNumberOption(value, minimum, maximum, fallback). + default_number_option(&value, minimum, maximum, fallback, context) +} + +/// Abstract operation `DefaultNumberOption ( value, minimum, maximum, fallback )` +/// +/// Converts `value` to a `Number value`, checks whether it is in the allowed range, +/// and fills in a `fallback` value if necessary. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-defaultnumberoption +#[allow(unused)] +pub(crate) fn default_number_option( + value: &JsValue, + minimum: f64, + maximum: f64, + fallback: Option, + context: &mut Context, +) -> JsResult> { + // 1. If value is undefined, return fallback. + if value.is_undefined() { + return Ok(fallback); + } + + // 2. Set value to ? ToNumber(value). + let value = value.to_number(context)?; + + // 3. If value is NaN or less than minimum or greater than maximum, throw a RangeError exception. + if value.is_nan() || value < minimum || value > maximum { + return context.throw_range_error("DefaultNumberOption: value is out of range."); + } + + // 4. Return floor(value). + Ok(Some(value.floor())) +} + +/// Abstract operation `CanonicalizeUnicodeLocaleId ( locale )`. +/// +/// This function differs slightly from the specification by modifying in-place +/// the provided [`Locale`] instead of creating a new canonicalized copy. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma402/#sec-canonicalizeunicodelocaleid +fn canonicalize_unicode_locale_id(locale: &mut Locale, canonicalizer: &LocaleCanonicalizer) { + canonicalizer.canonicalize(locale); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/tests.rs new file mode 100644 index 0000000..6abfb0c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/intl/tests.rs @@ -0,0 +1,545 @@ +use crate::{ + builtins::intl::date_time_format::{to_date_time_options, DateTimeReqs}, + builtins::intl::{ + best_available_locale, best_fit_matcher, default_locale, default_number_option, + get_number_option, get_option, insert_unicode_extension_and_canonicalize, lookup_matcher, + resolve_locale, unicode_extension_components, DateTimeFormatRecord, GetOptionType, + }, + object::JsObject, + Context, JsString, JsValue, +}; + +use icu_locale_canonicalizer::LocaleCanonicalizer; +use rustc_hash::FxHashMap; + +#[test] +fn best_avail_loc() { + let no_extensions_locale = JsString::new("en-US"); + let available_locales = Vec::::new(); + assert_eq!( + best_available_locale(&available_locales, &no_extensions_locale,), + None + ); + + let no_extensions_locale = JsString::new("de-DE"); + let available_locales = vec![no_extensions_locale.clone()]; + assert_eq!( + best_available_locale(&available_locales, &no_extensions_locale,), + Some(no_extensions_locale) + ); + + let locale_part = "fr".to_string(); + let no_extensions_locale = JsString::new(locale_part.clone() + "-CA"); + let available_locales = vec![JsString::new(locale_part.clone())]; + assert_eq!( + best_available_locale(&available_locales, &no_extensions_locale,), + Some(JsString::new(locale_part)) + ); + + let ja_kana_t = JsString::new("ja-Kana-JP-t"); + let ja_kana = JsString::new("ja-Kana-JP"); + let no_extensions_locale = JsString::new("ja-Kana-JP-t-it-latn-it"); + let available_locales = vec![ja_kana_t, ja_kana.clone()]; + assert_eq!( + best_available_locale(&available_locales, &no_extensions_locale,), + Some(ja_kana) + ); +} + +#[test] +fn lookup_match() { + let provider = icu_testdata::get_provider(); + let canonicalizer = + LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer"); + // available: [], requested: [] + let available_locales = Vec::::new(); + let requested_locales = Vec::::new(); + + let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); + assert_eq!( + matcher.locale, + default_locale(&canonicalizer).to_string().as_str() + ); + assert_eq!(matcher.extension, ""); + + // available: [de-DE], requested: [] + let available_locales = vec![JsString::new("de-DE")]; + let requested_locales = Vec::::new(); + + let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); + assert_eq!( + matcher.locale, + default_locale(&canonicalizer).to_string().as_str() + ); + assert_eq!(matcher.extension, ""); + + // available: [fr-FR], requested: [fr-FR-u-hc-h12] + let available_locales = vec![JsString::new("fr-FR")]; + let requested_locales = vec![JsString::new("fr-FR-u-hc-h12")]; + + let matcher = lookup_matcher(&available_locales, &requested_locales, &canonicalizer); + assert_eq!(matcher.locale, "fr-FR"); + assert_eq!(matcher.extension, "u-hc-h12"); + + // available: [es-ES], requested: [es-ES] + let available_locales = vec![JsString::new("es-ES")]; + let requested_locales = vec![JsString::new("es-ES")]; + + let matcher = best_fit_matcher(&available_locales, &requested_locales, &canonicalizer); + assert_eq!(matcher.locale, "es-ES"); + assert_eq!(matcher.extension, ""); +} + +#[test] +fn insert_unicode_ext() { + let provider = icu_testdata::get_provider(); + let canonicalizer = + LocaleCanonicalizer::new(&provider).expect("Could not create canonicalizer"); + let locale = JsString::new("hu-HU"); + let ext = JsString::empty(); + assert_eq!( + insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer), + locale + ); + + let locale = JsString::new("hu-HU"); + let ext = JsString::new("-u-hc-h12"); + assert_eq!( + insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer), + JsString::new("hu-HU-u-hc-h12") + ); + + let locale = JsString::new("hu-HU-x-PRIVATE"); + let ext = JsString::new("-u-hc-h12"); + assert_eq!( + insert_unicode_extension_and_canonicalize(&locale, &ext, &canonicalizer), + JsString::new("hu-HU-u-hc-h12-x-private") + ); +} + +#[test] +fn uni_ext_comp() { + let ext = JsString::new("-u-ca-japanese-hc-h12"); + let components = unicode_extension_components(&ext); + assert!(components.attributes.is_empty()); + assert_eq!(components.keywords.len(), 2); + assert_eq!(components.keywords[0].key, "ca"); + assert_eq!(components.keywords[0].value, "japanese"); + assert_eq!(components.keywords[1].key, "hc"); + assert_eq!(components.keywords[1].value, "h12"); + + let ext = JsString::new("-u-alias-co-phonebk-ka-shifted"); + let components = unicode_extension_components(&ext); + assert_eq!(components.attributes, vec![JsString::new("alias")]); + assert_eq!(components.keywords.len(), 2); + assert_eq!(components.keywords[0].key, "co"); + assert_eq!(components.keywords[0].value, "phonebk"); + assert_eq!(components.keywords[1].key, "ka"); + assert_eq!(components.keywords[1].value, "shifted"); + + let ext = JsString::new("-u-ca-buddhist-kk-nu-thai"); + let components = unicode_extension_components(&ext); + assert!(components.attributes.is_empty()); + assert_eq!(components.keywords.len(), 3); + assert_eq!(components.keywords[0].key, "ca"); + assert_eq!(components.keywords[0].value, "buddhist"); + assert_eq!(components.keywords[1].key, "kk"); + assert_eq!(components.keywords[1].value, ""); + assert_eq!(components.keywords[2].key, "nu"); + assert_eq!(components.keywords[2].value, "thai"); + + let ext = JsString::new("-u-ca-islamic-civil"); + let components = unicode_extension_components(&ext); + assert!(components.attributes.is_empty()); + assert_eq!(components.keywords.len(), 1); + assert_eq!(components.keywords[0].key, "ca"); + assert_eq!(components.keywords[0].value, "islamic-civil"); +} + +#[test] +fn locale_resolution() { + let mut context = Context::default(); + + // test lookup + let available_locales = Vec::::new(); + let requested_locales = Vec::::new(); + let relevant_extension_keys = Vec::::new(); + let locale_data = FxHashMap::default(); + let options = DateTimeFormatRecord { + locale_matcher: JsString::new("lookup"), + properties: FxHashMap::default(), + }; + + let locale_record = resolve_locale( + &available_locales, + &requested_locales, + &options, + &relevant_extension_keys, + &locale_data, + &mut context, + ); + assert_eq!( + locale_record.locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert_eq!( + locale_record.data_locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert!(locale_record.properties.is_empty()); + + // test best fit + let available_locales = Vec::::new(); + let requested_locales = Vec::::new(); + let relevant_extension_keys = Vec::::new(); + let locale_data = FxHashMap::default(); + let options = DateTimeFormatRecord { + locale_matcher: JsString::new("best-fit"), + properties: FxHashMap::default(), + }; + + let locale_record = resolve_locale( + &available_locales, + &requested_locales, + &options, + &relevant_extension_keys, + &locale_data, + &mut context, + ); + assert_eq!( + locale_record.locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert_eq!( + locale_record.data_locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert!(locale_record.properties.is_empty()); + + // available: [es-ES], requested: [es-ES] + let available_locales = vec![JsString::new("es-ES")]; + let requested_locales = vec![JsString::new("es-ES")]; + let relevant_extension_keys = Vec::::new(); + let locale_data = FxHashMap::default(); + let options = DateTimeFormatRecord { + locale_matcher: JsString::new("lookup"), + properties: FxHashMap::default(), + }; + + let locale_record = resolve_locale( + &available_locales, + &requested_locales, + &options, + &relevant_extension_keys, + &locale_data, + &mut context, + ); + assert_eq!(locale_record.locale, "es-ES"); + assert_eq!(locale_record.data_locale, "es-ES"); + assert!(locale_record.properties.is_empty()); + + // available: [zh-CN], requested: [] + let available_locales = vec![JsString::new("zh-CN")]; + let requested_locales = Vec::::new(); + let relevant_extension_keys = Vec::::new(); + let locale_data = FxHashMap::default(); + let options = DateTimeFormatRecord { + locale_matcher: JsString::new("lookup"), + properties: FxHashMap::default(), + }; + + let locale_record = resolve_locale( + &available_locales, + &requested_locales, + &options, + &relevant_extension_keys, + &locale_data, + &mut context, + ); + assert_eq!( + locale_record.locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert_eq!( + locale_record.data_locale, + default_locale(context.icu().locale_canonicalizer()) + .to_string() + .as_str() + ); + assert!(locale_record.properties.is_empty()); +} + +#[test] +fn get_opt() { + let mut context = Context::default(); + + let values = Vec::::new(); + let fallback = JsValue::String(JsString::new("fallback")); + let options_obj = JsObject::empty(); + let option_type = GetOptionType::String; + let get_option_result = get_option( + &options_obj, + "", + &option_type, + &values, + &fallback, + &mut context, + ) + .expect("GetOption should not fail on fallback test"); + assert_eq!(get_option_result, fallback); + + let values = Vec::::new(); + let fallback = JsValue::String(JsString::new("fallback")); + let options_obj = JsObject::empty(); + let locale_value = JsValue::String(JsString::new("en-US")); + options_obj + .set("Locale", locale_value.clone(), true, &mut context) + .expect("Setting a property should not fail"); + let option_type = GetOptionType::String; + let get_option_result = get_option( + &options_obj, + "Locale", + &option_type, + &values, + &fallback, + &mut context, + ) + .expect("GetOption should not fail on string test"); + assert_eq!(get_option_result, locale_value); + + let fallback = JsValue::String(JsString::new("fallback")); + let options_obj = JsObject::empty(); + let locale_string = JsString::new("en-US"); + let locale_value = JsValue::String(locale_string.clone()); + let values = vec![locale_string]; + options_obj + .set("Locale", locale_value.clone(), true, &mut context) + .expect("Setting a property should not fail"); + let option_type = GetOptionType::String; + let get_option_result = get_option( + &options_obj, + "Locale", + &option_type, + &values, + &fallback, + &mut context, + ) + .expect("GetOption should not fail on values test"); + assert_eq!(get_option_result, locale_value); + + let fallback = JsValue::new(false); + let options_obj = JsObject::empty(); + let boolean_value = JsValue::new(true); + let values = Vec::::new(); + options_obj + .set("boolean_val", boolean_value.clone(), true, &mut context) + .expect("Setting a property should not fail"); + let option_type = GetOptionType::Boolean; + let get_option_result = get_option( + &options_obj, + "boolean_val", + &option_type, + &values, + &fallback, + &mut context, + ) + .expect("GetOption should not fail on boolean test"); + assert_eq!(get_option_result, boolean_value); + + let fallback = JsValue::String(JsString::new("fallback")); + let options_obj = JsObject::empty(); + let locale_value = JsValue::String(JsString::new("en-US")); + let other_locale_str = JsString::new("de-DE"); + let values = vec![other_locale_str]; + options_obj + .set("Locale", locale_value, true, &mut context) + .expect("Setting a property should not fail"); + let option_type = GetOptionType::String; + let get_option_result = get_option( + &options_obj, + "Locale", + &option_type, + &values, + &fallback, + &mut context, + ); + assert!(get_option_result.is_err()); + + let value = JsValue::undefined(); + let minimum = 1.0; + let maximum = 10.0; + let fallback_val = 5.0; + let fallback = Some(fallback_val); + let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); + assert_eq!(get_option_result, Ok(fallback)); + + let value = JsValue::nan(); + let minimum = 1.0; + let maximum = 10.0; + let fallback = Some(5.0); + let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); + assert!(get_option_result.is_err()); + + let value = JsValue::new(0); + let minimum = 1.0; + let maximum = 10.0; + let fallback = Some(5.0); + let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); + assert!(get_option_result.is_err()); + + let value = JsValue::new(11); + let minimum = 1.0; + let maximum = 10.0; + let fallback = Some(5.0); + let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); + assert!(get_option_result.is_err()); + + let value_f64 = 7.0; + let value = JsValue::new(value_f64); + let minimum = 1.0; + let maximum = 10.0; + let fallback = Some(5.0); + let get_option_result = default_number_option(&value, minimum, maximum, fallback, &mut context); + assert_eq!(get_option_result, Ok(Some(value_f64))); + + let options = JsObject::empty(); + let property = "fractionalSecondDigits"; + let minimum = 1.0; + let maximum = 10.0; + let fallback_val = 5.0; + let fallback = Some(fallback_val); + let get_option_result = + get_number_option(&options, property, minimum, maximum, fallback, &mut context); + assert_eq!(get_option_result, Ok(fallback)); + + let options = JsObject::empty(); + let value_f64 = 8.0; + let value = JsValue::new(value_f64); + let property = "fractionalSecondDigits"; + options + .set(property, value, true, &mut context) + .expect("Setting a property should not fail"); + let minimum = 1.0; + let maximum = 10.0; + let fallback = Some(5.0); + let get_option_result = + get_number_option(&options, property, minimum, maximum, fallback, &mut context); + assert_eq!(get_option_result, Ok(Some(value_f64))); +} + +#[test] +fn to_date_time_opts() { + let mut context = Context::default(); + + let options_obj = JsObject::empty(); + options_obj + .set("timeStyle", JsObject::empty(), true, &mut context) + .expect("Setting a property should not fail"); + let date_time_opts = to_date_time_options( + &JsValue::new(options_obj), + &DateTimeReqs::Date, + &DateTimeReqs::Date, + &mut context, + ); + assert!(date_time_opts.is_err()); + + let options_obj = JsObject::empty(); + options_obj + .set("dateStyle", JsObject::empty(), true, &mut context) + .expect("Setting a property should not fail"); + let date_time_opts = to_date_time_options( + &JsValue::new(options_obj), + &DateTimeReqs::Time, + &DateTimeReqs::Time, + &mut context, + ); + assert!(date_time_opts.is_err()); + + let date_time_opts = to_date_time_options( + &JsValue::undefined(), + &DateTimeReqs::Date, + &DateTimeReqs::Date, + &mut context, + ) + .expect("toDateTimeOptions should not fail in date test"); + + let numeric_jsstring = JsValue::String(JsString::new("numeric")); + assert_eq!( + date_time_opts.get("year", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("month", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("day", &mut context), + Ok(numeric_jsstring) + ); + + let date_time_opts = to_date_time_options( + &JsValue::undefined(), + &DateTimeReqs::Time, + &DateTimeReqs::Time, + &mut context, + ) + .expect("toDateTimeOptions should not fail in time test"); + + let numeric_jsstring = JsValue::String(JsString::new("numeric")); + assert_eq!( + date_time_opts.get("hour", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("minute", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("second", &mut context), + Ok(numeric_jsstring) + ); + + let date_time_opts = to_date_time_options( + &JsValue::undefined(), + &DateTimeReqs::AnyAll, + &DateTimeReqs::AnyAll, + &mut context, + ) + .expect("toDateTimeOptions should not fail when testing required = 'any'"); + + let numeric_jsstring = JsValue::String(JsString::new("numeric")); + assert_eq!( + date_time_opts.get("year", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("month", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("day", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("hour", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("minute", &mut context), + Ok(numeric_jsstring.clone()) + ); + assert_eq!( + date_time_opts.get("second", &mut context), + Ok(numeric_jsstring) + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/iterable/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/iterable/mod.rs new file mode 100644 index 0000000..ee92160 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/iterable/mod.rs @@ -0,0 +1,508 @@ +use crate::{ + builtins::{ + regexp::regexp_string_iterator::RegExpStringIterator, + string::string_iterator::StringIterator, ArrayIterator, ForInIterator, MapIterator, + SetIterator, + }, + object::{JsObject, ObjectInitializer}, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; + +#[derive(Debug, Default)] +pub struct IteratorPrototypes { + /// %IteratorPrototype% + iterator_prototype: JsObject, + /// %MapIteratorPrototype% + array_iterator: JsObject, + /// %SetIteratorPrototype% + set_iterator: JsObject, + /// %StringIteratorPrototype% + string_iterator: JsObject, + /// %RegExpStringIteratorPrototype% + regexp_string_iterator: JsObject, + /// %MapIteratorPrototype% + map_iterator: JsObject, + /// %ForInIteratorPrototype% + for_in_iterator: JsObject, +} + +impl IteratorPrototypes { + pub(crate) fn init(context: &mut Context) -> Self { + let _timer = Profiler::global().start_event("IteratorPrototypes::init", "init"); + + let iterator_prototype = create_iterator_prototype(context); + Self { + array_iterator: ArrayIterator::create_prototype(iterator_prototype.clone(), context), + set_iterator: SetIterator::create_prototype(iterator_prototype.clone(), context), + string_iterator: StringIterator::create_prototype(iterator_prototype.clone(), context), + regexp_string_iterator: RegExpStringIterator::create_prototype( + iterator_prototype.clone(), + context, + ), + map_iterator: MapIterator::create_prototype(iterator_prototype.clone(), context), + for_in_iterator: ForInIterator::create_prototype(iterator_prototype.clone(), context), + iterator_prototype, + } + } + + #[inline] + pub fn array_iterator(&self) -> JsObject { + self.array_iterator.clone() + } + + #[inline] + pub fn iterator_prototype(&self) -> JsObject { + self.iterator_prototype.clone() + } + + #[inline] + pub fn set_iterator(&self) -> JsObject { + self.set_iterator.clone() + } + + #[inline] + pub fn string_iterator(&self) -> JsObject { + self.string_iterator.clone() + } + + #[inline] + pub fn regexp_string_iterator(&self) -> JsObject { + self.regexp_string_iterator.clone() + } + + #[inline] + pub fn map_iterator(&self) -> JsObject { + self.map_iterator.clone() + } + + #[inline] + pub fn for_in_iterator(&self) -> JsObject { + self.for_in_iterator.clone() + } +} + +/// `CreateIterResultObject( value, done )` +/// +/// Generates an object supporting the `IteratorResult` interface. +#[inline] +pub fn create_iter_result_object(value: JsValue, done: bool, context: &mut Context) -> JsValue { + let _timer = Profiler::global().start_event("create_iter_result_object", "init"); + + // 1. Assert: Type(done) is Boolean. + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%). + let obj = context.construct_object(); + + // 3. Perform ! CreateDataPropertyOrThrow(obj, "value", value). + obj.create_data_property_or_throw("value", value, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + // 4. Perform ! CreateDataPropertyOrThrow(obj, "done", done). + obj.create_data_property_or_throw("done", done, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + // 5. Return obj. + obj.into() +} + +/// Iterator hint for `GetIterator`. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum IteratorHint { + Sync, + Async, +} + +impl JsValue { + /// `GetIterator ( obj [ , hint [ , method ] ] )` + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-getiterator + #[inline] + pub fn get_iterator( + &self, + context: &mut Context, + hint: Option, + method: Option, + ) -> JsResult { + // 1. If hint is not present, set hint to sync. + let hint = hint.unwrap_or(IteratorHint::Sync); + + // 2. If method is not present, then + let method = if let Some(method) = method { + method + } else { + // a. If hint is async, then + if hint == IteratorHint::Async { + // i. Set method to ? GetMethod(obj, @@asyncIterator). + if let Some(method) = + self.get_method(WellKnownSymbols::async_iterator(), context)? + { + method.into() + } else { + // ii. If method is undefined, then + // 1. Let syncMethod be ? GetMethod(obj, @@iterator). + let sync_method = self + .get_method(WellKnownSymbols::iterator(), context)? + .map_or(Self::Undefined, Self::from); + // 2. Let syncIteratorRecord be ? GetIterator(obj, sync, syncMethod). + let _sync_iterator_record = + self.get_iterator(context, Some(IteratorHint::Sync), Some(sync_method)); + // 3. Return ! CreateAsyncFromSyncIterator(syncIteratorRecord). + todo!("CreateAsyncFromSyncIterator"); + } + } else { + // b. Otherwise, set method to ? GetMethod(obj, @@iterator). + self.get_method(WellKnownSymbols::iterator(), context)? + .map_or(Self::Undefined, Self::from) + } + }; + + // 3. Let iterator be ? Call(method, obj). + let iterator = context.call(&method, self, &[])?; + + // 4. If Type(iterator) is not Object, throw a TypeError exception. + let iterator_obj = iterator + .as_object() + .ok_or_else(|| context.construct_type_error("the iterator is not an object"))?; + + // 5. Let nextMethod be ? GetV(iterator, "next"). + let next_method = iterator.get_v("next", context)?; + + // 6. Let iteratorRecord be the Record { [[Iterator]]: iterator, [[NextMethod]]: nextMethod, [[Done]]: false }. + // 7. Return iteratorRecord. + Ok(IteratorRecord::new( + iterator_obj.clone(), + next_method, + false, + )) + } +} + +/// Create the `%IteratorPrototype%` object +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-%iteratorprototype%-object +#[inline] +fn create_iterator_prototype(context: &mut Context) -> JsObject { + let _timer = Profiler::global().start_event("Iterator Prototype", "init"); + + let symbol_iterator = WellKnownSymbols::iterator(); + let iterator_prototype = ObjectInitializer::new(context) + .function( + |v, _, _| Ok(v.clone()), + (symbol_iterator, "[Symbol.iterator]"), + 0, + ) + .build(); + iterator_prototype +} + +/// The result of the iteration process. +#[derive(Debug)] +pub struct IteratorResult { + object: JsObject, +} + +impl IteratorResult { + /// `IteratorComplete ( iterResult )` + /// + /// The abstract operation `IteratorComplete` takes argument `iterResult` (an `Object`) and + /// returns either a normal completion containing a `Boolean` or a throw completion. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratorcomplete + #[inline] + pub fn complete(&self, context: &mut Context) -> JsResult { + // 1. Return ToBoolean(? Get(iterResult, "done")). + Ok(self.object.get("done", context)?.to_boolean()) + } + + /// `IteratorValue ( iterResult )` + /// + /// The abstract operation `IteratorValue` takes argument `iterResult` (an `Object`) and + /// returns either a normal completion containing an ECMAScript language value or a throw + /// completion. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratorvalue + #[inline] + pub fn value(&self, context: &mut Context) -> JsResult { + // 1. Return ? Get(iterResult, "value"). + self.object.get("value", context) + } +} + +/// Iterator Record +/// +/// An Iterator Record is a Record value used to encapsulate an +/// `Iterator` or `AsyncIterator` along with the `next` method. +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-iterator-records +#[derive(Debug)] +pub struct IteratorRecord { + /// `[[Iterator]]` + /// + /// An object that conforms to the `Iterator` or `AsyncIterator` interface. + iterator: JsObject, + + /// `[[NextMethod]]` + /// + /// The `next` method of the `[[Iterator]]` object. + next_method: JsValue, + + /// `[[Done]]` + /// + /// Whether the iterator has been closed. + done: bool, +} + +impl IteratorRecord { + /// Creates a new `IteratorRecord` with the given iterator object, next method and `done` flag. + #[inline] + pub fn new(iterator: JsObject, next_method: JsValue, done: bool) -> Self { + Self { + iterator, + next_method, + done, + } + } + + /// Get the `[[Iterator]]` field of the `IteratorRecord`. + #[inline] + pub(crate) fn iterator(&self) -> &JsObject { + &self.iterator + } + + /// Get the `[[NextMethod]]` field of the `IteratorRecord`. + #[inline] + pub(crate) fn next_method(&self) -> &JsValue { + &self.next_method + } + + /// Get the `[[Done]]` field of the `IteratorRecord`. + #[inline] + pub(crate) fn done(&self) -> bool { + self.done + } + + /// Sets the `[[Done]]` field of the `IteratorRecord`. + #[inline] + pub(crate) fn set_done(&mut self, done: bool) { + self.done = done; + } + + /// `IteratorNext ( iteratorRecord [ , value ] )` + /// + /// The abstract operation `IteratorNext` takes argument `iteratorRecord` (an `Iterator` + /// Record) and optional argument `value` (an ECMAScript language value) and returns either a + /// normal completion containing an `Object` or a throw completion. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratornext + #[inline] + pub(crate) fn next( + &self, + value: Option, + context: &mut Context, + ) -> JsResult { + let _timer = Profiler::global().start_event("IteratorRecord::next", "iterator"); + + // Note: We check if iteratorRecord.[[NextMethod]] is callable here. + // This check would happen in `Call` according to the spec, but we do not implement call for `JsValue`. + let next_method = if let Some(next_method) = self.next_method.as_callable() { + next_method + } else { + return context.throw_type_error("iterable next method not a function"); + }; + + let result = if let Some(value) = value { + // 2. Else, + // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]], « value »). + next_method.call(&self.iterator.clone().into(), &[value], context)? + } else { + // 1. If value is not present, then + // a. Let result be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). + next_method.call(&self.iterator.clone().into(), &[], context)? + }; + + // 3. If Type(result) is not Object, throw a TypeError exception. + // 4. Return result. + if let Some(o) = result.as_object() { + Ok(IteratorResult { object: o.clone() }) + } else { + context.throw_type_error("next value should be an object") + } + } + + /// `IteratorStep ( iteratorRecord )` + /// + /// The abstract operation `IteratorStep` takes argument `iteratorRecord` (an `Iterator` + /// Record) and returns either a normal completion containing either an `Object` or `false`, or + /// a throw completion. It requests the next value from `iteratorRecord.[[Iterator]]` by + /// calling `iteratorRecord.[[NextMethod]]` and returns either `false` indicating that the + /// iterator has reached its end or the `IteratorResult` object if a next value is available. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratorstep + pub(crate) fn step(&self, context: &mut Context) -> JsResult> { + let _timer = Profiler::global().start_event("IteratorRecord::step", "iterator"); + + // 1. Let result be ? IteratorNext(iteratorRecord). + let result = self.next(None, context)?; + + // 2. Let done be ? IteratorComplete(result). + let done = result.complete(context)?; + + // 3. If done is true, return false. + if done { + return Ok(None); + } + + // 4. Return result. + Ok(Some(result)) + } + + /// `IteratorClose ( iteratorRecord, completion )` + /// + /// The abstract operation `IteratorClose` takes arguments `iteratorRecord` (an + /// [Iterator Record][Self]) and `completion` (a `Completion` Record) and returns a + /// `Completion` Record. It is used to notify an iterator that it should perform any actions it + /// would normally perform when it has reached its completed state. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-iteratorclose + #[inline] + pub(crate) fn close( + &self, + completion: JsResult, + context: &mut Context, + ) -> JsResult { + let _timer = Profiler::global().start_event("IteratorRecord::close", "iterator"); + + // 1. Assert: Type(iteratorRecord.[[Iterator]]) is Object. + + // 2. Let iterator be iteratorRecord.[[Iterator]]. + let iterator = &self.iterator; + + // 3. Let innerResult be Completion(GetMethod(iterator, "return")). + let inner_result = iterator.get_method("return", context); + + // 4. If innerResult.[[Type]] is normal, then + let inner_result = match inner_result { + Ok(inner_result) => { + // a. Let return be innerResult.[[Value]]. + let r#return = inner_result; + + if let Some(r#return) = r#return { + // c. Set innerResult to Completion(Call(return, iterator)). + r#return.call(&iterator.clone().into(), &[], context) + } else { + // b. If return is undefined, return ? completion. + return completion; + } + } + Err(inner_result) => { + // 5. If completion.[[Type]] is throw, return ? completion. + completion?; + + // 6. If innerResult.[[Type]] is throw, return ? innerResult. + return Err(inner_result); + } + }; + + // 5. If completion.[[Type]] is throw, return ? completion. + let completion = completion?; + + // 6. If innerResult.[[Type]] is throw, return ? innerResult. + let inner_result = inner_result?; + + if inner_result.is_object() { + // 8. Return ? completion. + Ok(completion) + } else { + // 7. If Type(innerResult.[[Value]]) is not Object, throw a TypeError exception. + context.throw_type_error("inner result was not an object") + } + } +} + +/// `IterableToList ( items [ , method ] )` +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-iterabletolist +pub(crate) fn iterable_to_list( + context: &mut Context, + items: &JsValue, + method: Option, +) -> JsResult> { + let _timer = Profiler::global().start_event("iterable_to_list", "iterator"); + + // 1. If method is present, then + let iterator_record = if let Some(method) = method { + // a. Let iteratorRecord be ? GetIterator(items, sync, method). + items.get_iterator(context, Some(IteratorHint::Sync), Some(method))? + } else { + // 2. Else, + + // a. Let iteratorRecord be ? GetIterator(items, sync). + items.get_iterator(context, Some(IteratorHint::Sync), None)? + }; + + // 3. Let values be a new empty List. + let mut values = Vec::new(); + + // 4. Let next be true. + // 5. Repeat, while next is not false, + // a. Set next to ? IteratorStep(iteratorRecord). + // b. If next is not false, then + // i. Let nextValue be ? IteratorValue(next). + // ii. Append nextValue to the end of the List values. + while let Some(next) = iterator_record.step(context)? { + let next_value = next.value(context)?; + values.push(next_value); + } + + // 6. Return values. + Ok(values) +} + +/// `IfAbruptCloseIterator ( value, iteratorRecord )` +/// +/// `IfAbruptCloseIterator` is a shorthand for a sequence of algorithm steps that use an `Iterator` +/// Record. +/// +/// More information: +/// - [ECMA reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ifabruptcloseiterator +macro_rules! if_abrupt_close_iterator { + ($value:expr, $iterator_record:expr, $context:expr) => { + match $value { + // 1. If value is an abrupt completion, return ? IteratorClose(iteratorRecord, value). + Err(err) => return $iterator_record.close(Err(err), $context), + // 2. Else if value is a Completion Record, set value to value. + Ok(value) => value, + } + }; +} + +// Export macro to crate level +pub(crate) use if_abrupt_close_iterator; diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/mod.rs new file mode 100644 index 0000000..e51e101 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/mod.rs @@ -0,0 +1,739 @@ +//! This module implements the global `JSON` object. +//! +//! The `JSON` object contains methods for parsing [JavaScript Object Notation (JSON)][spec] +//! and converting values to JSON. It can't be called or constructed, and aside from its +//! two method properties, it has no interesting functionality of its own. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! - [JSON specification][json] +//! +//! [spec]: https://tc39.es/ecma262/#sec-json +//! [json]: https://www.json.org/json-en.html +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON + +use super::JsArgs; +use crate::{ + builtins::{ + string::{is_leading_surrogate, is_trailing_surrogate}, + BuiltIn, + }, + object::{JsObject, ObjectInitializer, RecursionLimiter}, + property::{Attribute, PropertyNameKind}, + symbol::WellKnownSymbols, + value::IntegerOrInfinity, + Context, JsResult, JsString, JsValue, +}; +use boa_profiler::Profiler; +use serde_json::{self, Value as JSONValue}; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +/// JavaScript `JSON` global object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Json; + +impl BuiltIn for Json { + const NAME: &'static str = "JSON"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE; + + ObjectInitializer::new(context) + .function(Self::parse, "parse", 2) + .function(Self::stringify, "stringify", 3) + .property(to_string_tag, Self::NAME, attribute) + .build() + .conv::() + .pipe(Some) + } +} + +impl Json { + /// `JSON.parse( text[, reviver] )` + /// + /// This `JSON` method parses a JSON string, constructing the JavaScript value or object described by the string. + /// + /// An optional `reviver` function can be provided to perform a transformation on the resulting object before it is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-json.parse + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse + pub(crate) fn parse(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let jsonString be ? ToString(text). + let json_string = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 2. Parse ! StringToCodePoints(jsonString) as a JSON text as specified in ECMA-404. + // Throw a SyntaxError exception if it is not a valid JSON text as defined in that specification. + if let Err(e) = serde_json::from_str::(&json_string) { + return context.throw_syntax_error(e.to_string()); + } + + // 3. Let scriptString be the string-concatenation of "(", jsonString, and ");". + let script_string = JsString::concat_array(&["(", json_string.as_str(), ");"]); + + // 4. Let script be ParseText(! StringToCodePoints(scriptString), Script). + // 5. NOTE: The early error rules defined in 13.2.5.1 have special handling for the above invocation of ParseText. + // 6. Assert: script is a Parse Node. + // 7. Let completion be the result of evaluating script. + // 8. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation. + // 9. Let unfiltered be completion.[[Value]]. + // 10. Assert: unfiltered is either a String, Number, Boolean, Null, or an Object that is defined by either an ArrayLiteral or an ObjectLiteral. + let unfiltered = context.eval(script_string.as_bytes())?; + + // 11. If IsCallable(reviver) is true, then + if let Some(obj) = args.get_or_undefined(1).as_callable() { + // a. Let root be ! OrdinaryObjectCreate(%Object.prototype%). + let root = context.construct_object(); + + // b. Let rootName be the empty String. + // c. Perform ! CreateDataPropertyOrThrow(root, rootName, unfiltered). + root.create_data_property_or_throw("", unfiltered, context) + .expect("CreateDataPropertyOrThrow should never throw here"); + + // d. Return ? InternalizeJSONProperty(root, rootName, reviver). + Self::internalize_json_property(&root, "".into(), obj, context) + } else { + // 12. Else, + // a. Return unfiltered. + Ok(unfiltered) + } + } + + /// `25.5.1.1 InternalizeJSONProperty ( holder, name, reviver )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-internalizejsonproperty + fn internalize_json_property( + holder: &JsObject, + name: JsString, + reviver: &JsObject, + context: &mut Context, + ) -> JsResult { + // 1. Let val be ? Get(holder, name). + let val = holder.get(name.clone(), context)?; + + // 2. If Type(val) is Object, then + if let Some(obj) = val.as_object() { + // a. Let isArray be ? IsArray(val). + // b. If isArray is true, then + if obj.is_array_abstract(context)? { + // i. Let I be 0. + // ii. Let len be ? LengthOfArrayLike(val). + // iii. Repeat, while I < len, + let len = obj.length_of_array_like(context)? as i64; + for i in 0..len { + // 1. Let prop be ! ToString(𝔽(I)). + // 2. Let newElement be ? InternalizeJSONProperty(val, prop, reviver). + let new_element = Self::internalize_json_property( + obj, + i.to_string().into(), + reviver, + context, + )?; + + // 3. If newElement is undefined, then + if new_element.is_undefined() { + // a. Perform ? val.[[Delete]](prop). + obj.__delete__(&i.into(), context)?; + } + // 4. Else, + else { + // a. Perform ? CreateDataProperty(val, prop, newElement). + obj.create_data_property(i, new_element, context)?; + } + } + } + // c. Else, + else { + // i. Let keys be ? EnumerableOwnPropertyNames(val, key). + let keys = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?; + + // ii. For each String P of keys, do + for p in keys { + // This is safe, because EnumerableOwnPropertyNames with 'key' type only returns strings. + let p = p + .as_string() + .expect("EnumerableOwnPropertyNames only returns strings"); + + // 1. Let newElement be ? InternalizeJSONProperty(val, P, reviver). + let new_element = + Self::internalize_json_property(obj, p.clone(), reviver, context)?; + + // 2. If newElement is undefined, then + if new_element.is_undefined() { + // a. Perform ? val.[[Delete]](P). + obj.__delete__(&p.clone().into(), context)?; + } + // 3. Else, + else { + // a. Perform ? CreateDataProperty(val, P, newElement). + obj.create_data_property(p.as_str(), new_element, context)?; + } + } + } + } + + // 3. Return ? Call(reviver, holder, « name, val »). + reviver.call(&holder.clone().into(), &[name.into(), val], context) + } + + /// `JSON.stringify( value[, replacer[, space]] )` + /// + /// This `JSON` method converts a JavaScript object or value to a JSON string. + /// + /// This method optionally replaces values if a `replacer` function is specified or + /// optionally including only the specified properties if a replacer array is specified. + /// + /// An optional `space` argument can be supplied of type `String` or `Number` that's used to insert + /// white space into the output JSON string for readability purposes. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-json.stringify + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify + pub(crate) fn stringify( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let stack be a new empty List. + let stack = Vec::new(); + + // 2. Let indent be the empty String. + let indent = JsString::new(""); + + // 3. Let PropertyList and ReplacerFunction be undefined. + let mut property_list = None; + let mut replacer_function = None; + + let replacer = args.get_or_undefined(1); + + // 4. If Type(replacer) is Object, then + if let Some(replacer_obj) = replacer.as_object() { + // a. If IsCallable(replacer) is true, then + if replacer_obj.is_callable() { + // i. Set ReplacerFunction to replacer. + replacer_function = Some(replacer_obj.clone()); + // b. Else, + } else { + // i. Let isArray be ? IsArray(replacer). + // ii. If isArray is true, then + if replacer_obj.is_array_abstract(context)? { + // 1. Set PropertyList to a new empty List. + let mut property_set = indexmap::IndexSet::new(); + + // 2. Let len be ? LengthOfArrayLike(replacer). + let len = replacer_obj.length_of_array_like(context)?; + + // 3. Let k be 0. + let mut k = 0; + + // 4. Repeat, while k < len, + while k < len { + // a. Let prop be ! ToString(𝔽(k)). + // b. Let v be ? Get(replacer, prop). + let v = replacer_obj.get(k, context)?; + + // c. Let item be undefined. + // d. If Type(v) is String, set item to v. + // e. Else if Type(v) is Number, set item to ! ToString(v). + // f. Else if Type(v) is Object, then + // g. If item is not undefined and item is not currently an element of PropertyList, then + // i. Append item to the end of PropertyList. + if let Some(s) = v.as_string() { + property_set.insert(s.clone()); + } else if v.is_number() { + property_set.insert( + v.to_string(context) + .expect("ToString cannot fail on number value"), + ); + } else if let Some(obj) = v.as_object() { + // i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v). + if obj.is_string() || obj.is_number() { + property_set.insert(v.to_string(context)?); + } + } + + // h. Set k to k + 1. + k += 1; + } + property_list = Some(property_set.into_iter().collect()); + } + } + } + + let mut space = args.get_or_undefined(2).clone(); + + // 5. If Type(space) is Object, then + if let Some(space_obj) = space.as_object() { + // a. If space has a [[NumberData]] internal slot, then + if space_obj.is_number() { + // i. Set space to ? ToNumber(space). + space = space.to_number(context)?.into(); + } + // b. Else if space has a [[StringData]] internal slot, then + else if space_obj.is_string() { + // i. Set space to ? ToString(space). + space = space.to_string(context)?.into(); + } + } + + // 6. If Type(space) is Number, then + let gap = if space.is_number() { + // a. Let spaceMV be ! ToIntegerOrInfinity(space). + // b. Set spaceMV to min(10, spaceMV). + // c. If spaceMV < 1, let gap be the empty String; otherwise let gap be the String value containing spaceMV occurrences of the code unit 0x0020 (SPACE). + match space + .to_integer_or_infinity(context) + .expect("ToIntegerOrInfinity cannot fail on number") + { + IntegerOrInfinity::PositiveInfinity => JsString::new(" "), + IntegerOrInfinity::NegativeInfinity => JsString::new(""), + IntegerOrInfinity::Integer(i) if i < 1 => JsString::new(""), + IntegerOrInfinity::Integer(i) => { + let mut s = String::new(); + let i = std::cmp::min(10, i); + for _ in 0..i { + s.push(' '); + } + s.into() + } + } + // 7. Else if Type(space) is String, then + } else if let Some(s) = space.as_string() { + // a. If the length of space is 10 or less, let gap be space; otherwise let gap be the substring of space from 0 to 10. + String::from_utf16_lossy(&s.encode_utf16().take(10).collect::>()).into() + // 8. Else, + } else { + // a. Let gap be the empty String. + JsString::new("") + }; + + // 9. Let wrapper be ! OrdinaryObjectCreate(%Object.prototype%). + let wrapper = context.construct_object(); + + // 10. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value). + wrapper + .create_data_property_or_throw("", args.get_or_undefined(0).clone(), context) + .expect("CreateDataPropertyOrThrow should never fail here"); + + // 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }. + let mut state = StateRecord { + replacer_function, + stack, + indent, + gap, + property_list, + }; + + // 12. Return ? SerializeJSONProperty(state, the empty String, wrapper). + Ok( + Self::serialize_json_property(&mut state, JsString::new(""), &wrapper, context)? + .map(Into::into) + .unwrap_or_default(), + ) + } + + /// `25.5.2.1 SerializeJSONProperty ( state, key, holder )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-serializejsonproperty + fn serialize_json_property( + state: &mut StateRecord, + key: JsString, + holder: &JsObject, + context: &mut Context, + ) -> JsResult> { + // 1. Let value be ? Get(holder, key). + let mut value = holder.get(key.clone(), context)?; + + // 2. If Type(value) is Object or BigInt, then + if value.is_object() || value.is_bigint() { + // a. Let toJSON be ? GetV(value, "toJSON"). + let to_json = value.get_v("toJSON", context)?; + + // b. If IsCallable(toJSON) is true, then + if let Some(obj) = to_json.as_object() { + if obj.is_callable() { + // i. Set value to ? Call(toJSON, value, « key »). + value = obj.call(&value, &[key.clone().into()], context)?; + } + } + } + + // 3. If state.[[ReplacerFunction]] is not undefined, then + if let Some(obj) = &state.replacer_function { + // a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »). + value = obj.call(&holder.clone().into(), &[key.into(), value], context)?; + } + + // 4. If Type(value) is Object, then + if let Some(obj) = value.as_object().cloned() { + // a. If value has a [[NumberData]] internal slot, then + if obj.is_number() { + // i. Set value to ? ToNumber(value). + value = value.to_number(context)?.into(); + } + // b. Else if value has a [[StringData]] internal slot, then + else if obj.is_string() { + // i. Set value to ? ToString(value). + value = value.to_string(context)?.into(); + } + // c. Else if value has a [[BooleanData]] internal slot, then + else if let Some(boolean) = obj.borrow().as_boolean() { + // i. Set value to value.[[BooleanData]]. + value = boolean.into(); + } + // d. Else if value has a [[BigIntData]] internal slot, then + else if let Some(bigint) = obj.borrow().as_bigint() { + // i. Set value to value.[[BigIntData]]. + value = bigint.clone().into(); + } + } + + // 5. If value is null, return "null". + if value.is_null() { + return Ok(Some(JsString::new("null"))); + } + + // 6. If value is true, return "true". + // 7. If value is false, return "false". + if value.is_boolean() { + return Ok(Some(JsString::new(if value.to_boolean() { + "true" + } else { + "false" + }))); + } + + // 8. If Type(value) is String, return QuoteJSONString(value). + if let Some(s) = value.as_string() { + return Ok(Some(Self::quote_json_string(s))); + } + + // 9. If Type(value) is Number, then + if let Some(n) = value.as_number() { + // a. If value is finite, return ! ToString(value). + if n.is_finite() { + return Ok(Some( + value + .to_string(context) + .expect("ToString should never fail here"), + )); + } + + // b. Return "null". + return Ok(Some(JsString::new("null"))); + } + + // 10. If Type(value) is BigInt, throw a TypeError exception. + if value.is_bigint() { + return context.throw_type_error("cannot serialize bigint to JSON"); + } + + // 11. If Type(value) is Object and IsCallable(value) is false, then + if let Some(obj) = value.as_object() { + if !obj.is_callable() { + // a. Let isArray be ? IsArray(value). + // b. If isArray is true, return ? SerializeJSONArray(state, value). + // c. Return ? SerializeJSONObject(state, value). + return if obj.is_array_abstract(context)? { + Ok(Some(Self::serialize_json_array(state, obj, context)?)) + } else { + Ok(Some(Self::serialize_json_object(state, obj, context)?)) + }; + } + } + + // 12. Return undefined. + Ok(None) + } + + /// `25.5.2.2 QuoteJSONString ( value )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-quotejsonstring + fn quote_json_string(value: &JsString) -> JsString { + // 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK). + let mut product = String::from('"'); + + // 2. For each code point C of ! StringToCodePoints(value), do + for code_point in value.encode_utf16() { + match code_point { + // a. If C is listed in the “Code Point” column of Table 73, then + // i. Set product to the string-concatenation of product and the escape sequence for C as specified in the “Escape Sequence” column of the corresponding row. + 0x8 => product.push_str("\\b"), + 0x9 => product.push_str("\\t"), + 0xA => product.push_str("\\n"), + 0xC => product.push_str("\\f"), + 0xD => product.push_str("\\r"), + 0x22 => product.push_str("\\\""), + 0x5C => product.push_str("\\\\"), + // b. Else if C has a numeric value less than 0x0020 (SPACE), or if C has the same numeric value as a leading surrogate or trailing surrogate, then + code_point + if is_leading_surrogate(code_point) || is_trailing_surrogate(code_point) => + { + // i. Let unit be the code unit whose numeric value is that of C. + // ii. Set product to the string-concatenation of product and UnicodeEscape(unit). + product.push_str(&format!("\\\\uAA{code_point:x}")); + } + // c. Else, + code_point => { + // i. Set product to the string-concatenation of product and ! UTF16EncodeCodePoint(C). + product.push( + char::from_u32(u32::from(code_point)) + .expect("char from code point cannot fail here"), + ); + } + } + } + + // 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK). + product.push('"'); + + // 4. Return product. + product.into() + } + + /// `25.5.2.4 SerializeJSONObject ( state, value )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-serializejsonobject + fn serialize_json_object( + state: &mut StateRecord, + value: &JsObject, + context: &mut Context, + ) -> JsResult { + // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. + let limiter = RecursionLimiter::new(value); + if limiter.live { + return context.throw_type_error("cyclic object value"); + } + + // 2. Append value to state.[[Stack]]. + state.stack.push(value.clone().into()); + + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); + + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = JsString::concat(&state.indent, &state.gap); + + // 5. If state.[[PropertyList]] is not undefined, then + let k = if let Some(p) = &state.property_list { + // a. Let K be state.[[PropertyList]]. + p.clone() + // 6. Else, + } else { + // a. Let K be ? EnumerableOwnPropertyNames(value, key). + let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?; + // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values. + keys.iter() + .map(|v| { + v.to_string(context) + .expect("EnumerableOwnPropertyNames only returns strings") + }) + .collect() + }; + + // 7. Let partial be a new empty List. + let mut partial = Vec::new(); + + // 8. For each element P of K, do + for p in &k { + // a. Let strP be ? SerializeJSONProperty(state, P, value). + let str_p = Self::serialize_json_property(state, p.clone(), value, context)?; + + // b. If strP is not undefined, then + if let Some(str_p) = str_p { + // i. Let member be QuoteJSONString(P). + // ii. Set member to the string-concatenation of member and ":". + // iii. If state.[[Gap]] is not the empty String, then + // 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE). + // iv. Set member to the string-concatenation of member and strP. + let member = if state.gap.is_empty() { + format!("{}:{}", Self::quote_json_string(p).as_str(), str_p.as_str()) + } else { + format!( + "{}: {}", + Self::quote_json_string(p).as_str(), + str_p.as_str() + ) + }; + + // v. Append member to partial. + partial.push(member); + } + } + + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "{}". + JsString::new("{}") + // 10. Else, + } else { + // a. If state.[[Gap]] is the empty String, then + if state.gap.is_empty() { + // i. Let properties be the String value formed by concatenating all the element Strings of partial + // with each adjacent pair of Strings separated with the code unit 0x002C (COMMA). + // A comma is not inserted either before the first String or after the last String. + // ii. Let final be the string-concatenation of "{", properties, and "}". + format!("{{{}}}", partial.join(",")).into() + // b. Else, + } else { + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let separator = format!(",\n{}", state.indent.as_str()); + // ii. Let properties be the String value formed by concatenating all the element Strings of partial + // with each adjacent pair of Strings separated with separator. + // The separator String is not inserted either before the first String or after the last String. + let properties = partial.join(&separator); + // iii. Let final be the string-concatenation of "{", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "}". + format!( + "{{\n{}{properties}\n{}}}", + state.indent.as_str(), + stepback.as_str() + ) + .into() + } + }; + + // 11. Remove the last element of state.[[Stack]]. + state.stack.pop(); + + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; + + // 13. Return final. + Ok(r#final) + } + + /// `25.5.2.5 SerializeJSONArray ( state, value )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-serializejsonarray + fn serialize_json_array( + state: &mut StateRecord, + value: &JsObject, + context: &mut Context, + ) -> JsResult { + // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. + let limiter = RecursionLimiter::new(value); + if limiter.live { + return context.throw_type_error("cyclic object value"); + } + + // 2. Append value to state.[[Stack]]. + state.stack.push(value.clone().into()); + + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); + + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = JsString::concat(&state.indent, &state.gap); + + // 5. Let partial be a new empty List. + let mut partial = Vec::new(); + + // 6. Let len be ? LengthOfArrayLike(value). + let len = value.length_of_array_like(context)?; + + // 7. Let index be 0. + let mut index = 0; + + // 8. Repeat, while index < len, + while index < len { + // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). + let str_p = + Self::serialize_json_property(state, index.to_string().into(), value, context)?; + + // b. If strP is undefined, then + if let Some(str_p) = str_p { + // i. Append strP to partial. + partial.push(str_p); + // c. Else, + } else { + // i. Append "null" to partial. + partial.push("null".into()); + } + + // d. Set index to index + 1. + index += 1; + } + + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "[]". + JsString::from("[]") + // 10. Else, + } else { + // a. If state.[[Gap]] is the empty String, then + if state.gap.is_empty() { + // i. Let properties be the String value formed by concatenating all the element Strings of partial + // with each adjacent pair of Strings separated with the code unit 0x002C (COMMA). + // A comma is not inserted either before the first String or after the last String. + // ii. Let final be the string-concatenation of "[", properties, and "]". + format!("[{}]", partial.join(",")).into() + // b. Else, + } else { + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let separator = format!(",\n{}", state.indent.as_str()); + // ii. Let properties be the String value formed by concatenating all the element Strings of partial + // with each adjacent pair of Strings separated with separator. + // The separator String is not inserted either before the first String or after the last String. + let properties = partial.join(&separator); + // iii. Let final be the string-concatenation of "[", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepback, and "]". + format!( + "[\n{}{properties}\n{}]", + state.indent.as_str(), + stepback.as_str() + ) + .into() + } + }; + + // 11. Remove the last element of state.[[Stack]]. + state.stack.pop(); + + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; + + // 13. Return final. + Ok(r#final) + } +} + +struct StateRecord { + replacer_function: Option, + stack: Vec, + indent: JsString, + gap: JsString, + property_list: Option>, +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/tests.rs new file mode 100644 index 0000000..0fa74a0 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/json/tests.rs @@ -0,0 +1,468 @@ +use crate::{forward, forward_val, Context}; + +#[test] +fn json_sanity() { + let mut context = Context::default(); + assert_eq!( + forward(&mut context, r#"JSON.parse('{"aaa":"bbb"}').aaa == 'bbb'"#), + "true" + ); + assert_eq!( + forward( + &mut context, + r#"JSON.stringify({aaa: 'bbb'}) == '{"aaa":"bbb"}'"# + ), + "true" + ); +} + +#[test] +fn json_stringify_remove_undefined_values_from_objects() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({ aaa: undefined, bbb: 'ccc' })"#, + ); + let expected = r#""{"bbb":"ccc"}""#; + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_remove_function_values_from_objects() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({ aaa: () => {}, bbb: 'ccc' })"#, + ); + let expected = r#""{"bbb":"ccc"}""#; + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_remove_symbols_from_objects() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({ aaa: Symbol(), bbb: 'ccc' })"#, + ); + let expected = r#""{"bbb":"ccc"}""#; + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_array_strings() { + let mut context = Context::default(); + let actual = forward( + &mut context, + r#"JSON.stringify({aaa: 'bbb', bbb: 'ccc', ccc: 'ddd'}, ['aaa', 'bbb'])"#, + ); + let expected = forward(&mut context, r#"'{"aaa":"bbb","bbb":"ccc"}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_array_numbers() { + let mut context = Context::default(); + let actual = forward( + &mut context, + r#"JSON.stringify({ 0: 'aaa', 1: 'bbb', 2: 'ccc'}, [1, 2])"#, + ); + let expected = forward(&mut context, r#"'{"1":"bbb","2":"ccc"}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_replacer_function() { + let mut context = Context::default(); + let actual = forward( + &mut context, + r#"JSON.stringify({ aaa: 1, bbb: 2}, (key, value) => { + if (key === 'aaa') { + return undefined; + } + + return value; + })"#, + ); + let expected = forward(&mut context, r#"'{"bbb":2}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_arrays() { + let mut context = Context::default(); + let actual = forward(&mut context, r#"JSON.stringify(['a', 'b'])"#); + let expected = forward(&mut context, r#"'["a","b"]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_object_array() { + let mut context = Context::default(); + let actual = forward(&mut context, r#"JSON.stringify([{a: 'b'}, {b: 'c'}])"#); + let expected = forward(&mut context, r#"'[{"a":"b"},{"b":"c"}]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_array_converts_undefined_to_null() { + let mut context = Context::default(); + let actual = forward(&mut context, r#"JSON.stringify([undefined])"#); + let expected = forward(&mut context, r#"'[null]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_array_converts_function_to_null() { + let mut context = Context::default(); + let actual = forward(&mut context, r#"JSON.stringify([() => {}])"#); + let expected = forward(&mut context, r#"'[null]'"#); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_array_converts_symbol_to_null() { + let mut context = Context::default(); + let actual = forward(&mut context, r#"JSON.stringify([Symbol()])"#); + let expected = forward(&mut context, r#"'[null]'"#); + + assert_eq!(actual, expected); +} +#[test] +fn json_stringify_function_replacer_propagate_error() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#" + let thrown = 0; + try { + JSON.stringify({x: 1}, (key, value) => { throw 1 }) + } catch (err) { + thrown = err; + } + thrown + "#, + ); + let expected = forward(&mut context, "1"); + + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_function() { + let mut context = Context::default(); + + let actual_function = forward(&mut context, r#"JSON.stringify(() => {})"#); + let expected = forward(&mut context, r#"undefined"#); + + assert_eq!(actual_function, expected); +} + +#[test] +fn json_stringify_undefined() { + let mut context = Context::default(); + let actual_undefined = forward(&mut context, r#"JSON.stringify(undefined)"#); + let expected = forward(&mut context, r#"undefined"#); + + assert_eq!(actual_undefined, expected); +} + +#[test] +fn json_stringify_symbol() { + let mut context = Context::default(); + + let actual_symbol = forward(&mut context, r#"JSON.stringify(Symbol())"#); + let expected = forward(&mut context, r#"undefined"#); + + assert_eq!(actual_symbol, expected); +} + +#[test] +fn json_stringify_no_args() { + let mut context = Context::default(); + + let actual_no_args = forward(&mut context, r#"JSON.stringify()"#); + let expected = forward(&mut context, r#"undefined"#); + + assert_eq!(actual_no_args, expected); +} + +#[test] +fn json_stringify_fractional_numbers() { + let mut context = Context::default(); + + let actual = forward(&mut context, r#"JSON.stringify(Math.round(1.0))"#); + let expected = forward(&mut context, r#""1""#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, undefined, 4)"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +' "a": "b",\n' + +' "b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_four_spaces() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, undefined, 4.3)"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +' "a": "b",\n' + +' "b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_twenty_spaces() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, ["a", "b"], 20)"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +' "a": "b",\n' + +' "b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_with_number_object() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, undefined, new Number(10))"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +' "a": "b",\n' + +' "b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_bad_space_argument() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, ["a", "b"], [])"#, + ); + let expected = forward(&mut context, r#"'{"a":"b","b":"c"}'"#); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_with_too_long_string() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, undefined, "abcdefghijklmn")"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +'abcdefghij"a": "b",\n' + +'abcdefghij"b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_stringify_pretty_print_with_string_object() { + let mut context = Context::default(); + + let actual = forward( + &mut context, + r#"JSON.stringify({a: "b", b: "c"}, undefined, new String("abcd"))"#, + ); + let expected = forward( + &mut context, + r#"'{\n' + +'abcd"a": "b",\n' + +'abcd"b": "c"\n' + +'}'"#, + ); + assert_eq!(actual, expected); +} + +#[test] +fn json_parse_array_with_reviver() { + let mut context = Context::default(); + let result = forward_val( + &mut context, + r#"JSON.parse('[1,2,3,4]', function(k, v){ + if (typeof v == 'number') { + return v * 2; + } else { + return v; + } + })"#, + ) + .unwrap(); + assert_eq!( + result + .get_v("0", &mut context) + .unwrap() + .to_number(&mut context) + .unwrap() as u8, + 2u8 + ); + assert_eq!( + result + .get_v("1", &mut context) + .unwrap() + .to_number(&mut context) + .unwrap() as u8, + 4u8 + ); + assert_eq!( + result + .get_v("2", &mut context) + .unwrap() + .to_number(&mut context) + .unwrap() as u8, + 6u8 + ); + assert_eq!( + result + .get_v("3", &mut context) + .unwrap() + .to_number(&mut context) + .unwrap() as u8, + 8u8 + ); +} + +#[test] +fn json_parse_object_with_reviver() { + let mut context = Context::default(); + let result = forward( + &mut context, + r#" + var myObj = new Object(); + myObj.firstname = "boa"; + myObj.lastname = "snake"; + var jsonString = JSON.stringify(myObj); + + function dataReviver(key, value) { + if (key == 'lastname') { + return 'interpreter'; + } else { + return value; + } + } + + var jsonObj = JSON.parse(jsonString, dataReviver); + + JSON.stringify(jsonObj);"#, + ); + assert_eq!(result, r#""{"firstname":"boa","lastname":"interpreter"}""#); +} + +#[test] +fn json_parse_sets_prototypes() { + let mut context = Context::default(); + let init = r#" + const jsonString = "{\"ob\":{\"ject\":1},\"arr\": [0,1]}"; + const jsonObj = JSON.parse(jsonString); + "#; + eprintln!("{}", forward(&mut context, init)); + let object_prototype = forward_val(&mut context, r#"jsonObj.ob"#) + .unwrap() + .as_object() + .unwrap() + .prototype() + .clone(); + let array_prototype = forward_val(&mut context, r#"jsonObj.arr"#) + .unwrap() + .as_object() + .unwrap() + .prototype() + .clone(); + let global_object_prototype = context + .intrinsics() + .constructors() + .object() + .prototype() + .into(); + let global_array_prototype = context + .intrinsics() + .constructors() + .array() + .prototype() + .into(); + assert_eq!(object_prototype, global_object_prototype); + assert_eq!(array_prototype, global_array_prototype); +} + +#[test] +fn json_fields_should_be_enumerable() { + let mut context = Context::default(); + let actual_object = forward( + &mut context, + r#" + var a = JSON.parse('{"x":0}'); + a.propertyIsEnumerable('x'); + "#, + ); + let actual_array_index = forward( + &mut context, + r#" + var b = JSON.parse('[0, 1]'); + b.propertyIsEnumerable('0'); + "#, + ); + let expected = forward(&mut context, r#"true"#); + + assert_eq!(actual_object, expected); + assert_eq!(actual_array_index, expected); +} + +#[test] +fn json_parse_with_no_args_throws_syntax_error() { + let mut context = Context::default(); + let result = forward(&mut context, "JSON.parse();"); + assert!(result.contains("SyntaxError")); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/map_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/map_iterator.rs new file mode 100644 index 0000000..d847beb --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/map_iterator.rs @@ -0,0 +1,144 @@ +use super::ordered_map::MapLock; +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue}, + object::{JsObject, ObjectData}, + property::{PropertyDescriptor, PropertyNameKind}, + symbol::WellKnownSymbols, + Context, JsResult, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +/// The Map Iterator object represents an iteration over a map. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-array-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct MapIterator { + iterated_map: Option, + map_next_index: usize, + map_iteration_kind: PropertyNameKind, + lock: MapLock, +} + +impl MapIterator { + pub(crate) const NAME: &'static str = "MapIterator"; + + /// Abstract operation `CreateMapIterator( map, kind )` + /// + /// Creates a new iterator over the given map. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createmapiterator + pub(crate) fn create_map_iterator( + map: &JsValue, + kind: PropertyNameKind, + context: &mut Context, + ) -> JsResult { + if let Some(map_obj) = map.as_object() { + if let Some(map) = map_obj.borrow_mut().as_map_mut() { + let lock = map.lock(map_obj.clone()); + let iter = Self { + iterated_map: Some(map_obj.clone()), + map_next_index: 0, + map_iteration_kind: kind, + lock, + }; + let map_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .map_iterator(), + ObjectData::map_iterator(iter), + ); + return Ok(map_iterator.into()); + } + } + context.throw_type_error("`this` is not a Map") + } + + /// %MapIteratorPrototype%.next( ) + /// + /// Advances the iterator and gets the next result in the map. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%.next + pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut map_iterator = this.as_object().map(JsObject::borrow_mut); + let map_iterator = map_iterator + .as_mut() + .and_then(|obj| obj.as_map_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not a MapIterator"))?; + + let item_kind = map_iterator.map_iteration_kind; + + if let Some(obj) = map_iterator.iterated_map.take() { + let e = { + let map = obj.borrow(); + let entries = map.as_map_ref().expect("iterator should only iterate maps"); + let len = entries.full_len(); + loop { + let element = entries + .get_index(map_iterator.map_next_index) + .map(|(v, k)| (v.clone(), k.clone())); + map_iterator.map_next_index += 1; + if element.is_some() || map_iterator.map_next_index >= len { + break element; + } + } + }; + if let Some((key, value)) = e { + let item = match item_kind { + PropertyNameKind::Key => Ok(create_iter_result_object(key, false, context)), + PropertyNameKind::Value => Ok(create_iter_result_object(value, false, context)), + PropertyNameKind::KeyAndValue => { + let result = Array::create_array_from_list([key, value], context); + Ok(create_iter_result_object(result.into(), false, context)) + } + }; + map_iterator.iterated_map = Some(obj); + return item; + } + } + + Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + } + + /// Create the `%MapIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%mapiteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let map_iterator = + JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &map_iterator, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("Map Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + map_iterator.insert(to_string_tag, to_string_tag_property); + map_iterator + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/mod.rs new file mode 100644 index 0000000..26bf207 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/mod.rs @@ -0,0 +1,591 @@ +//! This module implements the global `Map` object. +//! +//! The JavaScript `Map` class is a global object that is used in the construction of maps; which +//! are high-level, key-value stores. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-map-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map + +#![allow(clippy::mutable_key_type)] + +use self::{map_iterator::MapIterator, ordered_map::OrderedMap}; +use super::JsArgs; +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::{Attribute, PropertyNameKind}, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use num_traits::Zero; +use tap::{Conv, Pipe}; + +pub mod map_iterator; +pub mod ordered_map; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub(crate) struct Map(OrderedMap); + +impl BuiltIn for Map { + const NAME: &'static str = "Map"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let get_size = FunctionBuilder::native(context, Self::get_size) + .name("get size") + .length(0) + .constructor(false) + .build(); + + let entries_function = FunctionBuilder::native(context, Self::entries) + .name("entries") + .length(0) + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().map().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .property( + "entries", + entries_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + WellKnownSymbols::iterator(), + entries_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .method(Self::clear, "clear", 0) + .method(Self::delete, "delete", 1) + .method(Self::for_each, "forEach", 1) + .method(Self::get, "get", 1) + .method(Self::has, "has", 1) + .method(Self::keys, "keys", 0) + .method(Self::set, "set", 2) + .method(Self::values, "values", 0) + .accessor("size", Some(get_size), None, Attribute::CONFIGURABLE) + .build() + .conv::() + .pipe(Some) + } +} + +impl Map { + pub(crate) const LENGTH: usize = 0; + + /// `Map ( [ iterable ] )` + /// + /// Constructor for `Map` objects. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map-iterable + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/Map + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context + .throw_type_error("calling a builtin Map constructor without new is forbidden"); + } + + // 2. Let map be ? OrdinaryCreateFromConstructor(NewTarget, "%Map.prototype%", « [[MapData]] »). + // 3. Set map.[[MapData]] to a new empty List. + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::map, context)?; + let map = JsObject::from_proto_and_data(prototype, ObjectData::map(OrderedMap::new())); + + // 4. If iterable is either undefined or null, return map. + let iterable = match args.get_or_undefined(0) { + val if !val.is_null_or_undefined() => val, + _ => return Ok(map.into()), + }; + + // 5. Let adder be ? Get(map, "set"). + let adder = map.get("set", context)?; + + // 6. Return ? AddEntriesFromIterable(map, iterable, adder). + add_entries_from_iterable(&map, iterable, &adder, context) + } + + /// `get Map [ @@species ]` + /// + /// The `Map [ @@species ]` accessor property returns the Map constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-map-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// `Map.prototype.entries()` + /// + /// Returns a new Iterator object that contains the [key, value] pairs for each element in the Map object in insertion order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries + pub(crate) fn entries( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Return ? CreateMapIterator(M, key+value). + MapIterator::create_map_iterator(this, PropertyNameKind::KeyAndValue, context) + } + + /// `Map.prototype.keys()` + /// + /// Returns a new Iterator object that contains the keys for each element in the Map object in insertion order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.keys + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/keys + pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let M be the this value. + // 2. Return ? CreateMapIterator(M, key). + MapIterator::create_map_iterator(this, PropertyNameKind::Key, context) + } + + /// `Map.prototype.set( key, value )` + /// + /// Inserts a new entry in the Map object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.set + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/set + pub(crate) fn set( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let key = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + + // 1. Let M be the this value. + if let Some(object) = this.as_object() { + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow_mut().as_map_mut() { + let key = match key { + JsValue::Rational(r) => { + // 5. If key is -0𝔽, set key to +0𝔽. + if r.is_zero() { + JsValue::Rational(0f64) + } else { + key.clone() + } + } + _ => key.clone(), + }; + // 4. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then + // i. Set p.[[Value]] to value. + // 6. Let p be the Record { [[Key]]: key, [[Value]]: value }. + // 7. Append p as the last element of entries. + map.insert(key, value.clone()); + // ii. Return M. + // 8. Return M. + return Ok(this.clone()); + } + } + context.throw_type_error("'this' is not a Map") + } + + /// `get Map.prototype.size` + /// + /// Obtains the size of the map, filtering empty keys to ensure it updates + /// while iterating. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-map.prototype.size + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/size + pub(crate) fn get_size( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let M be the this value. + if let Some(object) = this.as_object() { + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow_mut().as_map_mut() { + // 4. Let count be 0. + // 5. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty, set count to count + 1. + // 6. Return 𝔽(count). + return Ok(map.len().into()); + } + } + context.throw_type_error("'this' is not a Map") + } + + /// `Map.prototype.delete( key )` + /// + /// Removes the element associated with the key, if it exists. + /// Returns true if there was an element, and false otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.delete + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/delete + pub(crate) fn delete( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let key = args.get_or_undefined(0); + + // 1. Let M be the this value. + if let Some(object) = this.as_object() { + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow_mut().as_map_mut() { + // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, then + // i. Set p.[[Key]] to empty. + // ii. Set p.[[Value]] to empty. + // iii. Return true. + // 5. Return false. + return Ok(map.remove(key).is_some().into()); + } + } + context.throw_type_error("'this' is not a Map") + } + + /// `Map.prototype.get( key )` + /// + /// Returns the value associated with the key, or undefined if there is none. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.get + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/get + pub(crate) fn get( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + const JS_ZERO: &JsValue = &JsValue::Rational(0f64); + + let key = args.get_or_undefined(0); + let key = match key { + JsValue::Rational(r) => { + if r.is_zero() { + JS_ZERO + } else { + key + } + } + _ => key, + }; + + // 1. Let M be the this value. + if let JsValue::Object(ref object) = this { + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow().as_map_ref() { + // 4. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return p.[[Value]]. + // 5. Return undefined. + return Ok(map.get(key).cloned().unwrap_or_default()); + } + } + + context.throw_type_error("'this' is not a Map") + } + + /// `Map.prototype.clear( )` + /// + /// Removes all entries from the map. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.clear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/clear + pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + if let Some(object) = this.as_object() { + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow_mut().as_map_mut() { + // 4. For each Record { [[Key]], [[Value]] } p of entries, do + // a. Set p.[[Key]] to empty. + // b. Set p.[[Value]] to empty. + map.clear(); + + // 5. Return undefined. + return Ok(JsValue::undefined()); + } + } + context.throw_type_error("'this' is not a Map") + } + + /// `Map.prototype.has( key )` + /// + /// Checks if the map contains an entry with the given key. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has + pub(crate) fn has( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + const JS_ZERO: &JsValue = &JsValue::Rational(0f64); + + let key = args.get_or_undefined(0); + let key = match key { + JsValue::Rational(r) => { + if r.is_zero() { + JS_ZERO + } else { + key + } + } + _ => key, + }; + + // 1. Let M be the this value. + if let JsValue::Object(ref object) = this { + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + // 3. Let entries be the List that is M.[[MapData]]. + if let Some(map) = object.borrow().as_map_ref() { + // 4. For each Record { [[Key]], [[Value]] } p of entries, do + // a. If p.[[Key]] is not empty and SameValueZero(p.[[Key]], key) is true, return true. + // 5. Return false. + return Ok(map.contains_key(key).into()); + } + } + + context.throw_type_error("'this' is not a Map") + } + + /// `Map.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// Executes the provided callback function for each key-value pair in the map. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/forEach + pub(crate) fn for_each( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Perform ? RequireInternalSlot(M, [[MapData]]). + let map = this + .as_object() + .filter(|obj| obj.is_map()) + .ok_or_else(|| context.construct_type_error("`this` is not a Map"))?; + + // 3. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback = args.get_or_undefined(0); + let callback = callback.as_callable().ok_or_else(|| { + context.construct_type_error(format!("{} is not a function", callback.display())) + })?; + + let this_arg = args.get_or_undefined(1); + + // NOTE: + // + // forEach does not directly mutate the object on which it is called but + // the object may be mutated by the calls to callbackfn. Each entry of a + // map's [[MapData]] is only visited once. New keys added after the call + // to forEach begins are visited. A key will be revisited if it is deleted + // after it has been visited and then re-added before the forEach call completes. + // Keys that are deleted after the call to forEach begins and before being visited + // are not visited unless the key is added again before the forEach call completes. + let _lock = map + .borrow_mut() + .as_map_mut() + .expect("checked that `this` was a map") + .lock(map.clone()); + + // 4. Let entries be the List that is M.[[MapData]]. + // 5. For each Record { [[Key]], [[Value]] } e of entries, do + let mut index = 0; + loop { + let arguments = { + let map = map.borrow(); + let map = map.as_map_ref().expect("checked that `this` was a map"); + if index < map.full_len() { + map.get_index(index) + .map(|(k, v)| [v.clone(), k.clone(), this.clone()]) + } else { + // 6. Return undefined. + return Ok(JsValue::undefined()); + } + }; + + // a. If e.[[Key]] is not empty, then + if let Some(arguments) = arguments { + // i. Perform ? Call(callbackfn, thisArg, « e.[[Value]], e.[[Key]], M »). + callback.call(this_arg, &arguments, context)?; + } + + index += 1; + } + } + + /// `Map.prototype.values()` + /// + /// Returns a new Iterator object that contains the values for each element in the Map object in insertion order. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/values + pub(crate) fn values( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let M be the this value. + // 2. Return ? CreateMapIterator(M, value). + MapIterator::create_map_iterator(this, PropertyNameKind::Value, context) + } +} + +/// `AddEntriesFromIterable` +/// +/// Allows adding entries to a map from any object that has a `@@Iterator` field. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-add-entries-from-iterable +pub(crate) fn add_entries_from_iterable( + target: &JsObject, + iterable: &JsValue, + adder: &JsValue, + context: &mut Context, +) -> JsResult { + // 1. If IsCallable(adder) is false, throw a TypeError exception. + let adder = adder.as_callable().ok_or_else(|| { + context.construct_type_error("property `set` of `NewTarget` is not callable") + })?; + + // 2. Let iteratorRecord be ? GetIterator(iterable). + let iterator_record = iterable.get_iterator(context, None, None)?; + + // 3. Repeat, + loop { + // a. Let next be ? IteratorStep(iteratorRecord). + let next = iterator_record.step(context)?; + + // b. If next is false, return target. + // c. Let nextItem be ? IteratorValue(next). + let next_item = if let Some(next) = next { + next.value(context)? + } else { + return Ok(target.clone().into()); + }; + + let next_item = if let Some(obj) = next_item.as_object() { + obj + // d. If Type(nextItem) is not Object, then + } else { + // i. Let error be ThrowCompletion(a newly created TypeError object). + let err = context + .throw_type_error("cannot get key and value from primitive item of `iterable`"); + + // ii. Return ? IteratorClose(iteratorRecord, error). + return iterator_record.close(err, context); + }; + + // e. Let k be Get(nextItem, "0"). + // f. IfAbruptCloseIterator(k, iteratorRecord). + let key = match next_item.get(0, context) { + Ok(val) => val, + err => return iterator_record.close(err, context), + }; + + // g. Let v be Get(nextItem, "1"). + // h. IfAbruptCloseIterator(v, iteratorRecord). + let value = match next_item.get(1, context) { + Ok(val) => val, + err => return iterator_record.close(err, context), + }; + + // i. Let status be Call(adder, target, « k, v »). + let status = adder.call(&target.clone().into(), &[key, value], context); + + // j. IfAbruptCloseIterator(status, iteratorRecord). + if status.is_err() { + return iterator_record.close(status, context); + } + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/ordered_map.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/ordered_map.rs new file mode 100644 index 0000000..7b5e60d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/ordered_map.rs @@ -0,0 +1,225 @@ +use crate::{object::JsObject, JsValue}; +use boa_gc::{custom_trace, Finalize, Trace}; +use indexmap::{Equivalent, IndexMap}; +use std::{ + collections::hash_map::RandomState, + fmt::Debug, + hash::{BuildHasher, Hash, Hasher}, +}; + +#[derive(PartialEq, Eq, Clone, Debug)] +enum MapKey { + Key(JsValue), + Empty(usize), // Necessary to ensure empty keys are still unique. +} + +// This ensures that a MapKey::Key(value) hashes to the same as value. The derived PartialEq implementation still holds. +#[allow(clippy::derive_hash_xor_eq)] +impl Hash for MapKey { + fn hash(&self, state: &mut H) { + match self { + MapKey::Key(v) => v.hash(state), + MapKey::Empty(e) => e.hash(state), + } + } +} + +impl Equivalent for JsValue { + fn equivalent(&self, key: &MapKey) -> bool { + match key { + MapKey::Key(v) => v == self, + MapKey::Empty(_) => false, + } + } +} + +/// A structure wrapping `indexmap::IndexMap`. +#[derive(Clone)] +pub struct OrderedMap { + map: IndexMap, S>, + lock: u32, + empty_count: usize, +} + +impl Finalize for OrderedMap {} +unsafe impl Trace for OrderedMap { + custom_trace!(this, { + for (k, v) in this.map.iter() { + if let MapKey::Key(key) = k { + mark(key); + } + mark(v); + } + }); +} + +impl Debug for OrderedMap { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.map.fmt(formatter) + } +} + +impl Default for OrderedMap { + fn default() -> Self { + Self::new() + } +} + +impl OrderedMap { + pub fn new() -> Self { + Self { + map: IndexMap::new(), + lock: 0, + empty_count: 0, + } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + map: IndexMap::with_capacity(capacity), + lock: 0, + empty_count: 0, + } + } + + /// Return the number of key-value pairs in the map, including empty values. + /// + /// Computes in **O(1)** time. + pub fn full_len(&self) -> usize { + self.map.len() + } + + /// Gets the number of key-value pairs in the map, not including empty values. + /// + /// Computes in **O(1)** time. + pub fn len(&self) -> usize { + self.map.len() - self.empty_count + } + + /// Returns true if the map contains no elements. + /// + /// Computes in **O(1)** time. + pub fn is_empty(&self) -> bool { + self.len() == 0 + } + + /// Insert a key-value pair in the map. + /// + /// If an equivalent key already exists in the map: the key remains and + /// retains in its place in the order, its corresponding value is updated + /// with `value` and the older value is returned inside `Some(_)`. + /// + /// If no equivalent key existed in the map: the new key-value pair is + /// inserted, last in order, and `None` is returned. + /// + /// Computes in **O(1)** time (amortized average). + pub fn insert(&mut self, key: JsValue, value: V) -> Option { + self.map.insert(MapKey::Key(key), Some(value)).flatten() + } + + /// Remove the key-value pair equivalent to `key` and return + /// its value. + /// + /// Like `Vec::remove`, the pair is removed by shifting all of the + /// elements that follow it, preserving their relative order. + /// **This perturbs the index of all of those elements!** + /// + /// Return `None` if `key` is not in map. + /// + /// Computes in **O(n)** time (average). + pub fn remove(&mut self, key: &JsValue) -> Option { + if self.lock == 0 { + self.map.shift_remove(key).flatten() + } else if self.map.contains_key(key) { + self.map.insert(MapKey::Empty(self.empty_count), None); + self.empty_count += 1; + self.map.swap_remove(key).flatten() + } else { + None + } + } + + /// Removes all elements from the map and resets the counter of + /// empty entries. + pub fn clear(&mut self) { + self.map.clear(); + self.map.shrink_to_fit(); + self.empty_count = 0; + } + + /// Return a reference to the value stored for `key`, if it is present, + /// else `None`. + /// + /// Computes in **O(1)** time (average). + pub fn get(&self, key: &JsValue) -> Option<&V> { + self.map.get(key).and_then(Option::as_ref) + } + + /// Get a key-value pair by index. + /// + /// Valid indices are `0 <= index < self.full_len()`. + /// + /// Computes in O(1) time. + pub fn get_index(&self, index: usize) -> Option<(&JsValue, &V)> { + if let (MapKey::Key(key), Some(value)) = self.map.get_index(index)? { + Some((key, value)) + } else { + None + } + } + + /// Return an iterator over the key-value pairs of the map, in their order + pub fn iter(&self) -> impl Iterator { + self.map.iter().filter_map(|o| { + if let (MapKey::Key(key), Some(value)) = o { + Some((key, value)) + } else { + None + } + }) + } + + /// Return `true` if an equivalent to `key` exists in the map. + /// + /// Computes in **O(1)** time (average). + pub fn contains_key(&self, key: &JsValue) -> bool { + self.map.contains_key(key) + } + + /// Increases the lock counter and returns a lock object that will decrement the counter when dropped. + /// + /// This allows objects to be removed from the map during iteration without affecting the indexes until the iteration has completed. + pub(crate) fn lock(&mut self, map: JsObject) -> MapLock { + self.lock += 1; + MapLock(map) + } + + /// Decreases the lock counter and, if 0, removes all empty entries. + fn unlock(&mut self) { + self.lock -= 1; + if self.lock == 0 { + self.map.retain(|k, _| matches!(k, MapKey::Key(_))); + self.empty_count = 0; + } + } +} + +/// Increases the lock count of the map for the lifetime of the guard. This should not be dropped until iteration has completed. +#[derive(Debug, Trace)] +pub(crate) struct MapLock(JsObject); + +impl Clone for MapLock { + fn clone(&self) -> Self { + let mut map = self.0.borrow_mut(); + let map = map.as_map_mut().expect("MapLock does not point to a map"); + map.lock(self.0.clone()) + } +} + +impl Finalize for MapLock { + fn finalize(&self) { + let mut map = self.0.borrow_mut(); + let map = map.as_map_mut().expect("MapLock does not point to a map"); + map.unlock(); + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/tests.rs new file mode 100644 index 0000000..23469e9 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/map/tests.rs @@ -0,0 +1,406 @@ +use crate::{forward, Context}; + +#[test] +fn construct_empty() { + let mut context = Context::default(); + let init = r#" + var empty = new Map(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "empty.size"); + assert_eq!(result, "0"); +} + +#[test] +fn construct_from_array() { + let mut context = Context::default(); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.size"); + assert_eq!(result, "2"); +} + +#[test] +fn clone() { + let mut context = Context::default(); + let init = r#" + let original = new Map([["1", "one"], ["2", "two"]]); + let clone = new Map(original); + "#; + forward(&mut context, init); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); + let result = forward( + &mut context, + r#" + original.set("3", "three"); + original.size"#, + ); + assert_eq!(result, "3"); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); +} + +#[test] +fn symbol_iterator() { + let mut context = Context::default(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const iterator = map1[Symbol.iterator](); + let item1 = iterator.next(); + let item2 = iterator.next(); + let item3 = iterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item1.value[0]"); + assert_eq!(result, "\"0\""); + let result = forward(&mut context, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item2.value[0]"); + assert_eq!(result, "1"); + let result = forward(&mut context, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +// Should behave the same as symbol_iterator +#[test] +fn entries() { + let mut context = Context::default(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const entriesIterator = map1.entries(); + let item1 = entriesIterator.next(); + let item2 = entriesIterator.next(); + let item3 = entriesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item1.value[0]"); + assert_eq!(result, "\"0\""); + let result = forward(&mut context, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item2.value[0]"); + assert_eq!(result, "1"); + let result = forward(&mut context, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn merge() { + let mut context = Context::default(); + let init = r#" + let first = new Map([["1", "one"], ["2", "two"]]); + let second = new Map([["2", "second two"], ["3", "three"]]); + let third = new Map([["4", "four"], ["5", "five"]]); + let merged1 = new Map([...first, ...second]); + let merged2 = new Map([...second, ...third]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "merged1.size"); + assert_eq!(result, "3"); + let result = forward(&mut context, "merged1.get('2')"); + assert_eq!(result, "\"second two\""); + let result = forward(&mut context, "merged2.size"); + assert_eq!(result, "4"); +} + +#[test] +fn get() { + let mut context = Context::default(); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.get('1')"); + assert_eq!(result, "\"one\""); + let result = forward(&mut context, "map.get('2')"); + assert_eq!(result, "\"two\""); + let result = forward(&mut context, "map.get('3')"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "map.get()"); + assert_eq!(result, "undefined"); +} + +#[test] +fn set() { + let mut context = Context::default(); + let init = r#" + let map = new Map(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.set()"); + assert_eq!(result, "Map { undefined → undefined }"); + let result = forward(&mut context, "map.set('1', 'one')"); + assert_eq!(result, "Map { undefined → undefined, \"1\" → \"one\" }"); + let result = forward(&mut context, "map.set('2')"); + assert_eq!( + result, + "Map { undefined → undefined, \"1\" → \"one\", \"2\" → undefined }" + ); +} + +#[test] +fn clear() { + let mut context = Context::default(); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + map.clear(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.size"); + assert_eq!(result, "0"); +} + +#[test] +fn delete() { + let mut context = Context::default(); + let init = r#" + let map = new Map([["1", "one"], ["2", "two"]]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.delete('1')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "map.size"); + assert_eq!(result, "1"); + let result = forward(&mut context, "map.delete('1')"); + assert_eq!(result, "false"); +} + +#[test] +fn has() { + let mut context = Context::default(); + let init = r#" + let map = new Map([["1", "one"]]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.has()"); + assert_eq!(result, "false"); + let result = forward(&mut context, "map.has('1')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "map.has('2')"); + assert_eq!(result, "false"); +} + +#[test] +fn keys() { + let mut context = Context::default(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const keysIterator = map1.keys(); + let item1 = keysIterator.next(); + let item2 = keysIterator.next(); + let item3 = keysIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"0\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "1"); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn for_each() { + let mut context = Context::default(); + let init = r#" + let map = new Map([[1, 5], [2, 10], [3, 15]]); + let valueSum = 0; + let keySum = 0; + let sizeSum = 0; + function callingCallback(value, key, map) { + valueSum += value; + keySum += key; + sizeSum += map.size; + } + map.forEach(callingCallback); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "valueSum"), "30"); + assert_eq!(forward(&mut context, "keySum"), "6"); + assert_eq!(forward(&mut context, "sizeSum"), "9"); +} + +#[test] +fn values() { + let mut context = Context::default(); + let init = r#" + const map1 = new Map(); + map1.set('0', 'foo'); + map1.set(1, 'bar'); + const valuesIterator = map1.values(); + let item1 = valuesIterator.next(); + let item2 = valuesIterator.next(); + let item3 = valuesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn modify_key() { + let mut context = Context::default(); + let init = r#" + let obj = new Object(); + let map = new Map([[obj, "one"]]); + obj.field = "Value"; + "#; + forward(&mut context, init); + let result = forward(&mut context, "map.get(obj)"); + assert_eq!(result, "\"one\""); +} + +#[test] +fn order() { + let mut context = Context::default(); + let init = r#" + let map = new Map([[1, "one"]]); + map.set(2, "two"); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map"); + assert_eq!(result, "Map { 1 → \"one\", 2 → \"two\" }"); + let result = forward(&mut context, "map.set(1, \"five\");map"); + assert_eq!(result, "Map { 1 → \"five\", 2 → \"two\" }"); + let result = forward(&mut context, "map.set();map"); + assert_eq!( + result, + "Map { 1 → \"five\", 2 → \"two\", undefined → undefined }" + ); + let result = forward(&mut context, "map.delete(2);map"); + assert_eq!(result, "Map { 1 → \"five\", undefined → undefined }"); + let result = forward(&mut context, "map.set(2, \"two\");map"); + assert_eq!( + result, + "Map { 1 → \"five\", undefined → undefined, 2 → \"two\" }" + ); +} + +#[test] +fn recursive_display() { + let mut context = Context::default(); + let init = r#" + let map = new Map(); + let array = new Array([map]); + map.set("y", map); + "#; + forward(&mut context, init); + let result = forward(&mut context, "map"); + assert_eq!(result, "Map { \"y\" → Map(1) }"); + let result = forward(&mut context, "map.set(\"z\", array)"); + assert_eq!(result, "Map { \"y\" → Map(2), \"z\" → Array(1) }"); +} + +#[test] +fn not_a_function() { + let mut context = Context::default(); + let init = r" + try { + let map = Map() + } catch(e) { + e.toString() + } + "; + assert_eq!( + forward(&mut context, init), + "\"TypeError: calling a builtin Map constructor without new is forbidden\"" + ); +} + +#[test] +fn for_each_delete() { + let mut context = Context::default(); + let init = r#" + let map = new Map([[0, "a"], [1, "b"], [2, "c"]]); + let result = []; + map.forEach(function(value, key) { + if (key === 0) { + map.delete(0); + map.set(3, "d"); + } + result.push([key, value]); + }) + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "result[0][0]"), "0"); + assert_eq!(forward(&mut context, "result[0][1]"), "\"a\""); + assert_eq!(forward(&mut context, "result[1][0]"), "1"); + assert_eq!(forward(&mut context, "result[1][1]"), "\"b\""); + assert_eq!(forward(&mut context, "result[2][0]"), "2"); + assert_eq!(forward(&mut context, "result[2][1]"), "\"c\""); + assert_eq!(forward(&mut context, "result[3][0]"), "3"); + assert_eq!(forward(&mut context, "result[3][1]"), "\"d\""); +} + +#[test] +fn for_of_delete() { + let mut context = Context::default(); + let init = r#" + let map = new Map([[0, "a"], [1, "b"], [2, "c"]]); + let result = []; + for (a of map) { + if (a[0] === 0) { + map.delete(0); + map.set(3, "d"); + } + result.push([a[0], a[1]]); + } + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "result[0][0]"), "0"); + assert_eq!(forward(&mut context, "result[0][1]"), "\"a\""); + assert_eq!(forward(&mut context, "result[1][0]"), "1"); + assert_eq!(forward(&mut context, "result[1][1]"), "\"b\""); + assert_eq!(forward(&mut context, "result[2][0]"), "2"); + assert_eq!(forward(&mut context, "result[2][1]"), "\"c\""); + assert_eq!(forward(&mut context, "result[3][0]"), "3"); + assert_eq!(forward(&mut context, "result[3][1]"), "\"d\""); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/mod.rs new file mode 100644 index 0000000..f3b919c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/mod.rs @@ -0,0 +1,889 @@ +//! This module implements the global `Math` object. +//! +//! `Math` is a built-in object that has properties and methods for mathematical constants and functions. It’s not a function object. +//! +//! `Math` works with the `Number` type. It doesn't work with `BigInt`. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-math-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math + +use super::JsArgs; +use crate::{ + builtins::BuiltIn, object::ObjectInitializer, property::Attribute, symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +/// Javascript `Math` object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Math; + +impl BuiltIn for Math { + const NAME: &'static str = "Math"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + let string_tag = WellKnownSymbols::to_string_tag(); + ObjectInitializer::new(context) + .property("E", std::f64::consts::E, attribute) + .property("LN10", std::f64::consts::LN_10, attribute) + .property("LN2", std::f64::consts::LN_2, attribute) + .property("LOG10E", std::f64::consts::LOG10_E, attribute) + .property("LOG2E", std::f64::consts::LOG2_E, attribute) + .property("PI", std::f64::consts::PI, attribute) + .property("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2, attribute) + .property("SQRT2", std::f64::consts::SQRT_2, attribute) + .function(Self::abs, "abs", 1) + .function(Self::acos, "acos", 1) + .function(Self::acosh, "acosh", 1) + .function(Self::asin, "asin", 1) + .function(Self::asinh, "asinh", 1) + .function(Self::atan, "atan", 1) + .function(Self::atanh, "atanh", 1) + .function(Self::atan2, "atan2", 2) + .function(Self::cbrt, "cbrt", 1) + .function(Self::ceil, "ceil", 1) + .function(Self::clz32, "clz32", 1) + .function(Self::cos, "cos", 1) + .function(Self::cosh, "cosh", 1) + .function(Self::exp, "exp", 1) + .function(Self::expm1, "expm1", 1) + .function(Self::floor, "floor", 1) + .function(Self::fround, "fround", 1) + .function(Self::hypot, "hypot", 2) + .function(Self::imul, "imul", 2) + .function(Self::log, "log", 1) + .function(Self::log1p, "log1p", 1) + .function(Self::log10, "log10", 1) + .function(Self::log2, "log2", 1) + .function(Self::max, "max", 2) + .function(Self::min, "min", 2) + .function(Self::pow, "pow", 2) + .function(Self::random, "random", 0) + .function(Self::round, "round", 1) + .function(Self::sign, "sign", 1) + .function(Self::sin, "sin", 1) + .function(Self::sinh, "sinh", 1) + .function(Self::sqrt, "sqrt", 1) + .function(Self::tan, "tan", 1) + .function(Self::tanh, "tanh", 1) + .function(Self::trunc, "trunc", 1) + .property( + string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl Math { + /// Get the absolute value of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.abs + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/abs + pub(crate) fn abs(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 3. If n is -0𝔽, return +0𝔽. + // 2. If n is NaN, return NaN. + // 4. If n is -∞𝔽, return +∞𝔽. + // 5. If n < +0𝔽, return -n. + // 6. Return n. + .abs() + .into()) + } + + /// Get the arccos of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.acos + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acos + pub(crate) fn acos(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n > 1𝔽, or n < -1𝔽, return NaN. + // 3. If n is 1𝔽, return +0𝔽. + // 4. Return an implementation-approximated value representing the result of the inverse cosine of ℝ(n). + .acos() + .into()) + } + + /// Get the hyperbolic arccos of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.acosh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/acosh + pub(crate) fn acosh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 4. If n < 1𝔽, return NaN. + // 2. If n is NaN or n is +∞𝔽, return n. + // 3. If n is 1𝔽, return +0𝔽. + // 5. Return an implementation-approximated value representing the result of the inverse hyperbolic cosine of ℝ(n). + .acosh() + .into()) + } + + /// Get the arcsine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.asin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asin + pub(crate) fn asin(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n > 1𝔽 or n < -1𝔽, return NaN. + // 4. Return an implementation-approximated value representing the result of the inverse sine of ℝ(n). + .asin() + .into()) + } + + /// Get the hyperbolic arcsine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.asinh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/asinh + pub(crate) fn asinh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. Return an implementation-approximated value representing the result of the inverse hyperbolic sine of ℝ(n). + .asinh() + .into()) + } + + /// Get the arctangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atan + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan + pub(crate) fn atan(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n is +∞𝔽, return an implementation-approximated value representing π / 2. + // 4. If n is -∞𝔽, return an implementation-approximated value representing -π / 2. + // 5. Return an implementation-approximated value representing the result of the inverse tangent of ℝ(n). + .atan() + .into()) + } + + /// Get the hyperbolic arctangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atanh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atanh + pub(crate) fn atanh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n > 1𝔽 or n < -1𝔽, return NaN. + // 4. If n is 1𝔽, return +∞𝔽. + // 5. If n is -1𝔽, return -∞𝔽. + // 6. Return an implementation-approximated value representing the result of the inverse hyperbolic tangent of ℝ(n). + .atanh() + .into()) + } + + /// Get the four quadrant arctangent of the quotient y / x. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.atan2 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/atan2 + pub(crate) fn atan2(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let ny be ? ToNumber(y). + let y = args.get_or_undefined(0).to_number(context)?; + + // 2. Let nx be ? ToNumber(x). + let x = args.get_or_undefined(1).to_number(context)?; + + // 4. If ny is +∞𝔽, then + // a. If nx is +∞𝔽, return an implementation-approximated value representing π / 4. + // b. If nx is -∞𝔽, return an implementation-approximated value representing 3π / 4. + // c. Return an implementation-approximated value representing π / 2. + // 5. If ny is -∞𝔽, then + // a. If nx is +∞𝔽, return an implementation-approximated value representing -π / 4. + // b. If nx is -∞𝔽, return an implementation-approximated value representing -3π / 4. + // c. Return an implementation-approximated value representing -π / 2. + // 6. If ny is +0𝔽, then + // a. If nx > +0𝔽 or nx is +0𝔽, return +0𝔽. + // b. Return an implementation-approximated value representing π. + // 7. If ny is -0𝔽, then + // a. If nx > +0𝔽 or nx is +0𝔽, return -0𝔽. + // b. Return an implementation-approximated value representing -π. + // 8. Assert: ny is finite and is neither +0𝔽 nor -0𝔽. + // 9. If ny > +0𝔽, then + // a. If nx is +∞𝔽, return +0𝔽. + // b. If nx is -∞𝔽, return an implementation-approximated value representing π. + // c. If nx is +0𝔽 or nx is -0𝔽, return an implementation-approximated value representing π / 2. + // 10. If ny < +0𝔽, then + // a. If nx is +∞𝔽, return -0𝔽. + // b. If nx is -∞𝔽, return an implementation-approximated value representing -π. + // c. If nx is +0𝔽 or nx is -0𝔽, return an implementation-approximated value representing -π / 2. + // 11. Assert: nx is finite and is neither +0𝔽 nor -0𝔽. + // 12. Return an implementation-approximated value representing the result of the inverse tangent of the quotient ℝ(ny) / ℝ(nx). + Ok(y.atan2(x).into()) + } + + /// Get the cubic root of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cbrt + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cbrt + pub(crate) fn cbrt(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. Return an implementation-approximated value representing the result of the cube root of ℝ(n). + .cbrt() + .into()) + } + + /// Get lowest integer above a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.ceil + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/ceil + pub(crate) fn ceil(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. If n < +0𝔽 and n > -1𝔽, return -0𝔽. + // 4. If n is an integral Number, return n. + // 5. Return the smallest (closest to -∞) integral Number value that is not less than n. + .ceil() + .into()) + } + + /// Get the number of leading zeros in the 32 bit representation of a number + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.clz32 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/clz32 + pub(crate) fn clz32(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToUint32(x). + .to_u32(context)? + // 2. Let p be the number of leading zero bits in the unsigned 32-bit binary representation of n. + // 3. Return 𝔽(p). + .leading_zeros() + .into()) + } + + /// Get the cosine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cos + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cos + pub(crate) fn cos(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +∞𝔽, or n is -∞𝔽, return NaN. + // 3. If n is +0𝔽 or n is -0𝔽, return 1𝔽. + // 4. Return an implementation-approximated value representing the result of the cosine of ℝ(n). + .cos() + .into()) + } + + /// Get the hyperbolic cosine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.cosh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/cosh + pub(crate) fn cosh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, return NaN. + // 3. If n is +∞𝔽 or n is -∞𝔽, return +∞𝔽. + // 4. If n is +0𝔽 or n is -0𝔽, return 1𝔽. + // 5. Return an implementation-approximated value representing the result of the hyperbolic cosine of ℝ(n). + .cosh() + .into()) + } + + /// Get the power to raise the natural logarithm to get the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.exp + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/exp + pub(crate) fn exp(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN or n is +∞𝔽, return n. + // 3. If n is +0𝔽 or n is -0𝔽, return 1𝔽. + // 4. If n is -∞𝔽, return +0𝔽. + // 5. Return an implementation-approximated value representing the result of the exponential function of ℝ(n). + .exp() + .into()) + } + + /// The Math.expm1() function returns e^x - 1, where x is the argument, and e the base of + /// the natural logarithms. The result is computed in a way that is accurate even when the + /// value of x is close 0 + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.expm1 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/expm1 + pub(crate) fn expm1(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n. + // 3. If n is -∞𝔽, return -1𝔽. + // 4. Return an implementation-approximated value representing the result of subtracting 1 from the exponential function of ℝ(n). + .exp_m1() + .into()) + } + + /// Get the highest integer below a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.floor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor + pub(crate) fn floor(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. If n < 1𝔽 and n > +0𝔽, return +0𝔽. + // 4. If n is an integral Number, return n. + // 5. Return the greatest (closest to +∞) integral Number value that is not greater than n. + .floor() + .into()) + } + + /// Get the nearest 32-bit single precision float representation of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.fround + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/fround + pub(crate) fn fround( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let n be ? ToNumber(x). + let x = args.get_or_undefined(0).to_number(context)?; + + // 2. If n is NaN, return NaN. + // 3. If n is one of +0𝔽, -0𝔽, +∞𝔽, or -∞𝔽, return n. + // 4. Let n32 be the result of converting n to a value in IEEE 754-2019 binary32 format using roundTiesToEven mode. + // 5. Let n64 be the result of converting n32 to a value in IEEE 754-2019 binary64 format. + // 6. Return the ECMAScript Number value corresponding to n64. + Ok(f64::from(x as f32).into()) + } + + /// Get an approximation of the square root of the sum of squares of all arguments. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.hypot + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/hypot + pub(crate) fn hypot(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let coerced be a new empty List. + // 2. For each element arg of args, do + // a. Let n be ? ToNumber(arg). + // b. Append n to coerced. + // 3. For each element number of coerced, do + // 5. For each element number of coerced, do + let mut result = 0f64; + for arg in args { + let num = arg.to_number(context)?; + // a. If number is +∞𝔽 or number is -∞𝔽, return +∞𝔽. + // 4. Let onlyZero be true. + // a. If number is NaN, return NaN. + // b. If number is neither +0𝔽 nor -0𝔽, set onlyZero to false. + // 6. If onlyZero is true, return +0𝔽. + // 7. Return an implementation-approximated value representing the square root of the sum of squares of the mathematical values of the elements of coerced. + result = result.hypot(num); + } + + Ok(result.into()) + } + + /// Get the result of the C-like 32-bit multiplication of the two parameters. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.imul + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/imul + pub(crate) fn imul(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let a be ℝ(? ToUint32(x)). + let x = args.get_or_undefined(0).to_u32(context)?; + + // 2. Let b be ℝ(? ToUint32(y)). + let y = args.get_or_undefined(1).to_u32(context)?; + + // 3. Let product be (a × b) modulo 2^32. + // 4. If product ≥ 2^31, return 𝔽(product - 2^32); otherwise return 𝔽(product). + Ok((x.wrapping_mul(y) as i32).into()) + } + + /// Get the natural logarithm of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log + pub(crate) fn log(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN or n is +∞𝔽, return n. + // 3. If n is 1𝔽, return +0𝔽. + // 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽. + // 5. If n < +0𝔽, return NaN. + // 6. Return an implementation-approximated value representing the result of the natural logarithm of ℝ(n). + .ln() + .into()) + } + + /// Get approximation to the natural logarithm of 1 + x. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log1p + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log1p + pub(crate) fn log1p(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n. + // 3. If n is -1𝔽, return -∞𝔽. + // 4. If n < -1𝔽, return NaN. + // 5. Return an implementation-approximated value representing the result of the natural logarithm of 1 + ℝ(n). + .ln_1p() + .into()) + } + + /// Get the base 10 logarithm of the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log10 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log10 + pub(crate) fn log10(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN or n is +∞𝔽, return n. + // 3. If n is 1𝔽, return +0𝔽. + // 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽. + // 5. If n < +0𝔽, return NaN. + // 6. Return an implementation-approximated value representing the result of the base 10 logarithm of ℝ(n). + .log10() + .into()) + } + + /// Get the base 2 logarithm of the number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.log2 + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/log2 + pub(crate) fn log2(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN or n is +∞𝔽, return n. + // 3. If n is 1𝔽, return +0𝔽. + // 4. If n is +0𝔽 or n is -0𝔽, return -∞𝔽. + // 5. If n < +0𝔽, return NaN. + // 6. Return an implementation-approximated value representing the result of the base 2 logarithm of ℝ(n). + .log2() + .into()) + } + + /// Get the maximum of several numbers. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.max + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/max + pub(crate) fn max(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let coerced be a new empty List. + // 2. For each element arg of args, do + // b. Append n to coerced. + // 3. Let highest be -∞𝔽. + let mut highest = f64::NEG_INFINITY; + + // 4. For each element number of coerced, do + for arg in args { + // a. Let n be ? ToNumber(arg). + let num = arg.to_number(context)?; + + highest = if highest.is_nan() { + continue; + } else if num.is_nan() { + // a. If number is NaN, return NaN. + f64::NAN + } else { + match (highest, num) { + // b. When x and y are +0𝔽 -0𝔽, return +0𝔽. + (x, y) if x == 0f64 && y == 0f64 && x.signum() != y.signum() => 0f64, + // c. Otherwise, return the maximum value. + (x, y) => x.max(y), + } + }; + } + // 5. Return highest. + Ok(highest.into()) + } + + /// Get the minimum of several numbers. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.min + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/min + pub(crate) fn min(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let coerced be a new empty List. + // 2. For each element arg of args, do + // b. Append n to coerced. + // 3. Let lowest be +∞𝔽. + let mut lowest = f64::INFINITY; + + // 4. For each element number of coerced, do + for arg in args { + // a. Let n be ? ToNumber(arg). + let num = arg.to_number(context)?; + + lowest = if lowest.is_nan() { + continue; + } else if num.is_nan() { + // a. If number is NaN, return NaN. + f64::NAN + } else { + match (lowest, num) { + // b. When x and y are +0𝔽 -0𝔽, return -0𝔽. + (x, y) if x == 0f64 && y == 0f64 && x.signum() != y.signum() => -0f64, + // c. Otherwise, return the minimum value. + (x, y) => x.min(y), + } + }; + } + // 5. Return lowest. + Ok(lowest.into()) + } + + /// Raise a number to a power. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.pow + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/pow + #[allow(clippy::float_cmp)] + pub(crate) fn pow(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Set base to ? ToNumber(base). + let x = args.get_or_undefined(0).to_number(context)?; + + // 2. Set exponent to ? ToNumber(exponent). + let y = args.get_or_undefined(1).to_number(context)?; + + // 3. If |x| = 1 and the exponent is infinite, return NaN. + if f64::abs(x) == 1f64 && y.is_infinite() { + return Ok(f64::NAN.into()); + } + + // 4. Return ! Number::exponentiate(base, exponent). + Ok(x.powf(y).into()) + } + + /// Generate a random floating-point number between `0` and `1`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.random + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/random + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn random(_: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // NOTE: Each Math.random function created for distinct realms must produce a distinct sequence of values from successive calls. + Ok(rand::random::().into()) + } + + /// Round a number to the nearest integer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.round + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/round + #[allow(clippy::float_cmp)] + pub(crate) fn round(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let num = args + .get_or_undefined(0) + //1. Let n be ? ToNumber(x). + .to_number(context)?; + + //2. If n is NaN, +∞𝔽, -∞𝔽, or an integral Number, return n. + //3. If n < 0.5𝔽 and n > +0𝔽, return +0𝔽. + //4. If n < +0𝔽 and n ≥ -0.5𝔽, return -0𝔽. + //5. Return the integral Number closest to n, preferring the Number closer to +∞ in the case of a tie. + + if num.fract() == -0.5 { + Ok(num.ceil().into()) + } else { + Ok(num.round().into()) + } + } + + /// Get the sign of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sign + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sign + pub(crate) fn sign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let n be ? ToNumber(x). + let n = args.get_or_undefined(0).to_number(context)?; + + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + if n == 0f64 { + return Ok(n.into()); + } + // 3. If n < +0𝔽, return -1𝔽. + // 4. Return 1𝔽. + Ok(n.signum().into()) + } + + /// Get the sine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sin + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sin + pub(crate) fn sin(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n is +∞𝔽 or n is -∞𝔽, return NaN. + // 4. Return an implementation-approximated value representing the result of the sine of ℝ(n). + .sin() + .into()) + } + + /// Get the hyperbolic sine of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sinh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sinh + pub(crate) fn sinh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. Return an implementation-approximated value representing the result of the hyperbolic sine of ℝ(n). + .sinh() + .into()) + } + + /// Get the square root of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.sqrt + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/sqrt + pub(crate) fn sqrt(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, or n is +∞𝔽, return n. + // 3. If n < +0𝔽, return NaN. + // 4. Return an implementation-approximated value representing the result of the square root of ℝ(n). + .sqrt() + .into()) + } + + /// Get the tangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.tan + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tan + pub(crate) fn tan(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n is +∞𝔽, or n is -∞𝔽, return NaN. + // 4. Return an implementation-approximated value representing the result of the tangent of ℝ(n). + .tan() + .into()) + } + + /// Get the hyperbolic tangent of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.tanh + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/tanh + pub(crate) fn tanh(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, or n is -0𝔽, return n. + // 3. If n is +∞𝔽, return 1𝔽. + // 4. If n is -∞𝔽, return -1𝔽. + // 5. Return an implementation-approximated value representing the result of the hyperbolic tangent of ℝ(n). + .tanh() + .into()) + } + + /// Get the integer part of a number. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-math.trunc + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/trunc + pub(crate) fn trunc(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + Ok(args + .get_or_undefined(0) + // 1. Let n be ? ToNumber(x). + .to_number(context)? + // 2. If n is NaN, n is +0𝔽, n is -0𝔽, n is +∞𝔽, or n is -∞𝔽, return n. + // 3. If n < 1𝔽 and n > +0𝔽, return +0𝔽. + // 4. If n < +0𝔽 and n > -1𝔽, return -0𝔽. + // 5. Return the integral Number nearest n in the direction of +0𝔽. + .trunc() + .into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/tests.rs new file mode 100644 index 0000000..d22e971 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/math/tests.rs @@ -0,0 +1,729 @@ +#![allow(clippy::float_cmp)] + +use crate::{forward, forward_val, Context}; +use std::f64; + +#[test] +fn abs() { + let mut context = Context::default(); + let init = r#" + var a = Math.abs(3 - 5); + var b = Math.abs(1.23456 - 7.89012); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 2.0); + assert_eq!(b.to_number(&mut context).unwrap(), 6.655_559_999_999_999_5); +} + +#[test] +fn acos() { + let mut context = Context::default(); + let init = r#" + var a = Math.acos(8 / 10); + var b = Math.acos(5 / 3); + var c = Math.acos(1); + var d = Math.acos(2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward(&mut context, "b"); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward(&mut context, "d"); + + assert_eq!(a.to_number(&mut context).unwrap(), 0.643_501_108_793_284_3); + assert_eq!(b, "NaN"); + assert_eq!(c.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(d, "NaN"); +} + +#[test] +fn acosh() { + let mut context = Context::default(); + let init = r#" + var a = Math.acosh(2); + var b = Math.acosh(-1); + var c = Math.acosh(0.5); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward(&mut context, "b"); + let c = forward(&mut context, "c"); + + assert_eq!(a.to_number(&mut context).unwrap(), 1.316_957_896_924_816_6); + assert_eq!(b, "NaN"); + assert_eq!(c, "NaN"); +} + +#[test] +fn asin() { + let mut context = Context::default(); + let init = r#" + var a = Math.asin(6 / 10); + var b = Math.asin(5 / 3); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward(&mut context, "b"); + + assert_eq!(a.to_number(&mut context).unwrap(), 0.643_501_108_793_284_4); + assert_eq!(b, String::from("NaN")); +} + +#[test] +fn asinh() { + let mut context = Context::default(); + let init = r#" + var a = Math.asinh(1); + var b = Math.asinh(0); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0.881_373_587_019_542_9); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); +} + +#[test] +fn atan() { + let mut context = Context::default(); + let init = r#" + var a = Math.atan(1); + var b = Math.atan(0); + var c = Math.atan(-0); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::FRAC_PI_4); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(c.to_number(&mut context).unwrap(), f64::from(-0)); +} + +#[test] +fn atan2() { + let mut context = Context::default(); + let init = r#" + var a = Math.atan2(90, 15); + var b = Math.atan2(15, 90); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1.405_647_649_380_269_9); + assert_eq!(b.to_number(&mut context).unwrap(), 0.165_148_677_414_626_83); +} + +#[test] +fn cbrt() { + let mut context = Context::default(); + let init = r#" + var a = Math.cbrt(64); + var b = Math.cbrt(-1); + var c = Math.cbrt(1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 4_f64); + assert_eq!(b.to_number(&mut context).unwrap(), -1_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 1_f64); +} + +#[test] +fn ceil() { + let mut context = Context::default(); + let init = r#" + var a = Math.ceil(1.95); + var b = Math.ceil(4); + var c = Math.ceil(-7.004); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 2_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 4_f64); + assert_eq!(c.to_number(&mut context).unwrap(), -7_f64); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn clz32() { + let mut context = Context::default(); + let init = r#" + var a = Math.clz32(); + var b = Math.clz32({}); + var c = Math.clz32(-173); + var d = Math.clz32("1"); + var e = Math.clz32(2147483647); + var f = Math.clz32(Infinity); + var g = Math.clz32(true); + var h = Math.clz32(0); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + let e = forward_val(&mut context, "e").unwrap(); + let f = forward_val(&mut context, "f").unwrap(); + let g = forward_val(&mut context, "g").unwrap(); + let h = forward_val(&mut context, "h").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 32_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 32_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(d.to_number(&mut context).unwrap(), 31_f64); + assert_eq!(e.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(f.to_number(&mut context).unwrap(), 32_f64); + assert_eq!(g.to_number(&mut context).unwrap(), 31_f64); + assert_eq!(h.to_number(&mut context).unwrap(), 32_f64); +} + +#[test] +fn cos() { + let mut context = Context::default(); + let init = r#" + var a = Math.cos(0); + var b = Math.cos(1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 0.540_302_305_868_139_8); +} + +#[test] +fn cosh() { + let mut context = Context::default(); + let init = r#" + var a = Math.cosh(0); + var b = Math.cosh(1); + var c = Math.cosh(-1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 1.543_080_634_815_243_7); + assert_eq!(c.to_number(&mut context).unwrap(), 1.543_080_634_815_243_7); +} + +#[test] +fn exp() { + let mut context = Context::default(); + let init = r#" + var a = Math.exp(0); + var b = Math.exp(-1); + var c = Math.exp(2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 0.367_879_441_171_442_33); + assert_eq!(c.to_number(&mut context).unwrap(), 7.389_056_098_930_65); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn expm1() { + let mut context = Context::default(); + let init = r#" + var a = Math.expm1(); + var b = Math.expm1({}); + var c = Math.expm1(1); + var d = Math.expm1(-1); + var e = Math.expm1(0); + var f = Math.expm1(2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward(&mut context, "a"); + let b = forward(&mut context, "b"); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + let e = forward_val(&mut context, "e").unwrap(); + let f = forward_val(&mut context, "f").unwrap(); + + assert_eq!(a, String::from("NaN")); + assert_eq!(b, String::from("NaN")); + assert!(float_cmp::approx_eq!( + f64, + c.to_number(&mut context).unwrap(), + 1.718_281_828_459_045 + )); + assert!(float_cmp::approx_eq!( + f64, + d.to_number(&mut context).unwrap(), + -0.632_120_558_828_557_7 + )); + assert!(float_cmp::approx_eq!( + f64, + e.to_number(&mut context).unwrap(), + 0_f64 + )); + assert!(float_cmp::approx_eq!( + f64, + f.to_number(&mut context).unwrap(), + 6.389_056_098_930_65 + )); +} + +#[test] +fn floor() { + let mut context = Context::default(); + let init = r#" + var a = Math.floor(1.95); + var b = Math.floor(-3.01); + var c = Math.floor(3.01); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(b.to_number(&mut context).unwrap(), -4_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 3_f64); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn fround() { + let mut context = Context::default(); + let init = r#" + var a = Math.fround(NaN); + var b = Math.fround(Infinity); + var c = Math.fround(5); + var d = Math.fround(5.5); + var e = Math.fround(5.05); + var f = Math.fround(-5.05); + var g = Math.fround(); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward(&mut context, "a"); + let b = forward(&mut context, "b"); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + let e = forward_val(&mut context, "e").unwrap(); + let f = forward_val(&mut context, "f").unwrap(); + let g = forward(&mut context, "g"); + + assert_eq!(a, String::from("NaN")); + assert_eq!(b, String::from("Infinity")); + assert_eq!(c.to_number(&mut context).unwrap(), 5f64); + assert_eq!(d.to_number(&mut context).unwrap(), 5.5f64); + assert_eq!(e.to_number(&mut context).unwrap(), 5.050_000_190_734_863); + assert_eq!(f.to_number(&mut context).unwrap(), -5.050_000_190_734_863); + assert_eq!(g, String::from("NaN")); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn hypot() { + let mut context = Context::default(); + let init = r#" + var a = Math.hypot(); + var b = Math.hypot(3, 4); + var c = Math.hypot(5, 12); + var d = Math.hypot(3, 4, -5); + var e = Math.hypot(4, [5], 6); + var f = Math.hypot(3, -Infinity); + var g = Math.hypot(12); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + let e = forward_val(&mut context, "e").unwrap(); + let f = forward_val(&mut context, "f").unwrap(); + let g = forward_val(&mut context, "g").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0f64); + assert_eq!(b.to_number(&mut context).unwrap(), 5f64); + assert_eq!(c.to_number(&mut context).unwrap(), 13f64); + assert_eq!(d.to_number(&mut context).unwrap(), 7.071_067_811_865_475_5); + assert_eq!(e.to_number(&mut context).unwrap(), 8.774964387392123); + assert!(f.to_number(&mut context).unwrap().is_infinite()); + assert_eq!(g.to_number(&mut context).unwrap(), 12f64); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn imul() { + let mut context = Context::default(); + let init = r#" + var a = Math.imul(3, 4); + var b = Math.imul(-5, 12); + var c = Math.imul(0xffffffff, 5); + var d = Math.imul(0xfffffffe, 5); + var e = Math.imul(12); + var f = Math.imul(); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + let e = forward_val(&mut context, "e").unwrap(); + let f = forward_val(&mut context, "f").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 12f64); + assert_eq!(b.to_number(&mut context).unwrap(), -60f64); + assert_eq!(c.to_number(&mut context).unwrap(), -5f64); + assert_eq!(d.to_number(&mut context).unwrap(), -10f64); + assert_eq!(e.to_number(&mut context).unwrap(), 0f64); + assert_eq!(f.to_number(&mut context).unwrap(), 0f64); +} + +#[test] +fn log() { + let mut context = Context::default(); + let init = r#" + var a = Math.log(1); + var b = Math.log(10); + var c = Math.log(-1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward(&mut context, "c"); + + assert_eq!(a.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(b.to_number(&mut context).unwrap(), f64::consts::LN_10); + assert_eq!(c, String::from("NaN")); +} + +#[test] +#[allow(clippy::many_single_char_names)] +fn log1p() { + let mut context = Context::default(); + let init = r#" + var a = Math.log1p(1); + var b = Math.log1p(0); + var c = Math.log1p(-0.9999999999999999); + var d = Math.log1p(-1); + var e = Math.log1p(-1.000000000000001); + var f = Math.log1p(-2); + var g = Math.log1p(); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward(&mut context, "d"); + let e = forward(&mut context, "e"); + let f = forward(&mut context, "f"); + let g = forward(&mut context, "g"); + + assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::LN_2); + assert_eq!(b.to_number(&mut context).unwrap(), 0f64); + assert_eq!(c.to_number(&mut context).unwrap(), -36.736_800_569_677_1); + assert_eq!(d, "-Infinity"); + assert_eq!(e, String::from("NaN")); + assert_eq!(f, String::from("NaN")); + assert_eq!(g, String::from("NaN")); +} + +#[test] +fn log10() { + let mut context = Context::default(); + let init = r#" + var a = Math.log10(2); + var b = Math.log10(1); + var c = Math.log10(-2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward(&mut context, "c"); + + assert_eq!(a.to_number(&mut context).unwrap(), f64::consts::LOG10_2); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(c, String::from("NaN")); +} + +#[test] +fn log2() { + let mut context = Context::default(); + let init = r#" + var a = Math.log2(3); + var b = Math.log2(1); + var c = Math.log2(-2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward(&mut context, "c"); + + assert_eq!(a.to_number(&mut context).unwrap(), 1.584_962_500_721_156); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(c, String::from("NaN")); +} + +#[test] +fn max() { + let mut context = Context::default(); + let init = r#" + var a = Math.max(10, 20); + var b = Math.max(-10, -20); + var c = Math.max(-10, 20); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 20_f64); + assert_eq!(b.to_number(&mut context).unwrap(), -10_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 20_f64); +} + +#[test] +fn min() { + let mut context = Context::default(); + let init = r#" + var a = Math.min(10, 20); + var b = Math.min(-10, -20); + var c = Math.min(-10, 20); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 10_f64); + assert_eq!(b.to_number(&mut context).unwrap(), -20_f64); + assert_eq!(c.to_number(&mut context).unwrap(), -10_f64); +} + +#[test] +fn pow() { + let mut context = Context::default(); + let init = r#" + var a = Math.pow(2, 10); + var b = Math.pow(-7, 2); + var c = Math.pow(4, 0.5); + var d = Math.pow(7, -2); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + let d = forward_val(&mut context, "d").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_024_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 49_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 2.0); + assert_eq!(d.to_number(&mut context).unwrap(), 0.020_408_163_265_306_12); +} + +#[test] +fn round() { + let mut context = Context::default(); + let init = r#" + var a = Math.round(20.5); + var b = Math.round(-20.3); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 21.0); + assert_eq!(b.to_number(&mut context).unwrap(), -20.0); +} + +#[test] +fn sign() { + let mut context = Context::default(); + let init = r#" + var a = Math.sign(3); + var b = Math.sign(-3); + var c = Math.sign(0); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(b.to_number(&mut context).unwrap(), -1_f64); + assert_eq!(c.to_number(&mut context).unwrap(), 0_f64); +} + +#[test] +fn sin() { + let mut context = Context::default(); + let init = r#" + var a = Math.sin(0); + var b = Math.sin(1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 0.841_470_984_807_896_5); +} + +#[test] +fn sinh() { + let mut context = Context::default(); + let init = r#" + var a = Math.sinh(0); + var b = Math.sinh(1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 1.175_201_193_643_801_4); +} + +#[test] +fn sqrt() { + let mut context = Context::default(); + let init = r#" + var a = Math.sqrt(0); + var b = Math.sqrt(2); + var c = Math.sqrt(9); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + let c = forward_val(&mut context, "c").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(b.to_number(&mut context).unwrap(), f64::consts::SQRT_2); + assert_eq!(c.to_number(&mut context).unwrap(), 3_f64); +} + +#[test] +fn tan() { + let mut context = Context::default(); + let init = r#" + var a = Math.tan(1.1); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + + assert!(float_cmp::approx_eq!( + f64, + a.to_number(&mut context).unwrap(), + 1.964_759_657_248_652_5 + )); +} + +#[test] +fn tanh() { + let mut context = Context::default(); + let init = r#" + var a = Math.tanh(1); + var b = Math.tanh(0); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 0.761_594_155_955_764_9); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); +} + +#[test] +fn trunc() { + let mut context = Context::default(); + let init = r#" + var a = Math.trunc(13.37); + var b = Math.trunc(0.123); + "#; + + eprintln!("{}", forward(&mut context, init)); + + let a = forward_val(&mut context, "a").unwrap(); + let b = forward_val(&mut context, "b").unwrap(); + + assert_eq!(a.to_number(&mut context).unwrap(), 13_f64); + assert_eq!(b.to_number(&mut context).unwrap(), 0_f64); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/mod.rs new file mode 100644 index 0000000..446a2d7 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/mod.rs @@ -0,0 +1,221 @@ +//! Builtins live here, such as Object, String, Math, etc. + +pub mod array; +pub mod array_buffer; +pub mod async_function; +pub mod bigint; +pub mod boolean; +pub mod dataview; +pub mod date; +pub mod error; +pub mod eval; +pub mod function; +pub mod generator; +pub mod generator_function; +pub mod global_this; +pub mod infinity; +pub mod iterable; +pub mod json; +pub mod map; +pub mod math; +pub mod nan; +pub mod number; +pub mod object; +pub mod promise; +pub mod proxy; +pub mod reflect; +pub mod regexp; +pub mod set; +pub mod string; +pub mod symbol; +pub mod typed_array; +pub mod undefined; + +#[cfg(feature = "console")] +pub mod console; + +#[cfg(feature = "intl")] +pub mod intl; + +pub(crate) use self::{ + array::{array_iterator::ArrayIterator, Array}, + async_function::AsyncFunction, + bigint::BigInt, + boolean::Boolean, + dataview::DataView, + date::Date, + error::{ + AggregateError, Error, EvalError, RangeError, ReferenceError, SyntaxError, TypeError, + UriError, + }, + eval::Eval, + function::BuiltInFunctionObject, + global_this::GlobalThis, + infinity::Infinity, + json::Json, + map::map_iterator::MapIterator, + map::Map, + math::Math, + nan::NaN, + number::Number, + object::for_in_iterator::ForInIterator, + object::Object as BuiltInObjectObject, + promise::Promise, + proxy::Proxy, + reflect::Reflect, + regexp::RegExp, + set::set_iterator::SetIterator, + set::Set, + string::String, + symbol::Symbol, + typed_array::{ + BigInt64Array, BigUint64Array, Float32Array, Float64Array, Int16Array, Int32Array, + Int8Array, Uint16Array, Uint32Array, Uint8Array, Uint8ClampedArray, + }, + undefined::Undefined, +}; + +use crate::{ + builtins::{ + array_buffer::ArrayBuffer, generator::Generator, generator_function::GeneratorFunction, + typed_array::TypedArray, + }, + property::{Attribute, PropertyDescriptor}, + Context, JsValue, +}; + +/// Trait representing a global built-in object such as `Math`, `Object` or +/// `String`. +/// +/// This trait must be implemented for any global built-in accessible from +/// Javascript. +pub(crate) trait BuiltIn { + /// Binding name of the built-in inside the global object. + /// + /// E.g. If you want access the properties of a `Complex` built-in + /// with the name `Cplx` you must assign `"Cplx"` to this constant, + /// making any property inside it accessible from Javascript as `Cplx.prop` + const NAME: &'static str; + + /// Property attribute flags of the built-in. + /// Check [Attribute] for more information. + const ATTRIBUTE: Attribute = Attribute::WRITABLE + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::CONFIGURABLE); + + /// Initialization code for the built-in. + /// This is where the methods, properties, static methods and the constructor + /// of a built-in must be initialized to be accessible from Javascript. + /// + /// # Note + /// + /// A return value of `None` indicates that the value must not be added as + /// a global attribute for the global object. + fn init(context: &mut Context) -> Option; +} + +/// Utility function that checks if a type implements `BuiltIn` before +/// initializing it as a global built-in. +#[inline] +fn init_builtin(context: &mut Context) { + if let Some(value) = B::init(context) { + let property = PropertyDescriptor::builder() + .value(value) + .writable(B::ATTRIBUTE.writable()) + .enumerable(B::ATTRIBUTE.enumerable()) + .configurable(B::ATTRIBUTE.configurable()) + .build(); + context + .global_bindings_mut() + .insert(B::NAME.into(), property); + } +} + +/// Initializes built-in objects and functions +#[inline] +pub fn init(context: &mut Context) { + macro_rules! globals { + ($( $builtin:ty ),*) => { + $(init_builtin::<$builtin>(context) + );* + } + } + + globals! { + Undefined, + Infinity, + NaN, + GlobalThis, + BuiltInFunctionObject, + BuiltInObjectObject, + Math, + Json, + Array, + Proxy, + ArrayBuffer, + BigInt, + Boolean, + Date, + DataView, + Map, + Number, + Eval, + Set, + String, + RegExp, + TypedArray, + Int8Array, + Uint8Array, + Uint8ClampedArray, + Int16Array, + Uint16Array, + Int32Array, + Uint32Array, + BigInt64Array, + BigUint64Array, + Float32Array, + Float64Array, + Symbol, + Error, + RangeError, + ReferenceError, + TypeError, + SyntaxError, + EvalError, + UriError, + AggregateError, + Reflect, + Generator, + GeneratorFunction, + Promise, + AsyncFunction + }; + + #[cfg(feature = "intl")] + init_builtin::(context); + + #[cfg(feature = "console")] + init_builtin::(context); +} + +pub trait JsArgs { + /// Utility function to `get` a parameter from + /// a `[JsValue]` or default to `JsValue::Undefined` + /// if `get` returns `None`. + /// + /// Call this if you are thinking of calling something similar to + /// `args.get(n).cloned().unwrap_or_default()` or + /// `args.get(n).unwrap_or(&undefined)`. + /// + /// This returns a reference for efficiency, in case + /// you only need to call methods of `JsValue`, so + /// try to minimize calling `clone`. + fn get_or_undefined(&self, index: usize) -> &JsValue; +} + +impl JsArgs for [JsValue] { + fn get_or_undefined(&self, index: usize) -> &JsValue { + const UNDEFINED: &JsValue = &JsValue::Undefined; + self.get(index).unwrap_or(UNDEFINED) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/mod.rs new file mode 100644 index 0000000..7c89361 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/mod.rs @@ -0,0 +1,35 @@ +//! This module implements the global `NaN` property. +//! +//! The global `NaN` is a property of the global object. In other words, +//! it is a variable in global scope. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-value-properties-of-the-global-object-nan +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NaN + +#[cfg(test)] +mod tests; + +use crate::{builtins::BuiltIn, property::Attribute, Context, JsValue}; +use boa_profiler::Profiler; + +/// JavaScript global `NaN` property. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct NaN; + +impl BuiltIn for NaN { + const NAME: &'static str = "NaN"; + + const ATTRIBUTE: Attribute = Attribute::READONLY + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::PERMANENT); + + fn init(_: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + Some(f64::NAN.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/tests.rs new file mode 100644 index 0000000..627d694 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/nan/tests.rs @@ -0,0 +1,10 @@ +use crate::exec; + +#[test] +fn nan_exists_on_global_object_and_evaluates_to_nan_value() { + let scenario = r#" + NaN; + "#; + + assert_eq!(&exec(scenario), "NaN"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/conversions.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/conversions.rs new file mode 100644 index 0000000..e9960cb --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/conversions.rs @@ -0,0 +1,86 @@ +/// Converts a 64-bit floating point number to an `i32` according to the [`ToInt32`][ToInt32] algorithm. +/// +/// [ToInt32]: https://tc39.es/ecma262/#sec-toint32 +#[inline] +#[allow(clippy::float_cmp)] +pub(crate) fn f64_to_int32(number: f64) -> i32 { + const SIGN_MASK: u64 = 0x8000000000000000; + const EXPONENT_MASK: u64 = 0x7FF0000000000000; + const SIGNIFICAND_MASK: u64 = 0x000FFFFFFFFFFFFF; + const HIDDEN_BIT: u64 = 0x0010000000000000; + const PHYSICAL_SIGNIFICAND_SIZE: i32 = 52; // Excludes the hidden bit. + const SIGNIFICAND_SIZE: i32 = 53; + + const EXPONENT_BIAS: i32 = 0x3FF + PHYSICAL_SIGNIFICAND_SIZE; + const DENORMAL_EXPONENT: i32 = -EXPONENT_BIAS + 1; + + #[inline] + fn is_denormal(number: f64) -> bool { + (number.to_bits() & EXPONENT_MASK) == 0 + } + + #[inline] + fn exponent(number: f64) -> i32 { + if is_denormal(number) { + return DENORMAL_EXPONENT; + } + + let d64 = number.to_bits(); + let biased_e = ((d64 & EXPONENT_MASK) >> PHYSICAL_SIGNIFICAND_SIZE) as i32; + + biased_e - EXPONENT_BIAS + } + + #[inline] + fn significand(number: f64) -> u64 { + let d64 = number.to_bits(); + let significand = d64 & SIGNIFICAND_MASK; + + if is_denormal(number) { + significand + } else { + significand + HIDDEN_BIT + } + } + + #[inline] + fn sign(number: f64) -> i64 { + if (number.to_bits() & SIGN_MASK) == 0 { + 1 + } else { + -1 + } + } + + if number.is_finite() && number <= f64::from(i32::MAX) && number >= f64::from(i32::MIN) { + let i = number as i32; + if f64::from(i) == number { + return i; + } + } + + let exponent = exponent(number); + let bits = if exponent < 0 { + if exponent <= -SIGNIFICAND_SIZE { + return 0; + } + + significand(number) >> -exponent + } else { + if exponent > 31 { + return 0; + } + + (significand(number) << exponent) & 0xFFFFFFFF + }; + + (sign(number) * (bits as i64)) as i32 +} + +/// Converts a 64-bit floating point number to an `u32` according to the [`ToUint32`][ToUint32] algorithm. +/// +/// [ToUint32]: https://tc39.es/ecma262/#sec-touint32 +#[inline] +pub(crate) fn f64_to_uint32(number: f64) -> u32 { + f64_to_int32(number) as u32 +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/mod.rs new file mode 100644 index 0000000..e39ade6 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/mod.rs @@ -0,0 +1,1190 @@ +//! This module implements the global `Number` object. +//! +//! The `Number` JavaScript object is a wrapper object allowing you to work with numerical values. +//! A `Number` object is created using the `Number()` constructor. A primitive type object number is created using the `Number()` **function**. +//! +//! The JavaScript `Number` type is double-precision 64-bit binary format IEEE 754 value. In more recent implementations, +//! JavaScript also supports integers with arbitrary precision using the `BigInt` type. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-number-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number + +use crate::{ + builtins::{string::is_trimmable_whitespace, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::Attribute, + value::{AbstractRelation, IntegerOrInfinity, JsValue}, + Context, JsResult, +}; +use boa_profiler::Profiler; +use num_traits::{float::FloatCore, Num}; + +mod conversions; + +pub(crate) use conversions::{f64_to_int32, f64_to_uint32}; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +const BUF_SIZE: usize = 2200; + +/// `Number` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Number; + +impl BuiltIn for Number { + const NAME: &'static str = "Number"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let parse_int = FunctionBuilder::native(context, Self::parse_int) + .name("parseInt") + .length(2) + .constructor(false) + .build(); + + let parse_float = FunctionBuilder::native(context, Self::parse_float) + .name("parseFloat") + .length(1) + .constructor(false) + .build(); + + context.register_global_property( + "parseInt", + parse_int.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + context.register_global_property( + "parseFloat", + parse_float.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ); + + context.register_global_builtin_function("isFinite", 1, Self::global_is_finite); + context.register_global_builtin_function("isNaN", 1, Self::global_is_nan); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().number().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_property("EPSILON", f64::EPSILON, attribute) + .static_property("MAX_SAFE_INTEGER", Self::MAX_SAFE_INTEGER, attribute) + .static_property("MIN_SAFE_INTEGER", Self::MIN_SAFE_INTEGER, attribute) + .static_property("MAX_VALUE", Self::MAX_VALUE, attribute) + .static_property("MIN_VALUE", Self::MIN_VALUE, attribute) + .static_property("NEGATIVE_INFINITY", f64::NEG_INFINITY, attribute) + .static_property("POSITIVE_INFINITY", f64::INFINITY, attribute) + .static_property("NaN", f64::NAN, attribute) + .static_property( + "parseInt", + parse_int, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_property( + "parseFloat", + parse_float, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .static_method(Self::number_is_finite, "isFinite", 1) + .static_method(Self::number_is_nan, "isNaN", 1) + .static_method(Self::is_safe_integer, "isSafeInteger", 1) + .static_method(Self::number_is_integer, "isInteger", 1) + .method(Self::to_exponential, "toExponential", 1) + .method(Self::to_fixed, "toFixed", 1) + .method(Self::to_locale_string, "toLocaleString", 0) + .method(Self::to_precision, "toPrecision", 1) + .method(Self::to_string, "toString", 1) + .method(Self::value_of, "valueOf", 0) + .build() + .conv::() + .pipe(Some) + } +} + +impl Number { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// The `Number.MAX_SAFE_INTEGER` constant represents the maximum safe integer in JavaScript (`2^53 - 1`). + /// + /// /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.max_safe_integer + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER + pub(crate) const MAX_SAFE_INTEGER: f64 = 9_007_199_254_740_991_f64; + + /// The `Number.MIN_SAFE_INTEGER` constant represents the minimum safe integer in JavaScript (`-(253 - 1)`). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.min_safe_integer + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_SAFE_INTEGER + pub(crate) const MIN_SAFE_INTEGER: f64 = -9_007_199_254_740_991_f64; + + /// The `Number.MAX_VALUE` property represents the maximum numeric value representable in JavaScript. + /// + /// The `MAX_VALUE` property has a value of approximately `1.79E+308`, or `2^1024`. + /// Values larger than `MAX_VALUE` are represented as `Infinity`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.max_value + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_VALUE + pub(crate) const MAX_VALUE: f64 = f64::MAX; + + /// The `Number.MIN_VALUE` property represents the smallest positive numeric value representable in JavaScript. + /// + /// The `MIN_VALUE` property is the number closest to `0`, not the most negative number, that JavaScript can represent. + /// It has a value of approximately `5e-324`. Values smaller than `MIN_VALUE` ("underflow values") are converted to `0`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.min_value + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MIN_VALUE + pub(crate) const MIN_VALUE: f64 = f64::MIN_POSITIVE; + + /// `Number( value )` + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let data = match args.get(0) { + Some(value) => value.to_numeric_number(context)?, + None => 0.0, + }; + if new_target.is_undefined() { + return Ok(JsValue::new(data)); + } + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::number, context)?; + let this = JsObject::from_proto_and_data(prototype, ObjectData::number(data)); + Ok(this.into()) + } + + /// This function returns a `JsResult` of the number `Value`. + /// + /// If the `Value` is a `Number` primitive of `Number` object the number is returned. + /// Otherwise an `TypeError` is thrown. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-thisnumbervalue + fn this_number_value(value: &JsValue, context: &mut Context) -> JsResult { + value + .as_number() + .or_else(|| value.as_object().and_then(|obj| obj.borrow().as_number())) + .ok_or_else(|| context.construct_type_error("'this' is not a number")) + } + + /// `Number.prototype.toExponential( [fractionDigits] )` + /// + /// The `toExponential()` method returns a string representing the Number object in exponential notation. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toexponential + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toExponential + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_exponential( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let x be ? thisNumberValue(this value). + let this_num = Self::this_number_value(this, context)?; + let precision = match args.get(0) { + None | Some(JsValue::Undefined) => None, + // 2. Let f be ? ToIntegerOrInfinity(fractionDigits). + Some(n) => Some(n.to_integer_or_infinity(context)?), + }; + // 4. If x is not finite, return ! Number::toString(x). + if !this_num.is_finite() { + return Ok(JsValue::new(Self::to_native_string(this_num))); + } + // Get rid of the '-' sign for -0.0 + let this_num = if this_num == 0. { 0. } else { this_num }; + let this_str_num = match precision { + None => f64_to_exponential(this_num), + Some(IntegerOrInfinity::Integer(precision)) if (0..=100).contains(&precision) => + // 5. If f < 0 or f > 100, throw a RangeError exception. + { + f64_to_exponential_with_precision(this_num, precision as usize) + } + _ => { + return context + .throw_range_error("toExponential() argument must be between 0 and 100") + } + }; + Ok(JsValue::new(this_str_num)) + } + + /// `Number.prototype.toFixed( [digits] )` + /// + /// The `toFixed()` method formats a number using fixed-point notation + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tofixed + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toFixed + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_fixed( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let this_num be ? thisNumberValue(this value). + let this_num = Self::this_number_value(this, context)?; + + // 2. Let f be ? ToIntegerOrInfinity(fractionDigits). + // 3. Assert: If fractionDigits is undefined, then f is 0. + let precision = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + // 4, 5. If f < 0 or f > 100, throw a RangeError exception. + let precision = precision + .as_integer() + .filter(|i| (0..=100).contains(i)) + .ok_or_else(|| { + context.construct_range_error("toFixed() digits argument must be between 0 and 100") + })? as usize; + + // 6. If x is not finite, return ! Number::toString(x). + if !this_num.is_finite() { + Ok(JsValue::new(Self::to_native_string(this_num))) + // 10. If x ≥ 10^21, then let m be ! ToString(𝔽(x)). + } else if this_num >= 1.0e21 { + Ok(JsValue::new(f64_to_exponential(this_num))) + } else { + // Get rid of the '-' sign for -0.0 because of 9. If x < 0, then set s to "-". + let this_num = if this_num == 0_f64 { 0_f64 } else { this_num }; + let this_fixed_num = format!("{this_num:.precision$}"); + Ok(JsValue::new(this_fixed_num)) + } + } + + /// `Number.prototype.toLocaleString( [locales [, options]] )` + /// + /// The `toLocaleString()` method returns a string with a language-sensitive representation of this number. + /// + /// Note that while this technically conforms to the Ecma standard, it does no actual + /// internationalization logic. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tolocalestring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toLocaleString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_locale_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let this_num = Self::this_number_value(this, context)?; + let this_str_num = this_num.to_string(); + Ok(JsValue::new(this_str_num)) + } + + /// `flt_str_to_exp` - used in `to_precision` + /// + /// This function traverses a string representing a number, + /// returning the floored log10 of this number. + /// + fn flt_str_to_exp(flt: &str) -> i32 { + let mut non_zero_encountered = false; + let mut dot_encountered = false; + for (i, c) in flt.chars().enumerate() { + if c == '.' { + if non_zero_encountered { + return (i as i32) - 1; + } + dot_encountered = true; + } else if c != '0' { + if dot_encountered { + return 1 - (i as i32); + } + non_zero_encountered = true; + } + } + (flt.len() as i32) - 1 + } + + /// `round_to_precision` - used in `to_precision` + /// + /// This procedure has two roles: + /// - If there are enough or more than enough digits in the + /// string to show the required precision, the number + /// represented by these digits is rounded using string + /// manipulation. + /// - Else, zeroes are appended to the string. + /// - Additionally, sometimes the exponent was wrongly computed and + /// while up-rounding we find that we need an extra digit. When this + /// happens, we return true so that the calling context can adjust + /// the exponent. The string is kept at an exact length of `precision`. + /// + /// When this procedure returns, `digits` is exactly `precision` long. + /// + fn round_to_precision(digits: &mut String, precision: usize) -> bool { + if digits.len() > precision { + let to_round = digits.split_off(precision); + let mut digit = digits + .pop() + .expect("already checked that length is bigger than precision") + as u8; + if let Some(first) = to_round.chars().next() { + if first > '4' { + digit += 1; + } + } + + if digit as char == ':' { + // ':' is '9' + 1 + // need to propagate the increment backward + let mut replacement = String::from("0"); + let mut propagated = false; + for c in digits.chars().rev() { + let d = match (c, propagated) { + ('0'..='8', false) => (c as u8 + 1) as char, + (_, false) => '0', + (_, true) => c, + }; + replacement.push(d); + if d != '0' { + propagated = true; + } + } + digits.clear(); + let replacement = if propagated { + replacement.as_str() + } else { + digits.push('1'); + &replacement.as_str()[1..] + }; + for c in replacement.chars().rev() { + digits.push(c); + } + !propagated + } else { + digits.push(digit as char); + false + } + } else { + digits.push_str(&"0".repeat(precision - digits.len())); + false + } + } + + /// `Number.prototype.toPrecision( [precision] )` + /// + /// The `toPrecision()` method returns a string representing the Number object to the specified precision. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.toprecision + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toPrecision + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_precision( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let precision = args.get_or_undefined(0); + + // 1 & 6 + let mut this_num = Self::this_number_value(this, context)?; + // 2 + if precision.is_undefined() { + return Self::to_string(this, &[], context); + } + + // 3 + let precision = precision.to_integer_or_infinity(context)?; + + // 4 + if !this_num.is_finite() { + return Self::to_string(this, &[], context); + } + + let precision = match precision { + IntegerOrInfinity::Integer(x) if (1..=100).contains(&x) => x as usize, + _ => { + // 5 + return context.throw_range_error( + "precision must be an integer at least 1 and no greater than 100", + ); + } + }; + let precision_i32 = precision as i32; + + // 7 + let mut prefix = String::new(); // spec: 's' + let mut suffix: String; // spec: 'm' + let mut exponent: i32; // spec: 'e' + + // 8 + if this_num < 0.0 { + prefix.push('-'); + this_num = -this_num; + } + + // 9 + if this_num == 0.0 { + suffix = "0".repeat(precision); + exponent = 0; + // 10 + } else { + // Due to f64 limitations, this part differs a bit from the spec, + // but has the same effect. It manipulates the string constructed + // by `format`: digits with an optional dot between two of them. + suffix = format!("{this_num:.100}"); + + // a: getting an exponent + exponent = Self::flt_str_to_exp(&suffix); + // b: getting relevant digits only + if exponent < 0 { + suffix = suffix.split_off((1 - exponent) as usize); + } else if let Some(n) = suffix.find('.') { + suffix.remove(n); + } + // impl: having exactly `precision` digits in `suffix` + if Self::round_to_precision(&mut suffix, precision) { + exponent += 1; + } + + // c: switching to scientific notation + let great_exp = exponent >= precision_i32; + if exponent < -6 || great_exp { + // ii + if precision > 1 { + suffix.insert(1, '.'); + } + // vi + suffix.push('e'); + // iii + if great_exp { + suffix.push('+'); + } + // iv, v + suffix.push_str(&exponent.to_string()); + + return Ok(JsValue::new(prefix + &suffix)); + } + } + + // 11 + let e_inc = exponent + 1; + if e_inc == precision_i32 { + return Ok(JsValue::new(prefix + &suffix)); + } + + // 12 + if exponent >= 0 { + suffix.insert(e_inc as usize, '.'); + // 13 + } else { + prefix.push('0'); + prefix.push('.'); + prefix.push_str(&"0".repeat(-e_inc as usize)); + } + + // 14 + Ok(JsValue::new(prefix + &suffix)) + } + + // https://golang.org/src/math/nextafter.go + #[inline] + fn next_after(x: f64, y: f64) -> f64 { + if x.is_nan() || y.is_nan() { + f64::NAN + } else if (x - y) == 0. { + x + } else if x == 0.0 { + f64::from_bits(1).copysign(y) + } else if y > x || x > 0.0 { + f64::from_bits(x.to_bits() + 1) + } else { + f64::from_bits(x.to_bits() - 1) + } + } + + // https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/numbers/conversions.cc#1230 + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_native_string_radix(mut value: f64, radix: u8) -> String { + assert!(radix >= 2); + assert!(radix <= 36); + assert!(value.is_finite()); + // assert_ne!(0.0, value); + + // Character array used for conversion. + // Temporary buffer for the result. We start with the decimal point in the + // middle and write to the left for the integer part and to the right for the + // fractional part. 1024 characters for the exponent and 52 for the mantissa + // either way, with additional space for sign, decimal point and string + // termination should be sufficient. + let mut buffer: [u8; BUF_SIZE] = [0; BUF_SIZE]; + let (int_buf, frac_buf) = buffer.split_at_mut(BUF_SIZE / 2); + let mut fraction_cursor = 0; + let negative = value.is_sign_negative(); + if negative { + value = -value; + } + // Split the value into an integer part and a fractional part. + // let mut integer = value.trunc(); + // let mut fraction = value.fract(); + let mut integer = value.floor(); + let mut fraction = value - integer; + + // We only compute fractional digits up to the input double's precision. + let mut delta = 0.5 * (Self::next_after(value, f64::MAX) - value); + delta = Self::next_after(0.0, f64::MAX).max(delta); + assert!(delta > 0.0); + if fraction >= delta { + // Insert decimal point. + frac_buf[fraction_cursor] = b'.'; + fraction_cursor += 1; + loop { + // Shift up by one digit. + fraction *= f64::from(radix); + delta *= f64::from(radix); + // Write digit. + let digit = fraction as u32; + frac_buf[fraction_cursor] = std::char::from_digit(digit, u32::from(radix)) + .expect("radix already checked") + as u8; + fraction_cursor += 1; + // Calculate remainder. + fraction -= f64::from(digit); + // Round to even. + if fraction + delta > 1.0 + && (fraction > 0.5 || (fraction - 0.5).abs() < f64::EPSILON && digit & 1 != 0) + { + loop { + // We need to back trace already written digits in case of carry-over. + fraction_cursor -= 1; + if fraction_cursor == 0 { + // CHECK_EQ('.', buffer[fraction_cursor]); + // Carry over to the integer part. + integer += 1.; + } else { + let c: u8 = frac_buf[fraction_cursor]; + // Reconstruct digit. + let digit_0 = (c as char) + .to_digit(10) + .expect("character was not a valid digit"); + if digit_0 + 1 >= u32::from(radix) { + continue; + } + frac_buf[fraction_cursor] = + std::char::from_digit(digit_0 + 1, u32::from(radix)) + .expect("digit was not a valid number in the given radix") + as u8; + fraction_cursor += 1; + } + break; + } + break; + } + if fraction < delta { + break; + } + } + } + + // Compute integer digits. Fill unrepresented digits with zero. + let mut int_iter = int_buf.iter_mut().enumerate().rev(); + while FloatCore::integer_decode(integer / f64::from(radix)).1 > 0 { + integer /= f64::from(radix); + *int_iter.next().expect("integer buffer exhausted").1 = b'0'; + } + + loop { + let remainder = integer % f64::from(radix); + *int_iter.next().expect("integer buffer exhausted").1 = + std::char::from_digit(remainder as u32, u32::from(radix)) + .expect("remainder not a digit in the given number") as u8; + integer = (integer - remainder) / f64::from(radix); + if integer <= 0f64 { + break; + } + } + // Add sign and terminate string. + if negative { + *int_iter.next().expect("integer buffer exhausted").1 = b'-'; + } + assert!(fraction_cursor < BUF_SIZE); + + let integer_cursor = int_iter.next().expect("integer buffer exhausted").0 + 1; + let fraction_cursor = fraction_cursor + BUF_SIZE / 2; + String::from_utf8_lossy(&buffer[integer_cursor..fraction_cursor]).into() + } + + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_native_string(x: f64) -> String { + let mut buffer = ryu_js::Buffer::new(); + buffer.format(x).to_string() + } + + /// `Number.prototype.toString( [radix] )` + /// + /// The `toString()` method returns a string representing the specified Number object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let x be ? thisNumberValue(this value). + let x = Self::this_number_value(this, context)?; + + let radix = args.get_or_undefined(0); + let radix_number = if radix.is_undefined() { + // 2. If radix is undefined, let radixNumber be 10. + 10 + } else { + // 3. Else, let radixMV be ? ToIntegerOrInfinity(radix). + radix + .to_integer_or_infinity(context)? + .as_integer() + // 4. If radixNumber < 2 or radixNumber > 36, throw a RangeError exception. + .filter(|i| (2..=36).contains(i)) + .ok_or_else(|| { + context.construct_range_error( + "radix must be an integer at least 2 and no greater than 36", + ) + })? + } as u8; + + // 5. If radixNumber = 10, return ! ToString(x). + if radix_number == 10 { + return Ok(JsValue::new(Self::to_native_string(x))); + } + + if x == -0. { + return Ok(JsValue::new("0")); + } else if x.is_nan() { + return Ok(JsValue::new("NaN")); + } else if x.is_infinite() && x.is_sign_positive() { + return Ok(JsValue::new("Infinity")); + } else if x.is_infinite() && x.is_sign_negative() { + return Ok(JsValue::new("-Infinity")); + } + + // This is a Optimization from the v8 source code to print values that can fit in a single character + // Since the actual num_to_string allocates a 2200 bytes buffer for actual conversion + // I am not sure if this part is effective as the v8 equivalent https://chromium.googlesource.com/v8/v8/+/refs/heads/master/src/builtins/number.tq#53 + // // Fast case where the result is a one character string. + // if x.is_sign_positive() && x.fract() == 0.0 && x < radix_number as f64 { + // return Ok(std::char::from_digit(x as u32, radix_number as u32).unwrap().to_string().into()) + // } + + // 6. Return the String representation of this Number value using the radix specified by radixNumber. + Ok(JsValue::new(Self::to_native_string_radix(x, radix_number))) + } + + /// `Number.prototype.toString()` + /// + /// The `valueOf()` method returns the wrapped primitive value of a Number object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/valueOf + pub(crate) fn value_of( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Ok(JsValue::new(Self::this_number_value(this, context)?)) + } + + /// Builtin javascript 'parseInt(str, radix)' function. + /// + /// Parses the given string as an integer using the given radix as a base. + /// + /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string. + /// + /// The radix must be an integer in the range [2, 36] inclusive. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-parseint-string-radix + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseInt + pub(crate) fn parse_int( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let (Some(val), radix) = (args.get(0), args.get_or_undefined(1)) { + // 1. Let inputString be ? ToString(string). + let input_string = val.to_string(context)?; + + // 2. Let S be ! TrimString(inputString, start). + let mut var_s = input_string.trim_start_matches(is_trimmable_whitespace); + + // 3. Let sign be 1. + // 4. If S is not empty and the first code unit of S is the code unit 0x002D (HYPHEN-MINUS), + // set sign to -1. + let sign = if !var_s.is_empty() && var_s.starts_with('\u{002D}') { + -1 + } else { + 1 + }; + + // 5. If S is not empty and the first code unit of S is the code unit 0x002B (PLUS SIGN) or + // the code unit 0x002D (HYPHEN-MINUS), remove the first code unit from S. + if !var_s.is_empty() { + var_s = var_s + .strip_prefix(&['\u{002B}', '\u{002D}'][..]) + .unwrap_or(var_s); + } + + // 6. Let R be ℝ(? ToInt32(radix)). + let mut var_r = radix.to_i32(context)?; + + // 7. Let stripPrefix be true. + let mut strip_prefix = true; + + // 8. If R ≠ 0, then + #[allow(clippy::if_not_else)] + if var_r != 0 { + // a. If R < 2 or R > 36, return NaN. + if !(2..=36).contains(&var_r) { + return Ok(JsValue::nan()); + } + + // b. If R ≠ 16, set stripPrefix to false. + if var_r != 16 { + strip_prefix = false; + } + } else { + // 9. Else, + // a. Set R to 10. + var_r = 10; + } + + // 10. If stripPrefix is true, then + // a. If the length of S is at least 2 and the first two code units of S are either "0x" or "0X", then + // i. Remove the first two code units from S. + // ii. Set R to 16. + if strip_prefix + && var_s.len() >= 2 + && (var_s.starts_with("0x") || var_s.starts_with("0X")) + { + var_s = var_s.split_at(2).1; + + var_r = 16; + } + + // 11. If S contains a code unit that is not a radix-R digit, let end be the index within S of the + // first such code unit; otherwise, let end be the length of S. + let end = if let Some(index) = var_s.find(|c: char| !c.is_digit(var_r as u32)) { + index + } else { + var_s.len() + }; + + // 12. Let Z be the substring of S from 0 to end. + let var_z = var_s.split_at(end).0; + + // 13. If Z is empty, return NaN. + if var_z.is_empty() { + return Ok(JsValue::nan()); + } + + // 14. Let mathInt be the integer value that is represented by Z in radix-R notation, using the + // letters A-Z and a-z for digits with values 10 through 35. (However, if R is 10 and Z contains + // more than 20 significant digits, every significant digit after the 20th may be replaced by a + // 0 digit, at the option of the implementation; and if R is not 2, 4, 8, 10, 16, or 32, then + // mathInt may be an implementation-approximated value representing the integer value that is + // represented by Z in radix-R notation.) + let math_int = u64::from_str_radix(var_z, var_r as u32).map_or_else( + |_| f64::from_str_radix(var_z, var_r as u32).expect("invalid_float_conversion"), + |i| i as f64, + ); + + // 15. If mathInt = 0, then + // a. If sign = -1, return -0𝔽. + // b. Return +0𝔽. + if math_int == 0_f64 { + if sign == -1 { + return Ok(JsValue::new(-0_f64)); + } + + return Ok(JsValue::new(0_f64)); + } + + // 16. Return 𝔽(sign × mathInt). + Ok(JsValue::new(f64::from(sign) * math_int)) + } else { + // Not enough arguments to parseInt. + Ok(JsValue::nan()) + } + } + + /// Builtin javascript 'parseFloat(str)' function. + /// + /// Parses the given string as a floating point value. + /// + /// An argument of type Number (i.e. Integer or Rational) is also accepted in place of string. + /// + /// To improve performance an Integer type Number is returned in place of a Rational if the given + /// string can be parsed and stored as an Integer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-parsefloat-string + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/parseFloat + pub(crate) fn parse_float( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(val) = args.get(0) { + let input_string = val.to_string(context)?; + let s = input_string.trim_start_matches(is_trimmable_whitespace); + let s_prefix_lower = s.chars().take(4).collect::().to_ascii_lowercase(); + + // TODO: write our own lexer to match syntax StrDecimalLiteral + if s.starts_with("Infinity") || s.starts_with("+Infinity") { + Ok(JsValue::new(f64::INFINITY)) + } else if s.starts_with("-Infinity") { + Ok(JsValue::new(f64::NEG_INFINITY)) + } else if s_prefix_lower.starts_with("inf") + || s_prefix_lower.starts_with("+inf") + || s_prefix_lower.starts_with("-inf") + { + // Prevent fast_float from parsing "inf", "+inf" as Infinity and "-inf" as -Infinity + Ok(JsValue::nan()) + } else { + Ok(fast_float::parse_partial::(s).map_or_else( + |_| JsValue::nan(), + |(f, len)| { + if len > 0 { + JsValue::new(f) + } else { + JsValue::nan() + } + }, + )) + } + } else { + // Not enough arguments to parseFloat. + Ok(JsValue::nan()) + } + } + + /// Builtin javascript 'isFinite(number)' function. + /// + /// Converts the argument to a number, throwing a type error if the conversion is invalid. + /// + /// If the number is `NaN`, `+∞`, or `-∞`, `false` is returned. + /// + /// Otherwise true is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isfinite-number + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isFinite + pub(crate) fn global_is_finite( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(value) = args.get(0) { + let number = value.to_number(context)?; + Ok(number.is_finite().into()) + } else { + Ok(false.into()) + } + } + + /// Builtin javascript 'isNaN(number)' function. + /// + /// Converts the argument to a number, throwing a type error if the conversion is invalid. + /// + /// If the number is `NaN`, `true` is returned. + /// + /// Otherwise false is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isnan-number + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/isNaN + pub(crate) fn global_is_nan( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(value) = args.get(0) { + let number = value.to_number(context)?; + Ok(number.is_nan().into()) + } else { + Ok(true.into()) + } + } + + /// `Number.isFinite( number )` + /// + /// Checks if the argument is a number, returning false if it isn't. + /// + /// If the number is `NaN`, `+∞`, or `-∞`, `false` is returned. + /// + /// Otherwise true is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.isfinite + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isFinite + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn number_is_finite( + _: &JsValue, + args: &[JsValue], + _ctx: &mut Context, + ) -> JsResult { + Ok(JsValue::new(if let Some(val) = args.get(0) { + match val { + JsValue::Integer(_) => true, + JsValue::Rational(number) => number.is_finite(), + _ => false, + } + } else { + false + })) + } + + /// `Number.isInteger( number )` + /// + /// Checks if the argument is an integer. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-number.isinteger + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isInteger + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn number_is_integer( + _: &JsValue, + args: &[JsValue], + _ctx: &mut Context, + ) -> JsResult { + Ok(args.get(0).map_or(false, Self::is_integer).into()) + } + + /// `Number.isNaN( number )` + /// + /// Checks if the argument is a number, returning false if it isn't. + /// + /// If the number is `NaN`, `true` is returned. + /// + /// Otherwise false is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isnan-number + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn number_is_nan( + _: &JsValue, + args: &[JsValue], + _ctx: &mut Context, + ) -> JsResult { + Ok(JsValue::new( + if let Some(&JsValue::Rational(number)) = args.get(0) { + number.is_nan() + } else { + false + }, + )) + } + + /// `Number.isSafeInteger( number )` + /// + /// Checks if the argument is an integer, returning false if it isn't. + /// + /// If `abs(number) ≤ MAX_SAFE_INTEGER`, `true` is returned. + /// + /// Otherwise false is returned. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isnan-number + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/isNaN + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn is_safe_integer( + _: &JsValue, + args: &[JsValue], + _ctx: &mut Context, + ) -> JsResult { + Ok(JsValue::new(match args.get(0) { + Some(JsValue::Integer(_)) => true, + Some(JsValue::Rational(number)) if Self::is_float_integer(*number) => { + number.abs() <= Self::MAX_SAFE_INTEGER + } + _ => false, + })) + } + + /// Checks if the argument is a finite integer number value. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isinteger + #[inline] + pub(crate) fn is_integer(val: &JsValue) -> bool { + match val { + JsValue::Integer(_) => true, + JsValue::Rational(number) => Self::is_float_integer(*number), + _ => false, + } + } + + /// Checks if the float argument is an integer. + #[inline] + #[allow(clippy::float_cmp)] + pub(crate) fn is_float_integer(number: f64) -> bool { + number.is_finite() && number.abs().floor() == number.abs() + } + + /// The abstract operation `Number::equal` takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// + #[inline] + #[allow(clippy::float_cmp)] + pub(crate) fn equal(x: f64, y: f64) -> bool { + x == y + } + + /// The abstract operation `Number::sameValue` takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// + #[allow(clippy::float_cmp)] + pub(crate) fn same_value(a: f64, b: f64) -> bool { + if a.is_nan() && b.is_nan() { + return true; + } + a == b && a.signum() == b.signum() + } + + /// The abstract operation `Number::sameValueZero` takes arguments + /// x (a Number) and y (a Number). It performs the following steps when called: + /// + /// + #[inline] + #[allow(clippy::float_cmp)] + pub(crate) fn same_value_zero(x: f64, y: f64) -> bool { + if x.is_nan() && y.is_nan() { + return true; + } + + x == y + } + + #[inline] + #[allow(clippy::float_cmp)] + pub(crate) fn less_than(x: f64, y: f64) -> AbstractRelation { + if x.is_nan() || y.is_nan() { + return AbstractRelation::Undefined; + } + if x == y || x == 0.0 && y == -0.0 || x == -0.0 && y == 0.0 { + return AbstractRelation::False; + } + if x.is_infinite() && x.is_sign_positive() { + return AbstractRelation::False; + } + if y.is_infinite() && y.is_sign_positive() { + return AbstractRelation::True; + } + if x.is_infinite() && x.is_sign_negative() { + return AbstractRelation::True; + } + if y.is_infinite() && y.is_sign_negative() { + return AbstractRelation::False; + } + (x < y).into() + } + + #[inline] + pub(crate) fn not(x: f64) -> i32 { + let x = f64_to_int32(x); + !x + } +} + +/// Helper function that formats a float as a ES6-style exponential number string. +fn f64_to_exponential(n: f64) -> String { + match n.abs() { + x if x >= 1.0 || x == 0.0 => format!("{n:e}").replace('e', "e+"), + _ => format!("{n:e}"), + } +} + +/// Helper function that formats a float as a ES6-style exponential number string with a given precision. +// We can't use the same approach as in `f64_to_exponential` +// because in cases like (0.999).toExponential(0) the result will be 1e0. +// Instead we get the index of 'e', and if the next character is not '-' we insert the plus sign +fn f64_to_exponential_with_precision(n: f64, prec: usize) -> String { + let mut res = format!("{n:.prec$e}"); + let idx = res.find('e').expect("'e' not found in exponential string"); + if res.as_bytes()[idx + 1] != b'-' { + res.insert(idx + 1, '+'); + } + res +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/tests.rs new file mode 100644 index 0000000..f7ea222 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/number/tests.rs @@ -0,0 +1,886 @@ +#![allow(clippy::float_cmp)] + +use crate::{builtins::Number, forward, forward_val, value::AbstractRelation, Context}; + +#[test] +fn integer_number_primitive_to_number_object() { + let mut context = Context::default(); + + let scenario = r#" + (100).toString() === "100" + "#; + + assert_eq!(forward(&mut context, scenario), "true"); +} + +#[test] +fn call_number() { + let mut context = Context::default(); + let init = r#" + var default_zero = Number(); + var int_one = Number(1); + var float_two = Number(2.1); + var str_three = Number('3.2'); + var bool_one = Number(true); + var bool_zero = Number(false); + var invalid_nan = Number("I am not a number"); + var from_exp = Number("2.34e+2"); + "#; + + eprintln!("{}", forward(&mut context, init)); + let default_zero = forward_val(&mut context, "default_zero").unwrap(); + let int_one = forward_val(&mut context, "int_one").unwrap(); + let float_two = forward_val(&mut context, "float_two").unwrap(); + let str_three = forward_val(&mut context, "str_three").unwrap(); + let bool_one = forward_val(&mut context, "bool_one").unwrap(); + let bool_zero = forward_val(&mut context, "bool_zero").unwrap(); + let invalid_nan = forward_val(&mut context, "invalid_nan").unwrap(); + let from_exp = forward_val(&mut context, "from_exp").unwrap(); + + assert_eq!(default_zero.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(int_one.to_number(&mut context).unwrap(), 1_f64); + assert_eq!(float_two.to_number(&mut context).unwrap(), 2.1); + assert_eq!(str_three.to_number(&mut context).unwrap(), 3.2); + assert_eq!(bool_one.to_number(&mut context).unwrap(), 1_f64); + assert!(invalid_nan.to_number(&mut context).unwrap().is_nan()); + assert_eq!(bool_zero.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(from_exp.to_number(&mut context).unwrap(), 234_f64); +} + +#[test] +fn to_exponential() { + let mut context = Context::default(); + let init = r#" + var default_exp = Number().toExponential(); + var int_exp = Number(5).toExponential(); + var float_exp = Number(1.234).toExponential(); + var big_exp = Number(1234).toExponential(); + var nan_exp = Number("I am also not a number").toExponential(); + var noop_exp = Number("1.23e+2").toExponential(); + "#; + + eprintln!("{}", forward(&mut context, init)); + let default_exp = forward(&mut context, "default_exp"); + let int_exp = forward(&mut context, "int_exp"); + let float_exp = forward(&mut context, "float_exp"); + let big_exp = forward(&mut context, "big_exp"); + let nan_exp = forward(&mut context, "nan_exp"); + let noop_exp = forward(&mut context, "noop_exp"); + + assert_eq!(default_exp, "\"0e+0\""); + assert_eq!(int_exp, "\"5e+0\""); + assert_eq!(float_exp, "\"1.234e+0\""); + assert_eq!(big_exp, "\"1.234e+3\""); + assert_eq!(nan_exp, "\"NaN\""); + assert_eq!(noop_exp, "\"1.23e+2\""); +} + +#[test] +fn to_fixed() { + let mut context = Context::default(); + let init = r#" + var default_fixed = Number().toFixed(); + var pos_fixed = Number("3.456e+4").toFixed(); + var neg_fixed = Number("3.456e-4").toFixed(); + var noop_fixed = Number(5).toFixed(); + var nan_fixed = Number("I am not a number").toFixed(); + "#; + + eprintln!("{}", forward(&mut context, init)); + let default_fixed = forward(&mut context, "default_fixed"); + let pos_fixed = forward(&mut context, "pos_fixed"); + let neg_fixed = forward(&mut context, "neg_fixed"); + let noop_fixed = forward(&mut context, "noop_fixed"); + let nan_fixed = forward(&mut context, "nan_fixed"); + + assert_eq!(default_fixed, "\"0\""); + assert_eq!(pos_fixed, "\"34560\""); + assert_eq!(neg_fixed, "\"0\""); + assert_eq!(noop_fixed, "\"5\""); + assert_eq!(nan_fixed, "\"NaN\""); +} + +#[test] +fn to_locale_string() { + let mut context = Context::default(); + let init = r#" + var default_locale = Number().toLocaleString(); + var small_locale = Number(5).toLocaleString(); + var big_locale = Number("345600").toLocaleString(); + var neg_locale = Number(-25).toLocaleString(); + "#; + + // TODO: We don't actually do any locale checking here + // To honor the spec we should print numbers according to user locale. + + eprintln!("{}", forward(&mut context, init)); + let default_locale = forward(&mut context, "default_locale"); + let small_locale = forward(&mut context, "small_locale"); + let big_locale = forward(&mut context, "big_locale"); + let neg_locale = forward(&mut context, "neg_locale"); + + assert_eq!(default_locale, "\"0\""); + assert_eq!(small_locale, "\"5\""); + assert_eq!(big_locale, "\"345600\""); + assert_eq!(neg_locale, "\"-25\""); +} + +#[test] +fn to_precision() { + let mut context = Context::default(); + let init = r#" + var infinity = (1/0).toPrecision(3); + var default_precision = Number().toPrecision(); + var explicit_ud_precision = Number().toPrecision(undefined); + var low_precision = (123456789).toPrecision(1); + var more_precision = (123456789).toPrecision(4); + var exact_precision = (123456789).toPrecision(9); + var over_precision = (123456789).toPrecision(50); + var neg_precision = (-123456789).toPrecision(4); + var neg_exponent = (0.1).toPrecision(4); + var ieee754_limits = (1/3).toPrecision(60); + "#; + + eprintln!("{}", forward(&mut context, init)); + let infinity = forward(&mut context, "infinity"); + let default_precision = forward(&mut context, "default_precision"); + let explicit_ud_precision = forward(&mut context, "explicit_ud_precision"); + let low_precision = forward(&mut context, "low_precision"); + let more_precision = forward(&mut context, "more_precision"); + let exact_precision = forward(&mut context, "exact_precision"); + let over_precision = forward(&mut context, "over_precision"); + let neg_precision = forward(&mut context, "neg_precision"); + let neg_exponent = forward(&mut context, "neg_exponent"); + let ieee754_limits = forward(&mut context, "ieee754_limits"); + + assert_eq!(infinity, String::from("\"Infinity\"")); + assert_eq!(default_precision, String::from("\"0\"")); + assert_eq!(explicit_ud_precision, String::from("\"0\"")); + assert_eq!(low_precision, String::from("\"1e+8\"")); + assert_eq!(more_precision, String::from("\"1.235e+8\"")); + assert_eq!(exact_precision, String::from("\"123456789\"")); + assert_eq!(neg_precision, String::from("\"-1.235e+8\"")); + assert_eq!( + over_precision, + String::from("\"123456789.00000000000000000000000000000000000000000\"") + ); + assert_eq!(neg_exponent, String::from("\"0.1000\"")); + assert_eq!( + ieee754_limits, + String::from("\"0.333333333333333314829616256247390992939472198486328125000000\"") + ); + + let expected = "Uncaught \"RangeError\": \"precision must be an integer at least 1 and no greater than 100\""; + + let range_error_1 = r#"(1).toPrecision(101);"#; + let range_error_2 = r#"(1).toPrecision(0);"#; + let range_error_3 = r#"(1).toPrecision(-2000);"#; + let range_error_4 = r#"(1).toPrecision('%');"#; + + assert_eq!(forward(&mut context, range_error_1), expected); + assert_eq!(forward(&mut context, range_error_2), expected); + assert_eq!(forward(&mut context, range_error_3), expected); + assert_eq!(forward(&mut context, range_error_4), expected); +} + +#[test] +fn to_string() { + let mut context = Context::default(); + + assert_eq!("\"NaN\"", &forward(&mut context, "Number(NaN).toString()")); + assert_eq!( + "\"Infinity\"", + &forward(&mut context, "Number(1/0).toString()") + ); + assert_eq!( + "\"-Infinity\"", + &forward(&mut context, "Number(-1/0).toString()") + ); + assert_eq!("\"0\"", &forward(&mut context, "Number(0).toString()")); + assert_eq!("\"9\"", &forward(&mut context, "Number(9).toString()")); + assert_eq!("\"90\"", &forward(&mut context, "Number(90).toString()")); + assert_eq!( + "\"90.12\"", + &forward(&mut context, "Number(90.12).toString()") + ); + assert_eq!("\"0.1\"", &forward(&mut context, "Number(0.1).toString()")); + assert_eq!( + "\"0.01\"", + &forward(&mut context, "Number(0.01).toString()") + ); + assert_eq!( + "\"0.0123\"", + &forward(&mut context, "Number(0.0123).toString()") + ); + assert_eq!( + "\"0.00001\"", + &forward(&mut context, "Number(0.00001).toString()") + ); + assert_eq!( + "\"0.000001\"", + &forward(&mut context, "Number(0.000001).toString()") + ); + assert_eq!( + "\"NaN\"", + &forward(&mut context, "Number(NaN).toString(16)") + ); + assert_eq!( + "\"Infinity\"", + &forward(&mut context, "Number(1/0).toString(16)") + ); + assert_eq!( + "\"-Infinity\"", + &forward(&mut context, "Number(-1/0).toString(16)") + ); + assert_eq!("\"0\"", &forward(&mut context, "Number(0).toString(16)")); + assert_eq!("\"9\"", &forward(&mut context, "Number(9).toString(16)")); + assert_eq!("\"5a\"", &forward(&mut context, "Number(90).toString(16)")); + assert_eq!( + "\"5a.1eb851eb852\"", + &forward(&mut context, "Number(90.12).toString(16)") + ); + assert_eq!( + "\"0.1999999999999a\"", + &forward(&mut context, "Number(0.1).toString(16)") + ); + assert_eq!( + "\"0.028f5c28f5c28f6\"", + &forward(&mut context, "Number(0.01).toString(16)") + ); + assert_eq!( + "\"0.032617c1bda511a\"", + &forward(&mut context, "Number(0.0123).toString(16)") + ); + assert_eq!( + "\"605f9f6dd18bc8000\"", + &forward(&mut context, "Number(111111111111111111111).toString(16)") + ); + assert_eq!( + "\"3c3bc3a4a2f75c0000\"", + &forward(&mut context, "Number(1111111111111111111111).toString(16)") + ); + assert_eq!( + "\"25a55a46e5da9a00000\"", + &forward(&mut context, "Number(11111111111111111111111).toString(16)") + ); + assert_eq!( + "\"0.0000a7c5ac471b4788\"", + &forward(&mut context, "Number(0.00001).toString(16)") + ); + assert_eq!( + "\"0.000010c6f7a0b5ed8d\"", + &forward(&mut context, "Number(0.000001).toString(16)") + ); + assert_eq!( + "\"0.000001ad7f29abcaf48\"", + &forward(&mut context, "Number(0.0000001).toString(16)") + ); + assert_eq!( + "\"0.000002036565348d256\"", + &forward(&mut context, "Number(0.00000012).toString(16)") + ); + assert_eq!( + "\"0.0000021047ee22aa466\"", + &forward(&mut context, "Number(0.000000123).toString(16)") + ); + assert_eq!( + "\"0.0000002af31dc4611874\"", + &forward(&mut context, "Number(0.00000001).toString(16)") + ); + assert_eq!( + "\"0.000000338a23b87483be\"", + &forward(&mut context, "Number(0.000000012).toString(16)") + ); + assert_eq!( + "\"0.00000034d3fe36aaa0a2\"", + &forward(&mut context, "Number(0.0000000123).toString(16)") + ); + + assert_eq!("\"0\"", &forward(&mut context, "Number(-0).toString(16)")); + assert_eq!("\"-9\"", &forward(&mut context, "Number(-9).toString(16)")); + assert_eq!( + "\"-5a\"", + &forward(&mut context, "Number(-90).toString(16)") + ); + assert_eq!( + "\"-5a.1eb851eb852\"", + &forward(&mut context, "Number(-90.12).toString(16)") + ); + assert_eq!( + "\"-0.1999999999999a\"", + &forward(&mut context, "Number(-0.1).toString(16)") + ); + assert_eq!( + "\"-0.028f5c28f5c28f6\"", + &forward(&mut context, "Number(-0.01).toString(16)") + ); + assert_eq!( + "\"-0.032617c1bda511a\"", + &forward(&mut context, "Number(-0.0123).toString(16)") + ); + assert_eq!( + "\"-605f9f6dd18bc8000\"", + &forward(&mut context, "Number(-111111111111111111111).toString(16)") + ); + assert_eq!( + "\"-3c3bc3a4a2f75c0000\"", + &forward(&mut context, "Number(-1111111111111111111111).toString(16)") + ); + assert_eq!( + "\"-25a55a46e5da9a00000\"", + &forward( + &mut context, + "Number(-11111111111111111111111).toString(16)" + ) + ); + assert_eq!( + "\"-0.0000a7c5ac471b4788\"", + &forward(&mut context, "Number(-0.00001).toString(16)") + ); + assert_eq!( + "\"-0.000010c6f7a0b5ed8d\"", + &forward(&mut context, "Number(-0.000001).toString(16)") + ); + assert_eq!( + "\"-0.000001ad7f29abcaf48\"", + &forward(&mut context, "Number(-0.0000001).toString(16)") + ); + assert_eq!( + "\"-0.000002036565348d256\"", + &forward(&mut context, "Number(-0.00000012).toString(16)") + ); + assert_eq!( + "\"-0.0000021047ee22aa466\"", + &forward(&mut context, "Number(-0.000000123).toString(16)") + ); + assert_eq!( + "\"-0.0000002af31dc4611874\"", + &forward(&mut context, "Number(-0.00000001).toString(16)") + ); + assert_eq!( + "\"-0.000000338a23b87483be\"", + &forward(&mut context, "Number(-0.000000012).toString(16)") + ); + assert_eq!( + "\"-0.00000034d3fe36aaa0a2\"", + &forward(&mut context, "Number(-0.0000000123).toString(16)") + ); +} + +#[test] +fn num_to_string_exponential() { + let mut context = Context::default(); + + assert_eq!("\"0\"", forward(&mut context, "(0).toString()")); + assert_eq!("\"0\"", forward(&mut context, "(-0).toString()")); + assert_eq!( + "\"111111111111111110000\"", + forward(&mut context, "(111111111111111111111).toString()") + ); + assert_eq!( + "\"1.1111111111111111e+21\"", + forward(&mut context, "(1111111111111111111111).toString()") + ); + assert_eq!( + "\"1.1111111111111111e+22\"", + forward(&mut context, "(11111111111111111111111).toString()") + ); + assert_eq!("\"1e-7\"", forward(&mut context, "(0.0000001).toString()")); + assert_eq!( + "\"1.2e-7\"", + forward(&mut context, "(0.00000012).toString()") + ); + assert_eq!( + "\"1.23e-7\"", + forward(&mut context, "(0.000000123).toString()") + ); + assert_eq!("\"1e-8\"", forward(&mut context, "(0.00000001).toString()")); + assert_eq!( + "\"1.2e-8\"", + forward(&mut context, "(0.000000012).toString()") + ); + assert_eq!( + "\"1.23e-8\"", + forward(&mut context, "(0.0000000123).toString()") + ); +} + +#[test] +fn value_of() { + let mut context = Context::default(); + // TODO: In addition to parsing numbers from strings, parse them bare As of October 2019 + // the parser does not understand scientific e.g., Xe+Y or -Xe-Y notation. + let init = r#" + var default_val = Number().valueOf(); + var int_val = Number("123").valueOf(); + var float_val = Number(1.234).valueOf(); + var exp_val = Number("1.2e+4").valueOf() + var neg_val = Number("-1.2e+4").valueOf() + "#; + + eprintln!("{}", forward(&mut context, init)); + let default_val = forward_val(&mut context, "default_val").unwrap(); + let int_val = forward_val(&mut context, "int_val").unwrap(); + let float_val = forward_val(&mut context, "float_val").unwrap(); + let exp_val = forward_val(&mut context, "exp_val").unwrap(); + let neg_val = forward_val(&mut context, "neg_val").unwrap(); + + assert_eq!(default_val.to_number(&mut context).unwrap(), 0_f64); + assert_eq!(int_val.to_number(&mut context).unwrap(), 123_f64); + assert_eq!(float_val.to_number(&mut context).unwrap(), 1.234); + assert_eq!(exp_val.to_number(&mut context).unwrap(), 12_000_f64); + assert_eq!(neg_val.to_number(&mut context).unwrap(), -12_000_f64); +} + +#[test] +fn equal() { + assert!(Number::equal(0.0, 0.0)); + assert!(Number::equal(-0.0, 0.0)); + assert!(Number::equal(0.0, -0.0)); + assert!(!Number::equal(f64::NAN, -0.0)); + assert!(!Number::equal(0.0, f64::NAN)); + + assert!(Number::equal(1.0, 1.0)); +} + +#[test] +fn same_value() { + assert!(Number::same_value(0.0, 0.0)); + assert!(!Number::same_value(-0.0, 0.0)); + assert!(!Number::same_value(0.0, -0.0)); + assert!(!Number::same_value(f64::NAN, -0.0)); + assert!(!Number::same_value(0.0, f64::NAN)); + assert!(Number::equal(1.0, 1.0)); +} + +#[test] +fn less_than() { + assert_eq!( + Number::less_than(f64::NAN, 0.0), + AbstractRelation::Undefined + ); + assert_eq!( + Number::less_than(0.0, f64::NAN), + AbstractRelation::Undefined + ); + assert_eq!( + Number::less_than(f64::NEG_INFINITY, 0.0), + AbstractRelation::True + ); + assert_eq!( + Number::less_than(0.0, f64::NEG_INFINITY), + AbstractRelation::False + ); + assert_eq!( + Number::less_than(f64::INFINITY, 0.0), + AbstractRelation::False + ); + assert_eq!( + Number::less_than(0.0, f64::INFINITY), + AbstractRelation::True + ); +} + +#[test] +fn same_value_zero() { + assert!(Number::same_value_zero(0.0, 0.0)); + assert!(Number::same_value_zero(-0.0, 0.0)); + assert!(Number::same_value_zero(0.0, -0.0)); + assert!(!Number::same_value_zero(f64::NAN, -0.0)); + assert!(!Number::same_value_zero(0.0, f64::NAN)); + assert!(Number::equal(1.0, 1.0)); +} + +#[test] +fn from_bigint() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "Number(0n)"), "0",); + assert_eq!(&forward(&mut context, "Number(100000n)"), "100000",); + assert_eq!(&forward(&mut context, "Number(100000n)"), "100000",); + assert_eq!(&forward(&mut context, "Number(1n << 1240n)"), "Infinity",); +} + +#[test] +fn number_constants() { + let mut context = Context::default(); + + assert!(!forward_val(&mut context, "Number.EPSILON") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.MAX_SAFE_INTEGER") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.MIN_SAFE_INTEGER") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.MAX_VALUE") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.MIN_VALUE") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.NEGATIVE_INFINITY") + .unwrap() + .is_null_or_undefined()); + assert!(!forward_val(&mut context, "Number.POSITIVE_INFINITY") + .unwrap() + .is_null_or_undefined()); +} + +#[test] +fn parse_int_simple() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"6\")"), "6"); +} + +#[test] +fn parse_int_negative() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"-9\")"), "-9"); +} + +#[test] +fn parse_int_already_int() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(100)"), "100"); +} + +#[test] +fn parse_int_float() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(100.5)"), "100"); +} + +#[test] +fn parse_int_float_str() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"100.5\")"), "100"); +} + +#[test] +fn parse_int_inferred_hex() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"0xA\")"), "10"); +} + +/// This test demonstrates that this version of parseInt treats strings starting with 0 to be parsed with +/// a radix 10 if no radix is specified. Some alternative implementations default to a radix of 8. +#[test] +fn parse_int_zero_start() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"018\")"), "18"); +} + +#[test] +fn parse_int_varying_radix() { + let mut context = Context::default(); + + let base_str = "1000"; + + for radix in 2..36 { + let expected = i32::from_str_radix(base_str, radix).unwrap(); + + assert_eq!( + forward(&mut context, &format!("parseInt(\"{base_str}\", {radix} )")), + expected.to_string() + ); + } +} + +#[test] +fn parse_int_negative_varying_radix() { + let mut context = Context::default(); + + let base_str = "-1000"; + + for radix in 2..36 { + let expected = i32::from_str_radix(base_str, radix).unwrap(); + + assert_eq!( + forward(&mut context, &format!("parseInt(\"{base_str}\", {radix} )")), + expected.to_string() + ); + } +} + +#[test] +fn parse_int_malformed_str() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"hello\")"), "NaN"); +} + +#[test] +fn parse_int_undefined() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(undefined)"), "NaN"); +} + +/// Shows that no arguments to parseInt is treated the same as if undefined was +/// passed as the first argument. +#[test] +fn parse_int_no_args() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt()"), "NaN"); +} + +/// Shows that extra arguments to parseInt are ignored. +#[test] +fn parse_int_too_many_args() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseInt(\"100\", 10, 10)"), "100"); +} + +#[test] +fn parse_float_simple() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(\"6.5\")"), "6.5"); +} + +#[test] +fn parse_float_int() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(10)"), "10"); +} + +#[test] +fn parse_float_int_str() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(\"8\")"), "8"); +} + +#[test] +fn parse_float_already_float() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(17.5)"), "17.5"); +} + +#[test] +fn parse_float_negative() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(\"-99.7\")"), "-99.7"); +} + +#[test] +fn parse_float_malformed_str() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(\"hello\")"), "NaN"); +} + +#[test] +fn parse_float_undefined() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(undefined)"), "NaN"); +} + +/// No arguments to parseFloat is treated the same as passing undefined as the first argument. +#[test] +fn parse_float_no_args() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat()"), "NaN"); +} + +/// Shows that the parseFloat function ignores extra arguments. +#[test] +fn parse_float_too_many_args() { + let mut context = Context::default(); + + assert_eq!(&forward(&mut context, "parseFloat(\"100.5\", 10)"), "100.5"); +} + +#[test] +fn global_is_finite() { + let mut context = Context::default(); + + assert_eq!("false", &forward(&mut context, "isFinite(Infinity)")); + assert_eq!("false", &forward(&mut context, "isFinite(NaN)")); + assert_eq!("false", &forward(&mut context, "isFinite(-Infinity)")); + assert_eq!("true", &forward(&mut context, "isFinite(0)")); + assert_eq!("true", &forward(&mut context, "isFinite(2e64)")); + assert_eq!("true", &forward(&mut context, "isFinite(910)")); + assert_eq!("true", &forward(&mut context, "isFinite(null)")); + assert_eq!("true", &forward(&mut context, "isFinite('0')")); + assert_eq!("false", &forward(&mut context, "isFinite()")); +} + +#[test] +fn global_is_nan() { + let mut context = Context::default(); + + assert_eq!("true", &forward(&mut context, "isNaN(NaN)")); + assert_eq!("true", &forward(&mut context, "isNaN('NaN')")); + assert_eq!("true", &forward(&mut context, "isNaN(undefined)")); + assert_eq!("true", &forward(&mut context, "isNaN({})")); + assert_eq!("false", &forward(&mut context, "isNaN(true)")); + assert_eq!("false", &forward(&mut context, "isNaN(null)")); + assert_eq!("false", &forward(&mut context, "isNaN(37)")); + assert_eq!("false", &forward(&mut context, "isNaN('37')")); + assert_eq!("false", &forward(&mut context, "isNaN('37.37')")); + assert_eq!("true", &forward(&mut context, "isNaN('37,5')")); + assert_eq!("true", &forward(&mut context, "isNaN('123ABC')")); + // Incorrect due to ToNumber implementation inconsistencies. + //assert_eq!("false", &forward(&mut context, "isNaN('')")); + //assert_eq!("false", &forward(&mut context, "isNaN(' ')")); + assert_eq!("true", &forward(&mut context, "isNaN('blabla')")); +} + +#[test] +fn number_is_finite() { + let mut context = Context::default(); + + assert_eq!("false", &forward(&mut context, "Number.isFinite(Infinity)")); + assert_eq!("false", &forward(&mut context, "Number.isFinite(NaN)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isFinite(-Infinity)") + ); + assert_eq!("true", &forward(&mut context, "Number.isFinite(0)")); + assert_eq!("true", &forward(&mut context, "Number.isFinite(2e64)")); + assert_eq!("true", &forward(&mut context, "Number.isFinite(910)")); + assert_eq!("false", &forward(&mut context, "Number.isFinite(null)")); + assert_eq!("false", &forward(&mut context, "Number.isFinite('0')")); + assert_eq!("false", &forward(&mut context, "Number.isFinite()")); + assert_eq!("false", &forward(&mut context, "Number.isFinite({})")); + assert_eq!("true", &forward(&mut context, "Number.isFinite(Number(5))")); + assert_eq!( + "false", + &forward(&mut context, "Number.isFinite(new Number(5))") + ); + assert_eq!( + "false", + &forward(&mut context, "Number.isFinite(new Number(NaN))") + ); + assert_eq!( + "false", + &forward(&mut context, "Number.isFinite(BigInt(5))") + ); +} + +#[test] +fn number_is_integer() { + let mut context = Context::default(); + + assert_eq!("true", &forward(&mut context, "Number.isInteger(0)")); + assert_eq!("true", &forward(&mut context, "Number.isInteger(1)")); + assert_eq!("true", &forward(&mut context, "Number.isInteger(-100000)")); + assert_eq!( + "true", + &forward(&mut context, "Number.isInteger(99999999999999999999999)") + ); + assert_eq!("false", &forward(&mut context, "Number.isInteger(0.1)")); + assert_eq!("false", &forward(&mut context, "Number.isInteger(Math.PI)")); + assert_eq!("false", &forward(&mut context, "Number.isInteger(NaN)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isInteger(Infinity)") + ); + assert_eq!( + "false", + &forward(&mut context, "Number.isInteger(-Infinity)") + ); + assert_eq!("false", &forward(&mut context, "Number.isInteger('10')")); + assert_eq!("false", &forward(&mut context, "Number.isInteger(true)")); + assert_eq!("false", &forward(&mut context, "Number.isInteger(false)")); + assert_eq!("false", &forward(&mut context, "Number.isInteger([1])")); + assert_eq!("true", &forward(&mut context, "Number.isInteger(5.0)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isInteger(5.000000000000001)") + ); + assert_eq!( + "true", + &forward(&mut context, "Number.isInteger(5.0000000000000001)") + ); + assert_eq!( + "false", + &forward(&mut context, "Number.isInteger(Number(5.000000000000001))") + ); + assert_eq!( + "true", + &forward(&mut context, "Number.isInteger(Number(5.0000000000000001))") + ); + assert_eq!("false", &forward(&mut context, "Number.isInteger()")); + assert_eq!( + "false", + &forward(&mut context, "Number.isInteger(new Number(5))") + ); +} + +#[test] +fn number_is_nan() { + let mut context = Context::default(); + + assert_eq!("true", &forward(&mut context, "Number.isNaN(NaN)")); + assert_eq!("true", &forward(&mut context, "Number.isNaN(Number.NaN)")); + assert_eq!("true", &forward(&mut context, "Number.isNaN(0 / 0)")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(undefined)")); + assert_eq!("false", &forward(&mut context, "Number.isNaN({})")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(true)")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(null)")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(37)")); + assert_eq!("false", &forward(&mut context, "Number.isNaN('37')")); + assert_eq!("false", &forward(&mut context, "Number.isNaN('37.37')")); + assert_eq!("false", &forward(&mut context, "Number.isNaN('37,5')")); + assert_eq!("false", &forward(&mut context, "Number.isNaN('123ABC')")); + // Incorrect due to ToNumber implementation inconsistencies. + //assert_eq!("false", &forward(&mut context, "Number.isNaN('')")); + //assert_eq!("false", &forward(&mut context, "Number.isNaN(' ')")); + assert_eq!("false", &forward(&mut context, "Number.isNaN('blabla')")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(Number(5))")); + assert_eq!("true", &forward(&mut context, "Number.isNaN(Number(NaN))")); + assert_eq!("false", &forward(&mut context, "Number.isNaN(BigInt(5))")); + assert_eq!( + "false", + &forward(&mut context, "Number.isNaN(new Number(5))") + ); + assert_eq!( + "false", + &forward(&mut context, "Number.isNaN(new Number(NaN))") + ); +} + +#[test] +fn number_is_safe_integer() { + let mut context = Context::default(); + + assert_eq!("true", &forward(&mut context, "Number.isSafeInteger(3)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isSafeInteger(Math.pow(2, 53))") + ); + assert_eq!( + "true", + &forward(&mut context, "Number.isSafeInteger(Math.pow(2, 53) - 1)") + ); + assert_eq!("false", &forward(&mut context, "Number.isSafeInteger(NaN)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isSafeInteger(Infinity)") + ); + assert_eq!("false", &forward(&mut context, "Number.isSafeInteger('3')")); + assert_eq!("false", &forward(&mut context, "Number.isSafeInteger(3.1)")); + assert_eq!("true", &forward(&mut context, "Number.isSafeInteger(3.0)")); + assert_eq!( + "false", + &forward(&mut context, "Number.isSafeInteger(new Number(5))") + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/for_in_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/for_in_iterator.rs new file mode 100644 index 0000000..612daa0 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/for_in_iterator.rs @@ -0,0 +1,152 @@ +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object}, + object::{JsObject, ObjectData}, + property::PropertyDescriptor, + property::PropertyKey, + symbol::WellKnownSymbols, + Context, JsResult, JsString, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; +use rustc_hash::FxHashSet; +use std::collections::VecDeque; + +/// The `ForInIterator` object represents an iteration over some specific object. +/// It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-for-in-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct ForInIterator { + object: JsValue, + visited_keys: FxHashSet, + remaining_keys: VecDeque, + object_was_visited: bool, +} + +impl ForInIterator { + pub(crate) const NAME: &'static str = "ForInIterator"; + + fn new(object: JsValue) -> Self { + Self { + object, + visited_keys: FxHashSet::default(), + remaining_keys: VecDeque::default(), + object_was_visited: false, + } + } + + /// `CreateForInIterator( object )` + /// + /// Creates a new iterator over the given object. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createforiniterator + pub(crate) fn create_for_in_iterator(object: JsValue, context: &Context) -> JsValue { + let for_in_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .for_in_iterator(), + ObjectData::for_in_iterator(Self::new(object)), + ); + for_in_iterator.into() + } + + /// %ForInIteratorPrototype%.next( ) + /// + /// Gets the next result in the object. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%foriniteratorprototype%.next + pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut iterator = this.as_object().map(JsObject::borrow_mut); + let iterator = iterator + .as_mut() + .and_then(|obj| obj.as_for_in_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not a ForInIterator"))?; + let mut object = iterator.object.to_object(context)?; + loop { + if !iterator.object_was_visited { + let keys = object.__own_property_keys__(context)?; + for k in keys { + match k { + PropertyKey::String(ref k) => { + iterator.remaining_keys.push_back(k.clone()); + } + PropertyKey::Index(i) => { + iterator.remaining_keys.push_back(i.to_string().into()); + } + PropertyKey::Symbol(_) => {} + } + } + iterator.object_was_visited = true; + } + while let Some(r) = iterator.remaining_keys.pop_front() { + if !iterator.visited_keys.contains(&r) { + if let Some(desc) = + object.__get_own_property__(&PropertyKey::from(r.clone()), context)? + { + iterator.visited_keys.insert(r.clone()); + if desc.expect_enumerable() { + return Ok(create_iter_result_object( + JsValue::new(r.to_string()), + false, + context, + )); + } + } + } + } + let proto = object.prototype().clone(); + match proto { + Some(o) => { + object = o; + } + _ => { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + } + } + iterator.object = JsValue::new(object.clone()); + iterator.object_was_visited = false; + } + } + + /// Create the `%ArrayIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%foriniteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let for_in_iterator = + JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &for_in_iterator, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("For In Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + for_in_iterator.insert(to_string_tag, to_string_tag_property); + for_in_iterator + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/mod.rs new file mode 100644 index 0000000..891360d --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/mod.rs @@ -0,0 +1,1330 @@ +//! This module implements the global `Object` object. +//! +//! The `Object` class represents one of JavaScript's data types. +//! +//! It is used to store various keyed collections and more complex entities. +//! Objects can be created using the `Object()` constructor or the +//! object initializer / literal syntax. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object + +use super::Array; +use crate::{ + builtins::{map, BuiltIn, JsArgs}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + IntegrityLevel, JsObject, ObjectData, ObjectKind, + }, + property::{Attribute, PropertyDescriptor, PropertyKey, PropertyNameKind}, + symbol::WellKnownSymbols, + value::JsValue, + Context, JsResult, JsString, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +pub mod for_in_iterator; +#[cfg(test)] +mod tests; + +/// The global JavaScript object. +#[derive(Debug, Clone, Copy)] +pub struct Object; + +impl BuiltIn for Object { + const NAME: &'static str = "Object"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let legacy_proto_getter = FunctionBuilder::native(context, Self::legacy_proto_getter) + .name("get __proto__") + .build(); + + let legacy_setter_proto = FunctionBuilder::native(context, Self::legacy_proto_setter) + .name("set __proto__") + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().object().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .inherit(None) + .accessor( + "__proto__", + Some(legacy_proto_getter), + Some(legacy_setter_proto), + Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .method(Self::has_own_property, "hasOwnProperty", 1) + .method(Self::property_is_enumerable, "propertyIsEnumerable", 1) + .method(Self::to_string, "toString", 0) + .method(Self::to_locale_string, "toLocaleString", 0) + .method(Self::value_of, "valueOf", 0) + .method(Self::is_prototype_of, "isPrototypeOf", 1) + .method(Self::legacy_define_getter, "__defineGetter__", 2) + .method(Self::legacy_define_setter, "__defineSetter__", 2) + .method(Self::legacy_lookup_getter, "__lookupGetter__", 1) + .method(Self::legacy_lookup_setter, "__lookupSetter__", 1) + .static_method(Self::create, "create", 2) + .static_method(Self::set_prototype_of, "setPrototypeOf", 2) + .static_method(Self::get_prototype_of, "getPrototypeOf", 1) + .static_method(Self::define_property, "defineProperty", 3) + .static_method(Self::define_properties, "defineProperties", 2) + .static_method(Self::assign, "assign", 2) + .static_method(Self::is, "is", 2) + .static_method(Self::keys, "keys", 1) + .static_method(Self::values, "values", 1) + .static_method(Self::entries, "entries", 1) + .static_method(Self::seal, "seal", 1) + .static_method(Self::is_sealed, "isSealed", 1) + .static_method(Self::freeze, "freeze", 1) + .static_method(Self::is_frozen, "isFrozen", 1) + .static_method(Self::prevent_extensions, "preventExtensions", 1) + .static_method(Self::is_extensible, "isExtensible", 1) + .static_method( + Self::get_own_property_descriptor, + "getOwnPropertyDescriptor", + 2, + ) + .static_method( + Self::get_own_property_descriptors, + "getOwnPropertyDescriptors", + 1, + ) + .static_method(Self::get_own_property_names, "getOwnPropertyNames", 1) + .static_method(Self::get_own_property_symbols, "getOwnPropertySymbols", 1) + .static_method(Self::has_own, "hasOwn", 2) + .static_method(Self::from_entries, "fromEntries", 1) + .build() + .conv::() + .pipe(Some) + } +} + +impl Object { + const LENGTH: usize = 1; + + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if !new_target.is_undefined() { + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::object, context)?; + let object = JsObject::from_proto_and_data(prototype, ObjectData::ordinary()); + return Ok(object.into()); + } + if let Some(arg) = args.get(0) { + if !arg.is_null_or_undefined() { + return Ok(arg.to_object(context)?.into()); + } + } + Ok(context.construct_object().into()) + } + + /// `get Object.prototype.__proto__` + /// + /// The `__proto__` getter function exposes the value of the + /// internal `[[Prototype]]` of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-object.prototype.__proto__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto + pub fn legacy_proto_getter( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let obj = this.to_object(context)?; + + // 2. Return ? O.[[GetPrototypeOf]](). + let proto = obj.__get_prototype_of__(context)?; + + Ok(proto.map_or(JsValue::Null, JsValue::new)) + } + + /// `set Object.prototype.__proto__` + /// + /// The `__proto__` setter allows the `[[Prototype]]` of + /// an object to be mutated. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set-object.prototype.__proto__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/proto + pub fn legacy_proto_setter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. If Type(proto) is neither Object nor Null, return undefined. + let proto = match args.get_or_undefined(0) { + JsValue::Object(proto) => Some(proto.clone()), + JsValue::Null => None, + _ => return Ok(JsValue::undefined()), + }; + + // 3. If Type(O) is not Object, return undefined. + let object = match this { + JsValue::Object(object) => object, + _ => return Ok(JsValue::undefined()), + }; + + // 4. Let status be ? O.[[SetPrototypeOf]](proto). + let status = object.__set_prototype_of__(proto, context)?; + + // 5. If status is false, throw a TypeError exception. + if !status { + return context.throw_type_error("__proto__ called on null or undefined"); + } + + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Object.prototype.__defineGetter__(prop, func)` + /// + /// Binds an object's property to a function to be called when that property is looked up. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.__defineGetter__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineGetter__ + pub fn legacy_define_getter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let getter = args.get_or_undefined(1); + + // 1. Let O be ? ToObject(this value). + let obj = this.to_object(context)?; + + // 2. If IsCallable(getter) is false, throw a TypeError exception. + if !getter.is_callable() { + return context + .throw_type_error("Object.prototype.__defineGetter__: Expecting function"); + } + + // 3. Let desc be PropertyDescriptor { [[Get]]: getter, [[Enumerable]]: true, [[Configurable]]: true }. + let desc = PropertyDescriptor::builder() + .get(getter) + .enumerable(true) + .configurable(true); + + // 4. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(0).to_property_key(context)?; + + // 5. Perform ? DefinePropertyOrThrow(O, key, desc). + obj.define_property_or_throw(key, desc, context)?; + + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Object.prototype.__defineSetter__(prop, func)` + /// + /// Binds an object's property to a function to be called when an attempt is made to set that property. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.__defineSetter__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__defineSetter__ + pub fn legacy_define_setter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let setter = args.get_or_undefined(1); + + // 1. Let O be ? ToObject(this value). + let obj = this.to_object(context)?; + + // 2. If IsCallable(setter) is false, throw a TypeError exception. + if !setter.is_callable() { + return context + .throw_type_error("Object.prototype.__defineSetter__: Expecting function"); + } + + // 3. Let desc be PropertyDescriptor { [[Set]]: setter, [[Enumerable]]: true, [[Configurable]]: true }. + let desc = PropertyDescriptor::builder() + .set(setter) + .enumerable(true) + .configurable(true); + + // 4. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(0).to_property_key(context)?; + + // 5. Perform ? DefinePropertyOrThrow(O, key, desc). + obj.define_property_or_throw(key, desc, context)?; + + // 6. Return undefined. + Ok(JsValue::undefined()) + } + + /// `Object.prototype.__lookupGetter__(prop)` + /// + /// Returns the function bound as a getter to the specified property. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.__lookupGetter__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupGetter__ + pub fn legacy_lookup_getter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let mut obj = this.to_object(context)?; + + // 2. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(0).to_property_key(context)?; + + // 3. Repeat + loop { + // a. Let desc be ? O.[[GetOwnProperty]](key). + let desc = obj.__get_own_property__(&key, context)?; + + // b. If desc is not undefined, then + if let Some(current_desc) = desc { + // i. If IsAccessorDescriptor(desc) is true, return desc.[[Get]]. + return if current_desc.is_accessor_descriptor() { + Ok(current_desc.expect_get().into()) + } else { + // ii. Return undefined. + Ok(JsValue::undefined()) + }; + } + match obj.__get_prototype_of__(context)? { + // c. Set O to ? O.[[GetPrototypeOf]](). + Some(o) => obj = o, + // d. If O is null, return undefined. + None => return Ok(JsValue::undefined()), + } + } + } + /// `Object.prototype.__lookupSetter__(prop)` + /// + /// Returns the function bound as a getter to the specified property. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.__lookupSetter__ + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/__lookupSetter__ + pub fn legacy_lookup_setter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? ToObject(this value). + let mut obj = this.to_object(context)?; + + // 2. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(0).to_property_key(context)?; + + // 3. Repeat + loop { + // a. Let desc be ? O.[[GetOwnProperty]](key). + let desc = obj.__get_own_property__(&key, context)?; + + // b. If desc is not undefined, then + if let Some(current_desc) = desc { + // i. If IsAccessorDescriptor(desc) is true, return desc.[[Set]]. + return if current_desc.is_accessor_descriptor() { + Ok(current_desc.expect_set().into()) + } else { + // ii. Return undefined. + Ok(JsValue::undefined()) + }; + } + match obj.__get_prototype_of__(context)? { + // c. Set O to ? O.[[GetPrototypeOf]](). + Some(o) => obj = o, + // d. If O is null, return undefined. + None => return Ok(JsValue::undefined()), + } + } + } + + /// `Object.create( proto, [propertiesObject] )` + /// + /// Creates a new object from the provided prototype. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.create + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/create + pub fn create(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let prototype = args.get_or_undefined(0); + let properties = args.get_or_undefined(1); + + let obj = match prototype { + JsValue::Object(_) | JsValue::Null => JsObject::from_proto_and_data( + prototype.as_object().cloned(), + ObjectData::ordinary(), + ), + _ => { + return context.throw_type_error(format!( + "Object prototype may only be an Object or null: {}", + prototype.display() + )) + } + }; + + if !properties.is_undefined() { + object_define_properties(&obj, properties, context)?; + return Ok(obj.into()); + } + + Ok(obj.into()) + } + + /// `Object.getOwnPropertyDescriptor( object, property )` + /// + /// Returns an object describing the configuration of a specific property on a given object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.getownpropertydescriptor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptor + pub fn get_own_property_descriptor( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let obj be ? ToObject(O). + let obj = args.get_or_undefined(0).to_object(context)?; + + // 2. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(1).to_property_key(context)?; + + // 3. Let desc be ? obj.[[GetOwnProperty]](key). + let desc = obj.__get_own_property__(&key, context)?; + + // 4. Return FromPropertyDescriptor(desc). + Ok(Self::from_property_descriptor(desc, context)) + } + + /// `Object.getOwnPropertyDescriptors( object )` + /// + /// Returns all own property descriptors of a given object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.getownpropertydescriptors + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyDescriptors + pub fn get_own_property_descriptors( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let obj be ? ToObject(O). + let obj = args.get_or_undefined(0).to_object(context)?; + + // 2. Let ownKeys be ? obj.[[OwnPropertyKeys]](). + let own_keys = obj.__own_property_keys__(context)?; + + // 3. Let descriptors be OrdinaryObjectCreate(%Object.prototype%). + let descriptors = context.construct_object(); + + // 4. For each element key of ownKeys, do + for key in own_keys { + // a. Let desc be ? obj.[[GetOwnProperty]](key). + let desc = obj.__get_own_property__(&key, context)?; + + // b. Let descriptor be FromPropertyDescriptor(desc). + let descriptor = Self::from_property_descriptor(desc, context); + + // c. If descriptor is not undefined, + // perform ! CreateDataPropertyOrThrow(descriptors, key, descriptor). + if !descriptor.is_undefined() { + descriptors + .create_data_property_or_throw(key, descriptor, context) + .expect("should not fail according to spec"); + } + } + + // 5. Return descriptors. + Ok(descriptors.into()) + } + + /// The abstract operation `FromPropertyDescriptor`. + /// + /// [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-frompropertydescriptor + pub(crate) fn from_property_descriptor( + desc: Option, + context: &mut Context, + ) -> JsValue { + match desc { + // 1. If Desc is undefined, return undefined. + None => JsValue::undefined(), + Some(desc) => { + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%). + // 3. Assert: obj is an extensible ordinary object with no own properties. + let obj = context.construct_object(); + + // 4. If Desc has a [[Value]] field, then + if let Some(value) = desc.value() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "value", Desc.[[Value]]). + obj.create_data_property_or_throw("value", value, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 5. If Desc has a [[Writable]] field, then + if let Some(writable) = desc.writable() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "writable", Desc.[[Writable]]). + obj.create_data_property_or_throw("writable", writable, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 6. If Desc has a [[Get]] field, then + if let Some(get) = desc.get() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "get", Desc.[[Get]]). + obj.create_data_property_or_throw("get", get, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 7. If Desc has a [[Set]] field, then + if let Some(set) = desc.set() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "set", Desc.[[Set]]). + obj.create_data_property_or_throw("set", set, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 8. If Desc has an [[Enumerable]] field, then + if let Some(enumerable) = desc.enumerable() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "enumerable", Desc.[[Enumerable]]). + obj.create_data_property_or_throw("enumerable", enumerable, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 9. If Desc has a [[Configurable]] field, then + if let Some(configurable) = desc.configurable() { + // a. Perform ! CreateDataPropertyOrThrow(obj, "configurable", Desc.[[Configurable]]). + obj.create_data_property_or_throw("configurable", configurable, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + } + + // 10. Return obj. + obj.into() + } + } + } + + /// Uses the `SameValue` algorithm to check equality of objects + pub fn is(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult { + let x = args.get_or_undefined(0); + let y = args.get_or_undefined(1); + + Ok(JsValue::same_value(x, y).into()) + } + + /// Get the `prototype` of an object. + /// + /// [More information][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.setprototypeof + pub fn get_prototype_of(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { + if args.is_empty() { + return ctx.throw_type_error( + "Object.getPrototypeOf: At least 1 argument required, but only 0 passed", + ); + } + + // 1. Let obj be ? ToObject(O). + let obj = args[0].clone().to_object(ctx)?; + + // 2. Return ? obj.[[GetPrototypeOf]](). + Ok(obj + .__get_prototype_of__(ctx)? + .map_or(JsValue::Null, JsValue::new)) + } + + /// Set the `prototype` of an object. + /// + /// [More information][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.setprototypeof + pub fn set_prototype_of(_: &JsValue, args: &[JsValue], ctx: &mut Context) -> JsResult { + if args.len() < 2 { + return ctx.throw_type_error(format!( + "Object.setPrototypeOf: At least 2 arguments required, but only {} passed", + args.len() + )); + } + + // 1. Set O to ? RequireObjectCoercible(O). + let o = args + .get(0) + .cloned() + .unwrap_or_default() + .require_object_coercible(ctx)? + .clone(); + + let proto = match args.get_or_undefined(1) { + JsValue::Object(obj) => Some(obj.clone()), + JsValue::Null => None, + // 2. If Type(proto) is neither Object nor Null, throw a TypeError exception. + val => { + return ctx + .throw_type_error(format!("expected an object or null, got {}", val.type_of())) + } + }; + + let obj = if let Some(obj) = o.as_object() { + obj + } else { + // 3. If Type(O) is not Object, return O. + return Ok(o); + }; + + // 4. Let status be ? O.[[SetPrototypeOf]](proto). + let status = obj.__set_prototype_of__(proto, ctx)?; + + // 5. If status is false, throw a TypeError exception. + if !status { + return ctx.throw_type_error("can't set prototype of this object"); + } + + // 6. Return O. + Ok(o) + } + + /// `Object.prototype.isPrototypeOf( proto )` + /// + /// Check whether or not an object exists within another object's prototype chain. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.isprototypeof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isPrototypeOf + pub fn is_prototype_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let v = args.get_or_undefined(0); + if !v.is_object() { + return Ok(JsValue::new(false)); + } + let mut v = v.clone(); + let o = JsValue::new(this.to_object(context)?); + loop { + v = Self::get_prototype_of(this, &[v], context)?; + if v.is_null() { + return Ok(JsValue::new(false)); + } + if JsValue::same_value(&o, &v) { + return Ok(JsValue::new(true)); + } + } + } + + /// Define a property in an object + pub fn define_property( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let object = args.get_or_undefined(0); + if let JsValue::Object(object) = object { + let key = args + .get(1) + .unwrap_or(&JsValue::Undefined) + .to_property_key(context)?; + let desc = args + .get(2) + .unwrap_or(&JsValue::Undefined) + .to_property_descriptor(context)?; + + object.define_property_or_throw(key, desc, context)?; + + Ok(object.clone().into()) + } else { + context.throw_type_error("Object.defineProperty called on non-object") + } + } + + /// `Object.defineProperties( proto, [propertiesObject] )` + /// + /// Creates or update own properties to the object + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.defineproperties + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperties + pub fn define_properties( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let arg = args.get_or_undefined(0); + if let JsValue::Object(obj) = arg { + let props = args.get_or_undefined(1); + object_define_properties(obj, props, context)?; + Ok(arg.clone()) + } else { + context.throw_type_error("Expected an object") + } + } + + /// `Object.prototype.valueOf()` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.valueof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/valueOf + pub fn value_of(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Return ? ToObject(this value). + Ok(this.to_object(context)?.into()) + } + + /// `Object.prototype.toString()` + /// + /// This method returns a string representing the object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toString + #[allow(clippy::wrong_self_convention)] + pub fn to_string(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. If the this value is undefined, return "[object Undefined]". + if this.is_undefined() { + return Ok("[object Undefined]".into()); + } + // 2. If the this value is null, return "[object Null]". + if this.is_null() { + return Ok("[object Null]".into()); + } + // 3. Let O be ! ToObject(this value). + let o = this.to_object(context).expect("toObject cannot fail here"); + + // 4. Let isArray be ? IsArray(O). + // 5. If isArray is true, let builtinTag be "Array". + let builtin_tag = if JsValue::from(o.clone()).is_array(context)? { + "Array" + } else { + // 6. Else if O has a [[ParameterMap]] internal slot, let builtinTag be "Arguments". + // 7. Else if O has a [[Call]] internal method, let builtinTag be "Function". + // 8. Else if O has an [[ErrorData]] internal slot, let builtinTag be "Error". + // 9. Else if O has a [[BooleanData]] internal slot, let builtinTag be "Boolean". + // 10. Else if O has a [[NumberData]] internal slot, let builtinTag be "Number". + // 11. Else if O has a [[StringData]] internal slot, let builtinTag be "String". + // 12. Else if O has a [[DateValue]] internal slot, let builtinTag be "Date". + // 13. Else if O has a [[RegExpMatcher]] internal slot, let builtinTag be "RegExp". + // 14. Else, let builtinTag be "Object". + let o = o.borrow(); + match o.kind() { + ObjectKind::Array => "Array", + ObjectKind::Arguments(_) => "Arguments", + ObjectKind::Function(_) => "Function", + ObjectKind::Error => "Error", + ObjectKind::Boolean(_) => "Boolean", + ObjectKind::Number(_) => "Number", + ObjectKind::String(_) => "String", + ObjectKind::Date(_) => "Date", + ObjectKind::RegExp(_) => "RegExp", + _ => "Object", + } + }; + + // 15. Let tag be ? Get(O, @@toStringTag). + let tag = o.get(WellKnownSymbols::to_string_tag(), context)?; + + // 16. If Type(tag) is not String, set tag to builtinTag. + let tag_str = tag.as_string().map_or(builtin_tag, JsString::as_str); + + // 17. Return the string-concatenation of "[object ", tag, and "]". + Ok(format!("[object {tag_str}]").into()) + } + + /// `Object.prototype.toLocaleString( [ reserved1 [ , reserved2 ] ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.tolocalestring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/toLocaleString + #[allow(clippy::wrong_self_convention)] + pub fn to_locale_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Return ? Invoke(O, "toString"). + this.invoke("toString", &[], context) + } + + /// `Object.prototype.hasOwnProperty( property )` + /// + /// The method returns a boolean indicating whether the object has the specified property + /// as its own property (as opposed to inheriting it). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.hasownproperty + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwnProperty + pub fn has_own_property( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let P be ? ToPropertyKey(V). + let key = args.get_or_undefined(0).to_property_key(context)?; + + // 2. Let O be ? ToObject(this value). + let object = this.to_object(context)?; + + // 3. Return ? HasOwnProperty(O, P). + Ok(object.has_own_property(key, context)?.into()) + } + + /// `Object.prototype.propertyIsEnumerable( property )` + /// + /// This method returns a Boolean indicating whether the specified property is + /// enumerable and is the object's own property. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.prototype.propertyisenumerable + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/propertyIsEnumerable + pub fn property_is_enumerable( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let key = match args.get(0) { + None => return Ok(JsValue::new(false)), + Some(key) => key, + }; + + let key = key.to_property_key(context)?; + let own_prop = this + .to_object(context)? + .__get_own_property__(&key, context)?; + + own_prop + .as_ref() + .and_then(PropertyDescriptor::enumerable) + .unwrap_or_default() + .conv::() + .pipe(Ok) + } + + /// `Object.assign( target, ...sources )` + /// + /// This method copies all enumerable own properties from one or more + /// source objects to a target object. It returns the target object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.assign + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/assign + pub fn assign(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let to be ? ToObject(target). + let to = args.get_or_undefined(0).to_object(context)?; + + // 2. If only one argument was passed, return to. + if args.len() == 1 { + return Ok(to.into()); + } + + // 3. For each element nextSource of sources, do + for source in &args[1..] { + // 3.a. If nextSource is neither undefined nor null, then + if !source.is_null_or_undefined() { + // 3.a.i. Let from be ! ToObject(nextSource). + let from = source + .to_object(context) + .expect("this ToObject call must not fail"); + // 3.a.ii. Let keys be ? from.[[OwnPropertyKeys]](). + let keys = from.__own_property_keys__(context)?; + // 3.a.iii. For each element nextKey of keys, do + for key in keys { + // 3.a.iii.1. Let desc be ? from.[[GetOwnProperty]](nextKey). + if let Some(desc) = from.__get_own_property__(&key, context)? { + // 3.a.iii.2. If desc is not undefined and desc.[[Enumerable]] is true, then + if desc.expect_enumerable() { + // 3.a.iii.2.a. Let propValue be ? Get(from, nextKey). + let property = from.get(key.clone(), context)?; + // 3.a.iii.2.b. Perform ? Set(to, nextKey, propValue, true). + to.set(key, property, true, context)?; + } + } + } + } + } + + // 4. Return to. + Ok(to.into()) + } + + /// `Object.keys( target )` + /// + /// This method returns an array of a given object's own enumerable + /// property names, iterated in the same order that a normal loop would. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.keys + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/keys + pub fn keys(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let obj be ? ToObject(target). + let obj = args + .get(0) + .cloned() + .unwrap_or_default() + .to_object(context)?; + + // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, key). + let name_list = obj.enumerable_own_property_names(PropertyNameKind::Key, context)?; + + // 3. Return CreateArrayFromList(nameList). + let result = Array::create_array_from_list(name_list, context); + + Ok(result.into()) + } + + /// `Object.values( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/values + pub fn values(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let obj be ? ToObject(target). + let obj = args + .get(0) + .cloned() + .unwrap_or_default() + .to_object(context)?; + + // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, value). + let name_list = obj.enumerable_own_property_names(PropertyNameKind::Value, context)?; + + // 3. Return CreateArrayFromList(nameList). + let result = Array::create_array_from_list(name_list, context); + + Ok(result.into()) + } + + /// `Object.entries( target )` + /// + /// This method returns an array of a given object's own enumerable string-keyed property [key, value] pairs. + /// This is the same as iterating with a for...in loop, + /// except that a for...in loop enumerates properties in the prototype chain as well). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/entries + pub fn entries(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let obj be ? ToObject(target). + let obj = args + .get(0) + .cloned() + .unwrap_or_default() + .to_object(context)?; + + // 2. Let nameList be ? EnumerableOwnPropertyNames(obj, key+value). + let name_list = + obj.enumerable_own_property_names(PropertyNameKind::KeyAndValue, context)?; + + // 3. Return CreateArrayFromList(nameList). + let result = Array::create_array_from_list(name_list, context); + + Ok(result.into()) + } + + /// `Object.seal( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.seal + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal + pub fn seal(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let o = args.get_or_undefined(0); + + if let Some(o) = o.as_object() { + // 2. Let status be ? SetIntegrityLevel(O, sealed). + let status = o.set_integrity_level(IntegrityLevel::Sealed, context)?; + // 3. If status is false, throw a TypeError exception. + if !status { + return context.throw_type_error("cannot seal object"); + } + } + // 1. If Type(O) is not Object, return O. + // 4. Return O. + Ok(o.clone()) + } + + /// `Object.isSealed( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.issealed + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isSealed + pub fn is_sealed(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let o = args.get_or_undefined(0); + + // 1. If Type(O) is not Object, return true. + // 2. Return ? TestIntegrityLevel(O, sealed). + if let Some(o) = o.as_object() { + Ok(o.test_integrity_level(IntegrityLevel::Sealed, context)? + .into()) + } else { + Ok(JsValue::new(true)) + } + } + + /// `Object.freeze( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.freeze + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze + pub fn freeze(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let o = args.get_or_undefined(0); + + if let Some(o) = o.as_object() { + // 2. Let status be ? SetIntegrityLevel(O, frozen). + let status = o.set_integrity_level(IntegrityLevel::Frozen, context)?; + // 3. If status is false, throw a TypeError exception. + if !status { + return context.throw_type_error("cannot freeze object"); + } + } + // 1. If Type(O) is not Object, return O. + // 4. Return O. + Ok(o.clone()) + } + + /// `Object.isFrozen( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.isfrozen + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isFrozen + pub fn is_frozen(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let o = args.get_or_undefined(0); + + // 1. If Type(O) is not Object, return true. + // 2. Return ? TestIntegrityLevel(O, frozen). + if let Some(o) = o.as_object() { + Ok(o.test_integrity_level(IntegrityLevel::Frozen, context)? + .into()) + } else { + Ok(JsValue::new(true)) + } + } + + /// `Object.preventExtensions( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.preventextensions + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/preventExtensions + pub fn prevent_extensions( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let o = args.get_or_undefined(0); + + if let Some(o) = o.as_object() { + // 2. Let status be ? O.[[PreventExtensions]](). + let status = o.__prevent_extensions__(context)?; + // 3. If status is false, throw a TypeError exception. + if !status { + return context.throw_type_error("cannot prevent extensions"); + } + } + // 1. If Type(O) is not Object, return O. + // 4. Return O. + Ok(o.clone()) + } + + /// `Object.isExtensible( target )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.isextensible + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/isExtensible + pub fn is_extensible( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let o = args.get_or_undefined(0); + // 1. If Type(O) is not Object, return false. + if let Some(o) = o.as_object() { + // 2. Return ? IsExtensible(O). + Ok(o.is_extensible(context)?.into()) + } else { + Ok(JsValue::new(false)) + } + } + + /// `Object.getOwnPropertyNames( object )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.getownpropertynames + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertyNames + pub fn get_own_property_names( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? GetOwnPropertyKeys(O, string). + let o = args.get_or_undefined(0); + get_own_property_keys(o, PropertyKeyType::String, context) + } + + /// `Object.getOwnPropertySymbols( object )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.getownpropertysymbols + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/getOwnPropertySymbols + pub fn get_own_property_symbols( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? GetOwnPropertyKeys(O, symbol). + let o = args.get_or_undefined(0); + get_own_property_keys(o, PropertyKeyType::Symbol, context) + } + + /// `Object.hasOwn( object, property )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.hasown + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/hasOwn + pub fn has_own(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let obj be ? ToObject(O). + let obj = args.get_or_undefined(0).to_object(context)?; + + // 2. Let key be ? ToPropertyKey(P). + let key = args.get_or_undefined(1).to_property_key(context)?; + + // 3. Return ? HasOwnProperty(obj, key). + Ok(obj.has_own_property(key, context)?.into()) + } + + /// `Object.fromEntries( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-object.fromentries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries + pub fn from_entries(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Perform ? RequireObjectCoercible(iterable). + let iterable = args.get_or_undefined(0).require_object_coercible(context)?; + + // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%). + // 3. Assert: obj is an extensible ordinary object with no own properties. + let obj = context.construct_object(); + + // 4. Let closure be a new Abstract Closure with parameters (key, value) that captures + // obj and performs the following steps when called: + let closure = FunctionBuilder::closure_with_captures( + context, + |_, args, obj, context| { + let key = args.get_or_undefined(0); + let value = args.get_or_undefined(1); + + // a. Let propertyKey be ? ToPropertyKey(key). + let property_key = key.to_property_key(context)?; + + // b. Perform ! CreateDataPropertyOrThrow(obj, propertyKey, value). + obj.create_data_property_or_throw(property_key, value, context)?; + + // c. Return undefined. + Ok(JsValue::undefined()) + }, + obj.clone(), + ); + + // 5. Let adder be ! CreateBuiltinFunction(closure, 2, "", « »). + let adder = closure.length(2).name("").build(); + + // 6. Return ? AddEntriesFromIterable(obj, iterable, adder). + map::add_entries_from_iterable(&obj, iterable, &adder.into(), context) + } +} + +/// The abstract operation `ObjectDefineProperties` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-object.defineproperties +#[inline] +fn object_define_properties( + object: &JsObject, + props: &JsValue, + context: &mut Context, +) -> JsResult<()> { + // 1. Assert: Type(O) is Object. + // 2. Let props be ? ToObject(Properties). + let props = &props.to_object(context)?; + + // 3. Let keys be ? props.[[OwnPropertyKeys]](). + let keys = props.__own_property_keys__(context)?; + + // 4. Let descriptors be a new empty List. + let mut descriptors: Vec<(PropertyKey, PropertyDescriptor)> = Vec::new(); + + // 5. For each element nextKey of keys, do + for next_key in keys { + // a. Let propDesc be ? props.[[GetOwnProperty]](nextKey). + // b. If propDesc is not undefined and propDesc.[[Enumerable]] is true, then + if let Some(prop_desc) = props.__get_own_property__(&next_key, context)? { + if prop_desc.expect_enumerable() { + // i. Let descObj be ? Get(props, nextKey). + let desc_obj = props.get(next_key.clone(), context)?; + + // ii. Let desc be ? ToPropertyDescriptor(descObj). + let desc = desc_obj.to_property_descriptor(context)?; + + // iii. Append the pair (a two element List) consisting of nextKey and desc to the end of descriptors. + descriptors.push((next_key, desc)); + } + } + } + + // 6. For each element pair of descriptors, do + // a. Let P be the first element of pair. + // b. Let desc be the second element of pair. + for (p, d) in descriptors { + // c. Perform ? DefinePropertyOrThrow(O, P, desc). + object.define_property_or_throw(p, d, context)?; + } + + // 7. Return O. + Ok(()) +} + +/// Type enum used in the abstract operation `GetOwnPropertyKeys`. +#[derive(Debug, Copy, Clone)] +enum PropertyKeyType { + String, + Symbol, +} + +/// The abstract operation `GetOwnPropertyKeys`. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-getownpropertykeys +fn get_own_property_keys( + o: &JsValue, + r#type: PropertyKeyType, + context: &mut Context, +) -> JsResult { + // 1. Let obj be ? ToObject(o). + let obj = o.to_object(context)?; + + // 2. Let keys be ? obj.[[OwnPropertyKeys]](). + let keys = obj.__own_property_keys__(context)?; + + // 3. Let nameList be a new empty List. + // 4. For each element nextKey of keys, do + let name_list = keys.iter().filter_map(|next_key| { + // a. If Type(nextKey) is Symbol and type is symbol or Type(nextKey) is String and type is string, then + // i. Append nextKey as the last element of nameList. + match (r#type, &next_key) { + (PropertyKeyType::String, PropertyKey::String(_)) + | (PropertyKeyType::Symbol, PropertyKey::Symbol(_)) => Some(next_key.into()), + (PropertyKeyType::String, PropertyKey::Index(index)) => Some(index.to_string().into()), + _ => None, + } + }); + + // 5. Return CreateArrayFromList(nameList). + Ok(Array::create_array_from_list(name_list, context).into()) +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/tests.rs new file mode 100644 index 0000000..8c41b30 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/object/tests.rs @@ -0,0 +1,422 @@ +use crate::{check_output, forward, Context, JsValue, TestAction}; + +#[test] +fn object_create_with_regular_object() { + let mut context = Context::default(); + + let init = r#" + const foo = { a: 5 }; + const bar = Object.create(foo); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "bar.a"), "5"); + assert_eq!(forward(&mut context, "Object.create.length"), "2"); +} + +#[test] +fn object_create_with_undefined() { + let mut context = Context::default(); + + let init = r#" + try { + const bar = Object.create(); + } catch (err) { + err.toString() + } + "#; + + let result = forward(&mut context, init); + assert_eq!( + result, + "\"TypeError: Object prototype may only be an Object or null: undefined\"" + ); +} + +#[test] +fn object_create_with_number() { + let mut context = Context::default(); + + let init = r#" + try { + const bar = Object.create(5); + } catch (err) { + err.toString() + } + "#; + + let result = forward(&mut context, init); + assert_eq!( + result, + "\"TypeError: Object prototype may only be an Object or null: 5\"" + ); +} + +#[test] +#[ignore] +// TODO: to test on __proto__ somehow. __proto__ getter is not working as expected currently +fn object_create_with_function() { + let mut context = Context::default(); + + let init = r#" + const x = function (){}; + const bar = Object.create(5); + bar.__proto__ + "#; + + let result = forward(&mut context, init); + assert_eq!(result, "...something on __proto__..."); +} + +#[test] +fn object_is() { + let mut context = Context::default(); + + let init = r#" + var foo = { a: 1}; + var bar = { a: 1}; + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "Object.is('foo', 'foo')"), "true"); + assert_eq!(forward(&mut context, "Object.is('foo', 'bar')"), "false"); + assert_eq!(forward(&mut context, "Object.is([], [])"), "false"); + assert_eq!(forward(&mut context, "Object.is(foo, foo)"), "true"); + assert_eq!(forward(&mut context, "Object.is(foo, bar)"), "false"); + assert_eq!(forward(&mut context, "Object.is(null, null)"), "true"); + assert_eq!(forward(&mut context, "Object.is(0, -0)"), "false"); + assert_eq!(forward(&mut context, "Object.is(-0, -0)"), "true"); + assert_eq!(forward(&mut context, "Object.is(NaN, 0/0)"), "true"); + assert_eq!(forward(&mut context, "Object.is()"), "true"); + assert_eq!(forward(&mut context, "Object.is(undefined)"), "true"); + assert!(context.global_object().is_global()); +} + +#[test] +fn object_has_own_property() { + let scenario = r#" + let symA = Symbol('a'); + let symB = Symbol('b'); + + let x = { + undefinedProp: undefined, + nullProp: null, + someProp: 1, + [symA]: 2, + 100: 3, + }; + "#; + + check_output(&[ + TestAction::Execute(scenario), + TestAction::TestEq("x.hasOwnProperty('hasOwnProperty')", "false"), + TestAction::TestEq("x.hasOwnProperty('undefinedProp')", "true"), + TestAction::TestEq("x.hasOwnProperty('nullProp')", "true"), + TestAction::TestEq("x.hasOwnProperty('someProp')", "true"), + TestAction::TestEq("x.hasOwnProperty(symB)", "false"), + TestAction::TestEq("x.hasOwnProperty(symA)", "true"), + TestAction::TestEq("x.hasOwnProperty(1000)", "false"), + TestAction::TestEq("x.hasOwnProperty(100)", "true"), + ]); +} + +#[test] +fn object_has_own() { + let scenario = r#" + let symA = Symbol('a'); + let symB = Symbol('b'); + + let x = { + undefinedProp: undefined, + nullProp: null, + someProp: 1, + [symA]: 2, + 100: 3, + }; + "#; + + check_output(&[ + TestAction::Execute(scenario), + TestAction::TestEq("Object.hasOwn(x, 'hasOwnProperty')", "false"), + TestAction::TestEq("Object.hasOwn(x, 'undefinedProp')", "true"), + TestAction::TestEq("Object.hasOwn(x, 'nullProp')", "true"), + TestAction::TestEq("Object.hasOwn(x, 'someProp')", "true"), + TestAction::TestEq("Object.hasOwn(x, symB)", "false"), + TestAction::TestEq("Object.hasOwn(x, symA)", "true"), + TestAction::TestEq("Object.hasOwn(x, 1000)", "false"), + TestAction::TestEq("Object.hasOwn(x, 100)", "true"), + ]); +} + +#[test] +fn object_property_is_enumerable() { + let mut context = Context::default(); + let init = r#" + let x = { enumerableProp: 'yes' }; + "#; + eprintln!("{}", forward(&mut context, init)); + assert_eq!( + forward(&mut context, r#"x.propertyIsEnumerable('enumerableProp')"#), + "true" + ); + assert_eq!( + forward(&mut context, r#"x.propertyIsEnumerable('hasOwnProperty')"#), + "false" + ); + assert_eq!( + forward(&mut context, r#"x.propertyIsEnumerable('not_here')"#), + "false", + ); + assert_eq!( + forward(&mut context, r#"x.propertyIsEnumerable()"#), + "false", + ); +} + +#[test] +fn object_to_string() { + let mut context = Context::default(); + let init = r#" + let u = undefined; + let n = null; + let a = []; + Array.prototype.toString = Object.prototype.toString; + let f = () => {}; + Function.prototype.toString = Object.prototype.toString; + let e = new Error('test'); + Error.prototype.toString = Object.prototype.toString; + let b = Boolean(); + Boolean.prototype.toString = Object.prototype.toString; + let i = Number(42); + Number.prototype.toString = Object.prototype.toString; + let s = String('boa'); + String.prototype.toString = Object.prototype.toString; + let d = new Date(Date.now()); + Date.prototype.toString = Object.prototype.toString; + let re = /boa/; + RegExp.prototype.toString = Object.prototype.toString; + let o = Object(); + "#; + eprintln!("{}", forward(&mut context, init)); + assert_eq!( + forward(&mut context, "Object.prototype.toString.call(u)"), + "\"[object Undefined]\"" + ); + assert_eq!( + forward(&mut context, "Object.prototype.toString.call(n)"), + "\"[object Null]\"" + ); + assert_eq!(forward(&mut context, "a.toString()"), "\"[object Array]\""); + assert_eq!( + forward(&mut context, "f.toString()"), + "\"[object Function]\"" + ); + assert_eq!(forward(&mut context, "e.toString()"), "\"[object Error]\""); + assert_eq!( + forward(&mut context, "b.toString()"), + "\"[object Boolean]\"" + ); + assert_eq!(forward(&mut context, "i.toString()"), "\"[object Number]\""); + assert_eq!(forward(&mut context, "s.toString()"), "\"[object String]\""); + assert_eq!(forward(&mut context, "d.toString()"), "\"[object Date]\""); + assert_eq!( + forward(&mut context, "re.toString()"), + "\"[object RegExp]\"" + ); + assert_eq!(forward(&mut context, "o.toString()"), "\"[object Object]\""); +} + +#[test] +fn define_symbol_property() { + let mut context = Context::default(); + + let init = r#" + let obj = {}; + let sym = Symbol("key"); + Object.defineProperty(obj, sym, { value: "val" }); + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "obj[sym]"), "\"val\""); +} + +#[test] +fn get_own_property_descriptor_1_arg_returns_undefined() { + let mut context = Context::default(); + let code = r#" + let obj = {a: 2}; + Object.getOwnPropertyDescriptor(obj) + "#; + assert_eq!(context.eval(code).unwrap(), JsValue::undefined()); +} + +#[test] +fn get_own_property_descriptor() { + let mut context = Context::default(); + forward( + &mut context, + r#" + let obj = {a: 2}; + let result = Object.getOwnPropertyDescriptor(obj, "a"); + "#, + ); + + assert_eq!(forward(&mut context, "result.enumerable"), "true"); + assert_eq!(forward(&mut context, "result.writable"), "true"); + assert_eq!(forward(&mut context, "result.configurable"), "true"); + assert_eq!(forward(&mut context, "result.value"), "2"); +} + +#[test] +fn get_own_property_descriptors() { + let mut context = Context::default(); + forward( + &mut context, + r#" + let obj = {a: 1, b: 2}; + let result = Object.getOwnPropertyDescriptors(obj); + "#, + ); + + assert_eq!(forward(&mut context, "result.a.enumerable"), "true"); + assert_eq!(forward(&mut context, "result.a.writable"), "true"); + assert_eq!(forward(&mut context, "result.a.configurable"), "true"); + assert_eq!(forward(&mut context, "result.a.value"), "1"); + + assert_eq!(forward(&mut context, "result.b.enumerable"), "true"); + assert_eq!(forward(&mut context, "result.b.writable"), "true"); + assert_eq!(forward(&mut context, "result.b.configurable"), "true"); + assert_eq!(forward(&mut context, "result.b.value"), "2"); +} + +#[test] +fn object_define_properties() { + let mut context = Context::default(); + + let init = r#" + const obj = {}; + + Object.defineProperties(obj, { + p: { + value: 42, + writable: true + } + }); + "#; + eprintln!("{}", forward(&mut context, init)); + + assert_eq!(forward(&mut context, "obj.p"), "42"); +} + +#[test] +fn object_is_prototype_of() { + let mut context = Context::default(); + + let init = r#" + Object.prototype.isPrototypeOf(String.prototype) + "#; + + assert_eq!(context.eval(init).unwrap(), JsValue::new(true)); +} + +#[test] +fn object_get_own_property_names_invalid_args() { + let error_message = r#"Uncaught "TypeError": "cannot convert 'null' or 'undefined' to object""#; + + check_output(&[ + TestAction::TestEq("Object.getOwnPropertyNames()", error_message), + TestAction::TestEq("Object.getOwnPropertyNames(null)", error_message), + TestAction::TestEq("Object.getOwnPropertyNames(undefined)", error_message), + ]); +} + +#[test] +fn object_get_own_property_names() { + check_output(&[ + TestAction::TestEq("Object.getOwnPropertyNames(0)", "[]"), + TestAction::TestEq("Object.getOwnPropertyNames(false)", "[]"), + TestAction::TestEq(r#"Object.getOwnPropertyNames(Symbol("a"))"#, "[]"), + TestAction::TestEq("Object.getOwnPropertyNames({})", "[]"), + TestAction::TestEq("Object.getOwnPropertyNames(NaN)", "[]"), + TestAction::TestEq( + "Object.getOwnPropertyNames([1, 2, 3])", + r#"[ "0", "1", "2", "length" ]"#, + ), + TestAction::TestEq( + r#"Object.getOwnPropertyNames({ + "a": 1, + "b": 2, + [ Symbol("c") ]: 3, + [ Symbol("d") ]: 4, + })"#, + r#"[ "a", "b" ]"#, + ), + ]); +} + +#[test] +fn object_get_own_property_symbols_invalid_args() { + let error_message = r#"Uncaught "TypeError": "cannot convert 'null' or 'undefined' to object""#; + + check_output(&[ + TestAction::TestEq("Object.getOwnPropertySymbols()", error_message), + TestAction::TestEq("Object.getOwnPropertySymbols(null)", error_message), + TestAction::TestEq("Object.getOwnPropertySymbols(undefined)", error_message), + ]); +} + +#[test] +fn object_get_own_property_symbols() { + check_output(&[ + TestAction::TestEq("Object.getOwnPropertySymbols(0)", "[]"), + TestAction::TestEq("Object.getOwnPropertySymbols(false)", "[]"), + TestAction::TestEq(r#"Object.getOwnPropertySymbols(Symbol("a"))"#, "[]"), + TestAction::TestEq("Object.getOwnPropertySymbols({})", "[]"), + TestAction::TestEq("Object.getOwnPropertySymbols(NaN)", "[]"), + TestAction::TestEq("Object.getOwnPropertySymbols([1, 2, 3])", "[]"), + TestAction::TestEq( + r#" + Object.getOwnPropertySymbols({ + "a": 1, + "b": 2, + [ Symbol("c") ]: 3, + [ Symbol("d") ]: 4, + })"#, + "[ Symbol(c), Symbol(d) ]", + ), + ]); +} + +#[test] +fn object_from_entries_invalid_args() { + let error_message = r#"Uncaught "TypeError": "cannot convert null or undefined to Object""#; + + check_output(&[ + TestAction::TestEq("Object.fromEntries()", error_message), + TestAction::TestEq("Object.fromEntries(null)", error_message), + TestAction::TestEq("Object.fromEntries(undefined)", error_message), + ]); +} + +#[test] +fn object_from_entries() { + let scenario = r#" + let sym = Symbol("sym"); + let map = Object.fromEntries([ + ["long key", 1], + ["short", 2], + [sym, 3], + [5, 4], + ]); + "#; + + check_output(&[ + TestAction::Execute(scenario), + TestAction::TestEq("map['long key']", "1"), + TestAction::TestEq("map.short", "2"), + TestAction::TestEq("map[sym]", "3"), + TestAction::TestEq("map[5]", "4"), + ]); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/mod.rs new file mode 100644 index 0000000..bccd87a --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/mod.rs @@ -0,0 +1,2072 @@ +//! This module implements the global `Promise` object. + +#[cfg(test)] +mod tests; + +mod promise_job; + +use self::promise_job::PromiseJob; +use super::{iterable::IteratorRecord, JsArgs}; +use crate::{ + builtins::{Array, BuiltIn}, + context::intrinsics::StandardConstructors, + job::JobCallback, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsFunction, JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptorBuilder}, + symbol::WellKnownSymbols, + value::JsValue, + Context, JsResult, +}; +use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; +use boa_profiler::Profiler; +use std::{cell::Cell, rc::Rc}; +use tap::{Conv, Pipe}; + +/// `IfAbruptRejectPromise ( value, capability )` +/// +/// `IfAbruptRejectPromise` is a shorthand for a sequence of algorithm steps that use a `PromiseCapability` Record. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-ifabruptrejectpromise +macro_rules! if_abrupt_reject_promise { + ($value:ident, $capability:expr, $context: expr) => { + let $value = match $value { + // 1. If value is an abrupt completion, then + Err(value) => { + // a. Perform ? Call(capability.[[Reject]], undefined, « value.[[Value]] »). + $context.call( + &$capability.reject.clone().into(), + &JsValue::undefined(), + &[value], + )?; + + // b. Return capability.[[Promise]]. + return Ok($capability.promise.clone().into()); + } + // 2. Else if value is a Completion Record, set value to value.[[Value]]. + Ok(value) => value, + }; + }; +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum PromiseState { + Pending, + Fulfilled, + Rejected, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct Promise { + promise_result: Option, + #[unsafe_ignore_trace] + promise_state: PromiseState, + promise_fulfill_reactions: Vec, + promise_reject_reactions: Vec, + promise_is_handled: bool, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct ReactionRecord { + promise_capability: Option, + #[unsafe_ignore_trace] + reaction_type: ReactionType, + handler: Option, +} + +#[derive(Debug, Clone, Copy)] +enum ReactionType { + Fulfill, + Reject, +} + +#[derive(Debug, Clone, Trace, Finalize)] +pub struct PromiseCapability { + promise: JsObject, + resolve: JsFunction, + reject: JsFunction, +} + +impl PromiseCapability { + /// `NewPromiseCapability ( C )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromisecapability + pub(crate) fn new(c: &JsValue, context: &mut Context) -> JsResult { + #[derive(Debug, Clone, Trace, Finalize)] + struct RejectResolve { + reject: JsValue, + resolve: JsValue, + } + + match c.as_constructor() { + // 1. If IsConstructor(C) is false, throw a TypeError exception. + None => context.throw_type_error("PromiseCapability: expected constructor"), + Some(c) => { + let c = c.clone(); + + // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1). + // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }. + let promise_capability = Gc::new(boa_gc::Cell::new(RejectResolve { + reject: JsValue::undefined(), + resolve: JsValue::undefined(), + })); + + // 4. Let executorClosure be a new Abstract Closure with parameters (resolve, reject) that captures promiseCapability and performs the following steps when called: + // 5. Let executor be CreateBuiltinFunction(executorClosure, 2, "", « »). + let executor = FunctionBuilder::closure_with_captures( + context, + |_this, args: &[JsValue], captures, context| { + let mut promise_capability = captures.borrow_mut(); + // a. If promiseCapability.[[Resolve]] is not undefined, throw a TypeError exception. + if !promise_capability.resolve.is_undefined() { + return context.throw_type_error( + "promiseCapability.[[Resolve]] is not undefined", + ); + } + + // b. If promiseCapability.[[Reject]] is not undefined, throw a TypeError exception. + if !promise_capability.reject.is_undefined() { + return context + .throw_type_error("promiseCapability.[[Reject]] is not undefined"); + } + + let resolve = args.get_or_undefined(0); + let reject = args.get_or_undefined(1); + + // c. Set promiseCapability.[[Resolve]] to resolve. + promise_capability.resolve = resolve.clone(); + + // d. Set promiseCapability.[[Reject]] to reject. + promise_capability.reject = reject.clone(); + + // e. Return undefined. + Ok(JsValue::Undefined) + }, + promise_capability.clone(), + ) + .name("") + .length(2) + .build() + .into(); + + // 6. Let promise be ? Construct(C, « executor »). + let promise = c.construct(&[executor], Some(&c), context)?; + + let promise_capability = promise_capability.borrow(); + + let resolve = promise_capability.resolve.clone(); + let reject = promise_capability.reject.clone(); + + // 7. If IsCallable(promiseCapability.[[Resolve]]) is false, throw a TypeError exception. + let resolve = resolve + .as_object() + .cloned() + .and_then(JsFunction::from_object) + .ok_or_else(|| { + context + .construct_type_error("promiseCapability.[[Resolve]] is not callable") + })?; + + // 8. If IsCallable(promiseCapability.[[Reject]]) is false, throw a TypeError exception. + let reject = reject + .as_object() + .cloned() + .and_then(JsFunction::from_object) + .ok_or_else(|| { + context.construct_type_error("promiseCapability.[[Reject]] is not callable") + })?; + + // 9. Set promiseCapability.[[Promise]] to promise. + // 10. Return promiseCapability. + Ok(PromiseCapability { + promise, + resolve, + reject, + }) + } + } + } + + /// Returns the promise object. + pub(crate) fn promise(&self) -> &JsObject { + &self.promise + } + + /// Returns the resolve function. + pub(crate) fn resolve(&self) -> &JsFunction { + &self.resolve + } + + /// Returns the reject function. + pub(crate) fn reject(&self) -> &JsFunction { + &self.reject + } +} + +impl BuiltIn for Promise { + const NAME: &'static str = "Promise"; + + const ATTRIBUTE: Attribute = Attribute::WRITABLE + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::CONFIGURABLE); + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().promise().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_method(Self::all, "all", 1) + .static_method(Self::all_settled, "allSettled", 1) + .static_method(Self::any, "any", 1) + .static_method(Self::race, "race", 1) + .static_method(Self::reject, "reject", 1) + .static_method(Self::resolve, "resolve", 1) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .method(Self::then, "then", 2) + .method(Self::catch, "catch", 1) + .method(Self::finally, "finally", 1) + // + .property( + WellKnownSymbols::to_string_tag(), + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +#[derive(Debug)] +struct ResolvingFunctionsRecord { + resolve: JsValue, + reject: JsValue, +} + +impl Promise { + const LENGTH: usize = 1; + + /// `Promise ( executor )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise-executor + fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context.throw_type_error("Promise NewTarget cannot be undefined"); + } + + let executor = args.get_or_undefined(0); + + // 2. If IsCallable(executor) is false, throw a TypeError exception. + if !executor.is_callable() { + return context.throw_type_error("Promise executor is not callable"); + } + + // 3. Let promise be ? OrdinaryCreateFromConstructor(NewTarget, "%Promise.prototype%", « [[PromiseState]], [[PromiseResult]], [[PromiseFulfillReactions]], [[PromiseRejectReactions]], [[PromiseIsHandled]] »). + let promise = + get_prototype_from_constructor(new_target, StandardConstructors::promise, context)?; + + let promise = JsObject::from_proto_and_data( + promise, + ObjectData::promise(Self { + promise_result: None, + // 4. Set promise.[[PromiseState]] to pending. + promise_state: PromiseState::Pending, + // 5. Set promise.[[PromiseFulfillReactions]] to a new empty List. + promise_fulfill_reactions: Vec::new(), + // 6. Set promise.[[PromiseRejectReactions]] to a new empty List. + promise_reject_reactions: Vec::new(), + // 7. Set promise.[[PromiseIsHandled]] to false. + promise_is_handled: false, + }), + ); + + // 8. Let resolvingFunctions be CreateResolvingFunctions(promise). + let resolving_functions = Self::create_resolving_functions(&promise, context); + + // 9. Let completion Completion(Call(executor, undefined, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)be ). + let completion = context.call( + executor, + &JsValue::Undefined, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + ); + + // 10. If completion is an abrupt completion, then + if let Err(value) = completion { + // a. Perform ? Call(resolvingFunctions.[[Reject]], undefined, « completion.[[Value]] »). + context.call(&resolving_functions.reject, &JsValue::Undefined, &[value])?; + } + + // 11. Return promise. + promise.conv::().pipe(Ok) + } + + /// `Promise.all ( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.all + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all + pub(crate) fn all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // Note: We already checked that `C` is a constructor in `NewPromiseCapability(C)`. + let c = c.as_object().expect("must be a constructor"); + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = Self::get_promise_resolve(c, context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; + + // 7. Let result be Completion(PerformPromiseAll(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_all( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve, + context, + ) + .map(JsValue::from); + + // 8. If result is an abrupt completion, then + if result.is_err() { + // a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)). + if !iterator_record.done() { + result = iterator_record.close(result, context); + } + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + return Ok(result); + } + + // 9. Return ? result. + result + } + + /// `PerformPromiseAll ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiseall + fn perform_promise_all( + iterator_record: &mut IteratorRecord, + constructor: &JsObject, + result_capability: &PromiseCapability, + promise_resolve: &JsObject, + context: &mut Context, + ) -> JsResult { + #[derive(Debug, Trace, Finalize)] + struct ResolveElementCaptures { + #[unsafe_ignore_trace] + already_called: Rc>, + index: usize, + values: Gc>>, + capability_resolve: JsFunction, + #[unsafe_ignore_trace] + remaining_elements_count: Rc>, + } + + // 1. Let values be a new empty List. + let values = Gc::new(GcCell::new(Vec::new())); + + // 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + let remaining_elements_count = Rc::new(Cell::new(1)); + + // 3. Let index be 0. + let mut index = 0; + + // 4. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + let next_value = match next { + Err(e) => { + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // c. ReturnIfAbrupt(next). + return Err(e); + } + // d. If next is false, then + Ok(None) => { + // i. Set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + values.borrow().iter().cloned(), + context, + ); + + // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). + result_capability.resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + )?; + } + + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } + Ok(Some(next)) => { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + match next_value { + Err(e) => { + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // g. ReturnIfAbrupt(nextValue). + return Err(e); + } + Ok(next_value) => next_value, + } + } + }; + + // h. Append undefined to values. + values.borrow_mut().push(JsValue::Undefined); + + // i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = + promise_resolve.call(&constructor.clone().into(), &[next_value], context)?; + + // j. Let steps be the algorithm steps defined in Promise.all Resolve Element Functions. + // k. Let length be the number of non-optional parameters of the function definition in Promise.all Resolve Element Functions. + // l. Let onFulfilled be CreateBuiltinFunction(steps, length, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »). + // m. Set onFulfilled.[[AlreadyCalled]] to false. + // n. Set onFulfilled.[[Index]] to index. + // o. Set onFulfilled.[[Values]] to values. + // p. Set onFulfilled.[[Capability]] to resultCapability. + // q. Set onFulfilled.[[RemainingElements]] to remainingElementsCount. + let on_fulfilled = FunctionBuilder::closure_with_captures( + context, + |_, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise.all-resolve-element-functions + + // 1. Let F be the active function object. + // 2. If F.[[AlreadyCalled]] is true, return undefined. + if captures.already_called.get() { + return Ok(JsValue::undefined()); + } + + // 3. Set F.[[AlreadyCalled]] to true. + captures.already_called.set(true); + + // 4. Let index be F.[[Index]]. + // 5. Let values be F.[[Values]]. + // 6. Let promiseCapability be F.[[Capability]]. + // 7. Let remainingElementsCount be F.[[RemainingElements]]. + + // 8. Set values[index] to x. + captures.values.borrow_mut()[captures.index] = args.get_or_undefined(0).clone(); + + // 9. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + captures + .remaining_elements_count + .set(captures.remaining_elements_count.get() - 1); + + // 10. If remainingElementsCount.[[Value]] is 0, then + if captures.remaining_elements_count.get() == 0 { + // a. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + captures.values.borrow().as_slice().iter().cloned(), + context, + ); + + // b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »). + return captures.capability_resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + ); + } + + // 11. Return undefined. + Ok(JsValue::undefined()) + }, + ResolveElementCaptures { + already_called: Rc::new(Cell::new(false)), + index, + values: values.clone(), + capability_resolve: result_capability.resolve.clone(), + remaining_elements_count: remaining_elements_count.clone(), + }, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // r. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1. + remaining_elements_count.set(remaining_elements_count.get() + 1); + + // s. Perform ? Invoke(nextPromise, "then", « onFulfilled, resultCapability.[[Reject]] »). + next_promise.invoke( + "then", + &[on_fulfilled.into(), result_capability.reject.clone().into()], + context, + )?; + + // t. Set index to index + 1. + index += 1; + } + } + + /// `Promise.allSettled ( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.allsettled + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled + pub(crate) fn all_settled( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // Note: We already checked that `C` is a constructor in `NewPromiseCapability(C)`. + let c = c.as_object().expect("must be a constructor"); + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = Self::get_promise_resolve(c, context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; + + // 7. Let result be Completion(PerformPromiseAllSettled(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_all_settled( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve, + context, + ) + .map(JsValue::from); + + // 8. If result is an abrupt completion, then + if result.is_err() { + // a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)). + if !iterator_record.done() { + result = iterator_record.close(result, context); + } + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + return Ok(result); + } + + // 9. Return ? result. + result + } + + /// `PerformPromiseAllSettled ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiseallsettled + fn perform_promise_all_settled( + iterator_record: &mut IteratorRecord, + constructor: &JsObject, + result_capability: &PromiseCapability, + promise_resolve: &JsObject, + context: &mut Context, + ) -> JsResult { + #[derive(Debug, Trace, Finalize)] + struct ResolveRejectElementCaptures { + #[unsafe_ignore_trace] + already_called: Rc>, + index: usize, + values: Gc>>, + capability: JsFunction, + #[unsafe_ignore_trace] + remaining_elements: Rc>, + } + + // 1. Let values be a new empty List. + let values = Gc::new(GcCell::new(Vec::new())); + + // 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + let remaining_elements_count = Rc::new(Cell::new(1)); + + // 3. Let index be 0. + let mut index = 0; + + // 4. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + let next_value = match next { + Err(e) => { + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // c. ReturnIfAbrupt(next). + return Err(e); + } + // d. If next is false, then + Ok(None) => { + // i. Set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let valuesArray be CreateArrayFromList(values). + let values_array = crate::builtins::Array::create_array_from_list( + values.borrow().as_slice().iter().cloned(), + context, + ); + + // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). + result_capability.resolve.call( + &JsValue::undefined(), + &[values_array.into()], + context, + )?; + } + + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } + Ok(Some(next)) => { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + match next_value { + Err(e) => { + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // g. ReturnIfAbrupt(nextValue). + return Err(e); + } + Ok(next_value) => next_value, + } + } + }; + + // h. Append undefined to values. + values.borrow_mut().push(JsValue::undefined()); + + // i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = + promise_resolve.call(&constructor.clone().into(), &[next_value], context)?; + + // j. Let stepsFulfilled be the algorithm steps defined in Promise.allSettled Resolve Element Functions. + // k. Let lengthFulfilled be the number of non-optional parameters of the function definition in Promise.allSettled Resolve Element Functions. + // l. Let onFulfilled be CreateBuiltinFunction(stepsFulfilled, lengthFulfilled, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »). + // m. Let alreadyCalled be the Record { [[Value]]: false }. + // n. Set onFulfilled.[[AlreadyCalled]] to alreadyCalled. + // o. Set onFulfilled.[[Index]] to index. + // p. Set onFulfilled.[[Values]] to values. + // q. Set onFulfilled.[[Capability]] to resultCapability. + // r. Set onFulfilled.[[RemainingElements]] to remainingElementsCount. + let on_fulfilled = FunctionBuilder::closure_with_captures( + context, + |_, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise.allsettled-resolve-element-functions + + // 1. Let F be the active function object. + // 2. Let alreadyCalled be F.[[AlreadyCalled]]. + + // 3. If alreadyCalled.[[Value]] is true, return undefined. + if captures.already_called.get() { + return Ok(JsValue::undefined()); + } + + // 4. Set alreadyCalled.[[Value]] to true. + captures.already_called.set(true); + + // 5. Let index be F.[[Index]]. + // 6. Let values be F.[[Values]]. + // 7. Let promiseCapability be F.[[Capability]]. + // 8. Let remainingElementsCount be F.[[RemainingElements]]. + + // 9. Let obj be OrdinaryObjectCreate(%Object.prototype%). + let obj = context.construct_object(); + + // 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "fulfilled"). + obj.create_data_property_or_throw("status", "fulfilled", context) + .expect("cannot fail per spec"); + + // 11. Perform ! CreateDataPropertyOrThrow(obj, "value", x). + obj.create_data_property_or_throw("value", args.get_or_undefined(0), context) + .expect("cannot fail per spec"); + + // 12. Set values[index] to obj. + captures.values.borrow_mut()[captures.index] = obj.into(); + + // 13. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + captures + .remaining_elements + .set(captures.remaining_elements.get() - 1); + + // 14. If remainingElementsCount.[[Value]] is 0, then + if captures.remaining_elements.get() == 0 { + // a. Let valuesArray be CreateArrayFromList(values). + let values_array = Array::create_array_from_list( + captures.values.borrow().as_slice().iter().cloned(), + context, + ); + + // b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »). + return captures.capability.call( + &JsValue::undefined(), + &[values_array.into()], + context, + ); + } + + // 15. Return undefined. + Ok(JsValue::undefined()) + }, + ResolveRejectElementCaptures { + already_called: Rc::new(Cell::new(false)), + index, + values: values.clone(), + capability: result_capability.resolve.clone(), + remaining_elements: remaining_elements_count.clone(), + }, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // s. Let stepsRejected be the algorithm steps defined in Promise.allSettled Reject Element Functions. + // t. Let lengthRejected be the number of non-optional parameters of the function definition in Promise.allSettled Reject Element Functions. + // u. Let onRejected be CreateBuiltinFunction(stepsRejected, lengthRejected, "", « [[AlreadyCalled]], [[Index]], [[Values]], [[Capability]], [[RemainingElements]] »). + // v. Set onRejected.[[AlreadyCalled]] to alreadyCalled. + // w. Set onRejected.[[Index]] to index. + // x. Set onRejected.[[Values]] to values. + // y. Set onRejected.[[Capability]] to resultCapability. + // z. Set onRejected.[[RemainingElements]] to remainingElementsCount. + let on_rejected = FunctionBuilder::closure_with_captures( + context, + |_, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise.allsettled-reject-element-functions + + // 1. Let F be the active function object. + // 2. Let alreadyCalled be F.[[AlreadyCalled]]. + + // 3. If alreadyCalled.[[Value]] is true, return undefined. + if captures.already_called.get() { + return Ok(JsValue::undefined()); + } + + // 4. Set alreadyCalled.[[Value]] to true. + captures.already_called.set(true); + + // 5. Let index be F.[[Index]]. + // 6. Let values be F.[[Values]]. + // 7. Let promiseCapability be F.[[Capability]]. + // 8. Let remainingElementsCount be F.[[RemainingElements]]. + + // 9. Let obj be OrdinaryObjectCreate(%Object.prototype%). + let obj = context.construct_object(); + + // 10. Perform ! CreateDataPropertyOrThrow(obj, "status", "rejected"). + obj.create_data_property_or_throw("status", "rejected", context) + .expect("cannot fail per spec"); + + // 11. Perform ! CreateDataPropertyOrThrow(obj, "reason", x). + obj.create_data_property_or_throw("reason", args.get_or_undefined(0), context) + .expect("cannot fail per spec"); + + // 12. Set values[index] to obj. + captures.values.borrow_mut()[captures.index] = obj.into(); + + // 13. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + captures + .remaining_elements + .set(captures.remaining_elements.get() - 1); + + // 14. If remainingElementsCount.[[Value]] is 0, then + if captures.remaining_elements.get() == 0 { + // a. Let valuesArray be CreateArrayFromList(values). + let values_array = Array::create_array_from_list( + captures.values.borrow().as_slice().iter().cloned(), + context, + ); + + // b. Return ? Call(promiseCapability.[[Resolve]], undefined, « valuesArray »). + return captures.capability.call( + &JsValue::undefined(), + &[values_array.into()], + context, + ); + } + + // 15. Return undefined. + Ok(JsValue::undefined()) + }, + ResolveRejectElementCaptures { + already_called: Rc::new(Cell::new(false)), + index, + values: values.clone(), + capability: result_capability.resolve.clone(), + remaining_elements: remaining_elements_count.clone(), + }, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // aa. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1. + remaining_elements_count.set(remaining_elements_count.get() + 1); + + // ab. Perform ? Invoke(nextPromise, "then", « onFulfilled, onRejected »). + next_promise.invoke("then", &[on_fulfilled.into(), on_rejected.into()], context)?; + + // ac. Set index to index + 1. + index += 1; + } + } + + /// `Promise.any ( iterable )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.any + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any + pub(crate) fn any( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // Note: We already checked that `C` is a constructor in `NewPromiseCapability(C)`. + let c = c.as_object().expect("must be a constructor"); + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = Self::get_promise_resolve(c, context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = args.get_or_undefined(0).get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; + + // 7. Let result be Completion(PerformPromiseAny(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_any( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve, + context, + ) + .map(JsValue::from); + + // 8. If result is an abrupt completion, then + if result.is_err() { + // a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)). + if !iterator_record.done() { + result = iterator_record.close(result, context); + } + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + return Ok(result); + } + + // 9. Return ? result. + result + } + + /// `PerformPromiseAny ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiseany + fn perform_promise_any( + iterator_record: &mut IteratorRecord, + constructor: &JsObject, + result_capability: &PromiseCapability, + promise_resolve: &JsObject, + context: &mut Context, + ) -> JsResult { + #[derive(Debug, Trace, Finalize)] + struct RejectElementCaptures { + #[unsafe_ignore_trace] + already_called: Rc>, + index: usize, + errors: Gc>>, + capability_reject: JsFunction, + #[unsafe_ignore_trace] + remaining_elements_count: Rc>, + } + + // 1. Let errors be a new empty List. + let errors = Gc::new(GcCell::new(Vec::new())); + + // 2. Let remainingElementsCount be the Record { [[Value]]: 1 }. + let remaining_elements_count = Rc::new(Cell::new(1)); + + // 3. Let index be 0. + let mut index = 0; + + // 4. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + let next_value = match next { + Err(e) => { + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // c. ReturnIfAbrupt(next). + return Err(e); + } + // d. If next is false, then + Ok(None) => { + // i. Set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // ii. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + remaining_elements_count.set(remaining_elements_count.get() - 1); + + // iii. If remainingElementsCount.[[Value]] is 0, then + if remaining_elements_count.get() == 0 { + // 1. Let error be a newly created AggregateError object. + let error = JsObject::from_proto_and_data( + context + .intrinsics() + .constructors() + .aggregate_error() + .prototype(), + ObjectData::error(), + ); + + // 2. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). + error + .define_property_or_throw( + "errors", + PropertyDescriptorBuilder::new() + .configurable(true) + .enumerable(false) + .writable(true) + .value(Array::create_array_from_list( + errors.borrow().as_slice().iter().cloned(), + context, + )), + context, + ) + .expect("cannot fail per spec"); + + // 3. Return ThrowCompletion(error). + return Err(error.into()); + } + + // iv. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone()); + } + Ok(Some(next)) => { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + match next_value { + Err(e) => { + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // g. ReturnIfAbrupt(nextValue). + return Err(e); + } + Ok(next_value) => next_value, + } + } + }; + + // h. Append undefined to errors. + errors.borrow_mut().push(JsValue::undefined()); + + // i. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = + promise_resolve.call(&constructor.clone().into(), &[next_value], context)?; + + // j. Let stepsRejected be the algorithm steps defined in Promise.any Reject Element Functions. + // k. Let lengthRejected be the number of non-optional parameters of the function definition in Promise.any Reject Element Functions. + // l. Let onRejected be CreateBuiltinFunction(stepsRejected, lengthRejected, "", « [[AlreadyCalled]], [[Index]], [[Errors]], [[Capability]], [[RemainingElements]] »). + // m. Set onRejected.[[AlreadyCalled]] to false. + // n. Set onRejected.[[Index]] to index. + // o. Set onRejected.[[Errors]] to errors. + // p. Set onRejected.[[Capability]] to resultCapability. + // q. Set onRejected.[[RemainingElements]] to remainingElementsCount. + let on_rejected = FunctionBuilder::closure_with_captures( + context, + |_, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise.any-reject-element-functions + + // 1. Let F be the active function object. + + // 2. If F.[[AlreadyCalled]] is true, return undefined. + if captures.already_called.get() { + return Ok(JsValue::undefined()); + } + + // 3. Set F.[[AlreadyCalled]] to true. + captures.already_called.set(true); + + // 4. Let index be F.[[Index]]. + // 5. Let errors be F.[[Errors]]. + // 6. Let promiseCapability be F.[[Capability]]. + // 7. Let remainingElementsCount be F.[[RemainingElements]]. + + // 8. Set errors[index] to x. + captures.errors.borrow_mut()[captures.index] = args.get_or_undefined(0).clone(); + + // 9. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] - 1. + captures + .remaining_elements_count + .set(captures.remaining_elements_count.get() - 1); + + // 10. If remainingElementsCount.[[Value]] is 0, then + if captures.remaining_elements_count.get() == 0 { + // a. Let error be a newly created AggregateError object. + let error = JsObject::from_proto_and_data( + context + .intrinsics() + .constructors() + .aggregate_error() + .prototype(), + ObjectData::error(), + ); + + // b. Perform ! DefinePropertyOrThrow(error, "errors", PropertyDescriptor { [[Configurable]]: true, [[Enumerable]]: false, [[Writable]]: true, [[Value]]: CreateArrayFromList(errors) }). + error + .define_property_or_throw( + "errors", + PropertyDescriptorBuilder::new() + .configurable(true) + .enumerable(false) + .writable(true) + .value(Array::create_array_from_list( + captures.errors.borrow().as_slice().iter().cloned(), + context, + )), + context, + ) + .expect("cannot fail per spec"); + + // c. Return ? Call(promiseCapability.[[Reject]], undefined, « error »). + return captures.capability_reject.call( + &JsValue::undefined(), + &[error.into()], + context, + ); + } + + // 11. Return undefined. + Ok(JsValue::undefined()) + }, + RejectElementCaptures { + already_called: Rc::new(Cell::new(false)), + index, + errors: errors.clone(), + capability_reject: result_capability.reject.clone(), + remaining_elements_count: remaining_elements_count.clone(), + }, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // r. Set remainingElementsCount.[[Value]] to remainingElementsCount.[[Value]] + 1. + remaining_elements_count.set(remaining_elements_count.get() + 1); + + // s. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], onRejected »). + next_promise.invoke( + "then", + &[result_capability.resolve.clone().into(), on_rejected.into()], + context, + )?; + + // t. Set index to index + 1. + index += 1; + } + } + + /// `CreateResolvingFunctions ( promise )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions + fn create_resolving_functions( + promise: &JsObject, + context: &mut Context, + ) -> ResolvingFunctionsRecord { + #[derive(Debug, Trace, Finalize)] + struct RejectResolveCaptures { + promise: JsObject, + #[unsafe_ignore_trace] + already_resolved: Rc>, + } + + // 1. Let alreadyResolved be the Record { [[Value]]: false }. + let already_resolved = Rc::new(Cell::new(false)); + + // 5. Set resolve.[[Promise]] to promise. + // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. + let resolve_captures = RejectResolveCaptures { + already_resolved: already_resolved.clone(), + promise: promise.clone(), + }; + + // 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions. + // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. + // 4. Let resolve be CreateBuiltinFunction(stepsResolve, lengthResolve, "", « [[Promise]], [[AlreadyResolved]] »). + let resolve = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise-resolve-functions + + // 1. Let F be the active function object. + // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // 3. Let promise be F.[[Promise]]. + // 4. Let alreadyResolved be F.[[AlreadyResolved]]. + let RejectResolveCaptures { + promise, + already_resolved, + } = captures; + + // 5. If alreadyResolved.[[Value]] is true, return undefined. + if already_resolved.get() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + let resolution = args.get_or_undefined(0); + + // 7. If SameValue(resolution, promise) is true, then + if JsValue::same_value(resolution, &promise.clone().into()) { + // a. Let selfResolutionError be a newly created TypeError object. + let self_resolution_error = + context.construct_type_error("SameValue(resolution, promise) is true"); + + // b. Perform RejectPromise(promise, selfResolutionError). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject_promise(&self_resolution_error, context); + + // c. Return undefined. + return Ok(JsValue::Undefined); + } + + let then = if let Some(resolution) = resolution.as_object() { + // 9. Let then be Completion(Get(resolution, "then")). + resolution.get("then", context) + } else { + // 8. If Type(resolution) is not Object, then + // a. Perform FulfillPromise(promise, resolution). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .fulfill_promise(resolution, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + }; + + let then_action = match then { + // 10. If then is an abrupt completion, then + Err(value) => { + // a. Perform RejectPromise(promise, then.[[Value]]). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject_promise(&value, context); + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + // 11. Let thenAction be then.[[Value]]. + Ok(then) => then, + }; + + // 12. If IsCallable(thenAction) is false, then + let then_action = match then_action.as_object() { + Some(then_action) if then_action.is_callable() => then_action, + _ => { + // a. Perform FulfillPromise(promise, resolution). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .fulfill_promise(resolution, context)?; + + // b. Return undefined. + return Ok(JsValue::Undefined); + } + }; + + // 13. Let thenJobCallback be HostMakeJobCallback(thenAction). + let then_job_callback = JobCallback::make_job_callback(then_action.clone()); + + // 14. Let job be NewPromiseResolveThenableJob(promise, resolution, thenJobCallback). + let job: JobCallback = PromiseJob::new_promise_resolve_thenable_job( + promise.clone(), + resolution.clone(), + then_job_callback, + context, + ); + + // 15. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + + // 16. Return undefined. + Ok(JsValue::Undefined) + }, + resolve_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 10. Set reject.[[Promise]] to promise. + // 11. Set reject.[[AlreadyResolved]] to alreadyResolved. + let reject_captures = RejectResolveCaptures { + promise: promise.clone(), + already_resolved, + }; + + // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. + // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. + // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). + let reject = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + // https://tc39.es/ecma262/#sec-promise-reject-functions + + // 1. Let F be the active function object. + // 2. Assert: F has a [[Promise]] internal slot whose value is an Object. + // 3. Let promise be F.[[Promise]]. + // 4. Let alreadyResolved be F.[[AlreadyResolved]]. + let RejectResolveCaptures { + promise, + already_resolved, + } = captures; + + // 5. If alreadyResolved.[[Value]] is true, return undefined. + if already_resolved.get() { + return Ok(JsValue::Undefined); + } + + // 6. Set alreadyResolved.[[Value]] to true. + already_resolved.set(true); + + // let reason = args.get_or_undefined(0); + // 7. Perform RejectPromise(promise, reason). + promise + .borrow_mut() + .as_promise_mut() + .expect("Expected promise to be a Promise") + .reject_promise(args.get_or_undefined(0), context); + + // 8. Return undefined. + Ok(JsValue::Undefined) + }, + reject_captures, + ) + .name("") + .length(1) + .constructor(false) + .build(); + + // 12. Return the Record { [[Resolve]]: resolve, [[Reject]]: reject }. + let resolve = resolve.conv::(); + let reject = reject.conv::(); + ResolvingFunctionsRecord { resolve, reject } + } + + /// `FulfillPromise ( promise, value )` + /// + /// The abstract operation `FulfillPromise` takes arguments `promise` and `value` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-fulfillpromise + pub fn fulfill_promise(&mut self, value: &JsValue, context: &mut Context) -> JsResult<()> { + // 1. Assert: The value of promise.[[PromiseState]] is pending. + assert_eq!( + self.promise_state, + PromiseState::Pending, + "promise was not pending" + ); + + // 2. Let reactions be promise.[[PromiseFulfillReactions]]. + let reactions = &self.promise_fulfill_reactions; + + // 7. Perform TriggerPromiseReactions(reactions, value). + Self::trigger_promise_reactions(reactions, value, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to value. + self.promise_result = Some(value.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to fulfilled. + self.promise_state = PromiseState::Fulfilled; + + // 8. Return unused. + Ok(()) + } + + /// `RejectPromise ( promise, reason )` + /// + /// The abstract operation `RejectPromise` takes arguments `promise` and `reason` and returns + /// `unused`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-rejectpromise + pub fn reject_promise(&mut self, reason: &JsValue, context: &mut Context) { + // 1. Assert: The value of promise.[[PromiseState]] is pending. + assert_eq!( + self.promise_state, + PromiseState::Pending, + "Expected promise.[[PromiseState]] to be pending" + ); + + // 2. Let reactions be promise.[[PromiseRejectReactions]]. + let reactions = &self.promise_reject_reactions; + + // 8. Perform TriggerPromiseReactions(reactions, reason). + Self::trigger_promise_reactions(reactions, reason, context); + // reordering this statement does not affect the semantics + + // 3. Set promise.[[PromiseResult]] to reason. + self.promise_result = Some(reason.clone()); + + // 4. Set promise.[[PromiseFulfillReactions]] to undefined. + self.promise_fulfill_reactions = Vec::new(); + + // 5. Set promise.[[PromiseRejectReactions]] to undefined. + self.promise_reject_reactions = Vec::new(); + + // 6. Set promise.[[PromiseState]] to rejected. + self.promise_state = PromiseState::Rejected; + + // 7. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "reject"). + if !self.promise_is_handled { + // TODO + } + + // 9. Return unused. + } + + /// `TriggerPromiseReactions ( reactions, argument )` + /// + /// The abstract operation `TriggerPromiseReactions` takes arguments `reactions` (a `List` of + /// `PromiseReaction` Records) and `argument` and returns unused. It enqueues a new `Job` for + /// each record in `reactions`. Each such `Job` processes the `[[Type]]` and `[[Handler]]` of + /// the `PromiseReaction` Record, and if the `[[Handler]]` is not `empty`, calls it passing the + /// given argument. If the `[[Handler]]` is `empty`, the behaviour is determined by the + /// `[[Type]]`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-triggerpromisereactions + pub fn trigger_promise_reactions( + reactions: &[ReactionRecord], + argument: &JsValue, + context: &mut Context, + ) { + // 1. For each element reaction of reactions, do + for reaction in reactions { + // a. Let job be NewPromiseReactionJob(reaction, argument). + let job = + PromiseJob::new_promise_reaction_job(reaction.clone(), argument.clone(), context); + + // b. Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]). + context.host_enqueue_promise_job(job); + } + + // 2. Return unused. + } + + /// `Promise.race ( iterable )` + /// + /// The `race` function returns a new promise which is settled in the same way as the first + /// passed promise to settle. It resolves all elements of the passed `iterable` to promises. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.race + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race + pub fn race(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let iterable = args.get_or_undefined(0); + + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // 3. Let promiseResolve be Completion(GetPromiseResolve(C)). + let promise_resolve = + Self::get_promise_resolve(c.as_object().expect("this was not an object"), context); + + // 4. IfAbruptRejectPromise(promiseResolve, promiseCapability). + if_abrupt_reject_promise!(promise_resolve, promise_capability, context); + + // 5. Let iteratorRecord be Completion(GetIterator(iterable)). + let iterator_record = iterable.get_iterator(context, None, None); + + // 6. IfAbruptRejectPromise(iteratorRecord, promiseCapability). + if_abrupt_reject_promise!(iterator_record, promise_capability, context); + let mut iterator_record = iterator_record; + + // 7. Let result be Completion(PerformPromiseRace(iteratorRecord, C, promiseCapability, promiseResolve)). + let mut result = Self::perform_promise_race( + &mut iterator_record, + c, + &promise_capability, + &promise_resolve.into(), + context, + ); + + // 8. If result is an abrupt completion, then + if result.is_err() { + // a. If iteratorRecord.[[Done]] is false, set result to Completion(IteratorClose(iteratorRecord, result)). + if !iterator_record.done() { + result = iterator_record.close(result, context); + } + + // b. IfAbruptRejectPromise(result, promiseCapability). + if_abrupt_reject_promise!(result, promise_capability, context); + + Ok(result) + } else { + // 9. Return ? result. + result + } + } + + /// `PerformPromiseRace ( iteratorRecord, constructor, resultCapability, promiseResolve )` + /// + /// The abstract operation `PerformPromiseRace` takes arguments `iteratorRecord`, `constructor` + /// (a constructor), `resultCapability` (a [`PromiseCapability`] Record), and `promiseResolve` + /// (a function object) and returns either a normal completion containing an ECMAScript + /// language value or a throw completion. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromiserace + fn perform_promise_race( + iterator_record: &mut IteratorRecord, + constructor: &JsValue, + result_capability: &PromiseCapability, + promise_resolve: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. Repeat, + loop { + // a. Let next be Completion(IteratorStep(iteratorRecord)). + let next = iterator_record.step(context); + + // b. If next is an abrupt completion, set iteratorRecord.[[Done]] to true. + if next.is_err() { + iterator_record.set_done(true); + } + + // c. ReturnIfAbrupt(next). + let next = next?; + + if let Some(next) = next { + // e. Let nextValue be Completion(IteratorValue(next)). + let next_value = next.value(context); + + // f. If nextValue is an abrupt completion, set iteratorRecord.[[Done]] to true. + if next_value.is_err() { + iterator_record.set_done(true); + } + + // g. ReturnIfAbrupt(nextValue). + let next_value = next_value?; + + // h. Let nextPromise be ? Call(promiseResolve, constructor, « nextValue »). + let next_promise = context.call(promise_resolve, constructor, &[next_value])?; + + // i. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], resultCapability.[[Reject]] »). + next_promise.invoke( + "then", + &[ + result_capability.resolve.clone().into(), + result_capability.reject.clone().into(), + ], + context, + )?; + } else { + // d. If next is false, then + // i. Set iteratorRecord.[[Done]] to true. + iterator_record.set_done(true); + + // ii. Return resultCapability.[[Promise]]. + return Ok(result_capability.promise.clone().into()); + } + } + } + + /// `Promise.reject ( r )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.reject + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/reject + pub fn reject(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let r = args.get_or_undefined(0); + + // 1. Let C be the this value. + let c = this; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(c, context)?; + + // 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). + context.call( + &promise_capability.reject.clone().into(), + &JsValue::undefined(), + &[r.clone()], + )?; + + // 4. Return promiseCapability.[[Promise]]. + Ok(promise_capability.promise.clone().into()) + } + + /// `Promise.resolve ( x )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.resolve + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve + pub fn resolve(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let x = args.get_or_undefined(0); + + // 1. Let C be the this value. + let c = this; + + if let Some(c) = c.as_object() { + // 3. Return ? PromiseResolve(C, x). + Self::promise_resolve(c.clone(), x.clone(), context) + } else { + // 2. If Type(C) is not Object, throw a TypeError exception. + context.throw_type_error("Promise.resolve() called on a non-object") + } + } + + /// `get Promise [ @@species ]` + /// + /// The `Promise [ @@species ]` accessor property returns the Promise constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-promise-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// `Promise.prototype.catch ( onRejected )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.catch + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch + pub fn catch(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let on_rejected = args.get_or_undefined(0); + + // 1. Let promise be the this value. + let promise = this; + // 2. Return ? Invoke(promise, "then", « undefined, onRejected »). + promise.invoke( + "then", + &[JsValue::undefined(), on_rejected.clone()], + context, + ) + } + + /// `Promise.prototype.finally ( onFinally )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.finally + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/finally + pub fn finally(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let promise be the this value. + let promise = this; + + // 2. If Type(promise) is not Object, throw a TypeError exception. + let promise_obj = if let Some(p) = promise.as_object() { + p + } else { + return context.throw_type_error("finally called with a non-object promise"); + }; + + // 3. Let C be ? SpeciesConstructor(promise, %Promise%). + let c = promise_obj.species_constructor(StandardConstructors::promise, context)?; + + // 4. Assert: IsConstructor(C) is true. + assert!(c.is_constructor()); + + let on_finally = args.get_or_undefined(0); + + // 5. If IsCallable(onFinally) is false, then + let (then_finally, catch_finally) = if on_finally.is_callable() { + /// Capture object for the `thenFinallyClosure` abstract closure. + #[derive(Debug, Trace, Finalize)] + struct FinallyCaptures { + on_finally: JsValue, + c: JsObject, + } + + // a. Let thenFinallyClosure be a new Abstract Closure with parameters (value) that captures onFinally and C and performs the following steps when called: + let then_finally_closure = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + /// Capture object for the abstract `returnValue` closure. + #[derive(Debug, Trace, Finalize)] + struct ReturnValueCaptures { + value: JsValue, + } + + let value = args.get_or_undefined(0); + + // i. Let result be ? Call(onFinally, undefined). + let result = context.call(&captures.on_finally, &JsValue::undefined(), &[])?; + + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + + // iii. Let returnValue be a new Abstract Closure with no parameters that captures value and performs the following steps when called: + let return_value = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, _context| { + // 1. Return value. + Ok(captures.value.clone()) + }, + ReturnValueCaptures { + value: value.clone(), + }, + ); + + // iv. Let valueThunk be CreateBuiltinFunction(returnValue, 0, "", « »). + let value_thunk = return_value.length(0).name("").build(); + + // v. Return ? Invoke(promise, "then", « valueThunk »). + promise.invoke("then", &[value_thunk.into()], context) + }, + FinallyCaptures { + on_finally: on_finally.clone(), + c: c.clone(), + }, + ); + + // b. Let thenFinally be CreateBuiltinFunction(thenFinallyClosure, 1, "", « »). + let then_finally = then_finally_closure.length(1).name("").build(); + + // c. Let catchFinallyClosure be a new Abstract Closure with parameters (reason) that captures onFinally and C and performs the following steps when called: + let catch_finally_closure = FunctionBuilder::closure_with_captures( + context, + |_this, args, captures, context| { + /// Capture object for the abstract `throwReason` closure. + #[derive(Debug, Trace, Finalize)] + struct ThrowReasonCaptures { + reason: JsValue, + } + + let reason = args.get_or_undefined(0); + + // i. Let result be ? Call(onFinally, undefined). + let result = context.call(&captures.on_finally, &JsValue::undefined(), &[])?; + + // ii. Let promise be ? PromiseResolve(C, result). + let promise = Self::promise_resolve(captures.c.clone(), result, context)?; + + // iii. Let throwReason be a new Abstract Closure with no parameters that captures reason and performs the following steps when called: + let throw_reason = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, _context| { + // 1. Return ThrowCompletion(reason). + Err(captures.reason.clone()) + }, + ThrowReasonCaptures { + reason: reason.clone(), + }, + ); + + // iv. Let thrower be CreateBuiltinFunction(throwReason, 0, "", « »). + let thrower = throw_reason.length(0).name("").build(); + + // v. Return ? Invoke(promise, "then", « thrower »). + promise.invoke("then", &[thrower.into()], context) + }, + FinallyCaptures { + on_finally: on_finally.clone(), + c, + }, + ); + + // d. Let catchFinally be CreateBuiltinFunction(catchFinallyClosure, 1, "", « »). + let catch_finally = catch_finally_closure.length(1).name("").build(); + + (then_finally.into(), catch_finally.into()) // TODO + } else { + // 6. Else, + // a. Let thenFinally be onFinally. + // b. Let catchFinally be onFinally. + (on_finally.clone(), on_finally.clone()) + }; + + // 7. Return ? Invoke(promise, "then", « thenFinally, catchFinally »). + promise.invoke("then", &[then_finally, catch_finally], context) + } + + /// `Promise.prototype.then ( onFulfilled, onRejected )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise.prototype.then + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then + pub fn then(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let promise be the this value. + let promise = this; + + // 2. If IsPromise(promise) is false, throw a TypeError exception. + let promise_obj = match promise.as_promise() { + Some(obj) => obj, + None => return context.throw_type_error("IsPromise(promise) is false"), + }; + + // 3. Let C be ? SpeciesConstructor(promise, %Promise%). + let c = promise_obj.species_constructor(StandardConstructors::promise, context)?; + + // 4. Let resultCapability be ? NewPromiseCapability(C). + let result_capability = PromiseCapability::new(&c.into(), context)?; + + let on_fulfilled = args.get_or_undefined(0); + let on_rejected = args.get_or_undefined(1); + + // 5. Return PerformPromiseThen(promise, onFulfilled, onRejected, resultCapability). + promise_obj + .borrow_mut() + .as_promise_mut() + .expect("IsPromise(promise) is false") + .perform_promise_then(on_fulfilled, on_rejected, Some(result_capability), context) + .pipe(Ok) + } + + /// `PerformPromiseThen ( promise, onFulfilled, onRejected [ , resultCapability ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-performpromisethen + pub(crate) fn perform_promise_then( + &mut self, + on_fulfilled: &JsValue, + on_rejected: &JsValue, + result_capability: Option, + context: &mut Context, + ) -> JsValue { + // 1. Assert: IsPromise(promise) is true. + + // 2. If resultCapability is not present, then + // a. Set resultCapability to undefined. + + let on_fulfilled_job_callback = match on_fulfilled.as_object() { + // 4. Else, + // a. Let onFulfilledJobCallback be HostMakeJobCallback(onFulfilled). + Some(on_fulfilled) if on_fulfilled.is_callable() => { + Some(JobCallback::make_job_callback(on_fulfilled.clone())) + } + // 3. If IsCallable(onFulfilled) is false, then + // a. Let onFulfilledJobCallback be empty. + _ => None, + }; + + let on_rejected_job_callback = match on_rejected.as_object() { + // 6. Else, + // a. Let onRejectedJobCallback be HostMakeJobCallback(onRejected). + Some(on_rejected) if on_rejected.is_callable() => { + Some(JobCallback::make_job_callback(on_rejected.clone())) + } + // 5. If IsCallable(onRejected) is false, then + // a. Let onRejectedJobCallback be empty. + _ => None, + }; + + // 7. Let fulfillReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Fulfill, [[Handler]]: onFulfilledJobCallback }. + let fulfill_reaction = ReactionRecord { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Fulfill, + handler: on_fulfilled_job_callback, + }; + + // 8. Let rejectReaction be the PromiseReaction { [[Capability]]: resultCapability, [[Type]]: Reject, [[Handler]]: onRejectedJobCallback }. + let reject_reaction = ReactionRecord { + promise_capability: result_capability.clone(), + reaction_type: ReactionType::Reject, + handler: on_rejected_job_callback, + }; + + match self.promise_state { + // 9. If promise.[[PromiseState]] is pending, then + PromiseState::Pending => { + // a. Append fulfillReaction as the last element of the List that is promise.[[PromiseFulfillReactions]]. + self.promise_fulfill_reactions.push(fulfill_reaction); + + // b. Append rejectReaction as the last element of the List that is promise.[[PromiseRejectReactions]]. + self.promise_reject_reactions.push(reject_reaction); + } + + // 10. Else if promise.[[PromiseState]] is fulfilled, then + PromiseState::Fulfilled => { + // a. Let value be promise.[[PromiseResult]]. + let value = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // b. Let fulfillJob be NewPromiseReactionJob(fulfillReaction, value). + let fulfill_job = + PromiseJob::new_promise_reaction_job(fulfill_reaction, value, context); + + // c. Perform HostEnqueuePromiseJob(fulfillJob.[[Job]], fulfillJob.[[Realm]]). + context.host_enqueue_promise_job(fulfill_job); + } + + // 11. Else, + // a. Assert: The value of promise.[[PromiseState]] is rejected. + PromiseState::Rejected => { + // b. Let reason be promise.[[PromiseResult]]. + let reason = self + .promise_result + .clone() + .expect("promise.[[PromiseResult]] cannot be empty"); + + // c. If promise.[[PromiseIsHandled]] is false, perform HostPromiseRejectionTracker(promise, "handle"). + if !self.promise_is_handled { + // HostPromiseRejectionTracker(promise, "handle") + // TODO + } + + // d. Let rejectJob be NewPromiseReactionJob(rejectReaction, reason). + let reject_job = + PromiseJob::new_promise_reaction_job(reject_reaction, reason, context); + + // e. Perform HostEnqueuePromiseJob(rejectJob.[[Job]], rejectJob.[[Realm]]). + context.host_enqueue_promise_job(reject_job); + + // 12. Set promise.[[PromiseIsHandled]] to true. + self.promise_is_handled = true; + } + } + + match result_capability { + // 13. If resultCapability is undefined, then + // a. Return undefined. + None => JsValue::Undefined, + + // 14. Else, + // a. Return resultCapability.[[Promise]]. + Some(result_capability) => result_capability.promise.clone().into(), + } + } + + /// `PromiseResolve ( C, x )` + /// + /// The abstract operation `PromiseResolve` takes arguments `C` (a constructor) and `x` (an + /// ECMAScript language value) and returns either a normal completion containing an ECMAScript + /// language value or a throw completion. It returns a new promise resolved with `x`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-promise-resolve + pub(crate) fn promise_resolve( + c: JsObject, + x: JsValue, + context: &mut Context, + ) -> JsResult { + // 1. If IsPromise(x) is true, then + if let Some(x) = x.as_promise() { + // a. Let xConstructor be ? Get(x, "constructor"). + let x_constructor = x.get("constructor", context)?; + // b. If SameValue(xConstructor, C) is true, return x. + if JsValue::same_value(&x_constructor, &JsValue::from(c.clone())) { + return Ok(JsValue::from(x.clone())); + } + } + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let promise_capability = PromiseCapability::new(&c.into(), context)?; + + // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). + context.call( + &promise_capability.resolve.clone().into(), + &JsValue::undefined(), + &[x], + )?; + + // 4. Return promiseCapability.[[Promise]]. + Ok(promise_capability.promise.clone().into()) + } + + /// `GetPromiseResolve ( promiseConstructor )` + /// + /// The abstract operation `GetPromiseResolve` takes argument `promiseConstructor` (a + /// constructor) and returns either a normal completion containing a function object or a throw + /// completion. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-getpromiseresolve + fn get_promise_resolve( + promise_constructor: &JsObject, + context: &mut Context, + ) -> JsResult { + // 1. Let promiseResolve be ? Get(promiseConstructor, "resolve"). + let promise_resolve = promise_constructor.get("resolve", context)?; + + // 2. If IsCallable(promiseResolve) is false, throw a TypeError exception. + if let Some(promise_resolve) = promise_resolve.as_callable() { + // 3. Return promiseResolve. + Ok(promise_resolve.clone()) + } else { + context.throw_type_error("retrieving a non-callable promise resolver") + } + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/promise_job.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/promise_job.rs new file mode 100644 index 0000000..30b486e --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/promise_job.rs @@ -0,0 +1,188 @@ +use super::{Promise, PromiseCapability}; +use crate::{ + builtins::promise::{ReactionRecord, ReactionType}, + job::JobCallback, + object::{FunctionBuilder, JsObject}, + Context, JsValue, +}; +use boa_gc::{Finalize, Trace}; + +#[derive(Debug, Clone, Copy)] +pub(crate) struct PromiseJob; + +impl PromiseJob { + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromisereactionjob + pub(crate) fn new_promise_reaction_job( + reaction: ReactionRecord, + argument: JsValue, + context: &mut Context, + ) -> JobCallback { + #[derive(Debug, Trace, Finalize)] + struct ReactionJobCaptures { + reaction: ReactionRecord, + argument: JsValue, + } + + // 1. Let job be a new Job Abstract Closure with no parameters that captures reaction and argument and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this, _args, captures, context| { + let ReactionJobCaptures { reaction, argument } = captures; + + let ReactionRecord { + // a. Let promiseCapability be reaction.[[Capability]]. + promise_capability, + // b. Let type be reaction.[[Type]]. + reaction_type, + // c. Let handler be reaction.[[Handler]]. + handler, + } = reaction; + + let handler_result = match handler { + // d. If handler is empty, then + None => match reaction_type { + // i. If type is Fulfill, let handlerResult be NormalCompletion(argument). + ReactionType::Fulfill => Ok(argument.clone()), + // ii. Else, + // 1. Assert: type is Reject. + ReactionType::Reject => { + // 2. Let handlerResult be ThrowCompletion(argument). + Err(argument.clone()) + } + }, + // e. Else, let handlerResult be Completion(HostCallJobCallback(handler, undefined, « argument »)). + Some(handler) => { + handler.call_job_callback(&JsValue::Undefined, &[argument.clone()], context) + } + }; + + match promise_capability { + None => { + // f. If promiseCapability is undefined, then + // i. Assert: handlerResult is not an abrupt completion. + assert!( + handler_result.is_ok(), + "Assertion: failed" + ); + + // ii. Return empty. + Ok(JsValue::Undefined) + } + Some(promise_capability_record) => { + // g. Assert: promiseCapability is a PromiseCapability Record. + let PromiseCapability { + promise: _, + resolve, + reject, + } = promise_capability_record; + + match handler_result { + // h. If handlerResult is an abrupt completion, then + Err(value) => { + // i. Return ? Call(promiseCapability.[[Reject]], undefined, « handlerResult.[[Value]] »). + context.call(&reject.clone().into(), &JsValue::Undefined, &[value]) + } + + // i. Else, + Ok(value) => { + // i. Return ? Call(promiseCapability.[[Resolve]], undefined, « handlerResult.[[Value]] »). + context.call(&resolve.clone().into(), &JsValue::Undefined, &[value]) + } + } + } + } + }, + ReactionJobCaptures { reaction, argument }, + ) + .build() + .into(); + + // 2. Let handlerRealm be null. + // 3. If reaction.[[Handler]] is not empty, then + // a. Let getHandlerRealmResult be Completion(GetFunctionRealm(reaction.[[Handler]].[[Callback]])). + // b. If getHandlerRealmResult is a normal completion, set handlerRealm to getHandlerRealmResult.[[Value]]. + // c. Else, set handlerRealm to the current Realm Record. + // d. NOTE: handlerRealm is never null unless the handler is undefined. When the handler is a revoked Proxy and no ECMAScript code runs, handlerRealm is used to create error objects. + // 4. Return the Record { [[Job]]: job, [[Realm]]: handlerRealm }. + JobCallback::make_job_callback(job) + } + + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-newpromiseresolvethenablejob + pub(crate) fn new_promise_resolve_thenable_job( + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, + context: &mut Context, + ) -> JobCallback { + // 1. Let job be a new Job Abstract Closure with no parameters that captures promiseToResolve, thenable, and then and performs the following steps when called: + let job = FunctionBuilder::closure_with_captures( + context, + |_this: &JsValue, _args: &[JsValue], captures, context: &mut Context| { + let JobCapture { + promise_to_resolve, + thenable, + then, + } = captures; + + // a. Let resolvingFunctions be CreateResolvingFunctions(promiseToResolve). + let resolving_functions = + Promise::create_resolving_functions(promise_to_resolve, context); + + // b. Let thenCallResult be Completion(HostCallJobCallback(then, thenable, « resolvingFunctions.[[Resolve]], resolvingFunctions.[[Reject]] »)). + let then_call_result = then.call_job_callback( + thenable, + &[ + resolving_functions.resolve, + resolving_functions.reject.clone(), + ], + context, + ); + + // c. If thenCallResult is an abrupt completion, then + if let Err(value) = then_call_result { + // i. Return ? Call(resolvingFunctions.[[Reject]], undefined, « thenCallResult.[[Value]] »). + return context.call( + &resolving_functions.reject, + &JsValue::Undefined, + &[value], + ); + } + + // d. Return ? thenCallResult. + then_call_result + }, + JobCapture::new(promise_to_resolve, thenable, then), + ) + .build(); + + // 2. Let getThenRealmResult be Completion(GetFunctionRealm(then.[[Callback]])). + // 3. If getThenRealmResult is a normal completion, let thenRealm be getThenRealmResult.[[Value]]. + // 4. Else, let thenRealm be the current Realm Record. + // 5. NOTE: thenRealm is never null. When then.[[Callback]] is a revoked Proxy and no code runs, thenRealm is used to create error objects. + // 6. Return the Record { [[Job]]: job, [[Realm]]: thenRealm }. + JobCallback::make_job_callback(job.into()) + } +} + +#[derive(Debug, Trace, Finalize)] +struct JobCapture { + promise_to_resolve: JsObject, + thenable: JsValue, + then: JobCallback, +} + +impl JobCapture { + fn new(promise_to_resolve: JsObject, thenable: JsValue, then: JobCallback) -> Self { + Self { + promise_to_resolve, + thenable, + then, + } + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/tests.rs new file mode 100644 index 0000000..f32dcd2 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/promise/tests.rs @@ -0,0 +1,19 @@ +use crate::{forward, Context}; + +#[test] +fn promise() { + let mut context = Context::default(); + let init = r#" + let count = 0; + const promise = new Promise((resolve, reject) => { + count += 1; + resolve(undefined); + }).then((_) => (count += 1)); + count += 1; + count; + "#; + let result = context.eval(init).unwrap(); + assert_eq!(result.as_number(), Some(2_f64)); + let after_completion = forward(&mut context, "count"); + assert_eq!(after_completion, String::from("3")); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/proxy/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/proxy/mod.rs new file mode 100644 index 0000000..91f58fa --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/proxy/mod.rs @@ -0,0 +1,186 @@ +//! This module implements the global `Proxy` object. +//! +//! The `Proxy` object enables you to create a proxy for another object, +//! which can intercept and redefine fundamental operations for that object. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-proxy-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy + +use crate::{ + builtins::{BuiltIn, JsArgs}, + object::{ConstructorBuilder, FunctionBuilder, JsFunction, JsObject, ObjectData}, + Context, JsResult, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; +/// Javascript `Proxy` object. +#[derive(Debug, Clone, Trace, Finalize)] +pub struct Proxy { + // (target, handler) + data: Option<(JsObject, JsObject)>, +} + +impl BuiltIn for Proxy { + const NAME: &'static str = "Proxy"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().proxy().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .has_prototype_property(false) + .static_method(Self::revocable, "revocable", 2) + .build() + .conv::() + .pipe(Some) + } +} + +impl Proxy { + const LENGTH: usize = 2; + + pub(crate) fn new(target: JsObject, handler: JsObject) -> Self { + Self { + data: Some((target, handler)), + } + } + + /// This is an internal method only built for usage in the proxy internal methods. + /// + /// It returns the (target, handler) of the proxy. + pub(crate) fn try_data(&self, context: &mut Context) -> JsResult<(JsObject, JsObject)> { + self.data.clone().ok_or_else(|| { + context.construct_type_error("Proxy object has empty handler and target") + }) + } + + /// `28.2.1.1 Proxy ( target, handler )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-proxy-target-handler + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context.throw_type_error("Proxy constructor called on undefined new target"); + } + + // 2. Return ? ProxyCreate(target, handler). + Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context).map(JsValue::from) + } + + // `10.5.14 ProxyCreate ( target, handler )` + // + // More information: + // - [ECMAScript reference][spec] + // + // [spec]: https://tc39.es/ecma262/#sec-proxycreate + pub(crate) fn create( + target: &JsValue, + handler: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. If Type(target) is not Object, throw a TypeError exception. + let target = target.as_object().ok_or_else(|| { + context.construct_type_error("Proxy constructor called with non-object target") + })?; + + // 2. If Type(handler) is not Object, throw a TypeError exception. + let handler = handler.as_object().ok_or_else(|| { + context.construct_type_error("Proxy constructor called with non-object handler") + })?; + + // 3. Let P be ! MakeBasicObject(« [[ProxyHandler]], [[ProxyTarget]] »). + // 4. Set P's essential internal methods, except for [[Call]] and [[Construct]], to the definitions specified in 10.5. + // 5. If IsCallable(target) is true, then + // a. Set P.[[Call]] as specified in 10.5.12. + // b. If IsConstructor(target) is true, then + // i. Set P.[[Construct]] as specified in 10.5.13. + // 6. Set P.[[ProxyTarget]] to target. + // 7. Set P.[[ProxyHandler]] to handler. + let p = JsObject::from_proto_and_data( + context.intrinsics().constructors().object().prototype(), + ObjectData::proxy( + Self::new(target.clone(), handler.clone()), + target.is_callable(), + target.is_constructor(), + ), + ); + + // 8. Return P. + Ok(p) + } + + pub(crate) fn revoker(proxy: JsObject, context: &mut Context) -> JsFunction { + // 3. Let revoker be ! CreateBuiltinFunction(revokerClosure, 0, "", « [[RevocableProxy]] »). + // 4. Set revoker.[[RevocableProxy]] to p. + FunctionBuilder::closure_with_captures( + context, + |_, _, revocable_proxy, _| { + // a. Let F be the active function object. + // b. Let p be F.[[RevocableProxy]]. + // d. Set F.[[RevocableProxy]] to null. + if let Some(p) = revocable_proxy.take() { + // e. Assert: p is a Proxy object. + // f. Set p.[[ProxyTarget]] to null. + // g. Set p.[[ProxyHandler]] to null. + p.borrow_mut() + .as_proxy_mut() + .expect("[[RevocableProxy]] must be a proxy object") + .data = None; + } + + // c. If p is null, return undefined. + // h. Return undefined. + Ok(JsValue::undefined()) + }, + Some(proxy), + ) + .build() + } + + /// `28.2.2.1 Proxy.revocable ( target, handler )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-proxy.revocable + fn revocable(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let p be ? ProxyCreate(target, handler). + let p = Self::create(args.get_or_undefined(0), args.get_or_undefined(1), context)?; + + // Revoker creation steps on `Proxy::revoker` + let revoker = Self::revoker(p.clone(), context); + + // 5. Let result be ! OrdinaryObjectCreate(%Object.prototype%). + let result = context.construct_object(); + + // 6. Perform ! CreateDataPropertyOrThrow(result, "proxy", p). + result + .create_data_property_or_throw("proxy", p, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + + // 7. Perform ! CreateDataPropertyOrThrow(result, "revoke", revoker). + result + .create_data_property_or_throw("revoke", revoker, context) + .expect("CreateDataPropertyOrThrow cannot fail here"); + + // 8. Return result. + Ok(result.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/mod.rs new file mode 100644 index 0000000..52da256 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/mod.rs @@ -0,0 +1,397 @@ +//! This module implements the global `Reflect` object. +//! +//! The `Reflect` global object is a built-in object that provides methods for interceptable +//! JavaScript operations. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-reflect-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect + +use super::{Array, JsArgs}; +use crate::{ + builtins::{self, BuiltIn}, + object::ObjectInitializer, + property::Attribute, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +/// Javascript `Reflect` object. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Reflect; + +impl BuiltIn for Reflect { + const NAME: &'static str = "Reflect"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + + ObjectInitializer::new(context) + .function(Self::apply, "apply", 3) + .function(Self::construct, "construct", 2) + .function(Self::define_property, "defineProperty", 3) + .function(Self::delete_property, "deleteProperty", 2) + .function(Self::get, "get", 2) + .function( + Self::get_own_property_descriptor, + "getOwnPropertyDescriptor", + 2, + ) + .function(Self::get_prototype_of, "getPrototypeOf", 1) + .function(Self::has, "has", 2) + .function(Self::is_extensible, "isExtensible", 1) + .function(Self::own_keys, "ownKeys", 1) + .function(Self::prevent_extensions, "preventExtensions", 1) + .function(Self::set, "set", 3) + .function(Self::set_prototype_of, "setPrototypeOf", 2) + .property( + to_string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl Reflect { + /// Calls a target function with arguments. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.apply + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply + pub(crate) fn apply(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be a function"))?; + let this_arg = args.get_or_undefined(1); + let args_list = args.get_or_undefined(2); + + if !target.is_callable() { + return context.throw_type_error("target must be a function"); + } + let args = args_list.create_list_from_array_like(&[], context)?; + target.call(this_arg, &args, context) + } + + /// Calls a target function as a constructor with arguments. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.construct + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct + pub(crate) fn construct( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If IsConstructor(target) is false, throw a TypeError exception. + let target = args + .get_or_undefined(0) + .as_constructor() + .ok_or_else(|| context.construct_type_error("target must be a constructor"))?; + + let new_target = if let Some(new_target) = args.get(2) { + // 3. Else if IsConstructor(newTarget) is false, throw a TypeError exception. + if let Some(new_target) = new_target.as_constructor() { + new_target + } else { + return context.throw_type_error("newTarget must be a constructor"); + } + } else { + // 2. If newTarget is not present, set newTarget to target. + target + }; + + // 4. Let args be ? CreateListFromArrayLike(argumentsList). + let args = args + .get_or_undefined(1) + .create_list_from_array_like(&[], context)?; + + // 5. Return ? Construct(target, args, newTarget). + target + .construct(&args, Some(new_target), context) + .map(JsValue::from) + } + + /// Defines a property on an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.defineProperty + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty + pub(crate) fn define_property( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + let key = args.get_or_undefined(1).to_property_key(context)?; + let prop_desc: JsValue = args + .get(2) + .and_then(|v| v.as_object().cloned()) + .ok_or_else(|| context.construct_type_error("property descriptor must be an object"))? + .into(); + + target + .__define_own_property__(key, prop_desc.to_property_descriptor(context)?, context) + .map(Into::into) + } + + /// Defines a property on an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.deleteproperty + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty + pub(crate) fn delete_property( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + let key = args.get_or_undefined(1).to_property_key(context)?; + + Ok(target.__delete__(&key, context)?.into()) + } + + /// Gets a property of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.get + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get + pub(crate) fn get(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. If Type(target) is not Object, throw a TypeError exception. + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + // 2. Let key be ? ToPropertyKey(propertyKey). + let key = args.get_or_undefined(1).to_property_key(context)?; + // 3. If receiver is not present, then + let receiver = if let Some(receiver) = args.get(2).cloned() { + receiver + } else { + // 3.a. Set receiver to target. + target.clone().into() + }; + // 4. Return ? target.[[Get]](key, receiver). + target.__get__(&key, receiver, context) + } + + /// Gets a property of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.getownpropertydescriptor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor + pub(crate) fn get_own_property_descriptor( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if args.get_or_undefined(0).is_object() { + // This function is the same as Object.prototype.getOwnPropertyDescriptor, that why + // it is invoked here. + builtins::object::Object::get_own_property_descriptor( + &JsValue::undefined(), + args, + context, + ) + } else { + context.throw_type_error("target must be an object") + } + } + + /// Gets the prototype of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.getprototypeof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf + pub(crate) fn get_prototype_of( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + Ok(target + .__get_prototype_of__(context)? + .map_or(JsValue::Null, JsValue::new)) + } + + /// Returns `true` if the object has the property, `false` otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.has + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has + pub(crate) fn has(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + let key = args + .get(1) + .unwrap_or(&JsValue::undefined()) + .to_property_key(context)?; + Ok(target.__has_property__(&key, context)?.into()) + } + + /// Returns `true` if the object is extensible, `false` otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.isextensible + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/isExtensible + pub(crate) fn is_extensible( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + Ok(target.__is_extensible__(context)?.into()) + } + + /// Returns an array of object own property keys. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.ownkeys + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys + pub(crate) fn own_keys( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + + let keys: Vec = target + .__own_property_keys__(context)? + .into_iter() + .map(Into::into) + .collect(); + + Ok(Array::create_array_from_list(keys, context).into()) + } + + /// Prevents new properties from ever being added to an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.preventextensions + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/preventExtensions + pub(crate) fn prevent_extensions( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + + Ok(target.__prevent_extensions__(context)?.into()) + } + + /// Sets a property of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.set + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set + pub(crate) fn set(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + let key = args.get_or_undefined(1).to_property_key(context)?; + let value = args.get_or_undefined(2); + let receiver = if let Some(receiver) = args.get(3).cloned() { + receiver + } else { + target.clone().into() + }; + Ok(target + .__set__(key, value.clone(), receiver, context)? + .into()) + } + + /// Sets the prototype of an object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-reflect.setprototypeof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf + pub(crate) fn set_prototype_of( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let target = args + .get(0) + .and_then(JsValue::as_object) + .ok_or_else(|| context.construct_type_error("target must be an object"))?; + let proto = match args.get_or_undefined(1) { + JsValue::Object(obj) => Some(obj.clone()), + JsValue::Null => None, + _ => return context.throw_type_error("proto must be an object or null"), + }; + Ok(target.__set_prototype_of__(proto, context)?.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/tests.rs new file mode 100644 index 0000000..87ad4cc --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/reflect/tests.rs @@ -0,0 +1,191 @@ +use crate::{forward, Context}; + +#[test] +fn apply() { + let mut context = Context::default(); + + let init = r#" + var called = {}; + function f(n) { called.result = n }; + Reflect.apply(f, undefined, [42]); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "called.result"), "42"); +} + +#[test] +fn construct() { + let mut context = Context::default(); + + let init = r#" + var called = {}; + function f(n) { called.result = n }; + Reflect.construct(f, [42]); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "called.result"), "42"); +} + +#[test] +fn define_property() { + let mut context = Context::default(); + + let init = r#" + let obj = {}; + Reflect.defineProperty(obj, 'p', { value: 42 }); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "obj.p"), "42"); +} + +#[test] +fn delete_property() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let deleted = Reflect.deleteProperty(obj, 'p'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "obj.p"), "undefined"); + assert_eq!(forward(&mut context, "deleted"), "true"); +} + +#[test] +fn get() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 } + let p = Reflect.get(obj, 'p'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "p"), "42"); +} + +#[test] +fn get_own_property_descriptor() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let desc = Reflect.getOwnPropertyDescriptor(obj, 'p'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "desc.value"), "42"); +} + +#[test] +fn get_prototype_of() { + let mut context = Context::default(); + + let init = r#" + function F() { this.p = 42 }; + let f = new F(); + let proto = Reflect.getPrototypeOf(f); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "proto.constructor.name"), "\"F\""); +} + +#[test] +fn has() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let hasP = Reflect.has(obj, 'p'); + let hasP2 = Reflect.has(obj, 'p2'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "hasP"), "true"); + assert_eq!(forward(&mut context, "hasP2"), "false"); +} + +#[test] +fn is_extensible() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let isExtensible = Reflect.isExtensible(obj); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "isExtensible"), "true"); +} + +#[test] +fn own_keys() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let ownKeys = Reflect.ownKeys(obj); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "ownKeys"), r#"[ "p" ]"#); +} + +#[test] +fn prevent_extensions() { + let mut context = Context::default(); + + let init = r#" + let obj = { p: 42 }; + let r = Reflect.preventExtensions(obj); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "r"), "true"); +} + +#[test] +fn set() { + let mut context = Context::default(); + + let init = r#" + let obj = {}; + Reflect.set(obj, 'p', 42); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "obj.p"), "42"); +} + +#[test] +fn set_prototype_of() { + let mut context = Context::default(); + + let init = r#" + function F() { this.p = 42 }; + let obj = {} + Reflect.setPrototypeOf(obj, F); + let p = Reflect.getPrototypeOf(obj); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "p.name"), "\"F\""); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/mod.rs new file mode 100644 index 0000000..485e46e --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/mod.rs @@ -0,0 +1,1754 @@ +//! This module implements the global `RegExp` object. +//! +//! The `RegExp` object is used for matching text with a pattern. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-regexp-constructor +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp + +pub mod regexp_string_iterator; + +use self::regexp_string_iterator::RegExpStringIterator; +use super::JsArgs; +use crate::{ + builtins::{array::Array, string, BuiltIn}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptorBuilder}, + symbol::WellKnownSymbols, + syntax::lexer::regex::RegExpFlags, + value::{IntegerOrInfinity, JsValue}, + Context, JsResult, JsString, +}; +use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_profiler::Profiler; +use regress::Regex; +use std::str::FromStr; +use tap::{Conv, Pipe}; + +#[cfg(test)] +mod tests; + +/// The internal representation on a `RegExp` object. +#[derive(Debug, Clone, Finalize)] +pub struct RegExp { + /// Regex matcher. + matcher: Regex, + flags: RegExpFlags, + original_source: JsString, + original_flags: JsString, +} + +// Only safe while regress::Regex doesn't implement Trace itself. +unsafe impl Trace for RegExp { + unsafe_empty_trace!(); +} + +impl BuiltIn for RegExp { + const NAME: &'static str = "RegExp"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let flag_attributes = Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE; + + let get_has_indices = FunctionBuilder::native(context, Self::get_has_indices) + .name("get hasIndices") + .constructor(false) + .build(); + let get_global = FunctionBuilder::native(context, Self::get_global) + .name("get global") + .constructor(false) + .build(); + let get_ignore_case = FunctionBuilder::native(context, Self::get_ignore_case) + .name("get ignoreCase") + .constructor(false) + .build(); + let get_multiline = FunctionBuilder::native(context, Self::get_multiline) + .name("get multiline") + .constructor(false) + .build(); + let get_dot_all = FunctionBuilder::native(context, Self::get_dot_all) + .name("get dotAll") + .constructor(false) + .build(); + let get_unicode = FunctionBuilder::native(context, Self::get_unicode) + .name("get unicode") + .constructor(false) + .build(); + let get_sticky = FunctionBuilder::native(context, Self::get_sticky) + .name("get sticky") + .constructor(false) + .build(); + let get_flags = FunctionBuilder::native(context, Self::get_flags) + .name("get flags") + .constructor(false) + .build(); + let get_source = FunctionBuilder::native(context, Self::get_source) + .name("get source") + .constructor(false) + .build(); + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().regexp().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .property("lastIndex", 0, Attribute::all()) + .method(Self::test, "test", 1) + .method(Self::exec, "exec", 1) + .method(Self::to_string, "toString", 0) + .method( + Self::r#match, + (WellKnownSymbols::r#match(), "[Symbol.match]"), + 1, + ) + .method( + Self::match_all, + (WellKnownSymbols::match_all(), "[Symbol.matchAll]"), + 1, + ) + .method( + Self::replace, + (WellKnownSymbols::replace(), "[Symbol.replace]"), + 2, + ) + .method( + Self::search, + (WellKnownSymbols::search(), "[Symbol.search]"), + 1, + ) + .method( + Self::split, + (WellKnownSymbols::split(), "[Symbol.split]"), + 2, + ) + .accessor("hasIndices", Some(get_has_indices), None, flag_attributes) + .accessor("global", Some(get_global), None, flag_attributes) + .accessor("ignoreCase", Some(get_ignore_case), None, flag_attributes) + .accessor("multiline", Some(get_multiline), None, flag_attributes) + .accessor("dotAll", Some(get_dot_all), None, flag_attributes) + .accessor("unicode", Some(get_unicode), None, flag_attributes) + .accessor("sticky", Some(get_sticky), None, flag_attributes) + .accessor("flags", Some(get_flags), None, flag_attributes) + .accessor("source", Some(get_source), None, flag_attributes) + .build() + .conv::() + .pipe(Some) + } +} + +impl RegExp { + /// The name of the object. + pub(crate) const NAME: &'static str = "RegExp"; + + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 2; + + /// `22.2.3.1 RegExp ( pattern, flags )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp-pattern-flags + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let pattern = args.get_or_undefined(0); + let flags = args.get_or_undefined(1); + + // 1. Let patternIsRegExp be ? IsRegExp(pattern). + let pattern_is_regexp = pattern.as_object().filter(|obj| obj.is_regexp()); + + // 2. If NewTarget is undefined, then + // 3. Else, let newTarget be NewTarget. + if new_target.is_undefined() { + // a. Let newTarget be the active function object. + // b. If patternIsRegExp is true and flags is undefined, then + if let Some(pattern) = pattern_is_regexp { + if flags.is_undefined() { + // i. Let patternConstructor be ? Get(pattern, "constructor"). + let pattern_constructor = pattern.get("constructor", context)?; + // ii. If SameValue(newTarget, patternConstructor) is true, return pattern. + if JsValue::same_value(new_target, &pattern_constructor) { + return Ok(pattern.clone().into()); + } + } + } + } + + // 4. If Type(pattern) is Object and pattern has a [[RegExpMatcher]] internal slot, then + // 6. Else, + let (p, f) = if let Some(pattern) = pattern_is_regexp { + let obj = pattern.borrow(); + let regexp = obj + .as_regexp() + .expect("already checked that IsRegExp returns true"); + + // a. Let P be pattern.[[OriginalSource]]. + // b. If flags is undefined, let F be pattern.[[OriginalFlags]]. + // c. Else, let F be flags. + if flags.is_undefined() { + ( + JsValue::new(regexp.original_source.clone()), + JsValue::new(regexp.original_flags.clone()), + ) + } else { + (JsValue::new(regexp.original_source.clone()), flags.clone()) + } + } else { + // a. Let P be pattern. + // b. Let F be flags. + (pattern.clone(), flags.clone()) + }; + + // 7. Let O be ? RegExpAlloc(newTarget). + let o = Self::alloc(new_target, context)?; + + // 8.Return ? RegExpInitialize(O, P, F). + Self::initialize(o, &p, &f, context) + } + + /// `22.2.3.2.1 RegExpAlloc ( newTarget )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexpalloc + fn alloc(new_target: &JsValue, context: &mut Context) -> JsResult { + // 1. Let obj be ? OrdinaryCreateFromConstructor(newTarget, "%RegExp.prototype%", « [[RegExpMatcher]], [[OriginalSource]], [[OriginalFlags]] »). + let proto = + get_prototype_from_constructor(new_target, StandardConstructors::regexp, context)?; + let obj = JsObject::from_proto_and_data(proto, ObjectData::ordinary()); + + // 2. Perform ! DefinePropertyOrThrow(obj, "lastIndex", PropertyDescriptor { [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }). + obj.define_property_or_throw( + "lastIndex", + PropertyDescriptorBuilder::new() + .writable(true) + .enumerable(false) + .configurable(false) + .build(), + context, + ) + .expect("this cannot fail per spec"); + + // 3. Return obj. + Ok(obj) + } + + /// `22.2.3.2.2 RegExpInitialize ( obj, pattern, flags )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexpinitialize + fn initialize( + obj: JsObject, + pattern: &JsValue, + flags: &JsValue, + context: &mut Context, + ) -> JsResult { + // 1. If pattern is undefined, let P be the empty String. + // 2. Else, let P be ? ToString(pattern). + let p = if pattern.is_undefined() { + JsString::new("") + } else { + pattern.to_string(context)? + }; + + // 3. If flags is undefined, let F be the empty String. + // 4. Else, let F be ? ToString(flags). + let f = if flags.is_undefined() { + JsString::new("") + } else { + flags.to_string(context)? + }; + + // 5. If F contains any code unit other than "g", "i", "m", "s", "u", or "y" + // or if it contains the same code unit more than once, throw a SyntaxError exception. + let flags = match RegExpFlags::from_str(&f) { + Err(msg) => return context.throw_syntax_error(msg), + Ok(result) => result, + }; + + // TODO: Correct UTF-16 handling in 6. - 8. + + // 9. Let parseResult be ParsePattern(patternText, u). + // 10. If parseResult is a non-empty List of SyntaxError objects, throw a SyntaxError exception. + // 11. Assert: parseResult is a Pattern Parse Node. + // 12. Set obj.[[OriginalSource]] to P. + // 13. Set obj.[[OriginalFlags]] to F. + // 14. NOTE: The definitions of DotAll, IgnoreCase, Multiline, and Unicode in 22.2.2.1 refer to this value of obj.[[OriginalFlags]]. + // 15. Set obj.[[RegExpMatcher]] to CompilePattern of parseResult. + let matcher = match Regex::with_flags(&p, f.as_ref()) { + Err(error) => { + return context + .throw_syntax_error(format!("failed to create matcher: {}", error.text)); + } + Ok(val) => val, + }; + let regexp = Self { + matcher, + flags, + original_source: p, + original_flags: f, + }; + obj.borrow_mut().data = ObjectData::reg_exp(Box::new(regexp)); + + // 16. Perform ? Set(obj, "lastIndex", +0𝔽, true). + obj.set("lastIndex", 0, true, context)?; + + // 16. Return obj. + Ok(obj.into()) + } + + /// `22.2.3.2.4 RegExpCreate ( P, F )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexpcreate + pub(crate) fn create(p: &JsValue, f: &JsValue, context: &mut Context) -> JsResult { + // 1. Let obj be ? RegExpAlloc(%RegExp%). + let obj = Self::alloc( + &context.global_object().clone().get(Self::NAME, context)?, + context, + )?; + + // 2. Return ? RegExpInitialize(obj, P, F). + Self::initialize(obj, p, f, context) + } + + /// `get RegExp [ @@species ]` + /// + /// The `RegExp [ @@species ]` accessor property returns the `RegExp` constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + #[inline] + fn regexp_has_flag(this: &JsValue, flag: u8, context: &mut Context) -> JsResult { + if let Some(object) = this.as_object() { + if let Some(regexp) = object.borrow().as_regexp() { + return Ok(JsValue::new(match flag { + b'd' => regexp.flags.contains(RegExpFlags::HAS_INDICES), + b'g' => regexp.flags.contains(RegExpFlags::GLOBAL), + b'm' => regexp.flags.contains(RegExpFlags::MULTILINE), + b's' => regexp.flags.contains(RegExpFlags::DOT_ALL), + b'i' => regexp.flags.contains(RegExpFlags::IGNORE_CASE), + b'u' => regexp.flags.contains(RegExpFlags::UNICODE), + b'y' => regexp.flags.contains(RegExpFlags::STICKY), + _ => unreachable!(), + })); + } + + if JsObject::equals( + object, + &context.intrinsics().constructors().regexp().prototype, + ) { + return Ok(JsValue::undefined()); + } + } + + let name = match flag { + b'd' => "hasIndices", + b'g' => "global", + b'm' => "multiline", + b's' => "dotAll", + b'i' => "ignoreCase", + b'u' => "unicode", + b'y' => "sticky", + _ => unreachable!(), + }; + + context.throw_type_error(format!( + "RegExp.prototype.{name} getter called on non-RegExp object", + )) + } + + /// `get RegExp.prototype.hasIndices` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.hasindices + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global + pub(crate) fn get_has_indices( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'd', context) + } + + /// `get RegExp.prototype.global` + /// + /// The `global` property indicates whether or not the "`g`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.global + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global + pub(crate) fn get_global( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'g', context) + } + + /// `get RegExp.prototype.ignoreCase` + /// + /// The `ignoreCase` property indicates whether or not the "`i`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.ignorecase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/ignoreCase + pub(crate) fn get_ignore_case( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'i', context) + } + + /// `get RegExp.prototype.multiline` + /// + /// The multiline property indicates whether or not the "m" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.multiline + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/multiline + pub(crate) fn get_multiline( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'm', context) + } + + /// `get RegExp.prototype.dotAll` + /// + /// The `dotAll` property indicates whether or not the "`s`" flag is used with the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.dotAll + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/dotAll + pub(crate) fn get_dot_all( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b's', context) + } + + /// `get RegExp.prototype.unicode` + /// + /// The unicode property indicates whether or not the "`u`" flag is used with a regular expression. + /// unicode is a read-only property of an individual regular expression instance. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.unicode + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/unicode + pub(crate) fn get_unicode( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'u', context) + } + + /// `get RegExp.prototype.sticky` + /// + /// This flag indicates that it matches only from the index indicated by the `lastIndex` property + /// of this regular expression in the target string (and does not attempt to match from any later indexes). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.sticky + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/sticky + pub(crate) fn get_sticky( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + Self::regexp_has_flag(this, b'y', context) + } + + /// `get RegExp.prototype.flags` + /// + /// The `flags` property returns a string consisting of the [`flags`][flags] of the current regular expression object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.flags + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/flags + /// [flags]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions#Advanced_searching_with_flags_2 + pub(crate) fn get_flags( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let R be the this value. + // 2. If Type(R) is not Object, throw a TypeError exception. + if let Some(object) = this.as_object() { + // 3. Let result be the empty String. + let mut result = String::new(); + + // 4. Let hasIndices be ToBoolean(? Get(R, "hasIndices")). + // 5. If hasIndices is true, append the code unit 0x0064 (LATIN SMALL LETTER D) as the last code unit of result. + if object.get("hasIndices", context)?.to_boolean() { + result.push('d'); + } + + // 6. Let global be ! ToBoolean(? Get(R, "global")). + // 7. If global is true, append the code unit 0x0067 (LATIN SMALL LETTER G) as the last code unit of result. + if object.get("global", context)?.to_boolean() { + result.push('g'); + } + // 8. Let ignoreCase be ! ToBoolean(? Get(R, "ignoreCase")). + // 9. If ignoreCase is true, append the code unit 0x0069 (LATIN SMALL LETTER I) as the last code unit of result. + if object.get("ignoreCase", context)?.to_boolean() { + result.push('i'); + } + + // 10. Let multiline be ! ToBoolean(? Get(R, "multiline")). + // 11. If multiline is true, append the code unit 0x006D (LATIN SMALL LETTER M) as the last code unit of result. + if object.get("multiline", context)?.to_boolean() { + result.push('m'); + } + + // 12. Let dotAll be ! ToBoolean(? Get(R, "dotAll")). + // 13. If dotAll is true, append the code unit 0x0073 (LATIN SMALL LETTER S) as the last code unit of result. + if object.get("dotAll", context)?.to_boolean() { + result.push('s'); + } + // 14. Let unicode be ! ToBoolean(? Get(R, "unicode")). + // 15. If unicode is true, append the code unit 0x0075 (LATIN SMALL LETTER U) as the last code unit of result. + if object.get("unicode", context)?.to_boolean() { + result.push('u'); + } + + // 16. Let sticky be ! ToBoolean(? Get(R, "sticky")). + // 17. If sticky is true, append the code unit 0x0079 (LATIN SMALL LETTER Y) as the last code unit of result. + if object.get("sticky", context)?.to_boolean() { + result.push('y'); + } + + // 18. Return result. + return Ok(result.into()); + } + + context.throw_type_error("RegExp.prototype.flags getter called on non-object") + } + + /// `get RegExp.prototype.source` + /// + /// The `source` property returns a `String` containing the source text of the regexp object, + /// and it doesn't contain the two forward slashes on both sides and any flags. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-regexp.prototype.source + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/source + pub(crate) fn get_source( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let R be the this value. + // 2. If Type(R) is not Object, throw a TypeError exception. + if let Some(object) = this.as_object() { + let object = object.borrow(); + + match object.as_regexp() { + // 3. If R does not have an [[OriginalSource]] internal slot, then + None => { + // a. If SameValue(R, %RegExp.prototype%) is true, return "(?:)". + // b. Otherwise, throw a TypeError exception. + if JsValue::same_value( + this, + &JsValue::new(context.intrinsics().constructors().regexp().prototype()), + ) { + Ok(JsValue::new("(?:)")) + } else { + context.throw_type_error( + "RegExp.prototype.source method called on incompatible value", + ) + } + } + // 4. Assert: R has an [[OriginalFlags]] internal slot. + Some(re) => { + // 5. Let src be R.[[OriginalSource]]. + // 6. Let flags be R.[[OriginalFlags]]. + // 7. Return EscapeRegExpPattern(src, flags). + Ok(Self::escape_pattern( + &re.original_source, + &re.original_flags, + )) + } + } + } else { + context.throw_type_error("RegExp.prototype.source method called on incompatible value") + } + } + + /// `22.2.3.2.5 EscapeRegExpPattern ( P, F )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-escaperegexppattern + fn escape_pattern(src: &str, _flags: &str) -> JsValue { + if src.is_empty() { + JsValue::new("(?:)") + } else { + let mut s = String::from(""); + + for c in src.chars() { + match c { + '/' => s.push_str("\\/"), + '\n' => s.push_str("\\\\n"), + '\r' => s.push_str("\\\\r"), + _ => s.push(c), + } + } + + JsValue::new(s) + } + } + + /// `RegExp.prototype.test( string )` + /// + /// The `test()` method executes a search for a match between a regular expression and a specified string. + /// + /// Returns `true` or `false`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.test + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/test + pub(crate) fn test( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let R be the this value. + // 2. If Type(R) is not Object, throw a TypeError exception. + let this = this.as_object().ok_or_else(|| { + context + .construct_type_error("RegExp.prototype.test method called on incompatible value") + })?; + + // 3. Let string be ? ToString(S). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let match be ? RegExpExec(R, string). + let m = Self::abstract_exec(this, arg_str, context)?; + + // 5. If match is not null, return true; else return false. + if m.is_some() { + Ok(JsValue::new(true)) + } else { + Ok(JsValue::new(false)) + } + } + + /// `RegExp.prototype.exec( string )` + /// + /// The exec() method executes a search for a match in a specified string. + /// + /// Returns a result array, or `null`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.exec + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/exec + pub(crate) fn exec( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let R be the this value. + // 2. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]). + let obj = this + .as_object() + .filter(|obj| obj.is_regexp()) + .ok_or_else(|| { + context.construct_type_error("RegExp.prototype.exec called with invalid value") + })?; + + // 3. Let S be ? ToString(string). + let arg_str = args.get_or_undefined(0).to_string(context)?; + + // 4. Return ? RegExpBuiltinExec(R, S). + if let Some(v) = Self::abstract_builtin_exec(obj, &arg_str, context)? { + Ok(v.into()) + } else { + Ok(JsValue::null()) + } + } + + /// `22.2.5.2.1 RegExpExec ( R, S )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexpexec + pub(crate) fn abstract_exec( + this: &JsObject, + input: JsString, + context: &mut Context, + ) -> JsResult> { + // 1. Assert: Type(R) is Object. + // 2. Assert: Type(S) is String. + + // 3. Let exec be ? Get(R, "exec"). + let exec = this.get("exec", context)?; + + // 4. If IsCallable(exec) is true, then + if let Some(exec) = exec.as_callable() { + // a. Let result be ? Call(exec, R, « S »). + let result = exec.call(&this.clone().into(), &[input.into()], context)?; + + // b. If Type(result) is neither Object nor Null, throw a TypeError exception. + if !result.is_object() && !result.is_null() { + return context.throw_type_error("regexp exec returned neither object nor null"); + } + + // c. Return result. + return Ok(result.as_object().cloned()); + } + + // 5. Perform ? RequireInternalSlot(R, [[RegExpMatcher]]). + if !this.is_regexp() { + return context.throw_type_error("RegExpExec called with invalid value"); + } + + // 6. Return ? RegExpBuiltinExec(R, S). + Self::abstract_builtin_exec(this, &input, context) + } + + /// `22.2.5.2.2 RegExpBuiltinExec ( R, S )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexpbuiltinexec + pub(crate) fn abstract_builtin_exec( + this: &JsObject, + input: &JsString, + context: &mut Context, + ) -> JsResult> { + // 1. Assert: R is an initialized RegExp instance. + let rx = { + let obj = this.borrow(); + if let Some(rx) = obj.as_regexp() { + rx.clone() + } else { + return context.throw_type_error("RegExpBuiltinExec called with invalid value"); + } + }; + + // 2. Assert: Type(S) is String. + + // 3. Let length be the number of code units in S. + let length = input.encode_utf16().count(); + + // 4. Let lastIndex be ℝ(? ToLength(? Get(R, "lastIndex"))). + let mut last_index = this.get("lastIndex", context)?.to_length(context)?; + + // 5. Let flags be R.[[OriginalFlags]]. + let flags = &rx.original_flags; + + // 6. If flags contains "g", let global be true; else let global be false. + let global = flags.contains('g'); + + // 7. If flags contains "y", let sticky be true; else let sticky be false. + let sticky = flags.contains('y'); + + // 8. If global is false and sticky is false, set lastIndex to 0. + if !global && !sticky { + last_index = 0; + } + + // 9. Let matcher be R.[[RegExpMatcher]]. + let matcher = &rx.matcher; + + // 10. If flags contains "u", let fullUnicode be true; else let fullUnicode be false. + let unicode = flags.contains('u'); + + // 11. Let matchSucceeded be false. + // 12. Repeat, while matchSucceeded is false, + let match_value = loop { + // a. If lastIndex > length, then + if last_index > length { + // i. If global is true or sticky is true, then + if global || sticky { + // 1. Perform ? Set(R, "lastIndex", +0𝔽, true). + this.set("lastIndex", 0, true, context)?; + } + + // ii. Return null. + return Ok(None); + } + + // b. Let r be matcher(S, lastIndex). + // Check if last_index is a valid utf8 index into input. + let last_byte_index = match String::from_utf16( + &input.encode_utf16().take(last_index).collect::>(), + ) { + Ok(s) => s.len(), + Err(_) => { + return context + .throw_type_error("Failed to get byte index from utf16 encoded string") + } + }; + let r = matcher.find_from(input, last_byte_index).next(); + + match r { + // c. If r is failure, then + None => { + // i. If sticky is true, then + if sticky { + // 1. Perform ? Set(R, "lastIndex", +0𝔽, true). + this.set("lastIndex", 0, true, context)?; + + // 2. Return null. + return Ok(None); + } + + // ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode). + last_index = advance_string_index(input, last_index, unicode); + } + + Some(m) => { + // c. If r is failure, then + #[allow(clippy::if_not_else)] + if m.start() != last_index { + // i. If sticky is true, then + if sticky { + // 1. Perform ? Set(R, "lastIndex", +0𝔽, true). + this.set("lastIndex", 0, true, context)?; + + // 2. Return null. + return Ok(None); + } + + // ii. Set lastIndex to AdvanceStringIndex(S, lastIndex, fullUnicode). + last_index = advance_string_index(input, last_index, unicode); + // d. Else, + } else { + //i. Assert: r is a State. + //ii. Set matchSucceeded to true. + break m; + } + } + } + }; + + // 13. Let e be r's endIndex value. + let mut e = match_value.end(); + + // 14. If fullUnicode is true, then + if unicode { + // e is an index into the Input character list, derived from S, matched by matcher. + // Let eUTF be the smallest index into S that corresponds to the character at element e of Input. + // If e is greater than or equal to the number of elements in Input, then eUTF is the number of code units in S. + // b. Set e to eUTF. + e = input.split_at(e).0.encode_utf16().count(); + } + + // 15. If global is true or sticky is true, then + if global || sticky { + // a. Perform ? Set(R, "lastIndex", 𝔽(e), true). + this.set("lastIndex", e, true, context)?; + } + + // 16. Let n be the number of elements in r's captures List. (This is the same value as 22.2.2.1's NcapturingParens.) + let n = match_value.captures.len(); + // 17. Assert: n < 23^2 - 1. + debug_assert!(n < 23usize.pow(2) - 1); + + // 18. Let A be ! ArrayCreate(n + 1). + // 19. Assert: The mathematical value of A's "length" property is n + 1. + let a = Array::array_create(n + 1, None, context)?; + + // 20. Perform ! CreateDataPropertyOrThrow(A, "index", 𝔽(lastIndex)). + a.create_data_property_or_throw("index", match_value.start(), context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 21. Perform ! CreateDataPropertyOrThrow(A, "input", S). + a.create_data_property_or_throw("input", input.clone(), context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 22. Let matchedSubstr be the substring of S from lastIndex to e. + let matched_substr = if let Some(s) = input.get(match_value.range()) { + s + } else { + "" + }; + + // 23. Perform ! CreateDataPropertyOrThrow(A, "0", matchedSubstr). + a.create_data_property_or_throw(0, matched_substr, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 24. If R contains any GroupName, then + // 25. Else, + let named_groups = match_value.named_groups(); + let groups = if named_groups.clone().count() > 0 { + // a. Let groups be ! OrdinaryObjectCreate(null). + let groups = JsValue::from(JsObject::empty()); + + // Perform 27.f here + // f. If the ith capture of R was defined with a GroupName, then + // i. Let s be the CapturingGroupName of the corresponding RegExpIdentifierName. + // ii. Perform ! CreateDataPropertyOrThrow(groups, s, capturedValue). + for (name, range) in named_groups { + if let Some(range) = range { + let value = if let Some(s) = input.get(range.clone()) { + s + } else { + "" + }; + + groups + .to_object(context)? + .create_data_property_or_throw(name, value, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + } + } + groups + } else { + // a. Let groups be undefined. + JsValue::undefined() + }; + + // 26. Perform ! CreateDataPropertyOrThrow(A, "groups", groups). + a.create_data_property_or_throw("groups", groups, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 27. For each integer i such that i ≥ 1 and i ≤ n, in ascending order, do + for i in 1..=n { + // a. Let captureI be ith element of r's captures List. + let capture = match_value.group(i); + + let captured_value = match capture { + // b. If captureI is undefined, let capturedValue be undefined. + None => JsValue::undefined(), + // c. Else if fullUnicode is true, then + // d. Else, + Some(range) => { + if let Some(s) = input.get(range) { + s.into() + } else { + "".into() + } + } + }; + + // e. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(i)), capturedValue). + a.create_data_property_or_throw(i, captured_value, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + } + + // 28. Return A. + Ok(Some(a)) + } + + /// `RegExp.prototype[ @@match ]( string )` + /// + /// This method retrieves the matches when matching a string against a regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@match + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@match + pub(crate) fn r#match( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + let rx = if let Some(rx) = this.as_object() { + rx + } else { + return context + .throw_type_error("RegExp.prototype.match method called on incompatible value"); + }; + + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let global be ! ToBoolean(? Get(rx, "global")). + let global = rx.get("global", context)?.to_boolean(); + + // 5. If global is false, then + #[allow(clippy::if_not_else)] + if !global { + // a. Return ? RegExpExec(rx, S). + if let Some(v) = Self::abstract_exec(rx, arg_str, context)? { + Ok(v.into()) + } else { + Ok(JsValue::null()) + } + // 6. Else, + } else { + // a. Assert: global is true. + + // b. Let fullUnicode be ! ToBoolean(? Get(rx, "unicode")). + let unicode = rx.get("unicode", context)?.to_boolean(); + + // c. Perform ? Set(rx, "lastIndex", +0𝔽, true). + rx.set("lastIndex", 0, true, context)?; + + // d. Let A be ! ArrayCreate(0). + let a = + Array::array_create(0, None, context).expect("this ArrayCreate call must not fail"); + + // e. Let n be 0. + let mut n = 0; + + // f. Repeat, + loop { + // i. Let result be ? RegExpExec(rx, S). + let result = Self::abstract_exec(rx, arg_str.clone(), context)?; + + // ii. If result is null, then + // iii. Else, + if let Some(result) = result { + // 1. Let matchStr be ? ToString(? Get(result, "0")). + let match_str = result.get("0", context)?.to_string(context)?; + + // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), matchStr). + a.create_data_property_or_throw(n, match_str.clone(), context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 3. If matchStr is the empty String, then + if match_str.is_empty() { + // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))). + let this_index = rx.get("lastIndex", context)?.to_length(context)?; + + // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode). + let next_index = advance_string_index(&arg_str, this_index, unicode); + + // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true). + rx.set("lastIndex", JsValue::new(next_index), true, context)?; + } + + // 4. Set n to n + 1. + n += 1; + } else { + // 1. If n = 0, return null. + if n == 0 { + return Ok(JsValue::null()); + } + // 2. Return A. + return Ok(a.into()); + } + } + } + } + + /// `RegExp.prototype.toString()` + /// + /// Return a string representing the regular expression. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let (body, flags) = if let Some(object) = this.as_object() { + let object = object.borrow(); + let regex = object.as_regexp().ok_or_else(|| { + context.construct_type_error(format!( + "Method RegExp.prototype.toString called on incompatible receiver {}", + this.display() + )) + })?; + (regex.original_source.clone(), regex.original_flags.clone()) + } else { + return context.throw_type_error(format!( + "Method RegExp.prototype.toString called on incompatible receiver {}", + this.display() + )); + }; + Ok(format!("/{body}/{flags}").into()) + } + + /// `RegExp.prototype[ @@matchAll ]( string )` + /// + /// The `[@@matchAll]` method returns all matches of the regular expression against a string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp-prototype-matchall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@matchAll + pub(crate) fn match_all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let R be the this value. + // 2. If Type(R) is not Object, throw a TypeError exception. + let regexp = this.as_object().ok_or_else(|| { + context.construct_type_error( + "RegExp.prototype.match_all method called on incompatible value", + ) + })?; + + // 3. Let S be ? ToString(string). + let arg_str = args.get_or_undefined(0).to_string(context)?; + + // 4. Let C be ? SpeciesConstructor(R, %RegExp%). + let c = regexp.species_constructor(StandardConstructors::regexp, context)?; + + // 5. Let flags be ? ToString(? Get(R, "flags")). + let flags = regexp.get("flags", context)?.to_string(context)?; + + // 6. Let matcher be ? Construct(C, « R, flags »). + let matcher = c.construct(&[this.clone(), flags.clone().into()], Some(&c), context)?; + + // 7. Let lastIndex be ? ToLength(? Get(R, "lastIndex")). + let last_index = regexp.get("lastIndex", context)?.to_length(context)?; + + // 8. Perform ? Set(matcher, "lastIndex", lastIndex, true). + matcher.set("lastIndex", last_index, true, context)?; + + // 9. If flags contains "g", let global be true. + // 10. Else, let global be false. + let global = flags.contains('g'); + + // 11. If flags contains "u", let fullUnicode be true. + // 12. Else, let fullUnicode be false. + let unicode = flags.contains('u'); + + // 13. Return ! CreateRegExpStringIterator(matcher, S, global, fullUnicode). + Ok(RegExpStringIterator::create_regexp_string_iterator( + matcher.clone(), + arg_str, + global, + unicode, + context, + )) + } + + /// `RegExp.prototype [ @@replace ] ( string, replaceValue )` + /// + /// The [@@replace]() method replaces some or all matches of a this pattern in a string by a replacement, + /// and returns the result of the replacement as a new string. + /// The replacement can be a string or a function to be called for each match. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@replace + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@replace + pub(crate) fn replace( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + let rx = if let Some(rx) = this.as_object() { + rx + } else { + return context.throw_type_error( + "RegExp.prototype[Symbol.replace] method called on incompatible value", + ); + }; + + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let lengthS be the number of code unit elements in S. + let length_arg_str = arg_str.encode_utf16().count(); + + // 5. Let functionalReplace be IsCallable(replaceValue). + let mut replace_value = args.get_or_undefined(1).clone(); + let functional_replace = replace_value + .as_object() + .map(JsObject::is_callable) + .unwrap_or_default(); + + // 6. If functionalReplace is false, then + if !functional_replace { + // a. Set replaceValue to ? ToString(replaceValue). + replace_value = replace_value.to_string(context)?.into(); + } + + // 7. Let global be ! ToBoolean(? Get(rx, "global")). + let global = rx.get("global", context)?.to_boolean(); + + // 8. If global is true, then + let mut unicode = false; + if global { + // a. Let fullUnicode be ! ToBoolean(? Get(rx, "unicode")). + unicode = rx.get("unicode", context)?.to_boolean(); + + // b. Perform ? Set(rx, "lastIndex", +0𝔽, true). + rx.set("lastIndex", 0, true, context)?; + } + + // 9. Let results be a new empty List. + let mut results = Vec::new(); + + // 10. Let done be false. + // 11. Repeat, while done is false, + loop { + // a. Let result be ? RegExpExec(rx, S). + let result = Self::abstract_exec(rx, arg_str.clone(), context)?; + + // b. If result is null, set done to true. + // c. Else, + if let Some(result) = result { + // i. Append result to the end of results. + results.push(result.clone()); + + // ii. If global is false, set done to true. + + if !global { + break; + } + // iii. Else, + // 1. Let matchStr be ? ToString(? Get(result, "0")). + let match_str = result.get("0", context)?.to_string(context)?; + + // 2. If matchStr is the empty String, then + if match_str.is_empty() { + // a. Let thisIndex be ℝ(? ToLength(? Get(rx, "lastIndex"))). + let this_index = rx.get("lastIndex", context)?.to_length(context)?; + + // b. Let nextIndex be AdvanceStringIndex(S, thisIndex, fullUnicode). + let next_index = advance_string_index(&arg_str, this_index, unicode); + + // c. Perform ? Set(rx, "lastIndex", 𝔽(nextIndex), true). + rx.set("lastIndex", JsValue::new(next_index), true, context)?; + } + } else { + break; + } + } + + // 12. Let accumulatedResult be the empty String. + let mut accumulated_result = JsString::new(""); + + // 13. Let nextSourcePosition be 0. + let mut next_source_position = 0; + + // 14. For each element result of results, do + for result in results { + // a. Let resultLength be ? LengthOfArrayLike(result). + let result_length = result.length_of_array_like(context)? as isize; + + // b. Let nCaptures be max(resultLength - 1, 0). + let n_captures = std::cmp::max(result_length - 1, 0); + + // c. Let matched be ? ToString(? Get(result, "0")). + let matched = result.get("0", context)?.to_string(context)?; + + // d. Let matchLength be the number of code units in matched. + let match_length = matched.encode_utf16().count(); + + // e. Let position be ? ToIntegerOrInfinity(? Get(result, "index")). + let position = result + .get("index", context)? + .to_integer_or_infinity(context)?; + + // f. Set position to the result of clamping position between 0 and lengthS. + //position = position. + let position = match position { + IntegerOrInfinity::Integer(i) => { + if i < 0 { + 0 + } else if i as usize > length_arg_str { + length_arg_str + } else { + i as usize + } + } + IntegerOrInfinity::PositiveInfinity => length_arg_str, + IntegerOrInfinity::NegativeInfinity => 0, + }; + + // h. Let captures be a new empty List. + let mut captures = Vec::new(); + + // g. Let n be 1. + // i. Repeat, while n ≤ nCaptures, + for n in 1..=n_captures { + // i. Let capN be ? Get(result, ! ToString(𝔽(n))). + let mut cap_n = result.get(n.to_string(), context)?; + + // ii. If capN is not undefined, then + if !cap_n.is_undefined() { + // 1. Set capN to ? ToString(capN). + cap_n = cap_n.to_string(context)?.into(); + } + + // iii. Append capN as the last element of captures. + captures.push(cap_n); + + // iv. Set n to n + 1. + } + + // j. Let namedCaptures be ? Get(result, "groups"). + let mut named_captures = result.get("groups", context)?; + + // k. If functionalReplace is true, then + // l. Else, + let replacement: JsString; + if functional_replace { + // i. Let replacerArgs be « matched ». + let mut replacer_args = vec![JsValue::new(matched)]; + + // ii. Append in List order the elements of captures to the end of the List replacerArgs. + replacer_args.extend(captures); + + // iii. Append 𝔽(position) and S to replacerArgs. + replacer_args.push(position.into()); + replacer_args.push(arg_str.clone().into()); + + // iv. If namedCaptures is not undefined, then + if !named_captures.is_undefined() { + // 1. Append namedCaptures as the last element of replacerArgs. + replacer_args.push(named_captures); + } + + // v. Let replValue be ? Call(replaceValue, undefined, replacerArgs). + let repl_value = + context.call(&replace_value, &JsValue::undefined(), &replacer_args)?; + + // vi. Let replacement be ? ToString(replValue). + replacement = repl_value.to_string(context)?; + } else { + // i. If namedCaptures is not undefined, then + if !named_captures.is_undefined() { + // 1. Set namedCaptures to ? ToObject(namedCaptures). + named_captures = named_captures.to_object(context)?.into(); + } + + // ii. Let replacement be ? GetSubstitution(matched, S, position, captures, namedCaptures, replaceValue). + replacement = string::get_substitution( + matched.as_str(), + arg_str.as_str(), + position, + &captures, + &named_captures, + &replace_value.to_string(context)?, + context, + )?; + } + + // m. If position ≥ nextSourcePosition, then + if position >= next_source_position { + // i. NOTE: position should not normally move backwards. + // If it does, it is an indication of an ill-behaving RegExp subclass + // or use of an access triggered side-effect to change the global flag or other characteristics of rx. + // In such cases, the corresponding substitution is ignored. + // ii. Set accumulatedResult to the string-concatenation of accumulatedResult, + // the substring of S from nextSourcePosition to position, and replacement. + accumulated_result = format!( + "{accumulated_result}{}{replacement}", + arg_str + .get(next_source_position..position) + .expect("index of a regexp match cannot be greater than the input string"), + ) + .into(); + + // iii. Set nextSourcePosition to position + matchLength. + next_source_position = position + match_length; + } + } + + // 15. If nextSourcePosition ≥ lengthS, return accumulatedResult. + if next_source_position >= length_arg_str { + return Ok(accumulated_result.into()); + } + + // 16. Return the string-concatenation of accumulatedResult and the substring of S from nextSourcePosition. + Ok(format!( + "{}{}", + accumulated_result, + arg_str + .get(next_source_position..) + .expect("next_source_position cannot be greater than the input string") + ) + .into()) + } + + /// `RegExp.prototype[ @@search ]( string )` + /// + /// This method executes a search for a match between a this regular expression and a string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@search + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@search + pub(crate) fn search( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + let rx = if let Some(rx) = this.as_object() { + rx + } else { + return context.throw_type_error( + "RegExp.prototype[Symbol.search] method called on incompatible value", + ); + }; + + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let previousLastIndex be ? Get(rx, "lastIndex"). + let previous_last_index = rx.get("lastIndex", context)?; + + // 5. If SameValue(previousLastIndex, +0𝔽) is false, then + if !JsValue::same_value(&previous_last_index, &JsValue::new(0)) { + // a. Perform ? Set(rx, "lastIndex", +0𝔽, true). + rx.set("lastIndex", 0, true, context)?; + } + + // 6. Let result be ? RegExpExec(rx, S). + let result = Self::abstract_exec(rx, arg_str, context)?; + + // 7. Let currentLastIndex be ? Get(rx, "lastIndex"). + let current_last_index = rx.get("lastIndex", context)?; + + // 8. If SameValue(currentLastIndex, previousLastIndex) is false, then + if !JsValue::same_value(¤t_last_index, &previous_last_index) { + // a. Perform ? Set(rx, "lastIndex", previousLastIndex, true). + rx.set("lastIndex", previous_last_index, true, context)?; + } + + // 9. If result is null, return -1𝔽. + // 10. Return ? Get(result, "index"). + if let Some(result) = result { + result.get("index", context) + } else { + Ok(JsValue::new(-1)) + } + } + + /// `RegExp.prototype [ @@split ] ( string, limit )` + /// + /// The [@@split]() method splits a String object into an array of strings by separating the string into substrings. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-regexp.prototype-@@split + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/@@split + pub(crate) fn split( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let rx be the this value. + // 2. If Type(rx) is not Object, throw a TypeError exception. + let rx = if let Some(rx) = this.as_object() { + rx + } else { + return context + .throw_type_error("RegExp.prototype.split method called on incompatible value"); + }; + + // 3. Let S be ? ToString(string). + let arg_str = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + + // 4. Let C be ? SpeciesConstructor(rx, %RegExp%). + let constructor = rx.species_constructor(StandardConstructors::regexp, context)?; + + // 5. Let flags be ? ToString(? Get(rx, "flags")). + let flags = rx.get("flags", context)?.to_string(context)?; + + // 6. If flags contains "u", let unicodeMatching be true. + // 7. Else, let unicodeMatching be false. + let unicode = flags.contains('u'); + + // 8. If flags contains "y", let newFlags be flags. + // 9. Else, let newFlags be the string-concatenation of flags and "y". + let new_flags = if flags.contains('y') { + flags.to_string() + } else { + format!("{flags}y") + }; + + // 10. Let splitter be ? Construct(C, « rx, newFlags »). + let splitter = constructor.construct( + &[this.clone(), new_flags.into()], + Some(&constructor), + context, + )?; + + // 11. Let A be ! ArrayCreate(0). + let a = Array::array_create(0, None, context).expect("this ArrayCreate call must not fail"); + + // 12. Let lengthA be 0. + let mut length_a = 0; + + // 13. If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)). + let limit = args.get_or_undefined(1); + let lim = if limit.is_undefined() { + u32::MAX + } else { + limit.to_u32(context)? + }; + + // 14. If lim is 0, return A. + if lim == 0 { + return Ok(a.into()); + } + + // 15. Let size be the length of S. + let size = arg_str.encode_utf16().count(); + + // 16. If size is 0, then + if size == 0 { + // a. Let z be ? RegExpExec(splitter, S). + let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?; + + // b. If z is not null, return A. + if result.is_some() { + return Ok(a.into()); + } + + // c. Perform ! CreateDataPropertyOrThrow(A, "0", S). + a.create_data_property_or_throw(0, arg_str, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // d. Return A. + return Ok(a.into()); + } + + // 17. Let p be 0. + // 18. Let q be p. + let mut p = 0; + let mut q = p; + + // 19. Repeat, while q < size, + while q < size { + // a. Perform ? Set(splitter, "lastIndex", 𝔽(q), true). + splitter.set("lastIndex", JsValue::new(q), true, context)?; + + // b. Let z be ? RegExpExec(splitter, S). + let result = Self::abstract_exec(&splitter, arg_str.clone(), context)?; + + // c. If z is null, set q to AdvanceStringIndex(S, q, unicodeMatching). + // d. Else, + if let Some(result) = result { + // i. Let e be ℝ(? ToLength(? Get(splitter, "lastIndex"))). + let mut e = splitter.get("lastIndex", context)?.to_length(context)?; + + // ii. Set e to min(e, size). + e = std::cmp::min(e, size); + + // iii. If e = p, set q to AdvanceStringIndex(S, q, unicodeMatching). + // iv. Else, + if e == p { + q = advance_string_index(&arg_str, q, unicode); + } else { + // 1. Let T be the substring of S from p to q. + let arg_str_substring = String::from_utf16_lossy( + &arg_str + .encode_utf16() + .skip(p) + .take(q - p) + .collect::>(), + ); + + // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T). + a.create_data_property_or_throw(length_a, arg_str_substring, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 3. Set lengthA to lengthA + 1. + length_a += 1; + + // 4. If lengthA = lim, return A. + if length_a == lim { + return Ok(a.into()); + } + + // 5. Set p to e. + p = e; + + // 6. Let numberOfCaptures be ? LengthOfArrayLike(z). + let mut number_of_captures = result.length_of_array_like(context)? as isize; + + // 7. Set numberOfCaptures to max(numberOfCaptures - 1, 0). + number_of_captures = if number_of_captures == 0 { + 0 + } else { + std::cmp::max(number_of_captures - 1, 0) + }; + + // 8. Let i be 1. + // 9. Repeat, while i ≤ numberOfCaptures, + for i in 1..=number_of_captures { + // a. Let nextCapture be ? Get(z, ! ToString(𝔽(i))). + let next_capture = result.get(i.to_string(), context)?; + + // b. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), nextCapture). + a.create_data_property_or_throw(length_a, next_capture, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // d. Set lengthA to lengthA + 1. + length_a += 1; + + // e. If lengthA = lim, return A. + if length_a == lim { + return Ok(a.into()); + } + } + + // 10. Set q to p. + q = p; + } + } else { + q = advance_string_index(&arg_str, q, unicode); + } + } + + // 20. Let T be the substring of S from p to size. + let arg_str_substring = String::from_utf16_lossy( + &arg_str + .encode_utf16() + .skip(p) + .take(size - p) + .collect::>(), + ); + + // 21. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T). + a.create_data_property_or_throw(length_a, arg_str_substring, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 22. Return A. + Ok(a.into()) + } +} + +/// `22.2.5.2.3 AdvanceStringIndex ( S, index, unicode )` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-advancestringindex +fn advance_string_index(s: &JsString, index: usize, unicode: bool) -> usize { + // Regress only works with utf8, so this function differs from the spec. + + // 1. Assert: index ≤ 2^53 - 1. + + // 2. If unicode is false, return index + 1. + if !unicode { + return index + 1; + } + + // 3. Let length be the number of code units in S. + let length = s.encode_utf16().count(); + + // 4. If index + 1 ≥ length, return index + 1. + if index + 1 > length { + return index + 1; + } + + // 5. Let cp be ! CodePointAt(S, index). + let (_, offset, _) = crate::builtins::string::code_point_at(s, index); + + index + offset as usize +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/regexp_string_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/regexp_string_iterator.rs new file mode 100644 index 0000000..620d266 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/regexp_string_iterator.rs @@ -0,0 +1,170 @@ +//! This module implements the global `RegExp String Iterator` object. +//! +//! A `RegExp` String Iterator is an object, that represents a specific iteration over some +//! specific String instance object, matching against some specific `RegExp` instance object. +//! There is not a named constructor for `RegExp` String Iterator objects. Instead, `RegExp` +//! String Iterator objects are created by calling certain methods of `RegExp` instance objects. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-regexp-string-iterator-objects + +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, regexp}, + object::{JsObject, ObjectData}, + property::PropertyDescriptor, + symbol::WellKnownSymbols, + Context, JsResult, JsString, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; +use regexp::{advance_string_index, RegExp}; + +// TODO: See todos in create_regexp_string_iterator and next. +#[derive(Debug, Clone, Finalize, Trace)] +pub struct RegExpStringIterator { + matcher: JsObject, + string: JsString, + global: bool, + unicode: bool, + completed: bool, +} + +// TODO: See todos in create_regexp_string_iterator and next. +impl RegExpStringIterator { + fn new(matcher: JsObject, string: JsString, global: bool, unicode: bool) -> Self { + Self { + matcher, + string, + global, + unicode, + completed: false, + } + } + + /// `22.2.7.1 CreateRegExpStringIterator ( R, S, global, fullUnicode )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createregexpstringiterator + pub(crate) fn create_regexp_string_iterator( + matcher: JsObject, + string: JsString, + global: bool, + unicode: bool, + context: &mut Context, + ) -> JsValue { + // TODO: Implement this with closures and generators. + // For now all values of the closure are stored in RegExpStringIterator and the actual closure execution is in `.next()`. + + // 1. Assert: Type(S) is String. + // 2. Assert: Type(global) is Boolean. + // 3. Assert: Type(fullUnicode) is Boolean. + + // 4. Let closure be a new Abstract Closure with no parameters that captures R, S, global, + // and fullUnicode and performs the following steps when called: + + // 5. Return ! CreateIteratorFromClosure(closure, "%RegExpStringIteratorPrototype%", %RegExpStringIteratorPrototype%). + + let regexp_string_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .regexp_string_iterator(), + ObjectData::reg_exp_string_iterator(Self::new(matcher, string, global, unicode)), + ); + + regexp_string_iterator.into() + } + + pub fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut iterator = this.as_object().map(JsObject::borrow_mut); + let iterator = iterator + .as_mut() + .and_then(|obj| obj.as_regexp_string_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not a RegExpStringIterator"))?; + if iterator.completed { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + // TODO: This is the code that should be created as a closure in create_regexp_string_iterator. + + // i. Let match be ? RegExpExec(R, S). + let m = RegExp::abstract_exec(&iterator.matcher, iterator.string.clone(), context)?; + + if let Some(m) = m { + // iii. If global is false, then + if !iterator.global { + // 1. Perform ? Yield(match). + // 2. Return undefined. + iterator.completed = true; + return Ok(create_iter_result_object(m.into(), false, context)); + } + + // iv. Let matchStr be ? ToString(? Get(match, "0")). + let m_str = m.get("0", context)?.to_string(context)?; + + // v. If matchStr is the empty String, then + if m_str.is_empty() { + // 1. Let thisIndex be ℝ(? ToLength(? Get(R, "lastIndex"))). + let this_index = iterator + .matcher + .get("lastIndex", context)? + .to_length(context)?; + + // 2. Let nextIndex be ! AdvanceStringIndex(S, thisIndex, fullUnicode). + let next_index = + advance_string_index(&iterator.string, this_index, iterator.unicode); + + // 3. Perform ? Set(R, "lastIndex", 𝔽(nextIndex), true). + iterator + .matcher + .set("lastIndex", next_index, true, context)?; + } + + // vi. Perform ? Yield(match). + Ok(create_iter_result_object(m.into(), false, context)) + } else { + // ii. If match is null, return undefined. + iterator.completed = true; + Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + } + } + + /// Create the `%ArrayIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event("RegExp String Iterator", "init"); + + // Create prototype + let result = JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &result, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("RegExp String Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + result.insert(to_string_tag, to_string_tag_property); + result + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/tests.rs new file mode 100644 index 0000000..66c156b --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/regexp/tests.rs @@ -0,0 +1,239 @@ +use crate::{forward, Context}; + +#[test] +fn constructors() { + let mut context = Context::default(); + let init = r#" + var constructed = new RegExp("[0-9]+(\\.[0-9]+)?"); + var literal = /[0-9]+(\.[0-9]+)?/; + var ctor_literal = new RegExp(/[0-9]+(\.[0-9]+)?/); + "#; + + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "constructed.test('1.0')"), "true"); + assert_eq!(forward(&mut context, "literal.test('1.0')"), "true"); + assert_eq!(forward(&mut context, "ctor_literal.test('1.0')"), "true"); +} + +#[test] +fn species() { + let mut context = Context::default(); + + let init = r#" + var descriptor = Object.getOwnPropertyDescriptor(RegExp, Symbol.species); + var accessor = Object.getOwnPropertyDescriptor(RegExp, Symbol.species).get; + var name = Object.getOwnPropertyDescriptor(descriptor.get, "name"); + var length = Object.getOwnPropertyDescriptor(descriptor.get, "length"); + var thisVal = {}; + "#; + eprintln!("{}", forward(&mut context, init)); + + // length + assert_eq!(forward(&mut context, "descriptor.get.length"), "0"); + assert_eq!(forward(&mut context, "length.enumerable"), "false"); + assert_eq!(forward(&mut context, "length.writable"), "false"); + assert_eq!(forward(&mut context, "length.configurable"), "true"); + + // return-value + assert_eq!( + forward(&mut context, "Object.is(accessor.call(thisVal), thisVal)"), + "true" + ); + + // symbol-species-name + assert_eq!( + forward(&mut context, "descriptor.get.name"), + "\"get [Symbol.species]\"" + ); + assert_eq!(forward(&mut context, "name.enumerable"), "false"); + assert_eq!(forward(&mut context, "name.writable"), "false"); + assert_eq!(forward(&mut context, "name.configurable"), "true"); + + // symbol-species + assert_eq!(forward(&mut context, "descriptor.set"), "undefined"); + assert_eq!( + forward(&mut context, "typeof descriptor.get"), + "\"function\"" + ); + assert_eq!(forward(&mut context, "descriptor.enumerable"), "false"); + assert_eq!(forward(&mut context, "descriptor.configurable"), "true"); +} + +// TODO: uncomment this test when property getters are supported + +// #[test] +// fn flags() { +// let mut context = Context::default(); +// let init = r#" +// var re_gi = /test/gi; +// var re_sm = /test/sm; +// "#; +// +// eprintln!("{}", forward(&mut context, init)); +// assert_eq!(forward(&mut context, "re_gi.global"), "true"); +// assert_eq!(forward(&mut context, "re_gi.ignoreCase"), "true"); +// assert_eq!(forward(&mut context, "re_gi.multiline"), "false"); +// assert_eq!(forward(&mut context, "re_gi.dotAll"), "false"); +// assert_eq!(forward(&mut context, "re_gi.unicode"), "false"); +// assert_eq!(forward(&mut context, "re_gi.sticky"), "false"); +// assert_eq!(forward(&mut context, "re_gi.flags"), "gi"); +// +// assert_eq!(forward(&mut context, "re_sm.global"), "false"); +// assert_eq!(forward(&mut context, "re_sm.ignoreCase"), "false"); +// assert_eq!(forward(&mut context, "re_sm.multiline"), "true"); +// assert_eq!(forward(&mut context, "re_sm.dotAll"), "true"); +// assert_eq!(forward(&mut context, "re_sm.unicode"), "false"); +// assert_eq!(forward(&mut context, "re_sm.sticky"), "false"); +// assert_eq!(forward(&mut context, "re_sm.flags"), "ms"); +// } + +#[test] +fn last_index() { + let mut context = Context::default(); + let init = r#" + var regex = /[0-9]+(\.[0-9]+)?/g; + "#; + + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "regex.lastIndex"), "0"); + assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "true"); + assert_eq!(forward(&mut context, "regex.lastIndex"), "3"); + assert_eq!(forward(&mut context, "regex.test('1.0foo')"), "false"); + assert_eq!(forward(&mut context, "regex.lastIndex"), "0"); +} + +#[test] +fn exec() { + let mut context = Context::default(); + let init = r#" + var re = /quick\s(brown).+?(jumps)/ig; + var result = re.exec('The Quick Brown Fox Jumps Over The Lazy Dog'); + "#; + + eprintln!("{}", forward(&mut context, init)); + assert_eq!( + forward(&mut context, "result[0]"), + "\"Quick Brown Fox Jumps\"" + ); + assert_eq!(forward(&mut context, "result[1]"), "\"Brown\""); + assert_eq!(forward(&mut context, "result[2]"), "\"Jumps\""); + assert_eq!(forward(&mut context, "result.index"), "4"); + assert_eq!( + forward(&mut context, "result.input"), + "\"The Quick Brown Fox Jumps Over The Lazy Dog\"" + ); +} + +#[test] +fn to_string() { + let mut context = Context::default(); + + assert_eq!( + forward(&mut context, "(new RegExp('a+b+c')).toString()"), + "\"/a+b+c/\"" + ); + assert_eq!( + forward(&mut context, "(new RegExp('bar', 'g')).toString()"), + "\"/bar/g\"" + ); + assert_eq!( + forward(&mut context, "(new RegExp('\\\\n', 'g')).toString()"), + "\"/\\n/g\"" + ); + assert_eq!(forward(&mut context, "/\\n/g.toString()"), "\"/\\n/g\""); +} + +#[test] +fn no_panic_on_invalid_character_escape() { + let mut context = Context::default(); + + // This used to panic, we now return an error + // The line below should not cause Boa to panic + forward(&mut context, r"const a = /,\;/"); +} + +#[test] +fn search() { + let mut context = Context::default(); + + // coerce-string + assert_eq!( + forward( + &mut context, + r#" + var obj = { + toString: function() { + return 'toString value'; + } + }; + /ring/[Symbol.search](obj) + "# + ), + "4" + ); + + // failure-return-val + assert_eq!(forward(&mut context, "/z/[Symbol.search]('a')"), "-1"); + + // length + assert_eq!( + forward(&mut context, "RegExp.prototype[Symbol.search].length"), + "1" + ); + + let init = + "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"length\")"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "false"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // name + assert_eq!( + forward(&mut context, "RegExp.prototype[Symbol.search].name"), + "\"[Symbol.search]\"" + ); + + let init = + "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype[Symbol.search], \"name\")"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "false"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // prop-desc + let init = "var obj = Object.getOwnPropertyDescriptor(RegExp.prototype, Symbol.search)"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "obj.enumerable"), "false"); + assert_eq!(forward(&mut context, "obj.writable"), "true"); + assert_eq!(forward(&mut context, "obj.configurable"), "true"); + + // success-return-val + assert_eq!(forward(&mut context, "/a/[Symbol.search]('abc')"), "0"); + assert_eq!(forward(&mut context, "/b/[Symbol.search]('abc')"), "1"); + assert_eq!(forward(&mut context, "/c/[Symbol.search]('abc')"), "2"); + + // this-val-non-obj + let error = "Uncaught \"TypeError\": \"RegExp.prototype[Symbol.search] method called on incompatible value\""; + let init = "var search = RegExp.prototype[Symbol.search]"; + eprintln!("{}", forward(&mut context, init)); + assert_eq!(forward(&mut context, "search.call()"), error); + assert_eq!(forward(&mut context, "search.call(undefined)"), error); + assert_eq!(forward(&mut context, "search.call(null)"), error); + assert_eq!(forward(&mut context, "search.call(true)"), error); + assert_eq!(forward(&mut context, "search.call('string')"), error); + assert_eq!(forward(&mut context, "search.call(Symbol.search)"), error); + assert_eq!(forward(&mut context, "search.call(86)"), error); + + // u-lastindex-advance + assert_eq!( + forward(&mut context, "/\\udf06/u[Symbol.search]('\\ud834\\udf06')"), + "-1" + ); + + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"a\")"), "0"); + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"ba\")"), "1"); + assert_eq!(forward(&mut context, "/a/[Symbol.search](\"bb\")"), "-1"); + assert_eq!(forward(&mut context, "/u/[Symbol.search](null)"), "1"); + assert_eq!(forward(&mut context, "/d/[Symbol.search](undefined)"), "2"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/mod.rs new file mode 100644 index 0000000..282f2f7 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/mod.rs @@ -0,0 +1,451 @@ +//! This module implements the global `Set` object. +//! +//! The JavaScript `Set` class is a global object that is used in the construction of sets; which +//! are high-level, collections of values. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-set-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set + +use self::{ordered_set::OrderedSet, set_iterator::SetIterator}; +use super::JsArgs; +use crate::{ + builtins::BuiltIn, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::{Attribute, PropertyNameKind}, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_profiler::Profiler; +use tap::{Conv, Pipe}; + +pub mod ordered_set; +pub mod set_iterator; +#[cfg(test)] +mod tests; + +#[derive(Debug, Clone)] +pub(crate) struct Set(OrderedSet); + +impl BuiltIn for Set { + const NAME: &'static str = "Set"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let size_getter = FunctionBuilder::native(context, Self::size_getter) + .constructor(false) + .name("get size") + .build(); + + let iterator_symbol = WellKnownSymbols::iterator(); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + + let values_function = FunctionBuilder::native(context, Self::values) + .name("values") + .length(0) + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().set().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .method(Self::add, "add", 1) + .method(Self::clear, "clear", 0) + .method(Self::delete, "delete", 1) + .method(Self::entries, "entries", 0) + .method(Self::for_each, "forEach", 1) + .method(Self::has, "has", 1) + .property( + "keys", + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .accessor("size", Some(size_getter), None, Attribute::CONFIGURABLE) + .property( + "values", + values_function.clone(), + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + iterator_symbol, + values_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + to_string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl Set { + pub(crate) const LENGTH: usize = 0; + + /// Create a new set + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context + .throw_type_error("calling a builtin Set constructor without new is forbidden"); + } + + // 2. Let set be ? OrdinaryCreateFromConstructor(NewTarget, "%Set.prototype%", « [[SetData]] »). + // 3. Set set.[[SetData]] to a new empty List. + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::set, context)?; + let set = JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::default())); + + // 4. If iterable is either undefined or null, return set. + let iterable = args.get_or_undefined(0); + if iterable.is_null_or_undefined() { + return Ok(set.into()); + } + + // 5. Let adder be ? Get(set, "add"). + let adder = set.get("add", context)?; + + // 6. If IsCallable(adder) is false, throw a TypeError exception. + let adder = adder.as_callable().ok_or_else(|| { + context.construct_type_error("'add' of 'newTarget' is not a function") + })?; + + // 7. Let iteratorRecord be ? GetIterator(iterable). + let iterator_record = iterable.clone().get_iterator(context, None, None)?; + + // 8. Repeat, + // a. Let next be ? IteratorStep(iteratorRecord). + // b. If next is false, return set. + // c. Let nextValue be ? IteratorValue(next). + // d. Let status be Completion(Call(adder, set, « nextValue »)). + // e. IfAbruptCloseIterator(status, iteratorRecord). + while let Some(next) = iterator_record.step(context)? { + // c + let next_value = next.value(context)?; + + // d, e + if let Err(status) = adder.call(&set.clone().into(), &[next_value], context) { + return iterator_record.close(Err(status), context); + } + } + + // 8.b + Ok(set.into()) + } + + /// Utility for constructing `Set` objects. + pub(crate) fn set_create(prototype: Option, context: &mut Context) -> JsObject { + let prototype = + prototype.unwrap_or_else(|| context.intrinsics().constructors().set().prototype()); + + JsObject::from_proto_and_data(prototype, ObjectData::set(OrderedSet::new())) + } + + /// Utility for constructing `Set` objects from an iterator of `JsValue`'s. + pub(crate) fn create_set_from_list(elements: I, context: &mut Context) -> JsObject + where + I: IntoIterator, + { + // Create empty Set + let set = Self::set_create(None, context); + // For each element e of elements, do + for elem in elements { + Self::add(&set.clone().into(), &[elem], context) + .expect("adding new element shouldn't error out"); + } + + set + } + + /// `get Set [ @@species ]` + /// + /// The Set[Symbol.species] accessor property returns the Set constructor. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-set-@@species + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// `Set.prototype.add( value )` + /// + /// This method adds an entry with value into the set. Returns the set object + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.add + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/add + pub(crate) fn add( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let value = args.get_or_undefined(0); + + if let Some(object) = this.as_object() { + if let Some(set) = object.borrow_mut().as_set_mut() { + set.add(if value.as_number().map_or(false, |n| n == -0f64) { + JsValue::Integer(0) + } else { + value.clone() + }); + } else { + return context.throw_type_error("'this' is not a Set"); + } + } else { + return context.throw_type_error("'this' is not a Set"); + }; + + Ok(this.clone()) + } + + /// `Set.prototype.clear( )` + /// + /// This method removes all entries from the set. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.clear + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/clear + pub(crate) fn clear(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + if let Some(object) = this.as_object() { + if object.borrow().is_set() { + this.set_data(ObjectData::set(OrderedSet::new())); + Ok(JsValue::undefined()) + } else { + context.throw_type_error("'this' is not a Set") + } + } else { + context.throw_type_error("'this' is not a Set") + } + } + + /// `Set.prototype.delete( value )` + /// + /// This method removes the entry for the given value if it exists. + /// Returns true if there was an element, false otherwise. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.delete + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/delete + pub(crate) fn delete( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let value = args.get_or_undefined(0); + + let res = if let Some(object) = this.as_object() { + if let Some(set) = object.borrow_mut().as_set_mut() { + set.delete(value) + } else { + return context.throw_type_error("'this' is not a Set"); + } + } else { + return context.throw_type_error("'this' is not a Set"); + }; + + Ok(res.into()) + } + + /// `Set.prototype.entries( )` + /// + /// This method returns an iterator over the entries of the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.entries + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/entries + pub(crate) fn entries( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(object) = this.as_object() { + let object = object.borrow(); + if !object.is_set() { + return context.throw_type_error( + "Method Set.prototype.entries called on incompatible receiver", + ); + } + } else { + return context + .throw_type_error("Method Set.prototype.entries called on incompatible receiver"); + } + + Ok(SetIterator::create_set_iterator( + this.clone(), + PropertyNameKind::KeyAndValue, + context, + )) + } + + /// `Set.prototype.forEach( callbackFn [ , thisArg ] )` + /// + /// This method executes the provided callback function for each value in the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.foreach + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/foreach + pub(crate) fn for_each( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + if args.is_empty() { + return Err(JsValue::new("Missing argument for Set.prototype.forEach")); + } + + let callback_arg = &args[0]; + let this_arg = args.get_or_undefined(1); + + // TODO: if condition should also check that we are not in strict mode + let this_arg = if this_arg.is_undefined() { + context.global_object().clone().into() + } else { + this_arg.clone() + }; + + let mut index = 0; + + while index < Self::get_size(this, context)? { + let arguments = this + .as_object() + .and_then(|obj| { + obj.borrow().as_set_ref().map(|set| { + set.get_index(index) + .map(|value| [value.clone(), value.clone(), this.clone()]) + }) + }) + .ok_or_else(|| context.construct_type_error("'this' is not a Set"))?; + + if let Some(arguments) = arguments { + context.call(callback_arg, &this_arg, &arguments)?; + } + + index += 1; + } + + Ok(JsValue::Undefined) + } + + /// `Map.prototype.has( key )` + /// + /// This method checks if the map contains an entry with the given key. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-map.prototype.has + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/has + pub(crate) fn has( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let value = args.get_or_undefined(0); + + this.as_object() + .and_then(|obj| { + obj.borrow() + .as_set_ref() + .map(|set| set.contains(value).into()) + }) + .ok_or_else(|| context.construct_type_error("'this' is not a Set")) + } + + /// `Set.prototype.values( )` + /// + /// This method returns an iterator over the values of the set + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-set.prototype.values + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Set/values + pub(crate) fn values( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + if let Some(object) = this.as_object() { + let object = object.borrow(); + if !object.is_set() { + return context.throw_type_error( + "Method Set.prototype.values called on incompatible receiver", + ); + } + } else { + return context + .throw_type_error("Method Set.prototype.values called on incompatible receiver"); + } + + Ok(SetIterator::create_set_iterator( + this.clone(), + PropertyNameKind::Value, + context, + )) + } + + fn size_getter(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + Self::get_size(this, context).map(JsValue::from) + } + + /// Helper function to get the size of the `Set` object. + pub(crate) fn get_size(set: &JsValue, context: &mut Context) -> JsResult { + set.as_object() + .and_then(|obj| obj.borrow().as_set_ref().map(OrderedSet::size)) + .ok_or_else(|| context.construct_type_error("'this' is not a Set")) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/ordered_set.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/ordered_set.rs new file mode 100644 index 0000000..758bb9c --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/ordered_set.rs @@ -0,0 +1,137 @@ +use boa_gc::{custom_trace, Finalize, Trace}; +use indexmap::{ + set::{IntoIter, Iter}, + IndexSet, +}; +use std::{ + collections::hash_map::RandomState, + fmt::Debug, + hash::{BuildHasher, Hash}, +}; + +/// A type wrapping `indexmap::IndexSet` +#[derive(Clone)] +pub struct OrderedSet +where + V: Hash + Eq, +{ + inner: IndexSet, +} + +impl Finalize for OrderedSet {} +unsafe impl Trace for OrderedSet { + custom_trace!(this, { + for v in this.inner.iter() { + mark(v); + } + }); +} + +impl Debug for OrderedSet { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { + self.inner.fmt(formatter) + } +} + +impl Default for OrderedSet { + fn default() -> Self { + Self::new() + } +} + +impl OrderedSet +where + V: Hash + Eq, +{ + pub fn new() -> Self { + Self { + inner: IndexSet::new(), + } + } + + pub fn with_capacity(capacity: usize) -> Self { + Self { + inner: IndexSet::with_capacity(capacity), + } + } + + /// Return the number of key-value pairs in the map. + /// + /// Computes in **O(1)** time. + pub fn size(&self) -> usize { + self.inner.len() + } + + /// Returns true if the map contains no elements. + /// + /// Computes in **O(1)** time. + pub fn is_empty(&self) -> bool { + self.inner.len() == 0 + } + + /// Insert a value pair in the set. + /// + /// If an equivalent value already exists in the set: ??? + /// + /// If no equivalent value existed in the set: the new value is + /// inserted, last in order, and false + /// + /// Computes in **O(1)** time (amortized average). + pub fn add(&mut self, value: V) -> bool { + self.inner.insert(value) + } + + /// Delete the `value` from the set and return true if successful + /// + /// Return `false` if `value` is not in map. + /// + /// Computes in **O(n)** time (average). + pub fn delete(&mut self, value: &V) -> bool { + self.inner.shift_remove(value) + } + + /// Checks if a given value is present in the set + /// + /// Return `true` if `value` is present in set, false otherwise. + /// + /// Computes in **O(n)** time (average). + pub fn contains(&self, value: &V) -> bool { + self.inner.contains(value) + } + + /// Get a key-value pair by index + /// Valid indices are 0 <= index < self.len() + /// Computes in O(1) time. + pub fn get_index(&self, index: usize) -> Option<&V> { + self.inner.get_index(index) + } + + /// Return an iterator over the values of the set, in their order + pub fn iter(&self) -> Iter<'_, V> { + self.inner.iter() + } +} + +impl<'a, V, S> IntoIterator for &'a OrderedSet +where + V: Hash + Eq, + S: BuildHasher, +{ + type Item = &'a V; + type IntoIter = Iter<'a, V>; + fn into_iter(self) -> Self::IntoIter { + self.inner.iter() + } +} + +impl IntoIterator for OrderedSet +where + V: Hash + Eq, + S: BuildHasher, +{ + type Item = V; + type IntoIter = IntoIter; + fn into_iter(self) -> IntoIter { + self.inner.into_iter() + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/set_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/set_iterator.rs new file mode 100644 index 0000000..af27d13 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/set_iterator.rs @@ -0,0 +1,153 @@ +use crate::{ + builtins::{function::make_builtin_fn, iterable::create_iter_result_object, Array, JsValue}, + object::{JsObject, ObjectData}, + property::{PropertyDescriptor, PropertyNameKind}, + symbol::WellKnownSymbols, + Context, JsResult, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +/// The Set Iterator object represents an iteration over a set. It implements the iterator protocol. +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-set-iterator-objects +#[derive(Debug, Clone, Finalize, Trace)] +pub struct SetIterator { + iterated_set: JsValue, + next_index: usize, + iteration_kind: PropertyNameKind, +} + +impl SetIterator { + pub(crate) const NAME: &'static str = "SetIterator"; + + /// Constructs a new `SetIterator`, that will iterate over `set`, starting at index 0 + fn new(set: JsValue, kind: PropertyNameKind) -> Self { + Self { + iterated_set: set, + next_index: 0, + iteration_kind: kind, + } + } + + /// Abstract operation `CreateSetIterator( set, kind )` + /// + /// Creates a new iterator over the given set. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-createsetiterator + pub(crate) fn create_set_iterator( + set: JsValue, + kind: PropertyNameKind, + context: &Context, + ) -> JsValue { + let set_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .set_iterator(), + ObjectData::set_iterator(Self::new(set, kind)), + ); + set_iterator.into() + } + + /// %SetIteratorPrototype%.next( ) + /// + /// Advances the iterator and gets the next result in the set. + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%.next + pub(crate) fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut set_iterator = this.as_object().map(JsObject::borrow_mut); + + let set_iterator = set_iterator + .as_mut() + .and_then(|obj| obj.as_set_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not an SetIterator"))?; + { + let m = &set_iterator.iterated_set; + let mut index = set_iterator.next_index; + let item_kind = &set_iterator.iteration_kind; + + if set_iterator.iterated_set.is_undefined() { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + + let entries = m.as_object().map(JsObject::borrow); + let entries = entries + .as_ref() + .and_then(|obj| obj.as_set_ref()) + .ok_or_else(|| context.construct_type_error("'this' is not a Set"))?; + + let num_entries = entries.size(); + while index < num_entries { + let e = entries.get_index(index); + index += 1; + set_iterator.next_index = index; + if let Some(value) = e { + match item_kind { + PropertyNameKind::Value => { + return Ok(create_iter_result_object(value.clone(), false, context)); + } + PropertyNameKind::KeyAndValue => { + let result = Array::create_array_from_list( + [value.clone(), value.clone()], + context, + ); + return Ok(create_iter_result_object(result.into(), false, context)); + } + PropertyNameKind::Key => { + panic!("tried to collect only keys of Set") + } + } + } + } + } + + set_iterator.iterated_set = JsValue::undefined(); + Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )) + } + + /// Create the `%SetIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%setiteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + // Create prototype + let set_iterator = + JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &set_iterator, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("Set Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + set_iterator.insert(to_string_tag, to_string_tag_property); + set_iterator + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/tests.rs new file mode 100644 index 0000000..1a29c07 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/set/tests.rs @@ -0,0 +1,248 @@ +use crate::{forward, Context}; + +#[test] +fn construct_empty() { + let mut context = Context::default(); + let init = r#" + var empty = new Set(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "empty.size"); + assert_eq!(result, "0"); +} + +#[test] +fn construct_from_array() { + let mut context = Context::default(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "2"); +} + +#[test] +fn clone() { + let mut context = Context::default(); + let init = r#" + let original = new Set(["one", "two"]); + let clone = new Set(original); + "#; + forward(&mut context, init); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); + let result = forward( + &mut context, + r#" + original.add("three"); + original.size"#, + ); + assert_eq!(result, "3"); + let result = forward(&mut context, "clone.size"); + assert_eq!(result, "2"); +} + +#[test] +fn symbol_iterator() { + let mut context = Context::default(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const iterator = set1[Symbol.iterator](); + let item1 = iterator.next(); + let item2 = iterator.next(); + let item3 = iterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn entries() { + let mut context = Context::default(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const entriesIterator = set1.entries(); + let item1 = entriesIterator.next(); + let item2 = entriesIterator.next(); + let item3 = entriesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item1.value[0]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.value[1]"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value.length"); + assert_eq!(result, "2"); + let result = forward(&mut context, "item2.value[0]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.value[1]"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); +} + +#[test] +fn merge() { + let mut context = Context::default(); + let init = r#" + let first = new Set(["one", "two"]); + let second = new Set(["three", "four"]); + let third = new Set(["four", "five"]); + let merged1 = new Set([...first, ...second]); + let merged2 = new Set([...second, ...third]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "merged1.size"); + assert_eq!(result, "4"); + let result = forward(&mut context, "merged2.size"); + assert_eq!(result, "3"); +} + +#[test] +fn clear() { + let mut context = Context::default(); + let init = r#" + let set = new Set(["one", "two"]); + set.clear(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "0"); +} + +#[test] +fn delete() { + let mut context = Context::default(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.delete('one')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.size"); + assert_eq!(result, "1"); + let result = forward(&mut context, "set.delete('one')"); + assert_eq!(result, "false"); +} + +#[test] +fn has() { + let mut context = Context::default(); + let init = r#" + let set = new Set(["one", "two"]); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set.has('one')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.has('two')"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set.has('three')"); + assert_eq!(result, "false"); + let result = forward(&mut context, "set.has()"); + assert_eq!(result, "false"); +} + +#[test] +fn values_and_keys() { + let mut context = Context::default(); + let init = r#" + const set1 = new Set(); + set1.add('foo'); + set1.add('bar'); + const valuesIterator = set1.values(); + let item1 = valuesIterator.next(); + let item2 = valuesIterator.next(); + let item3 = valuesIterator.next(); + "#; + forward(&mut context, init); + let result = forward(&mut context, "item1.value"); + assert_eq!(result, "\"foo\""); + let result = forward(&mut context, "item1.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item2.value"); + assert_eq!(result, "\"bar\""); + let result = forward(&mut context, "item2.done"); + assert_eq!(result, "false"); + let result = forward(&mut context, "item3.value"); + assert_eq!(result, "undefined"); + let result = forward(&mut context, "item3.done"); + assert_eq!(result, "true"); + let result = forward(&mut context, "set1.values == set1.keys"); + assert_eq!(result, "true"); +} + +#[test] +fn for_each() { + let mut context = Context::default(); + let init = r#" + let set = new Set([5, 10, 15]); + let value1Sum = 0; + let value2Sum = 0; + let sizeSum = 0; + function callingCallback(value1, value2, set) { + value1Sum += value1; + value2Sum += value2; + sizeSum += set.size; + } + set.forEach(callingCallback); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "value1Sum"), "30"); + assert_eq!(forward(&mut context, "value2Sum"), "30"); + assert_eq!(forward(&mut context, "sizeSum"), "9"); +} + +#[test] +fn recursive_display() { + let mut context = Context::default(); + let init = r#" + let set = new Set(); + let array = new Array([set]); + set.add(set); + "#; + forward(&mut context, init); + let result = forward(&mut context, "set"); + assert_eq!(result, "Set { Set(1) }"); + let result = forward(&mut context, "set.add(array)"); + assert_eq!(result, "Set { Set(2), Array(1) }"); +} + +#[test] +fn not_a_function() { + let mut context = Context::default(); + let init = r" + try { + let set = Set() + } catch(e) { + e.toString() + } + "; + assert_eq!( + forward(&mut context, init), + "\"TypeError: calling a builtin Set constructor without new is forbidden\"" + ); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/mod.rs new file mode 100644 index 0000000..6033325 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/mod.rs @@ -0,0 +1,2346 @@ +//! This module implements the global `String` object. +//! +//! The `String` global object is a constructor for strings or a sequence of characters. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-string-object +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String + +pub mod string_iterator; +#[cfg(test)] +mod tests; + +use super::JsArgs; +use crate::{ + builtins::{string::string_iterator::StringIterator, Array, BuiltIn, Number, RegExp}, + context::intrinsics::StandardConstructors, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, JsObject, ObjectData, + }, + property::{Attribute, PropertyDescriptor}, + symbol::WellKnownSymbols, + value::IntegerOrInfinity, + Context, JsResult, JsString, JsValue, +}; +use boa_profiler::Profiler; +use std::{ + char::from_u32, + cmp::{max, min}, + string::String as StdString, +}; +use tap::{Conv, Pipe}; +use unicode_normalization::UnicodeNormalization; + +#[derive(Clone, Copy, Eq, PartialEq)] +pub(crate) enum Placement { + Start, + End, +} + +pub(crate) fn code_point_at(string: &JsString, position: usize) -> (u32, u8, bool) { + let mut encoded = string.encode_utf16(); + let size = encoded.clone().count(); + + let first = encoded + .nth(position) + .expect("The callers of this function must've already checked bounds."); + if !is_leading_surrogate(first) && !is_trailing_surrogate(first) { + return (u32::from(first), 1, false); + } + + if is_trailing_surrogate(first) || position + 1 == size { + return (u32::from(first), 1, true); + } + + let second = encoded + .next() + .expect("The callers of this function must've already checked bounds."); + if !is_trailing_surrogate(second) { + return (u32::from(first), 1, true); + } + let cp = (u32::from(first) - 0xD800) * 0x400 + (u32::from(second) - 0xDC00) + 0x10000; + (cp, 2, false) +} + +/// Helper function to check if a `char` is trimmable. +#[inline] +pub(crate) fn is_trimmable_whitespace(c: char) -> bool { + // The rust implementation of `trim` does not regard the same characters whitespace as ecma standard does + // + // Rust uses \p{White_Space} by default, which also includes: + // `\u{0085}' (next line) + // And does not include: + // '\u{FEFF}' (zero width non-breaking space) + // Explicit whitespace: https://tc39.es/ecma262/#sec-white-space + matches!( + c, + '\u{0009}' | '\u{000B}' | '\u{000C}' | '\u{0020}' | '\u{00A0}' | '\u{FEFF}' | + // Unicode Space_Separator category + '\u{1680}' | '\u{2000}' + ..='\u{200A}' | '\u{202F}' | '\u{205F}' | '\u{3000}' | + // Line terminators: https://tc39.es/ecma262/#sec-line-terminators + '\u{000A}' | '\u{000D}' | '\u{2028}' | '\u{2029}' + ) +} + +pub(crate) fn is_leading_surrogate(value: u16) -> bool { + (0xD800..=0xDBFF).contains(&value) +} + +pub(crate) fn is_trailing_surrogate(value: u16) -> bool { + (0xDC00..=0xDFFF).contains(&value) +} + +/// JavaScript `String` implementation. +#[derive(Debug, Clone, Copy)] +pub(crate) struct String; + +impl BuiltIn for String { + const NAME: &'static str = "String"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let symbol_iterator = WellKnownSymbols::iterator(); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().string().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .property("length", 0, attribute) + .static_method(Self::raw, "raw", 1) + .static_method(Self::from_char_code, "fromCharCode", 1) + .static_method(Self::from_code_point, "fromCodePoint", 1) + .method(Self::char_at, "charAt", 1) + .method(Self::char_code_at, "charCodeAt", 1) + .method(Self::code_point_at, "codePointAt", 1) + .method(Self::to_string, "toString", 0) + .method(Self::concat, "concat", 1) + .method(Self::repeat, "repeat", 1) + .method(Self::slice, "slice", 2) + .method(Self::starts_with, "startsWith", 1) + .method(Self::ends_with, "endsWith", 1) + .method(Self::includes, "includes", 1) + .method(Self::index_of, "indexOf", 1) + .method(Self::last_index_of, "lastIndexOf", 1) + .method(Self::r#match, "match", 1) + .method(Self::normalize, "normalize", 1) + .method(Self::pad_end, "padEnd", 1) + .method(Self::pad_start, "padStart", 1) + .method(Self::trim, "trim", 0) + .method(Self::trim_start, "trimStart", 0) + .method(Self::trim_end, "trimEnd", 0) + .method(Self::to_lowercase, "toLowerCase", 0) + .method(Self::to_uppercase, "toUpperCase", 0) + .method(Self::substring, "substring", 2) + .method(Self::substr, "substr", 2) + .method(Self::split, "split", 2) + .method(Self::value_of, "valueOf", 0) + .method(Self::match_all, "matchAll", 1) + .method(Self::replace, "replace", 2) + .method(Self::replace_all, "replaceAll", 2) + .method(Self::iterator, (symbol_iterator, "[Symbol.iterator]"), 0) + .method(Self::search, "search", 1) + .method(Self::at, "at", 1) + .build() + .conv::() + .pipe(Some) + } +} + +impl String { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 1; + + /// JavaScript strings must be between `0` and less than positive `Infinity` and cannot be a negative number. + /// The range of allowed values can be described like this: `[0, +∞)`. + /// + /// The resulting string can also not be larger than the maximum string size, + /// which can differ in JavaScript engines. In Boa it is `2^32 - 1` + pub(crate) const MAX_STRING_LENGTH: usize = u32::MAX as usize; + + /// `String( value )` + /// + /// + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // This value is used by console.log and other routines to match Object type + // to its Javascript Identifier (global constructor method name) + let string = match args.get(0) { + // 2. Else, + // a. If NewTarget is undefined and Type(value) is Symbol, return SymbolDescriptiveString(value). + Some(JsValue::Symbol(ref sym)) if new_target.is_undefined() => { + return Ok(sym.descriptive_string().into()) + } + // b. Let s be ? ToString(value). + Some(value) => value.to_string(context)?, + // 1. If value is not present, let s be the empty String. + None => JsString::default(), + }; + + // 3. If NewTarget is undefined, return s. + if new_target.is_undefined() { + return Ok(string.into()); + } + + let prototype = + get_prototype_from_constructor(new_target, StandardConstructors::string, context)?; + Ok(Self::string_create(string, prototype, context).into()) + } + + /// Abstract function `StringCreate( value, prototype )`. + /// + /// Call this function if you want to create a `String` exotic object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-stringcreate + fn string_create(value: JsString, prototype: JsObject, context: &mut Context) -> JsObject { + // 7. Let length be the number of code unit elements in value. + let len = value.encode_utf16().count(); + + // 1. Let S be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[StringData]] »). + // 2. Set S.[[Prototype]] to prototype. + // 3. Set S.[[StringData]] to value. + // 4. Set S.[[GetOwnProperty]] as specified in 10.4.3.1. + // 5. Set S.[[DefineOwnProperty]] as specified in 10.4.3.2. + // 6. Set S.[[OwnPropertyKeys]] as specified in 10.4.3.3. + let s = JsObject::from_proto_and_data(prototype, ObjectData::string(value)); + + // 8. Perform ! DefinePropertyOrThrow(S, "length", PropertyDescriptor { [[Value]]: 𝔽(length), + // [[Writable]]: false, [[Enumerable]]: false, [[Configurable]]: false }). + s.define_property_or_throw( + "length", + PropertyDescriptor::builder() + .value(len) + .writable(false) + .enumerable(false) + .configurable(false), + context, + ) + .expect("length definition for a new string must not fail"); + + // 9. Return S. + s + } + + /// Abstract operation `thisStringValue( value )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#thisstringvalue + fn this_string_value(this: &JsValue, context: &mut Context) -> JsResult { + // 1. If Type(value) is String, return value. + this.as_string() + .cloned() + // 2. If Type(value) is Object and value has a [[StringData]] internal slot, then + // a. Let s be value.[[StringData]]. + // b. Assert: Type(s) is String. + // c. Return s. + .or_else(|| this.as_object().and_then(|obj| obj.borrow().as_string())) + // 3. Throw a TypeError exception. + .ok_or_else(|| context.construct_type_error("'this' is not a string")) + } + + /// `String.fromCodePoint(num1[, ...[, numN]])` + /// + /// The static `String.fromCodePoint()` method returns a string created by using the specified sequence of code points. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.fromcodepoint + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/fromCodePoint + pub(crate) fn from_code_point( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let result be the empty String. + let mut result = StdString::new(); + + // 2. For each element next of codePoints, do + for arg in args.iter() { + // a. Let nextCP be ? ToNumber(next). + let nextcp = arg.to_number(context)?; + + // b. If ! IsIntegralNumber(nextCP) is false, throw a RangeError exception. + if !Number::is_float_integer(nextcp) { + return Err(context.construct_range_error(format!("invalid code point: {nextcp}"))); + } + + // c. If ℝ(nextCP) < 0 or ℝ(nextCP) > 0x10FFFF, throw a RangeError exception. + if nextcp < 0.0 || nextcp > f64::from(0x10FFFF) { + return Err(context.construct_range_error(format!("invalid code point: {nextcp}"))); + } + + // TODO: Full UTF-16 support + // d. Set result to the string-concatenation of result and ! UTF16EncodeCodePoint(ℝ(nextCP)). + result.push(char::try_from(nextcp as u32).unwrap_or('\u{FFFD}' /* replacement char */)); + } + + // 3. Assert: If codePoints is empty, then result is the empty String. + // 4. Return result. + Ok(result.into()) + } + + /// `String.prototype.raw( template, ...substitutions )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.raw + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/raw + pub(crate) fn raw(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let substitutions = args.get(1..).unwrap_or_default(); + + // 1. Let numberOfSubstitutions be the number of elements in substitutions. + let number_of_substitutions = substitutions.len(); + + // 2. Let cooked be ? ToObject(template). + let cooked = args.get_or_undefined(0).to_object(context)?; + + // 3. Let raw be ? ToObject(? Get(cooked, "raw")). + let raw = cooked.get("raw", context)?.to_object(context)?; + + // 4. Let literalSegments be ? LengthOfArrayLike(raw). + let literal_segments = raw.length_of_array_like(context)?; + + // 5. If literalSegments ≤ 0, return the empty String. + // This is not <= because a `usize` is always positive. + if literal_segments == 0 { + return Ok(JsString::empty().into()); + } + + // 6. Let stringElements be a new empty List. + let mut string_elements = vec![]; + + // 7. Let nextIndex be 0. + let mut next_index = 0; + // 8. Repeat, + loop { + // a. Let nextKey be ! ToString(𝔽(nextIndex)). + let next_key = next_index; + + // b. Let nextSeg be ? ToString(? Get(raw, nextKey)). + let next_seg = raw.get(next_key, context)?.to_string(context)?; + + // c. Append the code unit elements of nextSeg to the end of stringElements. + string_elements.extend(next_seg.encode_utf16()); + + // d. If nextIndex + 1 = literalSegments, then + if next_index + 1 == literal_segments { + // i. Return the String value whose code units are the elements in the List stringElements. + // If stringElements has no elements, the empty String is returned. + return Ok(StdString::from_utf16_lossy(&string_elements).into()); + } + + // e. If nextIndex < numberOfSubstitutions, let next be substitutions[nextIndex]. + let next = if next_index < number_of_substitutions { + substitutions.get_or_undefined(next_index).clone() + + // f. Else, let next be the empty String. + } else { + JsString::empty().into() + }; + + // g. Let nextSub be ? ToString(next). + let next_sub = next.to_string(context)?; + + // h. Append the code unit elements of nextSub to the end of stringElements. + string_elements.extend(next_sub.encode_utf16()); + + // i. Set nextIndex to nextIndex + 1. + next_index += 1; + } + } + + /// `String.fromCharCode(...codePoints)` + /// + /// Construct a `String` from one or more code points (as numbers). + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/multipage/text-processing.html#sec-string.fromcharcode + pub(crate) fn from_char_code( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let length be the number of elements in codeUnits. + // 2. Let elements be a new empty List. + let mut elements = Vec::new(); + // 3. For each element next of codeUnits, do + for next in args { + // 3a. Let nextCU be ℝ(? ToUint16(next)). + // 3b. Append nextCU to the end of elements. + elements.push(next.to_uint16(context)?); + } + + // 4. Return the String value whose code units are the elements in the List elements. + // If codeUnits is empty, the empty String is returned. + + let s = std::string::String::from_utf16_lossy(elements.as_slice()); + Ok(JsValue::String(JsString::new(s))) + } + + /// `String.prototype.toString ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tostring + #[allow(clippy::wrong_self_convention)] + #[inline] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? thisStringValue(this value). + Ok(Self::this_string_value(this, context)?.into()) + } + + /// `String.prototype.charAt( index )` + /// + /// The `String` object's `charAt()` method returns a new string consisting of the single UTF-16 code unit located at the specified offset into the string. + /// + /// Characters in a string are indexed from left to right. The index of the first character is `0`, + /// and the index of the last character—in a string called `stringName`—is `stringName.length - 1`. + /// If the `index` you supply is out of this range, JavaScript returns an empty string. + /// + /// If no index is provided to `charAt()`, the default is `0`. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charAt + pub(crate) fn char_at( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 4. Let size be the length of S. + let size = string.encode_utf16().count() as i64; + + // 3. Let position be ? ToIntegerOrInfinity(pos). + match args.get_or_undefined(0).to_integer_or_infinity(context)? { + IntegerOrInfinity::Integer(position) if (0..size).contains(&position) => { + // 6. Return the substring of S from position to position + 1. + let char = string + .encode_utf16() + .nth(position as usize) + .expect("Already checked bounds above"); + + Ok(char::try_from(u32::from(char)) + .unwrap_or('\u{FFFD}' /* replacement char */) + .into()) + } + _ => { + // 5. If position < 0 or position ≥ size, return the empty String. + Ok("".into()) + } + } + } + + /// `String.prototype.at ( index )` + /// + /// This String object's at() method returns a String consisting of the single UTF-16 code unit located at the specified position. + /// Returns undefined if the given index cannot be found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/proposal-relative-indexing-method/#sec-string.prototype.at + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/at + pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let s = this.to_string(context)?; + + // 3. Let len be the length of S. + let len = s.encode_utf16().count() as i64; + + // 4. Let relativeIndex be ? ToIntegerOrInfinity(index). + let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?; + let k = match relative_index { + // 5. If relativeIndex ≥ 0, then + // a. Let k be relativeIndex. + IntegerOrInfinity::Integer(i) if i >= 0 && i < len => i as usize, + // 6. Else, + // a. Let k be len + relativeIndex. + IntegerOrInfinity::Integer(i) if i < 0 && (-i) <= len => (len + i) as usize, + // 7. If k < 0 or k ≥ len, return undefined. + _ => return Ok(JsValue::undefined()), + }; + + // 8. Return the substring of S from k to k + 1. + if let Some(utf16_val) = s.encode_utf16().nth(k) { + Ok(JsValue::new( + from_u32(u32::from(utf16_val)).expect("invalid utf-16 character"), + )) + } else { + Ok(JsValue::undefined()) + } + } + + /// `String.prototype.codePointAt( index )` + /// + /// The `codePointAt()` method returns an integer between `0` to `1114111` (`0x10FFFF`) representing the UTF-16 code unit at the given index. + /// + /// If no UTF-16 surrogate pair begins at the index, the code point at the index is returned. + /// + /// `codePointAt()` returns `undefined` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.codepointat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/codePointAt + pub(crate) fn code_point_at( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let position be ? ToIntegerOrInfinity(pos). + let position = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + // 4. Let size be the length of S. + let size = string.encode_utf16().count() as i64; + + match position { + IntegerOrInfinity::Integer(position) if (0..size).contains(&position) => { + // 6. Let cp be ! CodePointAt(S, position). + // 7. Return 𝔽(cp.[[CodePoint]]). + Ok(code_point_at(&string, position as usize).0.into()) + } + // 5. If position < 0 or position ≥ size, return undefined. + _ => Ok(JsValue::undefined()), + } + } + + /// `String.prototype.charCodeAt( index )` + /// + /// The `charCodeAt()` method returns an integer between `0` and `65535` representing the UTF-16 code unit at the given index. + /// + /// Unicode code points range from `0` to `1114111` (`0x10FFFF`). The first 128 Unicode code points are a direct match of the ASCII character encoding. + /// + /// `charCodeAt()` returns `NaN` if the given index is less than `0`, or if it is equal to or greater than the `length` of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.charcodeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/charCodeAt + pub(crate) fn char_code_at( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let position be ? ToIntegerOrInfinity(pos). + let position = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + // 4. Let size be the length of S. + let size = string.encode_utf16().count() as i64; + + match position { + IntegerOrInfinity::Integer(position) if (0..size).contains(&position) => { + // 6. Return the Number value for the numeric value of the code unit at index position within the String S. + let char_code = u32::from( + string + .encode_utf16() + .nth(position as usize) + .expect("Already checked bounds above."), + ); + Ok(char_code.into()) + } + // 5. If position < 0 or position ≥ size, return NaN. + _ => Ok(JsValue::nan()), + } + } + + /// `String.prototype.concat( str1[, ...strN] )` + /// + /// The `concat()` method concatenates the string arguments to the calling string and returns a new string. + /// + /// Changes to the original string or the returned string don't affect the other. + /// + /// If the arguments are not of the type string, they are converted to string values before concatenating. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.concat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/concat + pub(crate) fn concat( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let mut string = this.to_string(context)?.to_string(); + + // 3. Let R be S. + // 4. For each element next of args, do + for arg in args { + // a. Let nextString be ? ToString(next). + // b. Set R to the string-concatenation of R and nextString. + string.push_str(&arg.to_string(context)?); + } + + // 5. Return R. + Ok(JsValue::new(string)) + } + + /// `String.prototype.repeat( count )` + /// + /// The `repeat()` method constructs and returns a new string which contains the specified number of + /// copies of the string on which it was called, concatenated together. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.repeat + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/repeat + pub(crate) fn repeat( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + let len = string.encode_utf16().count(); + + // 3. Let n be ? ToIntegerOrInfinity(count). + match args.get_or_undefined(0).to_integer_or_infinity(context)? { + IntegerOrInfinity::Integer(n) + if n > 0 && (n as usize) * len <= Self::MAX_STRING_LENGTH => + { + if string.is_empty() { + return Ok("".into()); + } + let n = n as usize; + let mut result = std::string::String::with_capacity(n * len); + + std::iter::repeat(&string[..]) + .take(n) + .for_each(|s| result.push_str(s)); + + // 6. Return the String value that is made from n copies of S appended together. + Ok(result.into()) + } + // 5. If n is 0, return the empty String. + IntegerOrInfinity::Integer(n) if n == 0 => Ok("".into()), + // 4. If n < 0 or n is +∞, throw a RangeError exception. + _ => context.throw_range_error( + "repeat count must be a positive finite number \ + that doesn't overflow the maximum string length (2^32 - 1)", + ), + } + } + + /// `String.prototype.slice( beginIndex [, endIndex] )` + /// + /// The `slice()` method extracts a section of a string and returns it as a new string, without modifying the original string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.slice + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/slice + pub(crate) fn slice( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let len be the length of S. + let len = string.encode_utf16().count() as i64; + + // 4. Let intStart be ? ToIntegerOrInfinity(start). + let from = match args.get_or_undefined(0).to_integer_or_infinity(context)? { + // 6. Else if intStart < 0, let from be max(len + intStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => max(len + i, 0), + + // 7. Else, let from be min(intStart, len). + IntegerOrInfinity::Integer(i) => min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + + // 5. If intStart is -∞, let from be 0. + IntegerOrInfinity::NegativeInfinity => 0, + } as usize; + + // 8. If end is undefined, let intEnd be len; else let intEnd be ? ToIntegerOrInfinity(end). + let to = match args + .get(1) + .filter(|end| !end.is_undefined()) + .map(|end| end.to_integer_or_infinity(context)) + .transpose()? + .unwrap_or(IntegerOrInfinity::Integer(len)) + { + // 10. Else if intEnd < 0, let to be max(len + intEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => max(len + i, 0), + + // 11. Else, let to be min(intEnd, len). + IntegerOrInfinity::Integer(i) => min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + + // 9. If intEnd is -∞, let to be 0. + IntegerOrInfinity::NegativeInfinity => 0, + } as usize; + + // 12. If from ≥ to, return the empty String. + if from >= to { + Ok("".into()) + } else { + // 13. Return the substring of S from from to to. + let span = to - from; + let substring_utf16: Vec = string.encode_utf16().skip(from).take(span).collect(); + let substring_lossy = StdString::from_utf16_lossy(&substring_utf16); + Ok(substring_lossy.into()) + } + } + + /// `String.prototype.startWith( searchString[, position] )` + /// + /// The `startsWith()` method determines whether a string begins with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.startswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/startsWith + pub(crate) fn starts_with( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + let search_string = args.get_or_undefined(0); + + // 3. Let isRegExp be ? IsRegExp(searchString). + // 4. If isRegExp is true, throw a TypeError exception. + if is_reg_exp(search_string, context)? { + context.throw_type_error( + "First argument to String.prototype.startsWith must not be a regular expression", + )?; + } + + // 5. Let searchStr be ? ToString(searchString). + let search_string = search_string.to_string(context)?; + + // 6. Let len be the length of S. + let len = string.encode_utf16().count() as i64; + + // 7. If position is undefined, let pos be 0; else let pos be ? ToIntegerOrInfinity(position). + let pos = match args.get_or_undefined(1) { + &JsValue::Undefined => IntegerOrInfinity::Integer(0), + position => position.to_integer_or_infinity(context)?, + }; + + // 8. Let start be the result of clamping pos between 0 and len. + let start = pos.clamp_finite(0, len) as usize; + + // 9. Let searchLength be the length of searchStr. + let search_length = search_string.encode_utf16().count(); + + // 10. If searchLength = 0, return true. + if search_length == 0 { + return Ok(JsValue::new(true)); + } + + // 11. Let end be start + searchLength. + let end = start + search_length; + + // 12. If end > len, return false. + if end > len as usize { + Ok(JsValue::new(false)) + } else { + // 13. Let substring be the substring of S from start to end. + // 14. Return ! SameValueNonNumeric(substring, searchStr). + // `SameValueNonNumeric` forwards to `==`, so directly check + // equality to avoid converting to `JsValue` + let substring_utf16 = string.encode_utf16().skip(start).take(search_length); + let search_str_utf16 = search_string.encode_utf16(); + Ok(JsValue::new(substring_utf16.eq(search_str_utf16))) + } + } + + /// `String.prototype.endsWith( searchString[, length] )` + /// + /// The `endsWith()` method determines whether a string ends with the characters of a specified string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.endswith + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/endsWith + pub(crate) fn ends_with( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + let search_str = match args.get_or_undefined(0) { + // 3. Let isRegExp be ? IsRegExp(searchString). + // 4. If isRegExp is true, throw a TypeError exception. + search_string if is_reg_exp(search_string, context)? => { + return context.throw_type_error( + "First argument to String.prototype.endsWith must not be a regular expression", + ); + } + // 5. Let searchStr be ? ToString(searchString). + search_string => search_string.to_string(context)?, + }; + + // 6. Let len be the length of S. + let len = string.encode_utf16().count() as i64; + + // 7. If endPosition is undefined, let pos be len; else let pos be ? ToIntegerOrInfinity(endPosition). + let end = match args.get_or_undefined(1) { + end_position if end_position.is_undefined() => IntegerOrInfinity::Integer(len), + end_position => end_position.to_integer_or_infinity(context)?, + }; + + // 8. Let end be the result of clamping pos between 0 and len. + let end = end.clamp_finite(0, len) as usize; + + // 9. Let searchLength be the length of searchStr. + let search_length = search_str.encode_utf16().count(); + + // 10. If searchLength = 0, return true. + if search_length == 0 { + return Ok(true.into()); + } + + // 11. Let start be end - searchLength. + if let Some(start) = end.checked_sub(search_length) { + // 13. Let substring be the substring of S from start to end. + // 14. Return ! SameValueNonNumeric(substring, searchStr). + // `SameValueNonNumeric` forwards to `==`, so directly check + // equality to avoid converting to `JsValue` + + let substring_utf16 = string.encode_utf16().skip(start).take(search_length); + let search_str_utf16 = search_str.encode_utf16(); + + Ok(JsValue::new(substring_utf16.eq(search_str_utf16))) + } else { + // 12. If start < 0, return false. + Ok(false.into()) + } + } + + /// `String.prototype.includes( searchString[, position] )` + /// + /// The `includes()` method determines whether one string may be found within another string, returning `true` or `false` as appropriate. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.includes + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes + pub(crate) fn includes( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + let search_str = match args.get_or_undefined(0) { + // 3. Let isRegExp be ? IsRegExp(searchString). + search_string if is_reg_exp(search_string, context)? => { + return context.throw_type_error( + // 4. If isRegExp is true, throw a TypeError exception. + "First argument to String.prototype.includes must not be a regular expression", + ); + } + // 5. Let searchStr be ? ToString(searchString). + search_string => search_string.to_string(context)?, + }; + + // 6. Let pos be ? ToIntegerOrInfinity(position). + // 7. Assert: If position is undefined, then pos is 0. + let pos = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + // 8. Let len be the length of S. + // 9. Let start be the result of clamping pos between 0 and len. + let start = pos.clamp_finite(0, string.encode_utf16().count() as i64) as usize; + + // 10. Let index be ! StringIndexOf(S, searchStr, start). + // 11. If index is not -1, return true. + // 12. Return false. + Ok(string.index_of(&search_str, start).is_some().into()) + } + + /// `String.prototype.replace( regexp|substr, newSubstr|function )` + /// + /// The `replace()` method returns a new string with some or all matches of a `pattern` replaced by a `replacement`. + /// + /// The `pattern` can be a string or a `RegExp`, and the `replacement` can be a string or a function to be called for each match. + /// If `pattern` is a string, only the first occurrence will be replaced. + /// + /// The original string is left unchanged. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replace + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + pub(crate) fn replace( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + this.require_object_coercible(context)?; + + let search_value = args.get_or_undefined(0); + + let replace_value = args.get_or_undefined(1); + + // 2. If searchValue is neither undefined nor null, then + if !search_value.is_null_or_undefined() { + // a. Let replacer be ? GetMethod(searchValue, @@replace). + let replacer = search_value.get_method(WellKnownSymbols::replace(), context)?; + + // b. If replacer is not undefined, then + if let Some(replacer) = replacer { + // i. Return ? Call(replacer, searchValue, « O, replaceValue »). + return replacer.call( + search_value, + &[this.clone(), replace_value.clone()], + context, + ); + } + } + + // 3. Let string be ? ToString(O). + let this_str = this.to_string(context)?; + + // 4. Let searchString be ? ToString(searchValue). + let search_str = search_value.to_string(context)?; + + // 5. Let functionalReplace be IsCallable(replaceValue). + let functional_replace = replace_value + .as_object() + .map(JsObject::is_callable) + .unwrap_or_default(); + + // 6. If functionalReplace is false, then + // a. Set replaceValue to ? ToString(replaceValue). + + // 7. Let searchLength be the length of searchString. + let search_length = search_str.len(); + + // 8. Let position be ! StringIndexOf(string, searchString, 0). + // 9. If position is -1, return string. + let position = if let Some(p) = this_str.index_of(&search_str, 0) { + p + } else { + return Ok(this_str.into()); + }; + + // 10. Let preserved be the substring of string from 0 to position. + let preserved = StdString::from_utf16_lossy( + &this_str.encode_utf16().take(position).collect::>(), + ); + + // 11. If functionalReplace is true, then + // 12. Else, + let replacement = if functional_replace { + // a. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(position), string »)). + context + .call( + replace_value, + &JsValue::undefined(), + &[search_str.into(), position.into(), this_str.clone().into()], + )? + .to_string(context)? + } else { + // a. Assert: Type(replaceValue) is String. + // b. Let captures be a new empty List. + let captures = Vec::new(); + + // c. Let replacement be ! GetSubstitution(searchString, string, position, captures, undefined, replaceValue). + get_substitution( + search_str.as_str(), + this_str.as_str(), + position, + &captures, + &JsValue::undefined(), + &replace_value.to_string(context)?, + context, + )? + }; + + // 13. Return the string-concatenation of preserved, replacement, and the substring of string from position + searchLength. + Ok(format!( + "{preserved}{replacement}{}", + StdString::from_utf16_lossy( + &this_str + .encode_utf16() + .skip(position + search_length) + .collect::>() + ) + ) + .into()) + } + + /// `22.1.3.18 String.prototype.replaceAll ( searchValue, replaceValue )` + /// + /// The replaceAll() method returns a new string with all matches of a pattern replaced by a + /// replacement. + /// + /// The pattern can be a string or a `RegExp`, and the replacement can be a string or a + /// function to be called for each match. + /// + /// The original string is left unchanged. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.replaceall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace + pub(crate) fn replace_all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let o = this.require_object_coercible(context)?; + + let search_value = args.get_or_undefined(0); + let replace_value = args.get_or_undefined(1); + + // 2. If searchValue is neither undefined nor null, then + if !search_value.is_null_or_undefined() { + // a. Let isRegExp be ? IsRegExp(searchValue). + if let Some(obj) = search_value.as_object() { + // b. If isRegExp is true, then + if is_reg_exp_object(obj, context)? { + // i. Let flags be ? Get(searchValue, "flags"). + let flags = obj.get("flags", context)?; + + // ii. Perform ? RequireObjectCoercible(flags). + flags.require_object_coercible(context)?; + + // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception. + if !flags.to_string(context)?.contains('g') { + return context.throw_type_error( + "String.prototype.replaceAll called with a non-global RegExp argument", + ); + } + } + } + + // c. Let replacer be ? GetMethod(searchValue, @@replace). + let replacer = search_value.get_method(WellKnownSymbols::replace(), context)?; + + // d. If replacer is not undefined, then + if let Some(replacer) = replacer { + // i. Return ? Call(replacer, searchValue, « O, replaceValue »). + return replacer.call(search_value, &[o.into(), replace_value.clone()], context); + } + } + + // 3. Let string be ? ToString(O). + let string = o.to_string(context)?; + + // 4. Let searchString be ? ToString(searchValue). + let search_string = search_value.to_string(context)?; + + // 5. Let functionalReplace be IsCallable(replaceValue). + let functional_replace = replace_value + .as_object() + .map(JsObject::is_callable) + .unwrap_or_default(); + + let replace_value_string = if functional_replace { + None + } else { + // a. Set replaceValue to ? ToString(replaceValue). + // 6. If functionalReplace is false, then + Some(replace_value.to_string(context)?) + }; + + // 7. Let searchLength be the length of searchString. + let search_length = search_string.encode_utf16().count(); + + // 8. Let advanceBy be max(1, searchLength). + let advance_by = max(1, search_length); + + // 9. Let matchPositions be a new empty List. + let mut match_positions = Vec::new(); + + // 10. Let position be ! StringIndexOf(string, searchString, 0). + let mut position = string.index_of(&search_string, 0); + + // 11. Repeat, while position is not -1, + while let Some(p) = position { + // a. Append position to the end of matchPositions. + match_positions.push(p); + + // b. Set position to ! StringIndexOf(string, searchString, position + advanceBy). + position = string.index_of(&search_string, p + advance_by); + } + + // 12. Let endOfLastMatch be 0. + let mut end_of_last_match = 0; + + // 13. Let result be the empty String. + let mut result = JsString::new(""); + + // 14. For each element p of matchPositions, do + for p in match_positions { + // a. Let preserved be the substring of string from endOfLastMatch to p. + let preserved = StdString::from_utf16_lossy( + &string + .clone() + .encode_utf16() + .skip(end_of_last_match) + .take(p - end_of_last_match) + .collect::>(), + ); + + // c. Else, + let replacement = if let Some(ref replace_value) = replace_value_string { + // i. Assert: Type(replaceValue) is String. + // ii. Let captures be a new empty List. + // iii. Let replacement be ! GetSubstitution(searchString, string, p, captures, undefined, replaceValue). + get_substitution( + &search_string, + &string, + p, + &[], + &JsValue::undefined(), + replace_value, + context, + ) + .expect("GetSubstitution should never fail here.") + } + // b. If functionalReplace is true, then + else { + // i. Let replacement be ? ToString(? Call(replaceValue, undefined, « searchString, 𝔽(p), string »)). + context + .call( + replace_value, + &JsValue::undefined(), + &[ + search_string.clone().into(), + p.into(), + string.clone().into(), + ], + )? + .to_string(context)? + }; + + // d. Set result to the string-concatenation of result, preserved, and replacement. + result = JsString::new(format!("{}{preserved}{replacement}", result.as_str())); + + // e. Set endOfLastMatch to p + searchLength. + end_of_last_match = p + search_length; + } + + // 15. If endOfLastMatch < the length of string, then + if end_of_last_match < string.encode_utf16().count() { + // a. Set result to the string-concatenation of result and the substring of string from endOfLastMatch. + result = JsString::new(format!( + "{}{}", + result.as_str(), + &StdString::from_utf16_lossy( + &string + .encode_utf16() + .skip(end_of_last_match) + .collect::>() + ) + )); + } + + // 16. Return result. + Ok(result.into()) + } + + /// `String.prototype.indexOf( searchValue[, fromIndex] )` + /// + /// The `indexOf()` method returns the index within the calling `String` object of the first occurrence + /// of the specified value, starting the search at `fromIndex`. + /// + /// Returns `-1` if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.indexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/indexOf + pub(crate) fn index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let searchStr be ? ToString(searchString). + let search_str = args.get_or_undefined(0).to_string(context)?; + + // 4. Let pos be ? ToIntegerOrInfinity(position). + // 5. Assert: If position is undefined, then pos is 0. + let pos = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + // 6. Let len be the length of S. + let len = string.encode_utf16().count() as i64; + + // 7. Let start be the result of clamping pos between 0 and len. + let start = pos.clamp_finite(0, len) as usize; + + // 8. Return 𝔽(! StringIndexOf(S, searchStr, start)). + Ok(string + .index_of(&search_str, start) + .map_or(-1, |i| i as i64) + .into()) + } + + /// `String.prototype.lastIndexOf( searchValue[, fromIndex] )` + /// + /// The `lastIndexOf()` method returns the index within the calling `String` object of the last occurrence + /// of the specified value, searching backwards from `fromIndex`. + /// + /// Returns `-1` if the value is not found. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.lastindexof + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/lastIndexOf + pub(crate) fn last_index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let searchStr be ? ToString(searchString). + let search_str = args.get_or_undefined(0).to_string(context)?; + + // 4. Let numPos be ? ToNumber(position). + // 5. Assert: If position is undefined, then numPos is NaN. + let num_pos = args.get_or_undefined(1).to_number(context)?; + + // 6. If numPos is NaN, let pos be +∞; otherwise, let pos be ! ToIntegerOrInfinity(numPos). + let pos = if num_pos.is_nan() { + IntegerOrInfinity::PositiveInfinity + } else { + JsValue::new(num_pos) + .to_integer_or_infinity(context) + .expect("Already called `to_number so this must not fail.") + }; + + // 7. Let len be the length of S. + let len = string.encode_utf16().count(); + // 8. Let start be the result of clamping pos between 0 and len. + let start = pos.clamp_finite(0, len as i64) as usize; + + // 9. If searchStr is the empty String, return 𝔽(start). + if search_str.is_empty() { + return Ok(JsValue::new(start)); + } + + // 10. Let searchLen be the length of searchStr. + let search_len = search_str.encode_utf16().count(); + + // 11. For each non-negative integer i starting with start such that i ≤ len - searchLen, in descending order, do + // a. Let candidate be the substring of S from i to i + searchLen. + let substring_utf16: Vec = string.encode_utf16().take(start + search_len).collect(); + let substring_lossy = StdString::from_utf16_lossy(&substring_utf16); + if let Some(position) = substring_lossy.rfind(search_str.as_str()) { + // b. If candidate is the same sequence of code units as searchStr, return 𝔽(i). + return Ok(JsValue::new( + substring_lossy[..position].encode_utf16().count(), + )); + } + + // 12. Return -1𝔽. + Ok(JsValue::new(-1)) + } + + /// `String.prototype.match( regexp )` + /// + /// The `match()` method retrieves the result of matching a **string** against a [`regular expression`][regex]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.match + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/match + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + pub(crate) fn r#match( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let o = this.require_object_coercible(context)?; + + // 2. If regexp is neither undefined nor null, then + let regexp = args.get_or_undefined(0); + if !regexp.is_null_or_undefined() { + // a. Let matcher be ? GetMethod(regexp, @@match). + let matcher = regexp.get_method(WellKnownSymbols::r#match(), context)?; + // b. If matcher is not undefined, then + if let Some(matcher) = matcher { + // i. Return ? Call(matcher, regexp, « O »). + return matcher.call(regexp, &[o.clone()], context); + } + } + + // 3. Let S be ? ToString(O). + let s = o.to_string(context)?; + + // 4. Let rx be ? RegExpCreate(regexp, undefined). + let rx = RegExp::create(regexp, &JsValue::Undefined, context)?; + + // 5. Return ? Invoke(rx, @@match, « S »). + rx.invoke(WellKnownSymbols::r#match(), &[JsValue::new(s)], context) + } + + /// Abstract operation `StringPad ( O, maxLength, fillString, placement )`. + /// + /// Performs the actual string padding for padStart/End. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-stringpad + fn string_pad( + object: &JsValue, + max_length: &JsValue, + fill_string: &JsValue, + placement: Placement, + context: &mut Context, + ) -> JsResult { + // 1. Let S be ? ToString(O). + let string = object.to_string(context)?; + + // 2. Let intMaxLength be ℝ(? ToLength(maxLength)). + let int_max_length = max_length.to_length(context)?; + + // 3. Let stringLength be the length of S. + let string_length = string.encode_utf16().count(); + + // 4. If intMaxLength ≤ stringLength, return S. + if int_max_length <= string_length { + return Ok(string.into()); + } + + // 5. If fillString is undefined, let filler be the String value consisting solely of the code unit 0x0020 (SPACE). + let filler = if fill_string.is_undefined() { + "\u{0020}".into() + } else { + // 6. Else, let filler be ? ToString(fillString). + fill_string.to_string(context)? + }; + + // 7. If filler is the empty String, return S. + if filler.is_empty() { + return Ok(string.into()); + } + + // 8. Let fillLen be intMaxLength - stringLength. + let fill_len = int_max_length - string_length; + let filler_len = filler.encode_utf16().count(); + + // 9. Let truncatedStringFiller be the String value consisting of repeated + // concatenations of filler truncated to length fillLen. + let repetitions = { + let q = fill_len / filler_len; + let r = fill_len % filler_len; + if r == 0 { + q + } else { + q + 1 + } + }; + + let truncated_string_filler = filler + .repeat(repetitions) + .encode_utf16() + .take(fill_len) + .collect::>(); + let truncated_string_filler = + std::string::String::from_utf16_lossy(truncated_string_filler.as_slice()); + + // 10. If placement is start, return the string-concatenation of truncatedStringFiller and S. + if placement == Placement::Start { + Ok(format!("{truncated_string_filler}{string}").into()) + } else { + // 11. Else, return the string-concatenation of S and truncatedStringFiller. + Ok(format!("{string}{truncated_string_filler}").into()) + } + } + + /// `String.prototype.padEnd( targetLength[, padString] )` + /// + /// The `padEnd()` method pads the current string with a given string (repeated, if needed) so that the resulting string reaches a given length. + /// + /// The padding is applied from the end of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padEnd + pub(crate) fn pad_end( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + let max_length = args.get_or_undefined(0); + let fill_string = args.get_or_undefined(1); + + // 2. Return ? StringPad(O, maxLength, fillString, end). + Self::string_pad(this, max_length, fill_string, Placement::End, context) + } + + /// `String.prototype.padStart( targetLength [, padString] )` + /// + /// The `padStart()` method pads the current string with another string (multiple times, if needed) until the resulting string reaches the given length. + /// + /// The padding is applied from the start of the current string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.padstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/padStart + pub(crate) fn pad_start( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + let max_length = args.get_or_undefined(0); + let fill_string = args.get_or_undefined(1); + + // 2. Return ? StringPad(O, maxLength, fillString, start). + Self::string_pad(this, max_length, fill_string, Placement::Start, context) + } + + /// String.prototype.trim() + /// + /// The `trim()` method removes whitespace from both ends of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trim + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trim + pub(crate) fn trim(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let object = this.require_object_coercible(context)?; + let string = object.to_string(context)?; + Ok(JsValue::new(string.trim_matches(is_trimmable_whitespace))) + } + + /// `String.prototype.trimStart()` + /// + /// The `trimStart()` method removes whitespace from the beginning of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimstart + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimStart + pub(crate) fn trim_start( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + Ok(JsValue::new( + string.trim_start_matches(is_trimmable_whitespace), + )) + } + + /// String.prototype.trimEnd() + /// + /// The `trimEnd()` method removes whitespace from the end of a string. + /// + /// Whitespace in this context is all the whitespace characters (space, tab, no-break space, etc.) and all the line terminator characters (LF, CR, etc.). + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.trimend + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/trimEnd + pub(crate) fn trim_end( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let this = this.require_object_coercible(context)?; + let string = this.to_string(context)?; + Ok(JsValue::new( + string.trim_end_matches(is_trimmable_whitespace), + )) + } + + /// `String.prototype.toLowerCase()` + /// + /// The `toLowerCase()` method returns the calling string value converted to lower case. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.tolowercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toLowerCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_lowercase( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let sText be ! StringToCodePoints(S). + // 4. Let lowerText be the result of toLowercase(sText), according to + // the Unicode Default Case Conversion algorithm. + // 5. Let L be ! CodePointsToString(lowerText). + // 6. Return L. + Ok(JsValue::new(string.to_lowercase())) + } + + /// `String.prototype.toUpperCase()` + /// + /// The `toUpperCase()` method returns the calling string value converted to uppercase. + /// + /// The value will be **converted** to a string if it isn't one + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.toUppercase + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/toUpperCase + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_uppercase( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // This function behaves in exactly the same way as `String.prototype.toLowerCase`, except that the String is + // mapped using the toUppercase algorithm of the Unicode Default Case Conversion. + + // Comments below are an adaptation of the `String.prototype.toLowerCase` documentation. + + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let sText be ! StringToCodePoints(S). + // 4. Let upperText be the result of toUppercase(sText), according to + // the Unicode Default Case Conversion algorithm. + // 5. Let L be ! CodePointsToString(upperText). + // 6. Return L. + Ok(JsValue::new(string.to_uppercase())) + } + + /// `String.prototype.substring( indexStart[, indexEnd] )` + /// + /// The `substring()` method returns the part of the `string` between the start and end indexes, or to the end of the string. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substring + pub(crate) fn substring( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let len be the length of S. + let len = string.encode_utf16().count() as i64; + + // 4. Let intStart be ? ToIntegerOrInfinity(start). + let int_start = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + // 5. If end is undefined, let intEnd be len; else let intEnd be ? ToIntegerOrInfinity(end). + let int_end = match args.get_or_undefined(1) { + &JsValue::Undefined => IntegerOrInfinity::Integer(len), + end => end.to_integer_or_infinity(context)?, + }; + + // 6. Let finalStart be the result of clamping intStart between 0 and len. + let final_start = int_start.clamp_finite(0, len) as usize; + + // 7. Let finalEnd be the result of clamping intEnd between 0 and len. + let final_end = int_end.clamp_finite(0, len) as usize; + + // 8. Let from be min(finalStart, finalEnd). + let from = min(final_start, final_end); + + // 9. Let to be max(finalStart, finalEnd). + let to = max(final_start, final_end); + + // 10. Return the substring of S from from to to. + // Extract the part of the string contained between the from index and the to index + // where from is guaranteed to be smaller or equal to to + // TODO: Full UTF-16 support + let substring_utf16: Vec = string.encode_utf16().skip(from).take(to - from).collect(); + let substring = StdString::from_utf16_lossy(&substring_utf16); + + Ok(substring.into()) + } + + /// `String.prototype.substr( start[, length] )` + /// + /// The `substr()` method returns a portion of the string, starting at the specified index and extending for a given number of characters afterward. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.substr + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/substr + /// + pub(crate) fn substr( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let string = this.to_string(context)?; + + // 3. Let size be the length of S. + let size = string.encode_utf16().count() as i64; + + // 4. Let intStart be ? ToIntegerOrInfinity(start). + let int_start = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + // 7. If length is undefined, let intLength be size; otherwise let intLength be ? ToIntegerOrInfinity(length). + // Moved it before to ensure an error throws before returning the empty string on `match int_start` + let int_length = match args.get_or_undefined(1) { + &JsValue::Undefined => IntegerOrInfinity::Integer(size), + val => val.to_integer_or_infinity(context)?, + }; + + let int_start = match int_start { + // 6. Else if intStart < 0, set intStart to max(size + intStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => max(size + i, 0), + IntegerOrInfinity::Integer(i) => i, + // 8. If intStart is +∞, ... return the empty String + IntegerOrInfinity::PositiveInfinity => return Ok("".into()), + // 5. If intStart is -∞, set intStart to 0. + IntegerOrInfinity::NegativeInfinity => 0, + } as usize; + + // 8. If ... intLength ≤ 0, or intLength is +∞, return the empty String. + let int_length = match int_length { + IntegerOrInfinity::Integer(i) if i > 0 => i, + _ => return Ok("".into()), + } as usize; + + // 9. Let intEnd be min(intStart + intLength, size). + let int_end = min(int_start + int_length, size as usize); + + // 11. Return the substring of S from intStart to intEnd. + // 10. If intStart ≥ intEnd, return the empty String. + let substring_utf16: Vec = string + .encode_utf16() + .skip(int_start) + .take(int_end - int_start) + .collect(); + let substring = StdString::from_utf16_lossy(&substring_utf16); + + Ok(substring.into()) + } + + /// `String.prototype.split ( separator, limit )` + /// + /// The split() method divides a String into an ordered list of substrings, puts these substrings into an array, and returns the array. + /// The division is done by searching for a pattern; where the pattern is provided as the first parameter in the method's call. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.split + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split + pub(crate) fn split( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + let separator = args.get_or_undefined(0); + let limit = args.get_or_undefined(1); + + // 2. If separator is neither undefined nor null, then + if !separator.is_null_or_undefined() { + // a. Let splitter be ? GetMethod(separator, @@split). + let splitter = separator.get_method(WellKnownSymbols::split(), context)?; + // b. If splitter is not undefined, then + if let Some(splitter) = splitter { + // i. Return ? Call(splitter, separator, « O, limit »). + return splitter.call(separator, &[this.clone(), limit.clone()], context); + } + } + + // 3. Let S be ? ToString(O). + let this_str = this.to_string(context)?; + + // 4. Let A be ! ArrayCreate(0). + let a = Array::array_create(0, None, context)?; + + // 5. Let lengthA be 0. + let mut length_a = 0; + + // 6. If limit is undefined, let lim be 2^32 - 1; else let lim be ℝ(? ToUint32(limit)). + let lim = if limit.is_undefined() { + u32::MAX + } else { + limit.to_u32(context)? + }; + + // 7. Let R be ? ToString(separator). + let separator_str = separator.to_string(context)?; + + // 8. If lim = 0, return A. + if lim == 0 { + return Ok(a.into()); + } + + // 9. If separator is undefined, then + if separator.is_undefined() { + // a. Perform ! CreateDataPropertyOrThrow(A, "0", S). + a.create_data_property_or_throw(0, this_str, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // b. Return A. + return Ok(a.into()); + } + + // 10. Let s be the length of S. + let this_str_length = this_str.encode_utf16().count(); + + // 11. If s = 0, then + if this_str_length == 0 { + // a. If R is not the empty String, then + if !separator_str.is_empty() { + // i. Perform ! CreateDataPropertyOrThrow(A, "0", S). + a.create_data_property_or_throw(0, this_str, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + } + + // b. Return A. + return Ok(a.into()); + } + + // 12. Let p be 0. + // 13. Let q be p. + let mut p = 0; + let mut q = p; + + // 14. Repeat, while q ≠ s, + while q != this_str_length { + // a. Let e be SplitMatch(S, q, R). + let e = split_match(&this_str, q, &separator_str); + + match e { + // b. If e is not-matched, set q to q + 1. + None => q += 1, + // c. Else, + Some(e) => { + // i. Assert: e is a non-negative integer ≤ s. + // ii. If e = p, set q to q + 1. + // iii. Else, + if e == p { + q += 1; + } else { + // 1. Let T be the substring of S from p to q. + let this_str_substring = StdString::from_utf16_lossy( + &this_str + .encode_utf16() + .skip(p) + .take(q - p) + .collect::>(), + ); + + // 2. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T). + a.create_data_property_or_throw(length_a, this_str_substring, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 3. Set lengthA to lengthA + 1. + length_a += 1; + + // 4. If lengthA = lim, return A. + if length_a == lim { + return Ok(a.into()); + } + + // 5. Set p to e. + p = e; + + // 6. Set q to p. + q = p; + } + } + } + } + + // 15. Let T be the substring of S from p to s. + let this_str_substring = StdString::from_utf16_lossy( + &this_str + .encode_utf16() + .skip(p) + .take(this_str_length - p) + .collect::>(), + ); + + // 16. Perform ! CreateDataPropertyOrThrow(A, ! ToString(𝔽(lengthA)), T). + a.create_data_property_or_throw(length_a, this_str_substring, context) + .expect("this CreateDataPropertyOrThrow call must not fail"); + + // 17. Return A. + Ok(a.into()) + } + + /// String.prototype.valueOf() + /// + /// The `valueOf()` method returns the primitive value of a `String` object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.value_of + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/valueOf + pub(crate) fn value_of( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? thisStringValue(this value). + Self::this_string_value(this, context).map(JsValue::from) + } + + /// `String.prototype.matchAll( regexp )` + /// + /// The `matchAll()` method returns an iterator of all results matching a string against a [`regular expression`][regex], including [capturing groups][cg]. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.matchall + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll + /// [regex]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions + /// [cg]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions/Groups_and_Ranges + pub(crate) fn match_all( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let o = this.require_object_coercible(context)?; + + // 2. If regexp is neither undefined nor null, then + let regexp = args.get_or_undefined(0); + if !regexp.is_null_or_undefined() { + // a. Let isRegExp be ? IsRegExp(regexp). + // b. If isRegExp is true, then + if let Some(regexp_obj) = regexp.as_object().filter(|obj| obj.is_regexp()) { + // i. Let flags be ? Get(regexp, "flags"). + let flags = regexp_obj.get("flags", context)?; + + // ii. Perform ? RequireObjectCoercible(flags). + flags.require_object_coercible(context)?; + + // iii. If ? ToString(flags) does not contain "g", throw a TypeError exception. + if !flags.to_string(context)?.contains('g') { + return context.throw_type_error( + "String.prototype.matchAll called with a non-global RegExp argument", + ); + } + } + // c. Let matcher be ? GetMethod(regexp, @@matchAll). + let matcher = regexp.get_method(WellKnownSymbols::match_all(), context)?; + // d. If matcher is not undefined, then + if let Some(matcher) = matcher { + return matcher.call(regexp, &[o.clone()], context); + } + } + + // 3. Let S be ? ToString(O). + let s = o.to_string(context)?; + + // 4. Let rx be ? RegExpCreate(regexp, "g"). + let rx = RegExp::create(regexp, &JsValue::new("g"), context)?; + + // 5. Return ? Invoke(rx, @@matchAll, « S »). + rx.invoke(WellKnownSymbols::match_all(), &[JsValue::new(s)], context) + } + + /// `String.prototype.normalize( [ form ] )` + /// + /// The normalize() method normalizes a string into a form specified in the Unicode® Standard Annex #15 + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.normalize + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize + pub(crate) fn normalize( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let this = this.require_object_coercible(context)?; + + // 2. Let S be ? ToString(O). + let s = this.to_string(context)?; + + let form = args.get_or_undefined(0); + + let f_str; + + let f = if form.is_undefined() { + // 3. If form is undefined, let f be "NFC". + "NFC" + } else { + // 4. Else, let f be ? ToString(form). + f_str = form.to_string(context)?; + f_str.as_str() + }; + + // 6. Let ns be the String value that is the result of normalizing S + // into the normalization form named by f as specified in + // https://unicode.org/reports/tr15/. + // 7. Return ns. + match f { + "NFC" => Ok(JsValue::new(s.nfc().collect::())), + "NFD" => Ok(JsValue::new(s.nfd().collect::())), + "NFKC" => Ok(JsValue::new(s.nfkc().collect::())), + "NFKD" => Ok(JsValue::new(s.nfkd().collect::())), + // 5. If f is not one of "NFC", "NFD", "NFKC", or "NFKD", throw a RangeError exception. + _ => context + .throw_range_error("The normalization form should be one of NFC, NFD, NFKC, NFKD."), + } + } + + /// `String.prototype.search( regexp )` + /// + /// The search() method executes a search for a match between a regular expression and this String object. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-string.prototype.search + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/search + pub(crate) fn search( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be ? RequireObjectCoercible(this value). + let o = this.require_object_coercible(context)?; + + // 2. If regexp is neither undefined nor null, then + let regexp = args.get_or_undefined(0); + if !regexp.is_null_or_undefined() { + // a. Let searcher be ? GetMethod(regexp, @@search). + let searcher = regexp.get_method(WellKnownSymbols::search(), context)?; + // b. If searcher is not undefined, then + if let Some(searcher) = searcher { + // i. Return ? Call(searcher, regexp, « O »). + return searcher.call(regexp, &[o.clone()], context); + } + } + + // 3. Let string be ? ToString(O). + let string = o.to_string(context)?; + + // 4. Let rx be ? RegExpCreate(regexp, undefined). + let rx = RegExp::create(regexp, &JsValue::Undefined, context)?; + + // 5. Return ? Invoke(rx, @@search, « string »). + rx.invoke(WellKnownSymbols::search(), &[JsValue::new(string)], context) + } + + pub(crate) fn iterator( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + StringIterator::create_string_iterator(this.clone(), context) + } +} + +/// Abstract operation `GetSubstitution ( matched, str, position, captures, namedCaptures, replacement )` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-getsubstitution +pub(crate) fn get_substitution( + matched: &str, + str: &str, + position: usize, + captures: &[JsValue], + named_captures: &JsValue, + replacement: &JsString, + context: &mut Context, +) -> JsResult { + // 1. Assert: Type(matched) is String. + + // 2. Let matchLength be the number of code units in matched. + let match_length = matched.encode_utf16().count(); + + // 3. Assert: Type(str) is String. + + // 4. Let stringLength be the number of code units in str. + let str_length = str.encode_utf16().count(); + + // 5. Assert: position ≤ stringLength. + // 6. Assert: captures is a possibly empty List of Strings. + // 7. Assert: Type(replacement) is String. + + // 8. Let tailPos be position + matchLength. + let tail_pos = position + match_length; + + // 9. Let m be the number of elements in captures. + let m = captures.len(); + + // 10. Let result be the String value derived from replacement by copying code unit elements + // from replacement to result while performing replacements as specified in Table 58. + // These $ replacements are done left-to-right, and, once such a replacement is performed, + // the new replacement text is not subject to further replacements. + let mut result = StdString::new(); + let mut chars = replacement.chars().peekable(); + + while let Some(first) = chars.next() { + if first == '$' { + let second = chars.next(); + let second_is_digit = second.as_ref().map_or(false, char::is_ascii_digit); + // we use peek so that it is still in the iterator if not used + let third = if second_is_digit { chars.peek() } else { None }; + let third_is_digit = third.map_or(false, char::is_ascii_digit); + + match (second, third) { + // $$ + (Some('$'), _) => { + // $ + result.push('$'); + } + // $& + (Some('&'), _) => { + // matched + result.push_str(matched); + } + // $` + (Some('`'), _) => { + // The replacement is the substring of str from 0 to position. + result.push_str(&StdString::from_utf16_lossy( + &str.encode_utf16().take(position).collect::>(), + )); + } + // $' + (Some('\''), _) => { + // If tailPos ≥ stringLength, the replacement is the empty String. + // Otherwise the replacement is the substring of str from tailPos. + if tail_pos < str_length { + result.push_str(&StdString::from_utf16_lossy( + &str.encode_utf16().skip(tail_pos).collect::>(), + )); + } + } + // $nn + (Some(second), Some(third)) if second_is_digit && third_is_digit => { + // The nnth element of captures, where nn is a two-digit decimal number in the range 01 to 99. + let tens = second + .to_digit(10) + .expect("could not convert character to digit after checking it") + as usize; + let units = third + .to_digit(10) + .expect("could not convert character to digit after checking it") + as usize; + let nn = 10 * tens + units; + + // If nn ≤ m and the nnth element of captures is undefined, use the empty String instead. + // If nn is 00 or nn > m, no replacement is done. + if nn == 0 || nn > m { + result.push('$'); + result.push(second); + result.push(*third); + } else if let Some(capture) = captures.get(nn - 1) { + if let Some(s) = capture.as_string() { + result.push_str(s); + } + } + + chars.next(); + } + // $n + (Some(second), _) if second_is_digit => { + // The nth element of captures, where n is a single digit in the range 1 to 9. + let n = second + .to_digit(10) + .expect("could not convert character to digit after checking it") + as usize; + + // If n ≤ m and the nth element of captures is undefined, use the empty String instead. + // If n > m, no replacement is done. + if n == 0 || n > m { + result.push('$'); + result.push(second); + } else if let Some(capture) = captures.get(n - 1) { + if let Some(s) = capture.as_string() { + result.push_str(s); + } + } + } + // $< + (Some('<'), _) => { + // 1. If namedCaptures is undefined, the replacement text is the String "$<". + // 2. Else, + if named_captures.is_undefined() { + result.push_str("$<"); + } else { + // a. Assert: Type(namedCaptures) is Object. + let named_captures = named_captures + .as_object() + .expect("should be an object according to spec"); + + // b. Scan until the next > U+003E (GREATER-THAN SIGN). + let mut group_name = StdString::new(); + let mut found = false; + loop { + match chars.next() { + Some('>') => { + found = true; + break; + } + Some(c) => group_name.push(c), + None => break, + } + } + + // c. If none is found, the replacement text is the String "$<". + #[allow(clippy::if_not_else)] + if !found { + result.push_str("$<"); + result.push_str(&group_name); + // d. Else, + } else { + // i. Let groupName be the enclosed substring. + // ii. Let capture be ? Get(namedCaptures, groupName). + let capture = named_captures.get(group_name, context)?; + + // iii. If capture is undefined, replace the text through > with the empty String. + // iv. Otherwise, replace the text through > with ? ToString(capture). + if !capture.is_undefined() { + result.push_str(capture.to_string(context)?.as_str()); + } + } + } + } + // $?, ? is none of the above + _ => { + result.push('$'); + if let Some(second) = second { + result.push(second); + } + } + } + } else { + result.push(first); + } + } + + // 11. Return result. + Ok(result.into()) +} + +/// `22.1.3.21.1 SplitMatch ( S, q, R )` +/// +/// More information: +/// - [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-splitmatch +fn split_match(s_str: &str, q: usize, r_str: &str) -> Option { + // 1. Let r be the number of code units in R. + let r = r_str.encode_utf16().count(); + + // 2. Let s be the number of code units in S. + let s = s_str.encode_utf16().count(); + + // 3. If q + r > s, return not-matched. + if q + r > s { + return None; + } + + // 4. If there exists an integer i between 0 (inclusive) and r (exclusive) + // such that the code unit at index q + i within S is different from the code unit at index i within R, + // return not-matched. + for i in 0..r { + if let Some(s_char) = s_str.encode_utf16().nth(q + i) { + if let Some(r_char) = r_str.encode_utf16().nth(i) { + if s_char != r_char { + return None; + } + } + } + } + + // 5. Return q + r. + Some(q + r) +} + +/// Abstract operation `IsRegExp( argument )` +/// +/// More information: +/// [ECMAScript reference][spec] +/// +/// [spec]: https://tc39.es/ecma262/#sec-isregexp +fn is_reg_exp(argument: &JsValue, context: &mut Context) -> JsResult { + // 1. If Type(argument) is not Object, return false. + let argument = match argument { + JsValue::Object(o) => o, + _ => return Ok(false), + }; + + is_reg_exp_object(argument, context) +} +fn is_reg_exp_object(argument: &JsObject, context: &mut Context) -> JsResult { + // 2. Let matcher be ? Get(argument, @@match). + let matcher = argument.get(WellKnownSymbols::r#match(), context)?; + + // 3. If matcher is not undefined, return ! ToBoolean(matcher). + if !matcher.is_undefined() { + return Ok(matcher.to_boolean()); + } + + // 4. If argument has a [[RegExpMatcher]] internal slot, return true. + // 5. Return false. + Ok(argument.is_regexp()) +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/string_iterator.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/string_iterator.rs new file mode 100644 index 0000000..31fd9c0 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/string_iterator.rs @@ -0,0 +1,100 @@ +use crate::{ + builtins::{ + function::make_builtin_fn, iterable::create_iter_result_object, string::code_point_at, + }, + object::{JsObject, ObjectData}, + property::PropertyDescriptor, + symbol::WellKnownSymbols, + Context, JsResult, JsValue, +}; +use boa_gc::{Finalize, Trace}; +use boa_profiler::Profiler; + +#[derive(Debug, Clone, Finalize, Trace)] +pub struct StringIterator { + string: JsValue, + next_index: i32, +} + +impl StringIterator { + fn new(string: JsValue) -> Self { + Self { + string, + next_index: 0, + } + } + + pub fn create_string_iterator(string: JsValue, context: &mut Context) -> JsResult { + let string_iterator = JsObject::from_proto_and_data( + context + .intrinsics() + .objects() + .iterator_prototypes() + .string_iterator(), + ObjectData::string_iterator(Self::new(string)), + ); + Ok(string_iterator.into()) + } + + pub fn next(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let mut string_iterator = this.as_object().map(JsObject::borrow_mut); + let string_iterator = string_iterator + .as_mut() + .and_then(|obj| obj.as_string_iterator_mut()) + .ok_or_else(|| context.construct_type_error("`this` is not an ArrayIterator"))?; + + if string_iterator.string.is_undefined() { + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + let native_string = string_iterator.string.to_string(context)?; + let len = native_string.encode_utf16().count() as i32; + let position = string_iterator.next_index; + if position >= len { + string_iterator.string = JsValue::undefined(); + return Ok(create_iter_result_object( + JsValue::undefined(), + true, + context, + )); + } + let (_, code_unit_count, _) = code_point_at(&native_string, position as usize); + string_iterator.next_index += i32::from(code_unit_count); + let result_string = crate::builtins::string::String::substring( + &string_iterator.string, + &[position.into(), string_iterator.next_index.into()], + context, + )?; + Ok(create_iter_result_object(result_string, false, context)) + } + + /// Create the `%ArrayIteratorPrototype%` object + /// + /// More information: + /// - [ECMA reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%arrayiteratorprototype%-object + pub(crate) fn create_prototype( + iterator_prototype: JsObject, + context: &mut Context, + ) -> JsObject { + let _timer = Profiler::global().start_event("String Iterator", "init"); + + // Create prototype + let array_iterator = + JsObject::from_proto_and_data(iterator_prototype, ObjectData::ordinary()); + make_builtin_fn(Self::next, "next", &array_iterator, 0, context); + + let to_string_tag = WellKnownSymbols::to_string_tag(); + let to_string_tag_property = PropertyDescriptor::builder() + .value("String Iterator") + .writable(false) + .enumerable(false) + .configurable(true); + array_iterator.insert(to_string_tag, to_string_tag_property); + array_iterator + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/tests.rs new file mode 100644 index 0000000..a8f3b2e --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/string/tests.rs @@ -0,0 +1,1152 @@ +use crate::{forward, forward_val, Context}; + +#[test] +fn length() { + //TEST262: https://github.com/tc39/test262/blob/master/test/built-ins/String/length.js + let mut context = Context::default(); + let init = r#" + const a = new String(' '); + const b = new String('\ud834\udf06'); + const c = new String(' \b '); + const d = new String('中文长度') + "#; + eprintln!("{}", forward(&mut context, init)); + let a = forward(&mut context, "a.length"); + assert_eq!(a, "1"); + let b = forward(&mut context, "b.length"); + // unicode surrogate pair length should be 1 + // utf16/usc2 length should be 2 + // utf8 length should be 4 + assert_eq!(b, "2"); + let c = forward(&mut context, "c.length"); + assert_eq!(c, "3"); + let d = forward(&mut context, "d.length"); + assert_eq!(d, "4"); +} + +#[test] +fn new_string_has_length() { + let mut context = Context::default(); + let init = r#" + let a = new String("1234"); + a + "#; + + forward(&mut context, init); + assert_eq!(forward(&mut context, "a.length"), "4"); +} + +#[test] +fn new_string_has_length_not_enumerable() { + let mut context = Context::default(); + let init = r#" + let a = new String("1234"); + "#; + + forward(&mut context, init); + assert_eq!( + forward(&mut context, "a.propertyIsEnumerable('length')"), + "false" + ); +} + +#[test] +fn new_utf8_string_has_length() { + let mut context = Context::default(); + let init = r#" + let a = new String("中文"); + a + "#; + + forward(&mut context, init); + assert_eq!(forward(&mut context, "a.length"), "2"); +} + +#[test] +fn concat() { + let mut context = Context::default(); + let init = r#" + var hello = new String('Hello, '); + var world = new String('world! '); + var nice = new String('Have a nice day.'); + "#; + eprintln!("{}", forward(&mut context, init)); + + let a = forward(&mut context, "hello.concat(world, nice)"); + assert_eq!(a, "\"Hello, world! Have a nice day.\""); + + let b = forward(&mut context, "hello + world + nice"); + assert_eq!(b, "\"Hello, world! Have a nice day.\""); +} + +#[test] +fn generic_concat() { + let mut context = Context::default(); + let init = r#" + Number.prototype.concat = String.prototype.concat; + let number = new Number(100); + "#; + eprintln!("{}", forward(&mut context, init)); + + let a = forward(&mut context, "number.concat(' - 50', ' = 50')"); + assert_eq!(a, "\"100 - 50 = 50\""); +} + +#[allow(clippy::unwrap_used)] +#[test] +/// Test the correct type is returned from call and construct +fn construct_and_call() { + let mut context = Context::default(); + let init = r#" + var hello = new String('Hello'); + var world = String('world'); + "#; + + forward(&mut context, init); + + let hello = forward_val(&mut context, "hello").unwrap(); + let world = forward_val(&mut context, "world").unwrap(); + + assert!(hello.is_object()); + assert!(world.is_string()); +} + +#[test] +fn repeat() { + let mut context = Context::default(); + let init = r#" + var empty = new String(''); + var en = new String('english'); + var zh = new String('中文'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "empty.repeat(0)"), "\"\""); + assert_eq!(forward(&mut context, "empty.repeat(1)"), "\"\""); + + assert_eq!(forward(&mut context, "en.repeat(0)"), "\"\""); + assert_eq!(forward(&mut context, "zh.repeat(0)"), "\"\""); + + assert_eq!(forward(&mut context, "en.repeat(1)"), "\"english\""); + assert_eq!(forward(&mut context, "zh.repeat(2)"), "\"中文中文\""); +} + +#[test] +fn repeat_throws_when_count_is_negative() { + let mut context = Context::default(); + + assert_eq!( + forward( + &mut context, + r#" + try { + 'x'.repeat(-1) + } catch (e) { + e.toString() + } + "# + ), + "\"RangeError: repeat count must be a positive finite number \ + that doesn't overflow the maximum string length (2^32 - 1)\"" + ); +} + +#[test] +fn repeat_throws_when_count_is_infinity() { + let mut context = Context::default(); + + assert_eq!( + forward( + &mut context, + r#" + try { + 'x'.repeat(Infinity) + } catch (e) { + e.toString() + } + "# + ), + "\"RangeError: repeat count must be a positive finite number \ + that doesn't overflow the maximum string length (2^32 - 1)\"" + ); +} + +#[test] +fn repeat_throws_when_count_overflows_max_length() { + let mut context = Context::default(); + + assert_eq!( + forward( + &mut context, + r#" + try { + 'x'.repeat(2 ** 64) + } catch (e) { + e.toString() + } + "# + ), + "\"RangeError: repeat count must be a positive finite number \ + that doesn't overflow the maximum string length (2^32 - 1)\"" + ); +} + +#[test] +fn repeat_generic() { + let mut context = Context::default(); + let init = "Number.prototype.repeat = String.prototype.repeat;"; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "(0).repeat(0)"), "\"\""); + assert_eq!(forward(&mut context, "(1).repeat(1)"), "\"1\""); + + assert_eq!(forward(&mut context, "(1).repeat(5)"), "\"11111\""); + assert_eq!(forward(&mut context, "(12).repeat(3)"), "\"121212\""); +} + +#[test] +fn replace() { + let mut context = Context::default(); + let init = r#" + var a = "abc"; + a = a.replace("a", "2"); + a + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "a"), "\"2bc\""); +} + +#[test] +fn replace_no_match() { + let mut context = Context::default(); + let init = r#" + var a = "abc"; + a = a.replace(/d/, "$&$&"); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "a"), "\"abc\""); +} + +#[test] +fn replace_with_capture_groups() { + let mut context = Context::default(); + let init = r#" + var re = /(\w+)\s(\w+)/; + var a = "John Smith"; + a = a.replace(re, '$2, $1'); + a + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "a"), "\"Smith, John\""); +} + +#[test] +fn replace_with_tenth_capture_group() { + let mut context = Context::default(); + let init = r#" + var re = /(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)(\d)/; + var a = "0123456789"; + let res = a.replace(re, '$10'); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "res"), "\"9\""); +} + +#[test] +fn replace_substitutions() { + let mut context = Context::default(); + let init = r#" + var re = / two /; + var a = "one two three"; + var dollar = a.replace(re, " $$ "); + var matched = a.replace(re, "$&$&"); + var start = a.replace(re, " $` "); + var end = a.replace(re, " $' "); + var no_sub = a.replace(re, " $_ "); + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "dollar"), "\"one $ three\""); + assert_eq!(forward(&mut context, "matched"), "\"one two two three\""); + assert_eq!(forward(&mut context, "start"), "\"one one three\""); + assert_eq!(forward(&mut context, "end"), "\"one three three\""); + assert_eq!(forward(&mut context, "no_sub"), "\"one $_ three\""); +} + +#[test] +fn replace_with_function() { + let mut context = Context::default(); + let init = r#" + var a = "ecmascript is cool"; + var p1, p2, p3, length; + var replacer = (match, cap1, cap2, cap3, len) => { + p1 = cap1; + p2 = cap2; + p3 = cap3; + length = len; + return "awesome!"; + }; + + a = a.replace(/c(o)(o)(l)/, replacer); + a; + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "a"), "\"ecmascript is awesome!\""); + + assert_eq!(forward(&mut context, "p1"), "\"o\""); + assert_eq!(forward(&mut context, "p2"), "\"o\""); + assert_eq!(forward(&mut context, "p3"), "\"l\""); + assert_eq!(forward(&mut context, "length"), "14"); +} + +#[test] +fn starts_with() { + let mut context = Context::default(); + let init = r#" + var empty = new String(''); + var en = new String('english'); + var zh = new String('中文'); + + var emptyLiteral = ''; + var enLiteral = 'english'; + var zhLiteral = '中文'; + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "empty.startsWith('')"), "true"); + assert_eq!(forward(&mut context, "en.startsWith('e')"), "true"); + assert_eq!(forward(&mut context, "zh.startsWith('中')"), "true"); + + assert_eq!(forward(&mut context, "emptyLiteral.startsWith('')"), "true"); + assert_eq!(forward(&mut context, "enLiteral.startsWith('e')"), "true"); + assert_eq!(forward(&mut context, "zhLiteral.startsWith('中')"), "true"); +} + +#[test] +fn starts_with_with_regex_arg() { + let mut context = Context::default(); + + let scenario = r#" + try { + 'Saturday night'.startsWith(/Saturday/); + } catch (e) { + e.toString(); + } + "#; + + assert_eq!( + forward( + &mut context, scenario + ), + "\"TypeError: First argument to String.prototype.startsWith must not be a regular expression\"" + ); +} + +#[test] +fn ends_with() { + let mut context = Context::default(); + let init = r#" + var empty = new String(''); + var en = new String('english'); + var zh = new String('中文'); + + var emptyLiteral = ''; + var enLiteral = 'english'; + var zhLiteral = '中文'; + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "empty.endsWith('')"), "true"); + assert_eq!(forward(&mut context, "en.endsWith('h')"), "true"); + assert_eq!(forward(&mut context, "zh.endsWith('文')"), "true"); + + assert_eq!(forward(&mut context, "emptyLiteral.endsWith('')"), "true"); + assert_eq!(forward(&mut context, "enLiteral.endsWith('h')"), "true"); + assert_eq!(forward(&mut context, "zhLiteral.endsWith('文')"), "true"); +} + +#[test] +fn ends_with_with_regex_arg() { + let mut context = Context::default(); + + let scenario = r#" + try { + 'Saturday night'.endsWith(/night/); + } catch (e) { + e.toString(); + } + "#; + + assert_eq!( + forward( + &mut context, scenario + ), + "\"TypeError: First argument to String.prototype.endsWith must not be a regular expression\"" + ); +} + +#[test] +fn includes() { + let mut context = Context::default(); + let init = r#" + var empty = new String(''); + var en = new String('english'); + var zh = new String('中文'); + + var emptyLiteral = ''; + var enLiteral = 'english'; + var zhLiteral = '中文'; + "#; + + forward(&mut context, init); + + assert_eq!(forward(&mut context, "empty.includes('')"), "true"); + assert_eq!(forward(&mut context, "en.includes('g')"), "true"); + assert_eq!(forward(&mut context, "zh.includes('文')"), "true"); + + assert_eq!(forward(&mut context, "emptyLiteral.includes('')"), "true"); + assert_eq!(forward(&mut context, "enLiteral.includes('g')"), "true"); + assert_eq!(forward(&mut context, "zhLiteral.includes('文')"), "true"); +} + +#[test] +fn includes_with_regex_arg() { + let mut context = Context::default(); + + let scenario = r#" + try { + 'Saturday night'.includes(/day/); + } catch (e) { + e.toString(); + } + "#; + + assert_eq!( + forward( + &mut context, scenario + ), + "\"TypeError: First argument to String.prototype.includes must not be a regular expression\"" + ); +} + +#[test] +fn match_all() { + let mut context = Context::default(); + + forward( + &mut context, + r#" + var groupMatches = 'test1test2'.matchAll(/t(e)(st(\d?))/g); + var m1 = groupMatches.next(); + var m2 = groupMatches.next(); + var m3 = groupMatches.next(); + "#, + ); + + assert_eq!(forward(&mut context, "m1.done"), "false"); + assert_eq!(forward(&mut context, "m2.done"), "false"); + assert_eq!(forward(&mut context, "m3.done"), "true"); + + assert_eq!(forward(&mut context, "m1.value[0]"), "\"test1\""); + assert_eq!(forward(&mut context, "m1.value[1]"), "\"e\""); + assert_eq!(forward(&mut context, "m1.value[2]"), "\"st1\""); + assert_eq!(forward(&mut context, "m1.value[3]"), "\"1\""); + assert_eq!(forward(&mut context, "m1.value[4]"), "undefined"); + assert_eq!(forward(&mut context, "m1.value.index"), "0"); + assert_eq!(forward(&mut context, "m1.value.input"), "\"test1test2\""); + assert_eq!(forward(&mut context, "m1.value.groups"), "undefined"); + + assert_eq!(forward(&mut context, "m2.value[0]"), "\"test2\""); + assert_eq!(forward(&mut context, "m2.value[1]"), "\"e\""); + assert_eq!(forward(&mut context, "m2.value[2]"), "\"st2\""); + assert_eq!(forward(&mut context, "m2.value[3]"), "\"2\""); + assert_eq!(forward(&mut context, "m2.value[4]"), "undefined"); + assert_eq!(forward(&mut context, "m2.value.index"), "5"); + assert_eq!(forward(&mut context, "m2.value.input"), "\"test1test2\""); + assert_eq!(forward(&mut context, "m2.value.groups"), "undefined"); + + assert_eq!(forward(&mut context, "m3.value"), "undefined"); + + forward( + &mut context, + r#" + var regexp = RegExp('foo[a-z]*','g'); + var str = 'table football, foosball'; + var matches = str.matchAll(regexp); + var m1 = matches.next(); + var m2 = matches.next(); + var m3 = matches.next(); + "#, + ); + + assert_eq!(forward(&mut context, "m1.done"), "false"); + assert_eq!(forward(&mut context, "m2.done"), "false"); + assert_eq!(forward(&mut context, "m3.done"), "true"); + + assert_eq!(forward(&mut context, "m1.value[0]"), "\"football\""); + assert_eq!(forward(&mut context, "m1.value[1]"), "undefined"); + assert_eq!(forward(&mut context, "m1.value.index"), "6"); + assert_eq!( + forward(&mut context, "m1.value.input"), + "\"table football, foosball\"" + ); + assert_eq!(forward(&mut context, "m1.value.groups"), "undefined"); + + assert_eq!(forward(&mut context, "m2.value[0]"), "\"foosball\""); + assert_eq!(forward(&mut context, "m2.value[1]"), "undefined"); + assert_eq!(forward(&mut context, "m2.value.index"), "16"); + assert_eq!( + forward(&mut context, "m1.value.input"), + "\"table football, foosball\"" + ); + assert_eq!(forward(&mut context, "m2.value.groups"), "undefined"); + + assert_eq!(forward(&mut context, "m3.value"), "undefined"); +} + +#[test] +fn test_match() { + let mut context = Context::default(); + let init = r#" + var str = new String('The Quick Brown Fox Jumps Over The Lazy Dog'); + var result1 = str.match(/quick\s(brown).+?(jumps)/i); + var result2 = str.match(/[A-Z]/g); + var result3 = str.match("T"); + var result4 = str.match(RegExp("B", 'g')); + "#; + + forward(&mut context, init); + + assert_eq!( + forward(&mut context, "result1[0]"), + "\"Quick Brown Fox Jumps\"" + ); + assert_eq!(forward(&mut context, "result1[1]"), "\"Brown\""); + assert_eq!(forward(&mut context, "result1[2]"), "\"Jumps\""); + assert_eq!(forward(&mut context, "result1.index"), "4"); + assert_eq!( + forward(&mut context, "result1.input"), + "\"The Quick Brown Fox Jumps Over The Lazy Dog\"" + ); + + assert_eq!(forward(&mut context, "result2[0]"), "\"T\""); + assert_eq!(forward(&mut context, "result2[1]"), "\"Q\""); + assert_eq!(forward(&mut context, "result2[2]"), "\"B\""); + assert_eq!(forward(&mut context, "result2[3]"), "\"F\""); + assert_eq!(forward(&mut context, "result2[4]"), "\"J\""); + assert_eq!(forward(&mut context, "result2[5]"), "\"O\""); + assert_eq!(forward(&mut context, "result2[6]"), "\"T\""); + assert_eq!(forward(&mut context, "result2[7]"), "\"L\""); + assert_eq!(forward(&mut context, "result2[8]"), "\"D\""); + + assert_eq!(forward(&mut context, "result3[0]"), "\"T\""); + assert_eq!(forward(&mut context, "result3.index"), "0"); + assert_eq!( + forward(&mut context, "result3.input"), + "\"The Quick Brown Fox Jumps Over The Lazy Dog\"" + ); + assert_eq!(forward(&mut context, "result4[0]"), "\"B\""); +} + +#[test] +fn trim() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, r#"'Hello'.trim()"#), "\"Hello\""); + assert_eq!(forward(&mut context, r#"' \nHello'.trim()"#), "\"Hello\""); + assert_eq!(forward(&mut context, r#"'Hello \n\r'.trim()"#), "\"Hello\""); + assert_eq!(forward(&mut context, r#"' Hello '.trim()"#), "\"Hello\""); +} + +#[test] +fn trim_start() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, r#"'Hello'.trimStart()"#), "\"Hello\""); + assert_eq!( + forward(&mut context, r#"' \nHello'.trimStart()"#), + "\"Hello\"" + ); + assert_eq!( + forward(&mut context, r#"'Hello \n'.trimStart()"#), + "\"Hello \n\"" + ); + assert_eq!( + forward(&mut context, r#"' Hello '.trimStart()"#), + "\"Hello \"" + ); +} + +#[test] +fn trim_end() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, r#"'Hello'.trimEnd()"#), "\"Hello\""); + assert_eq!( + forward(&mut context, r#"' \nHello'.trimEnd()"#), + "\" \nHello\"" + ); + assert_eq!( + forward(&mut context, r#"'Hello \n'.trimEnd()"#), + "\"Hello\"" + ); + assert_eq!( + forward(&mut context, r#"' Hello '.trimEnd()"#), + "\" Hello\"" + ); +} + +#[test] +fn split() { + let mut context = Context::default(); + assert_eq!( + forward(&mut context, "'Hello'.split()"), + forward(&mut context, "['Hello']") + ); + assert_eq!( + forward(&mut context, "'Hello'.split(null)"), + forward(&mut context, "['Hello']") + ); + assert_eq!( + forward(&mut context, "'Hello'.split(undefined)"), + forward(&mut context, "['Hello']") + ); + assert_eq!( + forward(&mut context, "'Hello'.split('')"), + forward(&mut context, "['H','e','l','l','o']") + ); + + assert_eq!( + forward(&mut context, "'x1x2'.split('x')"), + forward(&mut context, "['','1','2']") + ); + assert_eq!( + forward(&mut context, "'x1x2x'.split('x')"), + forward(&mut context, "['','1','2','']") + ); + + assert_eq!( + forward(&mut context, "'x1x2x'.split('x', 0)"), + forward(&mut context, "[]") + ); + assert_eq!( + forward(&mut context, "'x1x2x'.split('x', 2)"), + forward(&mut context, "['','1']") + ); + assert_eq!( + forward(&mut context, "'x1x2x'.split('x', 10)"), + forward(&mut context, "['','1','2','']") + ); + + assert_eq!( + forward(&mut context, "'x1x2x'.split(1)"), + forward(&mut context, "['x','x2x']") + ); + + assert_eq!( + forward(&mut context, "'Hello'.split(null, 0)"), + forward(&mut context, "[]") + ); + assert_eq!( + forward(&mut context, "'Hello'.split(undefined, 0)"), + forward(&mut context, "[]") + ); + + assert_eq!( + forward(&mut context, "''.split()"), + forward(&mut context, "['']") + ); + assert_eq!( + forward(&mut context, "''.split(undefined)"), + forward(&mut context, "['']") + ); + assert_eq!( + forward(&mut context, "''.split('')"), + forward(&mut context, "[]") + ); + assert_eq!( + forward(&mut context, "''.split('1')"), + forward(&mut context, "['']") + ); + + // TODO: Support keeping invalid code point in string + assert_eq!( + forward( + &mut context, + "\'\u{1d7d8}\u{1d7d9}\u{1d7da}\u{1d7db}\'.split(\'\')" + ), + forward(&mut context, "['�','�','�','�','�','�','�','�']") + ); +} + +#[test] +fn split_with_symbol_split_method() { + assert_eq!( + forward( + &mut Context::default(), + r#" + let sep = {}; + sep[Symbol.split] = function(s, limit) { return s + limit.toString(); }; + 'hello'.split(sep, 10) + "# + ), + "\"hello10\"" + ); + + assert_eq!( + forward( + &mut Context::default(), + r#" + let sep = {}; + sep[Symbol.split] = undefined; + 'hello'.split(sep) + "# + ), + "[ \"hello\" ]" + ); + + assert_eq!( + forward( + &mut Context::default(), + r#" + try { + let sep = {}; + sep[Symbol.split] = 10; + 'hello'.split(sep, 10); + } catch(e) { + e.toString() + } + "# + ), + "\"TypeError: value returned for property of object is not a function\"" + ); +} + +#[test] +fn index_of_with_no_arguments() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.indexOf()"), "-1"); + assert_eq!(forward(&mut context, "'undefined'.indexOf()"), "0"); + assert_eq!(forward(&mut context, "'a1undefined'.indexOf()"), "2"); + assert_eq!(forward(&mut context, "'a1undefined1a'.indexOf()"), "2"); + assert_eq!(forward(&mut context, "'µµµundefined'.indexOf()"), "3"); + assert_eq!(forward(&mut context, "'µµµundefinedµµµ'.indexOf()"), "3"); +} + +#[test] +fn index_of_with_string_search_string_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.indexOf('hello')"), "-1"); + assert_eq!( + forward(&mut context, "'undefined'.indexOf('undefined')"), + "0" + ); + assert_eq!( + forward(&mut context, "'a1undefined'.indexOf('undefined')"), + "2" + ); + assert_eq!( + forward(&mut context, "'a1undefined1a'.indexOf('undefined')"), + "2" + ); + assert_eq!( + forward(&mut context, "'µµµundefined'.indexOf('undefined')"), + "3" + ); + assert_eq!( + forward(&mut context, "'µµµundefinedµµµ'.indexOf('undefined')"), + "3" + ); +} + +#[test] +fn index_of_with_non_string_search_string_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.indexOf(1)"), "-1"); + assert_eq!(forward(&mut context, "'1'.indexOf(1)"), "0"); + assert_eq!(forward(&mut context, "'true'.indexOf(true)"), "0"); + assert_eq!(forward(&mut context, "'ab100ba'.indexOf(100)"), "2"); + assert_eq!(forward(&mut context, "'µµµfalse'.indexOf(true)"), "-1"); + assert_eq!(forward(&mut context, "'µµµ5µµµ'.indexOf(5)"), "3"); +} + +#[test] +fn index_of_with_from_index_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.indexOf('x', 2)"), "-1"); + assert_eq!(forward(&mut context, "'x'.indexOf('x', 2)"), "-1"); + assert_eq!(forward(&mut context, "'abcx'.indexOf('x', 2)"), "3"); + assert_eq!(forward(&mut context, "'x'.indexOf('x', 2)"), "-1"); + assert_eq!(forward(&mut context, "'µµµxµµµ'.indexOf('x', 2)"), "3"); + + assert_eq!( + forward(&mut context, "'µµµxµµµ'.indexOf('x', 10000000)"), + "-1" + ); +} + +#[test] +fn generic_index_of() { + let mut context = Context::default(); + forward_val( + &mut context, + "Number.prototype.indexOf = String.prototype.indexOf", + ) + .unwrap(); + + assert_eq!(forward(&mut context, "(10).indexOf(9)"), "-1"); + assert_eq!(forward(&mut context, "(10).indexOf(0)"), "1"); + assert_eq!(forward(&mut context, "(10).indexOf('0')"), "1"); +} + +#[test] +fn index_of_empty_search_string() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "''.indexOf('')"), "0"); + assert_eq!(forward(&mut context, "''.indexOf('', 10)"), "0"); + assert_eq!(forward(&mut context, "'ABC'.indexOf('', 1)"), "1"); + assert_eq!(forward(&mut context, "'ABC'.indexOf('', 2)"), "2"); + assert_eq!(forward(&mut context, "'ABC'.indexOf('', 10)"), "3"); +} + +#[test] +fn last_index_of_with_no_arguments() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.lastIndexOf()"), "-1"); + assert_eq!(forward(&mut context, "'undefined'.lastIndexOf()"), "0"); + assert_eq!(forward(&mut context, "'a1undefined'.lastIndexOf()"), "2"); + assert_eq!( + forward(&mut context, "'a1undefined1aundefined'.lastIndexOf()"), + "13" + ); + assert_eq!( + forward(&mut context, "'µµµundefinedundefined'.lastIndexOf()"), + "12" + ); + assert_eq!( + forward(&mut context, "'µµµundefinedµµµundefined'.lastIndexOf()"), + "15" + ); +} + +#[test] +fn last_index_of_with_string_search_string_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.lastIndexOf('hello')"), "-1"); + assert_eq!( + forward(&mut context, "'undefined'.lastIndexOf('undefined')"), + "0" + ); + assert_eq!( + forward(&mut context, "'a1undefined'.lastIndexOf('undefined')"), + "2" + ); + assert_eq!( + forward( + &mut context, + "'a1undefined1aundefined'.lastIndexOf('undefined')" + ), + "13" + ); + assert_eq!( + forward( + &mut context, + "'µµµundefinedundefined'.lastIndexOf('undefined')" + ), + "12" + ); + assert_eq!( + forward( + &mut context, + "'µµµundefinedµµµundefined'.lastIndexOf('undefined')" + ), + "15" + ); +} + +#[test] +fn last_index_of_with_non_string_search_string_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.lastIndexOf(1)"), "-1"); + assert_eq!(forward(&mut context, "'1'.lastIndexOf(1)"), "0"); + assert_eq!(forward(&mut context, "'11'.lastIndexOf(1)"), "1"); + assert_eq!( + forward(&mut context, "'truefalsetrue'.lastIndexOf(true)"), + "9" + ); + assert_eq!(forward(&mut context, "'ab100ba'.lastIndexOf(100)"), "2"); + assert_eq!(forward(&mut context, "'µµµfalse'.lastIndexOf(true)"), "-1"); + assert_eq!(forward(&mut context, "'µµµ5µµµ65µ'.lastIndexOf(5)"), "8"); +} + +#[test] +fn last_index_of_with_from_index_argument() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.lastIndexOf('x', 2)"), "-1"); + assert_eq!(forward(&mut context, "'x'.lastIndexOf('x', 2)"), "0"); + assert_eq!(forward(&mut context, "'abcxx'.lastIndexOf('x', 2)"), "-1"); + assert_eq!(forward(&mut context, "'x'.lastIndexOf('x', 2)"), "0"); + assert_eq!(forward(&mut context, "'µµµxµµµ'.lastIndexOf('x', 2)"), "-1"); + + assert_eq!( + forward(&mut context, "'µµµxµµµ'.lastIndexOf('x', 10000000)"), + "3" + ); +} + +#[test] +fn last_index_with_empty_search_string() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "''.lastIndexOf('')"), "0"); + assert_eq!(forward(&mut context, "'x'.lastIndexOf('', 2)"), "1"); + assert_eq!(forward(&mut context, "'abcxx'.lastIndexOf('', 4)"), "4"); + assert_eq!(forward(&mut context, "'µµµxµµµ'.lastIndexOf('', 2)"), "2"); + + assert_eq!( + forward(&mut context, "'abc'.lastIndexOf('', 10000000)"), + "3" + ); +} + +#[test] +fn generic_last_index_of() { + let mut context = Context::default(); + forward_val( + &mut context, + "Number.prototype.lastIndexOf = String.prototype.lastIndexOf", + ) + .unwrap(); + + assert_eq!(forward(&mut context, "(1001).lastIndexOf(9)"), "-1"); + assert_eq!(forward(&mut context, "(1001).lastIndexOf(0)"), "2"); + assert_eq!(forward(&mut context, "(1001).lastIndexOf('0')"), "2"); +} + +#[test] +fn last_index_non_integer_position_argument() { + let mut context = Context::default(); + assert_eq!( + forward(&mut context, "''.lastIndexOf('x', new Number(4))"), + "-1" + ); + assert_eq!( + forward(&mut context, "'abc'.lastIndexOf('b', new Number(1))"), + "1" + ); + assert_eq!( + forward(&mut context, "'abcx'.lastIndexOf('x', new String('1'))"), + "-1" + ); + assert_eq!( + forward(&mut context, "'abcx'.lastIndexOf('x', new String('100'))"), + "3" + ); + assert_eq!(forward(&mut context, "'abcx'.lastIndexOf('x', null)"), "-1"); +} + +#[test] +fn char_at() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "'abc'.charAt(-1)"), "\"\""); + assert_eq!(forward(&mut context, "'abc'.charAt(1)"), "\"b\""); + assert_eq!(forward(&mut context, "'abc'.charAt(9)"), "\"\""); + assert_eq!(forward(&mut context, "'abc'.charAt()"), "\"a\""); + assert_eq!(forward(&mut context, "'abc'.charAt(null)"), "\"a\""); + assert_eq!(forward(&mut context, "'\\uDBFF'.charAt(0)"), "\"\u{FFFD}\""); +} + +#[test] +fn char_code_at() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "'abc'.charCodeAt(-1)"), "NaN"); + assert_eq!(forward(&mut context, "'abc'.charCodeAt(1)"), "98"); + assert_eq!(forward(&mut context, "'abc'.charCodeAt(9)"), "NaN"); + assert_eq!(forward(&mut context, "'abc'.charCodeAt()"), "97"); + assert_eq!(forward(&mut context, "'abc'.charCodeAt(null)"), "97"); + assert_eq!(forward(&mut context, "'\\uFFFF'.charCodeAt(0)"), "65535"); +} + +#[test] +fn code_point_at() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "'abc'.codePointAt(-1)"), "undefined"); + assert_eq!(forward(&mut context, "'abc'.codePointAt(1)"), "98"); + assert_eq!(forward(&mut context, "'abc'.codePointAt(9)"), "undefined"); + assert_eq!(forward(&mut context, "'abc'.codePointAt()"), "97"); + assert_eq!(forward(&mut context, "'abc'.codePointAt(null)"), "97"); + assert_eq!( + forward(&mut context, "'\\uD800\\uDC00'.codePointAt(0)"), + "65536" + ); + assert_eq!( + forward(&mut context, "'\\uD800\\uDFFF'.codePointAt(0)"), + "66559" + ); + assert_eq!( + forward(&mut context, "'\\uDBFF\\uDC00'.codePointAt(0)"), + "1113088" + ); + assert_eq!( + forward(&mut context, "'\\uDBFF\\uDFFF'.codePointAt(0)"), + "1114111" + ); + assert_eq!( + forward(&mut context, "'\\uD800\\uDC00'.codePointAt(1)"), + "56320" + ); + assert_eq!( + forward(&mut context, "'\\uD800\\uDFFF'.codePointAt(1)"), + "57343" + ); + assert_eq!( + forward(&mut context, "'\\uDBFF\\uDC00'.codePointAt(1)"), + "56320" + ); + assert_eq!( + forward(&mut context, "'\\uDBFF\\uDFFF'.codePointAt(1)"), + "57343" + ); +} + +#[test] +fn slice() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "'abc'.slice()"), "\"abc\""); + assert_eq!(forward(&mut context, "'abc'.slice(1)"), "\"bc\""); + assert_eq!(forward(&mut context, "'abc'.slice(-1)"), "\"c\""); + assert_eq!(forward(&mut context, "'abc'.slice(0, 9)"), "\"abc\""); + assert_eq!(forward(&mut context, "'abc'.slice(9, 10)"), "\"\""); +} + +#[test] +fn empty_iter() { + let mut context = Context::default(); + let init = r#" + let iter = new String()[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn ascii_iter() { + let mut context = Context::default(); + let init = r#" + let iter = new String("Hello World")[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "\"H\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"e\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"l\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"l\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"o\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\" \""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"W\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"o\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"r\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"l\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"d\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn unicode_iter() { + let mut context = Context::default(); + let init = r#" + let iter = new String("C🙂🙂l W🙂rld")[Symbol.iterator](); + let next = iter.next(); + "#; + forward(&mut context, init); + assert_eq!(forward(&mut context, "next.value"), "\"C\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"l\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\" \""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"W\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"🙂\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"r\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"l\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "\"d\""); + assert_eq!(forward(&mut context, "next.done"), "false"); + forward(&mut context, "next = iter.next()"); + assert_eq!(forward(&mut context, "next.value"), "undefined"); + assert_eq!(forward(&mut context, "next.done"), "true"); +} + +#[test] +fn string_get_property() { + let mut context = Context::default(); + assert_eq!(forward(&mut context, "'abc'[-1]"), "undefined"); + assert_eq!(forward(&mut context, "'abc'[1]"), "\"b\""); + assert_eq!(forward(&mut context, "'abc'[2]"), "\"c\""); + assert_eq!(forward(&mut context, "'abc'[3]"), "undefined"); + assert_eq!(forward(&mut context, "'abc'['foo']"), "undefined"); + assert_eq!(forward(&mut context, "'😀'[0]"), "\"�\""); +} + +#[test] +fn search() { + let mut context = Context::default(); + + assert_eq!(forward(&mut context, "'aa'.search(/b/)"), "-1"); + assert_eq!(forward(&mut context, "'aa'.search(/a/)"), "0"); + assert_eq!(forward(&mut context, "'aa'.search(/a/g)"), "0"); + assert_eq!(forward(&mut context, "'ba'.search(/a/)"), "1"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/mod.rs new file mode 100644 index 0000000..3dd5807 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/mod.rs @@ -0,0 +1,346 @@ +//! This module implements the global `Symbol` object. +//! +//! The data type symbol is a primitive data type. +//! The `Symbol()` function returns a value of type symbol, has static properties that expose +//! several members of built-in objects, has static methods that expose the global symbol registry, +//! and resembles a built-in object class, but is incomplete as a constructor because it does not +//! support the syntax "`new Symbol()`". +//! +//! Every symbol value returned from `Symbol()` is unique. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-symbol-value +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol + +#[cfg(test)] +mod tests; + +use super::JsArgs; +use crate::{ + builtins::BuiltIn, + object::{ConstructorBuilder, FunctionBuilder}, + property::Attribute, + symbol::{JsSymbol, WellKnownSymbols}, + value::JsValue, + Context, JsResult, JsString, +}; +use boa_profiler::Profiler; +use rustc_hash::FxHashMap; +use std::cell::RefCell; +use tap::{Conv, Pipe}; + +thread_local! { + static GLOBAL_SYMBOL_REGISTRY: RefCell = RefCell::new(GlobalSymbolRegistry::new()); +} + +struct GlobalSymbolRegistry { + keys: FxHashMap, + symbols: FxHashMap, +} + +impl GlobalSymbolRegistry { + fn new() -> Self { + Self { + keys: FxHashMap::default(), + symbols: FxHashMap::default(), + } + } + + fn get_or_insert_key(&mut self, key: JsString) -> JsSymbol { + if let Some(symbol) = self.keys.get(&key) { + return symbol.clone(); + } + + let symbol = JsSymbol::new(Some(key.clone())); + self.keys.insert(key.clone(), symbol.clone()); + self.symbols.insert(symbol.clone(), key); + symbol + } + + fn get_symbol(&self, sym: &JsSymbol) -> Option { + if let Some(key) = self.symbols.get(sym) { + return Some(key.clone()); + } + + None + } +} + +#[derive(Debug, Clone, Copy)] +pub struct Symbol; + +impl BuiltIn for Symbol { + const NAME: &'static str = "Symbol"; + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let symbol_async_iterator = WellKnownSymbols::async_iterator(); + let symbol_has_instance = WellKnownSymbols::has_instance(); + let symbol_is_concat_spreadable = WellKnownSymbols::is_concat_spreadable(); + let symbol_iterator = WellKnownSymbols::iterator(); + let symbol_match = WellKnownSymbols::r#match(); + let symbol_match_all = WellKnownSymbols::match_all(); + let symbol_replace = WellKnownSymbols::replace(); + let symbol_search = WellKnownSymbols::search(); + let symbol_species = WellKnownSymbols::species(); + let symbol_split = WellKnownSymbols::split(); + let symbol_to_primitive = WellKnownSymbols::to_primitive(); + let symbol_to_string_tag = WellKnownSymbols::to_string_tag(); + let symbol_unscopables = WellKnownSymbols::unscopables(); + + let attribute = Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT; + + let to_primitive = FunctionBuilder::native(context, Self::to_primitive) + .name("[Symbol.toPrimitive]") + .length(1) + .constructor(false) + .build(); + + let get_description = FunctionBuilder::native(context, Self::get_description) + .name("get description") + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().symbol().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .callable(true) + .constructor(true) + .static_method(Self::for_, "for", 1) + .static_method(Self::key_for, "keyFor", 1) + .static_property("asyncIterator", symbol_async_iterator, attribute) + .static_property("hasInstance", symbol_has_instance, attribute) + .static_property("isConcatSpreadable", symbol_is_concat_spreadable, attribute) + .static_property("iterator", symbol_iterator, attribute) + .static_property("match", symbol_match, attribute) + .static_property("matchAll", symbol_match_all, attribute) + .static_property("replace", symbol_replace, attribute) + .static_property("search", symbol_search, attribute) + .static_property("species", symbol_species, attribute) + .static_property("split", symbol_split, attribute) + .static_property("toPrimitive", symbol_to_primitive.clone(), attribute) + .static_property("toStringTag", symbol_to_string_tag.clone(), attribute) + .static_property("unscopables", symbol_unscopables, attribute) + .method(Self::to_string, "toString", 0) + .method(Self::value_of, "valueOf", 0) + .accessor( + "description", + Some(get_description), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .property( + symbol_to_string_tag, + Self::NAME, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .property( + symbol_to_primitive, + to_primitive, + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .build() + .conv::() + .pipe(Some) + } +} + +impl Symbol { + /// The amount of arguments this function object takes. + pub(crate) const LENGTH: usize = 0; + + /// The `Symbol()` constructor returns a value of type symbol. + /// + /// It is incomplete as a constructor because it does not support + /// the syntax `new Symbol()` and it is not intended to be subclassed. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#sec-symbol-description + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/Symbol + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is not undefined, throw a TypeError exception. + if !new_target.is_undefined() { + return context.throw_type_error("Symbol is not a constructor"); + } + + // 2. If description is undefined, let descString be undefined. + // 3. Else, let descString be ? ToString(description). + let description = match args.get(0) { + Some(value) if !value.is_undefined() => Some(value.to_string(context)?), + _ => None, + }; + + // 4. Return a new unique Symbol value whose [[Description]] value is descString. + Ok(JsSymbol::new(description).into()) + } + + fn this_symbol_value(value: &JsValue, context: &mut Context) -> JsResult { + value + .as_symbol() + .or_else(|| value.as_object().and_then(|obj| obj.borrow().as_symbol())) + .ok_or_else(|| context.construct_type_error("'this' is not a Symbol")) + } + + /// `Symbol.prototype.toString()` + /// + /// This method returns a string representing the specified `Symbol` object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.tostring + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/toString + #[allow(clippy::wrong_self_convention)] + pub(crate) fn to_string( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let sym be ? thisSymbolValue(this value). + let symbol = Self::this_symbol_value(this, context)?; + + // 2. Return SymbolDescriptiveString(sym). + Ok(symbol.descriptive_string().into()) + } + + /// `Symbol.prototype.valueOf()` + /// + /// This method returns a `Symbol` that is the primitive value of the specified `Symbol` object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/valueOf + /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.valueof + pub(crate) fn value_of( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Return ? thisSymbolValue(this value). + let symbol = Self::this_symbol_value(this, context)?; + Ok(JsValue::Symbol(symbol)) + } + + /// `get Symbol.prototype.description` + /// + /// This accessor returns the description of the `Symbol` object. + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.description + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/description + pub(crate) fn get_description( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let symbol = Self::this_symbol_value(this, context)?; + if let Some(ref description) = symbol.description() { + Ok(description.clone().into()) + } else { + Ok(JsValue::undefined()) + } + } + + /// `Symbol.for( key )` + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.for + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/for + pub(crate) fn for_(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let stringKey be ? ToString(key). + let string_key = args + .get(0) + .cloned() + .unwrap_or_default() + .to_string(context)?; + // 2. For each element e of the GlobalSymbolRegistry List, do + // a. If SameValue(e.[[Key]], stringKey) is true, return e.[[Symbol]]. + // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for stringKey. + // 4. Let newSymbol be a new unique Symbol value whose [[Description]] value is stringKey. + // 5. Append the Record { [[Key]]: stringKey, [[Symbol]]: newSymbol } to the GlobalSymbolRegistry List. + // 6. Return newSymbol. + Ok(GLOBAL_SYMBOL_REGISTRY + .with(move |registry| { + let mut registry = registry.borrow_mut(); + registry.get_or_insert_key(string_key) + }) + .into()) + } + + /// `Symbol.keyFor( sym )` + /// + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-symbol.prototype.keyfor + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/keyFor + pub(crate) fn key_for( + _: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let sym = args.get_or_undefined(0); + // 1. If Type(sym) is not Symbol, throw a TypeError exception. + if let Some(sym) = sym.as_symbol() { + // 2. For each element e of the GlobalSymbolRegistry List (see 20.4.2.2), do + // a. If SameValue(e.[[Symbol]], sym) is true, return e.[[Key]]. + // 3. Assert: GlobalSymbolRegistry does not currently contain an entry for sym. + // 4. Return undefined. + let symbol = GLOBAL_SYMBOL_REGISTRY.with(move |registry| { + let registry = registry.borrow(); + registry.get_symbol(&sym) + }); + + Ok(symbol.map(JsValue::from).unwrap_or_default()) + } else { + context.throw_type_error("Symbol.keyFor: sym is not a symbol") + } + } + + /// `Symbol.prototype [ @@toPrimitive ]` + /// + /// This function is called by ECMAScript language operators to convert a Symbol object to a primitive value. + /// NOTE: The argument is ignored + /// + /// More information: + /// - [MDN documentation][mdn] + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/multipage/#sec-symbol.prototype-@@toprimitive + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol/@@toPrimitive + pub(crate) fn to_primitive( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + let sym = Self::this_symbol_value(this, context)?; + // 1. Return ? thisSymbolValue(this value). + Ok(sym.into()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/tests.rs new file mode 100644 index 0000000..6c68304 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/symbol/tests.rs @@ -0,0 +1,40 @@ +use crate::{check_output, forward, forward_val, Context, TestAction}; + +#[test] +fn call_symbol_and_check_return_type() { + let mut context = Context::default(); + let init = r#" + var sym = Symbol(); + "#; + eprintln!("{}", forward(&mut context, init)); + let sym = forward_val(&mut context, "sym").unwrap(); + assert!(sym.is_symbol()); +} + +#[test] +fn print_symbol_expect_description() { + let mut context = Context::default(); + let init = r#" + var sym = Symbol("Hello"); + "#; + eprintln!("{}", forward(&mut context, init)); + let sym = forward_val(&mut context, "sym.toString()").unwrap(); + assert_eq!(sym.display().to_string(), "\"Symbol(Hello)\""); +} + +#[test] +fn symbol_access() { + let init = r#" + var x = {}; + var sym1 = Symbol("Hello"); + var sym2 = Symbol("Hello"); + x[sym1] = 10; + x[sym2] = 20; + "#; + check_output(&[ + TestAction::Execute(init), + TestAction::TestEq("x[sym1]", "10"), + TestAction::TestEq("x[sym2]", "20"), + TestAction::TestEq("x['Symbol(Hello)']", "undefined"), + ]); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/integer_indexed_object.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/integer_indexed_object.rs new file mode 100644 index 0000000..8d1e100 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/integer_indexed_object.rs @@ -0,0 +1,151 @@ +//! This module implements the `Integer-Indexed` exotic object. +//! +//! An `Integer-Indexed` exotic object is an exotic object that performs +//! special handling of integer index property keys. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-integer-indexed-exotic-objects + +use crate::{ + builtins::typed_array::TypedArrayKind, + object::{JsObject, ObjectData}, + Context, +}; +use boa_gc::{unsafe_empty_trace, Finalize, Trace}; + +/// Type of the array content. +#[derive(Debug, Clone, Copy, Finalize, PartialEq)] +pub(crate) enum ContentType { + Number, + BigInt, +} + +unsafe impl Trace for ContentType { + // safe because `ContentType` is `Copy` + unsafe_empty_trace!(); +} + +/// +#[derive(Debug, Clone, Trace, Finalize)] +pub struct IntegerIndexed { + viewed_array_buffer: Option, + typed_array_name: TypedArrayKind, + byte_offset: usize, + byte_length: usize, + array_length: usize, +} + +impl IntegerIndexed { + pub(crate) fn new( + viewed_array_buffer: Option, + typed_array_name: TypedArrayKind, + byte_offset: usize, + byte_length: usize, + array_length: usize, + ) -> Self { + Self { + viewed_array_buffer, + typed_array_name, + byte_offset, + byte_length, + array_length, + } + } + + /// `IntegerIndexedObjectCreate ( prototype )` + /// + /// Create a new `JsObject` from a prototype and a `IntegerIndexedObject` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-integerindexedobjectcreate + pub(super) fn create(prototype: JsObject, data: Self, context: &Context) -> JsObject { + // 1. Let internalSlotsList be « [[Prototype]], [[Extensible]], [[ViewedArrayBuffer]], + // [[TypedArrayName]], [[ContentType]], [[ByteLength]], [[ByteOffset]], + // [[ArrayLength]] ». + // 2. Let A be ! MakeBasicObject(internalSlotsList). + let a = context.construct_object(); + + // 3. Set A.[[GetOwnProperty]] as specified in 10.4.5.1. + // 4. Set A.[[HasProperty]] as specified in 10.4.5.2. + // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.5.3. + // 6. Set A.[[Get]] as specified in 10.4.5.4. + // 7. Set A.[[Set]] as specified in 10.4.5.5. + // 8. Set A.[[Delete]] as specified in 10.4.5.6. + // 9. Set A.[[OwnPropertyKeys]] as specified in 10.4.5.7. + a.borrow_mut().data = ObjectData::integer_indexed(data); + + // 10. Set A.[[Prototype]] to prototype. + a.set_prototype(prototype.into()); + + // 11. Return A. + a + } + + /// Abstract operation `IsDetachedBuffer ( arrayBuffer )`. + /// + /// Check if `[[ArrayBufferData]]` is null. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-isdetachedbuffer + pub(crate) fn is_detached(&self) -> bool { + if let Some(obj) = &self.viewed_array_buffer { + obj.borrow() + .as_array_buffer() + .expect("Typed array must have internal array buffer object") + .is_detached_buffer() + } else { + false + } + } + + /// Get the integer indexed object's byte offset. + pub(crate) fn byte_offset(&self) -> usize { + self.byte_offset + } + + /// Set the integer indexed object's byte offset. + pub(crate) fn set_byte_offset(&mut self, byte_offset: usize) { + self.byte_offset = byte_offset; + } + + /// Get the integer indexed object's typed array name. + pub(crate) fn typed_array_name(&self) -> TypedArrayKind { + self.typed_array_name + } + + /// Get a reference to the integer indexed object's viewed array buffer. + pub fn viewed_array_buffer(&self) -> Option<&JsObject> { + self.viewed_array_buffer.as_ref() + } + + ///(crate) Set the integer indexed object's viewed array buffer. + pub fn set_viewed_array_buffer(&mut self, viewed_array_buffer: Option) { + self.viewed_array_buffer = viewed_array_buffer; + } + + /// Get the integer indexed object's byte length. + pub fn byte_length(&self) -> usize { + self.byte_length + } + + /// Set the integer indexed object's byte length. + pub(crate) fn set_byte_length(&mut self, byte_length: usize) { + self.byte_length = byte_length; + } + + /// Get the integer indexed object's array length. + pub fn array_length(&self) -> usize { + self.array_length + } + + /// Set the integer indexed object's array length. + pub(crate) fn set_array_length(&mut self, array_length: usize) { + self.array_length = array_length; + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/mod.rs new file mode 100644 index 0000000..61625f8 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/typed_array/mod.rs @@ -0,0 +1,3482 @@ +//! This module implements the global `TypedArray` objects. +//! +//! A `TypedArray` object describes an array-like view of an underlying binary data buffer. +//! There is no global property named `TypedArray`, nor is there a directly visible `TypedArray` constructor. +//! Instead, there are a number of different global properties, +//! whose values are typed array constructors for specific element types. +//! +//! More information: +//! - [ECMAScript reference][spec] +//! - [MDN documentation][mdn] +//! +//! [spec]: https://tc39.es/ecma262/#sec-typedarray-objects +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/TypedArray + +use crate::{ + builtins::{ + array_buffer::{ArrayBuffer, SharedMemoryOrder}, + iterable::iterable_to_list, + typed_array::integer_indexed_object::{ContentType, IntegerIndexed}, + Array, ArrayIterator, BuiltIn, JsArgs, + }, + context::intrinsics::{StandardConstructor, StandardConstructors}, + object::{ + internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder, + JsObject, ObjectData, + }, + property::{Attribute, PropertyNameKind}, + symbol::WellKnownSymbols, + value::{IntegerOrInfinity, JsValue}, + Context, JsResult, JsString, +}; +use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_profiler::Profiler; +use num_traits::{Signed, Zero}; +use std::cmp::Ordering; + +use tap::{Conv, Pipe}; + +pub mod integer_indexed_object; + +macro_rules! typed_array { + ($ty:ident, $variant:ident, $name:literal, $global_object_name:ident) => { + #[doc = concat!("JavaScript `", $name, "` built-in implementation.")] + #[derive(Debug, Clone, Copy)] + pub struct $ty; + + impl BuiltIn for $ty { + const NAME: &'static str = $name; + + const ATTRIBUTE: Attribute = Attribute::WRITABLE + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::CONFIGURABLE); + + fn init(context: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + let typed_array_constructor = context + .intrinsics() + .constructors() + .typed_array() + .constructor(); + let typed_array_constructor_proto = context + .intrinsics() + .constructors() + .typed_array() + .prototype(); + + let get_species = FunctionBuilder::native(context, TypedArray::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context + .intrinsics() + .constructors() + .$global_object_name() + .clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .property( + "BYTES_PER_ELEMENT", + TypedArrayKind::$variant.element_size(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ) + .static_property( + "BYTES_PER_ELEMENT", + TypedArrayKind::$variant.element_size(), + Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ) + .custom_prototype(typed_array_constructor) + .inherit(typed_array_constructor_proto) + .build() + .conv::() + .pipe(Some) + } + } + + impl $ty { + const LENGTH: usize = 3; + + /// `23.2.5.1 TypedArray ( ...args )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-typedarray + pub(crate) fn constructor( + new_target: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If NewTarget is undefined, throw a TypeError exception. + if new_target.is_undefined() { + return context.throw_type_error(concat!( + "new target was undefined when constructing an ", + $name + )); + } + + // 2. Let constructorName be the String value of the Constructor Name value specified in Table 72 for this TypedArray constructor. + let constructor_name = TypedArrayKind::$variant; + + // 3. Let proto be "%TypedArray.prototype%". + let proto = StandardConstructors::$global_object_name; + + // 4. Let numberOfArgs be the number of elements in args. + let number_of_args = args.len(); + + // 5. If numberOfArgs = 0, then + if number_of_args == 0 { + // a. Return ? AllocateTypedArray(constructorName, NewTarget, proto, 0). + return Ok(TypedArray::allocate( + constructor_name, + new_target, + proto, + Some(0), + context, + )? + .into()); + } + // 6. Else, + + // a. Let firstArgument be args[0]. + let first_argument = &args[0]; + + // b. If Type(firstArgument) is Object, then + if let Some(first_argument) = first_argument.as_object() { + // i. Let O be ? AllocateTypedArray(constructorName, NewTarget, proto). + let o = + TypedArray::allocate(constructor_name, new_target, proto, None, context)?; + + // ii. If firstArgument has a [[TypedArrayName]] internal slot, then + if first_argument.is_typed_array() { + // 1. Perform ? InitializeTypedArrayFromTypedArray(O, firstArgument). + TypedArray::initialize_from_typed_array(&o, first_argument, context)?; + } else if first_argument.is_array_buffer() { + // iii. Else if firstArgument has an [[ArrayBufferData]] internal slot, then + + // 1. If numberOfArgs > 1, let byteOffset be args[1]; else let byteOffset be undefined. + let byte_offset = args.get_or_undefined(1); + + // 2. If numberOfArgs > 2, let length be args[2]; else let length be undefined. + let length = args.get_or_undefined(2); + + // 3. Perform ? InitializeTypedArrayFromArrayBuffer(O, firstArgument, byteOffset, length). + TypedArray::initialize_from_array_buffer( + &o, + first_argument.clone(), + byte_offset, + length, + context, + )?; + } else { + // iv. Else, + + // 1. Assert: Type(firstArgument) is Object and firstArgument does not have + // either a [[TypedArrayName]] or an [[ArrayBufferData]] internal slot. + + // 2. Let usingIterator be ? GetMethod(firstArgument, @@iterator). + + let first_argument_v = JsValue::from(first_argument.clone()); + let using_iterator = + first_argument_v.get_method(WellKnownSymbols::replace(), context)?; + + // 3. If usingIterator is not undefined, then + if let Some(using_iterator) = using_iterator { + // a. Let values be ? IterableToList(firstArgument, usingIterator). + let values = iterable_to_list( + context, + &first_argument_v, + Some(using_iterator.into()), + )?; + + // b. Perform ? InitializeTypedArrayFromList(O, values). + TypedArray::initialize_from_list(&o, values, context)?; + } else { + // 4. Else, + + // a. NOTE: firstArgument is not an Iterable so assume it is already an array-like object. + // b. Perform ? InitializeTypedArrayFromArrayLike(O, firstArgument). + TypedArray::initialize_from_array_like(&o, &first_argument, context)?; + } + } + + // v. Return O. + Ok(o.into()) + } else { + // c. Else, + + // i. Assert: firstArgument is not an Object. + assert!(!first_argument.is_object(), "firstArgument was an object"); + + // ii. Let elementLength be ? ToIndex(firstArgument). + let element_length = first_argument.to_index(context)?; + + // iii. Return ? AllocateTypedArray(constructorName, NewTarget, proto, elementLength). + Ok(TypedArray::allocate( + constructor_name, + new_target, + proto, + Some(element_length), + context, + )? + .into()) + } + } + } + }; +} + +/// The JavaScript `%TypedArray%` object. +/// +/// +#[derive(Debug, Clone, Copy)] +pub(crate) struct TypedArray; + +impl BuiltIn for TypedArray { + const NAME: &'static str = "TypedArray"; + fn init(context: &mut Context) -> Option { + let get_species = FunctionBuilder::native(context, Self::get_species) + .name("get [Symbol.species]") + .constructor(false) + .build(); + + let get_buffer = FunctionBuilder::native(context, Self::buffer) + .name("get buffer") + .constructor(false) + .build(); + + let get_byte_length = FunctionBuilder::native(context, Self::byte_length) + .name("get byteLength") + .constructor(false) + .build(); + + let get_byte_offset = FunctionBuilder::native(context, Self::byte_offset) + .name("get byteOffset") + .constructor(false) + .build(); + + let get_length = FunctionBuilder::native(context, Self::length) + .name("get length") + .constructor(false) + .build(); + + let get_to_string_tag = FunctionBuilder::native(context, Self::to_string_tag) + .name("get [Symbol.toStringTag]") + .constructor(false) + .build(); + + let values_function = FunctionBuilder::native(context, Self::values) + .name("values") + .length(0) + .constructor(false) + .build(); + + ConstructorBuilder::with_standard_constructor( + context, + Self::constructor, + context.intrinsics().constructors().typed_array().clone(), + ) + .name(Self::NAME) + .length(Self::LENGTH) + .static_accessor( + WellKnownSymbols::species(), + Some(get_species), + None, + Attribute::CONFIGURABLE, + ) + .property( + "length", + 0, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT, + ) + .property( + WellKnownSymbols::iterator(), + values_function, + Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, + ) + .accessor( + "buffer", + Some(get_buffer), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .accessor( + "byteLength", + Some(get_byte_length), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .accessor( + "byteOffset", + Some(get_byte_offset), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .accessor( + "length", + Some(get_length), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .accessor( + WellKnownSymbols::to_string_tag(), + Some(get_to_string_tag), + None, + Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE, + ) + .static_method(Self::from, "from", 1) + .static_method(Self::of, "of", 0) + .method(Self::at, "at", 1) + .method(Self::copy_within, "copyWithin", 2) + .method(Self::entries, "entries", 0) + .method(Self::every, "every", 1) + .method(Self::fill, "fill", 1) + .method(Self::filter, "filter", 1) + .method(Self::find, "find", 1) + .method(Self::findindex, "findIndex", 1) + .method(Self::foreach, "forEach", 1) + .method(Self::includes, "includes", 1) + .method(Self::index_of, "indexOf", 1) + .method(Self::join, "join", 1) + .method(Self::keys, "keys", 0) + .method(Self::last_index_of, "lastIndexOf", 1) + .method(Self::map, "map", 1) + .method(Self::reduce, "reduce", 1) + .method(Self::reduceright, "reduceRight", 1) + .method(Self::reverse, "reverse", 0) + .method(Self::set, "set", 1) + .method(Self::slice, "slice", 2) + .method(Self::some, "some", 1) + .method(Self::sort, "sort", 1) + .method(Self::subarray, "subarray", 2) + .method(Self::values, "values", 0) + // 23.2.3.29 %TypedArray%.prototype.toString ( ) + // The initial value of the %TypedArray%.prototype.toString data property is the same + // built-in function object as the Array.prototype.toString method defined in 23.1.3.30. + .method(Array::to_string, "toString", 0) + .build(); + + None + } +} +impl TypedArray { + const LENGTH: usize = 0; + + /// `23.2.1.1 %TypedArray% ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray% + fn constructor( + _new_target: &JsValue, + _args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Throw a TypeError exception. + context.throw_type_error("the TypedArray constructor should never be called directly") + } + + /// `23.2.2.1 %TypedArray%.from ( source [ , mapfn [ , thisArg ] ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.from + fn from(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let C be the this value. + // 2. If IsConstructor(C) is false, throw a TypeError exception. + let constructor = match this.as_object() { + Some(obj) if obj.is_constructor() => obj, + _ => { + return context + .throw_type_error("TypedArray.from called on non-constructable value") + } + }; + + let mapping = match args.get(1) { + // 3. If mapfn is undefined, let mapping be false. + None | Some(JsValue::Undefined) => None, + // 4. Else, + Some(v) => match v.as_object() { + // b. Let mapping be true. + Some(obj) if obj.is_callable() => Some(obj), + // a. If IsCallable(mapfn) is false, throw a TypeError exception. + _ => { + return context + .throw_type_error("TypedArray.from called with non-callable mapfn") + } + }, + }; + + // 5. Let usingIterator be ? GetMethod(source, @@iterator). + let source = args.get_or_undefined(0); + let using_iterator = source.get_method(WellKnownSymbols::iterator(), context)?; + + let this_arg = args.get_or_undefined(2); + + // 6. If usingIterator is not undefined, then + if let Some(using_iterator) = using_iterator { + // a. Let values be ? IterableToList(source, usingIterator). + let values = iterable_to_list(context, source, Some(using_iterator.into()))?; + + // b. Let len be the number of elements in values. + // c. Let targetObj be ? TypedArrayCreate(C, « 𝔽(len) »). + let target_obj = Self::create(constructor, &[values.len().into()], context)?; + + // d. Let k be 0. + // e. Repeat, while k < len, + for (k, k_value) in values.iter().enumerate() { + // i. Let Pk be ! ToString(𝔽(k)). + // ii. Let kValue be the first element of values and remove that element from values. + // iii. If mapping is true, then + let mapped_value = if let Some(map_fn) = &mapping { + // 1. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + map_fn.call(this_arg, &[k_value.clone(), k.into()], context)? + } + // iv. Else, let mappedValue be kValue. + else { + k_value.clone() + }; + + // v. Perform ? Set(targetObj, Pk, mappedValue, true). + target_obj.set(k, mapped_value, true, context)?; + } + + // f. Assert: values is now an empty List. + // g. Return targetObj. + return Ok(target_obj.into()); + } + + // 7. NOTE: source is not an Iterable so assume it is already an array-like object. + // 8. Let arrayLike be ! ToObject(source). + let array_like = source + .to_object(context) + .expect("ToObject cannot fail here"); + + // 9. Let len be ? LengthOfArrayLike(arrayLike). + let len = array_like.length_of_array_like(context)?; + + // 10. Let targetObj be ? TypedArrayCreate(C, « 𝔽(len) »). + let target_obj = Self::create(constructor, &[len.into()], context)?; + + // 11. Let k be 0. + // 12. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(arrayLike, Pk). + let k_value = array_like.get(k, context)?; + + // c. If mapping is true, then + let mapped_value = if let Some(map_fn) = &mapping { + // i. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + map_fn.call(this_arg, &[k_value, k.into()], context)? + } + // d. Else, let mappedValue be kValue. + else { + k_value + }; + + // e. Perform ? Set(targetObj, Pk, mappedValue, true). + target_obj.set(k, mapped_value, true, context)?; + } + + // 13. Return targetObj. + Ok(target_obj.into()) + } + + /// `23.2.2.2 %TypedArray%.of ( ...items )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.of + fn of(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let len be the number of elements in items. + + // 2. Let C be the this value. + // 3. If IsConstructor(C) is false, throw a TypeError exception. + let constructor = match this.as_object() { + Some(obj) if obj.is_constructor() => obj, + _ => { + return context.throw_type_error("TypedArray.of called on non-constructable value") + } + }; + + // 4. Let newObj be ? TypedArrayCreate(C, « 𝔽(len) »). + let new_obj = Self::create(constructor, &[args.len().into()], context)?; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for (k, k_value) in args.iter().enumerate() { + // a. Let kValue be items[k]. + // b. Let Pk be ! ToString(𝔽(k)). + // c. Perform ? Set(newObj, Pk, kValue, true). + new_obj.set(k, k_value, true, context)?; + } + + // 7. Return newObj. + Ok(new_obj.into()) + } + + /// `23.2.2.4 get %TypedArray% [ @@species ]` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%-@@species + #[allow(clippy::unnecessary_wraps)] + fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Return the this value. + Ok(this.clone()) + } + + /// `23.2.3.1 %TypedArray%.prototype.at ( index )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.at + pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. Let relativeIndex be ? ToIntegerOrInfinity(index). + let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + let k = match relative_index { + // Note: Early undefined return on infinity. + IntegerOrInfinity::PositiveInfinity | IntegerOrInfinity::NegativeInfinity => { + return Ok(JsValue::undefined()) + } + // 5. If relativeIndex ≥ 0, then + // a. Let k be relativeIndex. + IntegerOrInfinity::Integer(i) if i >= 0 => i, + // 6. Else, + // a. Let k be len + relativeIndex. + IntegerOrInfinity::Integer(i) => len + i, + }; + + // 7. If k < 0 or k ≥ len, return undefined. + if k < 0 || k >= len { + return Ok(JsValue::undefined()); + } + + // 8. Return ! Get(O, ! ToString(𝔽(k))). + Ok(obj.get(k, context).expect("Get cannot fail here")) + } + + /// `23.2.3.2 get %TypedArray%.prototype.buffer` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%.prototype.buffer + fn buffer(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[TypedArrayName]]). + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let typed_array = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + // 5. Return buffer. + Ok(typed_array + .viewed_array_buffer() + .map_or_else(JsValue::undefined, |buffer| buffer.clone().into())) + } + + /// `23.2.3.3 get %TypedArray%.prototype.byteLength` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%.prototype.bytelength + pub(crate) fn byte_length( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[TypedArrayName]]). + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let typed_array = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + // 5. If IsDetachedBuffer(buffer) is true, return +0𝔽. + // 6. Let size be O.[[ByteLength]]. + // 7. Return 𝔽(size). + if typed_array.is_detached() { + Ok(0.into()) + } else { + Ok(typed_array.byte_length().into()) + } + } + + /// `23.2.3.4 get %TypedArray%.prototype.byteOffset` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%.prototype.byteoffset + pub(crate) fn byte_offset( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[TypedArrayName]]). + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let typed_array = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + // 5. If IsDetachedBuffer(buffer) is true, return +0𝔽. + // 6. Let offset be O.[[ByteOffset]]. + // 7. Return 𝔽(offset). + if typed_array.is_detached() { + Ok(0.into()) + } else { + Ok(typed_array.byte_offset().into()) + } + } + + /// `23.2.3.6 %TypedArray%.prototype.copyWithin ( target, start [ , end ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.copywithin + fn copy_within(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + let len = { + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 2. Perform ? ValidateTypedArray(O). + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + o.array_length() as i64 + }; + + // 4. Let relativeTarget be ? ToIntegerOrInfinity(target). + let relative_target = args.get_or_undefined(0).to_integer_or_infinity(context)?; + + let to = match relative_target { + // 5. If relativeTarget is -∞, let to be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 6. Else if relativeTarget < 0, let to be max(len + relativeTarget, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 7. Else, let to be min(relativeTarget, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 8. Let relativeStart be ? ToIntegerOrInfinity(start). + let relative_start = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + let from = match relative_start { + // 9. If relativeStart is -∞, let from be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 10. Else if relativeStart < 0, let from be max(len + relativeStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 11. Else, let from be min(relativeStart, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 12. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + let end = args.get_or_undefined(2); + let relative_end = if end.is_undefined() { + IntegerOrInfinity::Integer(len) + } else { + end.to_integer_or_infinity(context)? + }; + + let r#final = match relative_end { + // 13. If relativeEnd is -∞, let final be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 14. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 15. Else, let final be min(relativeEnd, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 16. Let count be min(final - from, len - to). + let count = std::cmp::min(r#final - from, len - to); + + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 17. If count > 0, then + if count > 0 { + // a. NOTE: The copying must be performed in a manner that preserves the bit-level encoding of the source data. + // b. Let buffer be O.[[ViewedArrayBuffer]]. + // c. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // d. Let typedArrayName be the String value of O.[[TypedArrayName]]. + let typed_array_name = o.typed_array_name(); + + // e. Let elementSize be the Element Size value specified in Table 73 for typedArrayName. + let element_size = typed_array_name.element_size() as i64; + + // f. Let byteOffset be O.[[ByteOffset]]. + let byte_offset = o.byte_offset() as i64; + + // g. Let toByteIndex be to × elementSize + byteOffset. + let mut to_byte_index = to * element_size + byte_offset; + + // h. Let fromByteIndex be from × elementSize + byteOffset. + let mut from_byte_index = from * element_size + byte_offset; + + // i. Let countBytes be count × elementSize. + let mut count_bytes = count * element_size; + + // j. If fromByteIndex < toByteIndex and toByteIndex < fromByteIndex + countBytes, then + let direction = if from_byte_index < to_byte_index + && to_byte_index < from_byte_index + count_bytes + { + // ii. Set fromByteIndex to fromByteIndex + countBytes - 1. + from_byte_index = from_byte_index + count_bytes - 1; + + // iii. Set toByteIndex to toByteIndex + countBytes - 1. + to_byte_index = to_byte_index + count_bytes - 1; + + // i. Let direction be -1. + -1 + } + // k. Else, + else { + // i. Let direction be 1. + 1 + }; + + let buffer_obj = o + .viewed_array_buffer() + .expect("Already checked for detached buffer"); + let mut buffer_obj_borrow = buffer_obj.borrow_mut(); + let buffer = buffer_obj_borrow + .as_array_buffer_mut() + .expect("Already checked for detached buffer"); + + // l. Repeat, while countBytes > 0, + while count_bytes > 0 { + // i. Let value be GetValueFromBuffer(buffer, fromByteIndex, Uint8, true, Unordered). + let value = buffer.get_value_from_buffer( + from_byte_index as usize, + TypedArrayKind::Uint8, + true, + SharedMemoryOrder::Unordered, + None, + ); + + // ii. Perform SetValueInBuffer(buffer, toByteIndex, Uint8, value, true, Unordered). + buffer.set_value_in_buffer( + to_byte_index as usize, + TypedArrayKind::Uint8, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // iii. Set fromByteIndex to fromByteIndex + direction. + from_byte_index += direction; + + // iv. Set toByteIndex to toByteIndex + direction. + to_byte_index += direction; + + // v. Set countBytes to countBytes - 1. + count_bytes -= 1; + } + } + + // 18. Return O. + Ok(this.clone()) + } + + /// `23.2.3.7 %TypedArray%.prototype.entries ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.entries + fn entries(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let o = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.borrow() + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))? + .is_detached() + { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Return CreateArrayIterator(O, key+value). + Ok(ArrayIterator::create_array_iterator( + o.clone(), + PropertyNameKind::KeyAndValue, + context, + )) + } + + /// `23.2.3.8 %TypedArray%.prototype.every ( callbackfn [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.every + pub(crate) fn every( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.every called with non-callable callback function", + ) + } + }; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context)?; + + // c. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + let test_result = callback_fn + .call( + args.get_or_undefined(1), + &[k_value, k.into(), this.clone()], + context, + )? + .to_boolean(); + + // d. If testResult is false, return false. + if !test_result { + return Ok(false.into()); + } + } + + // 7. Return true. + Ok(true.into()) + } + + /// `23.2.3.9 %TypedArray%.prototype.fill ( value [ , start [ , end ] ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.fill + pub(crate) fn fill( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. If O.[[ContentType]] is BigInt, set value to ? ToBigInt(value). + let value: JsValue = if o.typed_array_name().content_type() == ContentType::BigInt { + args.get_or_undefined(0).to_bigint(context)?.into() + // 5. Otherwise, set value to ? ToNumber(value). + } else { + args.get_or_undefined(0).to_number(context)?.into() + }; + + // 6. Let relativeStart be ? ToIntegerOrInfinity(start). + let mut k = match args.get_or_undefined(1).to_integer_or_infinity(context)? { + // 7. If relativeStart is -∞, let k be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 8. Else if relativeStart < 0, let k be max(len + relativeStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 9. Else, let k be min(relativeStart, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 10. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + let end = args.get_or_undefined(2); + let relative_end = if end.is_undefined() { + IntegerOrInfinity::Integer(len) + } else { + end.to_integer_or_infinity(context)? + }; + + let r#final = match relative_end { + // 11. If relativeEnd is -∞, let final be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 12. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 13. Else, let final be min(relativeEnd, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 14. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is true, throw a TypeError exception. + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 15. Repeat, while k < final, + while k < r#final { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Perform ! Set(O, Pk, value, true). + obj.set(k, value.clone(), true, context) + .expect("Set cannot fail here"); + + // c. Set k to k + 1. + k += 1; + } + + // 16. Return O. + Ok(this.clone()) + } + + /// `23.2.3.10 %TypedArray%.prototype.filter ( callbackfn [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.filter + pub(crate) fn filter( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.filter called with non-callable callback function", + ) + } + }; + + // 5. Let kept be a new empty List. + let mut kept = Vec::new(); + + // 6. Let k be 0. + // 7. Let captured be 0. + let mut captured = 0; + + // 8. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Let selected be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).# + let selected = callback_fn + .call( + args.get_or_undefined(1), + &[k_value.clone(), k.into(), this.clone()], + context, + )? + .to_boolean(); + + // d. If selected is true, then + if selected { + // i. Append kValue to the end of kept. + kept.push(k_value); + + // ii. Set captured to captured + 1. + captured += 1; + } + } + + // 9. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(captured) »). + let a = Self::species_create(obj, o.typed_array_name(), &[captured.into()], context)?; + + // 10. Let n be 0. + // 11. For each element e of kept, do + for (n, e) in kept.iter().enumerate() { + // a. Perform ! Set(A, ! ToString(𝔽(n)), e, true). + a.set(n, e.clone(), true, context) + .expect("Set cannot fail here"); + // b. Set n to n + 1. + } + + // 12. Return A. + Ok(a.into()) + } + + /// `23.2.3.11 %TypedArray%.prototype.find ( predicate [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.find + pub(crate) fn find( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.find called with non-callable predicate function", + ) + } + }; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + // d. If testResult is true, return kValue. + if predicate + .call( + args.get_or_undefined(1), + &[k_value.clone(), k.into(), this.clone()], + context, + )? + .to_boolean() + { + return Ok(k_value); + } + } + + // 7. Return undefined. + Ok(JsValue::undefined()) + } + + /// `23.2.3.12 %TypedArray%.prototype.findIndex ( predicate [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.findindex + pub(crate) fn findindex( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(predicate) is false, throw a TypeError exception. + let predicate = + match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => return context.throw_type_error( + "TypedArray.prototype.findindex called with non-callable predicate function", + ), + }; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)). + // d. If testResult is true, return 𝔽(k). + if predicate + .call( + args.get_or_undefined(1), + &[k_value.clone(), k.into(), this.clone()], + context, + )? + .to_boolean() + { + return Ok(k.into()); + } + } + + // 7. Return -1𝔽. + Ok((-1).into()) + } + + /// `23.2.3.13 %TypedArray%.prototype.forEach ( callbackfn [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.foreach + pub(crate) fn foreach( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.foreach called with non-callable callback function", + ) + } + }; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + callback_fn.call( + args.get_or_undefined(1), + &[k_value, k.into(), this.clone()], + context, + )?; + } + + // 7. Return undefined. + Ok(JsValue::undefined()) + } + + /// `23.2.3.14 %TypedArray%.prototype.includes ( searchElement [ , fromIndex ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.includes + pub(crate) fn includes( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. If len is 0, return false. + if len == 0 { + return Ok(false.into()); + } + + // 5. Let n be ? ToIntegerOrInfinity(fromIndex). + // 6. Assert: If fromIndex is undefined, then n is 0. + let n = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + let n = match n { + // 7. If n is +∞, return false. + IntegerOrInfinity::PositiveInfinity => return Ok(false.into()), + // 8. Else if n is -∞, set n to 0. + IntegerOrInfinity::NegativeInfinity => 0, + IntegerOrInfinity::Integer(i) => i, + }; + + // 9. If n ≥ 0, then + let mut k = if n >= 0 { + // a. Let k be n. + n + // 10. Else, + } else { + // a. Let k be len + n. + // b. If k < 0, set k to 0. + if len + n < 0 { + 0 + } else { + len + n + } + }; + + // 11. Repeat, while k < len, + while k < len { + // a. Let elementK be ! Get(O, ! ToString(𝔽(k))). + let element_k = obj.get(k, context).expect("Get cannot fail here"); + + // b. If SameValueZero(searchElement, elementK) is true, return true. + if JsValue::same_value_zero(args.get_or_undefined(0), &element_k) { + return Ok(true.into()); + } + + // c. Set k to k + 1. + k += 1; + } + + // 12. Return false. + Ok(false.into()) + } + + /// `23.2.3.15 %TypedArray%.prototype.indexOf ( searchElement [ , fromIndex ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.indexof + pub(crate) fn index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. If len is 0, return -1𝔽. + if len == 0 { + return Ok((-1).into()); + } + + // 5. Let n be ? ToIntegerOrInfinity(fromIndex). + // 6. Assert: If fromIndex is undefined, then n is 0. + let n = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + let n = match n { + // 7. If n is +∞, return -1𝔽. + IntegerOrInfinity::PositiveInfinity => return Ok((-1).into()), + // 8. Else if n is -∞, set n to 0. + IntegerOrInfinity::NegativeInfinity => 0, + IntegerOrInfinity::Integer(i) => i, + }; + + // 9. If n ≥ 0, then + let mut k = if n >= 0 { + // a. Let k be n. + n + // 10. Else, + } else { + // a. Let k be len + n. + // b. If k < 0, set k to 0. + if len + n < 0 { + 0 + } else { + len + n + } + }; + + // 11. Repeat, while k < len, + while k < len { + // a. Let kPresent be ! HasProperty(O, ! ToString(𝔽(k))). + let k_present = obj + .has_property(k, context) + .expect("HasProperty cannot fail here"); + + // b. If kPresent is true, then + if k_present { + // i. Let elementK be ! Get(O, ! ToString(𝔽(k))). + let element_k = obj.get(k, context).expect("Get cannot fail here"); + + // ii. Let same be IsStrictlyEqual(searchElement, elementK). + // iii. If same is true, return 𝔽(k). + if args.get_or_undefined(0).strict_equals(&element_k) { + return Ok(k.into()); + } + } + + // c. Set k to k + 1. + k += 1; + } + + // 12. Return -1𝔽. + Ok((-1).into()) + } + + /// `23.2.3.16 %TypedArray%.prototype.join ( separator )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.join + pub(crate) fn join( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If separator is undefined, let sep be the single-element String ",". + let separator = args.get_or_undefined(0); + let sep = if separator.is_undefined() { + JsString::new(",") + // 5. Else, let sep be ? ToString(separator). + } else { + separator.to_string(context)? + }; + + // 6. Let R be the empty String. + let mut r = JsString::new(""); + + // 7. Let k be 0. + // 8. Repeat, while k < len, + for k in 0..len { + // a. If k > 0, set R to the string-concatenation of R and sep. + if k > 0 { + r = JsString::concat(r, sep.clone()); + } + + // b. Let element be ! Get(O, ! ToString(𝔽(k))). + let element = obj.get(k, context).expect("Get cannot fail here"); + + // c. If element is undefined, let next be the empty String; otherwise, let next be ! ToString(element). + // d. Set R to the string-concatenation of R and next. + if !element.is_undefined() { + r = JsString::concat(r, element.to_string(context)?); + } + } + + // 9. Return R. + Ok(r.into()) + } + + /// `23.2.3.17 %TypedArray%.prototype.keys ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.keys + pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let o = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.borrow() + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))? + .is_detached() + { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Return CreateArrayIterator(O, key). + Ok(ArrayIterator::create_array_iterator( + o.clone(), + PropertyNameKind::Key, + context, + )) + } + + /// `23.2.3.18 %TypedArray%.prototype.lastIndexOf ( searchElement [ , fromIndex ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.lastindexof + pub(crate) fn last_index_of( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. If len is 0, return -1𝔽. + if len == 0 { + return Ok((-1).into()); + } + + // 5. If fromIndex is present, let n be ? ToIntegerOrInfinity(fromIndex); else let n be len - 1. + let n = if let Some(n) = args.get(1) { + n.to_integer_or_infinity(context)? + } else { + IntegerOrInfinity::Integer(len - 1) + }; + + let mut k = match n { + // 6. If n is -∞, return -1𝔽. + IntegerOrInfinity::NegativeInfinity => return Ok((-1).into()), + // 7. If n ≥ 0, then + // a. Let k be min(n, len - 1). + IntegerOrInfinity::Integer(i) if i >= 0 => std::cmp::min(i, len - 1), + IntegerOrInfinity::PositiveInfinity => len - 1, + // 8. Else, + // a. Let k be len + n. + IntegerOrInfinity::Integer(i) => len + i, + }; + + // 9. Repeat, while k ≥ 0, + while k >= 0 { + // a. Let kPresent be ! HasProperty(O, ! ToString(𝔽(k))). + let k_present = obj + .has_property(k, context) + .expect("HasProperty cannot fail here"); + + // b. If kPresent is true, then + if k_present { + // i. Let elementK be ! Get(O, ! ToString(𝔽(k))). + let element_k = obj.get(k, context).expect("Get cannot fail here"); + + // ii. Let same be IsStrictlyEqual(searchElement, elementK). + // iii. If same is true, return 𝔽(k). + if args.get_or_undefined(0).strict_equals(&element_k) { + return Ok(k.into()); + } + } + + // c. Set k to k - 1. + k -= 1; + } + + // 10. Return -1𝔽. + Ok((-1).into()) + } + + /// `23.2.3.19 get %TypedArray%.prototype.length` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%.prototype.length + pub(crate) fn length( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[TypedArrayName]]). + // 3. Assert: O has [[ViewedArrayBuffer]] and [[ArrayLength]] internal slots. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let typed_array = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + // 5. If IsDetachedBuffer(buffer) is true, return +0𝔽. + // 6. Let length be O.[[ArrayLength]]. + // 7. Return 𝔽(length). + if typed_array.is_detached() { + Ok(0.into()) + } else { + Ok(typed_array.array_length().into()) + } + } + + /// `23.2.3.20 %TypedArray%.prototype.map ( callbackfn [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.map + pub(crate) fn map( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.map called with non-callable callback function", + ) + } + }; + + // 5. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(len) »). + let a = Self::species_create(obj, o.typed_array_name(), &[len.into()], context)?; + + // 6. Let k be 0. + // 7. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »). + let mapped_value = callback_fn.call( + args.get_or_undefined(1), + &[k_value, k.into(), this.clone()], + context, + )?; + + // d. Perform ? Set(A, Pk, mappedValue, true). + a.set(k, mapped_value, true, context)?; + } + + // 8. Return A. + Ok(a.into()) + } + + /// `23.2.3.21 %TypedArray%.prototype.reduce ( callbackfn [ , initialValue ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.reduce + pub(crate) fn reduce( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.reduce called with non-callable callback function", + ) + } + }; + + // 5. If len = 0 and initialValue is not present, throw a TypeError exception. + if len == 0 && args.get(1).is_none() { + return context + .throw_type_error("Typed array length is 0 and initial value is not present"); + } + + // 6. Let k be 0. + let mut k = 0; + + // 7. Let accumulator be undefined. + // 8. If initialValue is present, then + let mut accumulator = if let Some(initial_value) = args.get(1) { + // a. Set accumulator to initialValue. + initial_value.clone() + // 9. Else, + } else { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Set accumulator to ! Get(O, Pk). + // c. Set k to k + 1. + k += 1; + obj.get(0, context).expect("Get cannot fail here") + }; + + // 10. Repeat, while k < len, + while k < len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »). + accumulator = callback_fn.call( + &JsValue::undefined(), + &[accumulator, k_value, k.into(), this.clone()], + context, + )?; + + // d. Set k to k + 1. + k += 1; + } + + // 11. Return accumulator. + Ok(accumulator) + } + + /// `23.2.3.22 %TypedArray%.prototype.reduceRight ( callbackfn [ , initialValue ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.reduceright + pub(crate) fn reduceright( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = + match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => return context.throw_type_error( + "TypedArray.prototype.reduceright called with non-callable callback function", + ), + }; + + // 5. If len = 0 and initialValue is not present, throw a TypeError exception. + if len == 0 && args.get(1).is_none() { + return context + .throw_type_error("Typed array length is 0 and initial value is not present"); + } + + // 6. Let k be len - 1. + let mut k = len - 1; + + // 7. Let accumulator be undefined. + // 8. If initialValue is present, then + let mut accumulator = if let Some(initial_value) = args.get(1) { + // a. Set accumulator to initialValue. + initial_value.clone() + // 9. Else, + } else { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Set accumulator to ! Get(O, Pk). + let accumulator = obj.get(k, context).expect("Get cannot fail here"); + + // c. Set k to k - 1. + k -= 1; + + accumulator + }; + + // 10. Repeat, while k ≥ 0, + while k >= 0 { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »). + accumulator = callback_fn.call( + &JsValue::undefined(), + &[accumulator, k_value, k.into(), this.clone()], + context, + )?; + + // d. Set k to k - 1. + k -= 1; + } + + // 11. Return accumulator. + Ok(accumulator) + } + + /// `23.2.3.23 %TypedArray%.prototype.reverse ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.reverse + #[allow(clippy::float_cmp)] + pub(crate) fn reverse( + this: &JsValue, + _: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as f64; + + // 4. Let middle be floor(len / 2). + let middle = (len / 2.0).floor(); + + // 5. Let lower be 0. + let mut lower = 0.0; + // 6. Repeat, while lower ≠ middle, + while lower != middle { + // a. Let upper be len - lower - 1. + let upper = len - lower - 1.0; + + // b. Let upperP be ! ToString(𝔽(upper)). + // c. Let lowerP be ! ToString(𝔽(lower)). + // d. Let lowerValue be ! Get(O, lowerP). + let lower_value = obj.get(lower, context).expect("Get cannot fail here"); + // e. Let upperValue be ! Get(O, upperP). + let upper_value = obj.get(upper, context).expect("Get cannot fail here"); + + // f. Perform ! Set(O, lowerP, upperValue, true). + obj.set(lower, upper_value, true, context) + .expect("Set cannot fail here"); + // g. Perform ! Set(O, upperP, lowerValue, true). + obj.set(upper, lower_value, true, context) + .expect("Set cannot fail here"); + + // h. Set lower to lower + 1. + lower += 1.0; + } + + // 7. Return O. + Ok(this.clone()) + } + + /// `23.2.3.24 %TypedArray%.prototype.set ( source [ , offset ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.set + pub(crate) fn set( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let target be the this value. + // 2. Perform ? RequireInternalSlot(target, [[TypedArrayName]]). + // 3. Assert: target has a [[ViewedArrayBuffer]] internal slot. + let target = this.as_object().ok_or_else(|| { + context.construct_type_error("TypedArray.set must be called on typed array object") + })?; + if !target.is_typed_array() { + return context.throw_type_error("TypedArray.set must be called on typed array object"); + } + + // 4. Let targetOffset be ? ToIntegerOrInfinity(offset). + let target_offset = args.get_or_undefined(1).to_integer_or_infinity(context)?; + + // 5. If targetOffset < 0, throw a RangeError exception. + match target_offset { + IntegerOrInfinity::Integer(i) if i < 0 => { + return context.throw_range_error("TypedArray.set called with negative offset") + } + IntegerOrInfinity::NegativeInfinity => { + return context.throw_range_error("TypedArray.set called with negative offset") + } + _ => {} + } + + let source = args.get_or_undefined(0); + match source { + // 6. If source is an Object that has a [[TypedArrayName]] internal slot, then + JsValue::Object(source) if source.is_typed_array() => { + // a. Perform ? SetTypedArrayFromTypedArray(target, targetOffset, source). + Self::set_typed_array_from_typed_array(target, target_offset, source, context)?; + } + // 7. Else, + _ => { + // a. Perform ? SetTypedArrayFromArrayLike(target, targetOffset, source). + Self::set_typed_array_from_array_like(target, target_offset, source, context)?; + } + } + + // 8. Return undefined. + Ok(JsValue::undefined()) + } + + /// `3.2.3.24.1 SetTypedArrayFromTypedArray ( target, targetOffset, source )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-settypedarrayfromtypedarray + fn set_typed_array_from_typed_array( + target: &JsObject, + target_offset: IntegerOrInfinity, + source: &JsObject, + context: &mut Context, + ) -> JsResult<()> { + let target_borrow = target.borrow(); + let target_array = target_borrow + .as_typed_array() + .expect("Target must be a typed array"); + + let source_borrow = source.borrow(); + let source_array = source_borrow + .as_typed_array() + .expect("Source must be a typed array"); + + // 1. Let targetBuffer be target.[[ViewedArrayBuffer]]. + // 2. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError exception. + if target_array.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + let target_buffer_obj = target_array + .viewed_array_buffer() + .expect("Already checked for detached buffer"); + + // 3. Let targetLength be target.[[ArrayLength]]. + let target_length = target_array.array_length(); + + // 4. Let srcBuffer be source.[[ViewedArrayBuffer]]. + // 5. If IsDetachedBuffer(srcBuffer) is true, throw a TypeError exception. + if source_array.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + let mut src_buffer_obj = source_array + .viewed_array_buffer() + .expect("Already checked for detached buffer") + .clone(); + + // 6. Let targetName be the String value of target.[[TypedArrayName]]. + // 7. Let targetType be the Element Type value in Table 73 for targetName. + let target_name = target_array.typed_array_name(); + + // 8. Let targetElementSize be the Element Size value specified in Table 73 for targetName. + let target_element_size = target_name.element_size(); + + // 9. Let targetByteOffset be target.[[ByteOffset]]. + let target_byte_offset = target_array.byte_offset(); + + // 10. Let srcName be the String value of source.[[TypedArrayName]]. + // 11. Let srcType be the Element Type value in Table 73 for srcName. + let src_name = source_array.typed_array_name(); + + // 12. Let srcElementSize be the Element Size value specified in Table 73 for srcName. + let src_element_size = src_name.element_size(); + + // 13. Let srcLength be source.[[ArrayLength]]. + let src_length = source_array.array_length(); + + // 14. Let srcByteOffset be source.[[ByteOffset]]. + let src_byte_offset = source_array.byte_offset(); + + // 15. If targetOffset is +∞, throw a RangeError exception. + let target_offset = match target_offset { + IntegerOrInfinity::Integer(i) if i >= 0 => i as usize, + IntegerOrInfinity::PositiveInfinity => { + return context.throw_range_error("Target offset cannot be Infinity"); + } + _ => unreachable!(), + }; + + // 16. If srcLength + targetOffset > targetLength, throw a RangeError exception. + if src_length + target_offset > target_length { + return context.throw_range_error( + "Source typed array and target offset longer than target typed array", + ); + } + + // 17. If target.[[ContentType]] ≠ source.[[ContentType]], throw a TypeError exception. + if target_name.content_type() != src_name.content_type() { + return context.throw_type_error( + "Source typed array and target typed array have different content types", + ); + } + + // TODO: Shared Array Buffer + // 18. If both IsSharedArrayBuffer(srcBuffer) and IsSharedArrayBuffer(targetBuffer) are true, then + + // a. If srcBuffer.[[ArrayBufferData]] and targetBuffer.[[ArrayBufferData]] are the same Shared Data Block values, let same be true; else let same be false. + + // 19. Else, let same be SameValue(srcBuffer, targetBuffer). + let same = JsObject::equals(&src_buffer_obj, target_buffer_obj); + + // 20. If same is true, then + let mut src_byte_index = if same { + // a. Let srcByteLength be source.[[ByteLength]]. + let src_byte_length = source_array.byte_length(); + + // b. Set srcBuffer to ? CloneArrayBuffer(srcBuffer, srcByteOffset, srcByteLength, %ArrayBuffer%). + // c. NOTE: %ArrayBuffer% is used to clone srcBuffer because is it known to not have any observable side-effects. + let array_buffer_constructor = context + .intrinsics() + .constructors() + .array_buffer() + .constructor() + .into(); + let s = src_buffer_obj + .borrow() + .as_array_buffer() + .expect("Already checked for detached buffer") + .clone_array_buffer( + src_byte_offset, + src_byte_length, + &array_buffer_constructor, + context, + )?; + src_buffer_obj = s; + + // d. Let srcByteIndex be 0. + 0 + } + // 21. Else, let srcByteIndex be srcByteOffset. + else { + src_byte_offset + }; + + // 22. Let targetByteIndex be targetOffset × targetElementSize + targetByteOffset. + let mut target_byte_index = target_offset * target_element_size + target_byte_offset; + + // 23. Let limit be targetByteIndex + targetElementSize × srcLength. + let limit = target_byte_index + target_element_size * src_length; + + let src_buffer_obj_borrow = src_buffer_obj.borrow(); + let src_buffer = src_buffer_obj_borrow + .as_array_buffer() + .expect("Must be an array buffer"); + + // 24. If srcType is the same as targetType, then + if src_name == target_name { + // a. NOTE: If srcType and targetType are the same, the transfer must be performed in a manner that preserves the bit-level encoding of the source data. + // b. Repeat, while targetByteIndex < limit, + while target_byte_index < limit { + // i. Let value be GetValueFromBuffer(srcBuffer, srcByteIndex, Uint8, true, Unordered). + let value = src_buffer.get_value_from_buffer( + src_byte_index, + TypedArrayKind::Uint8, + true, + SharedMemoryOrder::Unordered, + None, + ); + + // ii. Perform SetValueInBuffer(targetBuffer, targetByteIndex, Uint8, value, true, Unordered). + target_buffer_obj + .borrow_mut() + .as_array_buffer_mut() + .expect("Must be an array buffer") + .set_value_in_buffer( + target_byte_index, + TypedArrayKind::Uint8, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // iii. Set srcByteIndex to srcByteIndex + 1. + src_byte_index += 1; + + // iv. Set targetByteIndex to targetByteIndex + 1. + target_byte_index += 1; + } + } + // 25. Else, + else { + // a. Repeat, while targetByteIndex < limit, + while target_byte_index < limit { + // i. Let value be GetValueFromBuffer(srcBuffer, srcByteIndex, srcType, true, Unordered). + let value = src_buffer.get_value_from_buffer( + src_byte_index, + src_name, + true, + SharedMemoryOrder::Unordered, + None, + ); + + // ii. Perform SetValueInBuffer(targetBuffer, targetByteIndex, targetType, value, true, Unordered). + target_buffer_obj + .borrow_mut() + .as_array_buffer_mut() + .expect("Must be an array buffer") + .set_value_in_buffer( + target_byte_index, + target_name, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // iii. Set srcByteIndex to srcByteIndex + srcElementSize. + src_byte_index += src_element_size; + + // iv. Set targetByteIndex to targetByteIndex + targetElementSize. + target_byte_index += target_element_size; + } + } + + Ok(()) + } + + /// `23.2.3.24.2 SetTypedArrayFromArrayLike ( target, targetOffset, source )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-settypedarrayfromarraylike + fn set_typed_array_from_array_like( + target: &JsObject, + target_offset: IntegerOrInfinity, + source: &JsValue, + context: &mut Context, + ) -> JsResult<()> { + let target_borrow = target.borrow(); + let target_array = target_borrow + .as_typed_array() + .expect("Target must be a typed array"); + + // 1. Let targetBuffer be target.[[ViewedArrayBuffer]]. + // 2. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError exception. + if target_array.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let targetLength be target.[[ArrayLength]]. + let target_length = target_array.array_length(); + + // 4. Let targetName be the String value of target.[[TypedArrayName]]. + // 6. Let targetType be the Element Type value in Table 73 for targetName. + let target_name = target_array.typed_array_name(); + + // 5. Let targetElementSize be the Element Size value specified in Table 73 for targetName. + let target_element_size = target_name.element_size(); + + // 7. Let targetByteOffset be target.[[ByteOffset]]. + let target_byte_offset = target_array.byte_offset(); + + // 8. Let src be ? ToObject(source). + let src = source.to_object(context)?; + + // 9. Let srcLength be ? LengthOfArrayLike(src). + let src_length = src.length_of_array_like(context)?; + + let target_offset = match target_offset { + // 10. If targetOffset is +∞, throw a RangeError exception. + IntegerOrInfinity::PositiveInfinity => { + return context.throw_range_error("Target offset cannot be Infinity") + } + IntegerOrInfinity::Integer(i) if i >= 0 => i as usize, + _ => unreachable!(), + }; + + // 11. If srcLength + targetOffset > targetLength, throw a RangeError exception. + if src_length + target_offset > target_length { + return context.throw_range_error( + "Source object and target offset longer than target typed array", + ); + } + + // 12. Let targetByteIndex be targetOffset × targetElementSize + targetByteOffset. + let mut target_byte_index = target_offset * target_element_size + target_byte_offset; + + // 13. Let k be 0. + let mut k = 0; + + // 14. Let limit be targetByteIndex + targetElementSize × srcLength. + let limit = target_byte_index + target_element_size * src_length; + + // 15. Repeat, while targetByteIndex < limit, + while target_byte_index < limit { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let value be ? Get(src, Pk). + let value = src.get(k, context)?; + + // c. If target.[[ContentType]] is BigInt, set value to ? ToBigInt(value). + // d. Otherwise, set value to ? ToNumber(value). + let value = if target_name.content_type() == ContentType::BigInt { + value.to_bigint(context)?.into() + } else { + value.to_number(context)?.into() + }; + + let target_buffer_obj = target_array + .viewed_array_buffer() + .expect("Already checked for detached buffer"); + let mut target_buffer_obj_borrow = target_buffer_obj.borrow_mut(); + let target_buffer = target_buffer_obj_borrow + .as_array_buffer_mut() + .expect("Already checked for detached buffer"); + + // e. If IsDetachedBuffer(targetBuffer) is true, throw a TypeError exception. + if target_buffer.is_detached_buffer() { + return context.throw_type_error("Cannot set value on detached array buffer"); + } + + // f. Perform SetValueInBuffer(targetBuffer, targetByteIndex, targetType, value, true, Unordered). + target_buffer.set_value_in_buffer( + target_byte_index, + target_name, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // g. Set k to k + 1. + k += 1; + + // h. Set targetByteIndex to targetByteIndex + targetElementSize. + target_byte_index += target_element_size; + } + + Ok(()) + } + + /// `23.2.3.25 %TypedArray%.prototype.slice ( start, end )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.slice + pub(crate) fn slice( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length() as i64; + + // 4. Let relativeStart be ? ToIntegerOrInfinity(start). + let mut k = match args.get_or_undefined(0).to_integer_or_infinity(context)? { + // 5. If relativeStart is -∞, let k be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 6. Else if relativeStart < 0, let k be max(len + relativeStart, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 7. Else, let k be min(relativeStart, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 8. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end). + let end = args.get_or_undefined(1); + let relative_end = if end.is_undefined() { + IntegerOrInfinity::Integer(len) + } else { + end.to_integer_or_infinity(context)? + }; + + let r#final = match relative_end { + // 9. If relativeEnd is -∞, let final be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 10. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(len + i, 0), + // 11. Else, let final be min(relativeEnd, len). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, len), + IntegerOrInfinity::PositiveInfinity => len, + }; + + // 12. Let count be max(final - k, 0). + let count = std::cmp::max(r#final - k, 0) as usize; + + // 13. Let A be ? TypedArraySpeciesCreate(O, « 𝔽(count) »). + let a = Self::species_create(obj, o.typed_array_name(), &[count.into()], context)?; + let a_borrow = a.borrow(); + let a_array = a_borrow + .as_typed_array() + .expect("This must be a typed array"); + + // 14. If count > 0, then + if count > 0 { + // a. If IsDetachedBuffer(O.[[ViewedArrayBuffer]]) is true, throw a TypeError exception. + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // b. Let srcName be the String value of O.[[TypedArrayName]]. + // c. Let srcType be the Element Type value in Table 73 for srcName. + // d. Let targetName be the String value of A.[[TypedArrayName]]. + // e. Let targetType be the Element Type value in Table 73 for targetName. + // f. If srcType is different from targetType, then + #[allow(clippy::if_not_else)] + if o.typed_array_name() != a_array.typed_array_name() { + // i. Let n be 0. + let mut n = 0; + // ii. Repeat, while k < final, + while k < r#final { + // 1. Let Pk be ! ToString(𝔽(k)). + // 2. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // 3. Perform ! Set(A, ! ToString(𝔽(n)), kValue, true). + a.set(n, k_value, true, context) + .expect("Set cannot fail here"); + + // 4. Set k to k + 1. + k += 1; + + // 5. Set n to n + 1. + n += 1; + } + // g. Else, + } else { + // i. Let srcBuffer be O.[[ViewedArrayBuffer]]. + let src_buffer_obj = o.viewed_array_buffer().expect("Cannot be detached here"); + let src_buffer_obj_borrow = src_buffer_obj.borrow(); + let src_buffer = src_buffer_obj_borrow + .as_array_buffer() + .expect("Cannot be detached here"); + + // ii. Let targetBuffer be A.[[ViewedArrayBuffer]]. + let target_buffer_obj = a_array + .viewed_array_buffer() + .expect("Cannot be detached here"); + let mut target_buffer_obj_borrow = target_buffer_obj.borrow_mut(); + let target_buffer = target_buffer_obj_borrow + .as_array_buffer_mut() + .expect("Cannot be detached here"); + + // iii. Let elementSize be the Element Size value specified in Table 73 for Element Type srcType. + let element_size = o.typed_array_name().element_size(); + + // iv. NOTE: If srcType and targetType are the same, the transfer must be performed in a manner that preserves the bit-level encoding of the source data. + + // v. Let srcByteOffset be O.[[ByteOffset]]. + let src_byte_offset = o.byte_offset(); + + // vi. Let targetByteIndex be A.[[ByteOffset]]. + let mut target_byte_index = a_array.byte_offset(); + + // vii. Let srcByteIndex be (k × elementSize) + srcByteOffset. + let mut src_byte_index = k as usize * element_size + src_byte_offset; + + // viii. Let limit be targetByteIndex + count × elementSize. + let limit = target_byte_index + count * element_size; + + // ix. Repeat, while targetByteIndex < limit, + while target_byte_index < limit { + // 1. Let value be GetValueFromBuffer(srcBuffer, srcByteIndex, Uint8, true, Unordered). + let value = src_buffer.get_value_from_buffer( + src_byte_index, + TypedArrayKind::Uint8, + true, + SharedMemoryOrder::Unordered, + None, + ); + + // 2. Perform SetValueInBuffer(targetBuffer, targetByteIndex, Uint8, value, true, Unordered). + target_buffer.set_value_in_buffer( + target_byte_index, + TypedArrayKind::Uint8, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // 3. Set srcByteIndex to srcByteIndex + 1. + src_byte_index += 1; + + // 4. Set targetByteIndex to targetByteIndex + 1. + target_byte_index += 1; + } + } + } + + drop(a_borrow); + + // 15. Return A. + Ok(a.into()) + } + + /// `23.2.3.26 %TypedArray%.prototype.some ( callbackfn [ , thisArg ] )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.some + pub(crate) fn some( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Let len be O.[[ArrayLength]]. + let len = o.array_length(); + + // 4. If IsCallable(callbackfn) is false, throw a TypeError exception. + let callback_fn = match args.get_or_undefined(0).as_object() { + Some(obj) if obj.is_callable() => obj, + _ => { + return context.throw_type_error( + "TypedArray.prototype.some called with non-callable callback function", + ) + } + }; + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ! Get(O, Pk). + let k_value = obj.get(k, context).expect("Get cannot fail here"); + + // c. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)). + // d. If testResult is true, return true. + if callback_fn + .call( + args.get_or_undefined(1), + &[k_value, k.into(), this.clone()], + context, + )? + .to_boolean() + { + return Ok(true.into()); + } + } + + // 7. Return false. + Ok(false.into()) + } + + /// `23.2.3.27 %TypedArray%.prototype.sort ( comparefn )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.sort + pub(crate) fn sort( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception. + let compare_fn = match args.get(0) { + None | Some(JsValue::Undefined) => None, + Some(JsValue::Object(obj)) if obj.is_callable() => Some(obj), + _ => { + return context + .throw_type_error("TypedArray.sort called with non-callable comparefn") + } + }; + + // 2. Let obj be the this value. + let obj = this.as_object().ok_or_else(|| { + context.construct_type_error("TypedArray.sort must be called on typed array object") + })?; + + // 4. Let buffer be obj.[[ViewedArrayBuffer]]. + // 5. Let len be obj.[[ArrayLength]]. + let (buffer, len) = { + // 3. Perform ? ValidateTypedArray(obj). + let obj_borrow = obj.borrow(); + let o = obj_borrow.as_typed_array().ok_or_else(|| { + context.construct_type_error("TypedArray.sort must be called on typed array object") + })?; + if o.is_detached() { + return context.throw_type_error( + "TypedArray.sort called on typed array object with detached array buffer", + ); + } + + ( + o.viewed_array_buffer() + .expect("Already checked for detached buffer") + .clone(), + o.array_length(), + ) + }; + + // 4. Let items be a new empty List. + let mut items = Vec::with_capacity(len); + + // 5. Let k be 0. + // 6. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kPresent be ? HasProperty(obj, Pk). + // c. If kPresent is true, then + if obj.has_property(k, context)? { + // i. Let kValue be ? Get(obj, Pk). + let k_val = obj.get(k, context)?; + // ii. Append kValue to items. + items.push(k_val); + } + // d. Set k to k + 1. + } + + // 7. Let itemCount be the number of elements in items. + let item_count = items.len(); + + let sort_compare = |x: &JsValue, + y: &JsValue, + compare_fn: Option<&JsObject>, + context: &mut Context| + -> JsResult { + // 1. Assert: Both Type(x) and Type(y) are Number or both are BigInt. + // 2. If comparefn is not undefined, then + if let Some(obj) = compare_fn { + // a. Let v be ? ToNumber(? Call(comparefn, undefined, « x, y »)). + let v = obj + .call(&JsValue::undefined(), &[x.clone(), y.clone()], context)? + .to_number(context)?; + + // b. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer + .borrow() + .as_array_buffer() + .expect("Must be array buffer") + .is_detached_buffer() + { + return context + .throw_type_error("Cannot sort typed array with detached buffer"); + } + + // c. If v is NaN, return +0𝔽. + // d. Return v. + return Ok(v.partial_cmp(&0.0).unwrap_or(Ordering::Equal)); + } + + if let (JsValue::BigInt(x), JsValue::BigInt(y)) = (x, y) { + // 6. If x < y, return -1𝔽. + if x < y { + return Ok(Ordering::Less); + } + + // 7. If x > y, return 1𝔽. + if x > y { + return Ok(Ordering::Greater); + } + + // 8. If x is -0𝔽 and y is +0𝔽, return -1𝔽. + if x.is_zero() + && y.is_zero() + && x.as_inner().is_negative() + && y.as_inner().is_positive() + { + return Ok(Ordering::Less); + } + + // 9. If x is +0𝔽 and y is -0𝔽, return 1𝔽. + if x.is_zero() + && y.is_zero() + && x.as_inner().is_positive() + && y.as_inner().is_negative() + { + return Ok(Ordering::Greater); + } + + // 10. Return +0𝔽. + Ok(Ordering::Equal) + } else { + let x = x + .as_number() + .expect("Typed array can only contain number or bigint"); + let y = y + .as_number() + .expect("Typed array can only contain number or bigint"); + + // 3. If x and y are both NaN, return +0𝔽. + if x.is_nan() && y.is_nan() { + return Ok(Ordering::Equal); + } + + // 4. If x is NaN, return 1𝔽. + if x.is_nan() { + return Ok(Ordering::Greater); + } + + // 5. If y is NaN, return -1𝔽. + if y.is_nan() { + return Ok(Ordering::Less); + } + + // 6. If x < y, return -1𝔽. + if x < y { + return Ok(Ordering::Less); + } + + // 7. If x > y, return 1𝔽. + if x > y { + return Ok(Ordering::Greater); + } + + // 8. If x is -0𝔽 and y is +0𝔽, return -1𝔽. + if x.is_zero() && y.is_zero() && x.is_sign_negative() && y.is_sign_positive() { + return Ok(Ordering::Less); + } + + // 9. If x is +0𝔽 and y is -0𝔽, return 1𝔽. + if x.is_zero() && y.is_zero() && x.is_sign_positive() && y.is_sign_negative() { + return Ok(Ordering::Greater); + } + + // 10. Return +0𝔽. + Ok(Ordering::Equal) + } + }; + + // 8. Sort items using an implementation-defined sequence of calls to SortCompare. + // If any such call returns an abrupt completion, stop before performing any further + // calls to SortCompare or steps in this algorithm and return that completion. + let mut sort_err = Ok(()); + items.sort_by(|x, y| { + if sort_err.is_ok() { + sort_compare(x, y, compare_fn, context).unwrap_or_else(|err| { + sort_err = Err(err); + Ordering::Equal + }) + } else { + Ordering::Equal + } + }); + sort_err?; + + // 9. Let j be 0. + // 10. Repeat, while j < itemCount, + for (j, item) in items.into_iter().enumerate() { + // a. Perform ? Set(obj, ! ToString(𝔽(j)), items[j], true). + obj.set(j, item, true, context)?; + // b. Set j to j + 1. + } + + // 11. Repeat, while j < len, + for j in item_count..len { + // a. Perform ? DeletePropertyOrThrow(obj, ! ToString(𝔽(j))). + obj.delete_property_or_throw(j, context)?; + // b. Set j to j + 1. + } + + // 12. Return obj. + Ok(obj.clone().into()) + } + + /// `23.2.3.28 %TypedArray%.prototype.subarray ( begin, end )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.subarray + pub(crate) fn subarray( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? RequireInternalSlot(O, [[TypedArrayName]]). + // 3. Assert: O has a [[ViewedArrayBuffer]] internal slot. + let obj = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + let obj_borrow = obj.borrow(); + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + + // 4. Let buffer be O.[[ViewedArrayBuffer]]. + let buffer = o + .viewed_array_buffer() + .expect("Buffer cannot be detached here"); + + // 5. Let srcLength be O.[[ArrayLength]]. + let src_length = o.array_length() as i64; + + // 6. Let relativeBegin be ? ToIntegerOrInfinity(begin). + let begin_index = match args.get_or_undefined(0).to_integer_or_infinity(context)? { + // 7. If relativeBegin is -∞, let beginIndex be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 8. Else if relativeBegin < 0, let beginIndex be max(srcLength + relativeBegin, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(src_length + i, 0), + // 9. Else, let beginIndex be min(relativeBegin, srcLength). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, src_length), + IntegerOrInfinity::PositiveInfinity => src_length, + }; + + // 10. If end is undefined, let relativeEnd be srcLength; else let relativeEnd be ? ToIntegerOrInfinity(end). + let end = args.get_or_undefined(1); + let relative_end = if end.is_undefined() { + IntegerOrInfinity::Integer(src_length) + } else { + end.to_integer_or_infinity(context)? + }; + + let end_index = match relative_end { + // 11. If relativeEnd is -∞, let endIndex be 0. + IntegerOrInfinity::NegativeInfinity => 0, + // 12. Else if relativeEnd < 0, let endIndex be max(srcLength + relativeEnd, 0). + IntegerOrInfinity::Integer(i) if i < 0 => std::cmp::max(src_length + i, 0), + // 13. Else, let endIndex be min(relativeEnd, srcLength). + IntegerOrInfinity::Integer(i) => std::cmp::min(i, src_length), + IntegerOrInfinity::PositiveInfinity => src_length, + }; + + // 14. Let newLength be max(endIndex - beginIndex, 0). + let new_length = std::cmp::max(end_index - begin_index, 0); + + // 15. Let constructorName be the String value of O.[[TypedArrayName]]. + // 16. Let elementSize be the Element Size value specified in Table 73 for constructorName. + let element_size = o.typed_array_name().element_size(); + + // 17. Let srcByteOffset be O.[[ByteOffset]]. + let src_byte_offset = o.byte_offset(); + + // 18. Let beginByteOffset be srcByteOffset + beginIndex × elementSize. + let begin_byte_offset = src_byte_offset + begin_index as usize * element_size; + + // 19. Let argumentsList be « buffer, 𝔽(beginByteOffset), 𝔽(newLength) ». + // 20. Return ? TypedArraySpeciesCreate(O, argumentsList). + Ok(Self::species_create( + obj, + o.typed_array_name(), + &[ + buffer.clone().into(), + begin_byte_offset.into(), + new_length.into(), + ], + context, + )? + .into()) + } + + // TODO: 23.2.3.29 %TypedArray%.prototype.toLocaleString ( [ reserved1 [ , reserved2 ] ] ) + + /// `23.2.3.31 %TypedArray%.prototype.values ( )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-%typedarray%.prototype.values + fn values(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. Perform ? ValidateTypedArray(O). + let o = this + .as_object() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.borrow() + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))? + .is_detached() + { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. Return CreateArrayIterator(O, value). + Ok(ArrayIterator::create_array_iterator( + o.clone(), + PropertyNameKind::Value, + context, + )) + } + + /// `23.2.3.33 get %TypedArray%.prototype [ @@toStringTag ]` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-get-%typedarray%.prototype-@@tostringtag + #[allow(clippy::unnecessary_wraps)] + fn to_string_tag(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + // 1. Let O be the this value. + // 2. If Type(O) is not Object, return undefined. + // 3. If O does not have a [[TypedArrayName]] internal slot, return undefined. + // 4. Let name be O.[[TypedArrayName]]. + // 5. Assert: Type(name) is String. + // 6. Return name. + Ok(this + .as_object() + .and_then(|obj| { + obj.borrow() + .as_typed_array() + .map(|o| o.typed_array_name().name().into()) + }) + .unwrap_or(JsValue::Undefined)) + } + + /// `23.2.4.1 TypedArraySpeciesCreate ( exemplar, argumentList )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#typedarray-species-create + fn species_create( + exemplar: &JsObject, + typed_array_name: TypedArrayKind, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let defaultConstructor be the intrinsic object listed in column one of Table 73 for exemplar.[[TypedArrayName]]. + let default_constructor = match typed_array_name { + TypedArrayKind::Int8 => StandardConstructors::typed_int8_array, + TypedArrayKind::Uint8 => StandardConstructors::typed_uint8_array, + TypedArrayKind::Uint8Clamped => StandardConstructors::typed_uint8clamped_array, + TypedArrayKind::Int16 => StandardConstructors::typed_int16_array, + TypedArrayKind::Uint16 => StandardConstructors::typed_uint16_array, + TypedArrayKind::Int32 => StandardConstructors::typed_int32_array, + TypedArrayKind::Uint32 => StandardConstructors::typed_uint32_array, + TypedArrayKind::BigInt64 => StandardConstructors::typed_bigint64_array, + TypedArrayKind::BigUint64 => StandardConstructors::typed_biguint64_array, + TypedArrayKind::Float32 => StandardConstructors::typed_float32_array, + TypedArrayKind::Float64 => StandardConstructors::typed_float64_array, + }; + + // 2. Let constructor be ? SpeciesConstructor(exemplar, defaultConstructor). + let constructor = exemplar.species_constructor(default_constructor, context)?; + + // 3. Let result be ? TypedArrayCreate(constructor, argumentList). + let result = Self::create(&constructor, args, context)?; + + // 4. Assert: result has [[TypedArrayName]] and [[ContentType]] internal slots. + // 5. If result.[[ContentType]] ≠ exemplar.[[ContentType]], throw a TypeError exception. + if result + .borrow() + .as_typed_array() + .expect("This can only be a typed array object") + .typed_array_name() + .content_type() + != typed_array_name.content_type() + { + return context + .throw_type_error("New typed array has different context type than exemplar"); + } + + // 6. Return result. + Ok(result) + } + + /// `23.2.4.2 TypedArrayCreate ( constructor, argumentList )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#typedarray-create + fn create( + constructor: &JsObject, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let newTypedArray be ? Construct(constructor, argumentList). + let new_typed_array = constructor.construct(args, Some(constructor), context)?; + + let obj_borrow = new_typed_array.borrow(); + // 2. Perform ? ValidateTypedArray(newTypedArray). + let o = obj_borrow + .as_typed_array() + .ok_or_else(|| context.construct_type_error("Value is not a typed array object"))?; + if o.is_detached() { + return context.throw_type_error("Buffer of the typed array is detached"); + } + + // 3. If argumentList is a List of a single Number, then + if args.len() == 1 { + if let Some(number) = args[0].as_number() { + // a. If newTypedArray.[[ArrayLength]] < ℝ(argumentList[0]), throw a TypeError exception. + if (o.array_length() as f64) < number { + return context + .throw_type_error("New typed array length is smaller than expected"); + } + } + } + + // 4. Return newTypedArray. + Ok(new_typed_array.clone()) + } + + /// + fn allocate_buffer( + indexed: &mut IntegerIndexed, + length: usize, + context: &mut Context, + ) -> JsResult<()> { + // 1. Assert: O.[[ViewedArrayBuffer]] is undefined. + assert!(indexed.viewed_array_buffer().is_none()); + + // 2. Let constructorName be the String value of O.[[TypedArrayName]]. + // 3. Let elementSize be the Element Size value specified in Table 73 for constructorName. + let element_size = indexed.typed_array_name().element_size(); + + // 4. Let byteLength be elementSize × length. + let byte_length = element_size * length; + + // 5. Let data be ? AllocateArrayBuffer(%ArrayBuffer%, byteLength). + let data = ArrayBuffer::allocate( + &context + .intrinsics() + .constructors() + .array_buffer() + .constructor() + .into(), + byte_length, + context, + )?; + + // 6. Set O.[[ViewedArrayBuffer]] to data. + indexed.set_viewed_array_buffer(Some(data)); + // 7. Set O.[[ByteLength]] to byteLength. + indexed.set_byte_length(byte_length); + // 8. Set O.[[ByteOffset]] to 0. + indexed.set_byte_offset(0); + // 9. Set O.[[ArrayLength]] to length. + indexed.set_array_length(length); + + // 10. Return O. + Ok(()) + } + + /// + pub(crate) fn initialize_from_list( + o: &JsObject, + values: Vec, + context: &mut Context, + ) -> JsResult<()> { + // 1. Let len be the number of elements in values. + let len = values.len(); + { + let mut o = o.borrow_mut(); + let o_inner = o.as_typed_array_mut().expect("expected a TypedArray"); + + // 2. Perform ? AllocateTypedArrayBuffer(O, len). + Self::allocate_buffer(o_inner, len, context)?; + } + + // 3. Let k be 0. + // 4. Repeat, while k < len, + for (k, k_value) in values.into_iter().enumerate() { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be the first element of values and remove that element from values. + // c. Perform ? Set(O, Pk, kValue, true). + o.set(k, k_value, true, context)?; + // d. Set k to k + 1. + } + + // 5. Assert: values is now an empty List. + // It no longer exists. + Ok(()) + } + + /// `AllocateTypedArray ( constructorName, newTarget, defaultProto [ , length ] )` + /// + /// It is used to validate and create an instance of a `TypedArray` constructor. If the `length` + /// argument is passed, an `ArrayBuffer` of that length is also allocated and associated with the + /// new `TypedArray` instance. `AllocateTypedArray` provides common semantics that is used by + /// `TypedArray`. + /// + /// For more information, check the [spec][spec]. + /// + /// [spec]: https://tc39.es/ecma262/#sec-allocatetypedarray + fn allocate

( + constructor_name: TypedArrayKind, + new_target: &JsValue, + default_proto: P, + length: Option, + context: &mut Context, + ) -> JsResult + where + P: FnOnce(&StandardConstructors) -> &StandardConstructor, + { + // 1. Let proto be ? GetPrototypeFromConstructor(newTarget, defaultProto). + let proto = get_prototype_from_constructor(new_target, default_proto, context)?; + + // 3. Assert: obj.[[ViewedArrayBuffer]] is undefined. + // 4. Set obj.[[TypedArrayName]] to constructorName. + // 5. If constructorName is "BigInt64Array" or "BigUint64Array", set obj.[[ContentType]] to BigInt. + // 6. Otherwise, set obj.[[ContentType]] to Number. + // 7. If length is not present, then + // a. Set obj.[[ByteLength]] to 0. + // b. Set obj.[[ByteOffset]] to 0. + // c. Set obj.[[ArrayLength]] to 0. + let mut indexed = IntegerIndexed::new(None, constructor_name, 0, 0, 0); + + // 8. Else, + if let Some(length) = length { + // a. Perform ? AllocateTypedArrayBuffer(obj, length). + Self::allocate_buffer(&mut indexed, length, context)?; + } + + // 2. Let obj be ! IntegerIndexedObjectCreate(proto). + let obj = IntegerIndexed::create(proto, indexed, context); + + // 9. Return obj. + Ok(obj) + } + + /// `23.2.5.1.2 InitializeTypedArrayFromTypedArray ( O, srcArray )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-initializetypedarrayfromtypedarray + fn initialize_from_typed_array( + o: &JsObject, + src_array: &JsObject, + context: &mut Context, + ) -> JsResult<()> { + let o_obj = o.borrow(); + let src_array_obj = src_array.borrow(); + let o_array = o_obj.as_typed_array().expect("this must be a typed array"); + let src_array = src_array_obj + .as_typed_array() + .expect("this must be a typed array"); + + // 1. Let srcData be srcArray.[[ViewedArrayBuffer]]. + // 2. If IsDetachedBuffer(srcData) is true, throw a TypeError exception. + if src_array.is_detached() { + return context.throw_type_error("Cannot initialize typed array from detached buffer"); + } + let src_data_obj = src_array + .viewed_array_buffer() + .expect("Already checked for detached buffer"); + + // 3. Let constructorName be the String value of O.[[TypedArrayName]]. + // 4. Let elementType be the Element Type value in Table 73 for constructorName. + // 10. Let elementSize be the Element Size value specified in Table 73 for constructorName. + let constructor_name = o_array.typed_array_name(); + + // 5. Let elementLength be srcArray.[[ArrayLength]]. + let element_length = src_array.array_length(); + + // 6. Let srcName be the String value of srcArray.[[TypedArrayName]]. + // 7. Let srcType be the Element Type value in Table 73 for srcName. + // 8. Let srcElementSize be the Element Size value specified in Table 73 for srcName. + let src_name = src_array.typed_array_name(); + + // 9. Let srcByteOffset be srcArray.[[ByteOffset]]. + let src_byte_offset = src_array.byte_offset(); + + // 11. Let byteLength be elementSize × elementLength. + let byte_length = constructor_name.element_size() * element_length; + + // 12. If IsSharedArrayBuffer(srcData) is false, then + // a. Let bufferConstructor be ? SpeciesConstructor(srcData, %ArrayBuffer%). + // TODO: Shared Array Buffer + // 13. Else, + // a. Let bufferConstructor be %ArrayBuffer%. + let buffer_constructor = + src_data_obj.species_constructor(StandardConstructors::array_buffer, context)?; + + let src_data_obj_b = src_data_obj.borrow(); + let src_data = src_data_obj_b + .as_array_buffer() + .expect("Already checked for detached buffer"); + + // 14. If elementType is the same as srcType, then + let data = if constructor_name == src_name { + // a. Let data be ? CloneArrayBuffer(srcData, srcByteOffset, byteLength, bufferConstructor). + src_data.clone_array_buffer( + src_byte_offset, + byte_length, + &buffer_constructor.into(), + context, + )? + // 15. Else, + } else { + // a. Let data be ? AllocateArrayBuffer(bufferConstructor, byteLength). + let data_obj = ArrayBuffer::allocate(&buffer_constructor.into(), byte_length, context)?; + let mut data_obj_b = data_obj.borrow_mut(); + let data = data_obj_b + .as_array_buffer_mut() + .expect("Must be ArrayBuffer"); + + // b. If IsDetachedBuffer(srcData) is true, throw a TypeError exception. + if src_data.is_detached_buffer() { + return context + .throw_type_error("Cannot initialize typed array from detached buffer"); + } + + // c. If srcArray.[[ContentType]] ≠ O.[[ContentType]], throw a TypeError exception. + if src_name.content_type() != constructor_name.content_type() { + return context + .throw_type_error("Cannot initialize typed array from different content type"); + } + + // d. Let srcByteIndex be srcByteOffset. + let mut src_byte_index = src_byte_offset; + // e. Let targetByteIndex be 0. + let mut target_byte_index = 0; + // f. Let count be elementLength. + let mut count = element_length; + // g. Repeat, while count > 0, + while count > 0 { + // i. Let value be GetValueFromBuffer(srcData, srcByteIndex, srcType, true, Unordered). + let value = src_data.get_value_from_buffer( + src_byte_index, + src_name, + true, + SharedMemoryOrder::Unordered, + None, + ); + + // ii. Perform SetValueInBuffer(data, targetByteIndex, elementType, value, true, Unordered). + data.set_value_in_buffer( + target_byte_index, + constructor_name, + &value, + SharedMemoryOrder::Unordered, + None, + context, + )?; + + // iii. Set srcByteIndex to srcByteIndex + srcElementSize. + src_byte_index += src_name.element_size(); + + // iv. Set targetByteIndex to targetByteIndex + elementSize. + target_byte_index += constructor_name.element_size(); + + // v. Set count to count - 1. + count -= 1; + } + drop(data_obj_b); + data_obj + }; + + // 16. Set O.[[ViewedArrayBuffer]] to data. + // 17. Set O.[[ByteLength]] to byteLength. + // 18. Set O.[[ByteOffset]] to 0. + // 19. Set O.[[ArrayLength]] to elementLength. + drop(o_obj); + o.borrow_mut().data = ObjectData::integer_indexed(IntegerIndexed::new( + Some(data), + constructor_name, + 0, + byte_length, + element_length, + )); + + Ok(()) + } + + /// `23.2.5.1.3 InitializeTypedArrayFromArrayBuffer ( O, buffer, byteOffset, length )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-initializetypedarrayfromarraybuffer + fn initialize_from_array_buffer( + o: &JsObject, + buffer: JsObject, + byte_offset: &JsValue, + length: &JsValue, + context: &mut Context, + ) -> JsResult<()> { + // 1. Let constructorName be the String value of O.[[TypedArrayName]]. + // 2. Let elementSize be the Element Size value specified in Table 73 for constructorName. + let constructor_name = o + .borrow() + .as_typed_array() + .expect("This must be a typed array") + .typed_array_name(); + + // 3. Let offset be ? ToIndex(byteOffset). + let offset = byte_offset.to_index(context)?; + + // 4. If offset modulo elementSize ≠ 0, throw a RangeError exception. + if offset % constructor_name.element_size() != 0 { + return context.throw_range_error("Invalid length for typed array"); + } + + let buffer_byte_length = { + let buffer_obj_b = buffer.borrow(); + let buffer_array = buffer_obj_b + .as_array_buffer() + .expect("This must be an ArrayBuffer"); + + // 6. If IsDetachedBuffer(buffer) is true, throw a TypeError exception. + if buffer_array.is_detached_buffer() { + return context + .throw_type_error("Cannot construct typed array from detached buffer"); + } + + // 7. Let bufferByteLength be buffer.[[ArrayBufferByteLength]]. + buffer_array.array_buffer_byte_length() + }; + + // 8. If length is undefined, then + let new_byte_length = if length.is_undefined() { + // a. If bufferByteLength modulo elementSize ≠ 0, throw a RangeError exception. + if buffer_byte_length % constructor_name.element_size() != 0 { + return context.throw_range_error("Invalid length for typed array"); + } + + // b. Let newByteLength be bufferByteLength - offset. + let new_byte_length = buffer_byte_length as isize - offset as isize; + + // c. If newByteLength < 0, throw a RangeError exception. + if new_byte_length < 0 { + return context.throw_range_error("Invalid length for typed array"); + } + + new_byte_length as usize + // 9. Else, + } else { + // 5. If length is not undefined, then + // a. Let newLength be ? ToIndex(length). + + // a. Let newByteLength be newLength × elementSize. + let new_byte_length = length.to_index(context)? * constructor_name.element_size(); + + // b. If offset + newByteLength > bufferByteLength, throw a RangeError exception. + if offset + new_byte_length > buffer_byte_length { + return context.throw_range_error("Invalid length for typed array"); + } + + new_byte_length + }; + + let mut o_obj_borrow = o.borrow_mut(); + let o = o_obj_borrow + .as_typed_array_mut() + .expect("This must be an ArrayBuffer"); + + // 10. Set O.[[ViewedArrayBuffer]] to buffer. + o.set_viewed_array_buffer(Some(buffer)); + // 11. Set O.[[ByteLength]] to newByteLength. + o.set_byte_length(new_byte_length); + // 12. Set O.[[ByteOffset]] to offset. + o.set_byte_offset(offset); + // 13. Set O.[[ArrayLength]] to newByteLength / elementSize. + o.set_array_length(new_byte_length / constructor_name.element_size()); + + Ok(()) + } + + /// `23.2.5.1.5 InitializeTypedArrayFromArrayLike ( O, arrayLike )` + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/ecma262/#sec-initializetypedarrayfromarraylike + fn initialize_from_array_like( + o: &JsObject, + array_like: &JsObject, + context: &mut Context, + ) -> JsResult<()> { + // 1. Let len be ? LengthOfArrayLike(arrayLike). + let len = array_like.length_of_array_like(context)?; + + // 2. Perform ? AllocateTypedArrayBuffer(O, len). + { + let mut o_borrow = o.borrow_mut(); + let o = o_borrow.as_typed_array_mut().expect("Must be typed array"); + Self::allocate_buffer(o, len, context)?; + } + + // 3. Let k be 0. + // 4. Repeat, while k < len, + for k in 0..len { + // a. Let Pk be ! ToString(𝔽(k)). + // b. Let kValue be ? Get(arrayLike, Pk). + let k_value = array_like.get(k, context)?; + + // c. Perform ? Set(O, Pk, kValue, true). + o.set(k, k_value, true, context)?; + } + + Ok(()) + } +} + +/// Names of all the typed arrays. +#[derive(Debug, Clone, Copy, Finalize, PartialEq)] +pub(crate) enum TypedArrayKind { + Int8, + Uint8, + Uint8Clamped, + Int16, + Uint16, + Int32, + Uint32, + BigInt64, + BigUint64, + Float32, + Float64, +} + +unsafe impl Trace for TypedArrayKind { + // Safe because `TypedArrayName` is `Copy` + unsafe_empty_trace!(); +} + +impl TypedArrayKind { + /// Gets the element size of the given typed array name, as per the [spec]. + /// + /// [spec]: https://tc39.es/ecma262/#table-the-typedarray-constructors + #[inline] + pub(crate) const fn element_size(self) -> usize { + match self { + Self::Int8 | Self::Uint8 | Self::Uint8Clamped => 1, + Self::Int16 | Self::Uint16 => 2, + Self::Int32 | Self::Uint32 | Self::Float32 => 4, + Self::BigInt64 | Self::BigUint64 | Self::Float64 => 8, + } + } + + /// Gets the content type of this typed array name. + #[inline] + pub(crate) const fn content_type(self) -> ContentType { + match self { + Self::BigInt64 | Self::BigUint64 => ContentType::BigInt, + _ => ContentType::Number, + } + } + + /// Gets the name of this typed array name. + #[inline] + pub(crate) const fn name(&self) -> &str { + match self { + Self::Int8 => "Int8Array", + Self::Uint8 => "Uint8Array", + Self::Uint8Clamped => "Uint8ClampedArray", + Self::Int16 => "Int16Array", + Self::Uint16 => "Uint16Array", + Self::Int32 => "Int32Array", + Self::Uint32 => "Uint32Array", + Self::BigInt64 => "BigInt64Array", + Self::BigUint64 => "BigUint64Array", + Self::Float32 => "Float32Array", + Self::Float64 => "Float64Array", + } + } + + pub(crate) fn is_big_int_element_type(self) -> bool { + matches!(self, Self::BigUint64 | Self::BigInt64) + } +} + +typed_array!(Int8Array, Int8, "Int8Array", typed_int8_array); +typed_array!(Uint8Array, Uint8, "Uint8Array", typed_uint8_array); +typed_array!( + Uint8ClampedArray, + Uint8Clamped, + "Uint8ClampedArray", + typed_uint8clamped_array +); +typed_array!(Int16Array, Int16, "Int16Array", typed_int16_array); +typed_array!(Uint16Array, Uint16, "Uint16Array", typed_uint16_array); +typed_array!(Int32Array, Int32, "Int32Array", typed_int32_array); +typed_array!(Uint32Array, Uint32, "Uint32Array", typed_uint32_array); +typed_array!( + BigInt64Array, + BigInt64, + "BigInt64Array", + typed_bigint64_array +); +typed_array!( + BigUint64Array, + BigUint64, + "BigUint64Array", + typed_biguint64_array +); +typed_array!(Float32Array, Float32, "Float32Array", typed_float32_array); +typed_array!(Float64Array, Float64, "Float64Array", typed_float64_array); diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/mod.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/mod.rs new file mode 100644 index 0000000..9b3e329 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/mod.rs @@ -0,0 +1,34 @@ +//! This module implements the global `undefined` property. +//! +//! The global undefined property represents the primitive value undefined. +//! +//! More information: +//! - [MDN documentation][mdn] +//! - [ECMAScript reference][spec] +//! +//! [spec]: https://tc39.es/ecma262/#sec-undefined +//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/undefined + +use crate::{builtins::BuiltIn, property::Attribute, Context, JsValue}; +use boa_profiler::Profiler; + +#[cfg(test)] +mod tests; + +/// JavaScript global `undefined` property. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub(crate) struct Undefined; + +impl BuiltIn for Undefined { + const NAME: &'static str = "undefined"; + + const ATTRIBUTE: Attribute = Attribute::READONLY + .union(Attribute::NON_ENUMERABLE) + .union(Attribute::PERMANENT); + + fn init(_: &mut Context) -> Option { + let _timer = Profiler::global().start_event(Self::NAME, "init"); + + Some(JsValue::undefined()) + } +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/tests.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/tests.rs new file mode 100644 index 0000000..39e7cee --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/builtins/undefined/tests.rs @@ -0,0 +1,18 @@ +use crate::exec; + +#[test] +fn undefined_direct_evaluation() { + let scenario = r#" + undefined; + "#; + assert_eq!(&exec(scenario), "undefined"); +} + +#[test] +fn undefined_assignment() { + let scenario = r#" + a = undefined; + a + "#; + assert_eq!(&exec(scenario), "undefined"); +} diff --git a/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bytecompiler.rs b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bytecompiler.rs new file mode 100644 index 0000000..c5c34f3 --- /dev/null +++ b/__wasm/wit-bindgen-sample/engine/boa/boa_engine/src/bytecompiler.rs @@ -0,0 +1,3122 @@ +use crate::{ + builtins::function::ThisMode, + environments::{BindingLocator, CompileTimeEnvironment}, + syntax::ast::{ + node::{ + declaration::{ + class_decl::ClassElement, BindingPatternTypeArray, BindingPatternTypeObject, + DeclarationPattern, + }, + iteration::IterableLoopInitializer, + object::{MethodDefinition, PropertyDefinition, PropertyName}, + operator::assign::AssignTarget, + template::TemplateElement, + Class, Declaration, FormalParameterList, GetConstField, GetField, GetSuperField, + StatementList, + }, + op::{AssignOp, BinOp, BitOp, CompOp, LogOp, NumOp, UnaryOp}, + Const, Node, + }, + vm::{BindingOpcode, CodeBlock, Opcode}, + Context, JsBigInt, JsResult, JsString, JsValue, +}; +use boa_gc::Gc; +use boa_interner::{Interner, Sym}; +use rustc_hash::FxHashMap; +use std::mem::size_of; + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum Literal { + String(JsString), + BigInt(JsBigInt), +} + +#[must_use] +#[derive(Debug, Clone, Copy)] +struct Label { + index: u32, +} + +#[derive(Debug, Clone)] +struct JumpControlInfo { + label: Option, + start_address: u32, + kind: JumpControlInfoKind, + breaks: Vec