Browse Source

Implement Reflect built-in object (#1033)

* Implement Reflect built-in object

* Use receiver in get and set

* Implement Reflect.construct with newTarget

Co-authored-by: tofpie <tofpie@users.noreply.github.com>
pull/1170/head
tofpie 4 years ago committed by GitHub
parent
commit
b7ba0b3924
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 7
      boa/src/builtins/function/mod.rs
  2. 3
      boa/src/builtins/mod.rs
  3. 385
      boa/src/builtins/reflect/mod.rs
  4. 191
      boa/src/builtins/reflect/tests.rs
  5. 50
      boa/src/context.rs
  6. 2
      boa/src/object/gcobject.rs
  7. 42
      boa/src/object/internal_methods.rs

7
boa/src/builtins/function/mod.rs

@ -324,9 +324,10 @@ impl BuiltInFunctionObject {
// TODO?: 3.a. PrepareForTailCall // TODO?: 3.a. PrepareForTailCall
return context.call(this, &this_arg, &[]); return context.call(this, &this_arg, &[]);
} }
let arg_list = context let arg_array = arg_array.as_object().ok_or_else(|| {
.extract_array_properties(&arg_array)? context.construct_type_error("argList must be null, undefined or an object")
.map_err(|()| arg_array)?; })?;
let arg_list = arg_array.create_list_from_array_like(&[], context)?;
// TODO?: 5. PrepareForTailCall // TODO?: 5. PrepareForTailCall
context.call(this, &this_arg, &arg_list) context.call(this, &this_arg, &arg_list)
} }

3
boa/src/builtins/mod.rs

@ -17,6 +17,7 @@ pub mod math;
pub mod nan; pub mod nan;
pub mod number; pub mod number;
pub mod object; pub mod object;
pub mod reflect;
pub mod regexp; pub mod regexp;
pub mod string; pub mod string;
pub mod symbol; pub mod symbol;
@ -39,6 +40,7 @@ pub(crate) use self::{
number::Number, number::Number,
object::for_in_iterator::ForInIterator, object::for_in_iterator::ForInIterator,
object::Object as BuiltInObjectObject, object::Object as BuiltInObjectObject,
reflect::Reflect,
regexp::RegExp, regexp::RegExp,
string::String, string::String,
symbol::Symbol, symbol::Symbol,
@ -86,6 +88,7 @@ pub fn init(context: &mut Context) {
SyntaxError::init, SyntaxError::init,
EvalError::init, EvalError::init,
UriError::init, UriError::init,
Reflect::init,
#[cfg(feature = "console")] #[cfg(feature = "console")]
console::Console::init, console::Console::init,
]; ];

385
boa/src/builtins/reflect/mod.rs

