we (web engine): Experimental web browser project to understand the limits of Claude
1//! Built-in JavaScript objects and functions: Object, Array, Function, Error,
2//! String, Number, Boolean, Symbol, Math, Date, JSON, Promise.
3//!
4//! Registers constructors, static methods, and prototype methods as globals
5//! in the VM. Callback-based array methods (map, filter, etc.) are defined
6//! via a JS preamble executed at init time.
7
8use crate::gc::{Gc, GcRef};
9use crate::shape::ShapeTable;
10use crate::vm::*;
11use std::cell::RefCell;
12use std::collections::{HashMap, HashSet};
13use std::time::{SystemTime, UNIX_EPOCH};
14
15/// Native callback type alias to satisfy clippy::type_complexity.
16type NativeMethod = (
17 &'static str,
18 fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>,
19);
20
21// ── Helpers ──────────────────────────────────────────────────
22
23/// Create a native function GcRef.
24pub fn make_native(
25 gc: &mut Gc<HeapObject>,
26 name: &str,
27 callback: fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>,
28) -> GcRef {
29 gc.alloc(HeapObject::Function(Box::new(FunctionData {
30 name: name.to_string(),
31 kind: FunctionKind::Native(NativeFunc { callback }),
32 prototype_obj: None,
33 properties: HashMap::new(),
34 upvalues: Vec::new(),
35 })))
36}
37
38/// Set a non-enumerable property on an object.
39pub fn set_builtin_prop(
40 gc: &mut Gc<HeapObject>,
41 shapes: &mut ShapeTable,
42 obj: GcRef,
43 key: &str,
44 val: Value,
45) {
46 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
47 data.insert_property(key.to_string(), Property::builtin(val), shapes);
48 }
49}
50
51/// Set a non-enumerable property on a function.
52fn set_func_prop(gc: &mut Gc<HeapObject>, func: GcRef, key: &str, val: Value) {
53 if let Some(HeapObject::Function(fdata)) = gc.get_mut(func) {
54 fdata
55 .properties
56 .insert(key.to_string(), Property::builtin(val));
57 }
58}
59
60/// Get own enumerable string keys of an object (for Object.keys, etc.).
61fn own_enumerable_keys(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj_ref: GcRef) -> Vec<String> {
62 match gc.get(obj_ref) {
63 Some(HeapObject::Object(data)) => {
64 let mut int_keys: Vec<(u32, String)> = Vec::new();
65 let mut str_keys: Vec<String> = Vec::new();
66 for (k, prop) in data.property_entries(shapes) {
67 if prop.enumerable {
68 if let Ok(idx) = k.parse::<u32>() {
69 int_keys.push((idx, k));
70 } else {
71 str_keys.push(k);
72 }
73 }
74 }
75 int_keys.sort_by_key(|(idx, _)| *idx);
76 let mut result: Vec<String> = int_keys.into_iter().map(|(_, k)| k).collect();
77 result.extend(str_keys);
78 result
79 }
80 _ => Vec::new(),
81 }
82}
83
84/// Get all own property names (enumerable or not) of an object.
85fn own_property_names(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj_ref: GcRef) -> Vec<String> {
86 match gc.get(obj_ref) {
87 Some(HeapObject::Object(data)) => {
88 let mut int_keys: Vec<(u32, String)> = Vec::new();
89 let mut str_keys: Vec<String> = Vec::new();
90 for k in data.property_keys(shapes) {
91 if let Ok(idx) = k.parse::<u32>() {
92 int_keys.push((idx, k));
93 } else {
94 str_keys.push(k);
95 }
96 }
97 int_keys.sort_by_key(|(idx, _)| *idx);
98 let mut result: Vec<String> = int_keys.into_iter().map(|(_, k)| k).collect();
99 result.extend(str_keys);
100 result
101 }
102 _ => Vec::new(),
103 }
104}
105
106/// Read the "length" property of an array-like object as usize.
107fn array_length(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: GcRef) -> usize {
108 match gc.get(obj) {
109 Some(HeapObject::Object(data)) => match data.get_property("length", shapes) {
110 Some(prop) => prop.value.to_number() as usize,
111 None => 0,
112 },
113 _ => 0,
114 }
115}
116
117/// Set the "length" property on an array-like object.
118fn set_array_length(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, obj: GcRef, len: usize) {
119 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
120 if !data.update_value("length", Value::Number(len as f64), shapes) {
121 data.insert_property(
122 "length".to_string(),
123 Property {
124 value: Value::Number(len as f64),
125 writable: true,
126 enumerable: false,
127 configurable: false,
128 },
129 shapes,
130 );
131 }
132 }
133}
134
135/// Check whether an object has a "length" property (i.e. is array-like).
136fn array_length_exists(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: GcRef) -> bool {
137 match gc.get(obj) {
138 Some(HeapObject::Object(data)) => data.contains_key("length", shapes),
139 _ => false,
140 }
141}
142
143/// Get an element by index from an array-like object.
144fn array_get(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: GcRef, idx: usize) -> Value {
145 match gc.get(obj) {
146 Some(HeapObject::Object(data)) => {
147 let key = idx.to_string();
148 data.get_property(&key, shapes)
149 .map(|p| p.value)
150 .unwrap_or(Value::Undefined)
151 }
152 _ => Value::Undefined,
153 }
154}
155
156/// Set an element by index on an array-like object.
157fn array_set(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, obj: GcRef, idx: usize, val: Value) {
158 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
159 data.insert_property(idx.to_string(), Property::data(val), shapes);
160 }
161}
162
163// ── Initialization ───────────────────────────────────────────
164
165/// Initialize all built-in objects and register them in the VM.
166pub fn init_builtins(vm: &mut Vm) {
167 // Create Object.prototype first (root of the prototype chain).
168 let obj_proto = vm.gc.alloc(HeapObject::Object(ObjectData::new()));
169 init_object_prototype(&mut vm.gc, &mut vm.shapes, obj_proto);
170
171 // Create Array.prototype (inherits from Object.prototype).
172 let mut arr_proto_data = ObjectData::new();
173 arr_proto_data.prototype = Some(obj_proto);
174 let arr_proto = vm.gc.alloc(HeapObject::Object(arr_proto_data));
175 init_array_prototype(&mut vm.gc, &mut vm.shapes, arr_proto);
176
177 // Create Error.prototype (inherits from Object.prototype).
178 let mut err_proto_data = ObjectData::new();
179 err_proto_data.prototype = Some(obj_proto);
180 err_proto_data.insert_property(
181 "name".to_string(),
182 Property::builtin(Value::String("Error".to_string())),
183 &mut vm.shapes,
184 );
185 err_proto_data.insert_property(
186 "message".to_string(),
187 Property::builtin(Value::String(String::new())),
188 &mut vm.shapes,
189 );
190 let err_proto = vm.gc.alloc(HeapObject::Object(err_proto_data));
191 init_error_prototype(&mut vm.gc, &mut vm.shapes, err_proto);
192
193 // Create String.prototype (inherits from Object.prototype).
194 let mut str_proto_data = ObjectData::new();
195 str_proto_data.prototype = Some(obj_proto);
196 let str_proto = vm.gc.alloc(HeapObject::Object(str_proto_data));
197 init_string_prototype(&mut vm.gc, &mut vm.shapes, str_proto);
198
199 // Create Number.prototype (inherits from Object.prototype).
200 let mut num_proto_data = ObjectData::new();
201 num_proto_data.prototype = Some(obj_proto);
202 let num_proto = vm.gc.alloc(HeapObject::Object(num_proto_data));
203 init_number_prototype(&mut vm.gc, &mut vm.shapes, num_proto);
204
205 // Create Boolean.prototype (inherits from Object.prototype).
206 let mut bool_proto_data = ObjectData::new();
207 bool_proto_data.prototype = Some(obj_proto);
208 let bool_proto = vm.gc.alloc(HeapObject::Object(bool_proto_data));
209 init_boolean_prototype(&mut vm.gc, &mut vm.shapes, bool_proto);
210
211 // Store prototypes in VM for use by CreateArray/CreateObject and auto-boxing.
212 vm.object_prototype = Some(obj_proto);
213 vm.array_prototype = Some(arr_proto);
214 vm.string_prototype = Some(str_proto);
215 vm.number_prototype = Some(num_proto);
216 vm.boolean_prototype = Some(bool_proto);
217
218 // Create and register Object constructor.
219 let obj_ctor = init_object_constructor(&mut vm.gc, obj_proto);
220 vm.set_global("Object", Value::Function(obj_ctor));
221
222 // Create and register Array constructor.
223 let arr_ctor = init_array_constructor(&mut vm.gc, arr_proto);
224 vm.set_global("Array", Value::Function(arr_ctor));
225
226 // Create and register Error constructors.
227 init_error_constructors(vm, err_proto);
228
229 // Create and register String constructor.
230 let str_ctor = init_string_constructor(&mut vm.gc, str_proto);
231 vm.set_global("String", Value::Function(str_ctor));
232
233 // Create and register Number constructor.
234 let num_ctor = init_number_constructor(&mut vm.gc, num_proto);
235 vm.set_global("Number", Value::Function(num_ctor));
236
237 // Create and register Boolean constructor.
238 let bool_ctor = init_boolean_constructor(&mut vm.gc, bool_proto);
239 vm.set_global("Boolean", Value::Function(bool_ctor));
240
241 // Create and register Symbol factory.
242 init_symbol_builtins(vm);
243
244 // Create and register Math object (static methods only, not a constructor).
245 init_math_object(vm);
246
247 // Create and register Date constructor.
248 init_date_builtins(vm);
249
250 // Create and register RegExp constructor.
251 init_regexp_builtins(vm);
252
253 // Create and register Map, Set, WeakMap, WeakSet constructors.
254 init_map_set_builtins(vm);
255
256 // Create and register Promise (prototype methods + native helpers).
257 init_promise_builtins(vm);
258
259 // Create and register JSON object (static methods only).
260 init_json_object(vm);
261
262 // Create and register console object.
263 init_console_object(vm);
264
265 // Register global utility functions.
266 init_global_functions(vm);
267
268 // Execute JS preamble for callback-based methods.
269 init_js_preamble(vm);
270}
271
272// ── Object.prototype ─────────────────────────────────────────
273
274fn init_object_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
275 let has_own = make_native(gc, "hasOwnProperty", object_proto_has_own_property);
276 set_builtin_prop(
277 gc,
278 shapes,
279 proto,
280 "hasOwnProperty",
281 Value::Function(has_own),
282 );
283
284 let to_string = make_native(gc, "toString", object_proto_to_string);
285 set_builtin_prop(gc, shapes, proto, "toString", Value::Function(to_string));
286
287 let value_of = make_native(gc, "valueOf", object_proto_value_of);
288 set_builtin_prop(gc, shapes, proto, "valueOf", Value::Function(value_of));
289}
290
291fn object_proto_has_own_property(
292 args: &[Value],
293 ctx: &mut NativeContext,
294) -> Result<Value, RuntimeError> {
295 let key = args
296 .first()
297 .map(|v| v.to_js_string(ctx.gc))
298 .unwrap_or_default();
299 match ctx.this.gc_ref() {
300 Some(obj_ref) => match ctx.gc.get(obj_ref) {
301 Some(HeapObject::Object(data)) => {
302 Ok(Value::Boolean(data.contains_key(&key, ctx.shapes)))
303 }
304 Some(HeapObject::Function(fdata)) => {
305 Ok(Value::Boolean(fdata.properties.contains_key(&key)))
306 }
307 _ => Ok(Value::Boolean(false)),
308 },
309 None => Ok(Value::Boolean(false)),
310 }
311}
312
313fn object_proto_to_string(
314 _args: &[Value],
315 _ctx: &mut NativeContext,
316) -> Result<Value, RuntimeError> {
317 Ok(Value::String("[object Object]".to_string()))
318}
319
320fn object_proto_value_of(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
321 Ok(ctx.this.clone())
322}
323
324// ── Object constructor + static methods ──────────────────────
325
326fn init_object_constructor(gc: &mut Gc<HeapObject>, obj_proto: GcRef) -> GcRef {
327 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
328 name: "Object".to_string(),
329 kind: FunctionKind::Native(NativeFunc {
330 callback: object_constructor,
331 }),
332 prototype_obj: Some(obj_proto),
333 properties: HashMap::new(),
334 upvalues: Vec::new(),
335 })));
336
337 // Static methods.
338 let keys = make_native(gc, "keys", object_keys);
339 set_func_prop(gc, ctor, "keys", Value::Function(keys));
340
341 let values = make_native(gc, "values", object_values);
342 set_func_prop(gc, ctor, "values", Value::Function(values));
343
344 let entries = make_native(gc, "entries", object_entries);
345 set_func_prop(gc, ctor, "entries", Value::Function(entries));
346
347 let assign = make_native(gc, "assign", object_assign);
348 set_func_prop(gc, ctor, "assign", Value::Function(assign));
349
350 let create = make_native(gc, "create", object_create);
351 set_func_prop(gc, ctor, "create", Value::Function(create));
352
353 let is = make_native(gc, "is", object_is);
354 set_func_prop(gc, ctor, "is", Value::Function(is));
355
356 let get_proto = make_native(gc, "getPrototypeOf", object_get_prototype_of);
357 set_func_prop(gc, ctor, "getPrototypeOf", Value::Function(get_proto));
358
359 let get_own_names = make_native(gc, "getOwnPropertyNames", object_get_own_property_names);
360 set_func_prop(
361 gc,
362 ctor,
363 "getOwnPropertyNames",
364 Value::Function(get_own_names),
365 );
366
367 let get_own_desc = make_native(gc, "getOwnPropertyDescriptor", object_get_own_prop_desc);
368 set_func_prop(
369 gc,
370 ctor,
371 "getOwnPropertyDescriptor",
372 Value::Function(get_own_desc),
373 );
374
375 let define_prop = make_native(gc, "defineProperty", object_define_property);
376 set_func_prop(gc, ctor, "defineProperty", Value::Function(define_prop));
377
378 let freeze = make_native(gc, "freeze", object_freeze);
379 set_func_prop(gc, ctor, "freeze", Value::Function(freeze));
380
381 let seal = make_native(gc, "seal", object_seal);
382 set_func_prop(gc, ctor, "seal", Value::Function(seal));
383
384 let is_frozen = make_native(gc, "isFrozen", object_is_frozen);
385 set_func_prop(gc, ctor, "isFrozen", Value::Function(is_frozen));
386
387 let is_sealed = make_native(gc, "isSealed", object_is_sealed);
388 set_func_prop(gc, ctor, "isSealed", Value::Function(is_sealed));
389
390 let from_entries = make_native(gc, "fromEntries", object_from_entries);
391 set_func_prop(gc, ctor, "fromEntries", Value::Function(from_entries));
392
393 let has_own = make_native(gc, "hasOwn", object_has_own);
394 set_func_prop(gc, ctor, "hasOwn", Value::Function(has_own));
395
396 ctor
397}
398
399fn object_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
400 match args.first() {
401 Some(Value::Object(_)) | Some(Value::Function(_)) => Ok(args[0].clone()),
402 Some(Value::Null) | Some(Value::Undefined) | None => {
403 let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new()));
404 Ok(Value::Object(obj))
405 }
406 _ => {
407 // Primitive wrapping (simplified): return a new object.
408 let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new()));
409 Ok(Value::Object(obj))
410 }
411 }
412}
413
414fn object_keys(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
415 let obj_ref = match args.first() {
416 Some(Value::Object(r)) | Some(Value::Function(r)) => *r,
417 _ => return Err(RuntimeError::type_error("Object.keys requires an object")),
418 };
419 let keys = own_enumerable_keys(ctx.gc, ctx.shapes, obj_ref);
420 Ok(make_string_array(ctx.gc, ctx.shapes, &keys))
421}
422
423fn object_values(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
424 let obj_ref = match args.first() {
425 Some(Value::Object(r)) => *r,
426 _ => return Err(RuntimeError::type_error("Object.values requires an object")),
427 };
428 let keys = own_enumerable_keys(ctx.gc, ctx.shapes, obj_ref);
429 let values: Vec<Value> = keys
430 .iter()
431 .map(|k| {
432 ctx.gc
433 .get(obj_ref)
434 .and_then(|ho| match ho {
435 HeapObject::Object(data) => data.get_property(k, ctx.shapes).map(|p| p.value),
436 _ => None,
437 })
438 .unwrap_or(Value::Undefined)
439 })
440 .collect();
441 Ok(make_value_array(ctx.gc, ctx.shapes, &values))
442}
443
444fn object_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
445 let obj_ref = match args.first() {
446 Some(Value::Object(r)) => *r,
447 _ => {
448 return Err(RuntimeError::type_error(
449 "Object.entries requires an object",
450 ))
451 }
452 };
453 let keys = own_enumerable_keys(ctx.gc, ctx.shapes, obj_ref);
454 let mut entries = Vec::new();
455 for k in &keys {
456 let val = ctx
457 .gc
458 .get(obj_ref)
459 .and_then(|ho| match ho {
460 HeapObject::Object(data) => data.get_property(k, ctx.shapes).map(|p| p.value),
461 _ => None,
462 })
463 .unwrap_or(Value::Undefined);
464 // Create a [key, value] pair array.
465 let pair = make_value_array(ctx.gc, ctx.shapes, &[Value::String(k.clone()), val]);
466 entries.push(pair);
467 }
468 Ok(make_value_array(ctx.gc, ctx.shapes, &entries))
469}
470
471fn object_assign(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
472 let target_ref = match args.first() {
473 Some(Value::Object(r)) => *r,
474 _ => {
475 return Err(RuntimeError::type_error(
476 "Object.assign requires an object target",
477 ))
478 }
479 };
480 for source in args.iter().skip(1) {
481 let src_ref = match source {
482 Value::Object(r) => *r,
483 Value::Null | Value::Undefined => continue,
484 _ => continue,
485 };
486 let keys = own_enumerable_keys(ctx.gc, ctx.shapes, src_ref);
487 for k in &keys {
488 let val = ctx
489 .gc
490 .get(src_ref)
491 .and_then(|ho| match ho {
492 HeapObject::Object(data) => data.get_property(k, ctx.shapes).map(|p| p.value),
493 _ => None,
494 })
495 .unwrap_or(Value::Undefined);
496 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(target_ref) {
497 data.insert_property(k.clone(), Property::data(val), ctx.shapes);
498 }
499 }
500 }
501 Ok(Value::Object(target_ref))
502}
503
504fn object_create(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
505 let proto = match args.first() {
506 Some(Value::Object(r)) => Some(*r),
507 Some(Value::Null) => None,
508 _ => {
509 return Err(RuntimeError::type_error(
510 "Object.create prototype must be an object or null",
511 ))
512 }
513 };
514 let mut obj = ObjectData::new();
515 obj.prototype = proto;
516 let gc_ref = ctx.gc.alloc(HeapObject::Object(obj));
517 Ok(Value::Object(gc_ref))
518}
519
520fn object_is(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
521 let x = args.first().cloned().unwrap_or(Value::Undefined);
522 let y = args.get(1).cloned().unwrap_or(Value::Undefined);
523 Ok(Value::Boolean(same_value(&x, &y)))
524}
525
526/// SameValue algorithm (ECMA-262 §7.2.10).
527fn same_value(x: &Value, y: &Value) -> bool {
528 match (x, y) {
529 (Value::Number(a), Value::Number(b)) => {
530 if a.is_nan() && b.is_nan() {
531 return true;
532 }
533 if *a == 0.0 && *b == 0.0 {
534 return a.is_sign_positive() == b.is_sign_positive();
535 }
536 a == b
537 }
538 (Value::Undefined, Value::Undefined) => true,
539 (Value::Null, Value::Null) => true,
540 (Value::Boolean(a), Value::Boolean(b)) => a == b,
541 (Value::String(a), Value::String(b)) => a == b,
542 (Value::Object(a), Value::Object(b)) => a == b,
543 (Value::Function(a), Value::Function(b)) => a == b,
544 _ => false,
545 }
546}
547
548fn object_get_prototype_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
549 let obj_ref = match args.first() {
550 Some(Value::Object(r)) => *r,
551 _ => {
552 return Err(RuntimeError::type_error(
553 "Object.getPrototypeOf requires an object",
554 ))
555 }
556 };
557 match ctx.gc.get(obj_ref) {
558 Some(HeapObject::Object(data)) => match data.prototype {
559 Some(proto) => Ok(Value::Object(proto)),
560 None => Ok(Value::Null),
561 },
562 _ => Ok(Value::Null),
563 }
564}
565
566fn object_get_own_property_names(
567 args: &[Value],
568 ctx: &mut NativeContext,
569) -> Result<Value, RuntimeError> {
570 let obj_ref = match args.first() {
571 Some(Value::Object(r)) | Some(Value::Function(r)) => *r,
572 _ => {
573 return Err(RuntimeError::type_error(
574 "Object.getOwnPropertyNames requires an object",
575 ))
576 }
577 };
578 let names = own_property_names(ctx.gc, ctx.shapes, obj_ref);
579 Ok(make_string_array(ctx.gc, ctx.shapes, &names))
580}
581
582fn object_get_own_prop_desc(
583 args: &[Value],
584 ctx: &mut NativeContext,
585) -> Result<Value, RuntimeError> {
586 let obj_ref = match args.first() {
587 Some(Value::Object(r)) => *r,
588 _ => return Ok(Value::Undefined),
589 };
590 let key = args
591 .get(1)
592 .map(|v| v.to_js_string(ctx.gc))
593 .unwrap_or_default();
594 let prop = match ctx.gc.get(obj_ref) {
595 Some(HeapObject::Object(data)) => data.get_property(&key, ctx.shapes),
596 _ => None,
597 };
598 match prop {
599 Some(p) => {
600 let mut desc = ObjectData::new();
601 desc.insert_property("value".to_string(), Property::data(p.value), ctx.shapes);
602 desc.insert_property(
603 "writable".to_string(),
604 Property::data(Value::Boolean(p.writable)),
605 ctx.shapes,
606 );
607 desc.insert_property(
608 "enumerable".to_string(),
609 Property::data(Value::Boolean(p.enumerable)),
610 ctx.shapes,
611 );
612 desc.insert_property(
613 "configurable".to_string(),
614 Property::data(Value::Boolean(p.configurable)),
615 ctx.shapes,
616 );
617 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(desc))))
618 }
619 None => Ok(Value::Undefined),
620 }
621}
622
623fn object_define_property(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
624 let obj_ref = match args.first() {
625 Some(Value::Object(r)) => *r,
626 _ => {
627 return Err(RuntimeError::type_error(
628 "Object.defineProperty requires an object",
629 ))
630 }
631 };
632 let key = args
633 .get(1)
634 .map(|v| v.to_js_string(ctx.gc))
635 .unwrap_or_default();
636 let desc_ref = match args.get(2) {
637 Some(Value::Object(r)) => *r,
638 _ => {
639 return Err(RuntimeError::type_error(
640 "Property descriptor must be an object",
641 ))
642 }
643 };
644
645 // Read descriptor properties.
646 let (value, writable, enumerable, configurable) = {
647 match ctx.gc.get(desc_ref) {
648 Some(HeapObject::Object(desc_data)) => {
649 let value = desc_data
650 .get_property("value", ctx.shapes)
651 .map(|p| p.value)
652 .unwrap_or(Value::Undefined);
653 let writable = desc_data
654 .get_property("writable", ctx.shapes)
655 .map(|p| p.value.to_boolean())
656 .unwrap_or(false);
657 let enumerable = desc_data
658 .get_property("enumerable", ctx.shapes)
659 .map(|p| p.value.to_boolean())
660 .unwrap_or(false);
661 let configurable = desc_data
662 .get_property("configurable", ctx.shapes)
663 .map(|p| p.value.to_boolean())
664 .unwrap_or(false);
665 (value, writable, enumerable, configurable)
666 }
667 _ => (Value::Undefined, false, false, false),
668 }
669 };
670
671 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
672 data.insert_property(
673 key,
674 Property {
675 value,
676 writable,
677 enumerable,
678 configurable,
679 },
680 ctx.shapes,
681 );
682 }
683 Ok(Value::Object(obj_ref))
684}
685
686fn object_freeze(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
687 let obj_ref = match args.first() {
688 Some(Value::Object(r)) => *r,
689 Some(other) => return Ok(other.clone()),
690 None => return Ok(Value::Undefined),
691 };
692 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
693 data.extensible = false;
694 data.modify_all_properties(ctx.shapes, |prop| {
695 prop.writable = false;
696 prop.configurable = false;
697 });
698 }
699 Ok(Value::Object(obj_ref))
700}
701
702fn object_seal(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
703 let obj_ref = match args.first() {
704 Some(Value::Object(r)) => *r,
705 Some(other) => return Ok(other.clone()),
706 None => return Ok(Value::Undefined),
707 };
708 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
709 data.extensible = false;
710 data.modify_all_properties(ctx.shapes, |prop| {
711 prop.configurable = false;
712 });
713 }
714 Ok(Value::Object(obj_ref))
715}
716
717fn object_is_frozen(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
718 let obj_ref = match args.first() {
719 Some(Value::Object(r)) => *r,
720 _ => return Ok(Value::Boolean(true)),
721 };
722 match ctx.gc.get(obj_ref) {
723 Some(HeapObject::Object(data)) => {
724 if data.extensible {
725 return Ok(Value::Boolean(false));
726 }
727 let frozen = data
728 .property_entries(ctx.shapes)
729 .iter()
730 .all(|(_, p)| !p.writable && !p.configurable);
731 Ok(Value::Boolean(frozen))
732 }
733 _ => Ok(Value::Boolean(true)),
734 }
735}
736
737fn object_is_sealed(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
738 let obj_ref = match args.first() {
739 Some(Value::Object(r)) => *r,
740 _ => return Ok(Value::Boolean(true)),
741 };
742 match ctx.gc.get(obj_ref) {
743 Some(HeapObject::Object(data)) => {
744 if data.extensible {
745 return Ok(Value::Boolean(false));
746 }
747 let sealed = data
748 .property_entries(ctx.shapes)
749 .iter()
750 .all(|(_, p)| !p.configurable);
751 Ok(Value::Boolean(sealed))
752 }
753 _ => Ok(Value::Boolean(true)),
754 }
755}
756
757fn object_from_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
758 let arr_ref = match args.first() {
759 Some(Value::Object(r)) => *r,
760 _ => {
761 return Err(RuntimeError::type_error(
762 "Object.fromEntries requires an iterable",
763 ))
764 }
765 };
766 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
767 let mut obj = ObjectData::new();
768 for i in 0..len {
769 let pair_val = array_get(ctx.gc, ctx.shapes, arr_ref, i);
770 if let Value::Object(pair_ref) = pair_val {
771 let key = array_get(ctx.gc, ctx.shapes, pair_ref, 0);
772 let val = array_get(ctx.gc, ctx.shapes, pair_ref, 1);
773 let key_str = key.to_js_string(ctx.gc);
774 obj.insert_property(key_str, Property::data(val), ctx.shapes);
775 }
776 }
777 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))))
778}
779
780fn object_has_own(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
781 let obj_ref = match args.first() {
782 Some(Value::Object(r)) => *r,
783 _ => return Ok(Value::Boolean(false)),
784 };
785 let key = args
786 .get(1)
787 .map(|v| v.to_js_string(ctx.gc))
788 .unwrap_or_default();
789 match ctx.gc.get(obj_ref) {
790 Some(HeapObject::Object(data)) => Ok(Value::Boolean(data.contains_key(&key, ctx.shapes))),
791 _ => Ok(Value::Boolean(false)),
792 }
793}
794
795// ── Array helpers ────────────────────────────────────────────
796
797/// Create a JS array from a slice of string values.
798fn make_string_array(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, items: &[String]) -> Value {
799 let mut obj = ObjectData::new();
800 for (i, s) in items.iter().enumerate() {
801 obj.insert_property(
802 i.to_string(),
803 Property::data(Value::String(s.clone())),
804 shapes,
805 );
806 }
807 obj.insert_property(
808 "length".to_string(),
809 Property {
810 value: Value::Number(items.len() as f64),
811 writable: true,
812 enumerable: false,
813 configurable: false,
814 },
815 shapes,
816 );
817 Value::Object(gc.alloc(HeapObject::Object(obj)))
818}
819
820/// Create a JS array from a slice of Values.
821fn make_value_array(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, items: &[Value]) -> Value {
822 let mut obj = ObjectData::new();
823 for (i, v) in items.iter().enumerate() {
824 obj.insert_property(i.to_string(), Property::data(v.clone()), shapes);
825 }
826 obj.insert_property(
827 "length".to_string(),
828 Property {
829 value: Value::Number(items.len() as f64),
830 writable: true,
831 enumerable: false,
832 configurable: false,
833 },
834 shapes,
835 );
836 Value::Object(gc.alloc(HeapObject::Object(obj)))
837}
838
839// ── Array.prototype ──────────────────────────────────────────
840
841fn init_array_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
842 let push = make_native(gc, "push", array_push);
843 set_builtin_prop(gc, shapes, proto, "push", Value::Function(push));
844
845 let pop = make_native(gc, "pop", array_pop);
846 set_builtin_prop(gc, shapes, proto, "pop", Value::Function(pop));
847
848 let shift = make_native(gc, "shift", array_shift);
849 set_builtin_prop(gc, shapes, proto, "shift", Value::Function(shift));
850
851 let unshift = make_native(gc, "unshift", array_unshift);
852 set_builtin_prop(gc, shapes, proto, "unshift", Value::Function(unshift));
853
854 let index_of = make_native(gc, "indexOf", array_index_of);
855 set_builtin_prop(gc, shapes, proto, "indexOf", Value::Function(index_of));
856
857 let last_index_of = make_native(gc, "lastIndexOf", array_last_index_of);
858 set_builtin_prop(
859 gc,
860 shapes,
861 proto,
862 "lastIndexOf",
863 Value::Function(last_index_of),
864 );
865
866 let includes = make_native(gc, "includes", array_includes);
867 set_builtin_prop(gc, shapes, proto, "includes", Value::Function(includes));
868
869 let join = make_native(gc, "join", array_join);
870 set_builtin_prop(gc, shapes, proto, "join", Value::Function(join));
871
872 let slice = make_native(gc, "slice", array_slice);
873 set_builtin_prop(gc, shapes, proto, "slice", Value::Function(slice));
874
875 let concat = make_native(gc, "concat", array_concat);
876 set_builtin_prop(gc, shapes, proto, "concat", Value::Function(concat));
877
878 let reverse = make_native(gc, "reverse", array_reverse);
879 set_builtin_prop(gc, shapes, proto, "reverse", Value::Function(reverse));
880
881 let splice = make_native(gc, "splice", array_splice);
882 set_builtin_prop(gc, shapes, proto, "splice", Value::Function(splice));
883
884 let fill = make_native(gc, "fill", array_fill);
885 set_builtin_prop(gc, shapes, proto, "fill", Value::Function(fill));
886
887 let to_string = make_native(gc, "toString", array_to_string);
888 set_builtin_prop(gc, shapes, proto, "toString", Value::Function(to_string));
889
890 let at = make_native(gc, "at", array_at);
891 set_builtin_prop(gc, shapes, proto, "at", Value::Function(at));
892
893 // @@iterator: returns an array iterator (values).
894 let iter = make_native(gc, "[Symbol.iterator]", array_iterator);
895 set_builtin_prop(gc, shapes, proto, "@@iterator", Value::Function(iter));
896
897 // Array.prototype.keys/values/entries
898 let keys_fn = make_native(gc, "keys", array_keys_iter);
899 set_builtin_prop(gc, shapes, proto, "keys", Value::Function(keys_fn));
900 let values_fn = make_native(gc, "values", array_values_iter);
901 set_builtin_prop(gc, shapes, proto, "values", Value::Function(values_fn));
902 let entries_fn = make_native(gc, "entries", array_entries_iter);
903 set_builtin_prop(gc, shapes, proto, "entries", Value::Function(entries_fn));
904}
905
906fn array_push(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
907 let obj_ref = match ctx.this.gc_ref() {
908 Some(r) => r,
909 None => return Err(RuntimeError::type_error("push called on non-object")),
910 };
911 let mut len = array_length(ctx.gc, ctx.shapes, obj_ref);
912 for val in args {
913 array_set(ctx.gc, ctx.shapes, obj_ref, len, val.clone());
914 len += 1;
915 }
916 set_array_length(ctx.gc, ctx.shapes, obj_ref, len);
917 Ok(Value::Number(len as f64))
918}
919
920fn array_pop(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
921 let _ = args;
922 let obj_ref = match ctx.this.gc_ref() {
923 Some(r) => r,
924 None => return Err(RuntimeError::type_error("pop called on non-object")),
925 };
926 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
927 if len == 0 {
928 return Ok(Value::Undefined);
929 }
930 let val = array_get(ctx.gc, ctx.shapes, obj_ref, len - 1);
931 // Remove the last element.
932 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
933 data.remove_property(&(len - 1).to_string(), ctx.shapes);
934 }
935 set_array_length(ctx.gc, ctx.shapes, obj_ref, len - 1);
936 Ok(val)
937}
938
939fn array_shift(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
940 let _ = args;
941 let obj_ref = match ctx.this.gc_ref() {
942 Some(r) => r,
943 None => return Err(RuntimeError::type_error("shift called on non-object")),
944 };
945 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
946 if len == 0 {
947 return Ok(Value::Undefined);
948 }
949 let first = array_get(ctx.gc, ctx.shapes, obj_ref, 0);
950 // Shift all elements down.
951 let mut vals = Vec::with_capacity(len - 1);
952 for i in 1..len {
953 vals.push(array_get(ctx.gc, ctx.shapes, obj_ref, i));
954 }
955 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
956 // Remove all numeric keys.
957 for i in 0..len {
958 data.remove_property(&i.to_string(), ctx.shapes);
959 }
960 // Re-insert shifted values.
961 for (i, v) in vals.into_iter().enumerate() {
962 data.insert_property(i.to_string(), Property::data(v), ctx.shapes);
963 }
964 }
965 set_array_length(ctx.gc, ctx.shapes, obj_ref, len - 1);
966 Ok(first)
967}
968
969fn array_unshift(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
970 let obj_ref = match ctx.this.gc_ref() {
971 Some(r) => r,
972 None => return Err(RuntimeError::type_error("unshift called on non-object")),
973 };
974 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
975 let insert_count = args.len();
976 // Read existing values.
977 let mut existing = Vec::with_capacity(len);
978 for i in 0..len {
979 existing.push(array_get(ctx.gc, ctx.shapes, obj_ref, i));
980 }
981 // Write new values at the start, then existing values after.
982 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
983 for i in 0..len {
984 data.remove_property(&i.to_string(), ctx.shapes);
985 }
986 for (i, v) in args.iter().enumerate() {
987 data.insert_property(i.to_string(), Property::data(v.clone()), ctx.shapes);
988 }
989 for (i, v) in existing.into_iter().enumerate() {
990 data.insert_property(
991 (i + insert_count).to_string(),
992 Property::data(v),
993 ctx.shapes,
994 );
995 }
996 }
997 let new_len = len + insert_count;
998 set_array_length(ctx.gc, ctx.shapes, obj_ref, new_len);
999 Ok(Value::Number(new_len as f64))
1000}
1001
1002fn array_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1003 let obj_ref = match ctx.this.gc_ref() {
1004 Some(r) => r,
1005 None => return Ok(Value::Number(-1.0)),
1006 };
1007 let search = args.first().cloned().unwrap_or(Value::Undefined);
1008 let from = args
1009 .get(1)
1010 .map(|v| {
1011 let n = v.to_number() as i64;
1012 let len = array_length(ctx.gc, ctx.shapes, obj_ref) as i64;
1013 if n < 0 {
1014 (len + n).max(0) as usize
1015 } else {
1016 n as usize
1017 }
1018 })
1019 .unwrap_or(0);
1020 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1021 for i in from..len {
1022 let elem = array_get(ctx.gc, ctx.shapes, obj_ref, i);
1023 if strict_eq_values(&elem, &search) {
1024 return Ok(Value::Number(i as f64));
1025 }
1026 }
1027 Ok(Value::Number(-1.0))
1028}
1029
1030fn array_last_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1031 let obj_ref = match ctx.this.gc_ref() {
1032 Some(r) => r,
1033 None => return Ok(Value::Number(-1.0)),
1034 };
1035 let search = args.first().cloned().unwrap_or(Value::Undefined);
1036 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1037 if len == 0 {
1038 return Ok(Value::Number(-1.0));
1039 }
1040 let from = args
1041 .get(1)
1042 .map(|v| {
1043 let n = v.to_number() as i64;
1044 if n < 0 {
1045 (len as i64 + n) as usize
1046 } else {
1047 (n as usize).min(len - 1)
1048 }
1049 })
1050 .unwrap_or(len - 1);
1051 for i in (0..=from).rev() {
1052 let elem = array_get(ctx.gc, ctx.shapes, obj_ref, i);
1053 if strict_eq_values(&elem, &search) {
1054 return Ok(Value::Number(i as f64));
1055 }
1056 }
1057 Ok(Value::Number(-1.0))
1058}
1059
1060fn array_includes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1061 let obj_ref = match ctx.this.gc_ref() {
1062 Some(r) => r,
1063 None => return Ok(Value::Boolean(false)),
1064 };
1065 let search = args.first().cloned().unwrap_or(Value::Undefined);
1066 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1067 let from = args
1068 .get(1)
1069 .map(|v| {
1070 let n = v.to_number() as i64;
1071 if n < 0 {
1072 (len as i64 + n).max(0) as usize
1073 } else {
1074 n as usize
1075 }
1076 })
1077 .unwrap_or(0);
1078 for i in from..len {
1079 let elem = array_get(ctx.gc, ctx.shapes, obj_ref, i);
1080 // includes uses SameValueZero (like === but NaN === NaN).
1081 if same_value_zero(&elem, &search) {
1082 return Ok(Value::Boolean(true));
1083 }
1084 }
1085 Ok(Value::Boolean(false))
1086}
1087
1088fn array_join(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1089 let obj_ref = match ctx.this.gc_ref() {
1090 Some(r) => r,
1091 None => return Ok(Value::String(String::new())),
1092 };
1093 let sep = args
1094 .first()
1095 .map(|v| {
1096 if matches!(v, Value::Undefined) {
1097 ",".to_string()
1098 } else {
1099 v.to_js_string(ctx.gc)
1100 }
1101 })
1102 .unwrap_or_else(|| ",".to_string());
1103 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1104 let mut parts = Vec::with_capacity(len);
1105 for i in 0..len {
1106 let elem = array_get(ctx.gc, ctx.shapes, obj_ref, i);
1107 if matches!(elem, Value::Undefined | Value::Null) {
1108 parts.push(String::new());
1109 } else {
1110 parts.push(elem.to_js_string(ctx.gc));
1111 }
1112 }
1113 Ok(Value::String(parts.join(&sep)))
1114}
1115
1116fn array_slice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1117 let obj_ref = match ctx.this.gc_ref() {
1118 Some(r) => r,
1119 None => return Ok(Value::Undefined),
1120 };
1121 let len = array_length(ctx.gc, ctx.shapes, obj_ref) as i64;
1122 let start = args
1123 .first()
1124 .map(|v| {
1125 let n = v.to_number() as i64;
1126 if n < 0 {
1127 (len + n).max(0)
1128 } else {
1129 n.min(len)
1130 }
1131 })
1132 .unwrap_or(0) as usize;
1133 let end = args
1134 .get(1)
1135 .map(|v| {
1136 if matches!(v, Value::Undefined) {
1137 len
1138 } else {
1139 let n = v.to_number() as i64;
1140 if n < 0 {
1141 (len + n).max(0)
1142 } else {
1143 n.min(len)
1144 }
1145 }
1146 })
1147 .unwrap_or(len) as usize;
1148
1149 let mut items = Vec::new();
1150 for i in start..end {
1151 items.push(array_get(ctx.gc, ctx.shapes, obj_ref, i));
1152 }
1153 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
1154}
1155
1156fn array_concat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1157 let obj_ref = match ctx.this.gc_ref() {
1158 Some(r) => r,
1159 None => return Ok(Value::Undefined),
1160 };
1161 let mut items = Vec::new();
1162 // First, add elements from `this`.
1163 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1164 for i in 0..len {
1165 items.push(array_get(ctx.gc, ctx.shapes, obj_ref, i));
1166 }
1167 // Then add from each argument.
1168 for arg in args {
1169 match arg {
1170 Value::Object(r) => {
1171 let arg_len = array_length(ctx.gc, ctx.shapes, *r);
1172 // Only spread array-like objects (those with a length property).
1173 if array_length_exists(ctx.gc, ctx.shapes, *r) {
1174 for i in 0..arg_len {
1175 items.push(array_get(ctx.gc, ctx.shapes, *r, i));
1176 }
1177 } else {
1178 items.push(arg.clone());
1179 }
1180 }
1181 _ => items.push(arg.clone()),
1182 }
1183 }
1184 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
1185}
1186
1187fn array_reverse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1188 let _ = args;
1189 let obj_ref = match ctx.this.gc_ref() {
1190 Some(r) => r,
1191 None => return Ok(Value::Undefined),
1192 };
1193 let len = array_length(ctx.gc, ctx.shapes, obj_ref);
1194 // Read all values.
1195 let mut vals: Vec<Value> = (0..len)
1196 .map(|i| array_get(ctx.gc, ctx.shapes, obj_ref, i))
1197 .collect();
1198 vals.reverse();
1199 // Write back.
1200 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
1201 for (i, v) in vals.into_iter().enumerate() {
1202 data.insert_property(i.to_string(), Property::data(v), ctx.shapes);
1203 }
1204 }
1205 Ok(ctx.this.clone())
1206}
1207
1208fn array_splice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1209 let obj_ref = match ctx.this.gc_ref() {
1210 Some(r) => r,
1211 None => return Ok(Value::Undefined),
1212 };
1213 let len = array_length(ctx.gc, ctx.shapes, obj_ref) as i64;
1214 let start = args
1215 .first()
1216 .map(|v| {
1217 let n = v.to_number() as i64;
1218 if n < 0 {
1219 (len + n).max(0)
1220 } else {
1221 n.min(len)
1222 }
1223 })
1224 .unwrap_or(0) as usize;
1225 let delete_count = args
1226 .get(1)
1227 .map(|v| {
1228 let n = v.to_number() as i64;
1229 n.max(0).min(len - start as i64) as usize
1230 })
1231 .unwrap_or((len - start as i64).max(0) as usize);
1232 let insert_items: Vec<Value> = args.iter().skip(2).cloned().collect();
1233
1234 // Collect current values.
1235 let all_vals: Vec<Value> = (0..len as usize)
1236 .map(|i| array_get(ctx.gc, ctx.shapes, obj_ref, i))
1237 .collect();
1238
1239 // Build removed slice.
1240 let removed: Vec<Value> = all_vals[start..start + delete_count].to_vec();
1241
1242 // Build new array content.
1243 let mut new_vals = Vec::new();
1244 new_vals.extend_from_slice(&all_vals[..start]);
1245 new_vals.extend(insert_items);
1246 new_vals.extend_from_slice(&all_vals[start + delete_count..]);
1247
1248 // Write back.
1249 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
1250 // Remove all numeric keys.
1251 let old_keys: Vec<String> = data
1252 .property_keys(ctx.shapes)
1253 .into_iter()
1254 .filter(|k| k.parse::<usize>().is_ok())
1255 .collect();
1256 for k in old_keys {
1257 data.remove_property(&k, ctx.shapes);
1258 }
1259 // Write new values.
1260 for (i, v) in new_vals.iter().enumerate() {
1261 data.insert_property(i.to_string(), Property::data(v.clone()), ctx.shapes);
1262 }
1263 }
1264 set_array_length(ctx.gc, ctx.shapes, obj_ref, new_vals.len());
1265 Ok(make_value_array(ctx.gc, ctx.shapes, &removed))
1266}
1267
1268fn array_fill(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1269 let obj_ref = match ctx.this.gc_ref() {
1270 Some(r) => r,
1271 None => return Ok(Value::Undefined),
1272 };
1273 let val = args.first().cloned().unwrap_or(Value::Undefined);
1274 let len = array_length(ctx.gc, ctx.shapes, obj_ref) as i64;
1275 let start = args
1276 .get(1)
1277 .map(|v| {
1278 let n = v.to_number() as i64;
1279 if n < 0 {
1280 (len + n).max(0)
1281 } else {
1282 n.min(len)
1283 }
1284 })
1285 .unwrap_or(0) as usize;
1286 let end = args
1287 .get(2)
1288 .map(|v| {
1289 if matches!(v, Value::Undefined) {
1290 len
1291 } else {
1292 let n = v.to_number() as i64;
1293 if n < 0 {
1294 (len + n).max(0)
1295 } else {
1296 n.min(len)
1297 }
1298 }
1299 })
1300 .unwrap_or(len) as usize;
1301 for i in start..end {
1302 array_set(ctx.gc, ctx.shapes, obj_ref, i, val.clone());
1303 }
1304 Ok(ctx.this.clone())
1305}
1306
1307fn array_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1308 // Array.prototype.toString is the same as join(",").
1309 array_join(
1310 &[Value::String(",".to_string())],
1311 &mut NativeContext {
1312 gc: ctx.gc,
1313 shapes: ctx.shapes,
1314 this: ctx.this.clone(),
1315 console_output: ctx.console_output,
1316 dom_bridge: ctx.dom_bridge,
1317 },
1318 )
1319 .or_else(|_| Ok(Value::String(String::new())))
1320}
1321
1322fn array_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1323 let obj_ref = match ctx.this.gc_ref() {
1324 Some(r) => r,
1325 None => return Ok(Value::Undefined),
1326 };
1327 let len = array_length(ctx.gc, ctx.shapes, obj_ref) as i64;
1328 let index = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1329 let actual = if index < 0 { len + index } else { index };
1330 if actual < 0 || actual >= len {
1331 Ok(Value::Undefined)
1332 } else {
1333 Ok(array_get(ctx.gc, ctx.shapes, obj_ref, actual as usize))
1334 }
1335}
1336
1337// ── Array constructor + static methods ───────────────────────
1338
1339fn init_array_constructor(gc: &mut Gc<HeapObject>, arr_proto: GcRef) -> GcRef {
1340 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1341 name: "Array".to_string(),
1342 kind: FunctionKind::Native(NativeFunc {
1343 callback: array_constructor,
1344 }),
1345 prototype_obj: Some(arr_proto),
1346 properties: HashMap::new(),
1347 upvalues: Vec::new(),
1348 })));
1349
1350 let is_array = make_native(gc, "isArray", array_is_array);
1351 set_func_prop(gc, ctor, "isArray", Value::Function(is_array));
1352
1353 let from = make_native(gc, "from", array_from);
1354 set_func_prop(gc, ctor, "from", Value::Function(from));
1355
1356 let of = make_native(gc, "of", array_of);
1357 set_func_prop(gc, ctor, "of", Value::Function(of));
1358
1359 ctor
1360}
1361
1362fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1363 if args.len() == 1 {
1364 if let Value::Number(n) = &args[0] {
1365 let len = *n as usize;
1366 let mut obj = ObjectData::new();
1367 obj.insert_property(
1368 "length".to_string(),
1369 Property {
1370 value: Value::Number(len as f64),
1371 writable: true,
1372 enumerable: false,
1373 configurable: false,
1374 },
1375 ctx.shapes,
1376 );
1377 return Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))));
1378 }
1379 }
1380 Ok(make_value_array(ctx.gc, ctx.shapes, args))
1381}
1382
1383fn array_is_array(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1384 // An "array" is an object that has a numeric length property.
1385 // This is a simplified check — real JS uses an internal [[Class]] slot.
1386 match args.first() {
1387 Some(Value::Object(r)) => match ctx.gc.get(*r) {
1388 Some(HeapObject::Object(data)) => {
1389 Ok(Value::Boolean(data.contains_key("length", ctx.shapes)))
1390 }
1391 _ => Ok(Value::Boolean(false)),
1392 },
1393 _ => Ok(Value::Boolean(false)),
1394 }
1395}
1396
1397fn array_from(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1398 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
1399 match iterable {
1400 Value::Object(r) => {
1401 let len = array_length(ctx.gc, ctx.shapes, r);
1402 let mut items = Vec::with_capacity(len);
1403 for i in 0..len {
1404 items.push(array_get(ctx.gc, ctx.shapes, r, i));
1405 }
1406 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
1407 }
1408 Value::String(s) => {
1409 let chars: Vec<Value> = s.chars().map(|c| Value::String(c.to_string())).collect();
1410 Ok(make_value_array(ctx.gc, ctx.shapes, &chars))
1411 }
1412 _ => Ok(make_value_array(ctx.gc, ctx.shapes, &[])),
1413 }
1414}
1415
1416fn array_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1417 Ok(make_value_array(ctx.gc, ctx.shapes, args))
1418}
1419
1420// ── Error constructors ───────────────────────────────────────
1421
1422fn init_error_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
1423 let to_string = make_native(gc, "toString", error_proto_to_string);
1424 set_builtin_prop(gc, shapes, proto, "toString", Value::Function(to_string));
1425}
1426
1427fn init_error_constructors(vm: &mut Vm, err_proto: GcRef) {
1428 // Base Error.
1429 let error_ctor = make_error_constructor(&mut vm.gc, "Error", err_proto);
1430 vm.set_global("Error", Value::Function(error_ctor));
1431
1432 // TypeError.
1433 let te_proto = make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "TypeError", err_proto);
1434 let te_ctor = make_error_constructor(&mut vm.gc, "TypeError", te_proto);
1435 vm.set_global("TypeError", Value::Function(te_ctor));
1436
1437 // ReferenceError.
1438 let re_proto =
1439 make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "ReferenceError", err_proto);
1440 let re_ctor = make_error_constructor(&mut vm.gc, "ReferenceError", re_proto);
1441 vm.set_global("ReferenceError", Value::Function(re_ctor));
1442
1443 // SyntaxError.
1444 let se_proto = make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "SyntaxError", err_proto);
1445 let se_ctor = make_error_constructor(&mut vm.gc, "SyntaxError", se_proto);
1446 vm.set_global("SyntaxError", Value::Function(se_ctor));
1447
1448 // RangeError.
1449 let rae_proto = make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "RangeError", err_proto);
1450 let rae_ctor = make_error_constructor(&mut vm.gc, "RangeError", rae_proto);
1451 vm.set_global("RangeError", Value::Function(rae_ctor));
1452
1453 // URIError.
1454 let ue_proto = make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "URIError", err_proto);
1455 let ue_ctor = make_error_constructor(&mut vm.gc, "URIError", ue_proto);
1456 vm.set_global("URIError", Value::Function(ue_ctor));
1457
1458 // EvalError.
1459 let ee_proto = make_error_subclass_proto(&mut vm.gc, &mut vm.shapes, "EvalError", err_proto);
1460 let ee_ctor = make_error_constructor(&mut vm.gc, "EvalError", ee_proto);
1461 vm.set_global("EvalError", Value::Function(ee_ctor));
1462}
1463
1464fn make_error_subclass_proto(
1465 gc: &mut Gc<HeapObject>,
1466 shapes: &mut ShapeTable,
1467 name: &str,
1468 parent_proto: GcRef,
1469) -> GcRef {
1470 let mut data = ObjectData::new();
1471 data.prototype = Some(parent_proto);
1472 data.insert_property(
1473 "name".to_string(),
1474 Property::builtin(Value::String(name.to_string())),
1475 shapes,
1476 );
1477 data.insert_property(
1478 "message".to_string(),
1479 Property::builtin(Value::String(String::new())),
1480 shapes,
1481 );
1482 gc.alloc(HeapObject::Object(data))
1483}
1484
1485fn make_error_constructor(gc: &mut Gc<HeapObject>, name: &str, proto: GcRef) -> GcRef {
1486 gc.alloc(HeapObject::Function(Box::new(FunctionData {
1487 name: name.to_string(),
1488 kind: FunctionKind::Native(NativeFunc {
1489 callback: error_constructor,
1490 }),
1491 prototype_obj: Some(proto),
1492 properties: HashMap::new(),
1493 upvalues: Vec::new(),
1494 })))
1495}
1496
1497fn error_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1498 let message = args
1499 .first()
1500 .map(|v| v.to_js_string(ctx.gc))
1501 .unwrap_or_default();
1502 let mut obj = ObjectData::new();
1503 obj.insert_property(
1504 "message".to_string(),
1505 Property::data(Value::String(message)),
1506 ctx.shapes,
1507 );
1508 // The "name" property comes from the prototype chain.
1509 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))))
1510}
1511
1512fn error_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1513 let obj_ref = match ctx.this.gc_ref() {
1514 Some(r) => r,
1515 None => return Ok(Value::String("Error".to_string())),
1516 };
1517 let name = match ctx.gc.get(obj_ref) {
1518 Some(HeapObject::Object(data)) => data
1519 .get_property("name", ctx.shapes)
1520 .map(|p| p.value.to_js_string(ctx.gc))
1521 .unwrap_or_else(|| "Error".to_string()),
1522 _ => "Error".to_string(),
1523 };
1524 let message = match ctx.gc.get(obj_ref) {
1525 Some(HeapObject::Object(data)) => data
1526 .get_property("message", ctx.shapes)
1527 .map(|p| p.value.to_js_string(ctx.gc))
1528 .unwrap_or_default(),
1529 _ => String::new(),
1530 };
1531 if message.is_empty() {
1532 Ok(Value::String(name))
1533 } else {
1534 Ok(Value::String(format!("{name}: {message}")))
1535 }
1536}
1537
1538// ── Array iterators ──────────────────────────────────────────
1539
1540/// Helper: create an iterator object that yields values from a closure.
1541/// `state` is a GcRef to an object with `__items__` (array) and `__idx__` (number).
1542fn make_simple_iterator(
1543 gc: &mut Gc<HeapObject>,
1544 shapes: &mut ShapeTable,
1545 items: GcRef,
1546 next_fn: fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>,
1547) -> Value {
1548 let mut obj = ObjectData::new();
1549 obj.insert_property(
1550 "__items__".to_string(),
1551 Property::builtin(Value::Object(items)),
1552 shapes,
1553 );
1554 obj.insert_property(
1555 "__idx__".to_string(),
1556 Property::builtin(Value::Number(0.0)),
1557 shapes,
1558 );
1559 let next = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1560 name: "next".to_string(),
1561 kind: FunctionKind::Native(NativeFunc { callback: next_fn }),
1562 prototype_obj: None,
1563 properties: HashMap::new(),
1564 upvalues: Vec::new(),
1565 })));
1566 obj.insert_property(
1567 "next".to_string(),
1568 Property::builtin(Value::Function(next)),
1569 shapes,
1570 );
1571
1572 // @@iterator returns self.
1573 let self_iter = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1574 name: "[Symbol.iterator]".to_string(),
1575 kind: FunctionKind::Native(NativeFunc {
1576 callback: iter_self,
1577 }),
1578 prototype_obj: None,
1579 properties: HashMap::new(),
1580 upvalues: Vec::new(),
1581 })));
1582 obj.insert_property(
1583 "@@iterator".to_string(),
1584 Property::builtin(Value::Function(self_iter)),
1585 shapes,
1586 );
1587
1588 let r = gc.alloc(HeapObject::Object(obj));
1589 Value::Object(r)
1590}
1591
1592fn iter_self(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1593 Ok(ctx.this.clone())
1594}
1595
1596fn make_iterator_result_native(
1597 gc: &mut Gc<HeapObject>,
1598 shapes: &mut ShapeTable,
1599 value: Value,
1600 done: bool,
1601) -> Value {
1602 let mut obj = ObjectData::new();
1603 obj.insert_property("value".to_string(), Property::data(value), shapes);
1604 obj.insert_property(
1605 "done".to_string(),
1606 Property::data(Value::Boolean(done)),
1607 shapes,
1608 );
1609 let r = gc.alloc(HeapObject::Object(obj));
1610 Value::Object(r)
1611}
1612
1613/// Array.prototype[@@iterator]() — same as values().
1614fn array_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1615 array_values_iter(_args, ctx)
1616}
1617
1618/// Array.prototype.values() — returns iterator over values.
1619fn array_values_iter(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1620 let obj_ref = ctx
1621 .this
1622 .gc_ref()
1623 .ok_or_else(|| RuntimeError::type_error("values called on non-object"))?;
1624 Ok(make_simple_iterator(
1625 ctx.gc,
1626 ctx.shapes,
1627 obj_ref,
1628 array_values_next,
1629 ))
1630}
1631
1632fn array_values_next(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1633 let iter_ref = ctx
1634 .this
1635 .gc_ref()
1636 .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?;
1637
1638 let (items_ref, idx) = get_iter_state(ctx.gc, ctx.shapes, iter_ref);
1639 let items_ref = match items_ref {
1640 Some(r) => r,
1641 None => {
1642 return Ok(make_iterator_result_native(
1643 ctx.gc,
1644 ctx.shapes,
1645 Value::Undefined,
1646 true,
1647 ))
1648 }
1649 };
1650
1651 let len = array_length(ctx.gc, ctx.shapes, items_ref);
1652 if idx >= len {
1653 return Ok(make_iterator_result_native(
1654 ctx.gc,
1655 ctx.shapes,
1656 Value::Undefined,
1657 true,
1658 ));
1659 }
1660
1661 let val = array_get(ctx.gc, ctx.shapes, items_ref, idx);
1662 set_iter_idx(ctx.gc, ctx.shapes, iter_ref, idx + 1);
1663 Ok(make_iterator_result_native(ctx.gc, ctx.shapes, val, false))
1664}
1665
1666/// Array.prototype.keys() — returns iterator over indices.
1667fn array_keys_iter(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1668 let obj_ref = ctx
1669 .this
1670 .gc_ref()
1671 .ok_or_else(|| RuntimeError::type_error("keys called on non-object"))?;
1672 Ok(make_simple_iterator(
1673 ctx.gc,
1674 ctx.shapes,
1675 obj_ref,
1676 array_keys_next,
1677 ))
1678}
1679
1680fn array_keys_next(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1681 let iter_ref = ctx
1682 .this
1683 .gc_ref()
1684 .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?;
1685 let (items_ref, idx) = get_iter_state(ctx.gc, ctx.shapes, iter_ref);
1686 let items_ref = match items_ref {
1687 Some(r) => r,
1688 None => {
1689 return Ok(make_iterator_result_native(
1690 ctx.gc,
1691 ctx.shapes,
1692 Value::Undefined,
1693 true,
1694 ))
1695 }
1696 };
1697 let len = array_length(ctx.gc, ctx.shapes, items_ref);
1698 if idx >= len {
1699 return Ok(make_iterator_result_native(
1700 ctx.gc,
1701 ctx.shapes,
1702 Value::Undefined,
1703 true,
1704 ));
1705 }
1706 set_iter_idx(ctx.gc, ctx.shapes, iter_ref, idx + 1);
1707 Ok(make_iterator_result_native(
1708 ctx.gc,
1709 ctx.shapes,
1710 Value::Number(idx as f64),
1711 false,
1712 ))
1713}
1714
1715/// Array.prototype.entries() — returns iterator over [index, value] pairs.
1716fn array_entries_iter(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1717 let obj_ref = ctx
1718 .this
1719 .gc_ref()
1720 .ok_or_else(|| RuntimeError::type_error("entries called on non-object"))?;
1721 Ok(make_simple_iterator(
1722 ctx.gc,
1723 ctx.shapes,
1724 obj_ref,
1725 array_entries_next,
1726 ))
1727}
1728
1729fn array_entries_next(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1730 let iter_ref = ctx
1731 .this
1732 .gc_ref()
1733 .ok_or_else(|| RuntimeError::type_error("next called on non-iterator"))?;
1734 let (items_ref, idx) = get_iter_state(ctx.gc, ctx.shapes, iter_ref);
1735 let items_ref = match items_ref {
1736 Some(r) => r,
1737 None => {
1738 return Ok(make_iterator_result_native(
1739 ctx.gc,
1740 ctx.shapes,
1741 Value::Undefined,
1742 true,
1743 ))
1744 }
1745 };
1746 let len = array_length(ctx.gc, ctx.shapes, items_ref);
1747 if idx >= len {
1748 return Ok(make_iterator_result_native(
1749 ctx.gc,
1750 ctx.shapes,
1751 Value::Undefined,
1752 true,
1753 ));
1754 }
1755 let val = array_get(ctx.gc, ctx.shapes, items_ref, idx);
1756 set_iter_idx(ctx.gc, ctx.shapes, iter_ref, idx + 1);
1757
1758 // Create [index, value] pair array.
1759 let mut pair = ObjectData::new();
1760 pair.insert_property(
1761 "0".to_string(),
1762 Property::data(Value::Number(idx as f64)),
1763 ctx.shapes,
1764 );
1765 pair.insert_property("1".to_string(), Property::data(val), ctx.shapes);
1766 pair.insert_property(
1767 "length".to_string(),
1768 Property {
1769 value: Value::Number(2.0),
1770 writable: true,
1771 enumerable: false,
1772 configurable: false,
1773 },
1774 ctx.shapes,
1775 );
1776 let pair_ref = ctx.gc.alloc(HeapObject::Object(pair));
1777 Ok(make_iterator_result_native(
1778 ctx.gc,
1779 ctx.shapes,
1780 Value::Object(pair_ref),
1781 false,
1782 ))
1783}
1784
1785/// Helper to read __items__ and __idx__ from an iterator state object.
1786fn get_iter_state(
1787 gc: &Gc<HeapObject>,
1788 shapes: &ShapeTable,
1789 iter_ref: GcRef,
1790) -> (Option<GcRef>, usize) {
1791 match gc.get(iter_ref) {
1792 Some(HeapObject::Object(data)) => {
1793 let items = data
1794 .get_property("__items__", shapes)
1795 .and_then(|p| p.value.gc_ref());
1796 let idx = data
1797 .get_property("__idx__", shapes)
1798 .map(|p| p.value.to_number() as usize)
1799 .unwrap_or(0);
1800 (items, idx)
1801 }
1802 _ => (None, 0),
1803 }
1804}
1805
1806/// Helper to update __idx__ on an iterator state object.
1807fn set_iter_idx(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, iter_ref: GcRef, idx: usize) {
1808 if let Some(HeapObject::Object(data)) = gc.get_mut(iter_ref) {
1809 data.insert_property(
1810 "__idx__".to_string(),
1811 Property::builtin(Value::Number(idx as f64)),
1812 shapes,
1813 );
1814 }
1815}
1816
1817// ── String built-in ──────────────────────────────────────────
1818
1819fn init_string_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
1820 let methods: &[NativeMethod] = &[
1821 ("charAt", string_proto_char_at),
1822 ("charCodeAt", string_proto_char_code_at),
1823 ("codePointAt", string_proto_code_point_at),
1824 ("concat", string_proto_concat),
1825 ("slice", string_proto_slice),
1826 ("substring", string_proto_substring),
1827 ("substr", string_proto_substr),
1828 ("indexOf", string_proto_index_of),
1829 ("lastIndexOf", string_proto_last_index_of),
1830 ("includes", string_proto_includes),
1831 ("startsWith", string_proto_starts_with),
1832 ("endsWith", string_proto_ends_with),
1833 ("trim", string_proto_trim),
1834 ("trimStart", string_proto_trim_start),
1835 ("trimEnd", string_proto_trim_end),
1836 ("padStart", string_proto_pad_start),
1837 ("padEnd", string_proto_pad_end),
1838 ("repeat", string_proto_repeat),
1839 ("split", string_proto_split),
1840 ("replace", string_proto_replace),
1841 ("replaceAll", string_proto_replace_all),
1842 ("match", string_proto_match),
1843 ("matchAll", string_proto_match_all),
1844 ("search", string_proto_search),
1845 ("toLowerCase", string_proto_to_lower_case),
1846 ("toUpperCase", string_proto_to_upper_case),
1847 ("at", string_proto_at),
1848 ("toString", string_proto_to_string),
1849 ("valueOf", string_proto_value_of),
1850 ];
1851 for &(name, callback) in methods {
1852 let f = make_native(gc, name, callback);
1853 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
1854 }
1855
1856 // @@iterator: iterates over characters.
1857 let iter_fn = make_native(gc, "[Symbol.iterator]", string_iterator);
1858 set_builtin_prop(gc, shapes, proto, "@@iterator", Value::Function(iter_fn));
1859}
1860
1861/// String.prototype[@@iterator]() — returns an iterator over characters.
1862fn string_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1863 let s = ctx.this.to_js_string(ctx.gc);
1864
1865 // Store the string's characters in an array-like object.
1866 let mut items = ObjectData::new();
1867 for (i, ch) in s.chars().enumerate() {
1868 items.insert_property(
1869 i.to_string(),
1870 Property::data(Value::String(ch.to_string())),
1871 ctx.shapes,
1872 );
1873 }
1874 items.insert_property(
1875 "length".to_string(),
1876 Property {
1877 value: Value::Number(s.chars().count() as f64),
1878 writable: true,
1879 enumerable: false,
1880 configurable: false,
1881 },
1882 ctx.shapes,
1883 );
1884 let items_ref = ctx.gc.alloc(HeapObject::Object(items));
1885 Ok(make_simple_iterator(
1886 ctx.gc,
1887 ctx.shapes,
1888 items_ref,
1889 array_values_next,
1890 ))
1891}
1892
1893fn init_string_constructor(gc: &mut Gc<HeapObject>, str_proto: GcRef) -> GcRef {
1894 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1895 name: "String".to_string(),
1896 kind: FunctionKind::Native(NativeFunc {
1897 callback: string_constructor,
1898 }),
1899 prototype_obj: Some(str_proto),
1900 properties: HashMap::new(),
1901 upvalues: Vec::new(),
1902 })));
1903 let from_char_code = make_native(gc, "fromCharCode", string_from_char_code);
1904 set_func_prop(gc, ctor, "fromCharCode", Value::Function(from_char_code));
1905 let from_code_point = make_native(gc, "fromCodePoint", string_from_code_point);
1906 set_func_prop(gc, ctor, "fromCodePoint", Value::Function(from_code_point));
1907 ctor
1908}
1909
1910fn string_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1911 let s = args
1912 .first()
1913 .map(|v| v.to_js_string(ctx.gc))
1914 .unwrap_or_default();
1915 Ok(Value::String(s))
1916}
1917
1918fn string_from_char_code(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1919 let s: String = args
1920 .iter()
1921 .filter_map(|v| {
1922 let code = v.to_number() as u32;
1923 char::from_u32(code)
1924 })
1925 .collect();
1926 Ok(Value::String(s))
1927}
1928
1929fn string_from_code_point(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1930 let mut s = String::new();
1931 for v in args {
1932 let code = v.to_number() as u32;
1933 match char::from_u32(code) {
1934 Some(c) => s.push(c),
1935 None => {
1936 return Err(RuntimeError::range_error(format!(
1937 "Invalid code point {code}"
1938 )))
1939 }
1940 }
1941 }
1942 Ok(Value::String(s))
1943}
1944
1945/// Helper: extract the string from `this` for String.prototype methods.
1946fn this_string(ctx: &NativeContext) -> String {
1947 ctx.this.to_js_string(ctx.gc)
1948}
1949
1950/// Helper: get chars as a Vec for index-based operations.
1951fn str_chars(s: &str) -> Vec<char> {
1952 s.chars().collect()
1953}
1954
1955fn string_proto_char_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1956 let s = this_string(ctx);
1957 let chars = str_chars(&s);
1958 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1959 if idx < 0 || idx as usize >= chars.len() {
1960 Ok(Value::String(String::new()))
1961 } else {
1962 Ok(Value::String(chars[idx as usize].to_string()))
1963 }
1964}
1965
1966fn string_proto_char_code_at(
1967 args: &[Value],
1968 ctx: &mut NativeContext,
1969) -> Result<Value, RuntimeError> {
1970 let s = this_string(ctx);
1971 let chars = str_chars(&s);
1972 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1973 if idx < 0 || idx as usize >= chars.len() {
1974 Ok(Value::Number(f64::NAN))
1975 } else {
1976 Ok(Value::Number(chars[idx as usize] as u32 as f64))
1977 }
1978}
1979
1980fn string_proto_code_point_at(
1981 args: &[Value],
1982 ctx: &mut NativeContext,
1983) -> Result<Value, RuntimeError> {
1984 let s = this_string(ctx);
1985 let chars = str_chars(&s);
1986 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1987 if idx < 0 || idx as usize >= chars.len() {
1988 Ok(Value::Undefined)
1989 } else {
1990 Ok(Value::Number(chars[idx as usize] as u32 as f64))
1991 }
1992}
1993
1994fn string_proto_concat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1995 let mut s = this_string(ctx);
1996 for arg in args {
1997 s.push_str(&arg.to_js_string(ctx.gc));
1998 }
1999 Ok(Value::String(s))
2000}
2001
2002fn string_proto_slice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2003 let s = this_string(ctx);
2004 let chars = str_chars(&s);
2005 let len = chars.len() as i64;
2006 let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
2007 let end = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len);
2008 let start = if start < 0 {
2009 (len + start).max(0) as usize
2010 } else {
2011 start.min(len) as usize
2012 };
2013 let end = if end < 0 {
2014 (len + end).max(0) as usize
2015 } else {
2016 end.min(len) as usize
2017 };
2018 if start >= end {
2019 Ok(Value::String(String::new()))
2020 } else {
2021 Ok(Value::String(chars[start..end].iter().collect()))
2022 }
2023}
2024
2025fn string_proto_substring(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2026 let s = this_string(ctx);
2027 let chars = str_chars(&s);
2028 let len = chars.len() as i64;
2029 let a = args
2030 .first()
2031 .map(|v| v.to_number() as i64)
2032 .unwrap_or(0)
2033 .clamp(0, len) as usize;
2034 let b = args
2035 .get(1)
2036 .map(|v| v.to_number() as i64)
2037 .unwrap_or(len)
2038 .clamp(0, len) as usize;
2039 let (start, end) = if a <= b { (a, b) } else { (b, a) };
2040 Ok(Value::String(chars[start..end].iter().collect()))
2041}
2042
2043fn string_proto_substr(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2044 let s = this_string(ctx);
2045 let chars = str_chars(&s);
2046 let len = chars.len() as i64;
2047 let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
2048 let start = if start < 0 {
2049 (len + start).max(0) as usize
2050 } else {
2051 start.min(len) as usize
2052 };
2053 let count = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len) as usize;
2054 let end = (start + count).min(chars.len());
2055 Ok(Value::String(chars[start..end].iter().collect()))
2056}
2057
2058fn string_proto_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2059 let s = this_string(ctx);
2060 let search = args
2061 .first()
2062 .map(|v| v.to_js_string(ctx.gc))
2063 .unwrap_or_default();
2064 let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
2065 let chars = str_chars(&s);
2066 let search_chars = str_chars(&search);
2067 if search_chars.is_empty() {
2068 return Ok(Value::Number(from.min(chars.len()) as f64));
2069 }
2070 for i in from..chars.len() {
2071 if i + search_chars.len() <= chars.len()
2072 && chars[i..i + search_chars.len()] == search_chars[..]
2073 {
2074 return Ok(Value::Number(i as f64));
2075 }
2076 }
2077 Ok(Value::Number(-1.0))
2078}
2079
2080fn string_proto_last_index_of(
2081 args: &[Value],
2082 ctx: &mut NativeContext,
2083) -> Result<Value, RuntimeError> {
2084 let s = this_string(ctx);
2085 let search = args
2086 .first()
2087 .map(|v| v.to_js_string(ctx.gc))
2088 .unwrap_or_default();
2089 let chars = str_chars(&s);
2090 let search_chars = str_chars(&search);
2091 let from = args
2092 .get(1)
2093 .map(|v| {
2094 let n = v.to_number();
2095 if n.is_nan() {
2096 chars.len()
2097 } else {
2098 n as usize
2099 }
2100 })
2101 .unwrap_or(chars.len());
2102 if search_chars.is_empty() {
2103 return Ok(Value::Number(from.min(chars.len()) as f64));
2104 }
2105 let max_start = from.min(chars.len().saturating_sub(search_chars.len()));
2106 for i in (0..=max_start).rev() {
2107 if i + search_chars.len() <= chars.len()
2108 && chars[i..i + search_chars.len()] == search_chars[..]
2109 {
2110 return Ok(Value::Number(i as f64));
2111 }
2112 }
2113 Ok(Value::Number(-1.0))
2114}
2115
2116fn string_proto_includes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2117 let s = this_string(ctx);
2118 let search = args
2119 .first()
2120 .map(|v| v.to_js_string(ctx.gc))
2121 .unwrap_or_default();
2122 let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
2123 let chars = str_chars(&s);
2124 let search_chars = str_chars(&search);
2125 if search_chars.is_empty() {
2126 return Ok(Value::Boolean(true));
2127 }
2128 for i in from..chars.len() {
2129 if i + search_chars.len() <= chars.len()
2130 && chars[i..i + search_chars.len()] == search_chars[..]
2131 {
2132 return Ok(Value::Boolean(true));
2133 }
2134 }
2135 Ok(Value::Boolean(false))
2136}
2137
2138fn string_proto_starts_with(
2139 args: &[Value],
2140 ctx: &mut NativeContext,
2141) -> Result<Value, RuntimeError> {
2142 let s = this_string(ctx);
2143 let search = args
2144 .first()
2145 .map(|v| v.to_js_string(ctx.gc))
2146 .unwrap_or_default();
2147 let pos = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
2148 let chars = str_chars(&s);
2149 let search_chars = str_chars(&search);
2150 if pos + search_chars.len() > chars.len() {
2151 return Ok(Value::Boolean(false));
2152 }
2153 Ok(Value::Boolean(
2154 chars[pos..pos + search_chars.len()] == search_chars[..],
2155 ))
2156}
2157
2158fn string_proto_ends_with(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2159 let s = this_string(ctx);
2160 let search = args
2161 .first()
2162 .map(|v| v.to_js_string(ctx.gc))
2163 .unwrap_or_default();
2164 let chars = str_chars(&s);
2165 let search_chars = str_chars(&search);
2166 let end_pos = args
2167 .get(1)
2168 .map(|v| (v.to_number() as usize).min(chars.len()))
2169 .unwrap_or(chars.len());
2170 if search_chars.len() > end_pos {
2171 return Ok(Value::Boolean(false));
2172 }
2173 let start = end_pos - search_chars.len();
2174 Ok(Value::Boolean(chars[start..end_pos] == search_chars[..]))
2175}
2176
2177fn string_proto_trim(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2178 let _ = args;
2179 Ok(Value::String(this_string(ctx).trim().to_string()))
2180}
2181
2182fn string_proto_trim_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2183 let _ = args;
2184 Ok(Value::String(this_string(ctx).trim_start().to_string()))
2185}
2186
2187fn string_proto_trim_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2188 let _ = args;
2189 Ok(Value::String(this_string(ctx).trim_end().to_string()))
2190}
2191
2192fn string_proto_pad_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2193 let s = this_string(ctx);
2194 let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
2195 let fill = args
2196 .get(1)
2197 .map(|v| v.to_js_string(ctx.gc))
2198 .unwrap_or_else(|| " ".to_string());
2199 let chars = str_chars(&s);
2200 if chars.len() >= target_len || fill.is_empty() {
2201 return Ok(Value::String(s));
2202 }
2203 let fill_chars = str_chars(&fill);
2204 let needed = target_len - chars.len();
2205 let mut pad = String::new();
2206 for i in 0..needed {
2207 pad.push(fill_chars[i % fill_chars.len()]);
2208 }
2209 pad.push_str(&s);
2210 Ok(Value::String(pad))
2211}
2212
2213fn string_proto_pad_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2214 let s = this_string(ctx);
2215 let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
2216 let fill = args
2217 .get(1)
2218 .map(|v| v.to_js_string(ctx.gc))
2219 .unwrap_or_else(|| " ".to_string());
2220 let chars = str_chars(&s);
2221 if chars.len() >= target_len || fill.is_empty() {
2222 return Ok(Value::String(s));
2223 }
2224 let fill_chars = str_chars(&fill);
2225 let needed = target_len - chars.len();
2226 let mut result = s;
2227 for i in 0..needed {
2228 result.push(fill_chars[i % fill_chars.len()]);
2229 }
2230 Ok(Value::String(result))
2231}
2232
2233fn string_proto_repeat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2234 let s = this_string(ctx);
2235 let count = args.first().map(|v| v.to_number()).unwrap_or(0.0);
2236 if count < 0.0 || count.is_infinite() {
2237 return Err(RuntimeError::range_error("Invalid count value"));
2238 }
2239 Ok(Value::String(s.repeat(count as usize)))
2240}
2241
2242fn string_proto_split(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2243 let s = this_string(ctx);
2244 if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) {
2245 return Ok(make_value_array(ctx.gc, ctx.shapes, &[Value::String(s)]));
2246 }
2247 let limit = args
2248 .get(1)
2249 .map(|v| v.to_number() as usize)
2250 .unwrap_or(usize::MAX);
2251
2252 // Check if separator is a RegExp.
2253 if let Some(arg0) = args.first() {
2254 if is_regexp(ctx.gc, ctx.shapes, arg0) {
2255 return string_split_regexp(ctx.gc, ctx.shapes, &s, arg0, limit);
2256 }
2257 }
2258
2259 let sep = args[0].to_js_string(ctx.gc);
2260 if sep.is_empty() {
2261 let items: Vec<Value> = str_chars(&s)
2262 .into_iter()
2263 .take(limit)
2264 .map(|c| Value::String(c.to_string()))
2265 .collect();
2266 return Ok(make_value_array(ctx.gc, ctx.shapes, &items));
2267 }
2268 let mut items = Vec::new();
2269 let mut start = 0;
2270 let sep_len = sep.len();
2271 while let Some(pos) = s[start..].find(&sep) {
2272 if items.len() >= limit {
2273 break;
2274 }
2275 items.push(Value::String(s[start..start + pos].to_string()));
2276 start += pos + sep_len;
2277 }
2278 if items.len() < limit {
2279 items.push(Value::String(s[start..].to_string()));
2280 }
2281 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
2282}
2283
2284fn string_split_regexp(
2285 gc: &mut Gc<HeapObject>,
2286 shapes: &mut ShapeTable,
2287 s: &str,
2288 regexp: &Value,
2289 limit: usize,
2290) -> Result<Value, RuntimeError> {
2291 use crate::regex::{exec, CompiledRegex};
2292
2293 let pattern = regexp_get_pattern(gc, shapes, regexp).unwrap_or_default();
2294 let flags_str = regexp_get_flags(gc, shapes, regexp).unwrap_or_default();
2295 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
2296 let chars: Vec<char> = s.chars().collect();
2297
2298 let mut items = Vec::new();
2299 let mut last_end = 0usize;
2300
2301 loop {
2302 if items.len() >= limit {
2303 break;
2304 }
2305 match exec(&compiled, s, last_end) {
2306 Some(m) => {
2307 // Avoid infinite loop on zero-length matches.
2308 if m.start == m.end && m.start == last_end {
2309 if last_end >= chars.len() {
2310 break;
2311 }
2312 items.push(Value::String(chars[last_end].to_string()));
2313 last_end += 1;
2314 continue;
2315 }
2316 let piece: String = chars[last_end..m.start].iter().collect();
2317 items.push(Value::String(piece));
2318 // Add capturing groups.
2319 for i in 1..m.captures.len() {
2320 if items.len() >= limit {
2321 break;
2322 }
2323 match m.captures[i] {
2324 Some((cs, ce)) => {
2325 let cap: String = chars[cs..ce].iter().collect();
2326 items.push(Value::String(cap));
2327 }
2328 None => items.push(Value::Undefined),
2329 }
2330 }
2331 last_end = m.end;
2332 }
2333 None => break,
2334 }
2335 }
2336 if items.len() < limit {
2337 let rest: String = chars[last_end..].iter().collect();
2338 items.push(Value::String(rest));
2339 }
2340 Ok(make_value_array(gc, shapes, &items))
2341}
2342
2343fn string_proto_replace(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2344 let s = this_string(ctx);
2345
2346 // Check if search argument is a RegExp.
2347 if let Some(arg0) = args.first() {
2348 if is_regexp(ctx.gc, ctx.shapes, arg0) {
2349 let replacement = args
2350 .get(1)
2351 .map(|v| v.to_js_string(ctx.gc))
2352 .unwrap_or_default();
2353 return string_replace_regexp(ctx.gc, ctx.shapes, &s, arg0, &replacement);
2354 }
2355 }
2356
2357 let search = args
2358 .first()
2359 .map(|v| v.to_js_string(ctx.gc))
2360 .unwrap_or_default();
2361 let replacement = args
2362 .get(1)
2363 .map(|v| v.to_js_string(ctx.gc))
2364 .unwrap_or_default();
2365 // Replace only the first occurrence.
2366 if let Some(pos) = s.find(&search) {
2367 let mut result = String::with_capacity(s.len());
2368 result.push_str(&s[..pos]);
2369 result.push_str(&replacement);
2370 result.push_str(&s[pos + search.len()..]);
2371 Ok(Value::String(result))
2372 } else {
2373 Ok(Value::String(s))
2374 }
2375}
2376
2377fn string_replace_regexp(
2378 gc: &mut Gc<HeapObject>,
2379 shapes: &mut ShapeTable,
2380 s: &str,
2381 regexp: &Value,
2382 replacement: &str,
2383) -> Result<Value, RuntimeError> {
2384 use crate::regex::{exec, CompiledRegex};
2385
2386 let pattern = regexp_get_pattern(gc, shapes, regexp).unwrap_or_default();
2387 let flags_str = regexp_get_flags(gc, shapes, regexp).unwrap_or_default();
2388 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
2389 let is_global = compiled.flags.global;
2390 let chars: Vec<char> = s.chars().collect();
2391 let mut result = String::new();
2392 let mut last_end = 0usize;
2393
2394 while let Some(m) = exec(&compiled, s, last_end) {
2395 // Append text before match.
2396 let before: String = chars[last_end..m.start].iter().collect();
2397 result.push_str(&before);
2398 // Process replacement with $-substitutions.
2399 let matched: String = chars[m.start..m.end].iter().collect();
2400 result.push_str(&apply_replacement(
2401 replacement,
2402 &matched,
2403 &m.captures,
2404 &chars,
2405 ));
2406 last_end = m.end;
2407 if !is_global {
2408 break;
2409 }
2410 // Avoid infinite loop on zero-length match.
2411 if m.start == m.end {
2412 if last_end < chars.len() {
2413 result.push(chars[last_end]);
2414 last_end += 1;
2415 } else {
2416 break;
2417 }
2418 }
2419 }
2420 let rest: String = chars[last_end..].iter().collect();
2421 result.push_str(&rest);
2422 Ok(Value::String(result))
2423}
2424
2425/// Apply replacement string with $-substitutions ($&, $1, etc.).
2426fn apply_replacement(
2427 replacement: &str,
2428 matched: &str,
2429 captures: &[Option<(usize, usize)>],
2430 chars: &[char],
2431) -> String {
2432 let rep_chars: Vec<char> = replacement.chars().collect();
2433 let mut result = String::new();
2434 let mut i = 0;
2435 while i < rep_chars.len() {
2436 if rep_chars[i] == '$' && i + 1 < rep_chars.len() {
2437 match rep_chars[i + 1] {
2438 '$' => {
2439 result.push('$');
2440 i += 2;
2441 }
2442 '&' => {
2443 result.push_str(matched);
2444 i += 2;
2445 }
2446 '`' => {
2447 // $` — text before match.
2448 if let Some(Some((start, _))) = captures.first() {
2449 let before: String = chars[..*start].iter().collect();
2450 result.push_str(&before);
2451 }
2452 i += 2;
2453 }
2454 '\'' => {
2455 // $' — text after match.
2456 if let Some(Some((_, end))) = captures.first() {
2457 let after: String = chars[*end..].iter().collect();
2458 result.push_str(&after);
2459 }
2460 i += 2;
2461 }
2462 d if d.is_ascii_digit() => {
2463 // $1, $12 etc.
2464 let mut num_str = String::new();
2465 let mut j = i + 1;
2466 while j < rep_chars.len() && rep_chars[j].is_ascii_digit() {
2467 num_str.push(rep_chars[j]);
2468 j += 1;
2469 }
2470 if let Ok(idx) = num_str.parse::<usize>() {
2471 if idx > 0 && idx < captures.len() {
2472 if let Some((s, e)) = captures[idx] {
2473 let cap: String = chars[s..e].iter().collect();
2474 result.push_str(&cap);
2475 }
2476 }
2477 }
2478 i = j;
2479 }
2480 _ => {
2481 result.push('$');
2482 i += 1;
2483 }
2484 }
2485 } else {
2486 result.push(rep_chars[i]);
2487 i += 1;
2488 }
2489 }
2490 result
2491}
2492
2493fn string_proto_replace_all(
2494 args: &[Value],
2495 ctx: &mut NativeContext,
2496) -> Result<Value, RuntimeError> {
2497 let s = this_string(ctx);
2498
2499 // If search is a RegExp, it must have the global flag.
2500 if let Some(arg0) = args.first() {
2501 if is_regexp(ctx.gc, ctx.shapes, arg0) {
2502 let flags = regexp_get_flags(ctx.gc, ctx.shapes, arg0).unwrap_or_default();
2503 if !flags.contains('g') {
2504 return Err(RuntimeError::type_error(
2505 "String.prototype.replaceAll called with a non-global RegExp argument",
2506 ));
2507 }
2508 let replacement = args
2509 .get(1)
2510 .map(|v| v.to_js_string(ctx.gc))
2511 .unwrap_or_default();
2512 return string_replace_regexp(ctx.gc, ctx.shapes, &s, arg0, &replacement);
2513 }
2514 }
2515
2516 let search = args
2517 .first()
2518 .map(|v| v.to_js_string(ctx.gc))
2519 .unwrap_or_default();
2520 let replacement = args
2521 .get(1)
2522 .map(|v| v.to_js_string(ctx.gc))
2523 .unwrap_or_default();
2524 Ok(Value::String(s.replace(&search, &replacement)))
2525}
2526
2527fn string_proto_match(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2528 let s = this_string(ctx);
2529 if args.is_empty() {
2530 return Ok(Value::Null);
2531 }
2532
2533 let arg0 = &args[0];
2534 // If arg is not a RegExp, create one.
2535 let regexp_val = if is_regexp(ctx.gc, ctx.shapes, arg0) {
2536 arg0.clone()
2537 } else {
2538 let pattern = arg0.to_js_string(ctx.gc);
2539 let proto = REGEXP_PROTO.with(|cell| cell.get());
2540 make_regexp_obj(ctx.gc, ctx.shapes, &pattern, "", proto)
2541 .map_err(RuntimeError::syntax_error)?
2542 };
2543
2544 let is_global = regexp_get_flags(ctx.gc, ctx.shapes, ®exp_val)
2545 .map(|f| f.contains('g'))
2546 .unwrap_or(false);
2547
2548 if !is_global {
2549 // Non-global: return exec result.
2550 return regexp_exec_internal(ctx.gc, ctx.shapes, ®exp_val, &s);
2551 }
2552
2553 // Global: collect all matches.
2554 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, 0.0);
2555 let mut matches = Vec::new();
2556 loop {
2557 let result = regexp_exec_internal(ctx.gc, ctx.shapes, ®exp_val, &s)?;
2558 if matches!(result, Value::Null) {
2559 break;
2560 }
2561 // Get the matched string (index 0 of the result array).
2562 if let Value::Object(r) = &result {
2563 if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) {
2564 if let Some(prop) = data.get_property("0", ctx.shapes) {
2565 matches.push(prop.value.clone());
2566 // Advance past zero-length matches.
2567 let match_str = prop.value.to_js_string(ctx.gc);
2568 if match_str.is_empty() {
2569 let li = regexp_get_last_index(ctx.gc, ctx.shapes, ®exp_val);
2570 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, li + 1.0);
2571 }
2572 }
2573 }
2574 }
2575 }
2576 if matches.is_empty() {
2577 Ok(Value::Null)
2578 } else {
2579 Ok(make_value_array(ctx.gc, ctx.shapes, &matches))
2580 }
2581}
2582
2583fn string_proto_match_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2584 let s = this_string(ctx);
2585 if args.is_empty() {
2586 return Ok(make_value_array(ctx.gc, ctx.shapes, &[]));
2587 }
2588
2589 let arg0 = &args[0];
2590 // If arg is a RegExp, it must have global flag.
2591 let regexp_val = if is_regexp(ctx.gc, ctx.shapes, arg0) {
2592 let flags = regexp_get_flags(ctx.gc, ctx.shapes, arg0).unwrap_or_default();
2593 if !flags.contains('g') {
2594 return Err(RuntimeError::type_error(
2595 "String.prototype.matchAll called with a non-global RegExp argument",
2596 ));
2597 }
2598 arg0.clone()
2599 } else {
2600 let pattern = arg0.to_js_string(ctx.gc);
2601 let proto = REGEXP_PROTO.with(|cell| cell.get());
2602 make_regexp_obj(ctx.gc, ctx.shapes, &pattern, "g", proto)
2603 .map_err(RuntimeError::syntax_error)?
2604 };
2605
2606 // Collect all match results.
2607 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, 0.0);
2608 let mut results = Vec::new();
2609 loop {
2610 let result = regexp_exec_internal(ctx.gc, ctx.shapes, ®exp_val, &s)?;
2611 if matches!(result, Value::Null) {
2612 break;
2613 }
2614 // Advance past zero-length matches.
2615 if let Value::Object(r) = &result {
2616 if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) {
2617 if let Some(prop) = data.get_property("0", ctx.shapes) {
2618 let match_str = prop.value.to_js_string(ctx.gc);
2619 if match_str.is_empty() {
2620 let li = regexp_get_last_index(ctx.gc, ctx.shapes, ®exp_val);
2621 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, li + 1.0);
2622 }
2623 }
2624 }
2625 }
2626 results.push(result);
2627 }
2628 Ok(make_value_array(ctx.gc, ctx.shapes, &results))
2629}
2630
2631fn string_proto_search(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2632 let s = this_string(ctx);
2633 if args.is_empty() {
2634 return Ok(Value::Number(0.0)); // /(?:)/ matches at 0.
2635 }
2636
2637 let arg0 = &args[0];
2638 let regexp_val = if is_regexp(ctx.gc, ctx.shapes, arg0) {
2639 arg0.clone()
2640 } else {
2641 let pattern = arg0.to_js_string(ctx.gc);
2642 let proto = REGEXP_PROTO.with(|cell| cell.get());
2643 make_regexp_obj(ctx.gc, ctx.shapes, &pattern, "", proto)
2644 .map_err(RuntimeError::syntax_error)?
2645 };
2646
2647 // search always starts from 0 and ignores global/lastIndex.
2648 // Save and restore lastIndex so exec_internal's global/sticky handling doesn't interfere.
2649 let saved_last_index = regexp_get_last_index(ctx.gc, ctx.shapes, ®exp_val);
2650 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, 0.0);
2651 let result = regexp_exec_internal(ctx.gc, ctx.shapes, ®exp_val, &s)?;
2652 regexp_set_last_index(ctx.gc, ctx.shapes, ®exp_val, saved_last_index);
2653 match result {
2654 Value::Null => Ok(Value::Number(-1.0)),
2655 Value::Object(r) => {
2656 let idx = match ctx.gc.get(r) {
2657 Some(HeapObject::Object(data)) => data
2658 .get_property("index", ctx.shapes)
2659 .map(|p| p.value.to_number())
2660 .unwrap_or(-1.0),
2661 _ => -1.0,
2662 };
2663 Ok(Value::Number(idx))
2664 }
2665 _ => Ok(Value::Number(-1.0)),
2666 }
2667}
2668
2669fn string_proto_to_lower_case(
2670 args: &[Value],
2671 ctx: &mut NativeContext,
2672) -> Result<Value, RuntimeError> {
2673 let _ = args;
2674 Ok(Value::String(this_string(ctx).to_lowercase()))
2675}
2676
2677fn string_proto_to_upper_case(
2678 args: &[Value],
2679 ctx: &mut NativeContext,
2680) -> Result<Value, RuntimeError> {
2681 let _ = args;
2682 Ok(Value::String(this_string(ctx).to_uppercase()))
2683}
2684
2685fn string_proto_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2686 let s = this_string(ctx);
2687 let chars = str_chars(&s);
2688 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
2689 let actual = if idx < 0 {
2690 chars.len() as i64 + idx
2691 } else {
2692 idx
2693 };
2694 if actual < 0 || actual as usize >= chars.len() {
2695 Ok(Value::Undefined)
2696 } else {
2697 Ok(Value::String(chars[actual as usize].to_string()))
2698 }
2699}
2700
2701fn string_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2702 let _ = args;
2703 Ok(Value::String(this_string(ctx)))
2704}
2705
2706fn string_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2707 let _ = args;
2708 Ok(Value::String(this_string(ctx)))
2709}
2710
2711// ── Number built-in ──────────────────────────────────────────
2712
2713fn init_number_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
2714 let methods: &[NativeMethod] = &[
2715 ("toString", number_proto_to_string),
2716 ("valueOf", number_proto_value_of),
2717 ("toFixed", number_proto_to_fixed),
2718 ("toPrecision", number_proto_to_precision),
2719 ("toExponential", number_proto_to_exponential),
2720 ];
2721 for &(name, callback) in methods {
2722 let f = make_native(gc, name, callback);
2723 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
2724 }
2725}
2726
2727fn init_number_constructor(gc: &mut Gc<HeapObject>, num_proto: GcRef) -> GcRef {
2728 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
2729 name: "Number".to_string(),
2730 kind: FunctionKind::Native(NativeFunc {
2731 callback: number_constructor,
2732 }),
2733 prototype_obj: Some(num_proto),
2734 properties: HashMap::new(),
2735 upvalues: Vec::new(),
2736 })));
2737 // Static methods.
2738 let is_nan = make_native(gc, "isNaN", number_is_nan);
2739 set_func_prop(gc, ctor, "isNaN", Value::Function(is_nan));
2740 let is_finite = make_native(gc, "isFinite", number_is_finite);
2741 set_func_prop(gc, ctor, "isFinite", Value::Function(is_finite));
2742 let is_integer = make_native(gc, "isInteger", number_is_integer);
2743 set_func_prop(gc, ctor, "isInteger", Value::Function(is_integer));
2744 let is_safe_integer = make_native(gc, "isSafeInteger", number_is_safe_integer);
2745 set_func_prop(gc, ctor, "isSafeInteger", Value::Function(is_safe_integer));
2746 let parse_int_fn = make_native(gc, "parseInt", crate::builtins::parse_int);
2747 set_func_prop(gc, ctor, "parseInt", Value::Function(parse_int_fn));
2748 let parse_float_fn = make_native(gc, "parseFloat", crate::builtins::parse_float);
2749 set_func_prop(gc, ctor, "parseFloat", Value::Function(parse_float_fn));
2750 // Constants.
2751 set_func_prop(gc, ctor, "EPSILON", Value::Number(f64::EPSILON));
2752 set_func_prop(
2753 gc,
2754 ctor,
2755 "MAX_SAFE_INTEGER",
2756 Value::Number(9007199254740991.0),
2757 );
2758 set_func_prop(
2759 gc,
2760 ctor,
2761 "MIN_SAFE_INTEGER",
2762 Value::Number(-9007199254740991.0),
2763 );
2764 set_func_prop(gc, ctor, "MAX_VALUE", Value::Number(f64::MAX));
2765 set_func_prop(gc, ctor, "MIN_VALUE", Value::Number(f64::MIN_POSITIVE));
2766 set_func_prop(gc, ctor, "NaN", Value::Number(f64::NAN));
2767 set_func_prop(gc, ctor, "POSITIVE_INFINITY", Value::Number(f64::INFINITY));
2768 set_func_prop(
2769 gc,
2770 ctor,
2771 "NEGATIVE_INFINITY",
2772 Value::Number(f64::NEG_INFINITY),
2773 );
2774 ctor
2775}
2776
2777fn number_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2778 let n = args.first().map(|v| v.to_number()).unwrap_or(0.0);
2779 Ok(Value::Number(n))
2780}
2781
2782fn number_is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2783 match args.first() {
2784 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_nan())),
2785 _ => Ok(Value::Boolean(false)),
2786 }
2787}
2788
2789fn number_is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2790 match args.first() {
2791 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite())),
2792 _ => Ok(Value::Boolean(false)),
2793 }
2794}
2795
2796fn number_is_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2797 match args.first() {
2798 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite() && n.trunc() == *n)),
2799 _ => Ok(Value::Boolean(false)),
2800 }
2801}
2802
2803fn number_is_safe_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2804 match args.first() {
2805 Some(Value::Number(n)) => {
2806 let safe = n.is_finite() && n.trunc() == *n && n.abs() <= 9007199254740991.0;
2807 Ok(Value::Boolean(safe))
2808 }
2809 _ => Ok(Value::Boolean(false)),
2810 }
2811}
2812
2813/// Helper: extract the number from `this` for Number.prototype methods.
2814fn this_number(ctx: &NativeContext) -> f64 {
2815 ctx.this.to_number()
2816}
2817
2818fn number_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2819 let n = this_number(ctx);
2820 let radix = args.first().map(|v| v.to_number() as u32).unwrap_or(10);
2821 if !(2..=36).contains(&radix) {
2822 return Err(RuntimeError::range_error(
2823 "toString() radix must be between 2 and 36",
2824 ));
2825 }
2826 if radix == 10 {
2827 return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc)));
2828 }
2829 if n.is_nan() {
2830 return Ok(Value::String("NaN".to_string()));
2831 }
2832 if n.is_infinite() {
2833 return Ok(Value::String(if n > 0.0 {
2834 "Infinity".to_string()
2835 } else {
2836 "-Infinity".to_string()
2837 }));
2838 }
2839 // Integer path for non-decimal radix.
2840 let neg = n < 0.0;
2841 let abs = n.abs() as u64;
2842 let mut digits = Vec::new();
2843 let mut val = abs;
2844 if val == 0 {
2845 digits.push('0');
2846 } else {
2847 while val > 0 {
2848 let d = (val % radix as u64) as u32;
2849 digits.push(char::from_digit(d, radix).unwrap_or('?'));
2850 val /= radix as u64;
2851 }
2852 }
2853 digits.reverse();
2854 let mut result = String::new();
2855 if neg {
2856 result.push('-');
2857 }
2858 result.extend(digits);
2859 Ok(Value::String(result))
2860}
2861
2862fn number_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2863 let _ = args;
2864 Ok(Value::Number(this_number(ctx)))
2865}
2866
2867fn number_proto_to_fixed(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2868 let n = this_number(ctx);
2869 let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
2870 if digits > 100 {
2871 return Err(RuntimeError::range_error(
2872 "toFixed() digits argument must be between 0 and 100",
2873 ));
2874 }
2875 Ok(Value::String(format!("{n:.digits$}")))
2876}
2877
2878fn number_proto_to_precision(
2879 args: &[Value],
2880 ctx: &mut NativeContext,
2881) -> Result<Value, RuntimeError> {
2882 let n = this_number(ctx);
2883 if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) {
2884 return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc)));
2885 }
2886 let prec = args[0].to_number() as usize;
2887 if !(1..=100).contains(&prec) {
2888 return Err(RuntimeError::range_error(
2889 "toPrecision() argument must be between 1 and 100",
2890 ));
2891 }
2892 Ok(Value::String(format!("{n:.prec$e}")))
2893}
2894
2895fn number_proto_to_exponential(
2896 args: &[Value],
2897 ctx: &mut NativeContext,
2898) -> Result<Value, RuntimeError> {
2899 let n = this_number(ctx);
2900 let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(6);
2901 if digits > 100 {
2902 return Err(RuntimeError::range_error(
2903 "toExponential() argument must be between 0 and 100",
2904 ));
2905 }
2906 Ok(Value::String(format!("{n:.digits$e}")))
2907}
2908
2909// ── Boolean built-in ─────────────────────────────────────────
2910
2911fn init_boolean_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
2912 let to_string = make_native(gc, "toString", boolean_proto_to_string);
2913 set_builtin_prop(gc, shapes, proto, "toString", Value::Function(to_string));
2914 let value_of = make_native(gc, "valueOf", boolean_proto_value_of);
2915 set_builtin_prop(gc, shapes, proto, "valueOf", Value::Function(value_of));
2916}
2917
2918fn init_boolean_constructor(gc: &mut Gc<HeapObject>, bool_proto: GcRef) -> GcRef {
2919 gc.alloc(HeapObject::Function(Box::new(FunctionData {
2920 name: "Boolean".to_string(),
2921 kind: FunctionKind::Native(NativeFunc {
2922 callback: boolean_constructor,
2923 }),
2924 prototype_obj: Some(bool_proto),
2925 properties: HashMap::new(),
2926 upvalues: Vec::new(),
2927 })))
2928}
2929
2930fn boolean_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2931 let b = args.first().map(|v| v.to_boolean()).unwrap_or(false);
2932 Ok(Value::Boolean(b))
2933}
2934
2935fn boolean_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2936 let _ = args;
2937 Ok(Value::String(
2938 if ctx.this.to_boolean() {
2939 "true"
2940 } else {
2941 "false"
2942 }
2943 .to_string(),
2944 ))
2945}
2946
2947fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2948 let _ = args;
2949 Ok(Value::Boolean(ctx.this.to_boolean()))
2950}
2951
2952// ── Symbol built-in ──────────────────────────────────────────
2953
2954/// Global symbol ID counter. Each Symbol() call increments this.
2955static SYMBOL_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2956
2957// Thread-local Date prototype GcRef for use in date_constructor.
2958// The Date constructor callback doesn't have VM access, so we store the
2959// prototype here during init and read it back during construction.
2960thread_local! {
2961 static DATE_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
2962}
2963
2964fn init_symbol_builtins(vm: &mut Vm) {
2965 // Create a function object so we can hang static props on it.
2966 let gc_ref = make_native(&mut vm.gc, "Symbol", symbol_factory);
2967
2968 // Well-known symbols as string constants.
2969 let well_known = [
2970 ("iterator", "@@iterator"),
2971 ("toPrimitive", "@@toPrimitive"),
2972 ("toStringTag", "@@toStringTag"),
2973 ("hasInstance", "@@hasInstance"),
2974 ];
2975 for (name, value) in well_known {
2976 set_func_prop(&mut vm.gc, gc_ref, name, Value::String(value.to_string()));
2977 }
2978
2979 // Symbol.for() and Symbol.keyFor().
2980 let sym_for = make_native(&mut vm.gc, "for", symbol_for);
2981 set_func_prop(&mut vm.gc, gc_ref, "for", Value::Function(sym_for));
2982 let sym_key_for = make_native(&mut vm.gc, "keyFor", symbol_key_for);
2983 set_func_prop(&mut vm.gc, gc_ref, "keyFor", Value::Function(sym_key_for));
2984
2985 vm.set_global("Symbol", Value::Function(gc_ref));
2986
2987 // Register global NaN and Infinity constants.
2988 vm.set_global("NaN", Value::Number(f64::NAN));
2989 vm.set_global("Infinity", Value::Number(f64::INFINITY));
2990 vm.set_global("undefined", Value::Undefined);
2991}
2992
2993fn symbol_factory(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2994 let desc = args
2995 .first()
2996 .map(|v| v.to_js_string(ctx.gc))
2997 .unwrap_or_default();
2998 let id = SYMBOL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2999 // Return a unique string representation. Real Symbol is a distinct type,
3000 // but this pragmatic approach works for property keys and identity checks.
3001 Ok(Value::String(format!("@@sym_{id}_{desc}")))
3002}
3003
3004fn symbol_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3005 let key = args
3006 .first()
3007 .map(|v| v.to_js_string(ctx.gc))
3008 .unwrap_or_default();
3009 // Deterministic: same key always produces same symbol string.
3010 Ok(Value::String(format!("@@global_{key}")))
3011}
3012
3013fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3014 let sym = args
3015 .first()
3016 .map(|v| v.to_js_string(ctx.gc))
3017 .unwrap_or_default();
3018 if let Some(key) = sym.strip_prefix("@@global_") {
3019 Ok(Value::String(key.to_string()))
3020 } else {
3021 Ok(Value::Undefined)
3022 }
3023}
3024
3025// ── Math object ──────────────────────────────────────────────
3026
3027/// Simple xorshift64 PRNG state (no crypto requirement).
3028static PRNG_STATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
3029
3030fn prng_next() -> f64 {
3031 let mut s = PRNG_STATE.load(std::sync::atomic::Ordering::Relaxed);
3032 if s == 0 {
3033 // Seed from system time.
3034 s = SystemTime::now()
3035 .duration_since(UNIX_EPOCH)
3036 .map(|d| d.as_nanos() as u64)
3037 .unwrap_or(123456789);
3038 }
3039 s ^= s << 13;
3040 s ^= s >> 7;
3041 s ^= s << 17;
3042 PRNG_STATE.store(s, std::sync::atomic::Ordering::Relaxed);
3043 // Map to [0, 1).
3044 (s >> 11) as f64 / ((1u64 << 53) as f64)
3045}
3046
3047fn init_math_object(vm: &mut Vm) {
3048 let mut data = ObjectData::new();
3049 if let Some(proto) = vm.object_prototype {
3050 data.prototype = Some(proto);
3051 }
3052 let math_ref = vm.gc.alloc(HeapObject::Object(data));
3053
3054 // Constants.
3055 let constants: &[(&str, f64)] = &[
3056 ("E", std::f64::consts::E),
3057 ("LN2", std::f64::consts::LN_2),
3058 ("LN10", std::f64::consts::LN_10),
3059 ("LOG2E", std::f64::consts::LOG2_E),
3060 ("LOG10E", std::f64::consts::LOG10_E),
3061 ("PI", std::f64::consts::PI),
3062 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
3063 ("SQRT2", std::f64::consts::SQRT_2),
3064 ];
3065 for &(name, val) in constants {
3066 set_builtin_prop(
3067 &mut vm.gc,
3068 &mut vm.shapes,
3069 math_ref,
3070 name,
3071 Value::Number(val),
3072 );
3073 }
3074
3075 // Methods.
3076 let methods: &[NativeMethod] = &[
3077 ("abs", math_abs),
3078 ("ceil", math_ceil),
3079 ("floor", math_floor),
3080 ("round", math_round),
3081 ("trunc", math_trunc),
3082 ("max", math_max),
3083 ("min", math_min),
3084 ("pow", math_pow),
3085 ("sqrt", math_sqrt),
3086 ("cbrt", math_cbrt),
3087 ("hypot", math_hypot),
3088 ("sin", math_sin),
3089 ("cos", math_cos),
3090 ("tan", math_tan),
3091 ("asin", math_asin),
3092 ("acos", math_acos),
3093 ("atan", math_atan),
3094 ("atan2", math_atan2),
3095 ("exp", math_exp),
3096 ("log", math_log),
3097 ("log2", math_log2),
3098 ("log10", math_log10),
3099 ("expm1", math_expm1),
3100 ("log1p", math_log1p),
3101 ("sign", math_sign),
3102 ("clz32", math_clz32),
3103 ("fround", math_fround),
3104 ("random", math_random),
3105 ("imul", math_imul),
3106 ];
3107 for &(name, cb) in methods {
3108 let f = make_native(&mut vm.gc, name, cb);
3109 set_builtin_prop(
3110 &mut vm.gc,
3111 &mut vm.shapes,
3112 math_ref,
3113 name,
3114 Value::Function(f),
3115 );
3116 }
3117
3118 vm.set_global("Math", Value::Object(math_ref));
3119}
3120
3121fn math_abs(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3122 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3123 Ok(Value::Number(n.abs()))
3124}
3125
3126fn math_ceil(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3127 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3128 Ok(Value::Number(n.ceil()))
3129}
3130
3131fn math_floor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3132 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3133 Ok(Value::Number(n.floor()))
3134}
3135
3136fn math_round(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3137 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3138 // JS Math.round rounds .5 up (toward +Infinity).
3139 Ok(Value::Number(n.round()))
3140}
3141
3142fn math_trunc(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3143 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3144 Ok(Value::Number(n.trunc()))
3145}
3146
3147fn math_max(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3148 if args.is_empty() {
3149 return Ok(Value::Number(f64::NEG_INFINITY));
3150 }
3151 let mut result = f64::NEG_INFINITY;
3152 for arg in args {
3153 let n = arg.to_number();
3154 if n.is_nan() {
3155 return Ok(Value::Number(f64::NAN));
3156 }
3157 if n > result {
3158 result = n;
3159 }
3160 }
3161 Ok(Value::Number(result))
3162}
3163
3164fn math_min(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3165 if args.is_empty() {
3166 return Ok(Value::Number(f64::INFINITY));
3167 }
3168 let mut result = f64::INFINITY;
3169 for arg in args {
3170 let n = arg.to_number();
3171 if n.is_nan() {
3172 return Ok(Value::Number(f64::NAN));
3173 }
3174 if n < result {
3175 result = n;
3176 }
3177 }
3178 Ok(Value::Number(result))
3179}
3180
3181fn math_pow(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3182 let base = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3183 let exp = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN);
3184 Ok(Value::Number(base.powf(exp)))
3185}
3186
3187fn math_sqrt(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3188 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3189 Ok(Value::Number(n.sqrt()))
3190}
3191
3192fn math_cbrt(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3193 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3194 Ok(Value::Number(n.cbrt()))
3195}
3196
3197fn math_hypot(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3198 if args.is_empty() {
3199 return Ok(Value::Number(0.0));
3200 }
3201 let mut sum = 0.0f64;
3202 for arg in args {
3203 let n = arg.to_number();
3204 if n.is_infinite() {
3205 return Ok(Value::Number(f64::INFINITY));
3206 }
3207 if n.is_nan() {
3208 return Ok(Value::Number(f64::NAN));
3209 }
3210 sum += n * n;
3211 }
3212 Ok(Value::Number(sum.sqrt()))
3213}
3214
3215fn math_sin(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3216 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3217 Ok(Value::Number(n.sin()))
3218}
3219
3220fn math_cos(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3221 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3222 Ok(Value::Number(n.cos()))
3223}
3224
3225fn math_tan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3226 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3227 Ok(Value::Number(n.tan()))
3228}
3229
3230fn math_asin(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3231 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3232 Ok(Value::Number(n.asin()))
3233}
3234
3235fn math_acos(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3236 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3237 Ok(Value::Number(n.acos()))
3238}
3239
3240fn math_atan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3241 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3242 Ok(Value::Number(n.atan()))
3243}
3244
3245fn math_atan2(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3246 let y = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3247 let x = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN);
3248 Ok(Value::Number(y.atan2(x)))
3249}
3250
3251fn math_exp(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3252 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3253 Ok(Value::Number(n.exp()))
3254}
3255
3256fn math_log(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3257 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3258 Ok(Value::Number(n.ln()))
3259}
3260
3261fn math_log2(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3262 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3263 Ok(Value::Number(n.log2()))
3264}
3265
3266fn math_log10(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3267 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3268 Ok(Value::Number(n.log10()))
3269}
3270
3271fn math_expm1(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3272 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3273 Ok(Value::Number(n.exp_m1()))
3274}
3275
3276fn math_log1p(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3277 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3278 Ok(Value::Number(n.ln_1p()))
3279}
3280
3281fn math_sign(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3282 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3283 if n.is_nan() {
3284 Ok(Value::Number(f64::NAN))
3285 } else if n == 0.0 {
3286 // Preserve -0 and +0.
3287 Ok(Value::Number(n))
3288 } else if n > 0.0 {
3289 Ok(Value::Number(1.0))
3290 } else {
3291 Ok(Value::Number(-1.0))
3292 }
3293}
3294
3295fn math_clz32(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3296 let n = args.first().map(|v| v.to_number()).unwrap_or(0.0);
3297 let i = n as u32;
3298 Ok(Value::Number(i.leading_zeros() as f64))
3299}
3300
3301fn math_fround(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3302 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3303 Ok(Value::Number((n as f32) as f64))
3304}
3305
3306fn math_random(_args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3307 Ok(Value::Number(prng_next()))
3308}
3309
3310fn math_imul(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3311 // ToUint32 then reinterpret as i32 per spec.
3312 let a = args.first().map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32;
3313 let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32;
3314 Ok(Value::Number(a.wrapping_mul(b) as f64))
3315}
3316
3317// ── Date built-in ────────────────────────────────────────────
3318
3319/// Milliseconds since Unix epoch from SystemTime.
3320fn now_ms() -> f64 {
3321 SystemTime::now()
3322 .duration_since(UNIX_EPOCH)
3323 .map(|d| d.as_millis() as f64)
3324 .unwrap_or(0.0)
3325}
3326
3327/// Calendar components from a timestamp (milliseconds since epoch).
3328/// Returns (year, month0, day, hours, minutes, seconds, ms, weekday) in UTC.
3329fn ms_to_utc_components(ms: f64) -> (i64, i64, i64, i64, i64, i64, i64, i64) {
3330 let total_ms = ms as i64;
3331 let ms_part = ((total_ms % 1000) + 1000) % 1000;
3332 let mut days = total_ms.div_euclid(86_400_000);
3333 // Weekday: Jan 1 1970 was Thursday (4).
3334 let weekday = ((days % 7) + 4 + 7) % 7;
3335
3336 let secs_in_day = (total_ms.rem_euclid(86_400_000)) / 1000;
3337 let hours = secs_in_day / 3600;
3338 let minutes = (secs_in_day % 3600) / 60;
3339 let seconds = secs_in_day % 60;
3340
3341 // Convert days since epoch to year/month/day using a civil calendar algorithm.
3342 // Shift epoch to March 1, 2000.
3343 days += 719_468;
3344 let era = if days >= 0 {
3345 days / 146_097
3346 } else {
3347 (days - 146_096) / 146_097
3348 };
3349 let doe = days - era * 146_097; // day of era [0, 146096]
3350 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
3351 let y = yoe + era * 400;
3352 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
3353 let mp = (5 * doy + 2) / 153;
3354 let d = doy - (153 * mp + 2) / 5 + 1;
3355 let m = if mp < 10 { mp + 3 } else { mp - 9 };
3356 let y = if m <= 2 { y + 1 } else { y };
3357
3358 (y, m - 1, d, hours, minutes, seconds, ms_part, weekday)
3359}
3360
3361/// Convert UTC components to milliseconds since epoch.
3362fn utc_components_to_ms(year: i64, month: i64, day: i64, h: i64, min: i64, s: i64, ms: i64) -> f64 {
3363 // Normalize month overflow.
3364 let y = year + month.div_euclid(12);
3365 let m = month.rem_euclid(12) + 1; // 1-based
3366
3367 // Civil calendar to days (inverse of above).
3368 let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
3369 let era = if y2 >= 0 { y2 / 400 } else { (y2 - 399) / 400 };
3370 let yoe = y2 - era * 400;
3371 let doy = (153 * m2 + 2) / 5 + day - 1;
3372 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
3373 let days = era * 146_097 + doe - 719_468;
3374
3375 (days * 86_400_000 + h * 3_600_000 + min * 60_000 + s * 1000 + ms) as f64
3376}
3377
3378/// Parse a subset of ISO 8601 date strings (YYYY-MM-DDTHH:MM:SS.sssZ).
3379fn parse_date_string(s: &str) -> Option<f64> {
3380 let s = s.trim();
3381 // Try YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD or YYYY
3382 let parts: Vec<&str> = s.splitn(2, 'T').collect();
3383 let date_part = parts[0];
3384 let time_part = parts.get(1).copied().unwrap_or("");
3385
3386 let date_fields: Vec<&str> = date_part.split('-').collect();
3387 if date_fields.is_empty() {
3388 return None;
3389 }
3390
3391 let year: i64 = date_fields[0].parse().ok()?;
3392 let month: i64 = date_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
3393 let day: i64 = date_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
3394
3395 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
3396 return None;
3397 }
3398
3399 let (h, min, sec, ms) = if time_part.is_empty() {
3400 (0, 0, 0, 0)
3401 } else {
3402 // Strip trailing Z.
3403 let tp = time_part.strip_suffix('Z').unwrap_or(time_part);
3404 // Split seconds from milliseconds.
3405 let (time_str, ms_val) = if let Some((t, m)) = tp.split_once('.') {
3406 let ms_str = &m[..m.len().min(3)];
3407 let mut ms_val: i64 = ms_str.parse().unwrap_or(0);
3408 // Pad to 3 digits if needed.
3409 for _ in ms_str.len()..3 {
3410 ms_val *= 10;
3411 }
3412 (t, ms_val)
3413 } else {
3414 (tp, 0i64)
3415 };
3416 let time_fields: Vec<&str> = time_str.split(':').collect();
3417 let h: i64 = time_fields
3418 .first()
3419 .and_then(|s| s.parse().ok())
3420 .unwrap_or(0);
3421 let min: i64 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
3422 let sec: i64 = time_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
3423 (h, min, sec, ms_val)
3424 };
3425
3426 Some(utc_components_to_ms(year, month - 1, day, h, min, sec, ms))
3427}
3428
3429static DAY_NAMES: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3430static MONTH_NAMES: [&str; 12] = [
3431 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
3432];
3433
3434fn init_date_builtins(vm: &mut Vm) {
3435 // Date.prototype.
3436 let mut date_proto_data = ObjectData::new();
3437 if let Some(proto) = vm.object_prototype {
3438 date_proto_data.prototype = Some(proto);
3439 }
3440 let date_proto = vm.gc.alloc(HeapObject::Object(date_proto_data));
3441 init_date_prototype(&mut vm.gc, &mut vm.shapes, date_proto);
3442
3443 // Store prototype for constructor access.
3444 vm.date_prototype = Some(date_proto);
3445 DATE_PROTO.with(|cell| cell.set(Some(date_proto)));
3446
3447 // Date constructor function.
3448 let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3449 name: "Date".to_string(),
3450 kind: FunctionKind::Native(NativeFunc {
3451 callback: date_constructor,
3452 }),
3453 prototype_obj: Some(date_proto),
3454 properties: HashMap::new(),
3455 upvalues: Vec::new(),
3456 })));
3457
3458 // Static methods.
3459 let now_fn = make_native(&mut vm.gc, "now", date_now);
3460 set_func_prop(&mut vm.gc, ctor, "now", Value::Function(now_fn));
3461
3462 let parse_fn = make_native(&mut vm.gc, "parse", date_parse);
3463 set_func_prop(&mut vm.gc, ctor, "parse", Value::Function(parse_fn));
3464
3465 let utc_fn = make_native(&mut vm.gc, "UTC", date_utc);
3466 set_func_prop(&mut vm.gc, ctor, "UTC", Value::Function(utc_fn));
3467
3468 vm.set_global("Date", Value::Function(ctor));
3469}
3470
3471/// Internal: create a Date object (plain object with __date_ms__ property).
3472fn make_date_obj(
3473 gc: &mut Gc<HeapObject>,
3474 shapes: &mut ShapeTable,
3475 ms: f64,
3476 proto: Option<GcRef>,
3477) -> Value {
3478 let mut data = ObjectData::new();
3479 if let Some(p) = proto {
3480 data.prototype = Some(p);
3481 }
3482 data.insert_property(
3483 "__date_ms__".to_string(),
3484 Property::builtin(Value::Number(ms)),
3485 shapes,
3486 );
3487 Value::Object(gc.alloc(HeapObject::Object(data)))
3488}
3489
3490/// Extract timestamp from a Date object.
3491fn date_get_ms(gc: &Gc<HeapObject>, shapes: &ShapeTable, this: &Value) -> f64 {
3492 match this {
3493 Value::Object(r) => match gc.get(*r) {
3494 Some(HeapObject::Object(data)) => data
3495 .get_property("__date_ms__", shapes)
3496 .map(|p| p.value.to_number())
3497 .unwrap_or(f64::NAN),
3498 _ => f64::NAN,
3499 },
3500 _ => f64::NAN,
3501 }
3502}
3503
3504/// Set timestamp on a Date object and return the new value.
3505fn date_set_ms(gc: &mut Gc<HeapObject>, shapes: &ShapeTable, this: &Value, ms: f64) -> f64 {
3506 if let Value::Object(r) = this {
3507 if let Some(HeapObject::Object(data)) = gc.get_mut(*r) {
3508 data.update_value("__date_ms__", Value::Number(ms), shapes);
3509 }
3510 }
3511 ms
3512}
3513
3514fn date_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3515 let proto = DATE_PROTO.with(|cell| cell.get());
3516 let ms = match args.len() {
3517 0 => now_ms(),
3518 1 => match &args[0] {
3519 Value::String(s) => parse_date_string(s).unwrap_or(f64::NAN),
3520 Value::Number(n) => *n,
3521 v => v.to_number(),
3522 },
3523 _ => {
3524 // new Date(year, month, day?, hours?, min?, sec?, ms?)
3525 let year = args[0].to_number() as i64;
3526 let month = args[1].to_number() as i64;
3527 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1);
3528 let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0);
3529 let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0);
3530 let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0);
3531 let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0);
3532 // Years 0-99 map to 1900-1999 per spec.
3533 let year = if (0..100).contains(&year) {
3534 year + 1900
3535 } else {
3536 year
3537 };
3538 utc_components_to_ms(year, month, day, h, min, sec, ms)
3539 }
3540 };
3541 Ok(make_date_obj(ctx.gc, ctx.shapes, ms, proto))
3542}
3543
3544fn date_now(_args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3545 Ok(Value::Number(now_ms()))
3546}
3547
3548fn date_parse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3549 let s = args
3550 .first()
3551 .map(|v| v.to_js_string(ctx.gc))
3552 .unwrap_or_default();
3553 Ok(Value::Number(parse_date_string(&s).unwrap_or(f64::NAN)))
3554}
3555
3556fn date_utc(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3557 let year = args.first().map(|v| v.to_number() as i64).unwrap_or(1970);
3558 let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0);
3559 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1);
3560 let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0);
3561 let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0);
3562 let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0);
3563 let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0);
3564 let year = if (0..100).contains(&year) {
3565 year + 1900
3566 } else {
3567 year
3568 };
3569 Ok(Value::Number(utc_components_to_ms(
3570 year, month, day, h, min, sec, ms,
3571 )))
3572}
3573
3574fn init_date_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
3575 let methods: &[NativeMethod] = &[
3576 ("getTime", date_get_time),
3577 ("valueOf", date_get_time), // valueOf === getTime
3578 ("getFullYear", date_get_full_year),
3579 ("getMonth", date_get_month),
3580 ("getDate", date_get_date),
3581 ("getDay", date_get_day),
3582 ("getHours", date_get_hours),
3583 ("getMinutes", date_get_minutes),
3584 ("getSeconds", date_get_seconds),
3585 ("getMilliseconds", date_get_milliseconds),
3586 ("getTimezoneOffset", date_get_timezone_offset),
3587 ("getUTCFullYear", date_get_full_year),
3588 ("getUTCMonth", date_get_month),
3589 ("getUTCDate", date_get_date),
3590 ("getUTCDay", date_get_day),
3591 ("getUTCHours", date_get_hours),
3592 ("getUTCMinutes", date_get_minutes),
3593 ("getUTCSeconds", date_get_seconds),
3594 ("getUTCMilliseconds", date_get_milliseconds),
3595 ("setTime", date_set_time),
3596 ("setFullYear", date_set_full_year),
3597 ("setMonth", date_set_month),
3598 ("setDate", date_set_date),
3599 ("setHours", date_set_hours),
3600 ("setMinutes", date_set_minutes),
3601 ("setSeconds", date_set_seconds),
3602 ("setMilliseconds", date_set_milliseconds),
3603 ("toString", date_to_string),
3604 ("toISOString", date_to_iso_string),
3605 ("toUTCString", date_to_utc_string),
3606 ("toLocaleDateString", date_to_locale_date_string),
3607 ("toJSON", date_to_json),
3608 ];
3609 for &(name, cb) in methods {
3610 let f = make_native(gc, name, cb);
3611 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
3612 }
3613}
3614
3615fn date_get_time(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3616 let _ = args;
3617 Ok(Value::Number(date_get_ms(ctx.gc, ctx.shapes, &ctx.this)))
3618}
3619
3620fn date_get_full_year(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3621 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3622 if ms.is_nan() {
3623 return Ok(Value::Number(f64::NAN));
3624 }
3625 let (y, ..) = ms_to_utc_components(ms);
3626 Ok(Value::Number(y as f64))
3627}
3628
3629fn date_get_month(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3630 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3631 if ms.is_nan() {
3632 return Ok(Value::Number(f64::NAN));
3633 }
3634 let (_, m, ..) = ms_to_utc_components(ms);
3635 Ok(Value::Number(m as f64))
3636}
3637
3638fn date_get_date(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3639 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3640 if ms.is_nan() {
3641 return Ok(Value::Number(f64::NAN));
3642 }
3643 let (_, _, d, ..) = ms_to_utc_components(ms);
3644 Ok(Value::Number(d as f64))
3645}
3646
3647fn date_get_day(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3648 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3649 if ms.is_nan() {
3650 return Ok(Value::Number(f64::NAN));
3651 }
3652 let (.., wd) = ms_to_utc_components(ms);
3653 Ok(Value::Number(wd as f64))
3654}
3655
3656fn date_get_hours(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3657 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3658 if ms.is_nan() {
3659 return Ok(Value::Number(f64::NAN));
3660 }
3661 let (_, _, _, h, ..) = ms_to_utc_components(ms);
3662 Ok(Value::Number(h as f64))
3663}
3664
3665fn date_get_minutes(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3666 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3667 if ms.is_nan() {
3668 return Ok(Value::Number(f64::NAN));
3669 }
3670 let (_, _, _, _, min, ..) = ms_to_utc_components(ms);
3671 Ok(Value::Number(min as f64))
3672}
3673
3674fn date_get_seconds(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3675 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3676 if ms.is_nan() {
3677 return Ok(Value::Number(f64::NAN));
3678 }
3679 let (_, _, _, _, _, s, ..) = ms_to_utc_components(ms);
3680 Ok(Value::Number(s as f64))
3681}
3682
3683fn date_get_milliseconds(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3684 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3685 if ms.is_nan() {
3686 return Ok(Value::Number(f64::NAN));
3687 }
3688 let (_, _, _, _, _, _, ms_part, _) = ms_to_utc_components(ms);
3689 Ok(Value::Number(ms_part as f64))
3690}
3691
3692fn date_get_timezone_offset(
3693 _args: &[Value],
3694 _ctx: &mut NativeContext,
3695) -> Result<Value, RuntimeError> {
3696 // We operate in UTC; return 0.
3697 Ok(Value::Number(0.0))
3698}
3699
3700fn date_set_time(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3701 let ms = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3702 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, ms);
3703 Ok(Value::Number(ms))
3704}
3705
3706fn date_set_full_year(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3707 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3708 let (_, m, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3709 let year = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3710 let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(m);
3711 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(d);
3712 let new_ms = utc_components_to_ms(year, month, day, h, min, s, ms);
3713 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3714 Ok(Value::Number(new_ms))
3715}
3716
3717fn date_set_month(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3718 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3719 let (y, _, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3720 let month = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3721 let day = args.get(1).map(|v| v.to_number() as i64).unwrap_or(d);
3722 let new_ms = utc_components_to_ms(y, month, day, h, min, s, ms);
3723 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3724 Ok(Value::Number(new_ms))
3725}
3726
3727fn date_set_date(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3728 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3729 let (y, m, _, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3730 let day = args.first().map(|v| v.to_number() as i64).unwrap_or(1);
3731 let new_ms = utc_components_to_ms(y, m, day, h, min, s, ms);
3732 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3733 Ok(Value::Number(new_ms))
3734}
3735
3736fn date_set_hours(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3737 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3738 let (y, m, d, _, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3739 let h = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3740 let min_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(min);
3741 let sec = args.get(2).map(|v| v.to_number() as i64).unwrap_or(s);
3742 let ms_v = args.get(3).map(|v| v.to_number() as i64).unwrap_or(ms);
3743 let new_ms = utc_components_to_ms(y, m, d, h, min_v, sec, ms_v);
3744 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3745 Ok(Value::Number(new_ms))
3746}
3747
3748fn date_set_minutes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3749 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3750 let (y, m, d, h, _, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3751 let min = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3752 let sec = args.get(1).map(|v| v.to_number() as i64).unwrap_or(s);
3753 let ms_v = args.get(2).map(|v| v.to_number() as i64).unwrap_or(ms);
3754 let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v);
3755 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3756 Ok(Value::Number(new_ms))
3757}
3758
3759fn date_set_seconds(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3760 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3761 let (y, m, d, h, min, _, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3762 let sec = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3763 let ms_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(ms);
3764 let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v);
3765 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3766 Ok(Value::Number(new_ms))
3767}
3768
3769fn date_set_milliseconds(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3770 let old = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3771 let (y, m, d, h, min, s, _, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3772 let ms_v = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3773 let new_ms = utc_components_to_ms(y, m, d, h, min, s, ms_v);
3774 date_set_ms(ctx.gc, ctx.shapes, &ctx.this, new_ms);
3775 Ok(Value::Number(new_ms))
3776}
3777
3778fn date_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3779 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3780 if ms.is_nan() {
3781 return Ok(Value::String("Invalid Date".to_string()));
3782 }
3783 let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms);
3784 Ok(Value::String(format!(
3785 "{} {} {:02} {} {:02}:{:02}:{:02} GMT",
3786 DAY_NAMES[wd as usize], MONTH_NAMES[m as usize], d, y, h, min, s
3787 )))
3788}
3789
3790fn date_to_iso_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3791 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3792 if ms.is_nan() {
3793 return Err(RuntimeError::range_error("Invalid time value"));
3794 }
3795 let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms);
3796 Ok(Value::String(format!(
3797 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
3798 y,
3799 m + 1,
3800 d,
3801 h,
3802 min,
3803 s,
3804 ms_part
3805 )))
3806}
3807
3808fn date_to_utc_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3809 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3810 if ms.is_nan() {
3811 return Ok(Value::String("Invalid Date".to_string()));
3812 }
3813 let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms);
3814 Ok(Value::String(format!(
3815 "{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
3816 DAY_NAMES[wd as usize], d, MONTH_NAMES[m as usize], y, h, min, s
3817 )))
3818}
3819
3820fn date_to_locale_date_string(
3821 _args: &[Value],
3822 ctx: &mut NativeContext,
3823) -> Result<Value, RuntimeError> {
3824 // Simplified: same as toISOString date portion.
3825 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3826 if ms.is_nan() {
3827 return Ok(Value::String("Invalid Date".to_string()));
3828 }
3829 let (y, m, d, ..) = ms_to_utc_components(ms);
3830 Ok(Value::String(format!("{:04}-{:02}-{:02}", y, m + 1, d)))
3831}
3832
3833fn date_to_json(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3834 let ms = date_get_ms(ctx.gc, ctx.shapes, &ctx.this);
3835 if ms.is_nan() {
3836 return Ok(Value::Null);
3837 }
3838 date_to_iso_string(_args, ctx)
3839}
3840
3841// ── RegExp built-in ──────────────────────────────────────────
3842
3843thread_local! {
3844 static REGEXP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3845}
3846
3847fn init_regexp_builtins(vm: &mut Vm) {
3848 // RegExp.prototype.
3849 let mut regexp_proto_data = ObjectData::new();
3850 if let Some(proto) = vm.object_prototype {
3851 regexp_proto_data.prototype = Some(proto);
3852 }
3853 let regexp_proto = vm.gc.alloc(HeapObject::Object(regexp_proto_data));
3854 init_regexp_prototype(&mut vm.gc, &mut vm.shapes, regexp_proto);
3855
3856 vm.regexp_prototype = Some(regexp_proto);
3857 REGEXP_PROTO.with(|cell| cell.set(Some(regexp_proto)));
3858
3859 // RegExp constructor function.
3860 let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3861 name: "RegExp".to_string(),
3862 kind: FunctionKind::Native(NativeFunc {
3863 callback: regexp_constructor,
3864 }),
3865 prototype_obj: Some(regexp_proto),
3866 properties: HashMap::new(),
3867 upvalues: Vec::new(),
3868 })));
3869
3870 vm.set_global("RegExp", Value::Function(ctor));
3871}
3872
3873fn init_regexp_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
3874 let methods: &[NativeMethod] = &[
3875 ("test", regexp_proto_test),
3876 ("exec", regexp_proto_exec),
3877 ("toString", regexp_proto_to_string),
3878 ];
3879 for &(name, callback) in methods {
3880 let f = make_native(gc, name, callback);
3881 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
3882 }
3883}
3884
3885/// Create a RegExp object storing compiled regex state in hidden properties.
3886pub fn make_regexp_obj(
3887 gc: &mut Gc<HeapObject>,
3888 shapes: &mut ShapeTable,
3889 pattern: &str,
3890 flags_str: &str,
3891 proto: Option<GcRef>,
3892) -> Result<Value, String> {
3893 use crate::regex::CompiledRegex;
3894
3895 let compiled = CompiledRegex::new(pattern, flags_str)?;
3896 let flags = compiled.flags;
3897
3898 let mut data = ObjectData::new();
3899 if let Some(p) = proto {
3900 data.prototype = Some(p);
3901 }
3902 // Store pattern and flags as properties.
3903 data.insert_property(
3904 "source".to_string(),
3905 Property::builtin(Value::String(pattern.to_string())),
3906 shapes,
3907 );
3908 let flags_string = flags.as_flag_string();
3909 data.insert_property(
3910 "flags".to_string(),
3911 Property::builtin(Value::String(flags_string)),
3912 shapes,
3913 );
3914 data.insert_property(
3915 "global".to_string(),
3916 Property::builtin(Value::Boolean(flags.global)),
3917 shapes,
3918 );
3919 data.insert_property(
3920 "ignoreCase".to_string(),
3921 Property::builtin(Value::Boolean(flags.ignore_case)),
3922 shapes,
3923 );
3924 data.insert_property(
3925 "multiline".to_string(),
3926 Property::builtin(Value::Boolean(flags.multiline)),
3927 shapes,
3928 );
3929 data.insert_property(
3930 "dotAll".to_string(),
3931 Property::builtin(Value::Boolean(flags.dot_all)),
3932 shapes,
3933 );
3934 data.insert_property(
3935 "unicode".to_string(),
3936 Property::builtin(Value::Boolean(flags.unicode)),
3937 shapes,
3938 );
3939 data.insert_property(
3940 "sticky".to_string(),
3941 Property::builtin(Value::Boolean(flags.sticky)),
3942 shapes,
3943 );
3944 data.insert_property(
3945 "lastIndex".to_string(),
3946 Property::data(Value::Number(0.0)),
3947 shapes,
3948 );
3949 // Hidden: serialized pattern for re-compilation.
3950 data.insert_property(
3951 "__regexp_pattern__".to_string(),
3952 Property::builtin(Value::String(pattern.to_string())),
3953 shapes,
3954 );
3955 data.insert_property(
3956 "__regexp_flags__".to_string(),
3957 Property::builtin(Value::String(flags.as_flag_string())),
3958 shapes,
3959 );
3960
3961 Ok(Value::Object(gc.alloc(HeapObject::Object(data))))
3962}
3963
3964/// Check if a Value is a RegExp object.
3965pub fn is_regexp(gc: &Gc<HeapObject>, shapes: &ShapeTable, val: &Value) -> bool {
3966 match val {
3967 Value::Object(r) => match gc.get(*r) {
3968 Some(HeapObject::Object(data)) => data.contains_key("__regexp_pattern__", shapes),
3969 _ => false,
3970 },
3971 _ => false,
3972 }
3973}
3974
3975/// Extract the pattern from a RegExp object.
3976fn regexp_get_pattern(gc: &Gc<HeapObject>, shapes: &ShapeTable, val: &Value) -> Option<String> {
3977 match val {
3978 Value::Object(r) => match gc.get(*r) {
3979 Some(HeapObject::Object(data)) => data
3980 .get_property("__regexp_pattern__", shapes)
3981 .and_then(|p| match &p.value {
3982 Value::String(s) => Some(s.clone()),
3983 _ => None,
3984 }),
3985 _ => None,
3986 },
3987 _ => None,
3988 }
3989}
3990
3991/// Extract the flags string from a RegExp object.
3992fn regexp_get_flags(gc: &Gc<HeapObject>, shapes: &ShapeTable, val: &Value) -> Option<String> {
3993 match val {
3994 Value::Object(r) => match gc.get(*r) {
3995 Some(HeapObject::Object(data)) => data
3996 .get_property("__regexp_flags__", shapes)
3997 .and_then(|p| match &p.value {
3998 Value::String(s) => Some(s.clone()),
3999 _ => None,
4000 }),
4001 _ => None,
4002 },
4003 _ => None,
4004 }
4005}
4006
4007/// Get lastIndex from a RegExp object.
4008fn regexp_get_last_index(gc: &Gc<HeapObject>, shapes: &ShapeTable, val: &Value) -> f64 {
4009 match val {
4010 Value::Object(r) => match gc.get(*r) {
4011 Some(HeapObject::Object(data)) => data
4012 .get_property("lastIndex", shapes)
4013 .map(|p| p.value.to_number())
4014 .unwrap_or(0.0),
4015 _ => 0.0,
4016 },
4017 _ => 0.0,
4018 }
4019}
4020
4021/// Set lastIndex on a RegExp object.
4022fn regexp_set_last_index(gc: &mut Gc<HeapObject>, shapes: &ShapeTable, val: &Value, idx: f64) {
4023 if let Value::Object(r) = val {
4024 if let Some(HeapObject::Object(data)) = gc.get_mut(*r) {
4025 data.update_value("lastIndex", Value::Number(idx), shapes);
4026 }
4027 }
4028}
4029
4030/// Execute the regex on a string and return a match result array or null.
4031fn regexp_exec_internal(
4032 gc: &mut Gc<HeapObject>,
4033 shapes: &mut ShapeTable,
4034 this: &Value,
4035 input: &str,
4036) -> Result<Value, RuntimeError> {
4037 use crate::regex::{exec, CompiledRegex};
4038
4039 let pattern = regexp_get_pattern(gc, shapes, this)
4040 .ok_or_else(|| RuntimeError::type_error("not a RegExp".to_string()))?;
4041 let flags_str = regexp_get_flags(gc, shapes, this).unwrap_or_default();
4042 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
4043 let is_global = compiled.flags.global;
4044 let is_sticky = compiled.flags.sticky;
4045
4046 let start_index = if is_global || is_sticky {
4047 let li = regexp_get_last_index(gc, shapes, this);
4048 if li < 0.0 {
4049 0
4050 } else {
4051 li as usize
4052 }
4053 } else {
4054 0
4055 };
4056
4057 let chars: Vec<char> = input.chars().collect();
4058 let result = exec(&compiled, input, start_index);
4059
4060 match result {
4061 Some(m) => {
4062 if is_global || is_sticky {
4063 regexp_set_last_index(gc, shapes, this, m.end as f64);
4064 }
4065
4066 // Build result array: [fullMatch, ...groups]
4067 let mut items: Vec<Value> = Vec::new();
4068
4069 // Full match (index 0).
4070 let full: String = chars[m.start..m.end].iter().collect();
4071 items.push(Value::String(full));
4072
4073 // Capture groups (index 1..n).
4074 for i in 1..m.captures.len() {
4075 match m.captures[i] {
4076 Some((s, e)) => {
4077 let cap: String = chars[s..e].iter().collect();
4078 items.push(Value::String(cap));
4079 }
4080 None => items.push(Value::Undefined),
4081 }
4082 }
4083
4084 // Build named groups object (if any) before creating the array.
4085 let groups_val = {
4086 let (node, _) =
4087 crate::regex::parse_pattern(&pattern).map_err(RuntimeError::syntax_error)?;
4088 let named = collect_named_groups(&node);
4089 if named.is_empty() {
4090 Value::Undefined
4091 } else {
4092 let mut groups_data = ObjectData::new();
4093 for (name, idx) in &named {
4094 let cap_idx = *idx as usize;
4095 let val = if cap_idx < m.captures.len() {
4096 match m.captures[cap_idx] {
4097 Some((s, e)) => {
4098 let cap: String = chars[s..e].iter().collect();
4099 Value::String(cap)
4100 }
4101 None => Value::Undefined,
4102 }
4103 } else {
4104 Value::Undefined
4105 };
4106 groups_data.insert_property(name.clone(), Property::data(val), shapes);
4107 }
4108 Value::Object(gc.alloc(HeapObject::Object(groups_data)))
4109 }
4110 };
4111
4112 let arr = make_value_array(gc, shapes, &items);
4113 // Set index, input, and groups properties on the result array.
4114 if let Value::Object(r) = arr {
4115 if let Some(HeapObject::Object(data)) = gc.get_mut(r) {
4116 data.insert_property(
4117 "index".to_string(),
4118 Property::data(Value::Number(m.start as f64)),
4119 shapes,
4120 );
4121 data.insert_property(
4122 "input".to_string(),
4123 Property::data(Value::String(input.to_string())),
4124 shapes,
4125 );
4126 data.insert_property("groups".to_string(), Property::data(groups_val), shapes);
4127 }
4128 Ok(Value::Object(r))
4129 } else {
4130 Ok(arr)
4131 }
4132 }
4133 None => {
4134 if is_global || is_sticky {
4135 regexp_set_last_index(gc, shapes, this, 0.0);
4136 }
4137 Ok(Value::Null)
4138 }
4139 }
4140}
4141
4142/// Collect named groups from a regex AST node.
4143fn collect_named_groups(node: &crate::regex::Node) -> Vec<(String, u32)> {
4144 use crate::regex::Node;
4145 let mut result = Vec::new();
4146 match node {
4147 Node::Group {
4148 index,
4149 name: Some(name),
4150 node: inner,
4151 } => {
4152 result.push((name.clone(), *index));
4153 result.extend(collect_named_groups(inner));
4154 }
4155 Node::Group { node: inner, .. }
4156 | Node::NonCapturingGroup(inner)
4157 | Node::Lookahead(inner)
4158 | Node::NegativeLookahead(inner) => {
4159 result.extend(collect_named_groups(inner));
4160 }
4161 Node::Quantifier { node: inner, .. } => {
4162 result.extend(collect_named_groups(inner));
4163 }
4164 Node::Sequence(nodes) | Node::Alternation(nodes) => {
4165 for n in nodes {
4166 result.extend(collect_named_groups(n));
4167 }
4168 }
4169 _ => {}
4170 }
4171 result
4172}
4173
4174fn regexp_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4175 let proto = REGEXP_PROTO.with(|cell| cell.get());
4176 let pattern = args
4177 .first()
4178 .map(|v| v.to_js_string(ctx.gc))
4179 .unwrap_or_default();
4180 let flags = args
4181 .get(1)
4182 .map(|v| {
4183 if matches!(v, Value::Undefined) {
4184 String::new()
4185 } else {
4186 v.to_js_string(ctx.gc)
4187 }
4188 })
4189 .unwrap_or_default();
4190
4191 make_regexp_obj(ctx.gc, ctx.shapes, &pattern, &flags, proto).map_err(RuntimeError::syntax_error)
4192}
4193
4194fn regexp_proto_test(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4195 let input = args
4196 .first()
4197 .map(|v| v.to_js_string(ctx.gc))
4198 .unwrap_or_default();
4199 let result = regexp_exec_internal(ctx.gc, ctx.shapes, &ctx.this, &input)?;
4200 Ok(Value::Boolean(!matches!(result, Value::Null)))
4201}
4202
4203fn regexp_proto_exec(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4204 let input = args
4205 .first()
4206 .map(|v| v.to_js_string(ctx.gc))
4207 .unwrap_or_default();
4208 regexp_exec_internal(ctx.gc, ctx.shapes, &ctx.this, &input)
4209}
4210
4211fn regexp_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4212 let pattern = regexp_get_pattern(ctx.gc, ctx.shapes, &ctx.this).unwrap_or_default();
4213 let flags = regexp_get_flags(ctx.gc, ctx.shapes, &ctx.this).unwrap_or_default();
4214 Ok(Value::String(format!("/{}/{}", pattern, flags)))
4215}
4216
4217// ── Map built-in ─────────────────────────────────────────────
4218
4219thread_local! {
4220 static MAP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
4221 static SET_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
4222 static WEAKMAP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
4223 static WEAKSET_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
4224}
4225
4226fn init_map_set_builtins(vm: &mut Vm) {
4227 // ── Map ──
4228 let mut map_proto_data = ObjectData::new();
4229 if let Some(proto) = vm.object_prototype {
4230 map_proto_data.prototype = Some(proto);
4231 }
4232 let map_proto = vm.gc.alloc(HeapObject::Object(map_proto_data));
4233 init_map_prototype(&mut vm.gc, &mut vm.shapes, map_proto);
4234 MAP_PROTO.with(|cell| cell.set(Some(map_proto)));
4235
4236 let map_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
4237 name: "Map".to_string(),
4238 kind: FunctionKind::Native(NativeFunc {
4239 callback: map_constructor,
4240 }),
4241 prototype_obj: Some(map_proto),
4242 properties: HashMap::new(),
4243 upvalues: Vec::new(),
4244 })));
4245 vm.set_global("Map", Value::Function(map_ctor));
4246
4247 // ── Set ──
4248 let mut set_proto_data = ObjectData::new();
4249 if let Some(proto) = vm.object_prototype {
4250 set_proto_data.prototype = Some(proto);
4251 }
4252 let set_proto = vm.gc.alloc(HeapObject::Object(set_proto_data));
4253 init_set_prototype(&mut vm.gc, &mut vm.shapes, set_proto);
4254 SET_PROTO.with(|cell| cell.set(Some(set_proto)));
4255
4256 let set_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
4257 name: "Set".to_string(),
4258 kind: FunctionKind::Native(NativeFunc {
4259 callback: set_constructor,
4260 }),
4261 prototype_obj: Some(set_proto),
4262 properties: HashMap::new(),
4263 upvalues: Vec::new(),
4264 })));
4265 vm.set_global("Set", Value::Function(set_ctor));
4266
4267 // ── WeakMap ──
4268 let mut wm_proto_data = ObjectData::new();
4269 if let Some(proto) = vm.object_prototype {
4270 wm_proto_data.prototype = Some(proto);
4271 }
4272 let wm_proto = vm.gc.alloc(HeapObject::Object(wm_proto_data));
4273 init_weakmap_prototype(&mut vm.gc, &mut vm.shapes, wm_proto);
4274 WEAKMAP_PROTO.with(|cell| cell.set(Some(wm_proto)));
4275
4276 let wm_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
4277 name: "WeakMap".to_string(),
4278 kind: FunctionKind::Native(NativeFunc {
4279 callback: weakmap_constructor,
4280 }),
4281 prototype_obj: Some(wm_proto),
4282 properties: HashMap::new(),
4283 upvalues: Vec::new(),
4284 })));
4285 vm.set_global("WeakMap", Value::Function(wm_ctor));
4286
4287 // ── WeakSet ──
4288 let mut ws_proto_data = ObjectData::new();
4289 if let Some(proto) = vm.object_prototype {
4290 ws_proto_data.prototype = Some(proto);
4291 }
4292 let ws_proto = vm.gc.alloc(HeapObject::Object(ws_proto_data));
4293 init_weakset_prototype(&mut vm.gc, &mut vm.shapes, ws_proto);
4294 WEAKSET_PROTO.with(|cell| cell.set(Some(ws_proto)));
4295
4296 let ws_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
4297 name: "WeakSet".to_string(),
4298 kind: FunctionKind::Native(NativeFunc {
4299 callback: weakset_constructor,
4300 }),
4301 prototype_obj: Some(ws_proto),
4302 properties: HashMap::new(),
4303 upvalues: Vec::new(),
4304 })));
4305 vm.set_global("WeakSet", Value::Function(ws_ctor));
4306}
4307
4308// ── Map / Set internal helpers ───────────────────────────────
4309
4310/// Internal storage key for Map/Set entries.
4311/// We store entries as a hidden object with indexed key/value pairs:
4312/// __entries__: GcRef to an object with "0_k", "0_v", "1_k", "1_v", ...
4313/// __entry_count__: total slots allocated (some may be deleted)
4314/// __live_count__: number of non-deleted entries
4315/// Deleted entries have their key set to a special "__deleted__" marker.
4316const ENTRIES_KEY: &str = "__entries__";
4317const ENTRY_COUNT_KEY: &str = "__entry_count__";
4318const LIVE_COUNT_KEY: &str = "__live_count__";
4319const DELETED_MARKER: &str = "__deleted__";
4320
4321/// Create a new empty Map/Set internal storage object.
4322fn make_collection_obj(
4323 gc: &mut Gc<HeapObject>,
4324 shapes: &mut ShapeTable,
4325 proto: Option<GcRef>,
4326) -> GcRef {
4327 let entries_obj = gc.alloc(HeapObject::Object(ObjectData::new()));
4328 let mut data = ObjectData::new();
4329 if let Some(p) = proto {
4330 data.prototype = Some(p);
4331 }
4332 data.insert_property(
4333 ENTRIES_KEY.to_string(),
4334 Property::builtin(Value::Object(entries_obj)),
4335 shapes,
4336 );
4337 data.insert_property(
4338 ENTRY_COUNT_KEY.to_string(),
4339 Property::builtin(Value::Number(0.0)),
4340 shapes,
4341 );
4342 data.insert_property(
4343 LIVE_COUNT_KEY.to_string(),
4344 Property::builtin(Value::Number(0.0)),
4345 shapes,
4346 );
4347 data.insert_property(
4348 "size".to_string(),
4349 Property::builtin(Value::Number(0.0)),
4350 shapes,
4351 );
4352 gc.alloc(HeapObject::Object(data))
4353}
4354
4355/// Get the entries object GcRef from a Map/Set object.
4356fn collection_entries(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: &Value) -> Option<GcRef> {
4357 let gc_ref = obj.gc_ref()?;
4358 let heap = gc.get(gc_ref)?;
4359 if let HeapObject::Object(data) = heap {
4360 if let Some(prop) = data.get_property(ENTRIES_KEY, shapes) {
4361 return prop.value.gc_ref();
4362 }
4363 }
4364 None
4365}
4366
4367/// Get the entry count from a Map/Set object.
4368fn collection_entry_count(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: &Value) -> usize {
4369 let gc_ref = match obj.gc_ref() {
4370 Some(r) => r,
4371 None => return 0,
4372 };
4373 match gc.get(gc_ref) {
4374 Some(HeapObject::Object(data)) => data
4375 .get_property(ENTRY_COUNT_KEY, shapes)
4376 .map(|p| p.value.to_number() as usize)
4377 .unwrap_or(0),
4378 _ => 0,
4379 }
4380}
4381
4382/// Get the live count from a Map/Set object.
4383fn collection_live_count(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: &Value) -> usize {
4384 let gc_ref = match obj.gc_ref() {
4385 Some(r) => r,
4386 None => return 0,
4387 };
4388 match gc.get(gc_ref) {
4389 Some(HeapObject::Object(data)) => data
4390 .get_property(LIVE_COUNT_KEY, shapes)
4391 .map(|p| p.value.to_number() as usize)
4392 .unwrap_or(0),
4393 _ => 0,
4394 }
4395}
4396
4397/// Set the entry count on a Map/Set object and update the `size` property.
4398fn set_collection_count(
4399 gc: &mut Gc<HeapObject>,
4400 shapes: &mut ShapeTable,
4401 obj: &Value,
4402 entry_count: usize,
4403 live: usize,
4404) {
4405 let gc_ref = match obj.gc_ref() {
4406 Some(r) => r,
4407 None => return,
4408 };
4409 if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) {
4410 data.insert_property(
4411 ENTRY_COUNT_KEY.to_string(),
4412 Property::builtin(Value::Number(entry_count as f64)),
4413 shapes,
4414 );
4415 data.insert_property(
4416 LIVE_COUNT_KEY.to_string(),
4417 Property::builtin(Value::Number(live as f64)),
4418 shapes,
4419 );
4420 data.insert_property(
4421 "size".to_string(),
4422 Property::builtin(Value::Number(live as f64)),
4423 shapes,
4424 );
4425 }
4426}
4427
4428/// Get the key at index `i` in the entries object (returns None if deleted or missing).
4429fn entry_key_at(
4430 gc: &Gc<HeapObject>,
4431 shapes: &ShapeTable,
4432 entries: GcRef,
4433 i: usize,
4434) -> Option<Value> {
4435 match gc.get(entries) {
4436 Some(HeapObject::Object(data)) => {
4437 let k = format!("{i}_k");
4438 let prop = data.get_property(&k, shapes)?;
4439 if let Value::String(s) = &prop.value {
4440 if s == DELETED_MARKER {
4441 return None;
4442 }
4443 }
4444 Some(prop.value.clone())
4445 }
4446 _ => None,
4447 }
4448}
4449
4450/// Get the value at index `i` in the entries object.
4451fn entry_value_at(gc: &Gc<HeapObject>, shapes: &ShapeTable, entries: GcRef, i: usize) -> Value {
4452 match gc.get(entries) {
4453 Some(HeapObject::Object(data)) => {
4454 let v = format!("{i}_v");
4455 data.get_property(&v, shapes)
4456 .map(|p| p.value)
4457 .unwrap_or(Value::Undefined)
4458 }
4459 _ => Value::Undefined,
4460 }
4461}
4462
4463/// Set an entry at index `i`.
4464fn set_entry_at(
4465 gc: &mut Gc<HeapObject>,
4466 shapes: &mut ShapeTable,
4467 entries: GcRef,
4468 i: usize,
4469 key: Value,
4470 value: Value,
4471) {
4472 if let Some(HeapObject::Object(data)) = gc.get_mut(entries) {
4473 data.insert_property(format!("{i}_k"), Property::builtin(key), shapes);
4474 data.insert_property(format!("{i}_v"), Property::builtin(value), shapes);
4475 }
4476}
4477
4478/// Mark entry at index `i` as deleted.
4479fn delete_entry_at(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, entries: GcRef, i: usize) {
4480 if let Some(HeapObject::Object(data)) = gc.get_mut(entries) {
4481 data.insert_property(
4482 format!("{i}_k"),
4483 Property::builtin(Value::String(DELETED_MARKER.to_string())),
4484 shapes,
4485 );
4486 data.remove_property(&format!("{i}_v"), shapes);
4487 }
4488}
4489
4490/// Find the index of a key in the entries, using SameValueZero.
4491fn find_key_index(
4492 gc: &Gc<HeapObject>,
4493 shapes: &ShapeTable,
4494 entries: GcRef,
4495 count: usize,
4496 key: &Value,
4497) -> Option<usize> {
4498 for i in 0..count {
4499 if let Some(existing) = entry_key_at(gc, shapes, entries, i) {
4500 if same_value_zero(&existing, key) {
4501 return Some(i);
4502 }
4503 }
4504 }
4505 None
4506}
4507
4508// ── Map constructor & prototype ──────────────────────────────
4509
4510fn map_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4511 let proto = MAP_PROTO.with(|cell| cell.get());
4512 let obj_ref = make_collection_obj(ctx.gc, ctx.shapes, proto);
4513 let obj = Value::Object(obj_ref);
4514
4515 // If an iterable argument is provided, add entries from it.
4516 // We support arrays of [key, value] pairs.
4517 if let Some(arg) = args.first() {
4518 if !matches!(arg, Value::Undefined | Value::Null) {
4519 if let Some(arr_ref) = arg.gc_ref() {
4520 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
4521 for i in 0..len {
4522 let pair =
4523 get_property(ctx.gc, ctx.shapes, &Value::Object(arr_ref), &i.to_string());
4524 if let Some(pair_ref) = pair.gc_ref() {
4525 let k = get_property(ctx.gc, ctx.shapes, &Value::Object(pair_ref), "0");
4526 let v = get_property(ctx.gc, ctx.shapes, &Value::Object(pair_ref), "1");
4527 map_set_internal(ctx.gc, ctx.shapes, &obj, k, v);
4528 }
4529 }
4530 }
4531 }
4532 }
4533
4534 Ok(obj)
4535}
4536
4537fn map_set_internal(
4538 gc: &mut Gc<HeapObject>,
4539 shapes: &mut ShapeTable,
4540 map: &Value,
4541 key: Value,
4542 value: Value,
4543) {
4544 let entries = match collection_entries(gc, shapes, map) {
4545 Some(e) => e,
4546 None => return,
4547 };
4548 let count = collection_entry_count(gc, shapes, map);
4549 let live = collection_live_count(gc, shapes, map);
4550
4551 // Check if key already exists.
4552 if let Some(idx) = find_key_index(gc, shapes, entries, count, &key) {
4553 set_entry_at(gc, shapes, entries, idx, key, value);
4554 return;
4555 }
4556
4557 // Add new entry.
4558 set_entry_at(gc, shapes, entries, count, key, value);
4559 set_collection_count(gc, shapes, map, count + 1, live + 1);
4560}
4561
4562fn init_map_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
4563 let methods: &[NativeMethod] = &[
4564 ("set", map_proto_set),
4565 ("get", map_proto_get),
4566 ("has", map_proto_has),
4567 ("delete", map_proto_delete),
4568 ("clear", map_proto_clear),
4569 ("forEach", map_proto_for_each),
4570 ("keys", map_proto_keys),
4571 ("values", map_proto_values),
4572 ("entries", map_proto_entries),
4573 ];
4574 for &(name, callback) in methods {
4575 let f = make_native(gc, name, callback);
4576 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
4577 }
4578 // Map.prototype[@@iterator] — returns an iterator of [key, value] pairs.
4579 let iter_fn = make_native(gc, "[Symbol.iterator]", map_symbol_iterator);
4580 set_builtin_prop(gc, shapes, proto, "@@iterator", Value::Function(iter_fn));
4581}
4582
4583fn map_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4584 let key = args.first().cloned().unwrap_or(Value::Undefined);
4585 let value = args.get(1).cloned().unwrap_or(Value::Undefined);
4586 // Normalize -0 to +0 for key.
4587 let key = normalize_zero(key);
4588 map_set_internal(ctx.gc, ctx.shapes, &ctx.this, key, value);
4589 Ok(ctx.this.clone())
4590}
4591
4592fn map_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4593 let key = args.first().cloned().unwrap_or(Value::Undefined);
4594 let key = normalize_zero(key);
4595 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4596 Some(e) => e,
4597 None => return Ok(Value::Undefined),
4598 };
4599 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4600 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &key) {
4601 return Ok(entry_value_at(ctx.gc, ctx.shapes, entries, idx));
4602 }
4603 Ok(Value::Undefined)
4604}
4605
4606fn map_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4607 let key = args.first().cloned().unwrap_or(Value::Undefined);
4608 let key = normalize_zero(key);
4609 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4610 Some(e) => e,
4611 None => return Ok(Value::Boolean(false)),
4612 };
4613 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4614 Ok(Value::Boolean(
4615 find_key_index(ctx.gc, ctx.shapes, entries, count, &key).is_some(),
4616 ))
4617}
4618
4619fn map_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4620 let key = args.first().cloned().unwrap_or(Value::Undefined);
4621 let key = normalize_zero(key);
4622 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4623 Some(e) => e,
4624 None => return Ok(Value::Boolean(false)),
4625 };
4626 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4627 let live = collection_live_count(ctx.gc, ctx.shapes, &ctx.this);
4628 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &key) {
4629 delete_entry_at(ctx.gc, ctx.shapes, entries, idx);
4630 set_collection_count(ctx.gc, ctx.shapes, &ctx.this, count, live.saturating_sub(1));
4631 return Ok(Value::Boolean(true));
4632 }
4633 Ok(Value::Boolean(false))
4634}
4635
4636fn map_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4637 clear_collection(ctx.gc, ctx.shapes, &ctx.this);
4638 Ok(Value::Undefined)
4639}
4640
4641/// Clear all entries from a Map/Set collection.
4642fn clear_collection(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, obj: &Value) {
4643 let new_entries = gc.alloc(HeapObject::Object(ObjectData::new()));
4644 if let Some(gc_ref) = obj.gc_ref() {
4645 if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) {
4646 data.insert_property(
4647 ENTRIES_KEY.to_string(),
4648 Property::builtin(Value::Object(new_entries)),
4649 shapes,
4650 );
4651 data.insert_property(
4652 ENTRY_COUNT_KEY.to_string(),
4653 Property::builtin(Value::Number(0.0)),
4654 shapes,
4655 );
4656 data.insert_property(
4657 LIVE_COUNT_KEY.to_string(),
4658 Property::builtin(Value::Number(0.0)),
4659 shapes,
4660 );
4661 data.insert_property(
4662 "size".to_string(),
4663 Property::builtin(Value::Number(0.0)),
4664 shapes,
4665 );
4666 }
4667 }
4668}
4669
4670fn map_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4671 let callback = match args.first() {
4672 Some(Value::Function(r)) => *r,
4673 _ => {
4674 return Err(RuntimeError::type_error(
4675 "Map.prototype.forEach requires a function argument",
4676 ))
4677 }
4678 };
4679 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4680 Some(e) => e,
4681 None => return Ok(Value::Undefined),
4682 };
4683 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4684 // Collect entries first to avoid borrow issues.
4685 let mut pairs = Vec::new();
4686 for i in 0..count {
4687 if let Some(k) = entry_key_at(ctx.gc, ctx.shapes, entries, i) {
4688 let v = entry_value_at(ctx.gc, ctx.shapes, entries, i);
4689 pairs.push((k, v));
4690 }
4691 }
4692 let this_val = ctx.this.clone();
4693 for (k, v) in pairs {
4694 call_native_callback(
4695 ctx.gc,
4696 ctx.shapes,
4697 callback,
4698 &[v, k, this_val.clone()],
4699 ctx.console_output,
4700 ctx.dom_bridge,
4701 )?;
4702 }
4703 Ok(Value::Undefined)
4704}
4705
4706fn map_proto_keys(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4707 map_proto_iter(args, ctx, IterKind::Keys)
4708}
4709
4710fn map_proto_values(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4711 map_proto_iter(args, ctx, IterKind::Values)
4712}
4713
4714fn map_proto_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4715 map_proto_iter(args, ctx, IterKind::Entries)
4716}
4717
4718enum IterKind {
4719 Keys,
4720 Values,
4721 Entries,
4722}
4723
4724fn map_proto_iter(
4725 _args: &[Value],
4726 ctx: &mut NativeContext,
4727 kind: IterKind,
4728) -> Result<Value, RuntimeError> {
4729 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4730 Some(e) => e,
4731 None => return Ok(make_value_array(ctx.gc, ctx.shapes, &[])),
4732 };
4733 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4734 let mut items = Vec::new();
4735 for i in 0..count {
4736 if let Some(k) = entry_key_at(ctx.gc, ctx.shapes, entries, i) {
4737 match kind {
4738 IterKind::Keys => items.push(k),
4739 IterKind::Values => {
4740 let v = entry_value_at(ctx.gc, ctx.shapes, entries, i);
4741 items.push(v);
4742 }
4743 IterKind::Entries => {
4744 let v = entry_value_at(ctx.gc, ctx.shapes, entries, i);
4745 let pair = make_value_array(ctx.gc, ctx.shapes, &[k, v]);
4746 items.push(pair);
4747 }
4748 }
4749 }
4750 }
4751 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
4752}
4753
4754/// Map[@@iterator]() — wraps entries array into an iterator.
4755fn map_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4756 let arr_val = map_proto_iter(_args, ctx, IterKind::Entries)?;
4757 let arr_ref = match arr_val.gc_ref() {
4758 Some(r) => r,
4759 None => return Ok(Value::Undefined),
4760 };
4761 Ok(make_simple_iterator(
4762 ctx.gc,
4763 ctx.shapes,
4764 arr_ref,
4765 array_values_next,
4766 ))
4767}
4768
4769/// Set[@@iterator]() — wraps values array into an iterator.
4770fn set_symbol_iterator(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4771 let arr_val = set_proto_values(_args, ctx)?;
4772 let arr_ref = match arr_val.gc_ref() {
4773 Some(r) => r,
4774 None => return Ok(Value::Undefined),
4775 };
4776 Ok(make_simple_iterator(
4777 ctx.gc,
4778 ctx.shapes,
4779 arr_ref,
4780 array_values_next,
4781 ))
4782}
4783
4784/// Normalize -0 to +0 for Map/Set key equality.
4785fn normalize_zero(val: Value) -> Value {
4786 if let Value::Number(n) = &val {
4787 if *n == 0.0 {
4788 return Value::Number(0.0);
4789 }
4790 }
4791 val
4792}
4793
4794/// Helper to get a property from an object value by key (used for reading iterable pairs).
4795fn get_property(gc: &Gc<HeapObject>, shapes: &ShapeTable, obj: &Value, key: &str) -> Value {
4796 let gc_ref = match obj.gc_ref() {
4797 Some(r) => r,
4798 None => return Value::Undefined,
4799 };
4800 match gc.get(gc_ref) {
4801 Some(HeapObject::Object(data)) => data
4802 .get_property(key, shapes)
4803 .map(|p| p.value)
4804 .unwrap_or(Value::Undefined),
4805 _ => Value::Undefined,
4806 }
4807}
4808
4809/// Call a native callback function (for forEach).
4810fn call_native_callback(
4811 gc: &mut Gc<HeapObject>,
4812 shapes: &mut ShapeTable,
4813 func_ref: GcRef,
4814 args: &[Value],
4815 console_output: &dyn ConsoleOutput,
4816 dom_bridge: Option<&DomBridge>,
4817) -> Result<Value, RuntimeError> {
4818 match gc.get(func_ref) {
4819 Some(HeapObject::Function(fdata)) => match &fdata.kind {
4820 FunctionKind::Native(native) => {
4821 let cb = native.callback;
4822 let mut ctx = NativeContext {
4823 gc,
4824 shapes,
4825 this: Value::Undefined,
4826 console_output,
4827 dom_bridge,
4828 };
4829 cb(args, &mut ctx)
4830 }
4831 FunctionKind::Bytecode(_) => Err(RuntimeError::type_error(
4832 "bytecode callbacks in forEach not yet supported",
4833 )),
4834 },
4835 _ => Err(RuntimeError::type_error("not a function")),
4836 }
4837}
4838
4839// ── Set constructor & prototype ──────────────────────────────
4840
4841fn set_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4842 let proto = SET_PROTO.with(|cell| cell.get());
4843 let obj_ref = make_collection_obj(ctx.gc, ctx.shapes, proto);
4844 let obj = Value::Object(obj_ref);
4845
4846 // If an iterable argument is provided, add values from it.
4847 if let Some(arg) = args.first() {
4848 if !matches!(arg, Value::Undefined | Value::Null) {
4849 if let Some(arr_ref) = arg.gc_ref() {
4850 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
4851 for i in 0..len {
4852 let v =
4853 get_property(ctx.gc, ctx.shapes, &Value::Object(arr_ref), &i.to_string());
4854 set_add_internal(ctx.gc, ctx.shapes, &obj, v);
4855 }
4856 }
4857 }
4858 }
4859
4860 Ok(obj)
4861}
4862
4863fn set_add_internal(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, set: &Value, value: Value) {
4864 let entries = match collection_entries(gc, shapes, set) {
4865 Some(e) => e,
4866 None => return,
4867 };
4868 let count = collection_entry_count(gc, shapes, set);
4869 let live = collection_live_count(gc, shapes, set);
4870
4871 // Check if value already exists.
4872 if find_key_index(gc, shapes, entries, count, &value).is_some() {
4873 return;
4874 }
4875
4876 // Add new entry (value stored as key, value slot unused).
4877 set_entry_at(gc, shapes, entries, count, value, Value::Undefined);
4878 set_collection_count(gc, shapes, set, count + 1, live + 1);
4879}
4880
4881fn init_set_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
4882 let methods: &[NativeMethod] = &[
4883 ("add", set_proto_add),
4884 ("has", set_proto_has),
4885 ("delete", set_proto_delete),
4886 ("clear", set_proto_clear),
4887 ("forEach", set_proto_for_each),
4888 ("keys", set_proto_keys),
4889 ("values", set_proto_values),
4890 ("entries", set_proto_entries),
4891 ];
4892 for &(name, callback) in methods {
4893 let f = make_native(gc, name, callback);
4894 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
4895 }
4896 // Set.prototype[@@iterator] — returns an iterator of values.
4897 let iter_fn = make_native(gc, "[Symbol.iterator]", set_symbol_iterator);
4898 set_builtin_prop(gc, shapes, proto, "@@iterator", Value::Function(iter_fn));
4899}
4900
4901fn set_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4902 let value = args.first().cloned().unwrap_or(Value::Undefined);
4903 let value = normalize_zero(value);
4904 set_add_internal(ctx.gc, ctx.shapes, &ctx.this, value);
4905 Ok(ctx.this.clone())
4906}
4907
4908fn set_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4909 let value = args.first().cloned().unwrap_or(Value::Undefined);
4910 let value = normalize_zero(value);
4911 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4912 Some(e) => e,
4913 None => return Ok(Value::Boolean(false)),
4914 };
4915 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4916 Ok(Value::Boolean(
4917 find_key_index(ctx.gc, ctx.shapes, entries, count, &value).is_some(),
4918 ))
4919}
4920
4921fn set_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4922 let value = args.first().cloned().unwrap_or(Value::Undefined);
4923 let value = normalize_zero(value);
4924 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4925 Some(e) => e,
4926 None => return Ok(Value::Boolean(false)),
4927 };
4928 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4929 let live = collection_live_count(ctx.gc, ctx.shapes, &ctx.this);
4930 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &value) {
4931 delete_entry_at(ctx.gc, ctx.shapes, entries, idx);
4932 set_collection_count(ctx.gc, ctx.shapes, &ctx.this, count, live.saturating_sub(1));
4933 return Ok(Value::Boolean(true));
4934 }
4935 Ok(Value::Boolean(false))
4936}
4937
4938fn set_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4939 clear_collection(ctx.gc, ctx.shapes, &ctx.this);
4940 Ok(Value::Undefined)
4941}
4942
4943fn set_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4944 let callback = match args.first() {
4945 Some(Value::Function(r)) => *r,
4946 _ => {
4947 return Err(RuntimeError::type_error(
4948 "Set.prototype.forEach requires a function argument",
4949 ))
4950 }
4951 };
4952 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4953 Some(e) => e,
4954 None => return Ok(Value::Undefined),
4955 };
4956 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4957 let mut values = Vec::new();
4958 for i in 0..count {
4959 if let Some(k) = entry_key_at(ctx.gc, ctx.shapes, entries, i) {
4960 values.push(k);
4961 }
4962 }
4963 let this_val = ctx.this.clone();
4964 for v in values {
4965 call_native_callback(
4966 ctx.gc,
4967 ctx.shapes,
4968 callback,
4969 &[v.clone(), v, this_val.clone()],
4970 ctx.console_output,
4971 ctx.dom_bridge,
4972 )?;
4973 }
4974 Ok(Value::Undefined)
4975}
4976
4977fn set_proto_keys(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4978 set_proto_values(_args, ctx)
4979}
4980
4981fn set_proto_values(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4982 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4983 Some(e) => e,
4984 None => return Ok(make_value_array(ctx.gc, ctx.shapes, &[])),
4985 };
4986 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
4987 let mut items = Vec::new();
4988 for i in 0..count {
4989 if let Some(k) = entry_key_at(ctx.gc, ctx.shapes, entries, i) {
4990 items.push(k);
4991 }
4992 }
4993 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
4994}
4995
4996fn set_proto_entries(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4997 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
4998 Some(e) => e,
4999 None => return Ok(make_value_array(ctx.gc, ctx.shapes, &[])),
5000 };
5001 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5002 let mut items = Vec::new();
5003 for i in 0..count {
5004 if let Some(k) = entry_key_at(ctx.gc, ctx.shapes, entries, i) {
5005 let pair = make_value_array(ctx.gc, ctx.shapes, &[k.clone(), k]);
5006 items.push(pair);
5007 }
5008 }
5009 Ok(make_value_array(ctx.gc, ctx.shapes, &items))
5010}
5011
5012// ── WeakMap constructor & prototype ──────────────────────────
5013
5014fn weakmap_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5015 let proto = WEAKMAP_PROTO.with(|cell| cell.get());
5016 let obj_ref = make_collection_obj(ctx.gc, ctx.shapes, proto);
5017 Ok(Value::Object(obj_ref))
5018}
5019
5020fn is_object_value(val: &Value) -> bool {
5021 matches!(val, Value::Object(_) | Value::Function(_))
5022}
5023
5024fn init_weakmap_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
5025 let methods: &[NativeMethod] = &[
5026 ("set", weakmap_proto_set),
5027 ("get", weakmap_proto_get),
5028 ("has", weakmap_proto_has),
5029 ("delete", weakmap_proto_delete),
5030 ];
5031 for &(name, callback) in methods {
5032 let f = make_native(gc, name, callback);
5033 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
5034 }
5035}
5036
5037fn weakmap_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5038 let key = args.first().cloned().unwrap_or(Value::Undefined);
5039 if !is_object_value(&key) {
5040 return Err(RuntimeError::type_error("WeakMap key must be an object"));
5041 }
5042 let value = args.get(1).cloned().unwrap_or(Value::Undefined);
5043 map_set_internal(ctx.gc, ctx.shapes, &ctx.this, key, value);
5044 Ok(ctx.this.clone())
5045}
5046
5047fn weakmap_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5048 let key = args.first().cloned().unwrap_or(Value::Undefined);
5049 if !is_object_value(&key) {
5050 return Ok(Value::Undefined);
5051 }
5052 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
5053 Some(e) => e,
5054 None => return Ok(Value::Undefined),
5055 };
5056 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5057 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &key) {
5058 return Ok(entry_value_at(ctx.gc, ctx.shapes, entries, idx));
5059 }
5060 Ok(Value::Undefined)
5061}
5062
5063fn weakmap_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5064 let key = args.first().cloned().unwrap_or(Value::Undefined);
5065 if !is_object_value(&key) {
5066 return Ok(Value::Boolean(false));
5067 }
5068 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
5069 Some(e) => e,
5070 None => return Ok(Value::Boolean(false)),
5071 };
5072 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5073 Ok(Value::Boolean(
5074 find_key_index(ctx.gc, ctx.shapes, entries, count, &key).is_some(),
5075 ))
5076}
5077
5078fn weakmap_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5079 let key = args.first().cloned().unwrap_or(Value::Undefined);
5080 if !is_object_value(&key) {
5081 return Ok(Value::Boolean(false));
5082 }
5083 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
5084 Some(e) => e,
5085 None => return Ok(Value::Boolean(false)),
5086 };
5087 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5088 let live = collection_live_count(ctx.gc, ctx.shapes, &ctx.this);
5089 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &key) {
5090 delete_entry_at(ctx.gc, ctx.shapes, entries, idx);
5091 set_collection_count(ctx.gc, ctx.shapes, &ctx.this, count, live.saturating_sub(1));
5092 return Ok(Value::Boolean(true));
5093 }
5094 Ok(Value::Boolean(false))
5095}
5096
5097// ── WeakSet constructor & prototype ──────────────────────────
5098
5099fn weakset_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5100 let proto = WEAKSET_PROTO.with(|cell| cell.get());
5101 let obj_ref = make_collection_obj(ctx.gc, ctx.shapes, proto);
5102 Ok(Value::Object(obj_ref))
5103}
5104
5105fn init_weakset_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
5106 let methods: &[NativeMethod] = &[
5107 ("add", weakset_proto_add),
5108 ("has", weakset_proto_has),
5109 ("delete", weakset_proto_delete),
5110 ];
5111 for &(name, callback) in methods {
5112 let f = make_native(gc, name, callback);
5113 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
5114 }
5115}
5116
5117fn weakset_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5118 let value = args.first().cloned().unwrap_or(Value::Undefined);
5119 if !is_object_value(&value) {
5120 return Err(RuntimeError::type_error("WeakSet value must be an object"));
5121 }
5122 set_add_internal(ctx.gc, ctx.shapes, &ctx.this, value);
5123 Ok(ctx.this.clone())
5124}
5125
5126fn weakset_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5127 let value = args.first().cloned().unwrap_or(Value::Undefined);
5128 if !is_object_value(&value) {
5129 return Ok(Value::Boolean(false));
5130 }
5131 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
5132 Some(e) => e,
5133 None => return Ok(Value::Boolean(false)),
5134 };
5135 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5136 Ok(Value::Boolean(
5137 find_key_index(ctx.gc, ctx.shapes, entries, count, &value).is_some(),
5138 ))
5139}
5140
5141fn weakset_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5142 let value = args.first().cloned().unwrap_or(Value::Undefined);
5143 if !is_object_value(&value) {
5144 return Ok(Value::Boolean(false));
5145 }
5146 let entries = match collection_entries(ctx.gc, ctx.shapes, &ctx.this) {
5147 Some(e) => e,
5148 None => return Ok(Value::Boolean(false)),
5149 };
5150 let count = collection_entry_count(ctx.gc, ctx.shapes, &ctx.this);
5151 let live = collection_live_count(ctx.gc, ctx.shapes, &ctx.this);
5152 if let Some(idx) = find_key_index(ctx.gc, ctx.shapes, entries, count, &value) {
5153 delete_entry_at(ctx.gc, ctx.shapes, entries, idx);
5154 set_collection_count(ctx.gc, ctx.shapes, &ctx.this, count, live.saturating_sub(1));
5155 return Ok(Value::Boolean(true));
5156 }
5157 Ok(Value::Boolean(false))
5158}
5159
5160// ── Promise built-in ─────────────────────────────────────────
5161
5162/// Promise states.
5163const PROMISE_PENDING: f64 = 0.0;
5164pub const PROMISE_FULFILLED: f64 = 1.0;
5165pub const PROMISE_REJECTED: f64 = 2.0;
5166
5167/// Hidden property keys for Promise objects.
5168const PROMISE_STATE_KEY: &str = "__promise_state__";
5169pub const PROMISE_RESULT_KEY: &str = "__promise_result__";
5170const PROMISE_REACTIONS_KEY: &str = "__promise_reactions__";
5171const PROMISE_REACTION_COUNT_KEY: &str = "__promise_reaction_count__";
5172
5173thread_local! {
5174 static PROMISE_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
5175 static MICROTASK_QUEUE: RefCell<Vec<Microtask>> = const { RefCell::new(Vec::new()) };
5176}
5177
5178/// A microtask queued by a promise settlement.
5179pub struct Microtask {
5180 /// The handler to call (onFulfilled or onRejected). None means identity/thrower.
5181 pub handler: Option<GcRef>,
5182 /// The value to pass to the handler.
5183 pub value: Value,
5184 /// The chained promise to resolve/reject with the handler's result.
5185 pub chained_promise: Option<GcRef>,
5186 /// Whether this is a fulfillment reaction (true) or rejection (false).
5187 pub is_fulfillment: bool,
5188}
5189
5190/// Take all pending microtasks from the queue (called by the VM).
5191pub fn take_microtasks() -> Vec<Microtask> {
5192 MICROTASK_QUEUE.with(|q| std::mem::take(&mut *q.borrow_mut()))
5193}
5194
5195fn enqueue_microtask(task: Microtask) {
5196 MICROTASK_QUEUE.with(|q| q.borrow_mut().push(task));
5197}
5198
5199/// Public wrappers for functions used by the VM's microtask drain.
5200pub fn promise_get_prop_pub(
5201 gc: &Gc<HeapObject>,
5202 shapes: &ShapeTable,
5203 promise: GcRef,
5204 key: &str,
5205) -> Value {
5206 promise_get_prop(gc, shapes, promise, key)
5207}
5208
5209pub fn promise_state_pub(gc: &Gc<HeapObject>, shapes: &ShapeTable, promise: GcRef) -> f64 {
5210 promise_state(gc, shapes, promise)
5211}
5212
5213pub fn is_promise_pub(gc: &Gc<HeapObject>, shapes: &ShapeTable, value: &Value) -> bool {
5214 is_promise(gc, shapes, value)
5215}
5216
5217pub fn chain_promise_pub(
5218 gc: &mut Gc<HeapObject>,
5219 shapes: &mut ShapeTable,
5220 source: GcRef,
5221 target: GcRef,
5222) {
5223 chain_promise(gc, shapes, source, target)
5224}
5225
5226pub fn create_promise_object_pub(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable) -> GcRef {
5227 create_promise_object(gc, shapes)
5228}
5229
5230pub fn enqueue_microtask_pub(task: Microtask) {
5231 enqueue_microtask(task);
5232}
5233
5234pub fn add_reaction_pub(
5235 gc: &mut Gc<HeapObject>,
5236 shapes: &mut ShapeTable,
5237 promise: GcRef,
5238 on_fulfilled: Value,
5239 on_rejected: Value,
5240) -> GcRef {
5241 add_reaction(gc, shapes, promise, on_fulfilled, on_rejected)
5242}
5243
5244/// Initialize Promise.prototype in a standalone GC (for unit tests that
5245/// create promise objects without a full VM).
5246pub fn init_promise_proto_for_test(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable) {
5247 let proto_data = ObjectData::new();
5248 let promise_proto = gc.alloc(HeapObject::Object(proto_data));
5249 init_promise_prototype(gc, shapes, promise_proto);
5250 PROMISE_PROTO.with(|cell| cell.set(Some(promise_proto)));
5251}
5252
5253fn init_promise_builtins(vm: &mut Vm) {
5254 // Create Promise.prototype (inherits from Object.prototype).
5255 let mut proto_data = ObjectData::new();
5256 if let Some(proto) = vm.object_prototype {
5257 proto_data.prototype = Some(proto);
5258 }
5259 let promise_proto = vm.gc.alloc(HeapObject::Object(proto_data));
5260 init_promise_prototype(&mut vm.gc, &mut vm.shapes, promise_proto);
5261 PROMISE_PROTO.with(|cell| cell.set(Some(promise_proto)));
5262 vm.promise_prototype = Some(promise_proto);
5263
5264 // Register native helper functions used by the JS preamble.
5265 let create_fn = make_native(&mut vm.gc, "__Promise_create", promise_native_create);
5266 vm.set_global("__Promise_create", Value::Function(create_fn));
5267
5268 let resolve_fn = make_native(&mut vm.gc, "__Promise_resolve", promise_native_resolve);
5269 vm.set_global("__Promise_resolve", Value::Function(resolve_fn));
5270
5271 let reject_fn = make_native(&mut vm.gc, "__Promise_reject", promise_native_reject);
5272 vm.set_global("__Promise_reject", Value::Function(reject_fn));
5273
5274 // Register Promise.resolve and Promise.reject as standalone globals
5275 // that the JS preamble will attach to the Promise constructor.
5276 let static_resolve = make_native(&mut vm.gc, "resolve", promise_static_resolve);
5277 vm.set_global("__Promise_static_resolve", Value::Function(static_resolve));
5278
5279 let static_reject = make_native(&mut vm.gc, "reject", promise_static_reject);
5280 vm.set_global("__Promise_static_reject", Value::Function(static_reject));
5281
5282 let static_all = make_native(&mut vm.gc, "all", promise_static_all);
5283 vm.set_global("__Promise_static_all", Value::Function(static_all));
5284
5285 let static_race = make_native(&mut vm.gc, "race", promise_static_race);
5286 vm.set_global("__Promise_static_race", Value::Function(static_race));
5287
5288 let static_all_settled = make_native(&mut vm.gc, "allSettled", promise_static_all_settled);
5289 vm.set_global(
5290 "__Promise_static_allSettled",
5291 Value::Function(static_all_settled),
5292 );
5293
5294 let static_any = make_native(&mut vm.gc, "any", promise_static_any);
5295 vm.set_global("__Promise_static_any", Value::Function(static_any));
5296}
5297
5298fn init_promise_prototype(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, proto: GcRef) {
5299 let methods: &[NativeMethod] = &[
5300 ("then", promise_proto_then),
5301 ("catch", promise_proto_catch),
5302 ("finally", promise_proto_finally),
5303 ];
5304 for &(name, callback) in methods {
5305 let f = make_native(gc, name, callback);
5306 set_builtin_prop(gc, shapes, proto, name, Value::Function(f));
5307 }
5308}
5309
5310/// Create a new pending promise object.
5311fn create_promise_object(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable) -> GcRef {
5312 let reactions = gc.alloc(HeapObject::Object(ObjectData::new()));
5313 let proto = PROMISE_PROTO.with(|cell| cell.get());
5314 let mut data = ObjectData::new();
5315 if let Some(p) = proto {
5316 data.prototype = Some(p);
5317 }
5318 data.insert_property(
5319 PROMISE_STATE_KEY.to_string(),
5320 Property::builtin(Value::Number(PROMISE_PENDING)),
5321 shapes,
5322 );
5323 data.insert_property(
5324 PROMISE_RESULT_KEY.to_string(),
5325 Property::builtin(Value::Undefined),
5326 shapes,
5327 );
5328 data.insert_property(
5329 PROMISE_REACTIONS_KEY.to_string(),
5330 Property::builtin(Value::Object(reactions)),
5331 shapes,
5332 );
5333 data.insert_property(
5334 PROMISE_REACTION_COUNT_KEY.to_string(),
5335 Property::builtin(Value::Number(0.0)),
5336 shapes,
5337 );
5338 gc.alloc(HeapObject::Object(data))
5339}
5340
5341/// Get a hidden property from a promise object.
5342fn promise_get_prop(gc: &Gc<HeapObject>, shapes: &ShapeTable, promise: GcRef, key: &str) -> Value {
5343 match gc.get(promise) {
5344 Some(HeapObject::Object(data)) => data
5345 .get_property(key, shapes)
5346 .map(|p| p.value)
5347 .unwrap_or(Value::Undefined),
5348 _ => Value::Undefined,
5349 }
5350}
5351
5352/// Set a hidden property on a promise object.
5353fn promise_set_prop(
5354 gc: &mut Gc<HeapObject>,
5355 shapes: &mut ShapeTable,
5356 promise: GcRef,
5357 key: &str,
5358 value: Value,
5359) {
5360 if let Some(HeapObject::Object(data)) = gc.get_mut(promise) {
5361 data.insert_property(key.to_string(), Property::builtin(value), shapes);
5362 }
5363}
5364
5365/// Get the state of a promise (PROMISE_PENDING/FULFILLED/REJECTED).
5366fn promise_state(gc: &Gc<HeapObject>, shapes: &ShapeTable, promise: GcRef) -> f64 {
5367 match promise_get_prop(gc, shapes, promise, PROMISE_STATE_KEY) {
5368 Value::Number(n) => n,
5369 _ => PROMISE_PENDING,
5370 }
5371}
5372
5373/// Resolve a pending promise with a value.
5374pub fn resolve_promise_internal(
5375 gc: &mut Gc<HeapObject>,
5376 shapes: &mut ShapeTable,
5377 promise: GcRef,
5378 value: Value,
5379) {
5380 if promise_state(gc, shapes, promise) != PROMISE_PENDING {
5381 return; // Already settled.
5382 }
5383 promise_set_prop(
5384 gc,
5385 shapes,
5386 promise,
5387 PROMISE_STATE_KEY,
5388 Value::Number(PROMISE_FULFILLED),
5389 );
5390 promise_set_prop(gc, shapes, promise, PROMISE_RESULT_KEY, value.clone());
5391 trigger_reactions(gc, shapes, promise, value, true);
5392}
5393
5394/// Reject a pending promise with a reason.
5395pub fn reject_promise_internal(
5396 gc: &mut Gc<HeapObject>,
5397 shapes: &mut ShapeTable,
5398 promise: GcRef,
5399 reason: Value,
5400) {
5401 if promise_state(gc, shapes, promise) != PROMISE_PENDING {
5402 return; // Already settled.
5403 }
5404 promise_set_prop(
5405 gc,
5406 shapes,
5407 promise,
5408 PROMISE_STATE_KEY,
5409 Value::Number(PROMISE_REJECTED),
5410 );
5411 promise_set_prop(gc, shapes, promise, PROMISE_RESULT_KEY, reason.clone());
5412 trigger_reactions(gc, shapes, promise, reason, false);
5413}
5414
5415/// Enqueue microtasks for all registered reactions on a promise.
5416fn trigger_reactions(
5417 gc: &mut Gc<HeapObject>,
5418 shapes: &mut ShapeTable,
5419 promise: GcRef,
5420 value: Value,
5421 fulfilled: bool,
5422) {
5423 let reactions_ref = match promise_get_prop(gc, shapes, promise, PROMISE_REACTIONS_KEY) {
5424 Value::Object(r) => r,
5425 _ => return,
5426 };
5427 let count = match promise_get_prop(gc, shapes, promise, PROMISE_REACTION_COUNT_KEY) {
5428 Value::Number(n) => n as usize,
5429 _ => 0,
5430 };
5431
5432 // Collect reactions before mutating.
5433 let mut reactions = Vec::new();
5434 for i in 0..count {
5435 let fulfill_key = format!("{i}_fulfill");
5436 let reject_key = format!("{i}_reject");
5437 let promise_key = format!("{i}_promise");
5438
5439 let on_fulfilled = match gc.get(reactions_ref) {
5440 Some(HeapObject::Object(data)) => data
5441 .get_property(&fulfill_key, shapes)
5442 .map(|p| p.value)
5443 .unwrap_or(Value::Undefined),
5444 _ => Value::Undefined,
5445 };
5446 let on_rejected = match gc.get(reactions_ref) {
5447 Some(HeapObject::Object(data)) => data
5448 .get_property(&reject_key, shapes)
5449 .map(|p| p.value)
5450 .unwrap_or(Value::Undefined),
5451 _ => Value::Undefined,
5452 };
5453 let chained = match gc.get(reactions_ref) {
5454 Some(HeapObject::Object(data)) => data
5455 .get_property(&promise_key, shapes)
5456 .and_then(|p| p.value.gc_ref()),
5457 _ => None,
5458 };
5459
5460 let handler = if fulfilled { on_fulfilled } else { on_rejected };
5461 let handler_ref = match &handler {
5462 Value::Function(r) => Some(*r),
5463 _ => None,
5464 };
5465
5466 reactions.push(Microtask {
5467 handler: handler_ref,
5468 value: value.clone(),
5469 chained_promise: chained,
5470 is_fulfillment: fulfilled,
5471 });
5472 }
5473
5474 for task in reactions {
5475 enqueue_microtask(task);
5476 }
5477}
5478
5479/// Add a reaction to a promise. Returns the chained promise GcRef.
5480fn add_reaction(
5481 gc: &mut Gc<HeapObject>,
5482 shapes: &mut ShapeTable,
5483 promise: GcRef,
5484 on_fulfilled: Value,
5485 on_rejected: Value,
5486) -> GcRef {
5487 let chained = create_promise_object(gc, shapes);
5488
5489 let reactions_ref = match promise_get_prop(gc, shapes, promise, PROMISE_REACTIONS_KEY) {
5490 Value::Object(r) => r,
5491 _ => return chained,
5492 };
5493 let count = match promise_get_prop(gc, shapes, promise, PROMISE_REACTION_COUNT_KEY) {
5494 Value::Number(n) => n as usize,
5495 _ => 0,
5496 };
5497
5498 if let Some(HeapObject::Object(data)) = gc.get_mut(reactions_ref) {
5499 data.insert_property(
5500 format!("{count}_fulfill"),
5501 Property::builtin(on_fulfilled),
5502 shapes,
5503 );
5504 data.insert_property(
5505 format!("{count}_reject"),
5506 Property::builtin(on_rejected),
5507 shapes,
5508 );
5509 data.insert_property(
5510 format!("{count}_promise"),
5511 Property::builtin(Value::Object(chained)),
5512 shapes,
5513 );
5514 }
5515
5516 promise_set_prop(
5517 gc,
5518 shapes,
5519 promise,
5520 PROMISE_REACTION_COUNT_KEY,
5521 Value::Number((count + 1) as f64),
5522 );
5523
5524 chained
5525}
5526
5527// ── Promise native helpers (called from JS preamble) ─────────
5528
5529/// `__Promise_create()` — create a new pending promise object.
5530fn promise_native_create(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5531 let promise = create_promise_object(ctx.gc, ctx.shapes);
5532 Ok(Value::Object(promise))
5533}
5534
5535/// `__Promise_resolve(promise, value)` — resolve a pending promise.
5536fn promise_native_resolve(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5537 let promise_ref = args.first().and_then(|v| v.gc_ref()).ok_or_else(|| {
5538 RuntimeError::type_error("__Promise_resolve: first arg must be a promise")
5539 })?;
5540 let value = args.get(1).cloned().unwrap_or(Value::Undefined);
5541
5542 // If value is a thenable (promise), chain it.
5543 if is_promise(ctx.gc, ctx.shapes, &value) {
5544 let value_ref = value.gc_ref().unwrap();
5545 let state = promise_state(ctx.gc, ctx.shapes, value_ref);
5546 if state == PROMISE_FULFILLED {
5547 let result = promise_get_prop(ctx.gc, ctx.shapes, value_ref, PROMISE_RESULT_KEY);
5548 resolve_promise_internal(ctx.gc, ctx.shapes, promise_ref, result);
5549 } else if state == PROMISE_REJECTED {
5550 let reason = promise_get_prop(ctx.gc, ctx.shapes, value_ref, PROMISE_RESULT_KEY);
5551 reject_promise_internal(ctx.gc, ctx.shapes, promise_ref, reason);
5552 } else {
5553 // Pending thenable: chain so that when value_ref settles,
5554 // promise_ref settles the same way.
5555 chain_promise(ctx.gc, ctx.shapes, value_ref, promise_ref);
5556 }
5557 } else {
5558 resolve_promise_internal(ctx.gc, ctx.shapes, promise_ref, value);
5559 }
5560
5561 Ok(Value::Undefined)
5562}
5563
5564/// Chain a source promise to a target: when source settles, settle target the same way.
5565fn chain_promise(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, source: GcRef, target: GcRef) {
5566 let reactions_ref = match promise_get_prop(gc, shapes, source, PROMISE_REACTIONS_KEY) {
5567 Value::Object(r) => r,
5568 _ => return,
5569 };
5570 let count = match promise_get_prop(gc, shapes, source, PROMISE_REACTION_COUNT_KEY) {
5571 Value::Number(n) => n as usize,
5572 _ => 0,
5573 };
5574
5575 // Store the target promise directly — the microtask drain handles identity propagation.
5576 if let Some(HeapObject::Object(data)) = gc.get_mut(reactions_ref) {
5577 data.insert_property(
5578 format!("{count}_fulfill"),
5579 Property::builtin(Value::Undefined),
5580 shapes,
5581 );
5582 data.insert_property(
5583 format!("{count}_reject"),
5584 Property::builtin(Value::Undefined),
5585 shapes,
5586 );
5587 data.insert_property(
5588 format!("{count}_promise"),
5589 Property::builtin(Value::Object(target)),
5590 shapes,
5591 );
5592 }
5593
5594 promise_set_prop(
5595 gc,
5596 shapes,
5597 source,
5598 PROMISE_REACTION_COUNT_KEY,
5599 Value::Number((count + 1) as f64),
5600 );
5601}
5602
5603/// Check if a value is a promise (has __promise_state__ property).
5604fn is_promise(gc: &Gc<HeapObject>, shapes: &ShapeTable, value: &Value) -> bool {
5605 let gc_ref = match value.gc_ref() {
5606 Some(r) => r,
5607 None => return false,
5608 };
5609 match gc.get(gc_ref) {
5610 Some(HeapObject::Object(data)) => data.contains_key(PROMISE_STATE_KEY, shapes),
5611 _ => false,
5612 }
5613}
5614
5615/// `__Promise_reject(promise, reason)` — reject a pending promise.
5616fn promise_native_reject(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5617 let promise_ref = args
5618 .first()
5619 .and_then(|v| v.gc_ref())
5620 .ok_or_else(|| RuntimeError::type_error("__Promise_reject: first arg must be a promise"))?;
5621 let reason = args.get(1).cloned().unwrap_or(Value::Undefined);
5622 reject_promise_internal(ctx.gc, ctx.shapes, promise_ref, reason);
5623 Ok(Value::Undefined)
5624}
5625
5626// ── Promise.prototype.then / catch / finally ─────────────────
5627
5628fn promise_proto_then(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5629 let promise_ref = match ctx.this.gc_ref() {
5630 Some(r) => r,
5631 None => return Err(RuntimeError::type_error("then called on non-object")),
5632 };
5633
5634 let on_fulfilled = args.first().cloned().unwrap_or(Value::Undefined);
5635 let on_rejected = args.get(1).cloned().unwrap_or(Value::Undefined);
5636
5637 let state = promise_state(ctx.gc, ctx.shapes, promise_ref);
5638
5639 if state == PROMISE_PENDING {
5640 // Register reaction for later.
5641 let chained = add_reaction(ctx.gc, ctx.shapes, promise_ref, on_fulfilled, on_rejected);
5642 return Ok(Value::Object(chained));
5643 }
5644
5645 // Already settled — enqueue microtask immediately.
5646 let result = promise_get_prop(ctx.gc, ctx.shapes, promise_ref, PROMISE_RESULT_KEY);
5647 let chained = create_promise_object(ctx.gc, ctx.shapes);
5648 let fulfilled = state == PROMISE_FULFILLED;
5649 let handler = if fulfilled {
5650 &on_fulfilled
5651 } else {
5652 &on_rejected
5653 };
5654 let handler_ref = match handler {
5655 Value::Function(r) => Some(*r),
5656 _ => None,
5657 };
5658
5659 enqueue_microtask(Microtask {
5660 handler: handler_ref,
5661 value: result,
5662 chained_promise: Some(chained),
5663 is_fulfillment: fulfilled,
5664 });
5665
5666 Ok(Value::Object(chained))
5667}
5668
5669fn promise_proto_catch(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5670 let on_rejected = args.first().cloned().unwrap_or(Value::Undefined);
5671 promise_proto_then(&[Value::Undefined, on_rejected], ctx)
5672}
5673
5674fn promise_proto_finally(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5675 let promise_ref = match ctx.this.gc_ref() {
5676 Some(r) => r,
5677 None => return Err(RuntimeError::type_error("finally called on non-object")),
5678 };
5679
5680 let on_finally = args.first().cloned().unwrap_or(Value::Undefined);
5681 let state = promise_state(ctx.gc, ctx.shapes, promise_ref);
5682
5683 if state == PROMISE_PENDING {
5684 // Register reaction: finally handler doesn't receive value, just runs.
5685 let chained = add_reaction(
5686 ctx.gc,
5687 ctx.shapes,
5688 promise_ref,
5689 on_finally.clone(),
5690 on_finally,
5691 );
5692 // Mark the chained promise reactions as "finally" so drain can handle them.
5693 promise_set_prop(
5694 ctx.gc,
5695 ctx.shapes,
5696 chained,
5697 "__finally__",
5698 Value::Boolean(true),
5699 );
5700 // Store parent result for propagation.
5701 promise_set_prop(
5702 ctx.gc,
5703 ctx.shapes,
5704 chained,
5705 "__finally_parent__",
5706 Value::Object(promise_ref),
5707 );
5708 return Ok(Value::Object(chained));
5709 }
5710
5711 // Already settled.
5712 let _result = promise_get_prop(ctx.gc, ctx.shapes, promise_ref, PROMISE_RESULT_KEY);
5713 let chained = create_promise_object(ctx.gc, ctx.shapes);
5714 let handler_ref = match &on_finally {
5715 Value::Function(r) => Some(*r),
5716 _ => None,
5717 };
5718
5719 // For finally, we enqueue the callback but propagate the original result.
5720 promise_set_prop(
5721 ctx.gc,
5722 ctx.shapes,
5723 chained,
5724 "__finally__",
5725 Value::Boolean(true),
5726 );
5727 promise_set_prop(
5728 ctx.gc,
5729 ctx.shapes,
5730 chained,
5731 "__finally_parent__",
5732 Value::Object(promise_ref),
5733 );
5734
5735 enqueue_microtask(Microtask {
5736 handler: handler_ref,
5737 value: Value::Undefined, // finally callback gets no arguments
5738 chained_promise: Some(chained),
5739 is_fulfillment: state == PROMISE_FULFILLED,
5740 });
5741
5742 Ok(Value::Object(chained))
5743}
5744
5745// ── Promise static methods ───────────────────────────────────
5746
5747fn promise_static_resolve(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5748 let value = args.first().cloned().unwrap_or(Value::Undefined);
5749
5750 // If already a promise, return it.
5751 if is_promise(ctx.gc, ctx.shapes, &value) {
5752 return Ok(value);
5753 }
5754
5755 let promise = create_promise_object(ctx.gc, ctx.shapes);
5756 resolve_promise_internal(ctx.gc, ctx.shapes, promise, value);
5757 Ok(Value::Object(promise))
5758}
5759
5760fn promise_static_reject(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5761 let reason = args.first().cloned().unwrap_or(Value::Undefined);
5762 let promise = create_promise_object(ctx.gc, ctx.shapes);
5763 reject_promise_internal(ctx.gc, ctx.shapes, promise, reason);
5764 Ok(Value::Object(promise))
5765}
5766
5767fn promise_static_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5768 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
5769 let arr_ref = match iterable.gc_ref() {
5770 Some(r) => r,
5771 None => {
5772 let p = create_promise_object(ctx.gc, ctx.shapes);
5773 reject_promise_internal(
5774 ctx.gc,
5775 ctx.shapes,
5776 p,
5777 Value::String("Promise.all requires an iterable".to_string()),
5778 );
5779 return Ok(Value::Object(p));
5780 }
5781 };
5782
5783 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
5784 let result_promise = create_promise_object(ctx.gc, ctx.shapes);
5785
5786 if len == 0 {
5787 let empty = make_value_array(ctx.gc, ctx.shapes, &[]);
5788 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, empty);
5789 return Ok(Value::Object(result_promise));
5790 }
5791
5792 // Create a results array and a counter object.
5793 let results = make_value_array(ctx.gc, ctx.shapes, &vec![Value::Undefined; len]);
5794 let results_ref = results.gc_ref().unwrap();
5795
5796 // We track remaining count and results in hidden props on result_promise.
5797 promise_set_prop(
5798 ctx.gc,
5799 ctx.shapes,
5800 result_promise,
5801 "__all_remaining__",
5802 Value::Number(len as f64),
5803 );
5804 promise_set_prop(
5805 ctx.gc,
5806 ctx.shapes,
5807 result_promise,
5808 "__all_results__",
5809 results.clone(),
5810 );
5811
5812 for i in 0..len {
5813 let item = array_get(ctx.gc, ctx.shapes, arr_ref, i);
5814 if is_promise(ctx.gc, ctx.shapes, &item) {
5815 let item_ref = item.gc_ref().unwrap();
5816 let state = promise_state(ctx.gc, ctx.shapes, item_ref);
5817 if state == PROMISE_FULFILLED {
5818 let val = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
5819 array_set(ctx.gc, ctx.shapes, results_ref, i, val);
5820 promise_all_decrement(ctx.gc, ctx.shapes, result_promise);
5821 } else if state == PROMISE_REJECTED {
5822 let reason = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
5823 reject_promise_internal(ctx.gc, ctx.shapes, result_promise, reason);
5824 return Ok(Value::Object(result_promise));
5825 } else {
5826 // Pending: we need to register a reaction. Store index info.
5827 promise_set_prop(
5828 ctx.gc,
5829 ctx.shapes,
5830 item_ref,
5831 &format!("__all_target_{i}__"),
5832 Value::Object(result_promise),
5833 );
5834 // Add a reaction — the microtask drain will handle all-tracking.
5835 let chained = add_reaction(
5836 ctx.gc,
5837 ctx.shapes,
5838 item_ref,
5839 Value::Undefined,
5840 Value::Undefined,
5841 );
5842 promise_set_prop(
5843 ctx.gc,
5844 ctx.shapes,
5845 chained,
5846 "__all_index__",
5847 Value::Number(i as f64),
5848 );
5849 promise_set_prop(
5850 ctx.gc,
5851 ctx.shapes,
5852 chained,
5853 "__all_target__",
5854 Value::Object(result_promise),
5855 );
5856 }
5857 } else {
5858 // Non-promise value: treat as immediately resolved.
5859 array_set(ctx.gc, ctx.shapes, results_ref, i, item);
5860 promise_all_decrement(ctx.gc, ctx.shapes, result_promise);
5861 }
5862 }
5863
5864 Ok(Value::Object(result_promise))
5865}
5866
5867fn promise_all_decrement(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, result_promise: GcRef) {
5868 let remaining = match promise_get_prop(gc, shapes, result_promise, "__all_remaining__") {
5869 Value::Number(n) => n as usize,
5870 _ => return,
5871 };
5872 let new_remaining = remaining.saturating_sub(1);
5873 promise_set_prop(
5874 gc,
5875 shapes,
5876 result_promise,
5877 "__all_remaining__",
5878 Value::Number(new_remaining as f64),
5879 );
5880 if new_remaining == 0 {
5881 let results = promise_get_prop(gc, shapes, result_promise, "__all_results__");
5882 resolve_promise_internal(gc, shapes, result_promise, results);
5883 }
5884}
5885
5886fn promise_static_race(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5887 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
5888 let arr_ref = match iterable.gc_ref() {
5889 Some(r) => r,
5890 None => {
5891 let p = create_promise_object(ctx.gc, ctx.shapes);
5892 reject_promise_internal(
5893 ctx.gc,
5894 ctx.shapes,
5895 p,
5896 Value::String("Promise.race requires an iterable".to_string()),
5897 );
5898 return Ok(Value::Object(p));
5899 }
5900 };
5901
5902 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
5903 let result_promise = create_promise_object(ctx.gc, ctx.shapes);
5904
5905 for i in 0..len {
5906 let item = array_get(ctx.gc, ctx.shapes, arr_ref, i);
5907 if is_promise(ctx.gc, ctx.shapes, &item) {
5908 let item_ref = item.gc_ref().unwrap();
5909 let state = promise_state(ctx.gc, ctx.shapes, item_ref);
5910 if state == PROMISE_FULFILLED {
5911 let val = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
5912 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, val);
5913 return Ok(Value::Object(result_promise));
5914 } else if state == PROMISE_REJECTED {
5915 let reason = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
5916 reject_promise_internal(ctx.gc, ctx.shapes, result_promise, reason);
5917 return Ok(Value::Object(result_promise));
5918 } else {
5919 chain_promise(ctx.gc, ctx.shapes, item_ref, result_promise);
5920 }
5921 } else {
5922 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, item);
5923 return Ok(Value::Object(result_promise));
5924 }
5925 }
5926
5927 Ok(Value::Object(result_promise))
5928}
5929
5930fn promise_static_all_settled(
5931 args: &[Value],
5932 ctx: &mut NativeContext,
5933) -> Result<Value, RuntimeError> {
5934 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
5935 let arr_ref = match iterable.gc_ref() {
5936 Some(r) => r,
5937 None => {
5938 let p = create_promise_object(ctx.gc, ctx.shapes);
5939 reject_promise_internal(
5940 ctx.gc,
5941 ctx.shapes,
5942 p,
5943 Value::String("Promise.allSettled requires an iterable".to_string()),
5944 );
5945 return Ok(Value::Object(p));
5946 }
5947 };
5948
5949 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
5950 let result_promise = create_promise_object(ctx.gc, ctx.shapes);
5951
5952 if len == 0 {
5953 let empty = make_value_array(ctx.gc, ctx.shapes, &[]);
5954 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, empty);
5955 return Ok(Value::Object(result_promise));
5956 }
5957
5958 let results = make_value_array(ctx.gc, ctx.shapes, &vec![Value::Undefined; len]);
5959 let results_ref = results.gc_ref().unwrap();
5960
5961 promise_set_prop(
5962 ctx.gc,
5963 ctx.shapes,
5964 result_promise,
5965 "__all_remaining__",
5966 Value::Number(len as f64),
5967 );
5968 promise_set_prop(
5969 ctx.gc,
5970 ctx.shapes,
5971 result_promise,
5972 "__all_results__",
5973 results,
5974 );
5975
5976 for i in 0..len {
5977 let item = array_get(ctx.gc, ctx.shapes, arr_ref, i);
5978 if is_promise(ctx.gc, ctx.shapes, &item) {
5979 let item_ref = item.gc_ref().unwrap();
5980 let state = promise_state(ctx.gc, ctx.shapes, item_ref);
5981 if state != PROMISE_PENDING {
5982 let val = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
5983 let status_str = if state == PROMISE_FULFILLED {
5984 "fulfilled"
5985 } else {
5986 "rejected"
5987 };
5988 let entry = make_settled_entry(
5989 ctx.gc,
5990 ctx.shapes,
5991 status_str,
5992 &val,
5993 state == PROMISE_FULFILLED,
5994 );
5995 array_set(ctx.gc, ctx.shapes, results_ref, i, entry);
5996 promise_all_decrement(ctx.gc, ctx.shapes, result_promise);
5997 } else {
5998 chain_promise(ctx.gc, ctx.shapes, item_ref, result_promise);
5999 }
6000 } else {
6001 let entry = make_settled_entry(ctx.gc, ctx.shapes, "fulfilled", &item, true);
6002 array_set(ctx.gc, ctx.shapes, results_ref, i, entry);
6003 promise_all_decrement(ctx.gc, ctx.shapes, result_promise);
6004 }
6005 }
6006
6007 Ok(Value::Object(result_promise))
6008}
6009
6010fn make_settled_entry(
6011 gc: &mut Gc<HeapObject>,
6012 shapes: &mut ShapeTable,
6013 status: &str,
6014 value: &Value,
6015 is_fulfilled: bool,
6016) -> Value {
6017 let mut data = ObjectData::new();
6018 data.insert_property(
6019 "status".to_string(),
6020 Property::data(Value::String(status.to_string())),
6021 shapes,
6022 );
6023 if is_fulfilled {
6024 data.insert_property("value".to_string(), Property::data(value.clone()), shapes);
6025 } else {
6026 data.insert_property("reason".to_string(), Property::data(value.clone()), shapes);
6027 }
6028 Value::Object(gc.alloc(HeapObject::Object(data)))
6029}
6030
6031fn promise_static_any(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6032 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
6033 let arr_ref = match iterable.gc_ref() {
6034 Some(r) => r,
6035 None => {
6036 let p = create_promise_object(ctx.gc, ctx.shapes);
6037 reject_promise_internal(
6038 ctx.gc,
6039 ctx.shapes,
6040 p,
6041 Value::String("Promise.any requires an iterable".to_string()),
6042 );
6043 return Ok(Value::Object(p));
6044 }
6045 };
6046
6047 let len = array_length(ctx.gc, ctx.shapes, arr_ref);
6048 let result_promise = create_promise_object(ctx.gc, ctx.shapes);
6049
6050 if len == 0 {
6051 // Reject with AggregateError.
6052 let err = Value::String("All promises were rejected".to_string());
6053 reject_promise_internal(ctx.gc, ctx.shapes, result_promise, err);
6054 return Ok(Value::Object(result_promise));
6055 }
6056
6057 let errors = make_value_array(ctx.gc, ctx.shapes, &vec![Value::Undefined; len]);
6058 let errors_ref = errors.gc_ref().unwrap();
6059
6060 promise_set_prop(
6061 ctx.gc,
6062 ctx.shapes,
6063 result_promise,
6064 "__any_remaining__",
6065 Value::Number(len as f64),
6066 );
6067 promise_set_prop(ctx.gc, ctx.shapes, result_promise, "__any_errors__", errors);
6068
6069 for i in 0..len {
6070 let item = array_get(ctx.gc, ctx.shapes, arr_ref, i);
6071 if is_promise(ctx.gc, ctx.shapes, &item) {
6072 let item_ref = item.gc_ref().unwrap();
6073 let state = promise_state(ctx.gc, ctx.shapes, item_ref);
6074 if state == PROMISE_FULFILLED {
6075 let val = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
6076 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, val);
6077 return Ok(Value::Object(result_promise));
6078 } else if state == PROMISE_REJECTED {
6079 let reason = promise_get_prop(ctx.gc, ctx.shapes, item_ref, PROMISE_RESULT_KEY);
6080 array_set(ctx.gc, ctx.shapes, errors_ref, i, reason);
6081 promise_any_decrement(ctx.gc, ctx.shapes, result_promise);
6082 } else {
6083 chain_promise(ctx.gc, ctx.shapes, item_ref, result_promise);
6084 }
6085 } else {
6086 resolve_promise_internal(ctx.gc, ctx.shapes, result_promise, item);
6087 return Ok(Value::Object(result_promise));
6088 }
6089 }
6090
6091 Ok(Value::Object(result_promise))
6092}
6093
6094fn promise_any_decrement(gc: &mut Gc<HeapObject>, shapes: &mut ShapeTable, result_promise: GcRef) {
6095 let remaining = match promise_get_prop(gc, shapes, result_promise, "__any_remaining__") {
6096 Value::Number(n) => n as usize,
6097 _ => return,
6098 };
6099 let new_remaining = remaining.saturating_sub(1);
6100 promise_set_prop(
6101 gc,
6102 shapes,
6103 result_promise,
6104 "__any_remaining__",
6105 Value::Number(new_remaining as f64),
6106 );
6107 if new_remaining == 0 {
6108 let err = Value::String("All promises were rejected".to_string());
6109 reject_promise_internal(gc, shapes, result_promise, err);
6110 }
6111}
6112
6113// ── JSON object ──────────────────────────────────────────────
6114
6115fn init_json_object(vm: &mut Vm) {
6116 let mut data = ObjectData::new();
6117 if let Some(proto) = vm.object_prototype {
6118 data.prototype = Some(proto);
6119 }
6120 let json_ref = vm.gc.alloc(HeapObject::Object(data));
6121
6122 let parse_fn = make_native(&mut vm.gc, "parse", json_parse);
6123 set_builtin_prop(
6124 &mut vm.gc,
6125 &mut vm.shapes,
6126 json_ref,
6127 "parse",
6128 Value::Function(parse_fn),
6129 );
6130
6131 let stringify_fn = make_native(&mut vm.gc, "stringify", json_stringify);
6132 set_builtin_prop(
6133 &mut vm.gc,
6134 &mut vm.shapes,
6135 json_ref,
6136 "stringify",
6137 Value::Function(stringify_fn),
6138 );
6139
6140 vm.set_global("JSON", Value::Object(json_ref));
6141}
6142
6143// ── JSON.parse ───────────────────────────────────────────────
6144
6145/// Minimal JSON tokenizer.
6146#[derive(Debug, Clone, PartialEq)]
6147enum JsonToken {
6148 LBrace,
6149 RBrace,
6150 LBracket,
6151 RBracket,
6152 Colon,
6153 Comma,
6154 String(String),
6155 Number(f64),
6156 True,
6157 False,
6158 Null,
6159}
6160
6161struct JsonLexer<'a> {
6162 chars: &'a [u8],
6163 pos: usize,
6164}
6165
6166impl<'a> JsonLexer<'a> {
6167 fn new(input: &'a str) -> Self {
6168 Self {
6169 chars: input.as_bytes(),
6170 pos: 0,
6171 }
6172 }
6173
6174 fn skip_ws(&mut self) {
6175 while self.pos < self.chars.len() {
6176 match self.chars[self.pos] {
6177 b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
6178 _ => break,
6179 }
6180 }
6181 }
6182
6183 fn next_token(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
6184 self.skip_ws();
6185 if self.pos >= self.chars.len() {
6186 return Ok(None);
6187 }
6188 let ch = self.chars[self.pos];
6189 match ch {
6190 b'{' => {
6191 self.pos += 1;
6192 Ok(Some(JsonToken::LBrace))
6193 }
6194 b'}' => {
6195 self.pos += 1;
6196 Ok(Some(JsonToken::RBrace))
6197 }
6198 b'[' => {
6199 self.pos += 1;
6200 Ok(Some(JsonToken::LBracket))
6201 }
6202 b']' => {
6203 self.pos += 1;
6204 Ok(Some(JsonToken::RBracket))
6205 }
6206 b':' => {
6207 self.pos += 1;
6208 Ok(Some(JsonToken::Colon))
6209 }
6210 b',' => {
6211 self.pos += 1;
6212 Ok(Some(JsonToken::Comma))
6213 }
6214 b'"' => self.read_string(),
6215 b't' => self.read_keyword("true", JsonToken::True),
6216 b'f' => self.read_keyword("false", JsonToken::False),
6217 b'n' => self.read_keyword("null", JsonToken::Null),
6218 b'-' | b'0'..=b'9' => self.read_number(),
6219 _ => Err(RuntimeError::syntax_error(format!(
6220 "Unexpected character '{}' in JSON",
6221 ch as char
6222 ))),
6223 }
6224 }
6225
6226 fn read_string(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
6227 self.pos += 1; // skip opening quote
6228 let mut s = String::new();
6229 while self.pos < self.chars.len() {
6230 let ch = self.chars[self.pos];
6231 self.pos += 1;
6232 if ch == b'"' {
6233 return Ok(Some(JsonToken::String(s)));
6234 }
6235 if ch == b'\\' {
6236 if self.pos >= self.chars.len() {
6237 return Err(RuntimeError::syntax_error("Unterminated string in JSON"));
6238 }
6239 let esc = self.chars[self.pos];
6240 self.pos += 1;
6241 match esc {
6242 b'"' => s.push('"'),
6243 b'\\' => s.push('\\'),
6244 b'/' => s.push('/'),
6245 b'b' => s.push('\u{0008}'),
6246 b'f' => s.push('\u{000C}'),
6247 b'n' => s.push('\n'),
6248 b'r' => s.push('\r'),
6249 b't' => s.push('\t'),
6250 b'u' => {
6251 if self.pos + 4 > self.chars.len() {
6252 return Err(RuntimeError::syntax_error("Invalid unicode escape"));
6253 }
6254 let hex = std::str::from_utf8(&self.chars[self.pos..self.pos + 4])
6255 .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?;
6256 self.pos += 4;
6257 let code = u32::from_str_radix(hex, 16)
6258 .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?;
6259 if let Some(c) = char::from_u32(code) {
6260 s.push(c);
6261 } else {
6262 return Err(RuntimeError::syntax_error("Invalid unicode code point"));
6263 }
6264 }
6265 _ => {
6266 return Err(RuntimeError::syntax_error(format!(
6267 "Invalid escape character '\\{}'",
6268 esc as char
6269 )));
6270 }
6271 }
6272 } else {
6273 s.push(ch as char);
6274 }
6275 }
6276 Err(RuntimeError::syntax_error("Unterminated string in JSON"))
6277 }
6278
6279 fn read_number(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
6280 let start = self.pos;
6281 if self.pos < self.chars.len() && self.chars[self.pos] == b'-' {
6282 self.pos += 1;
6283 }
6284 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
6285 self.pos += 1;
6286 }
6287 if self.pos < self.chars.len() && self.chars[self.pos] == b'.' {
6288 self.pos += 1;
6289 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
6290 self.pos += 1;
6291 }
6292 }
6293 if self.pos < self.chars.len()
6294 && (self.chars[self.pos] == b'e' || self.chars[self.pos] == b'E')
6295 {
6296 self.pos += 1;
6297 if self.pos < self.chars.len()
6298 && (self.chars[self.pos] == b'+' || self.chars[self.pos] == b'-')
6299 {
6300 self.pos += 1;
6301 }
6302 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
6303 self.pos += 1;
6304 }
6305 }
6306 let num_str = std::str::from_utf8(&self.chars[start..self.pos])
6307 .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?;
6308 let n: f64 = num_str
6309 .parse()
6310 .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?;
6311 Ok(Some(JsonToken::Number(n)))
6312 }
6313
6314 fn read_keyword(
6315 &mut self,
6316 keyword: &str,
6317 token: JsonToken,
6318 ) -> Result<Option<JsonToken>, RuntimeError> {
6319 let end = self.pos + keyword.len();
6320 if end <= self.chars.len() && &self.chars[self.pos..end] == keyword.as_bytes() {
6321 self.pos = end;
6322 Ok(Some(token))
6323 } else {
6324 Err(RuntimeError::syntax_error(format!(
6325 "Unexpected token in JSON at position {}",
6326 self.pos
6327 )))
6328 }
6329 }
6330}
6331
6332struct JsonParser<'a> {
6333 lexer: JsonLexer<'a>,
6334 current: Option<JsonToken>,
6335}
6336
6337impl<'a> JsonParser<'a> {
6338 fn new(input: &'a str) -> Result<Self, RuntimeError> {
6339 let mut lexer = JsonLexer::new(input);
6340 let current = lexer.next_token()?;
6341 Ok(Self { lexer, current })
6342 }
6343
6344 fn advance(&mut self) -> Result<(), RuntimeError> {
6345 self.current = self.lexer.next_token()?;
6346 Ok(())
6347 }
6348
6349 fn parse_value(
6350 &mut self,
6351 gc: &mut Gc<HeapObject>,
6352 shapes: &mut ShapeTable,
6353 ) -> Result<Value, RuntimeError> {
6354 match self.current.take() {
6355 Some(JsonToken::Null) => {
6356 self.advance()?;
6357 Ok(Value::Null)
6358 }
6359 Some(JsonToken::True) => {
6360 self.advance()?;
6361 Ok(Value::Boolean(true))
6362 }
6363 Some(JsonToken::False) => {
6364 self.advance()?;
6365 Ok(Value::Boolean(false))
6366 }
6367 Some(JsonToken::Number(n)) => {
6368 self.advance()?;
6369 Ok(Value::Number(n))
6370 }
6371 Some(JsonToken::String(s)) => {
6372 self.advance()?;
6373 Ok(Value::String(s))
6374 }
6375 Some(JsonToken::LBracket) => self.parse_array(gc, shapes),
6376 Some(JsonToken::LBrace) => self.parse_object(gc, shapes),
6377 Some(other) => {
6378 // Put it back for error context.
6379 self.current = Some(other);
6380 Err(RuntimeError::syntax_error("Unexpected token in JSON"))
6381 }
6382 None => Err(RuntimeError::syntax_error("Unexpected end of JSON input")),
6383 }
6384 }
6385
6386 fn parse_array(
6387 &mut self,
6388 gc: &mut Gc<HeapObject>,
6389 shapes: &mut ShapeTable,
6390 ) -> Result<Value, RuntimeError> {
6391 // Current token was LBracket, already consumed via take().
6392 self.advance()?; // move past '['
6393 let mut items: Vec<Value> = Vec::new();
6394 if self.current == Some(JsonToken::RBracket) {
6395 self.advance()?;
6396 let mut obj = ObjectData::new();
6397 obj.insert_property(
6398 "length".to_string(),
6399 Property {
6400 value: Value::Number(0.0),
6401 writable: true,
6402 enumerable: false,
6403 configurable: false,
6404 },
6405 shapes,
6406 );
6407 return Ok(Value::Object(gc.alloc(HeapObject::Object(obj))));
6408 }
6409 loop {
6410 let val = self.parse_value(gc, shapes)?;
6411 items.push(val);
6412 match &self.current {
6413 Some(JsonToken::Comma) => {
6414 self.advance()?;
6415 }
6416 Some(JsonToken::RBracket) => {
6417 self.advance()?;
6418 break;
6419 }
6420 _ => {
6421 return Err(RuntimeError::syntax_error(
6422 "Expected ',' or ']' in JSON array",
6423 ));
6424 }
6425 }
6426 }
6427 let mut obj = ObjectData::new();
6428 for (i, v) in items.iter().enumerate() {
6429 obj.insert_property(i.to_string(), Property::data(v.clone()), shapes);
6430 }
6431 obj.insert_property(
6432 "length".to_string(),
6433 Property {
6434 value: Value::Number(items.len() as f64),
6435 writable: true,
6436 enumerable: false,
6437 configurable: false,
6438 },
6439 shapes,
6440 );
6441 Ok(Value::Object(gc.alloc(HeapObject::Object(obj))))
6442 }
6443
6444 fn parse_object(
6445 &mut self,
6446 gc: &mut Gc<HeapObject>,
6447 shapes: &mut ShapeTable,
6448 ) -> Result<Value, RuntimeError> {
6449 self.advance()?; // move past '{'
6450 let mut obj = ObjectData::new();
6451 if self.current == Some(JsonToken::RBrace) {
6452 self.advance()?;
6453 return Ok(Value::Object(gc.alloc(HeapObject::Object(obj))));
6454 }
6455 loop {
6456 let key = match self.current.take() {
6457 Some(JsonToken::String(s)) => s,
6458 _ => {
6459 return Err(RuntimeError::syntax_error(
6460 "Expected string key in JSON object",
6461 ));
6462 }
6463 };
6464 self.advance()?;
6465 if self.current != Some(JsonToken::Colon) {
6466 return Err(RuntimeError::syntax_error(
6467 "Expected ':' after key in JSON object",
6468 ));
6469 }
6470 self.advance()?;
6471 let val = self.parse_value(gc, shapes)?;
6472 obj.insert_property(key, Property::data(val), shapes);
6473 match &self.current {
6474 Some(JsonToken::Comma) => {
6475 self.advance()?;
6476 }
6477 Some(JsonToken::RBrace) => {
6478 self.advance()?;
6479 break;
6480 }
6481 _ => {
6482 return Err(RuntimeError::syntax_error(
6483 "Expected ',' or '}}' in JSON object",
6484 ));
6485 }
6486 }
6487 }
6488 Ok(Value::Object(gc.alloc(HeapObject::Object(obj))))
6489 }
6490}
6491
6492/// Public wrapper for `json_parse` used by the fetch module.
6493pub fn json_parse_pub(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6494 json_parse(args, ctx)
6495}
6496
6497fn json_parse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6498 let text = args
6499 .first()
6500 .map(|v| v.to_js_string(ctx.gc))
6501 .unwrap_or_default();
6502 let mut parser = JsonParser::new(&text)?;
6503 let value = parser.parse_value(ctx.gc, ctx.shapes)?;
6504 // Ensure no trailing content.
6505 if parser.current.is_some() {
6506 return Err(RuntimeError::syntax_error(
6507 "Unexpected token after JSON value",
6508 ));
6509 }
6510 // TODO: reviver support — skip for now.
6511 Ok(value)
6512}
6513
6514// ── JSON.stringify ───────────────────────────────────────────
6515
6516fn json_stringify(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6517 let value = args.first().cloned().unwrap_or(Value::Undefined);
6518 let indent = match args.get(2) {
6519 Some(Value::Number(n)) => {
6520 let n = *n as usize;
6521 if n > 0 {
6522 " ".repeat(n.min(10))
6523 } else {
6524 String::new()
6525 }
6526 }
6527 Some(Value::String(s)) => s[..s.len().min(10)].to_string(),
6528 _ => String::new(),
6529 };
6530 // Track visited objects for circular reference detection.
6531 let mut visited: Vec<GcRef> = Vec::new();
6532 let result = json_stringify_value(&value, ctx.gc, ctx.shapes, &indent, "", &mut visited)?;
6533 match result {
6534 Some(s) => Ok(Value::String(s)),
6535 None => Ok(Value::Undefined),
6536 }
6537}
6538
6539fn json_stringify_value(
6540 value: &Value,
6541 gc: &Gc<HeapObject>,
6542 shapes: &ShapeTable,
6543 indent: &str,
6544 current_indent: &str,
6545 visited: &mut Vec<GcRef>,
6546) -> Result<Option<String>, RuntimeError> {
6547 match value {
6548 Value::Null => Ok(Some("null".to_string())),
6549 Value::Boolean(true) => Ok(Some("true".to_string())),
6550 Value::Boolean(false) => Ok(Some("false".to_string())),
6551 Value::Number(n) => {
6552 if n.is_nan() || n.is_infinite() {
6553 Ok(Some("null".to_string()))
6554 } else {
6555 Ok(Some(js_number_to_string(*n)))
6556 }
6557 }
6558 Value::String(s) => Ok(Some(json_quote_string(s))),
6559 Value::Undefined | Value::Function(_) => Ok(None),
6560 Value::Object(gc_ref) => {
6561 // Circular reference check.
6562 if visited.contains(gc_ref) {
6563 return Err(RuntimeError::type_error(
6564 "Converting circular structure to JSON",
6565 ));
6566 }
6567 visited.push(*gc_ref);
6568
6569 let result = match gc.get(*gc_ref) {
6570 Some(HeapObject::Object(data)) => {
6571 // Check for toJSON method.
6572 if let Some(prop) = data.get_property("toJSON", shapes) {
6573 if matches!(prop.value, Value::Function(_)) {
6574 // We can't call JS functions from here easily,
6575 // but for Date objects we recognize the __date_ms__ pattern.
6576 if let Some(date_prop) = data.get_property("__date_ms__", shapes) {
6577 let ms = date_prop.value.to_number();
6578 if ms.is_nan() {
6579 visited.pop();
6580 return Ok(Some("null".to_string()));
6581 }
6582 let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms);
6583 visited.pop();
6584 return Ok(Some(format!(
6585 "\"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z\"",
6586 y,
6587 m + 1,
6588 d,
6589 h,
6590 min,
6591 s,
6592 ms_part
6593 )));
6594 }
6595 }
6596 }
6597 if array_length_exists(gc, shapes, *gc_ref) {
6598 json_stringify_array(gc, shapes, *gc_ref, indent, current_indent, visited)
6599 } else {
6600 json_stringify_object(gc, shapes, data, indent, current_indent, visited)
6601 }
6602 }
6603 _ => Ok(Some("{}".to_string())),
6604 };
6605
6606 visited.pop();
6607 result
6608 }
6609 }
6610}
6611
6612fn json_stringify_array(
6613 gc: &Gc<HeapObject>,
6614 shapes: &ShapeTable,
6615 arr: GcRef,
6616 indent: &str,
6617 current_indent: &str,
6618 visited: &mut Vec<GcRef>,
6619) -> Result<Option<String>, RuntimeError> {
6620 let len = array_length(gc, shapes, arr);
6621 if len == 0 {
6622 return Ok(Some("[]".to_string()));
6623 }
6624 let has_indent = !indent.is_empty();
6625 let next_indent = if has_indent {
6626 format!("{}{}", current_indent, indent)
6627 } else {
6628 String::new()
6629 };
6630 let mut parts: Vec<String> = Vec::new();
6631 for i in 0..len {
6632 let val = array_get(gc, shapes, arr, i);
6633 match json_stringify_value(&val, gc, shapes, indent, &next_indent, visited)? {
6634 Some(s) => parts.push(s),
6635 None => parts.push("null".to_string()),
6636 }
6637 }
6638 if has_indent {
6639 Ok(Some(format!(
6640 "[\n{}{}\n{}]",
6641 next_indent,
6642 parts.join(&format!(",\n{}", next_indent)),
6643 current_indent
6644 )))
6645 } else {
6646 Ok(Some(format!("[{}]", parts.join(","))))
6647 }
6648}
6649
6650fn json_stringify_object(
6651 gc: &Gc<HeapObject>,
6652 shapes: &ShapeTable,
6653 data: &ObjectData,
6654 indent: &str,
6655 current_indent: &str,
6656 visited: &mut Vec<GcRef>,
6657) -> Result<Option<String>, RuntimeError> {
6658 let has_indent = !indent.is_empty();
6659 let next_indent = if has_indent {
6660 format!("{}{}", current_indent, indent)
6661 } else {
6662 String::new()
6663 };
6664 let mut parts: Vec<String> = Vec::new();
6665 // Collect and sort keys for deterministic output.
6666 let entries = data.property_entries(shapes);
6667 let mut sorted_entries: Vec<(String, Property)> = entries;
6668 sorted_entries.sort_by(|(a, _), (b, _)| a.cmp(b));
6669 for (key, prop) in &sorted_entries {
6670 if !prop.enumerable {
6671 continue;
6672 }
6673 if let Some(val_str) =
6674 json_stringify_value(&prop.value, gc, shapes, indent, &next_indent, visited)?
6675 {
6676 if has_indent {
6677 parts.push(format!("{}: {}", json_quote_string(key), val_str));
6678 } else {
6679 parts.push(format!("{}:{}", json_quote_string(key), val_str));
6680 }
6681 }
6682 }
6683 if parts.is_empty() {
6684 return Ok(Some("{}".to_string()));
6685 }
6686 if has_indent {
6687 Ok(Some(format!(
6688 "{{\n{}{}\n{}}}",
6689 next_indent,
6690 parts.join(&format!(",\n{}", next_indent)),
6691 current_indent
6692 )))
6693 } else {
6694 Ok(Some(format!("{{{}}}", parts.join(","))))
6695 }
6696}
6697
6698fn json_quote_string(s: &str) -> String {
6699 let mut out = String::with_capacity(s.len() + 2);
6700 out.push('"');
6701 for ch in s.chars() {
6702 match ch {
6703 '"' => out.push_str("\\\""),
6704 '\\' => out.push_str("\\\\"),
6705 '\n' => out.push_str("\\n"),
6706 '\r' => out.push_str("\\r"),
6707 '\t' => out.push_str("\\t"),
6708 '\u{0008}' => out.push_str("\\b"),
6709 '\u{000C}' => out.push_str("\\f"),
6710 c if c < '\u{0020}' => {
6711 out.push_str(&format!("\\u{:04x}", c as u32));
6712 }
6713 c => out.push(c),
6714 }
6715 }
6716 out.push('"');
6717 out
6718}
6719
6720// ── Console object ──────────────────────────────────────────
6721
6722fn init_console_object(vm: &mut Vm) {
6723 let mut data = ObjectData::new();
6724 if let Some(proto) = vm.object_prototype {
6725 data.prototype = Some(proto);
6726 }
6727 let console_ref = vm.gc.alloc(HeapObject::Object(data));
6728
6729 let methods: &[NativeMethod] = &[
6730 ("log", console_log),
6731 ("info", console_log),
6732 ("debug", console_log),
6733 ("error", console_error),
6734 ("warn", console_warn),
6735 ];
6736 for &(name, callback) in methods {
6737 let func = make_native(&mut vm.gc, name, callback);
6738 set_builtin_prop(
6739 &mut vm.gc,
6740 &mut vm.shapes,
6741 console_ref,
6742 name,
6743 Value::Function(func),
6744 );
6745 }
6746
6747 vm.set_global("console", Value::Object(console_ref));
6748}
6749
6750/// Format a JS value for console output with richer detail than `to_js_string`.
6751/// Arrays show their contents and objects show their properties.
6752fn console_format_value(
6753 value: &Value,
6754 gc: &Gc<HeapObject>,
6755 shapes: &ShapeTable,
6756 depth: usize,
6757 seen: &mut HashSet<GcRef>,
6758) -> String {
6759 const MAX_DEPTH: usize = 4;
6760 match value {
6761 Value::Undefined => "undefined".to_string(),
6762 Value::Null => "null".to_string(),
6763 Value::Boolean(b) => b.to_string(),
6764 Value::Number(n) => js_number_to_string(*n),
6765 Value::String(s) => s.clone(),
6766 Value::Function(gc_ref) => gc
6767 .get(*gc_ref)
6768 .and_then(|obj| match obj {
6769 HeapObject::Function(f) => Some(format!("[Function: {}]", f.name)),
6770 _ => None,
6771 })
6772 .unwrap_or_else(|| "[Function]".to_string()),
6773 Value::Object(gc_ref) => {
6774 if depth > MAX_DEPTH || seen.contains(gc_ref) {
6775 return "[Object]".to_string();
6776 }
6777 seen.insert(*gc_ref);
6778 let result = match gc.get(*gc_ref) {
6779 Some(HeapObject::Object(obj_data)) => {
6780 // Check if it's an array (has a "length" property).
6781 if obj_data.contains_key("length", shapes) {
6782 format_array(obj_data, gc, shapes, depth, seen)
6783 } else {
6784 format_object(obj_data, gc, shapes, depth, seen)
6785 }
6786 }
6787 _ => "[Object]".to_string(),
6788 };
6789 seen.remove(gc_ref);
6790 result
6791 }
6792 }
6793}
6794
6795fn format_array(
6796 data: &ObjectData,
6797 gc: &Gc<HeapObject>,
6798 shapes: &ShapeTable,
6799 depth: usize,
6800 seen: &mut HashSet<GcRef>,
6801) -> String {
6802 let len = data
6803 .get_property("length", shapes)
6804 .map(|p| p.value.to_number() as usize)
6805 .unwrap_or(0);
6806 let mut parts = Vec::with_capacity(len);
6807 for i in 0..len {
6808 let val = data
6809 .get_property(&i.to_string(), shapes)
6810 .map(|p| p.value)
6811 .unwrap_or(Value::Undefined);
6812 parts.push(console_format_value(&val, gc, shapes, depth + 1, seen));
6813 }
6814 format!("[ {} ]", parts.join(", "))
6815}
6816
6817fn format_object(
6818 data: &ObjectData,
6819 gc: &Gc<HeapObject>,
6820 shapes: &ShapeTable,
6821 depth: usize,
6822 seen: &mut HashSet<GcRef>,
6823) -> String {
6824 if data.is_empty() {
6825 return "{}".to_string();
6826 }
6827 let mut parts = Vec::new();
6828 for (key, prop) in data.property_entries(shapes) {
6829 let val_str = console_format_value(&prop.value, gc, shapes, depth + 1, seen);
6830 parts.push(format!("{}: {}", key, val_str));
6831 }
6832 parts.sort();
6833 format!("{{ {} }}", parts.join(", "))
6834}
6835
6836/// Format all arguments for a console method, separated by spaces.
6837fn console_format_args(args: &[Value], gc: &Gc<HeapObject>, shapes: &ShapeTable) -> String {
6838 let mut seen = HashSet::new();
6839 args.iter()
6840 .map(|v| console_format_value(v, gc, shapes, 0, &mut seen))
6841 .collect::<Vec<_>>()
6842 .join(" ")
6843}
6844
6845fn console_log(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6846 let msg = console_format_args(args, ctx.gc, ctx.shapes);
6847 ctx.console_output.log(&msg);
6848 Ok(Value::Undefined)
6849}
6850
6851fn console_error(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6852 let msg = console_format_args(args, ctx.gc, ctx.shapes);
6853 ctx.console_output.error(&msg);
6854 Ok(Value::Undefined)
6855}
6856
6857fn console_warn(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6858 let msg = console_format_args(args, ctx.gc, ctx.shapes);
6859 ctx.console_output.warn(&msg);
6860 Ok(Value::Undefined)
6861}
6862
6863// ── Global utility functions ─────────────────────────────────
6864
6865fn init_global_functions(vm: &mut Vm) {
6866 vm.define_native("parseInt", parse_int);
6867 vm.define_native("parseFloat", parse_float);
6868 vm.define_native("isNaN", is_nan);
6869 vm.define_native("isFinite", is_finite);
6870
6871 // Timer APIs.
6872 vm.define_native("setTimeout", crate::timers::set_timeout);
6873 vm.define_native("clearTimeout", crate::timers::clear_timeout);
6874 vm.define_native("setInterval", crate::timers::set_interval);
6875 vm.define_native("clearInterval", crate::timers::clear_interval);
6876 vm.define_native(
6877 "requestAnimationFrame",
6878 crate::timers::request_animation_frame,
6879 );
6880 vm.define_native(
6881 "cancelAnimationFrame",
6882 crate::timers::cancel_animation_frame,
6883 );
6884}
6885
6886fn parse_int(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6887 let s = args
6888 .first()
6889 .map(|v| v.to_js_string(ctx.gc))
6890 .unwrap_or_default();
6891 let radix = args.get(1).map(|v| v.to_number() as u32).unwrap_or(0);
6892 let s = s.trim();
6893 if s.is_empty() {
6894 return Ok(Value::Number(f64::NAN));
6895 }
6896 let (neg, s) = if let Some(rest) = s.strip_prefix('-') {
6897 (true, rest)
6898 } else if let Some(rest) = s.strip_prefix('+') {
6899 (false, rest)
6900 } else {
6901 (false, s)
6902 };
6903 // Auto-detect hex.
6904 let (radix, s) = if radix == 0 || radix == 16 {
6905 if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
6906 (16, rest)
6907 } else {
6908 (if radix == 0 { 10 } else { radix }, s)
6909 }
6910 } else {
6911 (radix, s)
6912 };
6913 if !(2..=36).contains(&radix) {
6914 return Ok(Value::Number(f64::NAN));
6915 }
6916 // Parse digits until invalid.
6917 let mut result: f64 = 0.0;
6918 let mut found = false;
6919 for c in s.chars() {
6920 let digit = match c.to_digit(radix) {
6921 Some(d) => d,
6922 None => break,
6923 };
6924 result = result * radix as f64 + digit as f64;
6925 found = true;
6926 }
6927 if !found {
6928 return Ok(Value::Number(f64::NAN));
6929 }
6930 if neg {
6931 result = -result;
6932 }
6933 Ok(Value::Number(result))
6934}
6935
6936fn parse_float(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6937 let s = args
6938 .first()
6939 .map(|v| v.to_js_string(ctx.gc))
6940 .unwrap_or_default();
6941 let s = s.trim();
6942 match s.parse::<f64>() {
6943 Ok(n) => Ok(Value::Number(n)),
6944 Err(_) => Ok(Value::Number(f64::NAN)),
6945 }
6946}
6947
6948fn is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6949 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
6950 Ok(Value::Boolean(n.is_nan()))
6951}
6952
6953fn is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
6954 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
6955 Ok(Value::Boolean(n.is_finite()))
6956}
6957
6958// ── Strict equality (for Array.indexOf etc.) ─────────────────
6959
6960fn strict_eq_values(a: &Value, b: &Value) -> bool {
6961 match (a, b) {
6962 (Value::Undefined, Value::Undefined) => true,
6963 (Value::Null, Value::Null) => true,
6964 (Value::Number(x), Value::Number(y)) => x == y,
6965 (Value::String(x), Value::String(y)) => x == y,
6966 (Value::Boolean(x), Value::Boolean(y)) => x == y,
6967 (Value::Object(x), Value::Object(y)) => x == y,
6968 (Value::Function(x), Value::Function(y)) => x == y,
6969 _ => false,
6970 }
6971}
6972
6973/// SameValueZero (like === but NaN === NaN is true).
6974fn same_value_zero(a: &Value, b: &Value) -> bool {
6975 match (a, b) {
6976 (Value::Number(x), Value::Number(y)) => {
6977 if x.is_nan() && y.is_nan() {
6978 return true;
6979 }
6980 x == y
6981 }
6982 _ => strict_eq_values(a, b),
6983 }
6984}
6985
6986// ── JS preamble for callback-based methods ───────────────────
6987
6988fn init_js_preamble(vm: &mut Vm) {
6989 // NOTE: All preamble methods capture `this` into a local `_t` variable
6990 // because nested method calls (e.g. result.push()) clobber the global `this`.
6991 let preamble = r#"
6992Array.prototype.forEach = function(cb) {
6993 var _t = this;
6994 for (var i = 0; i < _t.length; i = i + 1) {
6995 cb(_t[i], i, _t);
6996 }
6997};
6998Array.prototype.map = function(cb) {
6999 var _t = this;
7000 var result = [];
7001 for (var i = 0; i < _t.length; i = i + 1) {
7002 result.push(cb(_t[i], i, _t));
7003 }
7004 return result;
7005};
7006Array.prototype.filter = function(cb) {
7007 var _t = this;
7008 var result = [];
7009 for (var i = 0; i < _t.length; i = i + 1) {
7010 if (cb(_t[i], i, _t)) {
7011 result.push(_t[i]);
7012 }
7013 }
7014 return result;
7015};
7016Array.prototype.reduce = function(cb, init) {
7017 var _t = this;
7018 var acc = init;
7019 var start = 0;
7020 if (acc === undefined) {
7021 acc = _t[0];
7022 start = 1;
7023 }
7024 for (var i = start; i < _t.length; i = i + 1) {
7025 acc = cb(acc, _t[i], i, _t);
7026 }
7027 return acc;
7028};
7029Array.prototype.reduceRight = function(cb, init) {
7030 var _t = this;
7031 var acc = init;
7032 var start = _t.length - 1;
7033 if (acc === undefined) {
7034 acc = _t[start];
7035 start = start - 1;
7036 }
7037 for (var i = start; i >= 0; i = i - 1) {
7038 acc = cb(acc, _t[i], i, _t);
7039 }
7040 return acc;
7041};
7042Array.prototype.find = function(cb) {
7043 var _t = this;
7044 for (var i = 0; i < _t.length; i = i + 1) {
7045 if (cb(_t[i], i, _t)) { return _t[i]; }
7046 }
7047 return undefined;
7048};
7049Array.prototype.findIndex = function(cb) {
7050 var _t = this;
7051 for (var i = 0; i < _t.length; i = i + 1) {
7052 if (cb(_t[i], i, _t)) { return i; }
7053 }
7054 return -1;
7055};
7056Array.prototype.some = function(cb) {
7057 var _t = this;
7058 for (var i = 0; i < _t.length; i = i + 1) {
7059 if (cb(_t[i], i, _t)) { return true; }
7060 }
7061 return false;
7062};
7063Array.prototype.every = function(cb) {
7064 var _t = this;
7065 for (var i = 0; i < _t.length; i = i + 1) {
7066 if (!cb(_t[i], i, _t)) { return false; }
7067 }
7068 return true;
7069};
7070Array.prototype.sort = function(cmp) {
7071 var _t = this;
7072 var len = _t.length;
7073 for (var i = 0; i < len; i = i + 1) {
7074 for (var j = 0; j < len - i - 1; j = j + 1) {
7075 var a = _t[j];
7076 var b = _t[j + 1];
7077 var order;
7078 if (cmp) {
7079 order = cmp(a, b);
7080 } else {
7081 var sa = "" + a;
7082 var sb = "" + b;
7083 if (sa > sb) { order = 1; }
7084 else if (sa < sb) { order = -1; }
7085 else { order = 0; }
7086 }
7087 if (order > 0) {
7088 _t[j] = b;
7089 _t[j + 1] = a;
7090 }
7091 }
7092 }
7093 return _t;
7094};
7095Array.prototype.flat = function(depth) {
7096 var _t = this;
7097 if (depth === undefined) { depth = 1; }
7098 var result = [];
7099 for (var i = 0; i < _t.length; i = i + 1) {
7100 var elem = _t[i];
7101 if (depth > 0 && Array.isArray(elem)) {
7102 var sub = elem.flat(depth - 1);
7103 for (var j = 0; j < sub.length; j = j + 1) {
7104 result.push(sub[j]);
7105 }
7106 } else {
7107 result.push(elem);
7108 }
7109 }
7110 return result;
7111};
7112Array.prototype.flatMap = function(cb) {
7113 var _t = this;
7114 return _t.map(cb).flat();
7115};
7116"#;
7117 // Compile and execute the preamble.
7118 let ast = match crate::parser::Parser::parse(preamble) {
7119 Ok(ast) => ast,
7120 Err(_) => return, // Silently skip if preamble fails to parse.
7121 };
7122 let func = match crate::compiler::compile(&ast) {
7123 Ok(func) => func,
7124 Err(_) => return,
7125 };
7126 let _ = vm.execute(&func);
7127
7128 // Promise constructor and static methods (defined in JS so the executor
7129 // callback can be called as bytecode, not just native functions).
7130 let promise_preamble = r#"
7131function Promise(executor) {
7132 var p = __Promise_create();
7133 var alreadyResolved = false;
7134 function resolve(value) {
7135 if (!alreadyResolved) {
7136 alreadyResolved = true;
7137 __Promise_resolve(p, value);
7138 }
7139 }
7140 function reject(reason) {
7141 if (!alreadyResolved) {
7142 alreadyResolved = true;
7143 __Promise_reject(p, reason);
7144 }
7145 }
7146 try {
7147 executor(resolve, reject);
7148 } catch (e) {
7149 reject(e);
7150 }
7151 return p;
7152}
7153Promise.resolve = __Promise_static_resolve;
7154Promise.reject = __Promise_static_reject;
7155Promise.all = __Promise_static_all;
7156Promise.race = __Promise_static_race;
7157Promise.allSettled = __Promise_static_allSettled;
7158Promise.any = __Promise_static_any;
7159"#;
7160 let ast = match crate::parser::Parser::parse(promise_preamble) {
7161 Ok(ast) => ast,
7162 Err(_) => return,
7163 };
7164 let func = match crate::compiler::compile(&ast) {
7165 Ok(func) => func,
7166 Err(_) => return,
7167 };
7168 let _ = vm.execute(&func);
7169}