From 615ae4e096879d00402d085cd4fd8a2cf0a44f8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Thu, 28 Sep 2023 01:52:04 +0000 Subject: [PATCH] Introduce experimental features (#3318) * Introduce experimental features * Fix tests --- .github/workflows/rust.yml | 6 +- boa_cli/Cargo.toml | 2 +- boa_engine/Cargo.toml | 3 + boa_engine/src/builtins/promise/mod.rs | 110 +++++++++++++++++++------ boa_engine/src/context/intrinsics.rs | 45 ++++++++-- boa_engine/src/vm/code_block.rs | 2 +- boa_tester/Cargo.toml | 2 +- boa_wasm/Cargo.toml | 2 +- test262_config.toml | 3 - 9 files changed, 134 insertions(+), 41 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index c027e4a83e1..cdfe3756210 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -33,7 +33,7 @@ jobs: uses: actions-rs/cargo@v1 with: command: tarpaulin - args: --workspace --features intl --ignore-tests --engine llvm --out xml + args: --workspace --features annex-b,intl,experimental --ignore-tests --engine llvm --out xml - name: Upload to codecov.io uses: codecov/codecov-action@v3 @@ -62,9 +62,9 @@ jobs: - name: Install latest nextest uses: taiki-e/install-action@nextest - name: Test with nextest - run: cargo nextest run --profile ci --cargo-profile ci --features intl + run: cargo nextest run --profile ci --cargo-profile ci --features annex-b,intl,experimental - name: Test docs - run: cargo test --doc --profile ci --features intl + run: cargo test --doc --profile ci --features annex-b,intl,experimental msrv: name: Minimum supported Rust version diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index cf87404e87d..6d30aa0164f 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -27,7 +27,7 @@ phf = { workspace = true, features = ["macros"] } pollster.workspace = true [features] -default = ["boa_engine/annex-b", "boa_engine/intl"] +default = ["boa_engine/annex-b", "boa_engine/experimental", "boa_engine/intl"] [target.x86_64-unknown-linux-gnu.dependencies] jemallocator.workspace = true diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 3f94ff1a323..f8a380fdd35 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -46,6 +46,9 @@ trace = [] # Enable Boa's additional ECMAScript features for web browsers. annex-b = ["boa_parser/annex-b"] +# Enable experimental features, like Stage 3 proposals. +experimental = [] + [dependencies] boa_interner.workspace = true boa_gc = { workspace = true, features = [ "thinvec" ] } diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 280e1191b19..ae5504001be 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -109,7 +109,7 @@ pub enum OperationType { /// the resolution value. /// /// [`Promise()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/Promise -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Finalize)] pub struct ResolvingFunctions { /// The `resolveFunc` parameter of the executor passed to `Promise()`. pub resolve: JsFunction, @@ -117,6 +117,14 @@ pub struct ResolvingFunctions { pub reject: JsFunction, } +// Manually implementing `Trace` to allow destructuring. +unsafe impl Trace for ResolvingFunctions { + custom_trace!(this, { + mark(&this.resolve); + mark(&this.reject); + }); +} + // ==================== Private API ==================== /// `IfAbruptRejectPromise ( value, capability )` @@ -155,16 +163,21 @@ pub(crate) use if_abrupt_reject_promise; /// - [ECMAScript reference][spec] /// /// [spec]: https://tc39.es/ecma262/#sec-promisecapability-records -#[derive(Debug, Clone, Trace, Finalize)] +#[derive(Debug, Clone, Finalize)] pub(crate) struct PromiseCapability { /// The `[[Promise]]` field. promise: JsObject, - /// The `[[Resolve]]` field. - resolve: JsFunction, + /// The resolving functions, + functions: ResolvingFunctions, +} - /// The `[[Reject]]` field. - reject: JsFunction, +// SAFETY: manually implementing `Trace` to allow destructuring. +unsafe impl Trace for PromiseCapability { + custom_trace!(this, { + mark(&this.promise); + mark(&this.functions); + }); } /// The internal `PromiseReaction` data type. @@ -297,8 +310,7 @@ impl PromiseCapability { // 10. Return promiseCapability. Ok(Self { promise, - resolve, - reject, + functions: ResolvingFunctions { resolve, reject }, }) } @@ -309,12 +321,12 @@ impl PromiseCapability { /// Returns the resolve function. pub(crate) const fn resolve(&self) -> &JsFunction { - &self.resolve + &self.functions.resolve } /// Returns the reject function. pub(crate) const fn reject(&self) -> &JsFunction { - &self.reject + &self.functions.reject } } @@ -326,7 +338,7 @@ impl IntrinsicObject for Promise { .name("get [Symbol.species]") .build(); - BuiltInBuilder::from_standard_constructor::(realm) + let builder = BuiltInBuilder::from_standard_constructor::(realm) .static_method(Self::all, "all", 1) .static_method(Self::all_settled, "allSettled", 1) .static_method(Self::any, "any", 1) @@ -347,8 +359,13 @@ impl IntrinsicObject for Promise { JsSymbol::to_string_tag(), Self::NAME, Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, - ) - .build(); + ); + + #[cfg(feature = "experimental")] + let builder = + builder.static_method(Self::with_resolvers, crate::js_string!("withResolvers"), 0); + + builder.build(); } fn get(intrinsics: &Intrinsics) -> JsObject { @@ -447,6 +464,42 @@ impl Promise { &self.state } + /// [`Promise.withResolvers ( )`][spec] + /// + /// Creates a new promise that is pending, and returns that promise plus the resolve and reject + /// functions associated with it. + /// + /// [spec]: https://tc39.es/proposal-promise-with-resolvers/#sec-promise.withResolvers + #[cfg(feature = "experimental")] + pub(crate) fn with_resolvers( + this: &JsValue, + _args: &[JsValue], + context: &mut Context<'_>, + ) -> JsResult { + // 1. Let C be the this value. + let c = this.as_object().ok_or_else(|| { + JsNativeError::typ().with_message("Promise.all() called on a non-object") + })?; + + // 2. Let promiseCapability be ? NewPromiseCapability(C). + let PromiseCapability { + promise, + functions: ResolvingFunctions { resolve, reject }, + } = PromiseCapability::new(c, context)?; + + // 3. Let obj be OrdinaryObjectCreate(%Object.prototype%). + // 4. Perform ! CreateDataPropertyOrThrow(obj, "promise", promiseCapability.[[Promise]]). + // 5. Perform ! CreateDataPropertyOrThrow(obj, "resolve", promiseCapability.[[Resolve]]). + // 6. Perform ! CreateDataPropertyOrThrow(obj, "reject", promiseCapability.[[Reject]]). + let obj = context.intrinsics().templates().with_resolvers().create( + ObjectData::ordinary(), + vec![promise.into(), resolve.into(), reject.into()], + ); + + // 7. Return obj. + Ok(obj.into()) + } + /// `Promise.all ( iterable )` /// /// More information: @@ -563,7 +616,7 @@ impl Promise { ); // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). - result_capability.resolve.call( + result_capability.functions.resolve.call( &JsValue::undefined(), &[values_array.into()], context, @@ -646,7 +699,7 @@ impl Promise { already_called: Rc::new(Cell::new(false)), index, values: values.clone(), - capability_resolve: result_capability.resolve.clone(), + capability_resolve: result_capability.functions.resolve.clone(), remaining_elements_count: remaining_elements_count.clone(), }, ), @@ -662,7 +715,10 @@ impl Promise { // s. Perform ? Invoke(nextPromise, "then", « onFulfilled, resultCapability.[[Reject]] »). next_promise.invoke( utf16!("then"), - &[on_fulfilled.into(), result_capability.reject.clone().into()], + &[ + on_fulfilled.into(), + result_capability.functions.reject.clone().into(), + ], context, )?; @@ -787,7 +843,7 @@ impl Promise { ); // 2. Perform ? Call(resultCapability.[[Resolve]], undefined, « valuesArray »). - result_capability.resolve.call( + result_capability.functions.resolve.call( &JsValue::undefined(), &[values_array.into()], context, @@ -887,7 +943,7 @@ impl Promise { already_called: Rc::new(Cell::new(false)), index, values: values.clone(), - capability: result_capability.resolve.clone(), + capability: result_capability.functions.resolve.clone(), remaining_elements: remaining_elements_count.clone(), }, ), @@ -973,7 +1029,7 @@ impl Promise { already_called: Rc::new(Cell::new(false)), index, values: values.clone(), - capability: result_capability.resolve.clone(), + capability: result_capability.functions.resolve.clone(), remaining_elements: remaining_elements_count.clone(), }, ), @@ -1208,7 +1264,7 @@ impl Promise { already_called: Rc::new(Cell::new(false)), index, errors: errors.clone(), - capability_reject: result_capability.reject.clone(), + capability_reject: result_capability.functions.reject.clone(), remaining_elements_count: remaining_elements_count.clone(), }, ), @@ -1224,7 +1280,10 @@ impl Promise { // s. Perform ? Invoke(nextPromise, "then", « resultCapability.[[Resolve]], onRejected »). next_promise.invoke( utf16!("then"), - &[result_capability.resolve.clone().into(), on_rejected.into()], + &[ + result_capability.functions.resolve.clone().into(), + on_rejected.into(), + ], context, )?; @@ -1344,8 +1403,8 @@ impl Promise { next_promise.invoke( utf16!("then"), &[ - result_capability.resolve.clone().into(), - result_capability.reject.clone().into(), + result_capability.functions.resolve.clone().into(), + result_capability.functions.reject.clone().into(), ], context, )?; @@ -1388,6 +1447,7 @@ impl Promise { // 3. Perform ? Call(promiseCapability.[[Reject]], undefined, « r »). promise_capability + .functions .reject .call(&JsValue::undefined(), &[e], context)?; @@ -1453,6 +1513,7 @@ impl Promise { // 3. Perform ? Call(promiseCapability.[[Resolve]], undefined, « x »). promise_capability + .functions .resolve .call(&JsValue::Undefined, &[x], context)?; @@ -2221,8 +2282,7 @@ fn new_promise_reaction_job( // g. Assert: promiseCapability is a PromiseCapability Record. let PromiseCapability { promise: _, - resolve, - reject, + functions: ResolvingFunctions { resolve, reject }, } = &promise_capability_record; match handler_result { diff --git a/boa_engine/src/context/intrinsics.rs b/boa_engine/src/context/intrinsics.rs index ef06b3e3479..87768a22e9d 100644 --- a/boa_engine/src/context/intrinsics.rs +++ b/boa_engine/src/context/intrinsics.rs @@ -4,6 +4,7 @@ use boa_gc::{Finalize, Trace}; use crate::{ builtins::{iterable::IteratorPrototypes, uri::UriFunctions}, + js_string, object::{ shape::{shared_shape::template::ObjectTemplate, RootShape}, JsFunction, JsObject, ObjectData, CONSTRUCTOR, PROTOTYPE, @@ -1100,6 +1101,9 @@ pub(crate) struct ObjectTemplates { function_with_prototype_without_proto: ObjectTemplate, namespace: ObjectTemplate, + + #[cfg(feature = "experimental")] + with_resolvers: ObjectTemplate, } impl ObjectTemplates { @@ -1110,7 +1114,7 @@ impl ObjectTemplates { let ordinary_object = ObjectTemplate::with_prototype(root_shape, constructors.object().prototype()); let mut array = ObjectTemplate::new(root_shape); - let length_property_key: PropertyKey = "length".into(); + let length_property_key: PropertyKey = js_string!("length").into(); array.property( length_property_key.clone(), Attribute::WRITABLE | Attribute::PERMANENT | Attribute::NON_ENUMERABLE, @@ -1129,7 +1133,7 @@ impl ObjectTemplates { ); string.set_prototype(constructors.string().prototype()); - let name_property_key: PropertyKey = "name".into(); + let name_property_key: PropertyKey = js_string!("name").into(); let mut function = ObjectTemplate::new(root_shape); function.property( length_property_key.clone(), @@ -1184,7 +1188,7 @@ impl ObjectTemplates { // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false, // [[Configurable]]: false }). unmapped_arguments.accessor( - "callee".into(), + js_string!("callee").into(), true, true, Attribute::NON_ENUMERABLE | Attribute::PERMANENT, @@ -1193,23 +1197,38 @@ impl ObjectTemplates { // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor { // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }). mapped_arguments.property( - "callee".into(), + js_string!("callee").into(), Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, ); let mut iterator_result = ordinary_object.clone(); iterator_result.property( - "value".into(), + js_string!("value").into(), Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE, ); iterator_result.property( - "done".into(), + js_string!("done").into(), Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::ENUMERABLE, ); let mut namespace = ObjectTemplate::new(root_shape); namespace.property(JsSymbol::to_string_tag().into(), Attribute::empty()); + #[cfg(feature = "experimental")] + let with_resolvers = { + let mut with_resolvers = ordinary_object.clone(); + + with_resolvers + // 4. Perform ! CreateDataPropertyOrThrow(obj, "promise", promiseCapability.[[Promise]]). + .property(js_string!("promise").into(), Attribute::all()) + // 5. Perform ! CreateDataPropertyOrThrow(obj, "resolve", promiseCapability.[[Resolve]]). + .property(js_string!("resolve").into(), Attribute::all()) + // 6. Perform ! CreateDataPropertyOrThrow(obj, "reject", promiseCapability.[[Reject]]). + .property(js_string!("reject").into(), Attribute::all()); + + with_resolvers + }; + Self { iterator_result, ordinary_object, @@ -1228,6 +1247,8 @@ impl ObjectTemplates { function_without_proto, function_with_prototype_without_proto, namespace, + #[cfg(feature = "experimental")] + with_resolvers, } } @@ -1404,4 +1425,16 @@ impl ObjectTemplates { pub(crate) const fn namespace(&self) -> &ObjectTemplate { &self.namespace } + + /// Cached object from the `Promise.withResolvers` method. + /// + /// Transitions: + /// + /// 1. `"promise"`: (`WRITABLE`, `ENUMERABLE`, `CONFIGURABLE`) + /// 2. `"resolve"`: (`WRITABLE`, `ENUMERABLE`, `CONFIGURABLE`) + /// 3. `"reject"`: (`WRITABLE`, `ENUMERABLE`, `CONFIGURABLE`) + #[cfg(feature = "experimental")] + pub(crate) const fn with_resolvers(&self) -> &ObjectTemplate { + &self.with_resolvers + } } diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index a08ab7a1c67..aa689f7b212 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -823,7 +823,7 @@ pub(crate) fn create_function_object( /// Creates a new function object. /// /// This is prefered over [`create_function_object`] if prototype is [`None`], -/// because it constructs the funtion from a pre-initialized object template, +/// because it constructs the function from a pre-initialized object template, /// with all the properties and prototype set. pub(crate) fn create_function_object_fast( code: Gc, diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index d45eef21add..8fc76245913 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -31,4 +31,4 @@ comfy-table = "7.0.1" serde_repr = "0.1.16" [features] -default = ["boa_engine/intl", "boa_engine/annex-b"] +default = ["boa_engine/intl", "boa_engine/experimental", "boa_engine/annex-b"] diff --git a/boa_wasm/Cargo.toml b/boa_wasm/Cargo.toml index da48035da74..a0a8446f581 100644 --- a/boa_wasm/Cargo.toml +++ b/boa_wasm/Cargo.toml @@ -19,7 +19,7 @@ chrono = { workspace = true, default-features = false, features = ["clock", "std console_error_panic_hook = "0.1.7" [features] -default = ["boa_engine/annex-b"] +default = ["boa_engine/annex-b", "boa_engine/intl", "boa_engine/experimental"] [lib] crate-type = ["cdylib", "lib"] diff --git a/test262_config.toml b/test262_config.toml index 53e8623d077..a4ac92e8bb9 100644 --- a/test262_config.toml +++ b/test262_config.toml @@ -66,9 +66,6 @@ features = [ # https://github.com/tc39/proposal-iterator-helpers "iterator-helpers", - # https://github.com/tc39/proposal-promise-with-resolvers - "promise-with-resolvers", - ### Non-standard "caller",