Skip to content

Commit

Permalink
feat(ecmascript): Implement a smattering of abstract operations
Browse files Browse the repository at this point in the history
  • Loading branch information
aapoalas committed Oct 29, 2023
1 parent cbc9f39 commit cdf9ca8
Show file tree
Hide file tree
Showing 16 changed files with 875 additions and 248 deletions.
1 change: 1 addition & 0 deletions nova_vm/src/ecmascript.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
mod abstract_operations;
pub mod builtins;
pub mod execution;
pub mod scripts_and_modules;
Expand Down
3 changes: 3 additions & 0 deletions nova_vm/src/ecmascript/abstract_operations.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod operations_on_objects;
mod testing_and_comparison;
mod type_conversion;
133 changes: 133 additions & 0 deletions nova_vm/src/ecmascript/abstract_operations/operations_on_objects.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
//! ## [7.3 Operations on Objects](https://tc39.es/ecma262/#sec-operations-on-objects)
use super::{testing_and_comparison::is_callable, type_conversion::to_object};
use crate::ecmascript::{
execution::{agent::JsError, Agent, JsResult},
types::{Function, InternalMethods, Object, PropertyKey, Value},
};

/// ### [7.3.1 MakeBasicObject ( internalSlotsList )](https://tc39.es/ecma262/#sec-makebasicobject)
///
/// The abstract operation MakeBasicObject takes argument internalSlotsList (a
/// List of internal slot names) and returns an Object. It is the source of all
/// ECMAScript objects that are created algorithmically, including both
/// ordinary objects and exotic objects. It factors out common steps used in
/// creating all objects, and centralizes object creation. It performs the
/// following steps when called:
///
/// > NOTE: Within this specification, exotic objects are created in abstract
/// > operations such as ArrayCreate and BoundFunctionCreate by first calling
/// > MakeBasicObject to obtain a basic, foundational object, and then
/// > overriding some or all of that object's internal methods. In order to
/// > encapsulate exotic object creation, the object's essential internal
/// > methods are never modified outside those operations.
pub(crate) fn make_basic_object(agent: &mut Agent, internal_slots_list: ()) -> Object {
// 1. Let obj be a newly created object with an internal slot for each name in internalSlotsList.
// 2. Set obj's essential internal methods to the default ordinary object definitions specified in 10.1.
// 3. Assert: If the caller will not be overriding both obj's [[GetPrototypeOf]] and [[SetPrototypeOf]] essential
// internal methods, then internalSlotsList contains [[Prototype]].
// 4. Assert: If the caller will not be overriding all of obj's [[SetPrototypeOf]], [[IsExtensible]], and
// [[PreventExtensions]] essential internal methods, then internalSlotsList contains [[Extensible]].
// 5. If internalSlotsList contains [[Extensible]], set obj.[[Extensible]] to true.
// 6. Return obj.
todo!()
}

/// ### [7.3.2 Get ( O, P )](https://tc39.es/ecma262/#sec-get-o-p)
///
/// The abstract operation Get takes arguments O (an Object) and P (a property
/// key) and returns either a normal completion containing an ECMAScript
/// language value or a throw completion. It is used to retrieve the value of a
/// specific property of an object.
pub(crate) fn get(agent: &mut Agent, o: Object, p: PropertyKey) -> JsResult<Value> {
// 1. Return ? O.[[Get]](P, O).
Object::get(agent, o, p, o.into())
}

/// ### [7.3.3 GetV ( V, P )](https://tc39.es/ecma262/#sec-getv)
///
/// The abstract operation GetV takes arguments V (an ECMAScript language
/// value) and P (a property key) and returns either a normal completion
/// containing an ECMAScript language value or a throw completion. It is used
/// to retrieve the value of a specific property of an ECMAScript language
/// value. If the value is not an object, the property lookup is performed
/// using a wrapper object appropriate for the type of the value.
pub(crate) fn get_v(agent: &mut Agent, v: Value, p: PropertyKey) -> JsResult<Value> {
// 1. Let O be ? ToObject(V).
let o = to_object(agent, v)?;
// 2. Return ? O.[[Get]](P, V).
Object::get(agent, o, p, o.into())
}

