boa_engine/builtins/function/
arguments.rs

1use crate::{
2    bytecompiler::ToJsString,
3    environments::DeclarativeEnvironment,
4    object::{
5        internal_methods::{
6            ordinary_define_own_property, ordinary_delete, ordinary_get, ordinary_get_own_property,
7            ordinary_set, ordinary_try_get, InternalMethodContext, InternalObjectMethods,
8            ORDINARY_INTERNAL_METHODS,
9        },
10        JsObject,
11    },
12    property::{DescriptorKind, PropertyDescriptor, PropertyKey},
13    Context, JsData, JsResult, JsValue,
14};
15use boa_ast::{function::FormalParameterList, operations::bound_names, scope::Scope};
16use boa_gc::{Finalize, Gc, Trace};
17use boa_interner::Interner;
18use rustc_hash::FxHashMap;
19use thin_vec::{thin_vec, ThinVec};
20
21#[derive(Debug, Copy, Clone, Trace, Finalize, JsData)]
22#[boa_gc(empty_trace)]
23pub(crate) struct UnmappedArguments;
24
25impl UnmappedArguments {
26    /// Creates a new unmapped Arguments ordinary object.
27    ///
28    /// More information:
29    ///  - [ECMAScript reference][spec]
30    ///
31    /// [spec]: https://tc39.es/ecma262/#sec-createunmappedargumentsobject
32    #[allow(clippy::new_ret_no_self)]
33    pub(crate) fn new(arguments_list: &[JsValue], context: &mut Context) -> JsObject {
34        // 1. Let len be the number of elements in argumentsList.
35        let len = arguments_list.len();
36
37        let values_function = context.intrinsics().objects().array_prototype_values();
38        let throw_type_error = context.intrinsics().objects().throw_type_error();
39
40        // 2. Let obj be ! OrdinaryObjectCreate(%Object.prototype%, « [[ParameterMap]] »).
41        // 3. Set obj.[[ParameterMap]] to undefined.
42        // skipped because the `Arguments` enum ensures ordinary argument objects don't have a `[[ParameterMap]]`
43        let obj = context
44            .intrinsics()
45            .templates()
46            .unmapped_arguments()
47            .create(
48                Self,
49                vec![
50                    // 4. Perform DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len),
51                    // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
52                    len.into(),
53                    // 7. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor {
54                    // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false,
55                    // [[Configurable]]: true }).
56                    values_function.into(),
57                    // 8. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {
58                    // [[Get]]: %ThrowTypeError%, [[Set]]: %ThrowTypeError%, [[Enumerable]]: false,
59                    // [[Configurable]]: false }).
60                    throw_type_error.clone().into(), // get
61                    throw_type_error.into(),         // set
62                ],
63            );
64
65        // 5. Let index be 0.
66        // 6. Repeat, while index < len,
67        //    a. Let val be argumentsList[index].
68        //    b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
69        //    c. Set index to index + 1.
70        obj.borrow_mut()
71            .properties_mut()
72            .override_indexed_properties(arguments_list.iter().cloned().collect());
73
74        // 9. Return obj.
75        obj
76    }
77}
78
79/// `MappedArguments` represents an Arguments exotic object.
80///
81/// This struct stores all the data to access mapped function parameters in their environment.
82#[derive(Debug, Clone, Trace, Finalize)]
83pub(crate) struct MappedArguments {
84    #[unsafe_ignore_trace]
85    binding_indices: Vec<Option<u32>>,
86    environment: Gc<DeclarativeEnvironment>,
87}
88
89impl JsData for MappedArguments {
90    fn internal_methods(&self) -> &'static InternalObjectMethods {
91        static METHODS: InternalObjectMethods = InternalObjectMethods {
92            __get_own_property__: arguments_exotic_get_own_property,
93            __define_own_property__: arguments_exotic_define_own_property,
94            __try_get__: arguments_exotic_try_get,
95            __get__: arguments_exotic_get,
96            __set__: arguments_exotic_set,
97            __delete__: arguments_exotic_delete,
98            ..ORDINARY_INTERNAL_METHODS
99        };
100
101        &METHODS
102    }
103}
104
105impl MappedArguments {
106    /// Deletes the binding with the given index from the parameter map.
107    pub(crate) fn delete(&mut self, index: u32) {
108        if let Some(binding) = self.binding_indices.get_mut(index as usize) {
109            *binding = None;
110        }
111    }
112
113    /// Get the value of the binding at the given index from the function environment.
114    ///
115    /// Note: This function is the abstract getter closure described in 10.4.4.7.1 `MakeArgGetter ( name, env )`
116    ///
117    /// More information:
118    ///  - [ECMAScript reference][spec]
119    ///
120    /// [spec]: https://tc39.es/ecma262/#sec-makearggetter
121    pub(crate) fn get(&self, index: u32) -> Option<JsValue> {
122        let binding_index = self
123            .binding_indices
124            .get(index as usize)
125            .copied()
126            .flatten()?;
127        self.environment.get(binding_index)
128    }
129
130    /// Set the value of the binding at the given index in the function environment.
131    ///
132    /// Note: This function is the abstract setter closure described in 10.4.4.7.2 `MakeArgSetter ( name, env )`
133    ///
134    /// More information:
135    ///  - [ECMAScript reference][spec]
136    ///
137    /// [spec]: https://tc39.es/ecma262/#sec-makeargsetter
138    pub(crate) fn set(&self, index: u32, value: &JsValue) {
139        if let Some(binding_index) = self.binding_indices.get(index as usize).copied().flatten() {
140            self.environment.set(binding_index, value.clone());
141        }
142    }
143}
144
145impl MappedArguments {
146    pub(crate) fn binding_indices(
147        formals: &FormalParameterList,
148        scope: &Scope,
149        interner: &Interner,
150    ) -> ThinVec<Option<u32>> {
151        // Section 17-19 are done first, for easier object creation in 11.
152        //
153        // The section 17-19 differs from the spec, due to the way the runtime environments work.
154        //
155        // This section creates getters and setters for all mapped arguments.
156        // Getting and setting values on the `arguments` object will actually access the bindings in the environment:
157        // ```
158        // function f(a) {console.log(a); arguments[0] = 1; console.log(a)};
159        // f(0) // 0, 1
160        // ```
161        //
162        // The spec assumes, that identifiers are used at runtime to reference bindings in the environment.
163        // We use indices to access environment bindings at runtime.
164        // To map to function parameters to binding indices, we use the fact, that bindings in a
165        // function environment start with all of the arguments in order:
166        //
167        // Note: The first binding (binding 0) is where "arguments" is stored.
168        //
169        // `function f (a,b,c)`
170        // | binding index | `arguments` property key | identifier |
171        // | 1             | 0                        | a          |
172        // | 2             | 1                        | b          |
173        // | 3             | 2                        | c          |
174        //
175        // Notice that the binding index does not correspond to the argument index:
176        // `function f (a,a,b)` => binding indices 0 (a), 1 (b), 2 (c)
177        // | binding index | `arguments` property key | identifier |
178        // | -             | 0                        | -          |
179        // | 1             | 1                        | a          |
180        // | 2             | 2                        | b          |
181        //
182        // While the `arguments` object contains all arguments, they must not be all bound.
183        // In the case of duplicate parameter names, the last one is bound as the environment binding.
184        //
185        // The following logic implements the steps 17-19 adjusted for our environment structure.
186        let mut bindings = FxHashMap::default();
187        let mut property_index = 0;
188        for name in bound_names(formals) {
189            let binding_index = scope
190                .get_binding(&name.to_js_string(interner))
191                .expect("binding must exist")
192                .binding_index();
193
194            let entry = bindings
195                .entry(name)
196                .or_insert((binding_index, property_index));
197
198            entry.1 = property_index;
199            property_index += 1;
200        }
201
202        let mut binding_indices = thin_vec![None; property_index];
203        for (binding_index, property_index) in bindings.values() {
204            binding_indices[*property_index] = Some(*binding_index);
205        }
206
207        binding_indices
208    }
209
210    /// Creates a new mapped Arguments exotic object.
211    ///
212    /// <https://tc39.es/ecma262/#sec-createmappedargumentsobject>
213    #[allow(clippy::new_ret_no_self)]
214    pub(crate) fn new(
215        func: &JsObject,
216        binding_indices: &[Option<u32>],
217        arguments_list: &[JsValue],
218        env: &Gc<DeclarativeEnvironment>,
219        context: &Context,
220    ) -> JsObject {
221        // 1. Assert: formals does not contain a rest parameter, any binding patterns, or any initializers.
222        // It may contain duplicate identifiers.
223        // 2. Let len be the number of elements in argumentsList.
224        let len = arguments_list.len();
225
226        // 3. Let obj be ! MakeBasicObject(« [[Prototype]], [[Extensible]], [[ParameterMap]] »).
227        // 4. Set obj.[[GetOwnProperty]] as specified in 10.4.4.1.
228        // 5. Set obj.[[DefineOwnProperty]] as specified in 10.4.4.2.
229        // 6. Set obj.[[Get]] as specified in 10.4.4.3.
230        // 7. Set obj.[[Set]] as specified in 10.4.4.4.
231        // 8. Set obj.[[Delete]] as specified in 10.4.4.5.
232        // 9. Set obj.[[Prototype]] to %Object.prototype%.
233
234        let range = binding_indices.len().min(len);
235        let map = MappedArguments {
236            binding_indices: binding_indices[..range].to_vec(),
237            environment: env.clone(),
238        };
239
240        // %Array.prototype.values%
241        let values_function = context.intrinsics().objects().array_prototype_values();
242
243        // 11. Set obj.[[ParameterMap]] to map.
244        let obj = context.intrinsics().templates().mapped_arguments().create(
245            map,
246            vec![
247                // 16. Perform ! DefinePropertyOrThrow(obj, "length", PropertyDescriptor { [[Value]]: 𝔽(len),
248                // [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
249                len.into(),
250                // 20. Perform ! DefinePropertyOrThrow(obj, @@iterator, PropertyDescriptor {
251                // [[Value]]: %Array.prototype.values%, [[Writable]]: true, [[Enumerable]]: false,
252                // [[Configurable]]: true }).
253                values_function.into(),
254                // 21. Perform ! DefinePropertyOrThrow(obj, "callee", PropertyDescriptor {
255                // [[Value]]: func, [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: true }).
256                func.clone().into(),
257            ],
258        );
259
260        // 14. Let index be 0.
261        // 15. Repeat, while index < len,
262        //     a. Let val be argumentsList[index].
263        //     b. Perform ! CreateDataPropertyOrThrow(obj, ! ToString(𝔽(index)), val).
264        //     Note: Direct initialization of indexed array is used here because `CreateDataPropertyOrThrow`
265        //     would cause a panic while executing exotic argument object set methods before the variables
266        //     in the environment are initialized.
267        obj.borrow_mut()
268            .properties_mut()
269            .override_indexed_properties(arguments_list.iter().cloned().collect());
270
271        // 22. Return obj.
272        obj
273    }
274}
275
276/// `[[GetOwnProperty]]` for arguments exotic objects.
277///
278/// More information:
279///  - [ECMAScript reference][spec]
280///
281/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-getownproperty-p
282pub(crate) fn arguments_exotic_get_own_property(
283    obj: &JsObject,
284    key: &PropertyKey,
285    context: &mut InternalMethodContext<'_>,
286) -> JsResult<Option<PropertyDescriptor>> {
287    // 1. Let desc be OrdinaryGetOwnProperty(args, P).
288    // 2. If desc is undefined, return desc.
289    let Some(desc) = ordinary_get_own_property(obj, key, context)? else {
290        return Ok(None);
291    };
292
293    // 3. Let map be args.[[ParameterMap]].
294    // 4. Let isMapped be ! HasOwnProperty(map, P).
295    // 5. If isMapped is true, then
296    if let PropertyKey::Index(index) = key {
297        if let Some(value) = obj
298            .downcast_ref::<MappedArguments>()
299            .expect("arguments exotic method must only be callable from arguments objects")
300            .get(index.get())
301        {
302            // a. Set desc.[[Value]] to Get(map, P).
303            return Ok(Some(
304                PropertyDescriptor::builder()
305                    .value(value)
306                    .maybe_writable(desc.writable())
307                    .maybe_enumerable(desc.enumerable())
308                    .maybe_configurable(desc.configurable())
309                    .build(),
310            ));
311        }
312    }
313
314    // 6. Return desc.
315    Ok(Some(desc))
316}
317
318/// `[[DefineOwnProperty]]` for arguments exotic objects.
319///
320/// More information:
321///  - [ECMAScript reference][spec]
322///
323/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-defineownproperty-p-desc
324#[allow(clippy::needless_pass_by_value)]
325pub(crate) fn arguments_exotic_define_own_property(
326    obj: &JsObject,
327    key: &PropertyKey,
328    desc: PropertyDescriptor,
329    context: &mut InternalMethodContext<'_>,
330) -> JsResult<bool> {
331    // 2. Let isMapped be HasOwnProperty(map, P).
332    let mapped = if let &PropertyKey::Index(index) = &key {
333        // 1. Let map be args.[[ParameterMap]].
334        obj.downcast_ref::<MappedArguments>()
335            .expect("arguments exotic method must only be callable from arguments objects")
336            .get(index.get())
337            .map(|value| (index, value))
338    } else {
339        None
340    };
341
342    let new_arg_desc = match desc.kind() {
343        // 4. If isMapped is true and IsDataDescriptor(Desc) is true, then
344        // a. If Desc.[[Value]] is not present and Desc.[[Writable]] is present and its
345        // value is false, then
346        DescriptorKind::Data {
347            writable: Some(false),
348            value: None,
349        } =>
350        // i. Set newArgDesc to a copy of Desc.
351        // ii. Set newArgDesc.[[Value]] to Get(map, P).
352        {
353            if let Some((_, value)) = &mapped {
354                PropertyDescriptor::builder()
355                    .value(value.clone())
356                    .writable(false)
357                    .maybe_enumerable(desc.enumerable())
358                    .maybe_configurable(desc.configurable())
359                    .build()
360            } else {
361                desc.clone()
362            }
363        }
364
365        // 3. Let newArgDesc be Desc.
366        _ => desc.clone(),
367    };
368
369    // 5. Let allowed be ? OrdinaryDefineOwnProperty(args, P, newArgDesc).
370    // 6. If allowed is false, return false.
371    if !ordinary_define_own_property(obj, key, new_arg_desc, context)? {
372        return Ok(false);
373    }
374
375    // 7. If isMapped is true, then
376    if let Some((index, _)) = mapped {
377        // 1. Let map be args.[[ParameterMap]].
378        let mut map = obj
379            .downcast_mut::<MappedArguments>()
380            .expect("arguments exotic method must only be callable from arguments objects");
381
382        // a. If IsAccessorDescriptor(Desc) is true, then
383        if desc.is_accessor_descriptor() {
384            // i. Call map.[[Delete]](P).
385            map.delete(index.get());
386        }
387        // b. Else,
388        else {
389            // i. If Desc.[[Value]] is present, then
390            if let Some(value) = desc.value() {
391                // 1. Let setStatus be Set(map, P, Desc.[[Value]], false).
392                // 2. Assert: setStatus is true because formal parameters mapped by argument objects are always writable.
393                map.set(index.get(), value);
394            }
395
396            // ii. If Desc.[[Writable]] is present and its value is false, then
397            if desc.writable() == Some(false) {
398                // 1. Call map.[[Delete]](P).
399                map.delete(index.get());
400            }
401        }
402    }
403
404    // 8. Return true.
405    Ok(true)
406}
407
408/// Internal optimization method for `Arguments` exotic objects.
409///
410/// This method combines the internal methods `OrdinaryHasProperty` and `[[Get]]`.
411///
412/// More information:
413///  - [ECMAScript reference OrdinaryHasProperty][spec0]
414///  - [ECMAScript reference Get][spec1]
415///
416/// [spec0]: https://tc39.es/ecma262/#sec-ordinaryhasproperty
417/// [spec1]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-get-p-receiver
418pub(crate) fn arguments_exotic_try_get(
419    obj: &JsObject,
420    key: &PropertyKey,
421    receiver: JsValue,
422    context: &mut InternalMethodContext<'_>,
423) -> JsResult<Option<JsValue>> {
424    if let PropertyKey::Index(index) = key {
425        // 1. Let map be args.[[ParameterMap]].
426        // 2. Let isMapped be ! HasOwnProperty(map, P).
427        if let Some(value) = obj
428            .downcast_ref::<MappedArguments>()
429            .expect("arguments exotic method must only be callable from arguments objects")
430            .get(index.get())
431        {
432            // a. Assert: map contains a formal parameter mapping for P.
433            // b. Return Get(map, P).
434            return Ok(Some(value));
435        }
436    }
437
438    // 3. If isMapped is false, then
439    // a. Return ? OrdinaryGet(args, P, Receiver).
440    ordinary_try_get(obj, key, receiver, context)
441}
442
443/// `[[Get]]` for arguments exotic objects.
444///
445/// More information:
446///  - [ECMAScript reference][spec]
447///
448/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-get-p-receiver
449pub(crate) fn arguments_exotic_get(
450    obj: &JsObject,
451    key: &PropertyKey,
452    receiver: JsValue,
453    context: &mut InternalMethodContext<'_>,
454) -> JsResult<JsValue> {
455    if let PropertyKey::Index(index) = key {
456        // 1. Let map be args.[[ParameterMap]].
457        // 2. Let isMapped be ! HasOwnProperty(map, P).
458        if let Some(value) = obj
459            .downcast_ref::<MappedArguments>()
460            .expect("arguments exotic method must only be callable from arguments objects")
461            .get(index.get())
462        {
463            // a. Assert: map contains a formal parameter mapping for P.
464            // b. Return Get(map, P).
465            return Ok(value);
466        }
467    }
468
469    // 3. If isMapped is false, then
470    // a. Return ? OrdinaryGet(args, P, Receiver).
471    ordinary_get(obj, key, receiver, context)
472}
473
474/// `[[Set]]` for arguments exotic objects.
475///
476/// More information:
477///  - [ECMAScript reference][spec]
478///
479/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-set-p-v-receiver
480pub(crate) fn arguments_exotic_set(
481    obj: &JsObject,
482    key: PropertyKey,
483    value: JsValue,
484    receiver: JsValue,
485    context: &mut InternalMethodContext<'_>,
486) -> JsResult<bool> {
487    // 1. If SameValue(args, Receiver) is false, then
488    // a. Let isMapped be false.
489    // 2. Else,
490    if let PropertyKey::Index(index) = &key {
491        if JsValue::same_value(&obj.clone().into(), &receiver) {
492            // a. Let map be args.[[ParameterMap]].
493            // b. Let isMapped be ! HasOwnProperty(map, P).
494            // 3. If isMapped is true, then
495            // a. Let setStatus be Set(map, P, V, false).
496            // b. Assert: setStatus is true because formal parameters mapped by argument objects are always writable.
497            obj.downcast_ref::<MappedArguments>()
498                .expect("arguments exotic method must only be callable from arguments objects")
499                .set(index.get(), &value);
500        }
501    }
502
503    // 4. Return ? OrdinarySet(args, P, V, Receiver).
504    ordinary_set(obj, key, value, receiver, context)
505}
506
507/// `[[Delete]]` for arguments exotic objects.
508///
509/// More information:
510///  - [ECMAScript reference][spec]
511///
512/// [spec]: https://tc39.es/ecma262/#sec-arguments-exotic-objects-delete-p
513pub(crate) fn arguments_exotic_delete(
514    obj: &JsObject,
515    key: &PropertyKey,
516    context: &mut InternalMethodContext<'_>,
517) -> JsResult<bool> {
518    // 3. Let result be ? OrdinaryDelete(args, P).
519    let result = ordinary_delete(obj, key, context)?;
520
521    if result {
522        if let PropertyKey::Index(index) = key {
523            // 1. Let map be args.[[ParameterMap]].
524            // 2. Let isMapped be ! HasOwnProperty(map, P).
525            // 4. If result is true and isMapped is true, then
526            // a. Call map.[[Delete]](P).
527            obj.downcast_mut::<MappedArguments>()
528                .expect("arguments exotic method must only be callable from arguments objects")
529                .delete(index.get());
530        }
531    }
532
533    // 5. Return result.
534    Ok(result)
535}