@ -0,0 +1,385 @@
//! This module implements the global `Reflect` object.
//!
//! The `Reflect` global object is a built-in object that provides methods for interceptable
//! JavaScript operations.
//!
//! More information:
//! - [ECMAScript reference][spec]
//! - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-reflect-object
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect
use crate::{
builtins::{self, BuiltIn},
object::{Object, ObjectData, ObjectInitializer},
property::{Attribute, DataDescriptor},
BoaProfiler, Context, Result, Value,
};
#[cfg(test)]
mod tests;
/// Javascript `Reflect` object.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct Reflect;
impl BuiltIn for Reflect {
const NAME: &'static str = "Reflect";
fn attribute() -> Attribute {
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE
}
fn init(context: &mut Context) -> (&'static str, Value, Attribute) {
let _timer = BoaProfiler::global().start_event(Self::NAME, "init");
let to_string_tag = context.well_known_symbols().to_string_tag_symbol();
let object = ObjectInitializer::new(context)
.function(Self::apply, "apply", 3)
.function(Self::construct, "construct", 2)
.function(Self::define_property, "defineProperty", 3)
.function(Self::delete_property, "deleteProperty", 2)
.function(Self::get, "get", 2)
.function(
Self::get_own_property_descriptor,
"getOwnPropertyDescriptor",
2,
)
.function(Self::get_prototype_of, "getPrototypeOf", 1)
.function(Self::has, "has", 2)
.function(Self::is_extensible, "isExtensible", 1)
.function(Self::own_keys, "ownKeys", 1)
.function(Self::prevent_extensions, "preventExtensions", 1)
.function(Self::set, "set", 3)
.function(Self::set_prototype_of, "setPrototypeOf", 3)
.property(
to_string_tag,
Reflect::NAME,
Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build();
(Self::NAME, object.into(), Self::attribute())
}
}
impl Reflect {
/// Calls a target function with arguments.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.apply
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/apply
pub(crate) fn apply(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let undefined = Value::undefined();
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be a function"))?;
let this_arg = args.get(1).unwrap_or(&undefined);
let args_list = args
.get(2)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("args list must be an object"))?;
if !target.is_callable() {
return context.throw_type_error("target must be a function");
}
let args = args_list.create_list_from_array_like(&[], context)?;
target.call(this_arg, &args, context)
}
/// Calls a target function as a constructor with arguments.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.construct
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/construct
pub(crate) fn construct(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be a function"))?;
let args_list = args
.get(1)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("args list must be an object"))?;
if !target.is_constructable() {
return context.throw_type_error("target must be a constructor");
}
let new_target = if let Some(new_target) = args.get(2) {
if new_target.as_object().map(|o| o.is_constructable()) != Some(true) {
return context.throw_type_error("newTarget must be constructor");
}
new_target.clone()
} else {
target.clone().into()
};
let args = args_list.create_list_from_array_like(&[], context)?;
target.construct(&args, new_target, context)
}
/// Defines a property on an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.defineProperty
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/defineProperty
pub(crate) fn define_property(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let undefined = Value::undefined();
let mut target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get(1).unwrap_or(&undefined).to_property_key(context)?;
let prop_desc = args
.get(2)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("property descriptor must be an object"))?
.to_property_descriptor(context)?;
target
.define_own_property(key, prop_desc, context)
.map(|b| b.into())
}
/// Defines a property on an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.deleteproperty
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/deleteProperty
pub(crate) fn delete_property(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let undefined = Value::undefined();
let mut target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get(1).unwrap_or(&undefined).to_property_key(context)?;
Ok(target.delete(&key).into())
}
/// Gets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.get
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/get
pub(crate) fn get(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let undefined = Value::undefined();
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get(1).unwrap_or(&undefined).to_property_key(context)?;
let receiver = if let Some(receiver) = args.get(2).cloned() {
receiver
} else {
target.clone().into()
};
target.get(&key, receiver, context)
}
/// Gets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.getownpropertydescriptor
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getOwnPropertyDescriptor
pub(crate) fn get_own_property_descriptor(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
match args.get(0) {
Some(v) if v.is_object() => (),
_ => return context.throw_type_error("target must be an object"),
}
// This function is the same as Object.prototype.getOwnPropertyDescriptor, that why
// it is invoked here.
builtins::object::Object::get_own_property_descriptor(&Value::undefined(), args, context)
}
/// Gets the prototype of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.getprototypeof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/getPrototypeOf
pub(crate) fn get_prototype_of(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target.get_prototype_of())
}
/// Returns `true` if the object has the property, `false` otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.has
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/has
pub(crate) fn has(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args
.get(1)
.unwrap_or(&Value::undefined())
.to_property_key(context)?;
Ok(target.has_property(&key).into())
}
/// Returns `true` if the object is extensible, `false` otherwise.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.isextensible
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/isExtensible
pub(crate) fn is_extensible(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target.is_extensible().into())
}
/// Returns an array of object own property keys.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.ownkeys
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/ownKeys
pub(crate) fn own_keys(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let array_prototype = context.standard_objects().array_object().prototype();
let result: Value =
Object::with_prototype(array_prototype.into(), ObjectData::Array).into();
result.set_property(
"length",
DataDescriptor::new(
0,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT,
),
);
let keys = target.own_property_keys();
for (i, k) in keys.iter().enumerate() {
result.set_field(i, k, context)?;
}
Ok(result)
}
/// Prevents new properties from ever being added to an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.preventextensions
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/preventExtensions
pub(crate) fn prevent_extensions(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let mut target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
Ok(target.prevent_extensions().into())
}
/// Sets a property of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.set
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
pub(crate) fn set(_: &Value, args: &[Value], context: &mut Context) -> Result<Value> {
let undefined = Value::undefined();
let mut target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let key = args.get(1).unwrap_or(&undefined).to_property_key(context)?;
let value = args.get(2).unwrap_or(&undefined);
let receiver = if let Some(receiver) = args.get(3).cloned() {
receiver
} else {
target.clone().into()
};
Ok(target.set(key, value.clone(), receiver, context)?.into())
}
/// Sets the prototype of an object.
///
/// More information:
/// - [ECMAScript reference][spec]
/// - [MDN documentation][mdn]
///
/// [spec]: https://tc39.es/ecma262/#sec-reflect.setprototypeof
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/setPrototypeOf
pub(crate) fn set_prototype_of(
_: &Value,
args: &[Value],
context: &mut Context,
) -> Result<Value> {
let undefined = Value::undefined();
let mut target = args
.get(0)
.and_then(|v| v.as_object())
.ok_or_else(|| context.construct_type_error("target must be an object"))?;
let proto = args.get(1).unwrap_or(&undefined);
if !proto.is_null() && !proto.is_object() {
return context.throw_type_error("proto must be an object or null");
}
Ok(target.set_prototype_of(proto.clone()).into())
}
}