/// ### [7.3.11 GetMethod ( V, P )](https://tc39.es/ecma262/#sec-getmethod)
///
/// The abstract operation GetMethod takes arguments V (an ECMAScript language
/// value) and P (a property key) and returns either a normal completion
/// containing either a function object or undefined, or a throw completion. It
/// is used to get the value of a specific property of an ECMAScript language
/// value when the value of the property is expected to be a function.
pub(crate) fn get_method(
agent: &mut Agent,
v: Value,
p: PropertyKey,
) -> JsResult<Option<Function>> {
// 1. Let func be ? GetV(V, P).
let func = get_v(agent, v, p)?;
// 2. If func is either undefined or null, return undefined.
if func.is_undefined() || func.is_null() {
return Ok(None);
}
// 3. If IsCallable(func) is false, throw a TypeError exception.
if !is_callable(func) {
return Err(JsError {});
}
// 4. Return func.
match func {
Value::Function(idx) => Ok(Some(Function::from(idx))),
_ => unreachable!(),
}
}

/// ### [7.3.14 Call ( F, V \[ , argumentsList \] )](https://tc39.es/ecma262/#sec-call)
///
/// The abstract operation Call takes arguments F (an ECMAScript language
/// value) and V (an ECMAScript language value) and optional argument
/// argumentsList (a List of ECMAScript language values) and returns either a
/// normal completion containing an ECMAScript language value or a throw
/// completion. It is used to call the [[Call]] internal method of a function
/// object. F is the function object, V is an ECMAScript language value that is
/// the this value of the [[Call]], and argumentsList is the value passed to
/// the corresponding argument of the internal method. If argumentsList is not
/// present, a new empty List is used as its value.
pub(crate) fn call(
agent: &mut Agent,
f: Value,
v: Value,
arguments_list: Option<&[Value]>,
) -> JsResult<Value> {
// 1. If argumentsList is not present, set argumentsList to a new empty List.
let arguments_list = arguments_list.unwrap_or(&[]);
// 2. If IsCallable(F) is false, throw a TypeError exception.
if !is_callable(f) {
Err(JsError {})
} else {
// 3. Return ? F.[[Call]](V, argumentsList).
if let Value::Function(idx) = f {
Function::call(agent, Function(idx), v, arguments_list)
} else {
unreachable!();
}
}
}

