Skip to content

Commit a493485

Browse files
xcb3dElior Nguyen
andauthored
feat: implement [[IsHTMLDDA]] internal slot (Annex B §B.3.6) (boa-dev#4980)
This Pull Request implements the `[[IsHTMLDDA]]` internal slot (ECMAScript Annex B §B.3.6), enabling correct handling of `document.all`-like objects. It changes the following: - Add `IsHTMLDDA` marker struct with a custom `[[Call]]` internal method that returns `undefined` (`builtins/is_html_dda.rs`) - Modify `typeof` operator to return `"undefined"` for objects with `[[IsHTMLDDA]]` (`value/variant.rs`) - Modify `ToBoolean` to return `false` for objects with `[[IsHTMLDDA]]` (`value/inner/nan_boxed.rs`, `value/inner/legacy.rs`) - Modify abstract equality (`==`) so that `[[IsHTMLDDA]]` objects are loosely equal to `null` and `undefined` (`value/equality.rs`) - Fix `String.prototype.{match,matchAll,replace,replaceAll,search,split}` to treat `[[IsHTMLDDA]]` objects as "undefined or null" in the spec's "neither undefined nor null" check (`builtins/string/mod.rs`) - Add `$262.IsHTMLDDA` to the Test262 harness (`tests/tester/src/exec/js262.rs`) - Remove `IsHTMLDDA` from ignored features in `test262_config.toml` --------- Co-authored-by: Elior Nguyen <anhtunguyendev@wavenet.com.tw>
1 parent 95edd2e commit a493485

9 files changed

Lines changed: 129 additions & 5 deletions

File tree

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
//! Implementation of the `[[IsHTMLDDA]]` internal slot.
2+
//!
3+
//! Objects with this internal slot have special behavior:
4+
//! - `typeof` returns `"undefined"`
5+
//! - `ToBoolean` returns `false`
6+
//! - Abstract equality with `null` or `undefined` returns `true`
7+
//!
8+
//! More information:
9+
//! - [ECMAScript reference][spec]
10+
//!
11+
//! [spec]: https://tc39.es/ecma262/#sec-IsHTMLDDA-internal-slot
12+
13+
use boa_gc::{Finalize, Trace};
14+
15+
use crate::{
16+
JsResult, JsValue,
17+
object::{
18+
JsData, JsObject,
19+
internal_methods::{
20+
CallValue, InternalMethodCallContext, InternalObjectMethods, ORDINARY_INTERNAL_METHODS,
21+
},
22+
},
23+
};
24+
25+
/// Marker struct for objects with the `[[IsHTMLDDA]]` internal slot.
26+
///
27+
/// This is used by the `$262.IsHTMLDDA` test harness object and models the
28+
/// legacy `document.all` behavior per ECMAScript Annex B §B.3.6.
29+
///
30+
/// The object is callable — when called, it returns `undefined`.
31+
#[derive(Debug, Clone, Copy, Trace, Finalize)]
32+
#[boa_gc(empty_trace)]
33+
pub struct IsHTMLDDA;
34+
35+
impl JsData for IsHTMLDDA {
36+
fn internal_methods(&self) -> &'static InternalObjectMethods {
37+
static IS_HTML_DDA_INTERNAL_METHODS: InternalObjectMethods = InternalObjectMethods {
38+
__call__: is_html_dda_call,
39+
..ORDINARY_INTERNAL_METHODS
40+
};
41+
&IS_HTML_DDA_INTERNAL_METHODS
42+
}
43+
}
44+
45+
/// The `[[Call]]` internal method for `IsHTMLDDA` objects.
46+
///
47+
/// When called, simply returns `undefined`.
48+
#[allow(clippy::unnecessary_wraps)]
49+
fn is_html_dda_call(
50+
_obj: &JsObject,
51+
argument_count: usize,
52+
context: &mut InternalMethodCallContext<'_>,
53+
) -> JsResult<CallValue> {
54+
// Pop the arguments, function, and this from the stack.
55+
let _args = context
56+
.vm
57+
.stack
58+
.calling_convention_pop_arguments(argument_count);
59+
let _func = context.vm.stack.pop();
60+
let _this = context.vm.stack.pop();
61+
62+
// Push undefined as the return value.
63+
context.vm.stack.push(JsValue::undefined());
64+
65+
Ok(CallValue::Complete)
66+
}

core/engine/src/builtins/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ pub mod eval;
1515
pub mod function;
1616
pub mod generator;
1717
pub mod generator_function;
18+
#[cfg(feature = "annex-b")]
19+
pub mod is_html_dda;
1820
pub mod iterable;
1921
pub mod json;
2022
pub mod map;

core/engine/src/value/equality.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use super::{JsBigInt, JsObject, JsResult, JsValue, PreferredType};
2+
#[cfg(feature = "annex-b")]
3+
use crate::builtins::is_html_dda::IsHTMLDDA;
24
use crate::{Context, JsVariant, builtins::Number};
35
use std::collections::HashSet;
46

@@ -75,6 +77,20 @@ impl JsValue {
7577
true
7678
}
7779

80+
// B.3.6.2: Objects with [[IsHTMLDDA]] are loosely equal to null and undefined.
81+
#[cfg(feature = "annex-b")]
82+
(JsVariant::Object(ref obj), JsVariant::Null | JsVariant::Undefined)
83+
if obj.is::<IsHTMLDDA>() =>
84+
{
85+
true
86+
}
87+
#[cfg(feature = "annex-b")]
88+
(JsVariant::Null | JsVariant::Undefined, JsVariant::Object(ref obj))
89+
if obj.is::<IsHTMLDDA>() =>
90+
{
91+
true
92+
}
93+
7894
// 3. If Type(x) is Number and Type(y) is String, return the result of the comparison x == ! ToNumber(y).
7995
// 4. If Type(x) is String and Type(y) is Number, return the result of the comparison ! ToNumber(x) == y.
8096
//

