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}