191
boa/src/builtins/reflect/tests.rs

@ -0,0 +1,191 @@
use crate::{forward, Context};
#[test]
fn apply() {
let mut context = Context::new();
let init = r#"
var called = {};
function f(n) { called.result = n };
Reflect.apply(f, undefined, [42]);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "called.result"), "42");
}
#[test]
fn construct() {
let mut context = Context::new();
let init = r#"
var called = {};
function f(n) { called.result = n };
Reflect.construct(f, [42]);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "called.result"), "42");
}
#[test]
fn define_property() {
let mut context = Context::new();
let init = r#"
let obj = {};
Reflect.defineProperty(obj, 'p', { value: 42 });
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "42");
}
#[test]
fn delete_property() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let deleted = Reflect.deleteProperty(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "undefined");
assert_eq!(forward(&mut context, "deleted"), "true");
}
#[test]
fn get() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 }
let p = Reflect.get(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "p"), "42");
}
#[test]
fn get_own_property_descriptor() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let desc = Reflect.getOwnPropertyDescriptor(obj, 'p');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "desc.value"), "42");
}
#[test]
fn get_prototype_of() {
let mut context = Context::new();
let init = r#"
function F() { this.p = 42 };
let f = new F();
let proto = Reflect.getPrototypeOf(f);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "proto.constructor.name"), "\"F\"");
}
#[test]
fn has() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let hasP = Reflect.has(obj, 'p');
let hasP2 = Reflect.has(obj, 'p2');
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "hasP"), "true");
assert_eq!(forward(&mut context, "hasP2"), "false");
}
#[test]
fn is_extensible() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let isExtensible = Reflect.isExtensible(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "isExtensible"), "true");
}
#[test]
fn own_keys() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let ownKeys = Reflect.ownKeys(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "ownKeys"), r#"[ "p" ]"#);
}
#[test]
fn prevent_extensions() {
let mut context = Context::new();
let init = r#"
let obj = { p: 42 };
let r = Reflect.preventExtensions(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "r"), "true");
}
#[test]
fn set() {
let mut context = Context::new();
let init = r#"
let obj = {};
Reflect.set(obj, 'p', 42);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "obj.p"), "42");
}
#[test]
fn set_prototype_of() {
let mut context = Context::new();
let init = r#"
function F() { this.p = 42 };
let obj = {}
Reflect.setPrototypeOf(obj, F);
let p = Reflect.getPrototypeOf(obj);
"#;
forward(&mut context, init);
assert_eq!(forward(&mut context, "p.name"), "\"F\"");
}

50
boa/src/context.rs