core/engine/src/value/inner/legacy.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
//! interface as the `NanBoxedValue` type, but using an enum instead of
33
//! a 64-bits float.
44
5+
#[cfg(feature = "annex-b")]
6+
use crate::builtins::is_html_dda::IsHTMLDDA;
57
use crate::{JsBigInt, JsObject, JsSymbol, value::Type};
68
use boa_engine::JsVariant;
79
use boa_gc::{Finalize, Trace, custom_trace};
@@ -270,7 +272,11 @@ impl EnumBasedValue {
270272
#[inline]
271273
pub(crate) fn to_boolean(&self) -> bool {
272274
match self {
273-
Self::Object(_) | Self::Symbol(_) => true,
275+
Self::Symbol(_) => true,
276+
// Objects are truthy, unless they have [[IsHTMLDDA]] (Annex B §B.3.6.1).
277+
#[cfg(feature = "annex-b")]
278+
Self::Object(obj) if obj.is::<IsHTMLDDA>() => false,
279+
Self::Object(_) => true,
274280
Self::Null | Self::Undefined => false,
275281
Self::Integer32(n) => *n != 0,
276282
Self::Boolean(v) => *v,

core/engine/src/value/inner/nan_boxed.rs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,8 @@
106106
//! with regular NAN should happen.
107107
#![allow(clippy::inline_always)]
108108

109+
#[cfg(feature = "annex-b")]
110+
use crate::builtins::is_html_dda::IsHTMLDDA;
109111
use crate::{
110112
JsBigInt, JsObject, JsSymbol, JsVariant, bigint::RawBigInt, object::ErasedVTableObject,
111113
symbol::RawJsSymbol, value::Type,
@@ -756,8 +758,19 @@ impl NanBoxedValue {
756758
#[inline(always)]
757759
pub(crate) fn to_boolean(&self) -> bool {
758760
match self.value() & bits::MASK_KIND {
759-
// Objects and Symbols are always truthy.
760-
bits::MASK_OBJECT | bits::MASK_SYMBOL => true,
761+
// Symbols are always truthy.
762+
bits::MASK_SYMBOL => true,
763+
// Objects are truthy, unless they have [[IsHTMLDDA]] (Annex B §B.3.6.1).
764+
bits::MASK_OBJECT => {
765+
#[cfg(feature = "annex-b")]
766+
{
767+
// SAFETY: tag confirmed this is an Object.
768+
if unsafe { self.as_object_unchecked().is::<IsHTMLDDA>() } {
769+
return false;
770+
}
771+
}
772+
true
773+
}
761774
// Null and Undefined are always falsy.
762775
bits::MASK_OTHER => false,
763776
bits::MASK_INT32 => bits::untag_i32(self.value()) != 0,

core/engine/src/value/variant.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
#[cfg(feature = "annex-b")]
2+
use crate::builtins::is_html_dda::IsHTMLDDA;
13
use crate::{JsBigInt, JsObject, JsSymbol, JsValue};
24
use boa_engine::js_string;
35
use boa_string::JsString;
@@ -71,6 +73,10 @@ impl JsVariant {
7173
JsVariant::Undefined => "undefined",
7274
JsVariant::BigInt(_) => "bigint",
7375
JsVariant::Object(object) => {
76+
#[cfg(feature = "annex-b")]
77+
if object.is::<IsHTMLDDA>() {
78+
return "undefined";
79+
}
7480
if object.is_callable() {
7581
"function"
7682
} else {
@@ -92,6 +98,10 @@ impl JsVariant {
9298
JsVariant::Undefined => js_string!("undefined"),
9399
JsVariant::BigInt(_) => js_string!("bigint"),
94100
JsVariant::Object(object) => {
101+
#[cfg(feature = "annex-b")]
102+
if object.is::<IsHTMLDDA>() {
103+
return js_string!("undefined");
104+
}
95105
if object.is_callable() {
96106
js_string!("function")
97107
} else {

test262_config.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ features = [
88
### Unimplemented features:
99

1010
"FinalizationRegistry",
11-
"IsHTMLDDA",
1211
"symbols-as-weakmap-keys",
1312
"Intl.DisplayNames",
1413
"Intl.RelativeTimeFormat",

tests/tester/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ bus.workspace = true
3232
cow-utils.workspace = true
3333

3434
[features]
35-
default = ["boa_engine/intl_bundled", "boa_engine/experimental", "boa_engine/annex-b"]
35+
annex-b = ["boa_engine/annex-b"]
36+
default = ["boa_engine/intl_bundled", "boa_engine/experimental", "annex-b"]
3637

3738
[lints]
3839
workspace = true

tests/tester/src/exec/js262.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ use std::{
66
time::Duration,
77
};
88

9+
#[cfg(feature = "annex-b")]
10+
use boa_engine::builtins::is_html_dda::IsHTMLDDA;
911
use boa_engine::{
1012
Context, JsArgs, JsNativeError, JsResult, JsValue, Source,
1113
builtins::array_buffer::{ArrayBuffer, SharedArrayBuffer},
@@ -107,6 +109,15 @@ pub(super) fn register_js262(
107109
)
108110
.build();
109111

112+
#[cfg(feature = "annex-b")]
113+
js262
114+
.create_data_property_or_throw(
115+
js_string!("IsHTMLDDA"),
116+
JsObject::from_proto_and_data(None, IsHTMLDDA),
117+
context,
118+
)
119+
.expect("the IsHTMLDDA property must be definable");
120+
110121
context
111122
.register_global_property(
112123
js_string!("$262"),

0 commit comments

Comments
 (0)