/// Abstract operation Call specialized for a Function.
pub(crate) fn call_function(
agent: &mut Agent,
f: Function,
v: Value,
arguments_list: Option<&[Value]>,
) -> JsResult<Value> {
let arguments_list = arguments_list.unwrap_or(&[]);
Function::call(agent, f, v, arguments_list)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
//! ## [7.2 Testing and Comparison Operations](https://tc39.es/ecma262/#sec-testing-and-comparison-operations)
use crate::ecmascript::{
execution::{agent::JsError, Agent, JsResult},
types::Value,
};

/// ### [7.2.1 RequireObjectCoercible ( argument )](https://tc39.es/ecma262/#sec-requireobjectcoercible)
///
/// The abstract operation RequireObjectCoercible takes argument argument (an
/// ECMAScript language value) and returns either a normal completion
/// containing an ECMAScript language value or a throw completion. It throws an
/// error if argument is a value that cannot be converted to an Object using
/// ToObject. It is defined by [Table 14](https://tc39.es/ecma262/#table-requireobjectcoercible-results):
pub(crate) fn require_object_coercible(agent: &mut Agent, argument: Value) -> JsResult<Value> {
if argument.is_undefined() || argument.is_null() {
Err(JsError {})
} else {
Ok(argument)
}
}

/// ### [7.2.2 IsArray ( argument )](https://tc39.es/ecma262/#sec-isarray)
///
/// The abstract operation IsArray takes argument argument (an ECMAScript
/// language value) and returns either a normal completion containing a Boolean
/// or a throw completion.
pub(crate) fn is_array(agent: &Agent, argument: Value) -> JsResult<bool> {
// 1. If argument is not an Object, return false.
// 2. If argument is an Array exotic object, return true.
Ok(matches!(argument, Value::Array(_)))
// TODO: Proxy
// 3. If argument is a Proxy exotic object, then
// a. Perform ? ValidateNonRevokedProxy(argument).
// b. Let proxyTarget be argument.[[ProxyTarget]].
// c. Return ? IsArray(proxyTarget).
// 4. Return false.
}

/// ### [7.2.3 IsCallable ( argument )](https://tc39.es/ecma262/#sec-iscallable)
///
/// The abstract operation IsCallable takes argument argument (an ECMAScript
/// language value) and returns a Boolean. It determines if argument is a
/// callable function with a [[Call]] internal method.
pub(crate) fn is_callable(argument: Value) -> bool {
// 1. If argument is not an Object, return false.
// 2. If argument has a [[Call]] internal method, return true.
// 3. Return false.
matches!(argument, Value::Function(_))
}
167 changes: 167 additions & 0 deletions nova_vm/src/ecmascript/abstract_operations/type_conversion.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
//! ## [7.1 Type Conversion](https://tc39.es/ecma262/#sec-type-conversion)
//!
//! The ECMAScript language implicitly performs automatic type conversion as
//! needed. To clarify the semantics of certain constructs it is useful to
//! define a set of conversion abstract operations. The conversion abstract
//! operations are polymorphic; they can accept a value of any ECMAScript
//! language type. But no other specification types are used with these
//! operations.
//!
//! The BigInt type has no implicit conversions in the ECMAScript language;
//! programmers must call BigInt explicitly to convert values from other types.
use crate::{
ecmascript::{
execution::{agent::JsError, Agent, JsResult},
types::{Object, PropertyKey, String, Value},
},
heap::WellKnownSymbolIndexes,
};

use super::{
operations_on_objects::{call, call_function, get, get_method},
testing_and_comparison::is_callable,
};

#[derive(Debug, Clone, Copy)]
pub enum PreferredType {
String = 1,
Number,
}

/// ### [7.1.1 ToPrimitive ( input \[ , preferredType \] )](https://tc39.es/ecma262/#sec-toprimitive)
///
/// The abstract operation ToPrimitive takes argument input (an ECMAScript
/// language value) and optional argument preferredType (STRING or NUMBER) and
/// returns either a normal completion containing an ECMAScript language value
/// or a throw completion. It converts its input argument to a non-Object type.
/// If an object is capable of converting to more than one primitive type, it
/// may use the optional hint preferredType to favour that type. It performs
/// the following steps when called:
///
/// > NOTE: When ToPrimitive is called without a hint, then it generally
/// behaves as if the hint were NUMBER. However, objects may over-ride this
/// behaviour by defining a @@toPrimitive method. Of the objects defined in
/// this specification only Dates (see 21.4.4.45) and Symbol objects (see
/// 20.4.3.5) over-ride the default ToPrimitive behaviour. Dates treat the
/// absence of a hint as if the hint were STRING.
pub(crate) fn to_primitive(
agent: &mut Agent,
input: Value,
preferred_type: Option<PreferredType>,
) -> JsResult<Value> {
// 1. If input is an Object, then
if let Ok(input) = Object::try_from(input) {
// a. Let exoticToPrim be ? GetMethod(input, @@toPrimitive).
let exotic_to_prim = get_method(
agent,
input.into_value(),
PropertyKey::Symbol(WellKnownSymbolIndexes::ToPrimitive.into()),
)?;
// b. If exoticToPrim is not undefined, then
if let Some(exotic_to_prim) = exotic_to_prim {
let hint = match preferred_type {
// i. If preferredType is not present, then
// 1. Let hint be "default".
None => String::from_small_string("default"),
// ii. Else if preferredType is STRING, then
// 1. Let hint be "string".
Some(PreferredType::String) => String::from_small_string("string"),
// iii. Else,
// 1. Assert: preferredType is NUMBER.
// 2. Let hint be "number".
Some(PreferredType::Number) => String::from_small_string("number"),
};
// iv. Let result be ? Call(exoticToPrim, input, « hint »).
let result: Value =
call_function(agent, exotic_to_prim, input.into(), Some(&[hint.into()]))?;
if !result.is_object() {
// v. If result is not an Object, return result.
Ok(result)
} else {
// vi. Throw a TypeError exception.
Err(JsError {})
}
} else {
// c. If preferredType is not present, let preferredType be NUMBER.
// d. Return ? OrdinaryToPrimitive(input, preferredType).
ordinary_to_primitive(
agent,
input,
preferred_type.unwrap_or(PreferredType::Number),
)
}
} else {
// 2. Return input.
Ok(input)
}
}

/// #### [7.1.1.1 OrdinaryToPrimitive ( O, hint )](https://tc39.es/ecma262/#sec-ordinarytoprimitive)
///
/// The abstract operation OrdinaryToPrimitive takes arguments O (an Object)
/// and hint (STRING or NUMBER) and returns either a normal completion
/// containing an ECMAScript language value or a throw completion.
pub(crate) fn ordinary_to_primitive(
agent: &mut Agent,
o: Object,
hint: PreferredType,
) -> JsResult<Value> {
let to_string_key = PropertyKey::from(String::from_str(agent, "toString"));
let value_of_key = PropertyKey::from(String::from_small_string("valueOf"));
let method_names = match hint {
PreferredType::String => {
// 1. If hint is STRING, then
// a. Let methodNames be « "toString", "valueOf" ».
[to_string_key, value_of_key]
}
PreferredType::Number => {
// 2. Else,
// a. Let methodNames be « "valueOf", "toString" ».
[value_of_key, to_string_key]
}
};
// 3. For each element name of methodNames, do
for name in method_names {
// a. Let method be ? Get(O, name).
let method = get(agent, o, name)?;
// b. If IsCallable(method) is true, then
if is_callable(method) {
// i. Let result be ? Call(method, O).
let result: Value = call(agent, method, o.into(), None)?;
// ii. If result is not an Object, return result.
if !result.is_object() {
return Ok(result);
}
}
}
// 4. Throw a TypeError exception.
Err(JsError {})
}

/// ### [7.1.18 ToObject ( argument )](https://tc39.es/ecma262/#sec-toobject)
///
/// The abstract operation ToObject takes argument argument (an ECMAScript
/// language value) and returns either a normal completion containing an Object
/// or a throw completion. It converts argument to a value of type Object
/// according to [Table 13](https://tc39.es/ecma262/#table-toobject-conversions):
pub(crate) fn to_object(agent: &mut Agent, argument: Value) -> JsResult<Object> {
match argument {
Value::Undefined | Value::Null => Err(JsError {}),
// Return a new Boolean object whose [[BooleanData]] internal slot is set to argument.
Value::Boolean(_) => todo!("BooleanObject"),
// Return a new String object whose [[StringData]] internal slot is set to argument.
Value::String(_) => todo!("StringObject"),
Value::SmallString(_) => todo!("StringObject"),
// Return a new Symbol object whose [[SymbolnData]] internal slot is set to argument.
Value::Symbol(_) => todo!("SymbolObject"),
// Return a new Number object whose [[NumberData]] internal slot is set to argument.
Value::Number(_) => todo!("NumberObject"),
Value::Integer(_) => todo!("NumberObject"),
Value::Float(_) => todo!("NumberObject"),
// Return a new BigInt object whose [[BigIntData]] internal slot is set to argument.
Value::BigInt(_) => todo!("BigIntObject"),
Value::SmallBigInt(_) => todo!("BigIntObject"),
_ => Ok(Object::try_from(argument).unwrap()),
}
}
11 changes: 8 additions & 3 deletions nova_vm/src/ecmascript/builtins/array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@
mod data;

use super::{create_builtin_function, ArgumentsList, Behaviour, Builtin, BuiltinFunctionArgs};
use crate::ecmascript::{
execution::{Agent, JsResult},
types::{Object, Value},
use crate::{
ecmascript::{
execution::{Agent, JsResult},
types::{Object, Value},
},
heap::indexes::ArrayIndex,
};

pub use data::ArrayHeapData;

pub struct Array(ArrayIndex);

pub struct ArrayConstructor;

impl Builtin for ArrayConstructor {
Expand Down
Loading

0 comments on commit cdf9ca8

Please sign in to comment.