diff --git a/boa/src/builtins/object/internal_methods_trait.rs b/boa/src/builtins/object/internal_methods_trait.rs index 5a03cb5b72..6d8a2a5a5e 100644 --- a/boa/src/builtins/object/internal_methods_trait.rs +++ b/boa/src/builtins/object/internal_methods_trait.rs @@ -6,7 +6,7 @@ //! [spec]: https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots use crate::builtins::{ - object::{Object, PROTOTYPE}, + object::{Object, INSTANCE_PROTOTYPE}, property::Property, value::{to_value, Value, ValueData}, }; @@ -178,7 +178,7 @@ pub trait ObjectInternalMethods { /// Returns either the prototype or null /// https://tc39.es/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-getprototypeof fn get_prototype_of(&self) -> Value { - self.get_internal_slot(PROTOTYPE) + self.get_internal_slot(INSTANCE_PROTOTYPE) } fn define_own_property(&mut self, property_key: String, desc: Property) -> bool; diff --git a/boa/src/builtins/value/mod.rs b/boa/src/builtins/value/mod.rs index ff20598631..cb39ba0a77 100644 --- a/boa/src/builtins/value/mod.rs +++ b/boa/src/builtins/value/mod.rs @@ -13,7 +13,7 @@ use crate::builtins::{ }, property::Property, }; -use gc::{Gc, GcCell}; +use gc::{Gc, GcCell, GcCellRef}; use gc_derive::{Finalize, Trace}; use serde_json::{map::Map, Number as JSONNumber, Value as JSONValue}; use std::{ @@ -238,6 +238,13 @@ impl ValueData { } } + pub fn as_object(&self) -> Option> { + match *self { + ValueData::Object(ref o) => Some(o.borrow()), + _ => None, + } + } + /// remove_prop removes a property from a Value object. /// /// It will return a boolean based on if the value was removed, if there was no value to remove false is returned diff --git a/boa/src/exec/mod.rs b/boa/src/exec/mod.rs index ce889777b8..6b2487b856 100644 --- a/boa/src/exec/mod.rs +++ b/boa/src/exec/mod.rs @@ -11,6 +11,7 @@ use crate::{ internal_methods_trait::ObjectInternalMethods, ObjectKind, INSTANCE_PROTOTYPE, PROTOTYPE, }, + property::Property, value::{from_value, to_value, ResultValue, Value, ValueData}, }, environment::lexical_environment::{ @@ -347,6 +348,13 @@ impl Executor for Interpreter { CompOp::GreaterThanOrEqual => v_a.to_num() >= v_b.to_num(), CompOp::LessThan => v_a.to_num() < v_b.to_num(), CompOp::LessThanOrEqual => v_a.to_num() <= v_b.to_num(), + CompOp::In => { + if !v_b.is_object() { + panic!("TypeError: {} is not an Object.", v_b); + } + let key = self.to_property_key(v_a); + self.has_property(v_b, &key) + } })) } Node::BinOp(BinOp::Log(ref op), ref a, ref b) => { @@ -739,6 +747,31 @@ impl Interpreter { } } + /// The abstract operation ToPropertyKey takes argument argument. It converts argument to a value that can be used as a property key. + /// https://tc39.es/ecma262/#sec-topropertykey + #[allow(clippy::wrong_self_convention)] + pub fn to_property_key(&mut self, value: &Value) -> Value { + let key = self.to_primitive(value, Some("string")); + if key.is_symbol() { + key + } else { + self.to_string(&key) + } + } + + /// https://tc39.es/ecma262/#sec-hasproperty + pub fn has_property(&self, obj: &Value, key: &Value) -> bool { + if let Some(obj) = obj.as_object() { + if !Property::is_property_key(key) { + false + } else { + obj.has_property(key) + } + } else { + false + } + } + /// The abstract operation ToObject converts argument to a value of type Object /// https://tc39.es/ecma262/#sec-toobject #[allow(clippy::wrong_self_convention)] diff --git a/boa/src/exec/tests.rs b/boa/src/exec/tests.rs index 0932e79500..720cf8c563 100644 --- a/boa/src/exec/tests.rs +++ b/boa/src/exec/tests.rs @@ -351,3 +351,72 @@ fn test_unary_post() { "#; assert_eq!(exec(execs_after), String::from("true")); } + +#[cfg(test)] +mod in_operator { + use super::*; + #[test] + fn test_p_in_o() { + let p_in_o = r#" + var o = {a: 'a'}; + var p = 'a'; + p in o + "#; + assert_eq!(exec(p_in_o), String::from("true")); + } + + #[test] + fn test_p_in_property_chain() { + let p_in_o = r#" + var o = {}; + var p = 'toString'; + p in o + "#; + assert_eq!(exec(p_in_o), String::from("true")); + } + + #[test] + fn test_p_not_in_o() { + let p_not_in_o = r#" + var o = {a: 'a'}; + var p = 'b'; + p in o + "#; + assert_eq!(exec(p_not_in_o), String::from("false")); + } + + #[test] + fn test_number_in_array() { + // Note: this is valid because the LHS is converted to a prop key with ToPropertyKey + // and arrays are just fancy objects like {'0': 'a'} + let num_in_array = r#" + var n = 0; + var a = ['a']; + n in a + "#; + assert_eq!(exec(num_in_array), String::from("true")); + } + + #[test] + #[ignore] + fn test_symbol_in_object() { + // FIXME: this scenario works in Firefox's console, this is probably an issue + // with Symbol comparison. + let sym_in_object = r#" + var sym = Symbol('hi'); + var o = {}; + o[sym] = 'hello'; + sym in o + "#; + assert_eq!(exec(sym_in_object), String::from("true")); + } + + #[test] + #[should_panic(expected = "TypeError: undefined is not an Object.")] + fn test_should_type_error_when_rhs_not_object() { + let scenario = r#" + 'fail' in undefined + "#; + exec(scenario); + } +} diff --git a/boa/src/syntax/ast/keyword.rs b/boa/src/syntax/ast/keyword.rs index 2ef9599cbc..a76195e5a8 100644 --- a/boa/src/syntax/ast/keyword.rs +++ b/boa/src/syntax/ast/keyword.rs @@ -7,7 +7,9 @@ //! [spec]: https://www.ecma-international.org/ecma-262/#sec-keywords //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Lexical_grammar#Keywords +use crate::syntax::ast::op::{BinOp, CompOp}; use std::{ + convert::TryInto, error, fmt::{Display, Error, Formatter}, str::FromStr, @@ -433,6 +435,23 @@ pub enum Keyword { Yield, } +impl Keyword { + pub fn as_binop(self) -> Option { + match self { + Keyword::In => Some(BinOp::Comp(CompOp::In)), + _ => None, + } + } +} + +impl TryInto for Keyword { + type Error = String; + fn try_into(self) -> Result { + self.as_binop() + .ok_or_else(|| format!("No binary operation for {}", self)) + } +} + #[derive(Debug, Clone, Copy)] pub struct KeywordError; impl Display for KeywordError { diff --git a/boa/src/syntax/ast/op.rs b/boa/src/syntax/ast/op.rs index 34552aab4f..f8a67cffbb 100644 --- a/boa/src/syntax/ast/op.rs +++ b/boa/src/syntax/ast/op.rs @@ -579,6 +579,19 @@ pub enum CompOp { /// [spec]: https://tc39.es/ecma262/#prod-RelationalExpression /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Comparison_Operators#Less_than_or_equal_operator LessThanOrEqual, + /// The `in` operator returns true if the specified property is in the specified object or its prototype chain. + /// + /// Syntax: `prop in object` + /// + /// Returns `true` the specified property is in the specified object or its prototype chain. + /// + /// More information: + /// - [ECMAScript reference][spec] + /// - [MDN documentation][mdn] + /// + /// [spec]: https://tc39.es/ecma262/#prod-RelationalExpression + /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/in + In, } impl Display for CompOp { @@ -595,6 +608,7 @@ impl Display for CompOp { Self::GreaterThanOrEqual => ">=", Self::LessThan => "<", Self::LessThanOrEqual => "<=", + Self::In => "in", } ) } @@ -727,7 +741,8 @@ impl Operator for BinOp { Self::Comp(CompOp::LessThan) | Self::Comp(CompOp::LessThanOrEqual) | Self::Comp(CompOp::GreaterThan) - | Self::Comp(CompOp::GreaterThanOrEqual) => 8, + | Self::Comp(CompOp::GreaterThanOrEqual) + | Self::Comp(CompOp::In) => 8, Self::Comp(CompOp::Equal) | Self::Comp(CompOp::NotEqual) | Self::Comp(CompOp::StrictEqual) diff --git a/boa/src/syntax/ast/punc.rs b/boa/src/syntax/ast/punc.rs index 6b237e3305..dab0dfcf70 100644 --- a/boa/src/syntax/ast/punc.rs +++ b/boa/src/syntax/ast/punc.rs @@ -6,7 +6,10 @@ //! [spec]: https://tc39.es/ecma262/#prod-Punctuator use crate::syntax::ast::op::{AssignOp, BinOp, BitOp, CompOp, LogOp, NumOp}; -use std::fmt::{Display, Error, Formatter}; +use std::{ + convert::TryInto, + fmt::{Display, Error, Formatter}, +}; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; @@ -169,6 +172,14 @@ impl Punctuator { } } +impl TryInto for Punctuator { + type Error = String; + fn try_into(self) -> Result { + self.as_binop() + .ok_or_else(|| format!("No binary operation for {}", self)) + } +} + impl Display for Punctuator { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error> { write!( diff --git a/boa/src/syntax/parser/expression/mod.rs b/boa/src/syntax/parser/expression/mod.rs index d225fe9d45..464784449b 100644 --- a/boa/src/syntax/parser/expression/mod.rs +++ b/boa/src/syntax/parser/expression/mod.rs @@ -18,7 +18,23 @@ mod update; use self::assignment::ExponentiationExpression; pub(super) use self::{assignment::AssignmentExpression, primary::Initializer}; use super::{AllowAwait, AllowIn, AllowYield, Cursor, ParseResult, TokenParser}; -use crate::syntax::ast::{node::Node, punc::Punctuator, token::TokenKind}; +use crate::syntax::ast::{keyword::Keyword, node::Node, punc::Punctuator, token::TokenKind}; + +// For use in the expression! macro to allow for both Punctuator and Keyword parameters. +// Always returns false. +impl PartialEq for Punctuator { + fn eq(&self, _other: &Keyword) -> bool { + false + } +} + +// For use in the expression! macro to allow for both Punctuator and Keyword parameters. +// Always returns false. +impl PartialEq for Keyword { + fn eq(&self, _other: &Punctuator) -> bool { + false + } +} /// Generates an expression parser. /// @@ -38,7 +54,15 @@ macro_rules! expression { ($name:ident, $lower:ident, [$( $op:path ),*], [$( $lo TokenKind::Punctuator(op) if $( op == $op )||* => { let _ = cursor.next().expect("token disappeared"); lhs = Node::bin_op( - op.as_binop().expect("could not get binary operation"), + op.as_binop().expect("Could not get binary operation."), + lhs, + $lower::new($( self.$low_param ),*).parse(cursor)? + ) + } + TokenKind::Keyword(op) if $( op == $op )||* => { + let _ = cursor.next().expect("token disappeared"); + lhs = Node::bin_op( + op.as_binop().expect("Could not get binary operation."), lhs, $lower::new($( self.$low_param ),*).parse(cursor)? ) @@ -360,7 +384,8 @@ expression!( Punctuator::LessThan, Punctuator::GreaterThan, Punctuator::LessThanOrEq, - Punctuator::GreaterThanOrEq + Punctuator::GreaterThanOrEq, + Keyword::In ], [allow_yield, allow_await] ); diff --git a/boa/src/syntax/parser/expression/tests.rs b/boa/src/syntax/parser/expression/tests.rs index 3b104ec885..11c21ee8e6 100644 --- a/boa/src/syntax/parser/expression/tests.rs +++ b/boa/src/syntax/parser/expression/tests.rs @@ -1,6 +1,6 @@ use crate::syntax::{ ast::node::Node, - ast::op::{AssignOp, BinOp, BitOp, NumOp}, + ast::op::{AssignOp, BinOp, BitOp, CompOp, NumOp}, parser::tests::check_parser, }; @@ -291,3 +291,47 @@ fn check_assign_operations() { )], ); } + +#[test] +fn check_relational_operations() { + check_parser( + "a < b", + &[Node::bin_op( + BinOp::Comp(CompOp::LessThan), + Node::Local(String::from("a")), + Node::Local(String::from("b")), + )], + ); + check_parser( + "a > b", + &[Node::bin_op( + BinOp::Comp(CompOp::GreaterThan), + Node::Local(String::from("a")), + Node::Local(String::from("b")), + )], + ); + check_parser( + "a <= b", + &[Node::bin_op( + BinOp::Comp(CompOp::LessThanOrEqual), + Node::Local(String::from("a")), + Node::Local(String::from("b")), + )], + ); + check_parser( + "a >= b", + &[Node::bin_op( + BinOp::Comp(CompOp::GreaterThanOrEqual), + Node::Local(String::from("a")), + Node::Local(String::from("b")), + )], + ); + check_parser( + "p in o", + &[Node::bin_op( + BinOp::Comp(CompOp::In), + Node::Local(String::from("p")), + Node::Local(String::from("o")), + )], + ); +} diff --git a/boa/src/syntax/parser/tests.rs b/boa/src/syntax/parser/tests.rs index 088f63ebd8..cbc38564cc 100644 --- a/boa/src/syntax/parser/tests.rs +++ b/boa/src/syntax/parser/tests.rs @@ -39,7 +39,7 @@ fn check_construct_call_precedence() { } #[test] -fn assing_operator_precedence() { +fn assign_operator_precedence() { check_parser( "a = a + 1", &[Node::assign(