@ -9,7 +9,7 @@ use crate::{
}, },
class::{Class, ClassBuilder}, class::{Class, ClassBuilder},
exec::Interpreter, exec::Interpreter,
object::{GcObject, Object, ObjectData, PROTOTYPE}, object::{GcObject, Object, PROTOTYPE},
property::{Attribute, DataDescriptor, PropertyKey}, property::{Attribute, DataDescriptor, PropertyKey},
realm::Realm, realm::Realm,
syntax::{ syntax::{
@ -25,7 +25,6 @@ use crate::{
value::{RcString, RcSymbol, Value}, value::{RcString, RcSymbol, Value},
BoaProfiler, Executable, Result, BoaProfiler, Executable, Result,
}; };
use std::result::Result as StdResult;
#[cfg(feature = "console")] #[cfg(feature = "console")]
use crate::builtins::console::Console; use crate::builtins::console::Console;
@ -561,53 +560,6 @@ impl Context {
Ok(()) Ok(())
} }
/// Converts an array object into a rust vector of values.
///
/// This is useful for the spread operator, for any other object an `Err` is returned
/// TODO: Not needed for spread of arrays. Check in the future for Map and remove if necessary
pub(crate) fn extract_array_properties(
&mut self,
value: &Value,
) -> Result<StdResult<Vec<Value>, ()>> {
if let Value::Object(ref x) = value {
// Check if object is array
if let ObjectData::Array = x.borrow().data {
let length = value.get_field("length", self)?.as_number().unwrap() as i32;
let values = (0..length)
.map(|idx| value.get_field(idx, self))
.collect::<Result<Vec<_>>>()?;
return Ok(Ok(values));
}
// Check if object is a Map
else if let ObjectData::Map(ref map) = x.borrow().data {
let values = map
.iter()
.map(|(key, value)| {
// Construct a new array containing the key-value pair
let array = Value::new_object(self);
array.set_data(ObjectData::Array);
array.as_object().expect("object").set_prototype_instance(
self.realm()
.environment
.get_binding_value("Array")
.expect("Array was not initialized")
.get_field(PROTOTYPE, self)?,
);
array.set_field(0, key, self)?;
array.set_field(1, value, self)?;
array.set_field("length", Value::from(2), self)?;
Ok(array)
})
.collect::<Result<Vec<_>>>()?;
return Ok(Ok(values));
}
return Ok(Err(()));
}
Ok(Err(()))
}
/// <https://tc39.es/ecma262/#sec-hasproperty> /// <https://tc39.es/ecma262/#sec-hasproperty>
#[inline] #[inline]
pub(crate) fn has_property(&self, obj: &Value, key: &PropertyKey) -> bool { pub(crate) fn has_property(&self, obj: &Value, key: &PropertyKey) -> bool {

2
boa/src/object/gcobject.rs

@ -417,7 +417,7 @@ impl GcObject {
} }
} }
/// Convert the object to a `PropertyDescritptor` /// Convert the object to a `PropertyDescriptor`
/// ///
/// # Panics /// # Panics
/// ///

42
boa/src/object/internal_methods.rs

@ -8,7 +8,7 @@
use crate::{ use crate::{
object::{GcObject, Object, ObjectData}, object::{GcObject, Object, ObjectData},
property::{AccessorDescriptor, Attribute, DataDescriptor, PropertyDescriptor, PropertyKey}, property::{AccessorDescriptor, Attribute, DataDescriptor, PropertyDescriptor, PropertyKey},
value::{same_value, Value}, value::{same_value, Type, Value},
BoaProfiler, Context, Result, BoaProfiler, Context, Result,
}; };
@ -560,6 +560,46 @@ impl GcObject {
pub fn is_global(&self) -> bool { pub fn is_global(&self) -> bool {
matches!(self.borrow().data, ObjectData::Global) matches!(self.borrow().data, ObjectData::Global)
} }
/// It is used to create List value whose elements are provided by the indexed properties of
/// self.
///
/// More information:
/// - [EcmaScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createlistfromarraylike
pub(crate) fn create_list_from_array_like(
&self,
element_types: &[Type],
context: &mut Context,
) -> Result<Vec<Value>> {
let types = if element_types.is_empty() {
&[
Type::Undefined,
Type::Null,
Type::Boolean,
Type::String,
Type::Symbol,
Type::Number,
Type::BigInt,
Type::Symbol,
]
} else {
element_types
};
let len = self
.get(&"length".into(), self.clone().into(), context)?
.to_length(context)?;
let mut list = Vec::with_capacity(len);
for index in 0..len {
let next = self.get(&index.into(), self.clone().into(), context)?;
if !types.contains(&next.get_type()) {
return Err(context.construct_type_error("bad type"));
}
list.push(next.clone());
}
Ok(list)
}
} }
impl Object { impl Object {

Loading…
Cancel
Save