diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b7689fb3299..753a57ebe20 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -40,7 +40,7 @@ jobs: with: key: tarpaulin - name: Install cargo-tarpaulin - uses: baptiste0928/cargo-install@v3.1.1 + uses: baptiste0928/cargo-install@v3.3.0 with: crate: cargo-tarpaulin diff --git a/.github/workflows/webassembly.yml b/.github/workflows/webassembly.yml index b9fb489ff7a..d3974e02a12 100644 --- a/.github/workflows/webassembly.yml +++ b/.github/workflows/webassembly.yml @@ -45,7 +45,7 @@ jobs: - uses: Swatinem/rust-cache@v2 - name: Install wasm-pack - uses: baptiste0928/cargo-install@v3.1.1 + uses: baptiste0928/cargo-install@v3.3.0 with: crate: wasm-pack - name: Build Playground diff --git a/Cargo.lock b/Cargo.lock index e9b84e2a181..0f9ec3cb33b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -161,7 +161,7 @@ dependencies = [ "async-task", "concurrent-queue", "fastrand 2.3.0", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "slab", ] @@ -173,7 +173,7 @@ checksum = "ebcd09b382f40fcd159c2d695175b2ae620ffa5f3bd6f664131efff4e8b9e04a" dependencies = [ "async-lock", "blocking", - "futures-lite 2.5.0", + "futures-lite 2.6.0", ] [[package]] @@ -186,7 +186,7 @@ dependencies = [ "cfg-if", "concurrent-queue", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "parking", "polling 3.7.4", "rustix", @@ -214,7 +214,7 @@ checksum = "b948000fad4873c1c9339d60f2623323a0cfd3816e5181033c6a5cb68b2accf7" dependencies = [ "async-io", "blocking", - "futures-lite 2.5.0", + "futures-lite 2.6.0", ] [[package]] @@ -231,7 +231,7 @@ dependencies = [ "blocking", "cfg-if", "event-listener 5.3.1", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "rustix", "tracing", ] @@ -301,9 +301,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.6.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36" dependencies = [ "serde", ] @@ -317,7 +317,7 @@ dependencies = [ "async-channel 2.3.1", "async-task", "futures-io", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "piper", ] @@ -326,7 +326,7 @@ name = "boa_ast" version = "0.20.0" dependencies = [ "arbitrary", - "bitflags 2.6.0", + "bitflags 2.8.0", "boa_interner", "boa_macros", "boa_string", @@ -347,6 +347,7 @@ dependencies = [ "clap", "color-eyre", "colored", + "cow-utils", "dhat", "jemallocator", "phf", @@ -361,7 +362,7 @@ name = "boa_engine" version = "0.20.0" dependencies = [ "arrayvec", - "bitflags 2.6.0", + "bitflags 2.8.0", "boa_ast", "boa_gc", "boa_icu_provider", @@ -379,7 +380,7 @@ dependencies = [ "fast-float2", "fixed_decimal", "float-cmp", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "hashbrown 0.15.2", "icu_calendar 1.5.2", "icu_casemap", @@ -439,7 +440,7 @@ dependencies = [ "boa_parser", "boa_runtime", "futures-concurrency", - "futures-lite 2.5.0", + "futures-lite 2.6.0", "isahc", "smol", "time", @@ -508,6 +509,7 @@ dependencies = [ name = "boa_macros" version = "0.20.0" dependencies = [ + "cow-utils", "proc-macro2", "quote", "syn", @@ -526,7 +528,7 @@ dependencies = [ name = "boa_parser" version = "0.20.0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "boa_ast", "boa_interner", "boa_macros", @@ -578,7 +580,7 @@ dependencies = [ name = "boa_tester" version = "0.20.0" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "boa_engine", "boa_gc", "boa_runtime", @@ -587,6 +589,7 @@ dependencies = [ "color-eyre", "colored", "comfy-table", + "cow-utils", "phf", "rayon", "rustc-hash 2.1.0", @@ -761,9 +764,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3135e7ec2ef7b10c6ed8950f0f792ed96ee093fa088608f1c76e569722700c84" +checksum = "a8eb5e908ef3a6efbe1ed62520fb7287959888c88485abe072543190ecc66783" dependencies = [ "clap_builder", "clap_derive", @@ -771,9 +774,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.23" +version = "4.5.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30582fc632330df2bd26877bde0c1f4470d57c582bbc070376afcd04d8cb4838" +checksum = "96b01801b5fc6a0a232407abc821660c9c6d25a1cafc0d4f85f29fb8d9afc121" dependencies = [ "anstream", "anstyle", @@ -783,9 +786,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.18" +version = "4.5.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ac6a0c7b1a9e9a5186361f67dfa1b88213572f427fb9ab038efb2bd8c582dab" +checksum = "54b755194d6389280185988721fffba69495eed5ee9feeee9a599b53db80318c" dependencies = [ "heck", "proc-macro2", @@ -1020,7 +1023,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "crossterm_winapi", "parking_lot", "rustix", @@ -1428,9 +1431,9 @@ dependencies = [ [[package]] name = "futures-lite" -version = "2.5.0" +version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cef40d21ae2c515b51041df9ed313ed21e572df340ea58a922a0aefe7e8891a1" +checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ "fastrand 2.3.0", "futures-core", @@ -2175,9 +2178,9 @@ checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] name = "indexmap" -version = "2.7.0" +version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" dependencies = [ "equivalent", "hashbrown 0.15.2", @@ -2284,8 +2287,7 @@ checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" [[package]] name = "ixdtf" version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb6cd1080e64f68b07c577e3c687f4a894b3d1bd6093cb36b55c7bd07675aa3a" +source = "git+https://github.com/unicode-org/icu4x.git?rev=3d187da4d3f05b7e37603c4be3f2c1ce45100e03#3d187da4d3f05b7e37603c4be3f2c1ce45100e03" dependencies = [ "displaydoc", "utf8_iter", @@ -2394,9 +2396,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.22" +version = "0.4.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +checksum = "04cbf5b083de1c7e0222a7a51dbfdba1cbe1c6ab0b15e29fff3f6c077fd9cd9f" [[package]] name = "loom" @@ -2527,7 +2529,7 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "cfg_aliases", "libc", @@ -2948,9 +2950,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.92" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -3026,7 +3028,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", ] [[package]] @@ -3132,7 +3134,7 @@ version = "0.38.42" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f93dc38ecbab2eb790ff964bb77fa94faf256fd3e73285fd7ba0903b76bedb85" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "errno", "libc", "linux-raw-sys", @@ -3183,7 +3185,7 @@ version = "15.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2ee1e066dc922e513bda599c6ccb5f3bb2b0ea5870a579448f2622993f0a9a2f" dependencies = [ - "bitflags 2.6.0", + "bitflags 2.8.0", "cfg-if", "clipboard-win", "fd-lock", @@ -3301,9 +3303,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.135" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b0d7ba2887406110130a978386c4e1befb98c674b4fba677954e4db976630d9" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -3435,7 +3437,7 @@ dependencies = [ "async-net", "async-process", "blocking", - "futures-lite 2.5.0", + "futures-lite 2.6.0", ] [[package]] @@ -3505,9 +3507,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "syn" -version = "2.0.95" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46f71c0377baf4ef1cc3e3402ded576dccc315800fbc62dfc7fe04b009773b4a" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", @@ -3549,16 +3551,14 @@ checksum = "42a4d50cdb458045afc8131fd91b64904da29548bcb63c7236e0844936c13078" [[package]] name = "temporal_rs" version = "0.0.4" -source = "git+https://github.com/boa-dev/temporal.git?rev=436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec#436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec" +source = "git+https://github.com/boa-dev/temporal.git?rev=cb10eecbd68a5249f5f60f08ba9e09d2a24040a9#cb10eecbd68a5249f5f60f08ba9e09d2a24040a9" dependencies = [ - "bitflags 2.6.0", "combine", "iana-time-zone", "icu_calendar 2.0.0-beta1", "ixdtf", "jiff-tzdb", "num-traits", - "rustc-hash 2.1.0", "tinystr 0.8.0", "tzif", "web-time", @@ -3626,18 +3626,18 @@ checksum = "a38c90d48152c236a3ab59271da4f4ae63d678c5d7ad6b7714d7cb9760be5e4b" [[package]] name = "thiserror" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.9" +version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" dependencies = [ "proc-macro2", "quote", @@ -3728,9 +3728,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.42.0" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cec9b21b0450273377fc97bd4c33a8acffc8c996c987a7c5b319a0083707551" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "pin-project-lite", @@ -3739,9 +3739,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", @@ -3875,9 +3875,9 @@ dependencies = [ [[package]] name = "trybuild" -version = "1.0.101" +version = "1.0.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dcd332a5496c026f1e14b7f3d2b7bd98e509660c04239c58b0ba38a12daded4" +checksum = "9f14b5c02a137632f68194ec657ecb92304138948e8957c932127eb1b58c23be" dependencies = [ "glob", "serde", diff --git a/Cargo.toml b/Cargo.toml index fd34e166fd5..9de6f0f0f67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,13 +47,13 @@ boa_string = { version = "~0.20.0", path = "core/string" } # Shared deps arbitrary = "1" -bitflags = "2.5.0" -clap = "4.5.23" +bitflags = "2.8.0" +clap = "4.5.26" colored = "2.2.0" cow-utils = "0.1.3" fast-float2 = "0.2.3" hashbrown = "0.15.2" -indexmap = { version = "2.7.0", default-features = false } +indexmap = { version = "2.7.1", default-features = false } indoc = "2.0.5" itoa = "1.0.14" jemallocator = "0.5.4" @@ -65,17 +65,17 @@ pollster = "0.4.0" regex = "1.11.1" regress = { version = "0.10.1", features = ["utf16"] } rustc-hash = { version = "2.1.0", default-features = false } -serde_json = "1.0.134" +serde_json = "1.0.137" serde = "1.0.217" static_assertions = "1.1.0" textwrap = "0.16.0" thin-vec = "0.2.13" time = { version = "0.3.37", default-features = false, features = ["local-offset", "large-dates", "wasm-bindgen", "parsing", "formatting", "macros"] } tinystr = "0.7.5" -log = "0.4.22" +log = "0.4.25" simple_logger = "5.0.0" cargo_metadata = "0.19.1" -trybuild = "1.0.101" +trybuild = "1.0.102" rayon = "1.10.0" toml = "0.8.19" color-eyre = "0.6.3" @@ -91,7 +91,7 @@ isahc = "1.7.2" rustyline = { version = "15.0.0", default-features = false } dhat = "0.3.3" quote = "1.0.38" -syn = { version = "2.0.95", default-features = false } +syn = { version = "2.0.96", default-features = false } proc-macro2 = "1.0" synstructure = "0.13" measureme = "12.0.0" @@ -102,7 +102,7 @@ rand = "0.8.5" num-integer = "0.1.46" ryu-js = "1.0.1" tap = "1.0.1" -thiserror = { version = "2.0.9", default-features = false } +thiserror = { version = "2.0.11", default-features = false } dashmap = "6.1.0" num_enum = "0.7.3" itertools = { version = "0.13.0", default-features = false } @@ -113,14 +113,14 @@ intrusive-collections = "0.9.7" cfg-if = "1.0.0" either = "1.13.0" sys-locale = "0.3.2" -temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "436b07d9b27e3e2274905c9a4eabf8bbff9ad9ec", features = ["tzdb"] } +temporal_rs = { git = "https://github.com/boa-dev/temporal.git", rev = "cb10eecbd68a5249f5f60f08ba9e09d2a24040a9", features = ["tzdb"] } web-time = "1.1.0" criterion = "0.5.1" float-cmp = "0.10.0" -futures-lite = "2.5.0" +futures-lite = "2.6.0" test-case = "3.3.1" url = "2.5.4" -tokio = { version = "1.42.0", default-features = false } +tokio = { version = "1.43.0", default-features = false } futures-concurrency = "7.6.2" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index d4d5bf6c0be..ed4987adddb 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -25,6 +25,7 @@ phf = { workspace = true, features = ["macros"] } pollster.workspace = true dhat = { workspace = true, optional = true } color-eyre.workspace = true +cow-utils.workspace = true [features] default = ["boa_engine/annex-b", "boa_engine/experimental", "boa_engine/intl_bundled"] diff --git a/cli/src/debug/function.rs b/cli/src/debug/function.rs index 8337ba708b7..8edc167d16b 100644 --- a/cli/src/debug/function.rs +++ b/cli/src/debug/function.rs @@ -5,6 +5,7 @@ use boa_engine::{ vm::flowgraph::{Direction, Graph}, Context, JsArgs, JsNativeError, JsObject, JsResult, JsValue, NativeFunction, }; +use cow_utils::CowUtils; use crate::FlowgraphFormat; @@ -14,7 +15,7 @@ fn flowgraph_parse_format_option(value: &JsValue) -> JsResult { } if let Some(string) = value.as_string() { - return match string.to_std_string_escaped().to_lowercase().as_str() { + return match string.to_std_string_escaped().cow_to_lowercase().as_ref() { "mermaid" => Ok(FlowgraphFormat::Mermaid), "graphviz" => Ok(FlowgraphFormat::Graphviz), format => Err(JsNativeError::typ() @@ -34,7 +35,7 @@ fn flowgraph_parse_direction_option(value: &JsValue) -> JsResult { } if let Some(string) = value.as_string() { - return match string.to_std_string_escaped().to_lowercase().as_str() { + return match string.to_std_string_escaped().cow_to_lowercase().as_ref() { "leftright" | "lr" => Ok(Direction::LeftToRight), "rightleft" | "rl" => Ok(Direction::RightToLeft), "topbottom" | "tb" => Ok(Direction::TopToBottom), diff --git a/clippy.toml b/clippy.toml index 59ef99e1fe1..5b248b4ae4b 100644 --- a/clippy.toml +++ b/clippy.toml @@ -1,5 +1,10 @@ doc-valid-idents = ['ECMAScript', 'JavaScript', 'SpiderMonkey', 'GitHub'] allow-print-in-tests = true disallowed-methods = [ + { path = "str::to_ascii_lowercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_ascii_lowercase` instead." }, { path = "str::to_ascii_uppercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_ascii_uppercase` instead." }, + { path = "str::to_lowercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_lowercase` instead." }, + { path = "str::to_uppercase", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_to_uppercase` instead." }, + { path = "str::replace", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replace` instead." }, + { path = "str::replacen", reason = "To avoid memory allocation, use `cow_utils::CowUtils::cow_replacen` instead." }, ] diff --git a/core/ast/src/function/class.rs b/core/ast/src/function/class.rs index 1d554ae6117..587f0668b7f 100644 --- a/core/ast/src/function/class.rs +++ b/core/ast/src/function/class.rs @@ -389,7 +389,7 @@ pub enum ClassElement { /// A private static field definition, only accessible from static methods and fields inside the /// class declaration. - PrivateStaticFieldDefinition(PrivateName, Option), + PrivateStaticFieldDefinition(PrivateFieldDefinition), /// A static block, where a class can have initialization logic for its static fields. StaticBlock(StaticBlockBody), @@ -406,7 +406,7 @@ pub enum ClassElement { #[derive(Clone, Debug, PartialEq)] pub struct ClassFieldDefinition { pub(crate) name: PropertyName, - pub(crate) field: Option, + pub(crate) initializer: Option, #[cfg_attr(feature = "serde", serde(skip))] pub(crate) scope: Scope, @@ -416,10 +416,10 @@ impl ClassFieldDefinition { /// Creates a new class field definition. #[inline] #[must_use] - pub fn new(name: PropertyName, field: Option) -> Self { + pub fn new(name: PropertyName, initializer: Option) -> Self { Self { name, - field, + initializer, scope: Scope::default(), } } @@ -431,11 +431,11 @@ impl ClassFieldDefinition { &self.name } - /// Returns the field of the class field definition. + /// Returns the initializer of the class field definition. #[inline] #[must_use] - pub const fn field(&self) -> Option<&Expression> { - self.field.as_ref() + pub const fn initializer(&self) -> Option<&Expression> { + self.initializer.as_ref() } /// Returns the scope of the class field definition. @@ -457,7 +457,7 @@ impl ClassFieldDefinition { #[derive(Clone, Debug, PartialEq)] pub struct PrivateFieldDefinition { pub(crate) name: PrivateName, - pub(crate) field: Option, + pub(crate) initializer: Option, #[cfg_attr(feature = "serde", serde(skip))] pub(crate) scope: Scope, @@ -467,10 +467,10 @@ impl PrivateFieldDefinition { /// Creates a new private field definition. #[inline] #[must_use] - pub fn new(name: PrivateName, field: Option) -> Self { + pub fn new(name: PrivateName, initializer: Option) -> Self { Self { name, - field, + initializer, scope: Scope::default(), } } @@ -482,11 +482,11 @@ impl PrivateFieldDefinition { &self.name } - /// Returns the field of the private field definition. + /// Returns the initializer of the private field definition. #[inline] #[must_use] - pub const fn field(&self) -> Option<&Expression> { - self.field.as_ref() + pub const fn initializer(&self) -> Option<&Expression> { + self.initializer.as_ref() } /// Returns the scope of the private field definition. @@ -502,7 +502,7 @@ impl ToIndentedString for ClassElement { let indentation = " ".repeat(indent_n + 1); match self { Self::MethodDefinition(m) => m.to_indented_string(interner, indent_n), - Self::FieldDefinition(field) => match &field.field { + Self::FieldDefinition(field) => match &field.initializer { Some(expr) => { format!( "{indentation}{} = {};\n", @@ -517,7 +517,7 @@ impl ToIndentedString for ClassElement { ) } }, - Self::StaticFieldDefinition(field) => match &field.field { + Self::StaticFieldDefinition(field) => match &field.initializer { Some(expr) => { format!( "{indentation}static {} = {};\n", @@ -532,8 +532,9 @@ impl ToIndentedString for ClassElement { ) } }, - Self::PrivateFieldDefinition(PrivateFieldDefinition { name, field, .. }) => match field - { + Self::PrivateFieldDefinition(PrivateFieldDefinition { + name, initializer, .. + }) => match initializer { Some(expr) => { format!( "{indentation}#{} = {};\n", @@ -548,7 +549,11 @@ impl ToIndentedString for ClassElement { ) } }, - Self::PrivateStaticFieldDefinition(name, field) => match field { + Self::PrivateStaticFieldDefinition(PrivateFieldDefinition { + name, + initializer, + .. + }) => match initializer { Some(expr) => { format!( "{indentation}static #{} = {};\n", @@ -593,16 +598,22 @@ impl VisitWith for ClassElement { } Self::FieldDefinition(field) | Self::StaticFieldDefinition(field) => { visitor.visit_property_name(&field.name)?; - if let Some(expr) = &field.field { + if let Some(expr) = &field.initializer { visitor.visit_expression(expr) } else { ControlFlow::Continue(()) } } - Self::PrivateFieldDefinition(PrivateFieldDefinition { name, field, .. }) - | Self::PrivateStaticFieldDefinition(name, field) => { + Self::PrivateFieldDefinition(PrivateFieldDefinition { + name, initializer, .. + }) + | Self::PrivateStaticFieldDefinition(PrivateFieldDefinition { + name, + initializer, + .. + }) => { visitor.visit_private_name(name)?; - if let Some(expr) = field { + if let Some(expr) = initializer { visitor.visit_expression(expr) } else { ControlFlow::Continue(()) @@ -631,16 +642,22 @@ impl VisitWith for ClassElement { } Self::FieldDefinition(field) | Self::StaticFieldDefinition(field) => { visitor.visit_property_name_mut(&mut field.name)?; - if let Some(expr) = &mut field.field { + if let Some(expr) = &mut field.initializer { visitor.visit_expression_mut(expr) } else { ControlFlow::Continue(()) } } - Self::PrivateFieldDefinition(PrivateFieldDefinition { name, field, .. }) - | Self::PrivateStaticFieldDefinition(name, field) => { + Self::PrivateFieldDefinition(PrivateFieldDefinition { + name, initializer, .. + }) + | Self::PrivateStaticFieldDefinition(PrivateFieldDefinition { + name, + initializer, + .. + }) => { visitor.visit_private_name_mut(name)?; - if let Some(expr) = field { + if let Some(expr) = initializer { visitor.visit_expression_mut(expr) } else { ControlFlow::Continue(()) diff --git a/core/ast/src/operations.rs b/core/ast/src/operations.rs index 45dee229113..ca5802e598a 100644 --- a/core/ast/src/operations.rs +++ b/core/ast/src/operations.rs @@ -1180,7 +1180,9 @@ impl<'ast> Visitor<'ast> for AllPrivateIdentifiersValidVisitor { } } ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { name, .. }) - | ClassElement::PrivateStaticFieldDefinition(name, _) => { + | ClassElement::PrivateStaticFieldDefinition(PrivateFieldDefinition { + name, .. + }) => { names.push(name.description()); } _ => {} @@ -1205,13 +1207,19 @@ impl<'ast> Visitor<'ast> for AllPrivateIdentifiersValidVisitor { ClassElement::FieldDefinition(field) | ClassElement::StaticFieldDefinition(field) => { visitor.visit(&field.name)?; - if let Some(expression) = &field.field { + if let Some(expression) = &field.initializer { visitor.visit(expression)?; } } - ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { field, .. }) - | ClassElement::PrivateStaticFieldDefinition(_, field) => { - if let Some(expression) = field { + ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { + initializer, + .. + }) + | ClassElement::PrivateStaticFieldDefinition(PrivateFieldDefinition { + initializer, + .. + }) => { + if let Some(expression) = initializer { visitor.visit(expression)?; } } @@ -1241,7 +1249,9 @@ impl<'ast> Visitor<'ast> for AllPrivateIdentifiersValidVisitor { } } ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { name, .. }) - | ClassElement::PrivateStaticFieldDefinition(name, _) => { + | ClassElement::PrivateStaticFieldDefinition(PrivateFieldDefinition { + name, .. + }) => { names.push(name.description()); } _ => {} @@ -1266,13 +1276,19 @@ impl<'ast> Visitor<'ast> for AllPrivateIdentifiersValidVisitor { ClassElement::FieldDefinition(field) | ClassElement::StaticFieldDefinition(field) => { visitor.visit(&field.name)?; - if let Some(expression) = &field.field { + if let Some(expression) = &field.initializer { visitor.visit(expression)?; } } - ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { field, .. }) - | ClassElement::PrivateStaticFieldDefinition(_, field) => { - if let Some(expression) = field { + ClassElement::PrivateFieldDefinition(PrivateFieldDefinition { + initializer, + .. + }) + | ClassElement::PrivateStaticFieldDefinition(PrivateFieldDefinition { + initializer, + .. + }) => { + if let Some(expression) = initializer { visitor.visit(expression)?; } } diff --git a/core/ast/src/scope_analyzer.rs b/core/ast/src/scope_analyzer.rs index 354c7dc0d4d..5e378fe6117 100644 --- a/core/ast/src/scope_analyzer.rs +++ b/core/ast/src/scope_analyzer.rs @@ -434,19 +434,14 @@ impl<'ast> VisitorMut<'ast> for BindingEscapeAnalyzer<'_> { ), ClassElement::FieldDefinition(field) | ClassElement::StaticFieldDefinition(field) => { self.visit_property_name_mut(&mut field.name)?; - if let Some(e) = &mut field.field { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } ControlFlow::Continue(()) } - ClassElement::PrivateFieldDefinition(field) => { - if let Some(e) = &mut field.field { - self.visit_expression_mut(e)?; - } - ControlFlow::Continue(()) - } - ClassElement::PrivateStaticFieldDefinition(_, e) => { - if let Some(e) = e { + ClassElement::PrivateFieldDefinition(field) + | ClassElement::PrivateStaticFieldDefinition(field) => { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } ControlFlow::Continue(()) @@ -816,29 +811,24 @@ impl<'ast> VisitorMut<'ast> for BindingCollectorVisitor<'_> { self.visit_property_name_mut(&mut field.name)?; let mut scope = Scope::new(self.scope.clone(), true); std::mem::swap(&mut self.scope, &mut scope); - if let Some(e) = &mut field.field { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } std::mem::swap(&mut self.scope, &mut scope); field.scope = scope; ControlFlow::Continue(()) } - ClassElement::PrivateFieldDefinition(field) => { + ClassElement::PrivateFieldDefinition(field) + | ClassElement::PrivateStaticFieldDefinition(field) => { let mut scope = Scope::new(self.scope.clone(), true); std::mem::swap(&mut self.scope, &mut scope); - if let Some(e) = &mut field.field { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } std::mem::swap(&mut self.scope, &mut scope); field.scope = scope; ControlFlow::Continue(()) } - ClassElement::PrivateStaticFieldDefinition(_, e) => { - if let Some(e) = e { - self.visit_expression_mut(e)?; - } - ControlFlow::Continue(()) - } ClassElement::StaticBlock(node) => { let strict = node.body.strict(); self.visit_function_like( @@ -1423,28 +1413,23 @@ impl<'ast> VisitorMut<'ast> for ScopeIndexVisitor { let index = self.index; self.index += 1; field.scope.set_index(self.index); - if let Some(e) = &mut field.field { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } self.index = index; ControlFlow::Continue(()) } - ClassElement::PrivateFieldDefinition(field) => { + ClassElement::PrivateFieldDefinition(field) + | ClassElement::PrivateStaticFieldDefinition(field) => { let index = self.index; self.index += 1; field.scope.set_index(self.index); - if let Some(e) = &mut field.field { + if let Some(e) = &mut field.initializer { self.visit_expression_mut(e)?; } self.index = index; ControlFlow::Continue(()) } - ClassElement::PrivateStaticFieldDefinition(_, e) => { - if let Some(e) = e { - self.visit_expression_mut(e)?; - } - ControlFlow::Continue(()) - } ClassElement::StaticBlock(node) => { let contains_direct_eval = contains(node.statements(), ContainsSymbol::DirectEval); self.visit_function_like( diff --git a/core/engine/src/bigint.rs b/core/engine/src/bigint.rs index 43d97afb913..041d161069b 100644 --- a/core/engine/src/bigint.rs +++ b/core/engine/src/bigint.rs @@ -80,6 +80,15 @@ impl JsBigInt { self.inner.to_f64().unwrap_or(f64::INFINITY) } + /// Converts the `BigInt` to a i128 type. + /// + /// Returns `i128::MAX` if the `BigInt` is too big. + #[inline] + #[must_use] + pub fn to_i128(&self) -> i128 { + self.inner.to_i128().unwrap_or(i128::MAX) + } + /// Converts a string to a `BigInt` with the specified radix. #[inline] #[must_use] diff --git a/core/engine/src/builtins/array/from_async.rs b/core/engine/src/builtins/array/from_async.rs new file mode 100644 index 00000000000..0110bb5bfc3 --- /dev/null +++ b/core/engine/src/builtins/array/from_async.rs @@ -0,0 +1,622 @@ +use boa_gc::{Finalize, Trace}; + +use super::Array; +use crate::builtins::iterable::IteratorRecord; +use crate::builtins::promise::ResolvingFunctions; +use crate::builtins::AsyncFromSyncIterator; +use crate::native_function::{CoroutineState, NativeCoroutine}; +use crate::object::{JsFunction, JsPromise}; +use crate::{ + js_string, Context, JsArgs, JsError, JsNativeError, JsObject, JsResult, JsSymbol, JsValue, +}; +use std::cell::Cell; + +impl Array { + /// [`Array.fromAsync ( asyncItems [ , mapfn [ , thisArg ] ] )`][spec] + /// + /// The `Array.fromAsync()` static method creates a new, + /// shallow-copied Array instance from a list or iterator of Promise-like values. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// + /// [spec]: https://tc39.es/proposal-array-from-async/#sec-array.fromAsync + #[allow(clippy::unnecessary_wraps)] + pub(crate) fn from_async( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + // 1. Let C be the this value. + // 2. Let promiseCapability be ! NewPromiseCapability(%Promise%). + let (promise, resolvers) = JsPromise::new_pending(context); + + let async_items = args.get_or_undefined(0); + let mapfn = args.get_or_undefined(1); + let this_arg = args.get_or_undefined(2).clone(); + + // 3. Let fromAsyncClosure be a new Abstract Closure with no parameters that captures C, mapfn, and thisArg and + // performs the following steps when called: + // 4. Perform AsyncFunctionStart(promiseCapability, fromAsyncClosure). + // NOTE: We avoid putting more state onto the coroutines by preprocessing all we can before allocating + // the coroutines. + let result: JsResult<()> = (|| { + // a. If mapfn is undefined, let mapping be false. + let mapfn = if mapfn.is_undefined() { + None + } else { + // b. Else, + // i. If IsCallable(mapfn) is false, throw a TypeError exception. + let Some(callable) = mapfn.as_callable().cloned() else { + return Err(JsNativeError::typ() + .with_message("Array.fromAsync: mapping function must be callable") + .into()); + }; + // ii. Let mapping be true. + Some(JsFunction::from_object_unchecked(callable)) + }; + + // c. Let usingAsyncIterator be ? GetMethod(asyncItems, @@asyncIterator). + // d. If usingAsyncIterator is undefined, then + // i. Let usingSyncIterator be ? GetMethod(asyncItems, @@iterator). + // e. Let iteratorRecord be undefined. + // f. If usingAsyncIterator is not undefined, then + let iterator_record = if let Some(method) = + async_items.get_method(JsSymbol::async_iterator(), context)? + { + // i. Set iteratorRecord to ? GetIterator(asyncItems, async, usingAsyncIterator). + async_items.get_iterator_from_method(&method, context)? + } + // g. Else if usingSyncIterator is not undefined, then + else if let Some(method) = async_items.get_method(JsSymbol::iterator(), context)? { + // i. Set iteratorRecord to ? CreateAsyncFromSyncIterator(GetIterator(asyncItems, sync, usingSyncIterator)). + AsyncFromSyncIterator::create( + async_items.get_iterator_from_method(&method, context)?, + context, + ) + } + // i. Else, + else { + // i. NOTE: asyncItems is neither an AsyncIterable nor an Iterable so assume it is an array-like object. + // ii. Let arrayLike be ! ToObject(asyncItems). + let array_like = async_items.to_object(context)?; + + // iii. Let len be ? LengthOfArrayLike(arrayLike). + let len = array_like.length_of_array_like(context)?; + // iv. If IsConstructor(C) is true, then + let a = if let Some(c) = this.as_constructor() { + // 1. Let A be ? Construct(C, « 𝔽(len) »). + c.construct(&[len.into()], None, context)? + } + // v. Else, + else { + // 1. Let A be ? ArrayCreate(len). + Array::array_create(len, None, context)? + }; + + let coroutine_state = ( + GlobalState { + mapfn, + this_arg, + resolvers: resolvers.clone(), + }, + Cell::new(Some(ArrayLikeStateMachine::LoopStart { + array_like, + a, + len, + // iii. Let k be 0. + k: 0, + })), + ); + + // Try to run the coroutine once to see if it finishes early. + // This avoids allocating a new coroutine that will immediately finish. + // Spec continues on `from_array_like`... + if let CoroutineState::Yielded(value) = + from_array_like(Ok(JsValue::undefined()), &coroutine_state, context) + { + // Coroutine yielded. We need to allocate it for a future execution. + JsPromise::resolve(value, context).await_native( + NativeCoroutine::from_copy_closure_with_captures( + from_array_like, + coroutine_state, + ), + context, + ); + } + + return Ok(()); + }; + + // h. If iteratorRecord is not undefined, then + + // i. If IsConstructor(C) is true, then + let a = if let Some(c) = this.as_constructor() { + // 1. Let A be ? Construct(C). + c.construct(&[], None, context)? + } + // ii. Else, + else { + // 1. Let A be ! ArrayCreate(0). + Array::array_create(0, None, context)? + }; + + let coroutine_state = ( + GlobalState { + mapfn, + this_arg, + resolvers: resolvers.clone(), + }, + Cell::new(Some(AsyncIteratorStateMachine::LoopStart { + // vi. Let k be 0. + k: 0, + a, + iterator_record, + })), + ); + + // Try to run the coroutine once to see if it finishes early. + // This avoids allocating a new coroutine that will immediately finish. + // Spec continues on `from_async_iterator`... + if let CoroutineState::Yielded(value) = + from_async_iterator(Ok(JsValue::undefined()), &coroutine_state, context) + { + JsPromise::resolve(value, context).await_native( + NativeCoroutine::from_copy_closure_with_captures( + from_async_iterator, + coroutine_state, + ), + context, + ); + } + + Ok(()) + })(); + + // AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + // https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + // -> + // AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ) + // https://tc39.es/ecma262/#sec-asyncblockstart + + // i. Assert: result is a throw completion. + if let Err(err) = result { + // ii. Perform ! Call(promiseCapability.[[Reject]], undefined, « result.[[Value]] »). + resolvers + .reject + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("resolving functions cannot fail"); + } + + // 5. Return promiseCapability.[[Promise]]. + Ok(promise.into()) + } +} + +#[derive(Trace, Finalize)] +struct GlobalState { + mapfn: Option, + this_arg: JsValue, + resolvers: ResolvingFunctions, +} + +#[derive(Trace, Finalize)] +#[boa_gc(unsafe_no_drop)] +enum AsyncIteratorStateMachine { + LoopStart { + a: JsObject, + k: u64, + iterator_record: IteratorRecord, + }, + LoopContinue { + a: JsObject, + k: u64, + iterator_record: IteratorRecord, + }, + LoopEnd { + a: JsObject, + k: u64, + iterator_record: IteratorRecord, + mapped_value: Option>, + }, + AsyncIteratorCloseStart { + err: JsError, + iterator: JsObject, + }, + AsyncIteratorCloseEnd { + err: JsError, + }, +} + +/// Part of [`Array.fromAsync ( asyncItems [ , mapfn [ , thisArg ] ] )`][]. +fn from_async_iterator( + mut result: JsResult, + (global_state, state_machine): &(GlobalState, Cell>), + context: &mut Context, +) -> CoroutineState { + let result = (|| { + let Some(mut sm) = state_machine.take() else { + return Ok(CoroutineState::Done); + }; + + // iv. Repeat, + loop { + match sm { + AsyncIteratorStateMachine::LoopStart { + a, + k, + iterator_record, + } => { + // Inverted conditional makes for a simpler code. + if k < 2u64.pow(53) - 1 { + // 2. Let Pk be ! ToString(𝔽(k)). + // 3. Let nextResult be ? Call(iteratorRecord.[[NextMethod]], iteratorRecord.[[Iterator]]). + let next_result = iterator_record.next_method().call( + &iterator_record.iterator().clone().into(), + &[], + context, + )?; + + state_machine.set(Some(AsyncIteratorStateMachine::LoopContinue { + a, + k, + iterator_record, + })); + + // 4. Set nextResult to ? Await(nextResult). + return Ok(CoroutineState::Yielded(next_result)); + } + + // 1. If k ≥ 2**53 - 1, then + + // a. Let error be ThrowCompletion(a newly created TypeError object). + // b. Return ? AsyncIteratorClose(iteratorRecord, error). + sm = AsyncIteratorStateMachine::AsyncIteratorCloseStart { + err: JsNativeError::typ() + .with_message( + "Array.fromAsync: \ + reached the maximum number of elements in an array \ + (2^53 - 1)", + ) + .into(), + iterator: iterator_record.iterator().clone(), + }; + } + AsyncIteratorStateMachine::LoopContinue { + a, + k, + mut iterator_record, + } => { + // `result` is `Await(nextResult)`. + let result = std::mem::replace(&mut result, Ok(JsValue::undefined())); + + // 5. If nextResult is not an Object, throw a TypeError exception. + // Implicit on the call to `update_result`. + iterator_record.update_result(result?, context)?; + + // 6. Let done be ? IteratorComplete(nextResult). + // 7. If done is true, + if iterator_record.done() { + // a. Perform ? Set(A, "length", 𝔽(k), true). + a.set(js_string!("length"), k, true, context)?; + + // b. Return Completion Record { [[Type]]: return, [[Value]]: A, [[Target]]: empty }. + // AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + // https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + // -> + // AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ) + // https://tc39.es/ecma262/#sec-asyncblockstart + + // g. Else if result is a return completion, then + // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »). + global_state + .resolvers + .resolve + .call(&JsValue::undefined(), &[a.into()], context) + .expect("resolving functions cannot fail"); + + return Ok(CoroutineState::Done); + } + + // 8. Let nextValue be ? IteratorValue(nextResult). + let next_value = iterator_record.value(context)?; + // 9. If mapping is true, then + if let Some(mapfn) = &global_state.mapfn { + // a. Let mappedValue be Call(mapfn, thisArg, « nextValue, 𝔽(k) »). + // b. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord). + // https://tc39.es/proposal-array-from-async/#sec-ifabruptcloseasynciterator + let mapped_value = match mapfn.call( + &global_state.this_arg, + &[next_value, k.into()], + context, + ) { + // 1. If value is an abrupt completion, then + Err(err) => { + // a. Perform ? AsyncIteratorClose(iteratorRecord, value). + // b. Return value. + sm = AsyncIteratorStateMachine::AsyncIteratorCloseStart { + err, + iterator: iterator_record.iterator().clone(), + }; + continue; + } + // 2. Else if value is a Completion Record, set value to value.[[Value]]. + Ok(value) => value, + }; + state_machine.set(Some(AsyncIteratorStateMachine::LoopEnd { + a, + k, + iterator_record, + mapped_value: None, + })); + // c. Set mappedValue to Await(mappedValue). + return Ok(CoroutineState::Yielded(mapped_value)); + } + + sm = AsyncIteratorStateMachine::LoopEnd { + a, + k, + iterator_record, + // 10. Else, let mappedValue be nextValue. + mapped_value: Some(Ok(next_value)), + } + } + AsyncIteratorStateMachine::LoopEnd { + a, + k, + iterator_record, + mapped_value, + } => { + // Either awaited `mappedValue` or directly set `mappedValue` to `nextValue`. + let result = std::mem::replace(&mut result, Ok(JsValue::undefined())); + + // d. IfAbruptCloseAsyncIterator(mappedValue, iteratorRecord). + // https://tc39.es/proposal-array-from-async/#sec-ifabruptcloseasynciterator + let mapped_value = match mapped_value.unwrap_or(result) { + // 1. If value is an abrupt completion, then + Err(err) => { + // a. Perform ? AsyncIteratorClose(iteratorRecord, value). + // b. Return value. + sm = AsyncIteratorStateMachine::AsyncIteratorCloseStart { + err, + iterator: iterator_record.iterator().clone(), + }; + continue; + } + // 2. Else if value is a Completion Record, set value to value.[[Value]]. + Ok(value) => value, + }; + + // 11. Let defineStatus be CreateDataPropertyOrThrow(A, Pk, mappedValue). + sm = if let Err(err) = a.create_data_property_or_throw(k, mapped_value, context) + { + // 12. If defineStatus is an abrupt completion, return ? AsyncIteratorClose(iteratorRecord, defineStatus). + AsyncIteratorStateMachine::AsyncIteratorCloseStart { + err, + iterator: iterator_record.iterator().clone(), + } + } else { + AsyncIteratorStateMachine::LoopStart { + a, + // 13. Set k to k + 1. + k: k + 1, + iterator_record, + } + }; + } + // AsyncIteratorClose ( iteratorRecord, completion ) + // https://tc39.es/ecma262/#sec-asynciteratorclose + // Simplified for only error completions. + AsyncIteratorStateMachine::AsyncIteratorCloseStart { err, iterator } => { + // 1. Assert: iteratorRecord.[[Iterator]] is an Object. + // 2. Let iterator be iteratorRecord.[[Iterator]]. + // 3. Let innerResult be Completion(GetMethod(iterator, "return")). + // 4. If innerResult is a normal completion, then + // a. Let return be innerResult.[[Value]]. + // b. If return is undefined, return ? completion. + // c. Set innerResult to Completion(Call(return, iterator)). + // d. If innerResult is a normal completion, set innerResult to Completion(Await(innerResult.[[Value]])). + // 5. If completion is a throw completion, return ? completion. + let Ok(Some(ret)) = iterator.get_method(js_string!("return"), context) else { + return Err(err); + }; + + let Ok(value) = ret.call(&iterator.into(), &[], context) else { + return Err(err); + }; + + state_machine.set(Some(AsyncIteratorStateMachine::AsyncIteratorCloseEnd { + err, + })); + return Ok(CoroutineState::Yielded(value)); + } + AsyncIteratorStateMachine::AsyncIteratorCloseEnd { err } => { + // Awaited `innerResult.[[Value]]`. + // Only need to return the original error. + return Err(err); + } + } + } + })(); + + // AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + // https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + // -> + // AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ) + // https://tc39.es/ecma262/#sec-asyncblockstart + match result { + Ok(cont) => cont, + + // i. Assert: result is a throw completion. + Err(err) => { + // ii. Perform ! Call(promiseCapability.[[Reject]], undefined, « result.[[Value]] »). + global_state + .resolvers + .reject + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("resolving functions cannot fail"); + CoroutineState::Done + } + } +} + +#[derive(Trace, Finalize)] +#[boa_gc(unsafe_no_drop)] +#[allow(clippy::enum_variant_names)] +enum ArrayLikeStateMachine { + LoopStart { + array_like: JsObject, + a: JsObject, + len: u64, + k: u64, + }, + LoopContinue { + array_like: JsObject, + a: JsObject, + len: u64, + k: u64, + }, + LoopEnd { + array_like: JsObject, + a: JsObject, + len: u64, + k: u64, + mapped_value: Option, + }, +} + +/// Part of [`Array.fromAsync ( asyncItems [ , mapfn [ , thisArg ] ] )`][]. +fn from_array_like( + mut result: JsResult, + (global_state, state_machine): &(GlobalState, Cell>), + context: &mut Context, +) -> CoroutineState { + let result: JsResult<_> = (|| { + let Some(mut sm) = state_machine.take() else { + return Ok(CoroutineState::Done); + }; + + loop { + match sm { + ArrayLikeStateMachine::LoopStart { + array_like, + a, + len, + k, + } => { + // vii. Repeat, while k < len, + if k >= len { + // viii. Perform ? Set(A, "length", 𝔽(len), true). + a.set(js_string!("length"), len, true, context)?; + + // ix. Return Completion Record { [[Type]]: return, [[Value]]: A, [[Target]]: empty }. + + // AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + // https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + // -> + // AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ) + // https://tc39.es/ecma262/#sec-asyncblockstart + + // g. Else if result is a return completion, then + // i. Perform ! Call(promiseCapability.[[Resolve]], undefined, « result.[[Value]] »). + global_state + .resolvers + .resolve + .call(&JsValue::undefined(), &[a.into()], context) + .expect("resolving functions cannot fail"); + + return Ok(CoroutineState::Done); + } + + // 1. Let Pk be ! ToString(𝔽(k)). + // 2. Let kValue be ? Get(arrayLike, Pk). + let k_value = array_like.get(k, context)?; + state_machine.set(Some(ArrayLikeStateMachine::LoopContinue { + array_like, + a, + len, + k, + })); + + // 3. Set kValue to ? Await(kValue). + return Ok(CoroutineState::Yielded(k_value)); + } + ArrayLikeStateMachine::LoopContinue { + array_like, + a, + len, + k, + } => { + // Awaited kValue + let k_value = std::mem::replace(&mut result, Ok(JsValue::undefined()))?; + + // 4. If mapping is true, then + if let Some(mapfn) = &global_state.mapfn { + // a. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »). + let mapped_value = + mapfn.call(&global_state.this_arg, &[k_value, k.into()], context)?; + state_machine.set(Some(ArrayLikeStateMachine::LoopEnd { + array_like, + a, + len, + k, + mapped_value: None, + })); + + // b. Set mappedValue to ? Await(mappedValue). + return Ok(CoroutineState::Yielded(mapped_value)); + } + // 5. Else, let mappedValue be kValue. + sm = ArrayLikeStateMachine::LoopEnd { + array_like, + a, + len, + k, + mapped_value: Some(k_value), + } + } + ArrayLikeStateMachine::LoopEnd { + array_like, + a, + len, + k, + mapped_value, + } => { + // Either awaited `mappedValue` or directly set this from `kValue`. + let result = std::mem::replace(&mut result, Ok(JsValue::undefined()))?; + let mapped_value = mapped_value.unwrap_or(result); + + // 6. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue). + a.create_data_property_or_throw(k, mapped_value, context)?; + + // 7. Set k to k + 1. + sm = ArrayLikeStateMachine::LoopStart { + array_like, + a, + len, + k: k + 1, + } + } + } + } + })(); + + // AsyncFunctionStart ( promiseCapability, asyncFunctionBody ) + // https://tc39.es/ecma262/#sec-async-functions-abstract-operations-async-function-start + // -> + // AsyncBlockStart ( promiseCapability, asyncBody, asyncContext ) + // https://tc39.es/ecma262/#sec-asyncblockstart + match result { + Ok(cont) => cont, + // i. Assert: result is a throw completion. + Err(err) => { + // ii. Perform ! Call(promiseCapability.[[Reject]], undefined, « result.[[Value]] »). + global_state + .resolvers + .reject + .call(&JsValue::undefined(), &[err.to_opaque(context)], context) + .expect("resolving functions cannot fail"); + CoroutineState::Done + } + } +} diff --git a/core/engine/src/builtins/array/mod.rs b/core/engine/src/builtins/array/mod.rs index ebbc0c80f9c..7b0990c47b6 100644 --- a/core/engine/src/builtins/array/mod.rs +++ b/core/engine/src/builtins/array/mod.rs @@ -41,6 +41,9 @@ mod array_iterator; use crate::value::JsVariant; pub(crate) use array_iterator::ArrayIterator; +#[cfg(feature = "experimental")] +mod from_async; + #[cfg(test)] mod tests; @@ -106,7 +109,7 @@ impl IntrinsicObject for Array { let unscopables_object = Self::unscopables_object(); - BuiltInBuilder::from_standard_constructor::(realm) + let builder = BuiltInBuilder::from_standard_constructor::(realm) // Static Methods .static_method(Self::from, js_string!("from"), 1) .static_method(Self::is_array, js_string!("isArray"), 1) @@ -177,8 +180,12 @@ impl IntrinsicObject for Array { symbol_unscopables, unscopables_object, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .build(); + ); + + #[cfg(feature = "experimental")] + let builder = builder.static_method(Self::from_async, js_string!("fromAsync"), 1); + + builder.build(); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -3424,6 +3431,33 @@ fn array_exotic_define_own_property( // 3. Else if P is an array index, then PropertyKey::Index(index) => { let index = index.get(); + let new_len = index + 1; + + // Optimization: If the shape of the object is the array template shape, + // we know the position of the "length" property. + if u64::from(new_len) < (2u64.pow(32) - 1) { + let borrowed_object = obj.borrow(); + if borrowed_object.properties().shape.to_addr_usize() + == context + .intrinsics() + .templates() + .array() + .shape() + .to_addr_usize() + { + let old_len = borrowed_object.properties().storage[0].clone(); + drop(borrowed_object); + let old_len = old_len.to_u32(context)?; + if new_len >= old_len { + if ordinary_define_own_property(obj, key, desc, context)? { + let mut borrowed_object = obj.borrow_mut(); + borrowed_object.properties_mut().storage[0] = JsValue::new(new_len); + return Ok(true); + } + return Ok(false); + } + } + } // a. Let oldLenDesc be OrdinaryGetOwnProperty(A, "length"). let old_len_desc = @@ -3455,7 +3489,7 @@ fn array_exotic_define_own_property( if index >= old_len { // i. Set oldLenDesc.[[Value]] to index + 1𝔽. let old_len_desc = PropertyDescriptor::builder() - .value(index + 1) + .value(new_len) .maybe_writable(old_len_desc.writable()) .maybe_enumerable(old_len_desc.enumerable()) .maybe_configurable(old_len_desc.configurable()); diff --git a/core/engine/src/builtins/number/globals.rs b/core/engine/src/builtins/number/globals.rs index c9ef2a4b2b8..1418e32837f 100644 --- a/core/engine/src/builtins/number/globals.rs +++ b/core/engine/src/builtins/number/globals.rs @@ -8,6 +8,7 @@ use crate::{ }; use boa_macros::js_str; +use cow_utils::CowUtils; /// Builtin javascript 'isFinite(number)' function. /// @@ -304,8 +305,8 @@ pub(crate) fn parse_float( // TODO: parse float with optimal utf16 algorithm let input_string = val.to_string(context)?.to_std_string_escaped(); let s = input_string.trim_start_matches(is_trimmable_whitespace); - let s_prefix_lower = s.chars().take(4).collect::().to_ascii_lowercase(); - + let s_prefix = s.chars().take(4).collect::(); + let s_prefix_lower = s_prefix.cow_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)) diff --git a/core/engine/src/builtins/number/mod.rs b/core/engine/src/builtins/number/mod.rs index 2611800feba..b9b0d86e019 100644 --- a/core/engine/src/builtins/number/mod.rs +++ b/core/engine/src/builtins/number/mod.rs @@ -26,6 +26,7 @@ use crate::{ Context, JsArgs, JsResult, JsString, }; use boa_profiler::Profiler; +use cow_utils::CowUtils; use num_traits::float::FloatCore; mod globals; @@ -916,7 +917,7 @@ impl Number { /// Helper function that formats a float as a ES6-style exponential number string. fn f64_to_exponential(n: f64) -> JsString { js_string!(match n.abs() { - x if x >= 1.0 || x == 0.0 => format!("{n:e}").replace('e', "e+"), + x if x >= 1.0 || x == 0.0 => format!("{n:e}").cow_replace('e', "e+").to_string(), _ => format!("{n:e}"), }) } diff --git a/core/engine/src/builtins/string/mod.rs b/core/engine/src/builtins/string/mod.rs index 77d75771af5..96113d4580b 100644 --- a/core/engine/src/builtins/string/mod.rs +++ b/core/engine/src/builtins/string/mod.rs @@ -25,6 +25,7 @@ use crate::{ use boa_macros::utf16; use boa_profiler::Profiler; +use cow_utils::CowUtils; use icu_normalizer::{ComposingNormalizer, DecomposingNormalizer}; use std::cmp::{max, min}; @@ -1727,9 +1728,9 @@ impl String { // the Unicode Default Case Conversion algorithm. let text = string.map_valid_segments(|s| { if UPPER { - s.to_uppercase() + s.cow_to_uppercase().to_string() } else { - s.to_lowercase() + s.cow_to_lowercase().to_string() } }); diff --git a/core/engine/src/builtins/temporal/duration/mod.rs b/core/engine/src/builtins/temporal/duration/mod.rs index f214acdd760..69237a4ebd7 100644 --- a/core/engine/src/builtins/temporal/duration/mod.rs +++ b/core/engine/src/builtins/temporal/duration/mod.rs @@ -2,7 +2,7 @@ use super::{ get_relative_to_option, - options::{get_temporal_unit, TemporalUnitGroup}, + options::{get_digits_option, get_temporal_unit, TemporalUnitGroup}, DateTimeValues, }; use crate::value::JsVariant; @@ -23,7 +23,10 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use temporal_rs::{ - options::{RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit}, + options::{ + RoundingIncrement, RoundingOptions, TemporalRoundingMode, TemporalUnit, + ToStringRoundingOptions, + }, partial::PartialDuration, Duration as InnerDuration, }; @@ -190,7 +193,7 @@ impl IntrinsicObject for Duration { .method(Self::subtract, js_string!("subtract"), 1) .method(Self::round, js_string!("round"), 1) .method(Self::total, js_string!("total"), 1) - .method(Self::to_string, js_string!("toString"), 1) + .method(Self::to_string, js_string!("toString"), 0) .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); @@ -811,17 +814,48 @@ impl Duration { } /// 7.3.22 `Temporal.Duration.prototype.toString ( [ options ] )` - pub(crate) fn to_string(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + pub(crate) fn to_string( + this: &JsValue, + args: &[JsValue], + context: &mut Context, + ) -> JsResult { + let duration = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Duration object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + + let result = duration.inner.to_temporal_string(ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + })?; + + Ok(JsString::from(result).into()) } /// 7.3.23 `Temporal.Duration.prototype.toJSON ( )` - pub(crate) fn to_json(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::error() - .with_message("not yet implemented.") - .into()) + pub(crate) fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let duration = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a Duration object.") + })?; + + let result = duration + .inner + .to_temporal_string(ToStringRoundingOptions::default())?; + + Ok(JsString::from(result).into()) } pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { @@ -990,7 +1024,7 @@ pub(crate) fn to_temporal_partial_duration( .map(|v| { let finite = v.to_finitef64(context)?; let integral_int = finite - .as_integer_if_integral::() + .as_integer_if_integral::() .map_err(JsError::from)?; integral_int.try_into().map_err(JsError::from) }) @@ -1042,7 +1076,7 @@ pub(crate) fn to_temporal_partial_duration( .map(|v| { let finite = v.to_finitef64(context)?; let integral_int = finite - .as_integer_if_integral::() + .as_integer_if_integral::() .map_err(JsError::from)?; integral_int.try_into().map_err(JsError::from) }) diff --git a/core/engine/src/builtins/temporal/instant/mod.rs b/core/engine/src/builtins/temporal/instant/mod.rs index 5177673d17a..7c6a8a026a9 100644 --- a/core/engine/src/builtins/temporal/instant/mod.rs +++ b/core/engine/src/builtins/temporal/instant/mod.rs @@ -1,6 +1,7 @@ //! Boa's implementation of ECMAScript's `Temporal.Instant` builtin object. -use super::options::get_difference_settings; +use super::options::{get_difference_settings, get_digits_option}; +use super::to_temporal_timezone_identifier; use crate::value::JsVariant; use crate::{ builtins::{ @@ -25,6 +26,7 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use num_traits::ToPrimitive; +use temporal_rs::options::{TemporalUnit, ToStringRoundingOptions}; use temporal_rs::{ options::{RoundingIncrement, RoundingOptions, TemporalRoundingMode}, Instant as InnerInstant, @@ -91,6 +93,8 @@ impl IntrinsicObject for Instant { .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) .method(Self::to_zoned_date_time, js_string!("toZonedDateTime"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .method( Self::to_zoned_date_time_iso, @@ -477,6 +481,60 @@ impl Instant { .into()) } + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let instant = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ() + .with_message("the this object must be a Temporal.Instant object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + // NOTE: There may be an order-of-operations here due to a check on Unit groups and smallest_unit value. + let timezone = options + .get(js_string!("timeZone"), context)? + .map(|v| to_temporal_timezone_identifier(v, context)) + .transpose()?; + + let options = ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }; + + let ixdtf = instant.inner.to_ixdtf_string_with_provider( + timezone.as_ref(), + options, + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let instant = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ() + .with_message("the this object must be a Temporal.Instant object.") + })?; + + let ixdtf = instant.inner.to_ixdtf_string_with_provider( + None, + ToStringRoundingOptions::default(), + context.tz_provider(), + )?; + Ok(JsString::from(ixdtf).into()) + } + pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") diff --git a/core/engine/src/builtins/temporal/now.rs b/core/engine/src/builtins/temporal/now.rs index 68e19f196cb..bfccbf60c86 100644 --- a/core/engine/src/builtins/temporal/now.rs +++ b/core/engine/src/builtins/temporal/now.rs @@ -68,7 +68,7 @@ impl Now { fn time_zone_id(_: &JsValue, _args: &[JsValue], context: &mut Context) -> JsResult { // 1. Return ! SystemTimeZone(). system_time_zone(context)? - .id() + .identifier() .map(|s| JsValue::from(js_string!(s.as_str()))) .map_err(Into::into) } diff --git a/core/engine/src/builtins/temporal/options.rs b/core/engine/src/builtins/temporal/options.rs index 3213435bbab..58d4110976a 100644 --- a/core/engine/src/builtins/temporal/options.rs +++ b/core/engine/src/builtins/temporal/options.rs @@ -12,9 +12,13 @@ use crate::{ builtins::options::{get_option, OptionType, ParsableOptionType}, js_string, Context, JsNativeError, JsObject, JsResult, JsString, JsValue, }; -use temporal_rs::options::{ - ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DurationOverflow, - OffsetDisambiguation, RoundingIncrement, TemporalRoundingMode, TemporalUnit, +use temporal_rs::{ + options::{ + ArithmeticOverflow, DifferenceSettings, Disambiguation, DisplayCalendar, DisplayOffset, + DisplayTimeZone, DurationOverflow, OffsetDisambiguation, RoundingIncrement, + TemporalRoundingMode, TemporalUnit, + }, + parsers::Precision, }; // TODO: Expand docs on the below options. @@ -62,6 +66,43 @@ pub(crate) fn get_difference_settings( Ok(settings) } +pub(crate) fn get_digits_option(options: &JsObject, context: &mut Context) -> JsResult { + // 1. Let digitsValue be ? Get(options, "fractionalSecondDigits"). + let digits_value = options.get(js_string!("fractionalSecondDigits"), context)?; + // 2. If digitsValue is undefined, return auto. + if digits_value.is_undefined() { + return Ok(Precision::Auto); + } + // 3. If digitsValue is not a Number, then + let Some(digits_number) = digits_value.as_number() else { + // a. If ? ToString(digitsValue) is not "auto", throw a RangeError exception. + if digits_value.to_string(context)? != js_string!("auto") { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be a digit or 'auto'") + .into()); + } + // b. Return auto. + return Ok(Precision::Auto); + }; + + // 4. If digitsValue is NaN, +∞𝔽, or -∞𝔽, throw a RangeError exception. + if !digits_number.is_finite() { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be a finite number") + .into()); + } + // 5. Let digitCount be floor(ℝ(digitsValue)). + let digits = digits_number.floor() as i32; + // 6. If digitCount < 0 or digitCount > 9, throw a RangeError exception. + if !(0..=9).contains(&digits) { + return Err(JsNativeError::range() + .with_message("fractionalSecondDigits must be in an inclusive range of 0-9") + .into()); + } + // 7. Return digitCount. + Ok(Precision::Digit(digits as u8)) +} + #[derive(Debug, Clone, Copy)] #[allow(unused)] pub(crate) enum TemporalUnitGroup { @@ -117,6 +158,8 @@ impl ParsableOptionType for Disambiguation {} impl ParsableOptionType for OffsetDisambiguation {} impl ParsableOptionType for TemporalRoundingMode {} impl ParsableOptionType for DisplayCalendar {} +impl ParsableOptionType for DisplayOffset {} +impl ParsableOptionType for DisplayTimeZone {} impl OptionType for RoundingIncrement { fn from_value(value: JsValue, context: &mut Context) -> JsResult { diff --git a/core/engine/src/builtins/temporal/plain_date/mod.rs b/core/engine/src/builtins/temporal/plain_date/mod.rs index a008fb98217..5a6c8b3c376 100644 --- a/core/engine/src/builtins/temporal/plain_date/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date/mod.rs @@ -22,7 +22,7 @@ use boa_profiler::Profiler; use temporal_rs::{ options::{ArithmeticOverflow, DisplayCalendar}, partial::PartialDate, - PlainDate as InnerDate, TinyAsciiStr, + Calendar, PlainDate as InnerDate, TinyAsciiStr, }; use super::{ @@ -258,7 +258,17 @@ impl BuiltInConstructor for PlainDate { .get_or_undefined(2) .to_finitef64(context)? .as_integer_with_truncation::(); - let calendar_slot = to_temporal_calendar_slot_value(args.get_or_undefined(3))?; + let calendar_slot = args + .get_or_undefined(3) + .map(|s| { + s.as_string() + .map(JsString::to_std_string_lossy) + .ok_or_else(|| JsNativeError::typ().with_message("calendar must be a string.")) + }) + .transpose()? + .map(|s| Calendar::from_utf8(s.as_bytes())) + .transpose()? + .unwrap_or_default(); let inner = InnerDate::try_new(year, month, day, calendar_slot)?; diff --git a/core/engine/src/builtins/temporal/plain_date_time/mod.rs b/core/engine/src/builtins/temporal/plain_date_time/mod.rs index 7bd400448e2..50ff0fa13d4 100644 --- a/core/engine/src/builtins/temporal/plain_date_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_date_time/mod.rs @@ -24,15 +24,18 @@ use boa_profiler::Profiler; mod tests; use temporal_rs::{ - options::{ArithmeticOverflow, RoundingIncrement, RoundingOptions, TemporalRoundingMode}, + options::{ + ArithmeticOverflow, DisplayCalendar, RoundingIncrement, RoundingOptions, + TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions, + }, partial::PartialDateTime, - PlainDateTime as InnerDateTime, PlainTime, + Calendar, PlainDateTime as InnerDateTime, PlainTime, }; use super::{ calendar::{get_temporal_calendar_slot_value_with_default, to_temporal_calendar_slot_value}, create_temporal_duration, - options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, + options::{get_difference_settings, get_digits_option, get_temporal_unit, TemporalUnitGroup}, to_temporal_duration_record, to_temporal_time, PlainDate, ZonedDateTime, }; use crate::value::JsVariant; @@ -279,6 +282,8 @@ impl IntrinsicObject for PlainDateTime { .method(Self::since, js_string!("since"), 1) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); } @@ -365,7 +370,17 @@ impl BuiltInConstructor for PlainDateTime { Ok(finite.as_integer_with_truncation::()) })?; - let calendar_slot = to_temporal_calendar_slot_value(args.get_or_undefined(9))?; + let calendar_slot = args + .get_or_undefined(9) + .map(|s| { + s.as_string() + .map(JsString::to_std_string_lossy) + .ok_or_else(|| JsNativeError::typ().with_message("calendar must be a string.")) + }) + .transpose()? + .map(|s| Calendar::from_utf8(s.as_bytes())) + .transpose()? + .unwrap_or_default(); let dt = InnerDateTime::new( iso_year, @@ -933,6 +948,50 @@ impl PlainDateTime { Ok((dt.inner == other).into()) } + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + + let show_calendar = + get_option::(&options, js_string!("calendarName"), context)? + .unwrap_or(DisplayCalendar::Auto); + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + + let ixdtf = dt.inner.to_ixdtf_string( + ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }, + show_calendar, + )?; + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let dt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainDateTime object.") + })?; + + let ixdtf = dt + .inner + .to_ixdtf_string(ToStringRoundingOptions::default(), DisplayCalendar::Auto)?; + Ok(JsString::from(ixdtf).into()) + } + pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") diff --git a/core/engine/src/builtins/temporal/plain_month_day/mod.rs b/core/engine/src/builtins/temporal/plain_month_day/mod.rs index 8030d1cf07a..0e75000c939 100644 --- a/core/engine/src/builtins/temporal/plain_month_day/mod.rs +++ b/core/engine/src/builtins/temporal/plain_month_day/mod.rs @@ -1,5 +1,4 @@ //! Boa's implementation of the ECMAScript `Temporal.PlainMonthDay` builtin object. -#![allow(dead_code, unused_variables)] use std::str::FromStr; use crate::{ @@ -22,7 +21,7 @@ use boa_profiler::Profiler; use temporal_rs::{ options::{ArithmeticOverflow, DisplayCalendar}, partial::PartialDate, - PlainMonthDay as InnerMonthDay, TinyAsciiStr, + Calendar, PlainMonthDay as InnerMonthDay, TinyAsciiStr, }; use super::{calendar::to_temporal_calendar_slot_value, DateTimeValues}; @@ -40,89 +39,6 @@ impl PlainMonthDay { } } -// ==== `Temporal.PlainMonthDay` static Methods ==== -impl PlainMonthDay { - // 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] ) - fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - let options = get_options_object(args.get_or_undefined(1))?; - let item = args.get_or_undefined(0); - to_temporal_month_day(item, &options, context) - } -} - -// === `PlainMonthDay` Accessor Implementations ===== / - -impl PlainMonthDay { - fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult { - let month_day = this - .as_object() - .and_then(JsObject::downcast_ref::) - .ok_or_else(|| { - JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") - })?; - let inner = &month_day.inner; - match field { - DateTimeValues::Day => Ok(inner.iso_day().into()), - DateTimeValues::MonthCode => Ok(js_string!(inner.month_code()?.to_string()).into()), - _ => unreachable!(), - } - } - - fn get_day(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - Self::get_internal_field(this, &DateTimeValues::Day) - } - - fn get_year(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - Self::get_internal_field(this, &DateTimeValues::Year) - } - - fn get_month_code(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - Self::get_internal_field(this, &DateTimeValues::MonthCode) - } - - fn get_calendar_id(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - let month_day = this - .as_object() - .and_then(JsObject::downcast_ref::) - .ok_or_else(|| { - JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") - })?; - let inner = &month_day.inner; - Ok(js_string!(inner.calendar().identifier()).into()) - } -} - -// ==== `Temporal.PlainMonthDay` Methods ==== -impl PlainMonthDay { - // 10.3.7 Temporal.PlainMonthDay.prototype.toString ( [ options ] ) - fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { - // 1. Let monthDay be the this value. - // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). - let month_day = this - .as_object() - .and_then(JsObject::downcast_ref::) - .ok_or_else(|| { - JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") - })?; - let inner = &month_day.inner; - // 3. Set options to ? NormalizeOptionsObject(options). - let options = get_options_object(args.get_or_undefined(0))?; - // 4. Let showCalendar be ? ToShowCalendarOption(options). - // Get calendarName from the options object - let show_calendar = - get_option::(&options, js_string!("calendarName"), context)? - .unwrap_or(DisplayCalendar::Auto); - - Ok(month_day_to_string(inner, show_calendar)) - } - - pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - Err(JsNativeError::typ() - .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") - .into()) - } -} - impl BuiltInObject for PlainMonthDay { const NAME: JsString = StaticJsStrings::PLAIN_MD_NAME; } @@ -166,9 +82,10 @@ impl IntrinsicObject for PlainMonthDay { None, Attribute::CONFIGURABLE, ) - .method(Self::to_string, js_string!("toString"), 1) + .static_method(Self::from, js_string!("from"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) - .static_method(Self::from, js_string!("from"), 2) .build(); } @@ -217,7 +134,18 @@ impl BuiltInConstructor for PlainMonthDay { .to_finitef64(context)? .as_integer_with_truncation::(); - let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(2))?; + let calendar = args + .get_or_undefined(2) + .map(|s| { + s.as_string() + .map(JsString::to_std_string_lossy) + .ok_or_else(|| JsNativeError::typ().with_message("calendar must be a string.")) + }) + .transpose()? + .map(|s| Calendar::from_utf8(s.as_bytes())) + .transpose()? + .unwrap_or_default(); + let inner = InnerMonthDay::new_with_overflow( m, d, @@ -229,44 +157,102 @@ impl BuiltInConstructor for PlainMonthDay { } } -// ==== `PlainMonthDay` Abstract Operations ==== +// ==== `Temporal.PlainMonthDay` static Methods ==== -fn month_day_to_string(inner: &InnerMonthDay, show_calendar: DisplayCalendar) -> JsValue { - // Let month be monthDay.[[ISOMonth]] formatted as a two-digit decimal number, padded to the left with a zero if necessary - let month = inner.iso_month().to_string(); - - // 2. Let day be ! FormatDayOfMonth(monthDay.[[ISODay]]). - let day = inner.iso_day().to_string(); - - // 3. Let result be the string-concatenation of month and the code unit 0x002D (HYPHEN-MINUS). - let mut result = format!("{month:0>2}-{day:0>2}"); - - // 4. Let calendarId be monthDay.[[Calendar]].[[id]]. - let calendar_id = inner.calendar().identifier(); - - // 5. Let calendar be monthDay.[[Calendar]]. - // 6. If showCalendar is "auto", then - // a. Set showCalendar to "always". - // 7. If showCalendar is "always", then - // a. Let calendarString be ! FormatCalendarAnnotation(calendar). - // b. Set result to the string-concatenation of result, the code unit 0x0040 (COMMERCIAL AT), and calendarString. - if (matches!( - show_calendar, - DisplayCalendar::Critical | DisplayCalendar::Always | DisplayCalendar::Auto - )) && !(matches!(show_calendar, DisplayCalendar::Auto) && calendar_id == "iso8601") - { - let year = inner.iso_year().to_string(); - let flag = if matches!(show_calendar, DisplayCalendar::Critical) { - "!" - } else { - "" - }; - result = format!("{year}-{result}[{flag}u-ca={calendar_id}]"); +impl PlainMonthDay { + // 10.2.2 Temporal.PlainMonthDay.from ( item [ , options ] ) + fn from(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let options = get_options_object(args.get_or_undefined(1))?; + let item = args.get_or_undefined(0); + to_temporal_month_day(item, &options, context) + } +} + +// ==== `PlainMonthDay` Accessor Implementations ==== + +impl PlainMonthDay { + fn get_internal_field(this: &JsValue, field: &DateTimeValues) -> JsResult { + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") + })?; + let inner = &month_day.inner; + match field { + DateTimeValues::Day => Ok(inner.iso_day().into()), + DateTimeValues::MonthCode => Ok(js_string!(inner.month_code()?.to_string()).into()), + _ => unreachable!(), + } + } + + fn get_calendar_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") + })?; + Ok(js_string!(month_day.inner.calendar().identifier()).into()) + } + + fn get_day(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::Day) + } + + fn get_month_code(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Self::get_internal_field(this, &DateTimeValues::MonthCode) } - // 8. Return result. - js_string!(result).into() } +// ==== `Temporal.PlainMonthDay` Methods ==== + +impl PlainMonthDay { + /// 10.3.8 `Temporal.PlainMonthDay.prototype.toString ( [ options ] )` + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + // 1. Let monthDay be the this value. + // 2. Perform ? RequireInternalSlot(monthDay, [[InitializedTemporalMonthDay]]). + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") + })?; + + // 3. Set options to ? NormalizeOptionsObject(options). + let options = get_options_object(args.get_or_undefined(0))?; + // 4. Let showCalendar be ? ToShowCalendarOption(options). + // Get calendarName from the options object + let show_calendar = + get_option::(&options, js_string!("calendarName"), context)? + .unwrap_or(DisplayCalendar::Auto); + + let ixdtf = month_day.inner.to_ixdtf_string(show_calendar); + Ok(JsString::from(ixdtf).into()) + } + + /// 10.3.10 Temporal.PlainMonthDay.prototype.toJSON ( ) + pub(crate) fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let month_day = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainMonthDay object.") + })?; + + Ok(JsString::from(month_day.inner.to_string()).into()) + } + + /// 9.3.11 Temporal.PlainMonthDay.prototype.valueOf ( ) + pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + Err(JsNativeError::typ() + .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") + .into()) + } +} + +// ==== `PlainMonthDay` Abstract Operations ==== + pub(crate) fn create_temporal_month_day( inner: InnerMonthDay, new_target: Option<&JsValue>, diff --git a/core/engine/src/builtins/temporal/plain_time/mod.rs b/core/engine/src/builtins/temporal/plain_time/mod.rs index 35cbe4a6742..84cbd121b2a 100644 --- a/core/engine/src/builtins/temporal/plain_time/mod.rs +++ b/core/engine/src/builtins/temporal/plain_time/mod.rs @@ -5,7 +5,7 @@ use super::{ options::{get_difference_settings, get_temporal_unit, TemporalUnitGroup}, to_temporal_duration_record, PlainDateTime, ZonedDateTime, }; -use crate::value::JsVariant; +use crate::{builtins::temporal::options::get_digits_option, value::JsVariant}; use crate::{ builtins::{ options::{get_option, get_options_object}, @@ -23,7 +23,7 @@ use crate::{ use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; use temporal_rs::{ - options::{ArithmeticOverflow, TemporalRoundingMode}, + options::{ArithmeticOverflow, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions}, partial::PartialTime, PlainTime as PlainTimeInner, }; @@ -118,7 +118,8 @@ impl IntrinsicObject for PlainTime { .method(Self::since, js_string!("since"), 1) .method(Self::round, js_string!("round"), 1) .method(Self::equals, js_string!("equals"), 1) - .method(Self::get_iso_fields, js_string!("getISOFields"), 0) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); } @@ -530,58 +531,50 @@ impl PlainTime { Ok((time.inner == other).into()) } - /// 4.3.18 Temporal.PlainTime.prototype.getISOFields ( ) - fn get_iso_fields(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { - // 1. Let temporalTime be the this value. - // 2. Perform ? RequireInternalSlot(temporalTime, [[InitializedTemporalTime]]). + /// 4.3.16 `Temporal.PlainTime.prototype.toString ( [ options ] )` + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { let time = this .as_object() - .and_then(JsObject::downcast_ref::) + .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a PlainTime object.") })?; - // 3. Let fields be OrdinaryObjectCreate(%Object.prototype%). - let fields = JsObject::with_object_proto(context.intrinsics()); + let options = get_options_object(args.get_or_undefined(0))?; - // 4. Perform ! CreateDataPropertyOrThrow(fields, "isoHour", 𝔽(temporalTime.[[ISOHour]])). - fields.create_data_property_or_throw(js_string!("isoHour"), time.inner.hour(), context)?; - // 5. Perform ! CreateDataPropertyOrThrow(fields, "isoMicrosecond", 𝔽(temporalTime.[[ISOMicrosecond]])). - fields.create_data_property_or_throw( - js_string!("isoMicrosecond"), - time.inner.microsecond(), - context, - )?; - // 6. Perform ! CreateDataPropertyOrThrow(fields, "isoMillisecond", 𝔽(temporalTime.[[ISOMillisecond]])). - fields.create_data_property_or_throw( - js_string!("isoMillisecond"), - time.inner.millisecond(), - context, - )?; - // 7. Perform ! CreateDataPropertyOrThrow(fields, "isoMinute", 𝔽(temporalTime.[[ISOMinute]])). - fields.create_data_property_or_throw( - js_string!("isoMinute"), - time.inner.minute(), - context, - )?; - // 8. Perform ! CreateDataPropertyOrThrow(fields, "isoNanosecond", 𝔽(temporalTime.[[ISONanosecond]])). - fields.create_data_property_or_throw( - js_string!("isoNanosecond"), - time.inner.nanosecond(), - context, - )?; - // 9. Perform ! CreateDataPropertyOrThrow(fields, "isoSecond", 𝔽(temporalTime.[[ISOSecond]])). - fields.create_data_property_or_throw( - js_string!("isoSecond"), - time.inner.second(), - context, - )?; + let precision = get_digits_option(&options, context)?; + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + + let options = ToStringRoundingOptions { + precision, + rounding_mode, + smallest_unit, + }; - // 10. Return fields. - Ok(fields.into()) + let ixdtf = time.inner.to_ixdtf_string(options)?; + + Ok(JsString::from(ixdtf).into()) + } + + /// 4.3.18 `Temporal.PlainTime.prototype.toJSON ( )` + fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let time = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a PlainTime object.") + })?; + + let ixdtf = time + .inner + .to_ixdtf_string(ToStringRoundingOptions::default())?; + Ok(JsString::from(ixdtf).into()) } - /// 4.3.22 Temporal.PlainTime.prototype.valueOf ( ) + /// 4.3.19 Temporal.PlainTime.prototype.valueOf ( ) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { // 1. Throw a TypeError exception. Err(JsNativeError::typ() diff --git a/core/engine/src/builtins/temporal/plain_year_month/mod.rs b/core/engine/src/builtins/temporal/plain_year_month/mod.rs index b06cbfa1403..2431a0722f6 100644 --- a/core/engine/src/builtins/temporal/plain_year_month/mod.rs +++ b/core/engine/src/builtins/temporal/plain_year_month/mod.rs @@ -21,7 +21,7 @@ use boa_profiler::Profiler; use temporal_rs::{ options::{ArithmeticOverflow, DisplayCalendar}, - Duration, PlainYearMonth as InnerYearMonth, + Calendar, Duration, PlainYearMonth as InnerYearMonth, }; use super::{calendar::to_temporal_calendar_slot_value, to_temporal_duration, DateTimeValues}; @@ -134,13 +134,14 @@ impl IntrinsicObject for PlainYearMonth { Attribute::CONFIGURABLE, ) .static_method(Self::from, js_string!("from"), 2) - .method(Self::with, js_string!("with"), 2) - .method(Self::add, js_string!("add"), 2) - .method(Self::subtract, js_string!("subtract"), 2) - .method(Self::until, js_string!("until"), 2) - .method(Self::since, js_string!("since"), 2) + .method(Self::with, js_string!("with"), 1) + .method(Self::add, js_string!("add"), 1) + .method(Self::subtract, js_string!("subtract"), 1) + .method(Self::until, js_string!("until"), 1) + .method(Self::since, js_string!("since"), 1) .method(Self::equals, js_string!("equals"), 1) - .method(Self::to_string, js_string!("toString"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .build(); } @@ -186,7 +187,17 @@ impl BuiltInConstructor for PlainYearMonth { .as_integer_with_truncation::(); // 5. Let calendar be ? ToTemporalCalendarSlotValue(calendarLike, "iso8601"). - let calendar = to_temporal_calendar_slot_value(args.get_or_undefined(2))?; + let calendar = args + .get_or_undefined(2) + .map(|s| { + s.as_string() + .map(JsString::to_std_string_lossy) + .ok_or_else(|| JsNativeError::typ().with_message("calendar must be a string.")) + }) + .transpose()? + .map(|s| Calendar::from_utf8(s.as_bytes())) + .transpose()? + .unwrap_or_default(); // 6. Let ref be ? ToIntegerWithTruncation(referenceISODay). let ref_day = args @@ -417,6 +428,7 @@ impl PlainYearMonth { .into()) } + /// `9.3.19 Temporal.PlainYearMonth.prototype.toString ( [ options ] )` fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { // 1. Let YearMonth be the this value. // 2. Perform ? RequireInternalSlot(yearMonth, [[InitializedTemporalYearMonth]]). @@ -427,7 +439,6 @@ impl PlainYearMonth { JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") })?; - let inner = &year_month.inner; // 3. Set options to ? NormalizeOptionsObject(options). let options = get_options_object(args.get_or_undefined(0))?; // 4. Let showCalendar be ? ToShowCalendarOption(options). @@ -436,9 +447,23 @@ impl PlainYearMonth { get_option::(&options, js_string!("calendarName"), context)? .unwrap_or(DisplayCalendar::Auto); - Ok(year_month_to_string(inner, show_calendar)) + let ixdtf = year_month.inner.to_ixdtf_string(show_calendar); + Ok(JsString::from(ixdtf).into()) + } + + /// `9.3.21 Temporal.PlainYearMonth.prototype.toJSON ( )` + pub(crate) fn to_json(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { + let year_month = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("this value must be a PlainYearMonth object.") + })?; + + Ok(JsString::from(year_month.inner.to_string()).into()) } + /// `9.3.22 Temporal.PlainYearMonth.prototype.valueOf ( )` pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() .with_message("`valueOf` not supported by Temporal built-ins. See 'compare', 'equals', or `toString`") @@ -528,35 +553,3 @@ fn add_or_subtract_duration( create_temporal_year_month(year_month_result, None, context) } - -fn year_month_to_string(inner: &InnerYearMonth, show_calendar: DisplayCalendar) -> JsValue { - // Let year be PadISOYear(yearMonth.[[ISOYear]]). - let year = inner.padded_iso_year_string(); - // Let month be ToZeroPaddedDecimalString(yearMonth.[[ISOMonth]], 2). - let month = inner.iso_month().to_string(); - - // Let result be the string-concatenation of year, the code unit 0x002D (HYPHEN-MINUS), and month. - let mut result = format!("{year}-{month:0>2}"); - - // 5. If showCalendar is one of "always" or "critical", or if calendarIdentifier is not "iso8601", then - // a. Let day be ToZeroPaddedDecimalString(yearMonth.[[ISODay]], 2). - // b. Set result to the string-concatenation of result, the code unit 0x002D (HYPHEN-MINUS), and day. - // 6. Let calendarString be FormatCalendarAnnotation(calendarIdentifier, showCalendar). - // 7. Set result to the string-concatenation of result and calendarString. - if matches!( - show_calendar, - DisplayCalendar::Critical | DisplayCalendar::Always | DisplayCalendar::Auto - ) && !(matches!(show_calendar, DisplayCalendar::Auto) && inner.calendar_id() == "iso8601") - { - let calendar = inner.calendar_id(); - let calendar_string = calendar.to_string(); - let flag = if matches!(show_calendar, DisplayCalendar::Critical) { - "!" - } else { - "" - }; - result.push_str(&format!("[{flag}c={calendar_string}]",)); - } - // 8. Return result. - js_string!(result).into() -} diff --git a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs index 5d989d086f6..cb2e8d47765 100644 --- a/core/engine/src/builtins/temporal/zoneddatetime/mod.rs +++ b/core/engine/src/builtins/temporal/zoneddatetime/mod.rs @@ -1,8 +1,7 @@ -use std::str::FromStr; - use crate::{ builtins::{ options::{get_option, get_options_object}, + temporal::options::get_digits_option, BuiltInBuilder, BuiltInConstructor, BuiltInObject, IntrinsicObject, }, context::intrinsics::{Intrinsics, StandardConstructor, StandardConstructors}, @@ -12,21 +11,25 @@ use crate::{ realm::Realm, string::StaticJsStrings, value::{IntoOrUndefined, PreferredType}, - Context, JsArgs, JsBigInt, JsData, JsError, JsNativeError, JsObject, JsResult, JsString, - JsSymbol, JsValue, JsVariant, + Context, JsArgs, JsBigInt, JsData, JsNativeError, JsObject, JsResult, JsString, JsSymbol, + JsValue, JsVariant, }; use boa_gc::{Finalize, Trace}; use boa_profiler::Profiler; -use num_traits::ToPrimitive; +use cow_utils::CowUtils; use temporal_rs::{ - options::{ArithmeticOverflow, Disambiguation, OffsetDisambiguation}, + options::{ + ArithmeticOverflow, Disambiguation, DisplayCalendar, DisplayOffset, DisplayTimeZone, + OffsetDisambiguation, TemporalRoundingMode, TemporalUnit, ToStringRoundingOptions, + }, partial::PartialZonedDateTime, Calendar, TimeZone, ZonedDateTime as ZonedDateTimeInner, }; use super::{ calendar::to_temporal_calendar_slot_value, create_temporal_date, create_temporal_datetime, - create_temporal_instant, create_temporal_time, to_partial_date_record, to_partial_time_record, + create_temporal_duration, create_temporal_instant, create_temporal_time, + options::get_difference_settings, to_partial_date_record, to_partial_time_record, to_temporal_duration, to_temporal_time, }; @@ -324,7 +327,11 @@ impl IntrinsicObject for ZonedDateTime { .method(Self::with_calendar, js_string!("withCalendar"), 1) .method(Self::add, js_string!("add"), 1) .method(Self::subtract, js_string!("subtract"), 1) + .method(Self::until, js_string!("until"), 1) + .method(Self::since, js_string!("since"), 1) .method(Self::equals, js_string!("equals"), 1) + .method(Self::to_string, js_string!("toString"), 0) + .method(Self::to_json, js_string!("toJSON"), 0) .method(Self::value_of, js_string!("valueOf"), 0) .method(Self::start_of_day, js_string!("startOfDay"), 0) .method(Self::to_instant, js_string!("toInstant"), 0) @@ -360,14 +367,8 @@ impl BuiltInConstructor for ZonedDateTime { .into()); } // 2. Set epochNanoseconds to ? ToBigInt(epochNanoseconds). - let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; // 3. If IsValidEpochNanoseconds(epochNanoseconds) is false, throw a RangeError exception. - // TODO: Better primitive for handling epochNanoseconds is needed in temporal_rs - let Some(nanos) = epoch_nanos.to_f64().to_i128() else { - return Err(JsNativeError::range() - .with_message("epochNanoseconds exceeded valid range.") - .into()); - }; + let epoch_nanos = args.get_or_undefined(0).to_bigint(context)?; // 4. If timeZone is not a String, throw a TypeError exception. let Some(timezone_str) = args.get_or_undefined(1).as_string() else { @@ -392,21 +393,18 @@ impl BuiltInConstructor for ZonedDateTime { // 9. If calendar is not a String, throw a TypeError exception. // 10. Set calendar to ? CanonicalizeCalendar(calendar). let calendar = args - .get(2) - .map(|v| { - if let Some(calendar_str) = v.as_string() { - Calendar::from_str(&calendar_str.to_std_string_escaped()) - .map_err(Into::::into) - } else { - Err(JsNativeError::typ() - .with_message("calendar must be a string.") - .into()) - } + .get_or_undefined(2) + .map(|s| { + s.as_string() + .map(JsString::to_std_string_lossy) + .ok_or_else(|| JsNativeError::typ().with_message("calendar must be a string.")) }) .transpose()? + .map(|s| Calendar::from_utf8(s.as_bytes())) + .transpose()? .unwrap_or_default(); - let inner = ZonedDateTimeInner::try_new(nanos, calendar, timezone)?; + let inner = ZonedDateTimeInner::try_new(epoch_nanos.to_i128(), calendar, timezone)?; // 11. Return ? CreateTemporalZonedDateTime(epochNanoseconds, timeZone, calendar, NewTarget). create_temporal_zoneddatetime(inner, Some(new_target), context).map(Into::into) @@ -430,16 +428,15 @@ impl ZonedDateTime { /// 6.3.4 get `Temporal.ZonedDateTime.prototype.timeZoneId` fn get_timezone_id(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { - let _zdt = this + let zdt = this .as_object() .and_then(JsObject::downcast_ref::) .ok_or_else(|| { JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") })?; - Err(JsNativeError::error() - .with_message("Not yet implemented.") - .into()) + let tz_id = zdt.inner.timezone().identifier()?; + Ok(JsString::from(tz_id).into()) } /// 6.3.5 get `Temporal.ZonedDateTime.prototype.era` @@ -453,7 +450,7 @@ impl ZonedDateTime { let era = zdt.inner.era_with_provider(context.tz_provider())?; Ok(era - .map(|tinystr| JsString::from(tinystr.to_lowercase())) + .map(|tinystr| JsString::from(tinystr.cow_to_lowercase().to_string())) .into_or_undefined()) } @@ -887,13 +884,10 @@ impl ZonedDateTime { let options = get_options_object(args.get_or_undefined(1))?; let overflow = get_option::(&options, js_string!("overflow"), context)?; - create_temporal_zoneddatetime( - zdt.inner - .add_with_provider(&duration, overflow, context.tz_provider())?, - None, - context, - ) - .map(Into::into) + let result = zdt + .inner + .add_with_provider(&duration, overflow, context.tz_provider())?; + create_temporal_zoneddatetime(result, None, context).map(Into::into) } /// 6.3.36 `Temporal.ZonedDateTime.prototype.subtract ( temporalDurationLike [ , options ] )` @@ -910,13 +904,48 @@ impl ZonedDateTime { let options = get_options_object(args.get_or_undefined(1))?; let overflow = get_option::(&options, js_string!("overflow"), context)?; - create_temporal_zoneddatetime( + let result = zdt.inner - .subtract_with_provider(&duration, overflow, context.tz_provider())?, - None, - context, - ) - .map(Into::into) + .subtract_with_provider(&duration, overflow, context.tz_provider())?; + create_temporal_zoneddatetime(result, None, context).map(Into::into) + } + + fn since(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let other = to_temporal_zoneddatetime(args.get_or_undefined(0), None, context)?; + + let options = get_options_object(args.get_or_undefined(1))?; + let settings = get_difference_settings(&options, context)?; + + let result = zdt + .inner + .since_with_provider(&other, settings, context.tz_provider())?; + create_temporal_duration(result, None, context).map(Into::into) + } + + fn until(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let other = to_temporal_zoneddatetime(args.get_or_undefined(0), None, context)?; + + let options = get_options_object(args.get_or_undefined(1))?; + let settings = get_difference_settings(&options, context)?; + + let result = zdt + .inner + .until_with_provider(&other, settings, context.tz_provider())?; + create_temporal_duration(result, None, context).map(Into::into) } /// 6.3.40 `Temporal.ZonedDateTime.prototype.equals ( other )` @@ -932,6 +961,66 @@ impl ZonedDateTime { Ok((zdt.inner == other).into()) } + fn to_string(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let options = get_options_object(args.get_or_undefined(0))?; + + let show_calendar = + get_option::(&options, js_string!("calendarName"), context)? + .unwrap_or(DisplayCalendar::Auto); + let precision = get_digits_option(&options, context)?; + let show_offset = get_option::(&options, js_string!("offset"), context)? + .unwrap_or(DisplayOffset::Auto); + let rounding_mode = + get_option::(&options, js_string!("roundingMode"), context)?; + let smallest_unit = + get_option::(&options, js_string!("smallestUnit"), context)?; + // NOTE: There may be an order-of-operations here due to a check on Unit groups and smallest_unit value. + let display_timezone = + get_option::(&options, js_string!("timeZoneName"), context)? + .unwrap_or(DisplayTimeZone::Auto); + + let options = ToStringRoundingOptions { + precision, + smallest_unit, + rounding_mode, + }; + let ixdtf = zdt.inner.to_ixdtf_string_with_provider( + show_offset, + display_timezone, + show_calendar, + options, + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + + fn to_json(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult { + let zdt = this + .as_object() + .and_then(JsObject::downcast_ref::) + .ok_or_else(|| { + JsNativeError::typ().with_message("the this object must be a ZonedDateTime object.") + })?; + + let ixdtf = zdt.inner.to_ixdtf_string_with_provider( + DisplayOffset::Auto, + DisplayTimeZone::Auto, + DisplayCalendar::Auto, + ToStringRoundingOptions::default(), + context.tz_provider(), + )?; + + Ok(JsString::from(ixdtf).into()) + } + /// 6.3.44 `Temporal.ZonedDateTime.prototype.valueOf ( )` pub(crate) fn value_of(_this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult { Err(JsNativeError::typ() diff --git a/core/engine/src/bytecompiler/class.rs b/core/engine/src/bytecompiler/class.rs index 1e360be4920..26e666b57fa 100644 --- a/core/engine/src/bytecompiler/class.rs +++ b/core/engine/src/bytecompiler/class.rs @@ -22,7 +22,12 @@ enum StaticElement { StaticBlock(Gc), // A static class field with it's function code, an optional name index and the information if the function is an anonymous function. - StaticField((Gc, Option, bool)), + StaticField { + code: Gc, + name_index: Option, + is_anonymous_function: bool, + is_private: bool, + }, } /// Describes the complete specification of a class. @@ -160,16 +165,12 @@ impl ByteCompiler<'_> { self.emit_u32(index); } } - ClassElement::PrivateFieldDefinition(field) => { + ClassElement::PrivateFieldDefinition(field) + | ClassElement::PrivateStaticFieldDefinition(field) => { count += 1; let index = self.get_or_insert_private_name(*field.name()); self.emit_u32(index); } - ClassElement::PrivateStaticFieldDefinition(name, _) => { - count += 1; - let index = self.get_or_insert_private_name(*name); - self.emit_u32(index); - } _ => {} } } @@ -294,7 +295,7 @@ impl ByteCompiler<'_> { // Function environment field_compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE; let _ = field_compiler.push_scope(field.scope()); - let is_anonymous_function = if let Some(node) = &field.field() { + let is_anonymous_function = if let Some(node) = &field.initializer() { field_compiler.compile_expr(node, true); node.is_anonymous_function_definition() } else { @@ -329,7 +330,7 @@ impl ByteCompiler<'_> { ); field_compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE; let _ = field_compiler.push_scope(field.scope()); - if let Some(node) = field.field() { + if let Some(node) = field.initializer() { field_compiler.compile_expr(node, true); } else { field_compiler.emit_opcode(Opcode::PushUndefined); @@ -371,7 +372,7 @@ impl ByteCompiler<'_> { ); field_compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE; let _ = field_compiler.push_scope(field.scope()); - let is_anonymous_function = if let Some(node) = &field.field() { + let is_anonymous_function = if let Some(node) = &field.initializer() { field_compiler.compile_expr(node, true); node.is_anonymous_function_definition() } else { @@ -385,21 +386,48 @@ impl ByteCompiler<'_> { let code = field_compiler.finish(); let code = Gc::new(code); - static_elements.push(StaticElement::StaticField(( + static_elements.push(StaticElement::StaticField { code, name_index, is_anonymous_function, - ))); + is_private: false, + }); } - ClassElement::PrivateStaticFieldDefinition(name, field) => { - self.emit_opcode(Opcode::Dup); - if let Some(node) = field { - self.compile_expr(node, true); + ClassElement::PrivateStaticFieldDefinition(field) => { + let name_index = self.get_or_insert_private_name(*field.name()); + let mut field_compiler = ByteCompiler::new( + class_name.clone(), + true, + self.json_parse, + self.variable_scope.clone(), + self.lexical_scope.clone(), + false, + false, + self.interner, + self.in_with, + ); + field_compiler.code_block_flags |= CodeBlockFlags::HAS_FUNCTION_SCOPE; + let _ = field_compiler.push_scope(field.scope()); + let is_anonymous_function = if let Some(node) = &field.initializer() { + field_compiler.compile_expr(node, true); + node.is_anonymous_function_definition() } else { - self.emit_opcode(Opcode::PushUndefined); - } - let index = self.get_or_insert_private_name(*name); - self.emit_with_varying_operand(Opcode::DefinePrivateField, index); + field_compiler.emit_opcode(Opcode::PushUndefined); + false + }; + field_compiler.emit_opcode(Opcode::SetReturnValue); + + field_compiler.code_block_flags |= CodeBlockFlags::IN_CLASS_FIELD_INITIALIZER; + + let code = field_compiler.finish(); + let code = Gc::new(code); + + static_elements.push(StaticElement::StaticField { + code, + name_index: Some(name_index), + is_anonymous_function, + is_private: true, + }); } ClassElement::StaticBlock(block) => { let mut compiler = ByteCompiler::new( @@ -447,7 +475,12 @@ impl ByteCompiler<'_> { self.emit_with_varying_operand(Opcode::Call, 0); self.emit_opcode(Opcode::Pop); } - StaticElement::StaticField((code, name_index, is_anonymous_function)) => { + StaticElement::StaticField { + code, + name_index, + is_anonymous_function, + is_private, + } => { self.emit_opcode(Opcode::Dup); self.emit_opcode(Opcode::Dup); let index = self.push_function_to_constants(code); @@ -455,8 +488,16 @@ impl ByteCompiler<'_> { self.emit_opcode(Opcode::SetHomeObject); self.emit_with_varying_operand(Opcode::Call, 0); if let Some(name_index) = name_index { - self.emit_with_varying_operand(Opcode::DefineOwnPropertyByName, name_index); + if is_private { + self.emit_with_varying_operand(Opcode::DefinePrivateField, name_index); + } else { + self.emit_with_varying_operand( + Opcode::DefineOwnPropertyByName, + name_index, + ); + } } else { + // Assume the name is not private. Private names cannot be dynamically computed. self.emit(Opcode::RotateLeft, &[Operand::U8(5)]); if is_anonymous_function { self.emit_opcode(Opcode::Dup); diff --git a/core/engine/src/module/synthetic.rs b/core/engine/src/module/synthetic.rs index 99e8f278264..909a909c8b8 100644 --- a/core/engine/src/module/synthetic.rs +++ b/core/engine/src/module/synthetic.rs @@ -58,7 +58,8 @@ pub struct SyntheticModuleInitializer { impl std::fmt::Debug for SyntheticModuleInitializer { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ModuleInitializer").finish_non_exhaustive() + f.debug_struct("SyntheticModuleInitializer") + .finish_non_exhaustive() } } diff --git a/core/engine/src/native_function/continuation.rs b/core/engine/src/native_function/continuation.rs new file mode 100644 index 00000000000..8f671abc0f2 --- /dev/null +++ b/core/engine/src/native_function/continuation.rs @@ -0,0 +1,104 @@ +use boa_gc::{Finalize, Gc, Trace}; + +use crate::{Context, JsResult, JsValue}; + +#[derive(Trace, Finalize)] +#[boa_gc(unsafe_no_drop)] +pub(crate) enum CoroutineState { + Yielded(JsValue), + Done, +} + +trait TraceableCoroutine: Trace { + fn call(&self, value: JsResult, context: &mut Context) -> CoroutineState; +} + +#[derive(Trace, Finalize)] +struct Coroutine +where + F: Fn(JsResult, &T, &mut Context) -> CoroutineState, + T: Trace, +{ + // SAFETY: `NativeCoroutine`'s safe API ensures only `Copy` closures are stored; its unsafe API, + // on the other hand, explains the invariants to hold in order for this to be safe, shifting + // the responsibility to the caller. + #[unsafe_ignore_trace] + f: F, + captures: T, +} + +impl TraceableCoroutine for Coroutine +where + F: Fn(JsResult, &T, &mut Context) -> CoroutineState, + T: Trace, +{ + fn call(&self, result: JsResult, context: &mut Context) -> CoroutineState { + (self.f)(result, &self.captures, context) + } +} + +/// A callable Rust coroutine that can be used to await promises. +/// +/// # Caveats +/// +/// By limitations of the Rust language, the garbage collector currently cannot inspect closures +/// in order to trace their captured variables. This means that only [`Copy`] closures are 100% safe +/// to use. All other closures can also be stored in a `NativeCoroutine`, albeit by using an `unsafe` +/// API, but note that passing closures implicitly capturing traceable types could cause +/// **Undefined Behaviour**. +#[derive(Clone, Trace, Finalize)] +pub(crate) struct NativeCoroutine { + inner: Gc, +} + +impl std::fmt::Debug for NativeCoroutine { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("NativeCoroutine").finish_non_exhaustive() + } +} + +impl NativeCoroutine { + /// Creates a `NativeCoroutine` from a `Copy` closure and a list of traceable captures. + pub(crate) fn from_copy_closure_with_captures(closure: F, captures: T) -> Self + where + F: Fn(JsResult, &T, &mut Context) -> CoroutineState + Copy + 'static, + T: Trace + 'static, + { + // SAFETY: The `Copy` bound ensures there are no traceable types inside the closure. + unsafe { Self::from_closure_with_captures(closure, captures) } + } + + /// Create a new `NativeCoroutine` from a closure and a list of traceable captures. + /// + /// # Safety + /// + /// Passing a closure that contains a captured variable that needs to be traced by the garbage + /// collector could cause an use after free, memory corruption or other kinds of **Undefined + /// Behaviour**. See for a technical explanation + /// on why that is the case. + pub(crate) unsafe fn from_closure_with_captures(closure: F, captures: T) -> Self + where + F: Fn(JsResult, &T, &mut Context) -> CoroutineState + 'static, + T: Trace + 'static, + { + // Hopefully, this unsafe operation will be replaced by the `CoerceUnsized` API in the + // future: https://github.com/rust-lang/rust/issues/18598 + let ptr = Gc::into_raw(Gc::new(Coroutine { + f: closure, + captures, + })); + // SAFETY: The pointer returned by `into_raw` is only used to coerce to a trait object, + // meaning this is safe. + unsafe { + Self { + inner: Gc::from_raw(ptr), + } + } + } + + /// Calls this `NativeCoroutine`, forwarding the arguments to the corresponding function. + #[inline] + pub(crate) fn call(&self, result: JsResult, context: &mut Context) -> CoroutineState { + self.inner.call(result, context) + } +} diff --git a/core/engine/src/native_function.rs b/core/engine/src/native_function/mod.rs similarity index 99% rename from core/engine/src/native_function.rs rename to core/engine/src/native_function/mod.rs index 75f3b933abc..c2dd986e4c1 100644 --- a/core/engine/src/native_function.rs +++ b/core/engine/src/native_function/mod.rs @@ -20,6 +20,12 @@ use crate::{ Context, JsNativeError, JsObject, JsResult, JsValue, }; +#[cfg(feature = "experimental")] +mod continuation; + +#[cfg(feature = "experimental")] +pub(crate) use continuation::{CoroutineState, NativeCoroutine}; + /// The required signature for all native built-in function pointers. /// /// # Arguments diff --git a/core/engine/src/object/builtins/jspromise.rs b/core/engine/src/object/builtins/jspromise.rs index a42370033cc..d47cc44b013 100644 --- a/core/engine/src/object/builtins/jspromise.rs +++ b/core/engine/src/object/builtins/jspromise.rs @@ -1154,6 +1154,139 @@ impl JsPromise { } } } + + #[cfg(feature = "experimental")] + pub(crate) fn await_native( + &self, + continuation: crate::native_function::NativeCoroutine, + context: &mut Context, + ) { + use crate::{ + builtins::{async_generator::AsyncGenerator, generator::GeneratorContext}, + js_string, + object::FunctionObjectBuilder, + }; + use std::cell::Cell; + + let mut frame = context.vm.frame().clone(); + frame.environments = context.vm.environments.clone(); + frame.realm = context.realm().clone(); + + let gen_ctx = GeneratorContext { + call_frame: Some(frame), + stack: context.vm.stack.clone(), + }; + + // 3. Let fulfilledClosure be a new Abstract Closure with parameters (value) that captures asyncContext and performs the following steps when called: + // 4. Let onFulfilled be CreateBuiltinFunction(fulfilledClosure, 1, "", « »). + let on_fulfilled = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + // a. Let prevContext be the running execution context. + // b. Suspend prevContext. + // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. + // d. Resume the suspended evaluation of asyncContext using NormalCompletion(value) as the result of the operation that suspended it. + let continuation = &captures.0; + let mut gen = captures.1.take().expect("should only run once"); + + // NOTE: We need to get the object before resuming, since it could clear the stack. + let async_generator = gen.async_generator_object(); + + std::mem::swap(&mut context.vm.stack, &mut gen.stack); + let frame = gen.call_frame.take().expect("should have a call frame"); + context.vm.push_frame(frame); + + if let crate::native_function::CoroutineState::Yielded(value) = + continuation.call(Ok(args.get_or_undefined(0).clone()), context) + { + JsPromise::resolve(value, context) + .await_native(continuation.clone(), context); + } + + std::mem::swap(&mut context.vm.stack, &mut gen.stack); + gen.call_frame = context.vm.pop_frame(); + assert!(gen.call_frame.is_some()); + + if let Some(async_generator) = async_generator { + async_generator + .downcast_mut::() + .expect("must be async generator") + .context = Some(gen); + } + + // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context. + // f. Return undefined. + Ok(JsValue::undefined()) + }, + (continuation.clone(), Cell::new(Some(gen_ctx.clone()))), + ), + ) + .name(js_string!()) + .length(1) + .build(); + + // 5. Let rejectedClosure be a new Abstract Closure with parameters (reason) that captures asyncContext and performs the following steps when called: + // 6. Let onRejected be CreateBuiltinFunction(rejectedClosure, 1, "", « »). + let on_rejected = FunctionObjectBuilder::new( + context.realm(), + NativeFunction::from_copy_closure_with_captures( + |_this, args, captures, context| { + // a. Let prevContext be the running execution context. + // b. Suspend prevContext. + // c. Push asyncContext onto the execution context stack; asyncContext is now the running execution context. + // d. Resume the suspended evaluation of asyncContext using ThrowCompletion(reason) as the result of the operation that suspended it. + // e. Assert: When we reach this step, asyncContext has already been removed from the execution context stack and prevContext is the currently running execution context. + // f. Return undefined. + let continuation = &captures.0; + let mut gen = captures.1.take().expect("should only run once"); + + // NOTE: We need to get the object before resuming, since it could clear the stack. + let async_generator = gen.async_generator_object(); + + std::mem::swap(&mut context.vm.stack, &mut gen.stack); + let frame = gen.call_frame.take().expect("should have a call frame"); + context.vm.push_frame(frame); + + if let crate::native_function::CoroutineState::Yielded(value) = continuation + .call( + Err(JsError::from_opaque(args.get_or_undefined(0).clone())), + context, + ) + { + JsPromise::resolve(value, context) + .await_native(continuation.clone(), context); + } + + std::mem::swap(&mut context.vm.stack, &mut gen.stack); + gen.call_frame = context.vm.pop_frame(); + assert!(gen.call_frame.is_some()); + + if let Some(async_generator) = async_generator { + async_generator + .downcast_mut::() + .expect("must be async generator") + .context = Some(gen); + } + + Ok(JsValue::undefined()) + }, + (continuation, Cell::new(Some(gen_ctx))), + ), + ) + .name(js_string!()) + .length(1) + .build(); + + // 7. Perform PerformPromiseThen(promise, onFulfilled, onRejected). + Promise::perform_promise_then( + &self.inner, + Some(on_fulfilled), + Some(on_rejected), + None, + context, + ); + } } impl From for JsObject { diff --git a/core/engine/src/tests/class.rs b/core/engine/src/tests/class.rs index 3e44d6b6d8c..158e41dd12e 100644 --- a/core/engine/src/tests/class.rs +++ b/core/engine/src/tests/class.rs @@ -45,3 +45,30 @@ fn class_superclass_from_regex_error() { "superclass must be a constructor", )]); } + +// https://github.com/boa-dev/boa/issues/3055 +#[test] +fn class_can_access_super_from_static_initializer() { + run_test_actions([ + TestAction::run(indoc! {r#" + class a { + static field = "super field"; + } + + class b extends a { + static #field = super.field; + static get field() { + return this.#field; + } + } + + class c extends a { + static field = super.field; + } + + "#}), + TestAction::assert_eq("a.field", js_str!("super field")), + TestAction::assert_eq("b.field", js_str!("super field")), + TestAction::assert_eq("c.field", js_str!("super field")), + ]); +} diff --git a/core/engine/src/vm/call_frame/mod.rs b/core/engine/src/vm/call_frame/mod.rs index f541a0d296e..eabc75d5176 100644 --- a/core/engine/src/vm/call_frame/mod.rs +++ b/core/engine/src/vm/call_frame/mod.rs @@ -62,7 +62,7 @@ pub struct CallFrame { // SAFETY: Nothing requires tracing, so this is safe. #[unsafe_ignore_trace] - pub(crate) local_binings_initialized: Box<[bool]>, + pub(crate) local_bindings_initialized: Box<[bool]>, /// How many iterations a loop has done. pub(crate) loop_iteration_count: u64, @@ -154,16 +154,15 @@ impl CallFrame { environments: EnvironmentStack, realm: Realm, ) -> Self { - let local_binings_initialized = code_block.local_bindings_initialized.clone(); Self { - code_block, pc: 0, rp: 0, env_fp: 0, argument_count: 0, iterators: ThinVec::new(), binding_stack: Vec::new(), - local_binings_initialized, + local_bindings_initialized: code_block.local_bindings_initialized.clone(), + code_block, loop_iteration_count: 0, active_runnable, environments, @@ -235,6 +234,7 @@ impl CallFrame { .cloned() } + #[track_caller] pub(crate) fn promise_capability(&self, stack: &[JsValue]) -> Option { if !self.code_block().is_async() { return None; diff --git a/core/engine/src/vm/opcode/locals/mod.rs b/core/engine/src/vm/opcode/locals/mod.rs index 20d4e626331..989322a7515 100644 --- a/core/engine/src/vm/opcode/locals/mod.rs +++ b/core/engine/src/vm/opcode/locals/mod.rs @@ -14,7 +14,7 @@ impl PopIntoLocal { #[allow(clippy::unnecessary_wraps)] #[allow(clippy::needless_pass_by_value)] fn operation(dst: u32, context: &mut Context) -> JsResult { - context.vm.frame_mut().local_binings_initialized[dst as usize] = true; + context.vm.frame_mut().local_bindings_initialized[dst as usize] = true; let value = context.vm.pop(); let rp = context.vm.frame().rp; @@ -55,7 +55,7 @@ impl PushFromLocal { #[allow(clippy::unnecessary_wraps)] #[allow(clippy::needless_pass_by_value)] fn operation(dst: u32, context: &mut Context) -> JsResult { - if !context.vm.frame().local_binings_initialized[dst as usize] { + if !context.vm.frame().local_bindings_initialized[dst as usize] { return Err(JsNativeError::reference() .with_message("access to uninitialized binding") .into()); diff --git a/core/macros/Cargo.toml b/core/macros/Cargo.toml index 6f43d7dd8cb..81fc6c29872 100644 --- a/core/macros/Cargo.toml +++ b/core/macros/Cargo.toml @@ -12,6 +12,7 @@ rust-version.workspace = true proc-macro = true [dependencies] +cow-utils.workspace = true quote.workspace = true syn = { workspace = true, features = ["full"] } proc-macro2.workspace = true diff --git a/core/macros/src/lib.rs b/core/macros/src/lib.rs index b5247c4b59b..9f22e884a4f 100644 --- a/core/macros/src/lib.rs +++ b/core/macros/src/lib.rs @@ -6,6 +6,7 @@ )] #![cfg_attr(not(test), forbid(clippy::unwrap_used))] +use cow_utils::CowUtils; use proc_macro::TokenStream; use proc_macro2::Literal; use quote::{quote, ToTokens}; @@ -65,14 +66,14 @@ impl Parse for Static { let ident = if let Some(ident) = ident { syn::parse2::(ident.into_token_stream())? } else { - Ident::new(&literal.value().to_uppercase(), literal.span()) + Ident::new(&literal.value().cow_to_uppercase(), literal.span()) }; Ok(Self { literal, ident }) } Expr::Lit(expr) => match expr.lit { Lit::Str(str) => Ok(Self { - ident: Ident::new(&str.value().to_uppercase(), str.span()), + ident: Ident::new(&str.value().cow_to_uppercase(), str.span()), literal: str, }), _ => Err(syn::Error::new_spanned( @@ -108,9 +109,9 @@ pub fn static_syms(input: TokenStream) -> TokenStream { "Symbol for the \"{}\" string.", lit.literal .value() - .replace('<', r"\<") - .replace('>', r"\>") - .replace('*', r"\*") + .cow_replace('<', r"\<") + .cow_replace('>', r"\>") + .cow_replace('*', r"\*") ); let ident = &lit.ident; idx += 1; diff --git a/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs b/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs index d878d77d96f..cc992dd786a 100644 --- a/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs +++ b/core/parser/src/parser/statement/declaration/hoistable/class_decl/mod.rs @@ -416,7 +416,7 @@ where } } function::ClassElement::PrivateFieldDefinition(field) => { - if let Some(node) = field.field() { + if let Some(node) = field.initializer() { if contains(node, ContainsSymbol::SuperCall) { return Err(Error::lex(LexError::Syntax( "invalid super usage".into(), @@ -434,8 +434,8 @@ where )); } } - function::ClassElement::PrivateStaticFieldDefinition(name, init) => { - if let Some(node) = init { + function::ClassElement::PrivateStaticFieldDefinition(field) => { + if let Some(node) = field.initializer() { if contains(node, ContainsSymbol::SuperCall) { return Err(Error::lex(LexError::Syntax( "invalid super usage".into(), @@ -444,7 +444,7 @@ where } } if private_elements_names - .insert(name.description(), PrivateElement::StaticValue) + .insert(field.name().description(), PrivateElement::StaticValue) .is_some() { return Err(Error::general( @@ -455,7 +455,7 @@ where } function::ClassElement::FieldDefinition(field) | function::ClassElement::StaticFieldDefinition(field) => { - if let Some(field) = field.field() { + if let Some(field) = field.initializer() { if contains(field, ContainsSymbol::SuperCall) { return Err(Error::lex(LexError::Syntax( "invalid super usage".into(), @@ -1094,15 +1094,11 @@ where .as_slice(), ); rhs.set_anonymous_function_definition_name(&Identifier::new(function_name)); + let field = PrivateFieldDefinition::new(PrivateName::new(name), Some(rhs)); if r#static { - function::ClassElement::PrivateStaticFieldDefinition( - PrivateName::new(name), - Some(rhs), - ) + function::ClassElement::PrivateStaticFieldDefinition(field) } else { - function::ClassElement::PrivateFieldDefinition( - PrivateFieldDefinition::new(PrivateName::new(name), Some(rhs)), - ) + function::ClassElement::PrivateFieldDefinition(field) } } TokenKind::Punctuator(Punctuator::OpenParen) => { @@ -1141,15 +1137,11 @@ where } _ => { cursor.expect_semicolon("expected semicolon", interner)?; + let field = PrivateFieldDefinition::new(PrivateName::new(name), None); if r#static { - function::ClassElement::PrivateStaticFieldDefinition( - PrivateName::new(name), - None, - ) + function::ClassElement::PrivateStaticFieldDefinition(field) } else { - function::ClassElement::PrivateFieldDefinition( - PrivateFieldDefinition::new(PrivateName::new(name), None), - ) + function::ClassElement::PrivateFieldDefinition(field) } } } @@ -1274,7 +1266,7 @@ where // It is a Syntax Error if Initializer is present and ContainsArguments of Initializer is true. function::ClassElement::FieldDefinition(field) | function::ClassElement::StaticFieldDefinition(field) => { - if let Some(field) = field.field() { + if let Some(field) = field.initializer() { if contains_arguments(field) { return Err(Error::general( "'arguments' not allowed in class field definition", @@ -1283,8 +1275,9 @@ where } } } - function::ClassElement::PrivateFieldDefinition(field) => { - if let Some(node) = field.field() { + function::ClassElement::PrivateFieldDefinition(field) + | function::ClassElement::PrivateStaticFieldDefinition(field) => { + if let Some(node) = field.initializer() { if contains_arguments(node) { return Err(Error::general( "'arguments' not allowed in class field definition", @@ -1293,14 +1286,6 @@ where } } } - function::ClassElement::PrivateStaticFieldDefinition(_, Some(node)) => { - if contains_arguments(node) { - return Err(Error::general( - "'arguments' not allowed in class field definition", - position, - )); - } - } _ => {} } diff --git a/core/string/src/lib.rs b/core/string/src/lib.rs index eb206dfd92f..96653ed52b1 100644 --- a/core/string/src/lib.rs +++ b/core/string/src/lib.rs @@ -36,6 +36,7 @@ pub use crate::{ iter::Iter, str::{JsStr, JsStrVariant}, }; +use std::borrow::Cow; use std::fmt::Write; use std::{ alloc::{alloc, dealloc, Layout}, @@ -982,6 +983,7 @@ impl From<&[JsString]> for JsString { ) } } + impl From for JsString { #[inline] fn from(s: String) -> Self { @@ -989,6 +991,16 @@ impl From for JsString { } } +impl<'a> From> for JsString { + #[inline] + fn from(s: Cow<'a, str>) -> Self { + match s { + Cow::Borrowed(s) => s.into(), + Cow::Owned(s) => s.into(), + } + } +} + impl From<&[u16; N]> for JsString { #[inline] fn from(s: &[u16; N]) -> Self { diff --git a/test262_config.toml b/test262_config.toml index a9555448817..4c4a9898bdb 100644 --- a/test262_config.toml +++ b/test262_config.toml @@ -50,9 +50,6 @@ features = [ # https://github.com/tc39/proposal-duplicate-named-capturing-groups "regexp-duplicate-named-groups", - # https://github.com/tc39/proposal-array-from-async - "Array.fromAsync", - # https://github.com/tc39/proposal-json-parse-with-source "json-parse-with-source", diff --git a/tests/tester/Cargo.toml b/tests/tester/Cargo.toml index f2e5f165df3..ce6f3b9023b 100644 --- a/tests/tester/Cargo.toml +++ b/tests/tester/Cargo.toml @@ -29,6 +29,7 @@ phf = { workspace = true, features = ["macros"] } comfy-table.workspace = true serde_repr.workspace = true bus.workspace = true +cow-utils.workspace = true [features] default = ["boa_engine/intl_bundled", "boa_engine/experimental", "boa_engine/annex-b"] diff --git a/tests/tester/src/read.rs b/tests/tester/src/read.rs index 1cc85a3a925..f9af9bd4ce9 100644 --- a/tests/tester/src/read.rs +++ b/tests/tester/src/read.rs @@ -11,6 +11,7 @@ use color_eyre::{ eyre::{OptionExt, WrapErr}, Result, }; +use cow_utils::CowUtils; use rustc_hash::{FxBuildHasher, FxHashMap}; use serde::Deserialize; @@ -247,7 +248,7 @@ fn read_metadata(test: &Path) -> Result { let (metadata, _) = metadata .split_once("---*/") .ok_or_eyre("invalid test metadata")?; - let metadata = metadata.replace('\r', "\n"); + let metadata = metadata.cow_replace('\r', "\n"); serde_yaml::from_str(&metadata).map_err(Into::into) }