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.
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::vm::*;
10use std::collections::HashMap;
11use std::time::{SystemTime, UNIX_EPOCH};
12
13/// Native callback type alias to satisfy clippy::type_complexity.
14type NativeMethod = (
15 &'static str,
16 fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>,
17);
18
19// ── Helpers ──────────────────────────────────────────────────
20
21/// Create a native function GcRef.
22fn make_native(
23 gc: &mut Gc<HeapObject>,
24 name: &str,
25 callback: fn(&[Value], &mut NativeContext) -> Result<Value, RuntimeError>,
26) -> GcRef {
27 gc.alloc(HeapObject::Function(Box::new(FunctionData {
28 name: name.to_string(),
29 kind: FunctionKind::Native(NativeFunc { callback }),
30 prototype_obj: None,
31 properties: HashMap::new(),
32 upvalues: Vec::new(),
33 })))
34}
35
36/// Set a non-enumerable property on an object.
37fn set_builtin_prop(gc: &mut Gc<HeapObject>, obj: GcRef, key: &str, val: Value) {
38 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
39 data.properties
40 .insert(key.to_string(), Property::builtin(val));
41 }
42}
43
44/// Set a non-enumerable property on a function.
45fn set_func_prop(gc: &mut Gc<HeapObject>, func: GcRef, key: &str, val: Value) {
46 if let Some(HeapObject::Function(fdata)) = gc.get_mut(func) {
47 fdata
48 .properties
49 .insert(key.to_string(), Property::builtin(val));
50 }
51}
52
53/// Get own enumerable string keys of an object (for Object.keys, etc.).
54fn own_enumerable_keys(gc: &Gc<HeapObject>, obj_ref: GcRef) -> Vec<String> {
55 match gc.get(obj_ref) {
56 Some(HeapObject::Object(data)) => {
57 let mut int_keys: Vec<(u32, String)> = Vec::new();
58 let mut str_keys: Vec<String> = Vec::new();
59 for (k, prop) in &data.properties {
60 if prop.enumerable {
61 if let Ok(idx) = k.parse::<u32>() {
62 int_keys.push((idx, k.clone()));
63 } else {
64 str_keys.push(k.clone());
65 }
66 }
67 }
68 int_keys.sort_by_key(|(idx, _)| *idx);
69 let mut result: Vec<String> = int_keys.into_iter().map(|(_, k)| k).collect();
70 result.extend(str_keys);
71 result
72 }
73 _ => Vec::new(),
74 }
75}
76
77/// Get all own property names (enumerable or not) of an object.
78fn own_property_names(gc: &Gc<HeapObject>, obj_ref: GcRef) -> Vec<String> {
79 match gc.get(obj_ref) {
80 Some(HeapObject::Object(data)) => {
81 let mut int_keys: Vec<(u32, String)> = Vec::new();
82 let mut str_keys: Vec<String> = Vec::new();
83 for k in data.properties.keys() {
84 if let Ok(idx) = k.parse::<u32>() {
85 int_keys.push((idx, k.clone()));
86 } else {
87 str_keys.push(k.clone());
88 }
89 }
90 int_keys.sort_by_key(|(idx, _)| *idx);
91 let mut result: Vec<String> = int_keys.into_iter().map(|(_, k)| k).collect();
92 result.extend(str_keys);
93 result
94 }
95 _ => Vec::new(),
96 }
97}
98
99/// Read the "length" property of an array-like object as usize.
100fn array_length(gc: &Gc<HeapObject>, obj: GcRef) -> usize {
101 match gc.get(obj) {
102 Some(HeapObject::Object(data)) => match data.properties.get("length") {
103 Some(prop) => prop.value.to_number() as usize,
104 None => 0,
105 },
106 _ => 0,
107 }
108}
109
110/// Set the "length" property on an array-like object.
111fn set_array_length(gc: &mut Gc<HeapObject>, obj: GcRef, len: usize) {
112 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
113 if let Some(prop) = data.properties.get_mut("length") {
114 prop.value = Value::Number(len as f64);
115 } else {
116 data.properties.insert(
117 "length".to_string(),
118 Property {
119 value: Value::Number(len as f64),
120 writable: true,
121 enumerable: false,
122 configurable: false,
123 },
124 );
125 }
126 }
127}
128
129/// Check whether an object has a "length" property (i.e. is array-like).
130fn array_length_exists(gc: &Gc<HeapObject>, obj: GcRef) -> bool {
131 match gc.get(obj) {
132 Some(HeapObject::Object(data)) => data.properties.contains_key("length"),
133 _ => false,
134 }
135}
136
137/// Get an element by index from an array-like object.
138fn array_get(gc: &Gc<HeapObject>, obj: GcRef, idx: usize) -> Value {
139 match gc.get(obj) {
140 Some(HeapObject::Object(data)) => {
141 let key = idx.to_string();
142 data.properties
143 .get(&key)
144 .map(|p| p.value.clone())
145 .unwrap_or(Value::Undefined)
146 }
147 _ => Value::Undefined,
148 }
149}
150
151/// Set an element by index on an array-like object.
152fn array_set(gc: &mut Gc<HeapObject>, obj: GcRef, idx: usize, val: Value) {
153 if let Some(HeapObject::Object(data)) = gc.get_mut(obj) {
154 data.properties.insert(idx.to_string(), Property::data(val));
155 }
156}
157
158// ── Initialization ───────────────────────────────────────────
159
160/// Initialize all built-in objects and register them in the VM.
161pub fn init_builtins(vm: &mut Vm) {
162 // Create Object.prototype first (root of the prototype chain).
163 let obj_proto = vm.gc.alloc(HeapObject::Object(ObjectData::new()));
164 init_object_prototype(&mut vm.gc, obj_proto);
165
166 // Create Array.prototype (inherits from Object.prototype).
167 let mut arr_proto_data = ObjectData::new();
168 arr_proto_data.prototype = Some(obj_proto);
169 let arr_proto = vm.gc.alloc(HeapObject::Object(arr_proto_data));
170 init_array_prototype(&mut vm.gc, arr_proto);
171
172 // Create Error.prototype (inherits from Object.prototype).
173 let mut err_proto_data = ObjectData::new();
174 err_proto_data.prototype = Some(obj_proto);
175 err_proto_data.properties.insert(
176 "name".to_string(),
177 Property::builtin(Value::String("Error".to_string())),
178 );
179 err_proto_data.properties.insert(
180 "message".to_string(),
181 Property::builtin(Value::String(String::new())),
182 );
183 let err_proto = vm.gc.alloc(HeapObject::Object(err_proto_data));
184 init_error_prototype(&mut vm.gc, err_proto);
185
186 // Create String.prototype (inherits from Object.prototype).
187 let mut str_proto_data = ObjectData::new();
188 str_proto_data.prototype = Some(obj_proto);
189 let str_proto = vm.gc.alloc(HeapObject::Object(str_proto_data));
190 init_string_prototype(&mut vm.gc, str_proto);
191
192 // Create Number.prototype (inherits from Object.prototype).
193 let mut num_proto_data = ObjectData::new();
194 num_proto_data.prototype = Some(obj_proto);
195 let num_proto = vm.gc.alloc(HeapObject::Object(num_proto_data));
196 init_number_prototype(&mut vm.gc, num_proto);
197
198 // Create Boolean.prototype (inherits from Object.prototype).
199 let mut bool_proto_data = ObjectData::new();
200 bool_proto_data.prototype = Some(obj_proto);
201 let bool_proto = vm.gc.alloc(HeapObject::Object(bool_proto_data));
202 init_boolean_prototype(&mut vm.gc, bool_proto);
203
204 // Store prototypes in VM for use by CreateArray/CreateObject and auto-boxing.
205 vm.object_prototype = Some(obj_proto);
206 vm.array_prototype = Some(arr_proto);
207 vm.string_prototype = Some(str_proto);
208 vm.number_prototype = Some(num_proto);
209 vm.boolean_prototype = Some(bool_proto);
210
211 // Create and register Object constructor.
212 let obj_ctor = init_object_constructor(&mut vm.gc, obj_proto);
213 vm.set_global("Object", Value::Function(obj_ctor));
214
215 // Create and register Array constructor.
216 let arr_ctor = init_array_constructor(&mut vm.gc, arr_proto);
217 vm.set_global("Array", Value::Function(arr_ctor));
218
219 // Create and register Error constructors.
220 init_error_constructors(vm, err_proto);
221
222 // Create and register String constructor.
223 let str_ctor = init_string_constructor(&mut vm.gc, str_proto);
224 vm.set_global("String", Value::Function(str_ctor));
225
226 // Create and register Number constructor.
227 let num_ctor = init_number_constructor(&mut vm.gc, num_proto);
228 vm.set_global("Number", Value::Function(num_ctor));
229
230 // Create and register Boolean constructor.
231 let bool_ctor = init_boolean_constructor(&mut vm.gc, bool_proto);
232 vm.set_global("Boolean", Value::Function(bool_ctor));
233
234 // Create and register Symbol factory.
235 init_symbol_builtins(vm);
236
237 // Create and register Math object (static methods only, not a constructor).
238 init_math_object(vm);
239
240 // Create and register Date constructor.
241 init_date_builtins(vm);
242
243 // Create and register RegExp constructor.
244 init_regexp_builtins(vm);
245
246 // Create and register Map, Set, WeakMap, WeakSet constructors.
247 init_map_set_builtins(vm);
248
249 // Create and register JSON object (static methods only).
250 init_json_object(vm);
251
252 // Register global utility functions.
253 init_global_functions(vm);
254
255 // Execute JS preamble for callback-based methods.
256 init_js_preamble(vm);
257}
258
259// ── Object.prototype ─────────────────────────────────────────
260
261fn init_object_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
262 let has_own = make_native(gc, "hasOwnProperty", object_proto_has_own_property);
263 set_builtin_prop(gc, proto, "hasOwnProperty", Value::Function(has_own));
264
265 let to_string = make_native(gc, "toString", object_proto_to_string);
266 set_builtin_prop(gc, proto, "toString", Value::Function(to_string));
267
268 let value_of = make_native(gc, "valueOf", object_proto_value_of);
269 set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of));
270}
271
272fn object_proto_has_own_property(
273 args: &[Value],
274 ctx: &mut NativeContext,
275) -> Result<Value, RuntimeError> {
276 let key = args
277 .first()
278 .map(|v| v.to_js_string(ctx.gc))
279 .unwrap_or_default();
280 match ctx.this.gc_ref() {
281 Some(obj_ref) => match ctx.gc.get(obj_ref) {
282 Some(HeapObject::Object(data)) => {
283 Ok(Value::Boolean(data.properties.contains_key(&key)))
284 }
285 Some(HeapObject::Function(fdata)) => {
286 Ok(Value::Boolean(fdata.properties.contains_key(&key)))
287 }
288 _ => Ok(Value::Boolean(false)),
289 },
290 None => Ok(Value::Boolean(false)),
291 }
292}
293
294fn object_proto_to_string(
295 _args: &[Value],
296 _ctx: &mut NativeContext,
297) -> Result<Value, RuntimeError> {
298 Ok(Value::String("[object Object]".to_string()))
299}
300
301fn object_proto_value_of(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
302 Ok(ctx.this.clone())
303}
304
305// ── Object constructor + static methods ──────────────────────
306
307fn init_object_constructor(gc: &mut Gc<HeapObject>, obj_proto: GcRef) -> GcRef {
308 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
309 name: "Object".to_string(),
310 kind: FunctionKind::Native(NativeFunc {
311 callback: object_constructor,
312 }),
313 prototype_obj: Some(obj_proto),
314 properties: HashMap::new(),
315 upvalues: Vec::new(),
316 })));
317
318 // Static methods.
319 let keys = make_native(gc, "keys", object_keys);
320 set_func_prop(gc, ctor, "keys", Value::Function(keys));
321
322 let values = make_native(gc, "values", object_values);
323 set_func_prop(gc, ctor, "values", Value::Function(values));
324
325 let entries = make_native(gc, "entries", object_entries);
326 set_func_prop(gc, ctor, "entries", Value::Function(entries));
327
328 let assign = make_native(gc, "assign", object_assign);
329 set_func_prop(gc, ctor, "assign", Value::Function(assign));
330
331 let create = make_native(gc, "create", object_create);
332 set_func_prop(gc, ctor, "create", Value::Function(create));
333
334 let is = make_native(gc, "is", object_is);
335 set_func_prop(gc, ctor, "is", Value::Function(is));
336
337 let get_proto = make_native(gc, "getPrototypeOf", object_get_prototype_of);
338 set_func_prop(gc, ctor, "getPrototypeOf", Value::Function(get_proto));
339
340 let get_own_names = make_native(gc, "getOwnPropertyNames", object_get_own_property_names);
341 set_func_prop(
342 gc,
343 ctor,
344 "getOwnPropertyNames",
345 Value::Function(get_own_names),
346 );
347
348 let get_own_desc = make_native(gc, "getOwnPropertyDescriptor", object_get_own_prop_desc);
349 set_func_prop(
350 gc,
351 ctor,
352 "getOwnPropertyDescriptor",
353 Value::Function(get_own_desc),
354 );
355
356 let define_prop = make_native(gc, "defineProperty", object_define_property);
357 set_func_prop(gc, ctor, "defineProperty", Value::Function(define_prop));
358
359 let freeze = make_native(gc, "freeze", object_freeze);
360 set_func_prop(gc, ctor, "freeze", Value::Function(freeze));
361
362 let seal = make_native(gc, "seal", object_seal);
363 set_func_prop(gc, ctor, "seal", Value::Function(seal));
364
365 let is_frozen = make_native(gc, "isFrozen", object_is_frozen);
366 set_func_prop(gc, ctor, "isFrozen", Value::Function(is_frozen));
367
368 let is_sealed = make_native(gc, "isSealed", object_is_sealed);
369 set_func_prop(gc, ctor, "isSealed", Value::Function(is_sealed));
370
371 let from_entries = make_native(gc, "fromEntries", object_from_entries);
372 set_func_prop(gc, ctor, "fromEntries", Value::Function(from_entries));
373
374 let has_own = make_native(gc, "hasOwn", object_has_own);
375 set_func_prop(gc, ctor, "hasOwn", Value::Function(has_own));
376
377 ctor
378}
379
380fn object_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
381 match args.first() {
382 Some(Value::Object(_)) | Some(Value::Function(_)) => Ok(args[0].clone()),
383 Some(Value::Null) | Some(Value::Undefined) | None => {
384 let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new()));
385 Ok(Value::Object(obj))
386 }
387 _ => {
388 // Primitive wrapping (simplified): return a new object.
389 let obj = ctx.gc.alloc(HeapObject::Object(ObjectData::new()));
390 Ok(Value::Object(obj))
391 }
392 }
393}
394
395fn object_keys(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
396 let obj_ref = match args.first() {
397 Some(Value::Object(r)) | Some(Value::Function(r)) => *r,
398 _ => return Err(RuntimeError::type_error("Object.keys requires an object")),
399 };
400 let keys = own_enumerable_keys(ctx.gc, obj_ref);
401 Ok(make_string_array(ctx.gc, &keys))
402}
403
404fn object_values(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
405 let obj_ref = match args.first() {
406 Some(Value::Object(r)) => *r,
407 _ => return Err(RuntimeError::type_error("Object.values requires an object")),
408 };
409 let keys = own_enumerable_keys(ctx.gc, obj_ref);
410 let values: Vec<Value> = keys
411 .iter()
412 .map(|k| {
413 ctx.gc
414 .get(obj_ref)
415 .and_then(|ho| match ho {
416 HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()),
417 _ => None,
418 })
419 .unwrap_or(Value::Undefined)
420 })
421 .collect();
422 Ok(make_value_array(ctx.gc, &values))
423}
424
425fn object_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
426 let obj_ref = match args.first() {
427 Some(Value::Object(r)) => *r,
428 _ => {
429 return Err(RuntimeError::type_error(
430 "Object.entries requires an object",
431 ))
432 }
433 };
434 let keys = own_enumerable_keys(ctx.gc, obj_ref);
435 let mut entries = Vec::new();
436 for k in &keys {
437 let val = ctx
438 .gc
439 .get(obj_ref)
440 .and_then(|ho| match ho {
441 HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()),
442 _ => None,
443 })
444 .unwrap_or(Value::Undefined);
445 // Create a [key, value] pair array.
446 let pair = make_value_array(ctx.gc, &[Value::String(k.clone()), val]);
447 entries.push(pair);
448 }
449 Ok(make_value_array(ctx.gc, &entries))
450}
451
452fn object_assign(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
453 let target_ref = match args.first() {
454 Some(Value::Object(r)) => *r,
455 _ => {
456 return Err(RuntimeError::type_error(
457 "Object.assign requires an object target",
458 ))
459 }
460 };
461 for source in args.iter().skip(1) {
462 let src_ref = match source {
463 Value::Object(r) => *r,
464 Value::Null | Value::Undefined => continue,
465 _ => continue,
466 };
467 let keys = own_enumerable_keys(ctx.gc, src_ref);
468 for k in &keys {
469 let val = ctx
470 .gc
471 .get(src_ref)
472 .and_then(|ho| match ho {
473 HeapObject::Object(data) => data.properties.get(k).map(|p| p.value.clone()),
474 _ => None,
475 })
476 .unwrap_or(Value::Undefined);
477 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(target_ref) {
478 data.properties.insert(k.clone(), Property::data(val));
479 }
480 }
481 }
482 Ok(Value::Object(target_ref))
483}
484
485fn object_create(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
486 let proto = match args.first() {
487 Some(Value::Object(r)) => Some(*r),
488 Some(Value::Null) => None,
489 _ => {
490 return Err(RuntimeError::type_error(
491 "Object.create prototype must be an object or null",
492 ))
493 }
494 };
495 let mut obj = ObjectData::new();
496 obj.prototype = proto;
497 let gc_ref = ctx.gc.alloc(HeapObject::Object(obj));
498 Ok(Value::Object(gc_ref))
499}
500
501fn object_is(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
502 let x = args.first().cloned().unwrap_or(Value::Undefined);
503 let y = args.get(1).cloned().unwrap_or(Value::Undefined);
504 Ok(Value::Boolean(same_value(&x, &y)))
505}
506
507/// SameValue algorithm (ECMA-262 §7.2.10).
508fn same_value(x: &Value, y: &Value) -> bool {
509 match (x, y) {
510 (Value::Number(a), Value::Number(b)) => {
511 if a.is_nan() && b.is_nan() {
512 return true;
513 }
514 if *a == 0.0 && *b == 0.0 {
515 return a.is_sign_positive() == b.is_sign_positive();
516 }
517 a == b
518 }
519 (Value::Undefined, Value::Undefined) => true,
520 (Value::Null, Value::Null) => true,
521 (Value::Boolean(a), Value::Boolean(b)) => a == b,
522 (Value::String(a), Value::String(b)) => a == b,
523 (Value::Object(a), Value::Object(b)) => a == b,
524 (Value::Function(a), Value::Function(b)) => a == b,
525 _ => false,
526 }
527}
528
529fn object_get_prototype_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
530 let obj_ref = match args.first() {
531 Some(Value::Object(r)) => *r,
532 _ => {
533 return Err(RuntimeError::type_error(
534 "Object.getPrototypeOf requires an object",
535 ))
536 }
537 };
538 match ctx.gc.get(obj_ref) {
539 Some(HeapObject::Object(data)) => match data.prototype {
540 Some(proto) => Ok(Value::Object(proto)),
541 None => Ok(Value::Null),
542 },
543 _ => Ok(Value::Null),
544 }
545}
546
547fn object_get_own_property_names(
548 args: &[Value],
549 ctx: &mut NativeContext,
550) -> Result<Value, RuntimeError> {
551 let obj_ref = match args.first() {
552 Some(Value::Object(r)) | Some(Value::Function(r)) => *r,
553 _ => {
554 return Err(RuntimeError::type_error(
555 "Object.getOwnPropertyNames requires an object",
556 ))
557 }
558 };
559 let names = own_property_names(ctx.gc, obj_ref);
560 Ok(make_string_array(ctx.gc, &names))
561}
562
563fn object_get_own_prop_desc(
564 args: &[Value],
565 ctx: &mut NativeContext,
566) -> Result<Value, RuntimeError> {
567 let obj_ref = match args.first() {
568 Some(Value::Object(r)) => *r,
569 _ => return Ok(Value::Undefined),
570 };
571 let key = args
572 .get(1)
573 .map(|v| v.to_js_string(ctx.gc))
574 .unwrap_or_default();
575 let prop = match ctx.gc.get(obj_ref) {
576 Some(HeapObject::Object(data)) => data.properties.get(&key).cloned(),
577 _ => None,
578 };
579 match prop {
580 Some(p) => {
581 let mut desc = ObjectData::new();
582 desc.properties
583 .insert("value".to_string(), Property::data(p.value));
584 desc.properties.insert(
585 "writable".to_string(),
586 Property::data(Value::Boolean(p.writable)),
587 );
588 desc.properties.insert(
589 "enumerable".to_string(),
590 Property::data(Value::Boolean(p.enumerable)),
591 );
592 desc.properties.insert(
593 "configurable".to_string(),
594 Property::data(Value::Boolean(p.configurable)),
595 );
596 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(desc))))
597 }
598 None => Ok(Value::Undefined),
599 }
600}
601
602fn object_define_property(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
603 let obj_ref = match args.first() {
604 Some(Value::Object(r)) => *r,
605 _ => {
606 return Err(RuntimeError::type_error(
607 "Object.defineProperty requires an object",
608 ))
609 }
610 };
611 let key = args
612 .get(1)
613 .map(|v| v.to_js_string(ctx.gc))
614 .unwrap_or_default();
615 let desc_ref = match args.get(2) {
616 Some(Value::Object(r)) => *r,
617 _ => {
618 return Err(RuntimeError::type_error(
619 "Property descriptor must be an object",
620 ))
621 }
622 };
623
624 // Read descriptor properties.
625 let (value, writable, enumerable, configurable) = {
626 match ctx.gc.get(desc_ref) {
627 Some(HeapObject::Object(desc_data)) => {
628 let value = desc_data
629 .properties
630 .get("value")
631 .map(|p| p.value.clone())
632 .unwrap_or(Value::Undefined);
633 let writable = desc_data
634 .properties
635 .get("writable")
636 .map(|p| p.value.to_boolean())
637 .unwrap_or(false);
638 let enumerable = desc_data
639 .properties
640 .get("enumerable")
641 .map(|p| p.value.to_boolean())
642 .unwrap_or(false);
643 let configurable = desc_data
644 .properties
645 .get("configurable")
646 .map(|p| p.value.to_boolean())
647 .unwrap_or(false);
648 (value, writable, enumerable, configurable)
649 }
650 _ => (Value::Undefined, false, false, false),
651 }
652 };
653
654 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
655 data.properties.insert(
656 key,
657 Property {
658 value,
659 writable,
660 enumerable,
661 configurable,
662 },
663 );
664 }
665 Ok(Value::Object(obj_ref))
666}
667
668fn object_freeze(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
669 let obj_ref = match args.first() {
670 Some(Value::Object(r)) => *r,
671 Some(other) => return Ok(other.clone()),
672 None => return Ok(Value::Undefined),
673 };
674 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
675 data.extensible = false;
676 for prop in data.properties.values_mut() {
677 prop.writable = false;
678 prop.configurable = false;
679 }
680 }
681 Ok(Value::Object(obj_ref))
682}
683
684fn object_seal(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
685 let obj_ref = match args.first() {
686 Some(Value::Object(r)) => *r,
687 Some(other) => return Ok(other.clone()),
688 None => return Ok(Value::Undefined),
689 };
690 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
691 data.extensible = false;
692 for prop in data.properties.values_mut() {
693 prop.configurable = false;
694 }
695 }
696 Ok(Value::Object(obj_ref))
697}
698
699fn object_is_frozen(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
700 let obj_ref = match args.first() {
701 Some(Value::Object(r)) => *r,
702 _ => return Ok(Value::Boolean(true)),
703 };
704 match ctx.gc.get(obj_ref) {
705 Some(HeapObject::Object(data)) => {
706 if data.extensible {
707 return Ok(Value::Boolean(false));
708 }
709 let frozen = data
710 .properties
711 .values()
712 .all(|p| !p.writable && !p.configurable);
713 Ok(Value::Boolean(frozen))
714 }
715 _ => Ok(Value::Boolean(true)),
716 }
717}
718
719fn object_is_sealed(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
720 let obj_ref = match args.first() {
721 Some(Value::Object(r)) => *r,
722 _ => return Ok(Value::Boolean(true)),
723 };
724 match ctx.gc.get(obj_ref) {
725 Some(HeapObject::Object(data)) => {
726 if data.extensible {
727 return Ok(Value::Boolean(false));
728 }
729 let sealed = data.properties.values().all(|p| !p.configurable);
730 Ok(Value::Boolean(sealed))
731 }
732 _ => Ok(Value::Boolean(true)),
733 }
734}
735
736fn object_from_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
737 let arr_ref = match args.first() {
738 Some(Value::Object(r)) => *r,
739 _ => {
740 return Err(RuntimeError::type_error(
741 "Object.fromEntries requires an iterable",
742 ))
743 }
744 };
745 let len = array_length(ctx.gc, arr_ref);
746 let mut obj = ObjectData::new();
747 for i in 0..len {
748 let pair_val = array_get(ctx.gc, arr_ref, i);
749 if let Value::Object(pair_ref) = pair_val {
750 let key = array_get(ctx.gc, pair_ref, 0);
751 let val = array_get(ctx.gc, pair_ref, 1);
752 let key_str = key.to_js_string(ctx.gc);
753 obj.properties.insert(key_str, Property::data(val));
754 }
755 }
756 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))))
757}
758
759fn object_has_own(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
760 let obj_ref = match args.first() {
761 Some(Value::Object(r)) => *r,
762 _ => return Ok(Value::Boolean(false)),
763 };
764 let key = args
765 .get(1)
766 .map(|v| v.to_js_string(ctx.gc))
767 .unwrap_or_default();
768 match ctx.gc.get(obj_ref) {
769 Some(HeapObject::Object(data)) => Ok(Value::Boolean(data.properties.contains_key(&key))),
770 _ => Ok(Value::Boolean(false)),
771 }
772}
773
774// ── Array helpers ────────────────────────────────────────────
775
776/// Create a JS array from a slice of string values.
777fn make_string_array(gc: &mut Gc<HeapObject>, items: &[String]) -> Value {
778 let mut obj = ObjectData::new();
779 for (i, s) in items.iter().enumerate() {
780 obj.properties
781 .insert(i.to_string(), Property::data(Value::String(s.clone())));
782 }
783 obj.properties.insert(
784 "length".to_string(),
785 Property {
786 value: Value::Number(items.len() as f64),
787 writable: true,
788 enumerable: false,
789 configurable: false,
790 },
791 );
792 Value::Object(gc.alloc(HeapObject::Object(obj)))
793}
794
795/// Create a JS array from a slice of Values.
796fn make_value_array(gc: &mut Gc<HeapObject>, items: &[Value]) -> Value {
797 let mut obj = ObjectData::new();
798 for (i, v) in items.iter().enumerate() {
799 obj.properties
800 .insert(i.to_string(), Property::data(v.clone()));
801 }
802 obj.properties.insert(
803 "length".to_string(),
804 Property {
805 value: Value::Number(items.len() as f64),
806 writable: true,
807 enumerable: false,
808 configurable: false,
809 },
810 );
811 Value::Object(gc.alloc(HeapObject::Object(obj)))
812}
813
814// ── Array.prototype ──────────────────────────────────────────
815
816fn init_array_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
817 let push = make_native(gc, "push", array_push);
818 set_builtin_prop(gc, proto, "push", Value::Function(push));
819
820 let pop = make_native(gc, "pop", array_pop);
821 set_builtin_prop(gc, proto, "pop", Value::Function(pop));
822
823 let shift = make_native(gc, "shift", array_shift);
824 set_builtin_prop(gc, proto, "shift", Value::Function(shift));
825
826 let unshift = make_native(gc, "unshift", array_unshift);
827 set_builtin_prop(gc, proto, "unshift", Value::Function(unshift));
828
829 let index_of = make_native(gc, "indexOf", array_index_of);
830 set_builtin_prop(gc, proto, "indexOf", Value::Function(index_of));
831
832 let last_index_of = make_native(gc, "lastIndexOf", array_last_index_of);
833 set_builtin_prop(gc, proto, "lastIndexOf", Value::Function(last_index_of));
834
835 let includes = make_native(gc, "includes", array_includes);
836 set_builtin_prop(gc, proto, "includes", Value::Function(includes));
837
838 let join = make_native(gc, "join", array_join);
839 set_builtin_prop(gc, proto, "join", Value::Function(join));
840
841 let slice = make_native(gc, "slice", array_slice);
842 set_builtin_prop(gc, proto, "slice", Value::Function(slice));
843
844 let concat = make_native(gc, "concat", array_concat);
845 set_builtin_prop(gc, proto, "concat", Value::Function(concat));
846
847 let reverse = make_native(gc, "reverse", array_reverse);
848 set_builtin_prop(gc, proto, "reverse", Value::Function(reverse));
849
850 let splice = make_native(gc, "splice", array_splice);
851 set_builtin_prop(gc, proto, "splice", Value::Function(splice));
852
853 let fill = make_native(gc, "fill", array_fill);
854 set_builtin_prop(gc, proto, "fill", Value::Function(fill));
855
856 let to_string = make_native(gc, "toString", array_to_string);
857 set_builtin_prop(gc, proto, "toString", Value::Function(to_string));
858
859 let at = make_native(gc, "at", array_at);
860 set_builtin_prop(gc, proto, "at", Value::Function(at));
861}
862
863fn array_push(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
864 let obj_ref = match ctx.this.gc_ref() {
865 Some(r) => r,
866 None => return Err(RuntimeError::type_error("push called on non-object")),
867 };
868 let mut len = array_length(ctx.gc, obj_ref);
869 for val in args {
870 array_set(ctx.gc, obj_ref, len, val.clone());
871 len += 1;
872 }
873 set_array_length(ctx.gc, obj_ref, len);
874 Ok(Value::Number(len as f64))
875}
876
877fn array_pop(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
878 let _ = args;
879 let obj_ref = match ctx.this.gc_ref() {
880 Some(r) => r,
881 None => return Err(RuntimeError::type_error("pop called on non-object")),
882 };
883 let len = array_length(ctx.gc, obj_ref);
884 if len == 0 {
885 return Ok(Value::Undefined);
886 }
887 let val = array_get(ctx.gc, obj_ref, len - 1);
888 // Remove the last element.
889 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
890 data.properties.remove(&(len - 1).to_string());
891 }
892 set_array_length(ctx.gc, obj_ref, len - 1);
893 Ok(val)
894}
895
896fn array_shift(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
897 let _ = args;
898 let obj_ref = match ctx.this.gc_ref() {
899 Some(r) => r,
900 None => return Err(RuntimeError::type_error("shift called on non-object")),
901 };
902 let len = array_length(ctx.gc, obj_ref);
903 if len == 0 {
904 return Ok(Value::Undefined);
905 }
906 let first = array_get(ctx.gc, obj_ref, 0);
907 // Shift all elements down.
908 let mut vals = Vec::with_capacity(len - 1);
909 for i in 1..len {
910 vals.push(array_get(ctx.gc, obj_ref, i));
911 }
912 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
913 // Remove all numeric keys.
914 for i in 0..len {
915 data.properties.remove(&i.to_string());
916 }
917 // Re-insert shifted values.
918 for (i, v) in vals.into_iter().enumerate() {
919 data.properties.insert(i.to_string(), Property::data(v));
920 }
921 }
922 set_array_length(ctx.gc, obj_ref, len - 1);
923 Ok(first)
924}
925
926fn array_unshift(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
927 let obj_ref = match ctx.this.gc_ref() {
928 Some(r) => r,
929 None => return Err(RuntimeError::type_error("unshift called on non-object")),
930 };
931 let len = array_length(ctx.gc, obj_ref);
932 let insert_count = args.len();
933 // Read existing values.
934 let mut existing = Vec::with_capacity(len);
935 for i in 0..len {
936 existing.push(array_get(ctx.gc, obj_ref, i));
937 }
938 // Write new values at the start, then existing values after.
939 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
940 for i in 0..len {
941 data.properties.remove(&i.to_string());
942 }
943 for (i, v) in args.iter().enumerate() {
944 data.properties
945 .insert(i.to_string(), Property::data(v.clone()));
946 }
947 for (i, v) in existing.into_iter().enumerate() {
948 data.properties
949 .insert((i + insert_count).to_string(), Property::data(v));
950 }
951 }
952 let new_len = len + insert_count;
953 set_array_length(ctx.gc, obj_ref, new_len);
954 Ok(Value::Number(new_len as f64))
955}
956
957fn array_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
958 let obj_ref = match ctx.this.gc_ref() {
959 Some(r) => r,
960 None => return Ok(Value::Number(-1.0)),
961 };
962 let search = args.first().cloned().unwrap_or(Value::Undefined);
963 let from = args
964 .get(1)
965 .map(|v| {
966 let n = v.to_number() as i64;
967 let len = array_length(ctx.gc, obj_ref) as i64;
968 if n < 0 {
969 (len + n).max(0) as usize
970 } else {
971 n as usize
972 }
973 })
974 .unwrap_or(0);
975 let len = array_length(ctx.gc, obj_ref);
976 for i in from..len {
977 let elem = array_get(ctx.gc, obj_ref, i);
978 if strict_eq_values(&elem, &search) {
979 return Ok(Value::Number(i as f64));
980 }
981 }
982 Ok(Value::Number(-1.0))
983}
984
985fn array_last_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
986 let obj_ref = match ctx.this.gc_ref() {
987 Some(r) => r,
988 None => return Ok(Value::Number(-1.0)),
989 };
990 let search = args.first().cloned().unwrap_or(Value::Undefined);
991 let len = array_length(ctx.gc, obj_ref);
992 if len == 0 {
993 return Ok(Value::Number(-1.0));
994 }
995 let from = args
996 .get(1)
997 .map(|v| {
998 let n = v.to_number() as i64;
999 if n < 0 {
1000 (len as i64 + n) as usize
1001 } else {
1002 (n as usize).min(len - 1)
1003 }
1004 })
1005 .unwrap_or(len - 1);
1006 for i in (0..=from).rev() {
1007 let elem = array_get(ctx.gc, obj_ref, i);
1008 if strict_eq_values(&elem, &search) {
1009 return Ok(Value::Number(i as f64));
1010 }
1011 }
1012 Ok(Value::Number(-1.0))
1013}
1014
1015fn array_includes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1016 let obj_ref = match ctx.this.gc_ref() {
1017 Some(r) => r,
1018 None => return Ok(Value::Boolean(false)),
1019 };
1020 let search = args.first().cloned().unwrap_or(Value::Undefined);
1021 let len = array_length(ctx.gc, obj_ref);
1022 let from = args
1023 .get(1)
1024 .map(|v| {
1025 let n = v.to_number() as i64;
1026 if n < 0 {
1027 (len as i64 + n).max(0) as usize
1028 } else {
1029 n as usize
1030 }
1031 })
1032 .unwrap_or(0);
1033 for i in from..len {
1034 let elem = array_get(ctx.gc, obj_ref, i);
1035 // includes uses SameValueZero (like === but NaN === NaN).
1036 if same_value_zero(&elem, &search) {
1037 return Ok(Value::Boolean(true));
1038 }
1039 }
1040 Ok(Value::Boolean(false))
1041}
1042
1043fn array_join(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1044 let obj_ref = match ctx.this.gc_ref() {
1045 Some(r) => r,
1046 None => return Ok(Value::String(String::new())),
1047 };
1048 let sep = args
1049 .first()
1050 .map(|v| {
1051 if matches!(v, Value::Undefined) {
1052 ",".to_string()
1053 } else {
1054 v.to_js_string(ctx.gc)
1055 }
1056 })
1057 .unwrap_or_else(|| ",".to_string());
1058 let len = array_length(ctx.gc, obj_ref);
1059 let mut parts = Vec::with_capacity(len);
1060 for i in 0..len {
1061 let elem = array_get(ctx.gc, obj_ref, i);
1062 if matches!(elem, Value::Undefined | Value::Null) {
1063 parts.push(String::new());
1064 } else {
1065 parts.push(elem.to_js_string(ctx.gc));
1066 }
1067 }
1068 Ok(Value::String(parts.join(&sep)))
1069}
1070
1071fn array_slice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1072 let obj_ref = match ctx.this.gc_ref() {
1073 Some(r) => r,
1074 None => return Ok(Value::Undefined),
1075 };
1076 let len = array_length(ctx.gc, obj_ref) as i64;
1077 let start = args
1078 .first()
1079 .map(|v| {
1080 let n = v.to_number() as i64;
1081 if n < 0 {
1082 (len + n).max(0)
1083 } else {
1084 n.min(len)
1085 }
1086 })
1087 .unwrap_or(0) as usize;
1088 let end = args
1089 .get(1)
1090 .map(|v| {
1091 if matches!(v, Value::Undefined) {
1092 len
1093 } else {
1094 let n = v.to_number() as i64;
1095 if n < 0 {
1096 (len + n).max(0)
1097 } else {
1098 n.min(len)
1099 }
1100 }
1101 })
1102 .unwrap_or(len) as usize;
1103
1104 let mut items = Vec::new();
1105 for i in start..end {
1106 items.push(array_get(ctx.gc, obj_ref, i));
1107 }
1108 Ok(make_value_array(ctx.gc, &items))
1109}
1110
1111fn array_concat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1112 let obj_ref = match ctx.this.gc_ref() {
1113 Some(r) => r,
1114 None => return Ok(Value::Undefined),
1115 };
1116 let mut items = Vec::new();
1117 // First, add elements from `this`.
1118 let len = array_length(ctx.gc, obj_ref);
1119 for i in 0..len {
1120 items.push(array_get(ctx.gc, obj_ref, i));
1121 }
1122 // Then add from each argument.
1123 for arg in args {
1124 match arg {
1125 Value::Object(r) => {
1126 let arg_len = array_length(ctx.gc, *r);
1127 // Only spread array-like objects (those with a length property).
1128 if array_length_exists(ctx.gc, *r) {
1129 for i in 0..arg_len {
1130 items.push(array_get(ctx.gc, *r, i));
1131 }
1132 } else {
1133 items.push(arg.clone());
1134 }
1135 }
1136 _ => items.push(arg.clone()),
1137 }
1138 }
1139 Ok(make_value_array(ctx.gc, &items))
1140}
1141
1142fn array_reverse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1143 let _ = args;
1144 let obj_ref = match ctx.this.gc_ref() {
1145 Some(r) => r,
1146 None => return Ok(Value::Undefined),
1147 };
1148 let len = array_length(ctx.gc, obj_ref);
1149 // Read all values.
1150 let mut vals: Vec<Value> = (0..len).map(|i| array_get(ctx.gc, obj_ref, i)).collect();
1151 vals.reverse();
1152 // Write back.
1153 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
1154 for (i, v) in vals.into_iter().enumerate() {
1155 data.properties.insert(i.to_string(), Property::data(v));
1156 }
1157 }
1158 Ok(ctx.this.clone())
1159}
1160
1161fn array_splice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1162 let obj_ref = match ctx.this.gc_ref() {
1163 Some(r) => r,
1164 None => return Ok(Value::Undefined),
1165 };
1166 let len = array_length(ctx.gc, obj_ref) as i64;
1167 let start = args
1168 .first()
1169 .map(|v| {
1170 let n = v.to_number() as i64;
1171 if n < 0 {
1172 (len + n).max(0)
1173 } else {
1174 n.min(len)
1175 }
1176 })
1177 .unwrap_or(0) as usize;
1178 let delete_count = args
1179 .get(1)
1180 .map(|v| {
1181 let n = v.to_number() as i64;
1182 n.max(0).min(len - start as i64) as usize
1183 })
1184 .unwrap_or((len - start as i64).max(0) as usize);
1185 let insert_items: Vec<Value> = args.iter().skip(2).cloned().collect();
1186
1187 // Collect current values.
1188 let all_vals: Vec<Value> = (0..len as usize)
1189 .map(|i| array_get(ctx.gc, obj_ref, i))
1190 .collect();
1191
1192 // Build removed slice.
1193 let removed: Vec<Value> = all_vals[start..start + delete_count].to_vec();
1194
1195 // Build new array content.
1196 let mut new_vals = Vec::new();
1197 new_vals.extend_from_slice(&all_vals[..start]);
1198 new_vals.extend(insert_items);
1199 new_vals.extend_from_slice(&all_vals[start + delete_count..]);
1200
1201 // Write back.
1202 if let Some(HeapObject::Object(data)) = ctx.gc.get_mut(obj_ref) {
1203 // Remove all numeric keys.
1204 let old_keys: Vec<String> = data
1205 .properties
1206 .keys()
1207 .filter(|k| k.parse::<usize>().is_ok())
1208 .cloned()
1209 .collect();
1210 for k in old_keys {
1211 data.properties.remove(&k);
1212 }
1213 // Write new values.
1214 for (i, v) in new_vals.iter().enumerate() {
1215 data.properties
1216 .insert(i.to_string(), Property::data(v.clone()));
1217 }
1218 }
1219 set_array_length(ctx.gc, obj_ref, new_vals.len());
1220 Ok(make_value_array(ctx.gc, &removed))
1221}
1222
1223fn array_fill(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1224 let obj_ref = match ctx.this.gc_ref() {
1225 Some(r) => r,
1226 None => return Ok(Value::Undefined),
1227 };
1228 let val = args.first().cloned().unwrap_or(Value::Undefined);
1229 let len = array_length(ctx.gc, obj_ref) as i64;
1230 let start = args
1231 .get(1)
1232 .map(|v| {
1233 let n = v.to_number() as i64;
1234 if n < 0 {
1235 (len + n).max(0)
1236 } else {
1237 n.min(len)
1238 }
1239 })
1240 .unwrap_or(0) as usize;
1241 let end = args
1242 .get(2)
1243 .map(|v| {
1244 if matches!(v, Value::Undefined) {
1245 len
1246 } else {
1247 let n = v.to_number() as i64;
1248 if n < 0 {
1249 (len + n).max(0)
1250 } else {
1251 n.min(len)
1252 }
1253 }
1254 })
1255 .unwrap_or(len) as usize;
1256 for i in start..end {
1257 array_set(ctx.gc, obj_ref, i, val.clone());
1258 }
1259 Ok(ctx.this.clone())
1260}
1261
1262fn array_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1263 // Array.prototype.toString is the same as join(",").
1264 array_join(
1265 &[Value::String(",".to_string())],
1266 &mut NativeContext {
1267 gc: ctx.gc,
1268 this: ctx.this.clone(),
1269 },
1270 )
1271 .or_else(|_| Ok(Value::String(String::new())))
1272}
1273
1274fn array_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1275 let obj_ref = match ctx.this.gc_ref() {
1276 Some(r) => r,
1277 None => return Ok(Value::Undefined),
1278 };
1279 let len = array_length(ctx.gc, obj_ref) as i64;
1280 let index = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1281 let actual = if index < 0 { len + index } else { index };
1282 if actual < 0 || actual >= len {
1283 Ok(Value::Undefined)
1284 } else {
1285 Ok(array_get(ctx.gc, obj_ref, actual as usize))
1286 }
1287}
1288
1289// ── Array constructor + static methods ───────────────────────
1290
1291fn init_array_constructor(gc: &mut Gc<HeapObject>, arr_proto: GcRef) -> GcRef {
1292 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1293 name: "Array".to_string(),
1294 kind: FunctionKind::Native(NativeFunc {
1295 callback: array_constructor,
1296 }),
1297 prototype_obj: Some(arr_proto),
1298 properties: HashMap::new(),
1299 upvalues: Vec::new(),
1300 })));
1301
1302 let is_array = make_native(gc, "isArray", array_is_array);
1303 set_func_prop(gc, ctor, "isArray", Value::Function(is_array));
1304
1305 let from = make_native(gc, "from", array_from);
1306 set_func_prop(gc, ctor, "from", Value::Function(from));
1307
1308 let of = make_native(gc, "of", array_of);
1309 set_func_prop(gc, ctor, "of", Value::Function(of));
1310
1311 ctor
1312}
1313
1314fn array_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1315 if args.len() == 1 {
1316 if let Value::Number(n) = &args[0] {
1317 let len = *n as usize;
1318 let mut obj = ObjectData::new();
1319 obj.properties.insert(
1320 "length".to_string(),
1321 Property {
1322 value: Value::Number(len as f64),
1323 writable: true,
1324 enumerable: false,
1325 configurable: false,
1326 },
1327 );
1328 return Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))));
1329 }
1330 }
1331 Ok(make_value_array(ctx.gc, args))
1332}
1333
1334fn array_is_array(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1335 // An "array" is an object that has a numeric length property.
1336 // This is a simplified check — real JS uses an internal [[Class]] slot.
1337 match args.first() {
1338 Some(Value::Object(r)) => match ctx.gc.get(*r) {
1339 Some(HeapObject::Object(data)) => {
1340 Ok(Value::Boolean(data.properties.contains_key("length")))
1341 }
1342 _ => Ok(Value::Boolean(false)),
1343 },
1344 _ => Ok(Value::Boolean(false)),
1345 }
1346}
1347
1348fn array_from(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1349 let iterable = args.first().cloned().unwrap_or(Value::Undefined);
1350 match iterable {
1351 Value::Object(r) => {
1352 let len = array_length(ctx.gc, r);
1353 let mut items = Vec::with_capacity(len);
1354 for i in 0..len {
1355 items.push(array_get(ctx.gc, r, i));
1356 }
1357 Ok(make_value_array(ctx.gc, &items))
1358 }
1359 Value::String(s) => {
1360 let chars: Vec<Value> = s.chars().map(|c| Value::String(c.to_string())).collect();
1361 Ok(make_value_array(ctx.gc, &chars))
1362 }
1363 _ => Ok(make_value_array(ctx.gc, &[])),
1364 }
1365}
1366
1367fn array_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1368 Ok(make_value_array(ctx.gc, args))
1369}
1370
1371// ── Error constructors ───────────────────────────────────────
1372
1373fn init_error_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
1374 let to_string = make_native(gc, "toString", error_proto_to_string);
1375 set_builtin_prop(gc, proto, "toString", Value::Function(to_string));
1376}
1377
1378fn init_error_constructors(vm: &mut Vm, err_proto: GcRef) {
1379 // Base Error.
1380 let error_ctor = make_error_constructor(&mut vm.gc, "Error", err_proto);
1381 vm.set_global("Error", Value::Function(error_ctor));
1382
1383 // TypeError.
1384 let te_proto = make_error_subclass_proto(&mut vm.gc, "TypeError", err_proto);
1385 let te_ctor = make_error_constructor(&mut vm.gc, "TypeError", te_proto);
1386 vm.set_global("TypeError", Value::Function(te_ctor));
1387
1388 // ReferenceError.
1389 let re_proto = make_error_subclass_proto(&mut vm.gc, "ReferenceError", err_proto);
1390 let re_ctor = make_error_constructor(&mut vm.gc, "ReferenceError", re_proto);
1391 vm.set_global("ReferenceError", Value::Function(re_ctor));
1392
1393 // SyntaxError.
1394 let se_proto = make_error_subclass_proto(&mut vm.gc, "SyntaxError", err_proto);
1395 let se_ctor = make_error_constructor(&mut vm.gc, "SyntaxError", se_proto);
1396 vm.set_global("SyntaxError", Value::Function(se_ctor));
1397
1398 // RangeError.
1399 let rae_proto = make_error_subclass_proto(&mut vm.gc, "RangeError", err_proto);
1400 let rae_ctor = make_error_constructor(&mut vm.gc, "RangeError", rae_proto);
1401 vm.set_global("RangeError", Value::Function(rae_ctor));
1402
1403 // URIError.
1404 let ue_proto = make_error_subclass_proto(&mut vm.gc, "URIError", err_proto);
1405 let ue_ctor = make_error_constructor(&mut vm.gc, "URIError", ue_proto);
1406 vm.set_global("URIError", Value::Function(ue_ctor));
1407
1408 // EvalError.
1409 let ee_proto = make_error_subclass_proto(&mut vm.gc, "EvalError", err_proto);
1410 let ee_ctor = make_error_constructor(&mut vm.gc, "EvalError", ee_proto);
1411 vm.set_global("EvalError", Value::Function(ee_ctor));
1412}
1413
1414fn make_error_subclass_proto(gc: &mut Gc<HeapObject>, name: &str, parent_proto: GcRef) -> GcRef {
1415 let mut data = ObjectData::new();
1416 data.prototype = Some(parent_proto);
1417 data.properties.insert(
1418 "name".to_string(),
1419 Property::builtin(Value::String(name.to_string())),
1420 );
1421 data.properties.insert(
1422 "message".to_string(),
1423 Property::builtin(Value::String(String::new())),
1424 );
1425 gc.alloc(HeapObject::Object(data))
1426}
1427
1428fn make_error_constructor(gc: &mut Gc<HeapObject>, name: &str, proto: GcRef) -> GcRef {
1429 gc.alloc(HeapObject::Function(Box::new(FunctionData {
1430 name: name.to_string(),
1431 kind: FunctionKind::Native(NativeFunc {
1432 callback: error_constructor,
1433 }),
1434 prototype_obj: Some(proto),
1435 properties: HashMap::new(),
1436 upvalues: Vec::new(),
1437 })))
1438}
1439
1440fn error_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1441 let message = args
1442 .first()
1443 .map(|v| v.to_js_string(ctx.gc))
1444 .unwrap_or_default();
1445 let mut obj = ObjectData::new();
1446 obj.properties.insert(
1447 "message".to_string(),
1448 Property::data(Value::String(message)),
1449 );
1450 // The "name" property comes from the prototype chain.
1451 Ok(Value::Object(ctx.gc.alloc(HeapObject::Object(obj))))
1452}
1453
1454fn error_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1455 let obj_ref = match ctx.this.gc_ref() {
1456 Some(r) => r,
1457 None => return Ok(Value::String("Error".to_string())),
1458 };
1459 let name = match ctx.gc.get(obj_ref) {
1460 Some(HeapObject::Object(data)) => data
1461 .properties
1462 .get("name")
1463 .map(|p| p.value.to_js_string(ctx.gc))
1464 .unwrap_or_else(|| "Error".to_string()),
1465 _ => "Error".to_string(),
1466 };
1467 let message = match ctx.gc.get(obj_ref) {
1468 Some(HeapObject::Object(data)) => data
1469 .properties
1470 .get("message")
1471 .map(|p| p.value.to_js_string(ctx.gc))
1472 .unwrap_or_default(),
1473 _ => String::new(),
1474 };
1475 if message.is_empty() {
1476 Ok(Value::String(name))
1477 } else {
1478 Ok(Value::String(format!("{name}: {message}")))
1479 }
1480}
1481
1482// ── String built-in ──────────────────────────────────────────
1483
1484fn init_string_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
1485 let methods: &[NativeMethod] = &[
1486 ("charAt", string_proto_char_at),
1487 ("charCodeAt", string_proto_char_code_at),
1488 ("codePointAt", string_proto_code_point_at),
1489 ("concat", string_proto_concat),
1490 ("slice", string_proto_slice),
1491 ("substring", string_proto_substring),
1492 ("substr", string_proto_substr),
1493 ("indexOf", string_proto_index_of),
1494 ("lastIndexOf", string_proto_last_index_of),
1495 ("includes", string_proto_includes),
1496 ("startsWith", string_proto_starts_with),
1497 ("endsWith", string_proto_ends_with),
1498 ("trim", string_proto_trim),
1499 ("trimStart", string_proto_trim_start),
1500 ("trimEnd", string_proto_trim_end),
1501 ("padStart", string_proto_pad_start),
1502 ("padEnd", string_proto_pad_end),
1503 ("repeat", string_proto_repeat),
1504 ("split", string_proto_split),
1505 ("replace", string_proto_replace),
1506 ("replaceAll", string_proto_replace_all),
1507 ("match", string_proto_match),
1508 ("matchAll", string_proto_match_all),
1509 ("search", string_proto_search),
1510 ("toLowerCase", string_proto_to_lower_case),
1511 ("toUpperCase", string_proto_to_upper_case),
1512 ("at", string_proto_at),
1513 ("toString", string_proto_to_string),
1514 ("valueOf", string_proto_value_of),
1515 ];
1516 for &(name, callback) in methods {
1517 let f = make_native(gc, name, callback);
1518 set_builtin_prop(gc, proto, name, Value::Function(f));
1519 }
1520}
1521
1522fn init_string_constructor(gc: &mut Gc<HeapObject>, str_proto: GcRef) -> GcRef {
1523 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
1524 name: "String".to_string(),
1525 kind: FunctionKind::Native(NativeFunc {
1526 callback: string_constructor,
1527 }),
1528 prototype_obj: Some(str_proto),
1529 properties: HashMap::new(),
1530 upvalues: Vec::new(),
1531 })));
1532 let from_char_code = make_native(gc, "fromCharCode", string_from_char_code);
1533 set_func_prop(gc, ctor, "fromCharCode", Value::Function(from_char_code));
1534 let from_code_point = make_native(gc, "fromCodePoint", string_from_code_point);
1535 set_func_prop(gc, ctor, "fromCodePoint", Value::Function(from_code_point));
1536 ctor
1537}
1538
1539fn string_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1540 let s = args
1541 .first()
1542 .map(|v| v.to_js_string(ctx.gc))
1543 .unwrap_or_default();
1544 Ok(Value::String(s))
1545}
1546
1547fn string_from_char_code(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1548 let s: String = args
1549 .iter()
1550 .filter_map(|v| {
1551 let code = v.to_number() as u32;
1552 char::from_u32(code)
1553 })
1554 .collect();
1555 Ok(Value::String(s))
1556}
1557
1558fn string_from_code_point(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1559 let mut s = String::new();
1560 for v in args {
1561 let code = v.to_number() as u32;
1562 match char::from_u32(code) {
1563 Some(c) => s.push(c),
1564 None => {
1565 return Err(RuntimeError::range_error(format!(
1566 "Invalid code point {code}"
1567 )))
1568 }
1569 }
1570 }
1571 Ok(Value::String(s))
1572}
1573
1574/// Helper: extract the string from `this` for String.prototype methods.
1575fn this_string(ctx: &NativeContext) -> String {
1576 ctx.this.to_js_string(ctx.gc)
1577}
1578
1579/// Helper: get chars as a Vec for index-based operations.
1580fn str_chars(s: &str) -> Vec<char> {
1581 s.chars().collect()
1582}
1583
1584fn string_proto_char_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1585 let s = this_string(ctx);
1586 let chars = str_chars(&s);
1587 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1588 if idx < 0 || idx as usize >= chars.len() {
1589 Ok(Value::String(String::new()))
1590 } else {
1591 Ok(Value::String(chars[idx as usize].to_string()))
1592 }
1593}
1594
1595fn string_proto_char_code_at(
1596 args: &[Value],
1597 ctx: &mut NativeContext,
1598) -> Result<Value, RuntimeError> {
1599 let s = this_string(ctx);
1600 let chars = str_chars(&s);
1601 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1602 if idx < 0 || idx as usize >= chars.len() {
1603 Ok(Value::Number(f64::NAN))
1604 } else {
1605 Ok(Value::Number(chars[idx as usize] as u32 as f64))
1606 }
1607}
1608
1609fn string_proto_code_point_at(
1610 args: &[Value],
1611 ctx: &mut NativeContext,
1612) -> Result<Value, RuntimeError> {
1613 let s = this_string(ctx);
1614 let chars = str_chars(&s);
1615 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1616 if idx < 0 || idx as usize >= chars.len() {
1617 Ok(Value::Undefined)
1618 } else {
1619 Ok(Value::Number(chars[idx as usize] as u32 as f64))
1620 }
1621}
1622
1623fn string_proto_concat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1624 let mut s = this_string(ctx);
1625 for arg in args {
1626 s.push_str(&arg.to_js_string(ctx.gc));
1627 }
1628 Ok(Value::String(s))
1629}
1630
1631fn string_proto_slice(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1632 let s = this_string(ctx);
1633 let chars = str_chars(&s);
1634 let len = chars.len() as i64;
1635 let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1636 let end = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len);
1637 let start = if start < 0 {
1638 (len + start).max(0) as usize
1639 } else {
1640 start.min(len) as usize
1641 };
1642 let end = if end < 0 {
1643 (len + end).max(0) as usize
1644 } else {
1645 end.min(len) as usize
1646 };
1647 if start >= end {
1648 Ok(Value::String(String::new()))
1649 } else {
1650 Ok(Value::String(chars[start..end].iter().collect()))
1651 }
1652}
1653
1654fn string_proto_substring(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1655 let s = this_string(ctx);
1656 let chars = str_chars(&s);
1657 let len = chars.len() as i64;
1658 let a = args
1659 .first()
1660 .map(|v| v.to_number() as i64)
1661 .unwrap_or(0)
1662 .clamp(0, len) as usize;
1663 let b = args
1664 .get(1)
1665 .map(|v| v.to_number() as i64)
1666 .unwrap_or(len)
1667 .clamp(0, len) as usize;
1668 let (start, end) = if a <= b { (a, b) } else { (b, a) };
1669 Ok(Value::String(chars[start..end].iter().collect()))
1670}
1671
1672fn string_proto_substr(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1673 let s = this_string(ctx);
1674 let chars = str_chars(&s);
1675 let len = chars.len() as i64;
1676 let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
1677 let start = if start < 0 {
1678 (len + start).max(0) as usize
1679 } else {
1680 start.min(len) as usize
1681 };
1682 let count = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len) as usize;
1683 let end = (start + count).min(chars.len());
1684 Ok(Value::String(chars[start..end].iter().collect()))
1685}
1686
1687fn string_proto_index_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1688 let s = this_string(ctx);
1689 let search = args
1690 .first()
1691 .map(|v| v.to_js_string(ctx.gc))
1692 .unwrap_or_default();
1693 let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
1694 let chars = str_chars(&s);
1695 let search_chars = str_chars(&search);
1696 if search_chars.is_empty() {
1697 return Ok(Value::Number(from.min(chars.len()) as f64));
1698 }
1699 for i in from..chars.len() {
1700 if i + search_chars.len() <= chars.len()
1701 && chars[i..i + search_chars.len()] == search_chars[..]
1702 {
1703 return Ok(Value::Number(i as f64));
1704 }
1705 }
1706 Ok(Value::Number(-1.0))
1707}
1708
1709fn string_proto_last_index_of(
1710 args: &[Value],
1711 ctx: &mut NativeContext,
1712) -> Result<Value, RuntimeError> {
1713 let s = this_string(ctx);
1714 let search = args
1715 .first()
1716 .map(|v| v.to_js_string(ctx.gc))
1717 .unwrap_or_default();
1718 let chars = str_chars(&s);
1719 let search_chars = str_chars(&search);
1720 let from = args
1721 .get(1)
1722 .map(|v| {
1723 let n = v.to_number();
1724 if n.is_nan() {
1725 chars.len()
1726 } else {
1727 n as usize
1728 }
1729 })
1730 .unwrap_or(chars.len());
1731 if search_chars.is_empty() {
1732 return Ok(Value::Number(from.min(chars.len()) as f64));
1733 }
1734 let max_start = from.min(chars.len().saturating_sub(search_chars.len()));
1735 for i in (0..=max_start).rev() {
1736 if i + search_chars.len() <= chars.len()
1737 && chars[i..i + search_chars.len()] == search_chars[..]
1738 {
1739 return Ok(Value::Number(i as f64));
1740 }
1741 }
1742 Ok(Value::Number(-1.0))
1743}
1744
1745fn string_proto_includes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1746 let s = this_string(ctx);
1747 let search = args
1748 .first()
1749 .map(|v| v.to_js_string(ctx.gc))
1750 .unwrap_or_default();
1751 let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
1752 let chars = str_chars(&s);
1753 let search_chars = str_chars(&search);
1754 if search_chars.is_empty() {
1755 return Ok(Value::Boolean(true));
1756 }
1757 for i in from..chars.len() {
1758 if i + search_chars.len() <= chars.len()
1759 && chars[i..i + search_chars.len()] == search_chars[..]
1760 {
1761 return Ok(Value::Boolean(true));
1762 }
1763 }
1764 Ok(Value::Boolean(false))
1765}
1766
1767fn string_proto_starts_with(
1768 args: &[Value],
1769 ctx: &mut NativeContext,
1770) -> Result<Value, RuntimeError> {
1771 let s = this_string(ctx);
1772 let search = args
1773 .first()
1774 .map(|v| v.to_js_string(ctx.gc))
1775 .unwrap_or_default();
1776 let pos = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0);
1777 let chars = str_chars(&s);
1778 let search_chars = str_chars(&search);
1779 if pos + search_chars.len() > chars.len() {
1780 return Ok(Value::Boolean(false));
1781 }
1782 Ok(Value::Boolean(
1783 chars[pos..pos + search_chars.len()] == search_chars[..],
1784 ))
1785}
1786
1787fn string_proto_ends_with(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1788 let s = this_string(ctx);
1789 let search = args
1790 .first()
1791 .map(|v| v.to_js_string(ctx.gc))
1792 .unwrap_or_default();
1793 let chars = str_chars(&s);
1794 let search_chars = str_chars(&search);
1795 let end_pos = args
1796 .get(1)
1797 .map(|v| (v.to_number() as usize).min(chars.len()))
1798 .unwrap_or(chars.len());
1799 if search_chars.len() > end_pos {
1800 return Ok(Value::Boolean(false));
1801 }
1802 let start = end_pos - search_chars.len();
1803 Ok(Value::Boolean(chars[start..end_pos] == search_chars[..]))
1804}
1805
1806fn string_proto_trim(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1807 let _ = args;
1808 Ok(Value::String(this_string(ctx).trim().to_string()))
1809}
1810
1811fn string_proto_trim_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1812 let _ = args;
1813 Ok(Value::String(this_string(ctx).trim_start().to_string()))
1814}
1815
1816fn string_proto_trim_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1817 let _ = args;
1818 Ok(Value::String(this_string(ctx).trim_end().to_string()))
1819}
1820
1821fn string_proto_pad_start(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1822 let s = this_string(ctx);
1823 let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
1824 let fill = args
1825 .get(1)
1826 .map(|v| v.to_js_string(ctx.gc))
1827 .unwrap_or_else(|| " ".to_string());
1828 let chars = str_chars(&s);
1829 if chars.len() >= target_len || fill.is_empty() {
1830 return Ok(Value::String(s));
1831 }
1832 let fill_chars = str_chars(&fill);
1833 let needed = target_len - chars.len();
1834 let mut pad = String::new();
1835 for i in 0..needed {
1836 pad.push(fill_chars[i % fill_chars.len()]);
1837 }
1838 pad.push_str(&s);
1839 Ok(Value::String(pad))
1840}
1841
1842fn string_proto_pad_end(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1843 let s = this_string(ctx);
1844 let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
1845 let fill = args
1846 .get(1)
1847 .map(|v| v.to_js_string(ctx.gc))
1848 .unwrap_or_else(|| " ".to_string());
1849 let chars = str_chars(&s);
1850 if chars.len() >= target_len || fill.is_empty() {
1851 return Ok(Value::String(s));
1852 }
1853 let fill_chars = str_chars(&fill);
1854 let needed = target_len - chars.len();
1855 let mut result = s;
1856 for i in 0..needed {
1857 result.push(fill_chars[i % fill_chars.len()]);
1858 }
1859 Ok(Value::String(result))
1860}
1861
1862fn string_proto_repeat(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1863 let s = this_string(ctx);
1864 let count = args.first().map(|v| v.to_number()).unwrap_or(0.0);
1865 if count < 0.0 || count.is_infinite() {
1866 return Err(RuntimeError::range_error("Invalid count value"));
1867 }
1868 Ok(Value::String(s.repeat(count as usize)))
1869}
1870
1871fn string_proto_split(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1872 let s = this_string(ctx);
1873 if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) {
1874 return Ok(make_value_array(ctx.gc, &[Value::String(s)]));
1875 }
1876 let limit = args
1877 .get(1)
1878 .map(|v| v.to_number() as usize)
1879 .unwrap_or(usize::MAX);
1880
1881 // Check if separator is a RegExp.
1882 if let Some(arg0) = args.first() {
1883 if is_regexp(ctx.gc, arg0) {
1884 return string_split_regexp(ctx.gc, &s, arg0, limit);
1885 }
1886 }
1887
1888 let sep = args[0].to_js_string(ctx.gc);
1889 if sep.is_empty() {
1890 let items: Vec<Value> = str_chars(&s)
1891 .into_iter()
1892 .take(limit)
1893 .map(|c| Value::String(c.to_string()))
1894 .collect();
1895 return Ok(make_value_array(ctx.gc, &items));
1896 }
1897 let mut items = Vec::new();
1898 let mut start = 0;
1899 let sep_len = sep.len();
1900 while let Some(pos) = s[start..].find(&sep) {
1901 if items.len() >= limit {
1902 break;
1903 }
1904 items.push(Value::String(s[start..start + pos].to_string()));
1905 start += pos + sep_len;
1906 }
1907 if items.len() < limit {
1908 items.push(Value::String(s[start..].to_string()));
1909 }
1910 Ok(make_value_array(ctx.gc, &items))
1911}
1912
1913fn string_split_regexp(
1914 gc: &mut Gc<HeapObject>,
1915 s: &str,
1916 regexp: &Value,
1917 limit: usize,
1918) -> Result<Value, RuntimeError> {
1919 use crate::regex::{exec, CompiledRegex};
1920
1921 let pattern = regexp_get_pattern(gc, regexp).unwrap_or_default();
1922 let flags_str = regexp_get_flags(gc, regexp).unwrap_or_default();
1923 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
1924 let chars: Vec<char> = s.chars().collect();
1925
1926 let mut items = Vec::new();
1927 let mut last_end = 0usize;
1928
1929 loop {
1930 if items.len() >= limit {
1931 break;
1932 }
1933 match exec(&compiled, s, last_end) {
1934 Some(m) => {
1935 // Avoid infinite loop on zero-length matches.
1936 if m.start == m.end && m.start == last_end {
1937 if last_end >= chars.len() {
1938 break;
1939 }
1940 items.push(Value::String(chars[last_end].to_string()));
1941 last_end += 1;
1942 continue;
1943 }
1944 let piece: String = chars[last_end..m.start].iter().collect();
1945 items.push(Value::String(piece));
1946 // Add capturing groups.
1947 for i in 1..m.captures.len() {
1948 if items.len() >= limit {
1949 break;
1950 }
1951 match m.captures[i] {
1952 Some((cs, ce)) => {
1953 let cap: String = chars[cs..ce].iter().collect();
1954 items.push(Value::String(cap));
1955 }
1956 None => items.push(Value::Undefined),
1957 }
1958 }
1959 last_end = m.end;
1960 }
1961 None => break,
1962 }
1963 }
1964 if items.len() < limit {
1965 let rest: String = chars[last_end..].iter().collect();
1966 items.push(Value::String(rest));
1967 }
1968 Ok(make_value_array(gc, &items))
1969}
1970
1971fn string_proto_replace(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
1972 let s = this_string(ctx);
1973
1974 // Check if search argument is a RegExp.
1975 if let Some(arg0) = args.first() {
1976 if is_regexp(ctx.gc, arg0) {
1977 let replacement = args
1978 .get(1)
1979 .map(|v| v.to_js_string(ctx.gc))
1980 .unwrap_or_default();
1981 return string_replace_regexp(ctx.gc, &s, arg0, &replacement);
1982 }
1983 }
1984
1985 let search = args
1986 .first()
1987 .map(|v| v.to_js_string(ctx.gc))
1988 .unwrap_or_default();
1989 let replacement = args
1990 .get(1)
1991 .map(|v| v.to_js_string(ctx.gc))
1992 .unwrap_or_default();
1993 // Replace only the first occurrence.
1994 if let Some(pos) = s.find(&search) {
1995 let mut result = String::with_capacity(s.len());
1996 result.push_str(&s[..pos]);
1997 result.push_str(&replacement);
1998 result.push_str(&s[pos + search.len()..]);
1999 Ok(Value::String(result))
2000 } else {
2001 Ok(Value::String(s))
2002 }
2003}
2004
2005fn string_replace_regexp(
2006 gc: &mut Gc<HeapObject>,
2007 s: &str,
2008 regexp: &Value,
2009 replacement: &str,
2010) -> Result<Value, RuntimeError> {
2011 use crate::regex::{exec, CompiledRegex};
2012
2013 let pattern = regexp_get_pattern(gc, regexp).unwrap_or_default();
2014 let flags_str = regexp_get_flags(gc, regexp).unwrap_or_default();
2015 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
2016 let is_global = compiled.flags.global;
2017 let chars: Vec<char> = s.chars().collect();
2018 let mut result = String::new();
2019 let mut last_end = 0usize;
2020
2021 while let Some(m) = exec(&compiled, s, last_end) {
2022 // Append text before match.
2023 let before: String = chars[last_end..m.start].iter().collect();
2024 result.push_str(&before);
2025 // Process replacement with $-substitutions.
2026 let matched: String = chars[m.start..m.end].iter().collect();
2027 result.push_str(&apply_replacement(
2028 replacement,
2029 &matched,
2030 &m.captures,
2031 &chars,
2032 ));
2033 last_end = m.end;
2034 if !is_global {
2035 break;
2036 }
2037 // Avoid infinite loop on zero-length match.
2038 if m.start == m.end {
2039 if last_end < chars.len() {
2040 result.push(chars[last_end]);
2041 last_end += 1;
2042 } else {
2043 break;
2044 }
2045 }
2046 }
2047 let rest: String = chars[last_end..].iter().collect();
2048 result.push_str(&rest);
2049 Ok(Value::String(result))
2050}
2051
2052/// Apply replacement string with $-substitutions ($&, $1, etc.).
2053fn apply_replacement(
2054 replacement: &str,
2055 matched: &str,
2056 captures: &[Option<(usize, usize)>],
2057 chars: &[char],
2058) -> String {
2059 let rep_chars: Vec<char> = replacement.chars().collect();
2060 let mut result = String::new();
2061 let mut i = 0;
2062 while i < rep_chars.len() {
2063 if rep_chars[i] == '$' && i + 1 < rep_chars.len() {
2064 match rep_chars[i + 1] {
2065 '$' => {
2066 result.push('$');
2067 i += 2;
2068 }
2069 '&' => {
2070 result.push_str(matched);
2071 i += 2;
2072 }
2073 '`' => {
2074 // $` — text before match.
2075 if let Some(Some((start, _))) = captures.first() {
2076 let before: String = chars[..*start].iter().collect();
2077 result.push_str(&before);
2078 }
2079 i += 2;
2080 }
2081 '\'' => {
2082 // $' — text after match.
2083 if let Some(Some((_, end))) = captures.first() {
2084 let after: String = chars[*end..].iter().collect();
2085 result.push_str(&after);
2086 }
2087 i += 2;
2088 }
2089 d if d.is_ascii_digit() => {
2090 // $1, $12 etc.
2091 let mut num_str = String::new();
2092 let mut j = i + 1;
2093 while j < rep_chars.len() && rep_chars[j].is_ascii_digit() {
2094 num_str.push(rep_chars[j]);
2095 j += 1;
2096 }
2097 if let Ok(idx) = num_str.parse::<usize>() {
2098 if idx > 0 && idx < captures.len() {
2099 if let Some((s, e)) = captures[idx] {
2100 let cap: String = chars[s..e].iter().collect();
2101 result.push_str(&cap);
2102 }
2103 }
2104 }
2105 i = j;
2106 }
2107 _ => {
2108 result.push('$');
2109 i += 1;
2110 }
2111 }
2112 } else {
2113 result.push(rep_chars[i]);
2114 i += 1;
2115 }
2116 }
2117 result
2118}
2119
2120fn string_proto_replace_all(
2121 args: &[Value],
2122 ctx: &mut NativeContext,
2123) -> Result<Value, RuntimeError> {
2124 let s = this_string(ctx);
2125
2126 // If search is a RegExp, it must have the global flag.
2127 if let Some(arg0) = args.first() {
2128 if is_regexp(ctx.gc, arg0) {
2129 let flags = regexp_get_flags(ctx.gc, arg0).unwrap_or_default();
2130 if !flags.contains('g') {
2131 return Err(RuntimeError::type_error(
2132 "String.prototype.replaceAll called with a non-global RegExp argument",
2133 ));
2134 }
2135 let replacement = args
2136 .get(1)
2137 .map(|v| v.to_js_string(ctx.gc))
2138 .unwrap_or_default();
2139 return string_replace_regexp(ctx.gc, &s, arg0, &replacement);
2140 }
2141 }
2142
2143 let search = args
2144 .first()
2145 .map(|v| v.to_js_string(ctx.gc))
2146 .unwrap_or_default();
2147 let replacement = args
2148 .get(1)
2149 .map(|v| v.to_js_string(ctx.gc))
2150 .unwrap_or_default();
2151 Ok(Value::String(s.replace(&search, &replacement)))
2152}
2153
2154fn string_proto_match(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2155 let s = this_string(ctx);
2156 if args.is_empty() {
2157 return Ok(Value::Null);
2158 }
2159
2160 let arg0 = &args[0];
2161 // If arg is not a RegExp, create one.
2162 let regexp_val = if is_regexp(ctx.gc, arg0) {
2163 arg0.clone()
2164 } else {
2165 let pattern = arg0.to_js_string(ctx.gc);
2166 let proto = REGEXP_PROTO.with(|cell| cell.get());
2167 make_regexp_obj(ctx.gc, &pattern, "", proto).map_err(RuntimeError::syntax_error)?
2168 };
2169
2170 let is_global = regexp_get_flags(ctx.gc, ®exp_val)
2171 .map(|f| f.contains('g'))
2172 .unwrap_or(false);
2173
2174 if !is_global {
2175 // Non-global: return exec result.
2176 return regexp_exec_internal(ctx.gc, ®exp_val, &s);
2177 }
2178
2179 // Global: collect all matches.
2180 regexp_set_last_index(ctx.gc, ®exp_val, 0.0);
2181 let mut matches = Vec::new();
2182 loop {
2183 let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?;
2184 if matches!(result, Value::Null) {
2185 break;
2186 }
2187 // Get the matched string (index 0 of the result array).
2188 if let Value::Object(r) = &result {
2189 if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) {
2190 if let Some(prop) = data.properties.get("0") {
2191 matches.push(prop.value.clone());
2192 // Advance past zero-length matches.
2193 let match_str = prop.value.to_js_string(ctx.gc);
2194 if match_str.is_empty() {
2195 let li = regexp_get_last_index(ctx.gc, ®exp_val);
2196 regexp_set_last_index(ctx.gc, ®exp_val, li + 1.0);
2197 }
2198 }
2199 }
2200 }
2201 }
2202 if matches.is_empty() {
2203 Ok(Value::Null)
2204 } else {
2205 Ok(make_value_array(ctx.gc, &matches))
2206 }
2207}
2208
2209fn string_proto_match_all(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2210 let s = this_string(ctx);
2211 if args.is_empty() {
2212 return Ok(make_value_array(ctx.gc, &[]));
2213 }
2214
2215 let arg0 = &args[0];
2216 // If arg is a RegExp, it must have global flag.
2217 let regexp_val = if is_regexp(ctx.gc, arg0) {
2218 let flags = regexp_get_flags(ctx.gc, arg0).unwrap_or_default();
2219 if !flags.contains('g') {
2220 return Err(RuntimeError::type_error(
2221 "String.prototype.matchAll called with a non-global RegExp argument",
2222 ));
2223 }
2224 arg0.clone()
2225 } else {
2226 let pattern = arg0.to_js_string(ctx.gc);
2227 let proto = REGEXP_PROTO.with(|cell| cell.get());
2228 make_regexp_obj(ctx.gc, &pattern, "g", proto).map_err(RuntimeError::syntax_error)?
2229 };
2230
2231 // Collect all match results.
2232 regexp_set_last_index(ctx.gc, ®exp_val, 0.0);
2233 let mut results = Vec::new();
2234 loop {
2235 let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?;
2236 if matches!(result, Value::Null) {
2237 break;
2238 }
2239 // Advance past zero-length matches.
2240 if let Value::Object(r) = &result {
2241 if let Some(HeapObject::Object(data)) = ctx.gc.get(*r) {
2242 if let Some(prop) = data.properties.get("0") {
2243 let match_str = prop.value.to_js_string(ctx.gc);
2244 if match_str.is_empty() {
2245 let li = regexp_get_last_index(ctx.gc, ®exp_val);
2246 regexp_set_last_index(ctx.gc, ®exp_val, li + 1.0);
2247 }
2248 }
2249 }
2250 }
2251 results.push(result);
2252 }
2253 Ok(make_value_array(ctx.gc, &results))
2254}
2255
2256fn string_proto_search(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2257 let s = this_string(ctx);
2258 if args.is_empty() {
2259 return Ok(Value::Number(0.0)); // /(?:)/ matches at 0.
2260 }
2261
2262 let arg0 = &args[0];
2263 let regexp_val = if is_regexp(ctx.gc, arg0) {
2264 arg0.clone()
2265 } else {
2266 let pattern = arg0.to_js_string(ctx.gc);
2267 let proto = REGEXP_PROTO.with(|cell| cell.get());
2268 make_regexp_obj(ctx.gc, &pattern, "", proto).map_err(RuntimeError::syntax_error)?
2269 };
2270
2271 // search always starts from 0 and ignores global/lastIndex.
2272 // Save and restore lastIndex so exec_internal's global/sticky handling doesn't interfere.
2273 let saved_last_index = regexp_get_last_index(ctx.gc, ®exp_val);
2274 regexp_set_last_index(ctx.gc, ®exp_val, 0.0);
2275 let result = regexp_exec_internal(ctx.gc, ®exp_val, &s)?;
2276 regexp_set_last_index(ctx.gc, ®exp_val, saved_last_index);
2277 match result {
2278 Value::Null => Ok(Value::Number(-1.0)),
2279 Value::Object(r) => {
2280 let idx = match ctx.gc.get(r) {
2281 Some(HeapObject::Object(data)) => data
2282 .properties
2283 .get("index")
2284 .map(|p| p.value.to_number())
2285 .unwrap_or(-1.0),
2286 _ => -1.0,
2287 };
2288 Ok(Value::Number(idx))
2289 }
2290 _ => Ok(Value::Number(-1.0)),
2291 }
2292}
2293
2294fn string_proto_to_lower_case(
2295 args: &[Value],
2296 ctx: &mut NativeContext,
2297) -> Result<Value, RuntimeError> {
2298 let _ = args;
2299 Ok(Value::String(this_string(ctx).to_lowercase()))
2300}
2301
2302fn string_proto_to_upper_case(
2303 args: &[Value],
2304 ctx: &mut NativeContext,
2305) -> Result<Value, RuntimeError> {
2306 let _ = args;
2307 Ok(Value::String(this_string(ctx).to_uppercase()))
2308}
2309
2310fn string_proto_at(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2311 let s = this_string(ctx);
2312 let chars = str_chars(&s);
2313 let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
2314 let actual = if idx < 0 {
2315 chars.len() as i64 + idx
2316 } else {
2317 idx
2318 };
2319 if actual < 0 || actual as usize >= chars.len() {
2320 Ok(Value::Undefined)
2321 } else {
2322 Ok(Value::String(chars[actual as usize].to_string()))
2323 }
2324}
2325
2326fn string_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2327 let _ = args;
2328 Ok(Value::String(this_string(ctx)))
2329}
2330
2331fn string_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2332 let _ = args;
2333 Ok(Value::String(this_string(ctx)))
2334}
2335
2336// ── Number built-in ──────────────────────────────────────────
2337
2338fn init_number_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
2339 let methods: &[NativeMethod] = &[
2340 ("toString", number_proto_to_string),
2341 ("valueOf", number_proto_value_of),
2342 ("toFixed", number_proto_to_fixed),
2343 ("toPrecision", number_proto_to_precision),
2344 ("toExponential", number_proto_to_exponential),
2345 ];
2346 for &(name, callback) in methods {
2347 let f = make_native(gc, name, callback);
2348 set_builtin_prop(gc, proto, name, Value::Function(f));
2349 }
2350}
2351
2352fn init_number_constructor(gc: &mut Gc<HeapObject>, num_proto: GcRef) -> GcRef {
2353 let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData {
2354 name: "Number".to_string(),
2355 kind: FunctionKind::Native(NativeFunc {
2356 callback: number_constructor,
2357 }),
2358 prototype_obj: Some(num_proto),
2359 properties: HashMap::new(),
2360 upvalues: Vec::new(),
2361 })));
2362 // Static methods.
2363 let is_nan = make_native(gc, "isNaN", number_is_nan);
2364 set_func_prop(gc, ctor, "isNaN", Value::Function(is_nan));
2365 let is_finite = make_native(gc, "isFinite", number_is_finite);
2366 set_func_prop(gc, ctor, "isFinite", Value::Function(is_finite));
2367 let is_integer = make_native(gc, "isInteger", number_is_integer);
2368 set_func_prop(gc, ctor, "isInteger", Value::Function(is_integer));
2369 let is_safe_integer = make_native(gc, "isSafeInteger", number_is_safe_integer);
2370 set_func_prop(gc, ctor, "isSafeInteger", Value::Function(is_safe_integer));
2371 let parse_int_fn = make_native(gc, "parseInt", crate::builtins::parse_int);
2372 set_func_prop(gc, ctor, "parseInt", Value::Function(parse_int_fn));
2373 let parse_float_fn = make_native(gc, "parseFloat", crate::builtins::parse_float);
2374 set_func_prop(gc, ctor, "parseFloat", Value::Function(parse_float_fn));
2375 // Constants.
2376 set_func_prop(gc, ctor, "EPSILON", Value::Number(f64::EPSILON));
2377 set_func_prop(
2378 gc,
2379 ctor,
2380 "MAX_SAFE_INTEGER",
2381 Value::Number(9007199254740991.0),
2382 );
2383 set_func_prop(
2384 gc,
2385 ctor,
2386 "MIN_SAFE_INTEGER",
2387 Value::Number(-9007199254740991.0),
2388 );
2389 set_func_prop(gc, ctor, "MAX_VALUE", Value::Number(f64::MAX));
2390 set_func_prop(gc, ctor, "MIN_VALUE", Value::Number(f64::MIN_POSITIVE));
2391 set_func_prop(gc, ctor, "NaN", Value::Number(f64::NAN));
2392 set_func_prop(gc, ctor, "POSITIVE_INFINITY", Value::Number(f64::INFINITY));
2393 set_func_prop(
2394 gc,
2395 ctor,
2396 "NEGATIVE_INFINITY",
2397 Value::Number(f64::NEG_INFINITY),
2398 );
2399 ctor
2400}
2401
2402fn number_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2403 let n = args.first().map(|v| v.to_number()).unwrap_or(0.0);
2404 Ok(Value::Number(n))
2405}
2406
2407fn number_is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2408 match args.first() {
2409 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_nan())),
2410 _ => Ok(Value::Boolean(false)),
2411 }
2412}
2413
2414fn number_is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2415 match args.first() {
2416 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite())),
2417 _ => Ok(Value::Boolean(false)),
2418 }
2419}
2420
2421fn number_is_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2422 match args.first() {
2423 Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite() && n.trunc() == *n)),
2424 _ => Ok(Value::Boolean(false)),
2425 }
2426}
2427
2428fn number_is_safe_integer(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2429 match args.first() {
2430 Some(Value::Number(n)) => {
2431 let safe = n.is_finite() && n.trunc() == *n && n.abs() <= 9007199254740991.0;
2432 Ok(Value::Boolean(safe))
2433 }
2434 _ => Ok(Value::Boolean(false)),
2435 }
2436}
2437
2438/// Helper: extract the number from `this` for Number.prototype methods.
2439fn this_number(ctx: &NativeContext) -> f64 {
2440 ctx.this.to_number()
2441}
2442
2443fn number_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2444 let n = this_number(ctx);
2445 let radix = args.first().map(|v| v.to_number() as u32).unwrap_or(10);
2446 if !(2..=36).contains(&radix) {
2447 return Err(RuntimeError::range_error(
2448 "toString() radix must be between 2 and 36",
2449 ));
2450 }
2451 if radix == 10 {
2452 return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc)));
2453 }
2454 if n.is_nan() {
2455 return Ok(Value::String("NaN".to_string()));
2456 }
2457 if n.is_infinite() {
2458 return Ok(Value::String(if n > 0.0 {
2459 "Infinity".to_string()
2460 } else {
2461 "-Infinity".to_string()
2462 }));
2463 }
2464 // Integer path for non-decimal radix.
2465 let neg = n < 0.0;
2466 let abs = n.abs() as u64;
2467 let mut digits = Vec::new();
2468 let mut val = abs;
2469 if val == 0 {
2470 digits.push('0');
2471 } else {
2472 while val > 0 {
2473 let d = (val % radix as u64) as u32;
2474 digits.push(char::from_digit(d, radix).unwrap_or('?'));
2475 val /= radix as u64;
2476 }
2477 }
2478 digits.reverse();
2479 let mut result = String::new();
2480 if neg {
2481 result.push('-');
2482 }
2483 result.extend(digits);
2484 Ok(Value::String(result))
2485}
2486
2487fn number_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2488 let _ = args;
2489 Ok(Value::Number(this_number(ctx)))
2490}
2491
2492fn number_proto_to_fixed(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2493 let n = this_number(ctx);
2494 let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(0);
2495 if digits > 100 {
2496 return Err(RuntimeError::range_error(
2497 "toFixed() digits argument must be between 0 and 100",
2498 ));
2499 }
2500 Ok(Value::String(format!("{n:.digits$}")))
2501}
2502
2503fn number_proto_to_precision(
2504 args: &[Value],
2505 ctx: &mut NativeContext,
2506) -> Result<Value, RuntimeError> {
2507 let n = this_number(ctx);
2508 if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) {
2509 return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc)));
2510 }
2511 let prec = args[0].to_number() as usize;
2512 if !(1..=100).contains(&prec) {
2513 return Err(RuntimeError::range_error(
2514 "toPrecision() argument must be between 1 and 100",
2515 ));
2516 }
2517 Ok(Value::String(format!("{n:.prec$e}")))
2518}
2519
2520fn number_proto_to_exponential(
2521 args: &[Value],
2522 ctx: &mut NativeContext,
2523) -> Result<Value, RuntimeError> {
2524 let n = this_number(ctx);
2525 let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(6);
2526 if digits > 100 {
2527 return Err(RuntimeError::range_error(
2528 "toExponential() argument must be between 0 and 100",
2529 ));
2530 }
2531 Ok(Value::String(format!("{n:.digits$e}")))
2532}
2533
2534// ── Boolean built-in ─────────────────────────────────────────
2535
2536fn init_boolean_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
2537 let to_string = make_native(gc, "toString", boolean_proto_to_string);
2538 set_builtin_prop(gc, proto, "toString", Value::Function(to_string));
2539 let value_of = make_native(gc, "valueOf", boolean_proto_value_of);
2540 set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of));
2541}
2542
2543fn init_boolean_constructor(gc: &mut Gc<HeapObject>, bool_proto: GcRef) -> GcRef {
2544 gc.alloc(HeapObject::Function(Box::new(FunctionData {
2545 name: "Boolean".to_string(),
2546 kind: FunctionKind::Native(NativeFunc {
2547 callback: boolean_constructor,
2548 }),
2549 prototype_obj: Some(bool_proto),
2550 properties: HashMap::new(),
2551 upvalues: Vec::new(),
2552 })))
2553}
2554
2555fn boolean_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2556 let b = args.first().map(|v| v.to_boolean()).unwrap_or(false);
2557 Ok(Value::Boolean(b))
2558}
2559
2560fn boolean_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2561 let _ = args;
2562 Ok(Value::String(
2563 if ctx.this.to_boolean() {
2564 "true"
2565 } else {
2566 "false"
2567 }
2568 .to_string(),
2569 ))
2570}
2571
2572fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2573 let _ = args;
2574 Ok(Value::Boolean(ctx.this.to_boolean()))
2575}
2576
2577// ── Symbol built-in ──────────────────────────────────────────
2578
2579/// Global symbol ID counter. Each Symbol() call increments this.
2580static SYMBOL_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2581
2582// Thread-local Date prototype GcRef for use in date_constructor.
2583// The Date constructor callback doesn't have VM access, so we store the
2584// prototype here during init and read it back during construction.
2585thread_local! {
2586 static DATE_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
2587}
2588
2589fn init_symbol_builtins(vm: &mut Vm) {
2590 // Create a function object so we can hang static props on it.
2591 let gc_ref = make_native(&mut vm.gc, "Symbol", symbol_factory);
2592
2593 // Well-known symbols as string constants.
2594 let well_known = [
2595 ("iterator", "@@iterator"),
2596 ("toPrimitive", "@@toPrimitive"),
2597 ("toStringTag", "@@toStringTag"),
2598 ("hasInstance", "@@hasInstance"),
2599 ];
2600 for (name, value) in well_known {
2601 set_func_prop(&mut vm.gc, gc_ref, name, Value::String(value.to_string()));
2602 }
2603
2604 // Symbol.for() and Symbol.keyFor().
2605 let sym_for = make_native(&mut vm.gc, "for", symbol_for);
2606 set_func_prop(&mut vm.gc, gc_ref, "for", Value::Function(sym_for));
2607 let sym_key_for = make_native(&mut vm.gc, "keyFor", symbol_key_for);
2608 set_func_prop(&mut vm.gc, gc_ref, "keyFor", Value::Function(sym_key_for));
2609
2610 vm.set_global("Symbol", Value::Function(gc_ref));
2611
2612 // Register global NaN and Infinity constants.
2613 vm.set_global("NaN", Value::Number(f64::NAN));
2614 vm.set_global("Infinity", Value::Number(f64::INFINITY));
2615 vm.set_global("undefined", Value::Undefined);
2616}
2617
2618fn symbol_factory(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2619 let desc = args
2620 .first()
2621 .map(|v| v.to_js_string(ctx.gc))
2622 .unwrap_or_default();
2623 let id = SYMBOL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
2624 // Return a unique string representation. Real Symbol is a distinct type,
2625 // but this pragmatic approach works for property keys and identity checks.
2626 Ok(Value::String(format!("@@sym_{id}_{desc}")))
2627}
2628
2629fn symbol_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2630 let key = args
2631 .first()
2632 .map(|v| v.to_js_string(ctx.gc))
2633 .unwrap_or_default();
2634 // Deterministic: same key always produces same symbol string.
2635 Ok(Value::String(format!("@@global_{key}")))
2636}
2637
2638fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2639 let sym = args
2640 .first()
2641 .map(|v| v.to_js_string(ctx.gc))
2642 .unwrap_or_default();
2643 if let Some(key) = sym.strip_prefix("@@global_") {
2644 Ok(Value::String(key.to_string()))
2645 } else {
2646 Ok(Value::Undefined)
2647 }
2648}
2649
2650// ── Math object ──────────────────────────────────────────────
2651
2652/// Simple xorshift64 PRNG state (no crypto requirement).
2653static PRNG_STATE: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
2654
2655fn prng_next() -> f64 {
2656 let mut s = PRNG_STATE.load(std::sync::atomic::Ordering::Relaxed);
2657 if s == 0 {
2658 // Seed from system time.
2659 s = SystemTime::now()
2660 .duration_since(UNIX_EPOCH)
2661 .map(|d| d.as_nanos() as u64)
2662 .unwrap_or(123456789);
2663 }
2664 s ^= s << 13;
2665 s ^= s >> 7;
2666 s ^= s << 17;
2667 PRNG_STATE.store(s, std::sync::atomic::Ordering::Relaxed);
2668 // Map to [0, 1).
2669 (s >> 11) as f64 / ((1u64 << 53) as f64)
2670}
2671
2672fn init_math_object(vm: &mut Vm) {
2673 let mut data = ObjectData::new();
2674 if let Some(proto) = vm.object_prototype {
2675 data.prototype = Some(proto);
2676 }
2677 let math_ref = vm.gc.alloc(HeapObject::Object(data));
2678
2679 // Constants.
2680 let constants: &[(&str, f64)] = &[
2681 ("E", std::f64::consts::E),
2682 ("LN2", std::f64::consts::LN_2),
2683 ("LN10", std::f64::consts::LN_10),
2684 ("LOG2E", std::f64::consts::LOG2_E),
2685 ("LOG10E", std::f64::consts::LOG10_E),
2686 ("PI", std::f64::consts::PI),
2687 ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2),
2688 ("SQRT2", std::f64::consts::SQRT_2),
2689 ];
2690 for &(name, val) in constants {
2691 set_builtin_prop(&mut vm.gc, math_ref, name, Value::Number(val));
2692 }
2693
2694 // Methods.
2695 let methods: &[NativeMethod] = &[
2696 ("abs", math_abs),
2697 ("ceil", math_ceil),
2698 ("floor", math_floor),
2699 ("round", math_round),
2700 ("trunc", math_trunc),
2701 ("max", math_max),
2702 ("min", math_min),
2703 ("pow", math_pow),
2704 ("sqrt", math_sqrt),
2705 ("cbrt", math_cbrt),
2706 ("hypot", math_hypot),
2707 ("sin", math_sin),
2708 ("cos", math_cos),
2709 ("tan", math_tan),
2710 ("asin", math_asin),
2711 ("acos", math_acos),
2712 ("atan", math_atan),
2713 ("atan2", math_atan2),
2714 ("exp", math_exp),
2715 ("log", math_log),
2716 ("log2", math_log2),
2717 ("log10", math_log10),
2718 ("expm1", math_expm1),
2719 ("log1p", math_log1p),
2720 ("sign", math_sign),
2721 ("clz32", math_clz32),
2722 ("fround", math_fround),
2723 ("random", math_random),
2724 ("imul", math_imul),
2725 ];
2726 for &(name, cb) in methods {
2727 let f = make_native(&mut vm.gc, name, cb);
2728 set_builtin_prop(&mut vm.gc, math_ref, name, Value::Function(f));
2729 }
2730
2731 vm.set_global("Math", Value::Object(math_ref));
2732}
2733
2734fn math_abs(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2735 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2736 Ok(Value::Number(n.abs()))
2737}
2738
2739fn math_ceil(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2740 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2741 Ok(Value::Number(n.ceil()))
2742}
2743
2744fn math_floor(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2745 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2746 Ok(Value::Number(n.floor()))
2747}
2748
2749fn math_round(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2750 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2751 // JS Math.round rounds .5 up (toward +Infinity).
2752 Ok(Value::Number(n.round()))
2753}
2754
2755fn math_trunc(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2756 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2757 Ok(Value::Number(n.trunc()))
2758}
2759
2760fn math_max(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2761 if args.is_empty() {
2762 return Ok(Value::Number(f64::NEG_INFINITY));
2763 }
2764 let mut result = f64::NEG_INFINITY;
2765 for arg in args {
2766 let n = arg.to_number();
2767 if n.is_nan() {
2768 return Ok(Value::Number(f64::NAN));
2769 }
2770 if n > result {
2771 result = n;
2772 }
2773 }
2774 Ok(Value::Number(result))
2775}
2776
2777fn math_min(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2778 if args.is_empty() {
2779 return Ok(Value::Number(f64::INFINITY));
2780 }
2781 let mut result = f64::INFINITY;
2782 for arg in args {
2783 let n = arg.to_number();
2784 if n.is_nan() {
2785 return Ok(Value::Number(f64::NAN));
2786 }
2787 if n < result {
2788 result = n;
2789 }
2790 }
2791 Ok(Value::Number(result))
2792}
2793
2794fn math_pow(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2795 let base = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2796 let exp = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN);
2797 Ok(Value::Number(base.powf(exp)))
2798}
2799
2800fn math_sqrt(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2801 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2802 Ok(Value::Number(n.sqrt()))
2803}
2804
2805fn math_cbrt(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2806 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2807 Ok(Value::Number(n.cbrt()))
2808}
2809
2810fn math_hypot(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2811 if args.is_empty() {
2812 return Ok(Value::Number(0.0));
2813 }
2814 let mut sum = 0.0f64;
2815 for arg in args {
2816 let n = arg.to_number();
2817 if n.is_infinite() {
2818 return Ok(Value::Number(f64::INFINITY));
2819 }
2820 if n.is_nan() {
2821 return Ok(Value::Number(f64::NAN));
2822 }
2823 sum += n * n;
2824 }
2825 Ok(Value::Number(sum.sqrt()))
2826}
2827
2828fn math_sin(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2829 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2830 Ok(Value::Number(n.sin()))
2831}
2832
2833fn math_cos(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2834 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2835 Ok(Value::Number(n.cos()))
2836}
2837
2838fn math_tan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2839 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2840 Ok(Value::Number(n.tan()))
2841}
2842
2843fn math_asin(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2844 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2845 Ok(Value::Number(n.asin()))
2846}
2847
2848fn math_acos(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2849 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2850 Ok(Value::Number(n.acos()))
2851}
2852
2853fn math_atan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2854 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2855 Ok(Value::Number(n.atan()))
2856}
2857
2858fn math_atan2(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2859 let y = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2860 let x = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN);
2861 Ok(Value::Number(y.atan2(x)))
2862}
2863
2864fn math_exp(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2865 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2866 Ok(Value::Number(n.exp()))
2867}
2868
2869fn math_log(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2870 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2871 Ok(Value::Number(n.ln()))
2872}
2873
2874fn math_log2(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2875 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2876 Ok(Value::Number(n.log2()))
2877}
2878
2879fn math_log10(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2880 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2881 Ok(Value::Number(n.log10()))
2882}
2883
2884fn math_expm1(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2885 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2886 Ok(Value::Number(n.exp_m1()))
2887}
2888
2889fn math_log1p(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2890 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2891 Ok(Value::Number(n.ln_1p()))
2892}
2893
2894fn math_sign(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2895 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2896 if n.is_nan() {
2897 Ok(Value::Number(f64::NAN))
2898 } else if n == 0.0 {
2899 // Preserve -0 and +0.
2900 Ok(Value::Number(n))
2901 } else if n > 0.0 {
2902 Ok(Value::Number(1.0))
2903 } else {
2904 Ok(Value::Number(-1.0))
2905 }
2906}
2907
2908fn math_clz32(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2909 let n = args.first().map(|v| v.to_number()).unwrap_or(0.0);
2910 let i = n as u32;
2911 Ok(Value::Number(i.leading_zeros() as f64))
2912}
2913
2914fn math_fround(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2915 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
2916 Ok(Value::Number((n as f32) as f64))
2917}
2918
2919fn math_random(_args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2920 Ok(Value::Number(prng_next()))
2921}
2922
2923fn math_imul(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
2924 // ToUint32 then reinterpret as i32 per spec.
2925 let a = args.first().map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32;
2926 let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32;
2927 Ok(Value::Number(a.wrapping_mul(b) as f64))
2928}
2929
2930// ── Date built-in ────────────────────────────────────────────
2931
2932/// Milliseconds since Unix epoch from SystemTime.
2933fn now_ms() -> f64 {
2934 SystemTime::now()
2935 .duration_since(UNIX_EPOCH)
2936 .map(|d| d.as_millis() as f64)
2937 .unwrap_or(0.0)
2938}
2939
2940/// Calendar components from a timestamp (milliseconds since epoch).
2941/// Returns (year, month0, day, hours, minutes, seconds, ms, weekday) in UTC.
2942fn ms_to_utc_components(ms: f64) -> (i64, i64, i64, i64, i64, i64, i64, i64) {
2943 let total_ms = ms as i64;
2944 let ms_part = ((total_ms % 1000) + 1000) % 1000;
2945 let mut days = total_ms.div_euclid(86_400_000);
2946 // Weekday: Jan 1 1970 was Thursday (4).
2947 let weekday = ((days % 7) + 4 + 7) % 7;
2948
2949 let secs_in_day = (total_ms.rem_euclid(86_400_000)) / 1000;
2950 let hours = secs_in_day / 3600;
2951 let minutes = (secs_in_day % 3600) / 60;
2952 let seconds = secs_in_day % 60;
2953
2954 // Convert days since epoch to year/month/day using a civil calendar algorithm.
2955 // Shift epoch to March 1, 2000.
2956 days += 719_468;
2957 let era = if days >= 0 {
2958 days / 146_097
2959 } else {
2960 (days - 146_096) / 146_097
2961 };
2962 let doe = days - era * 146_097; // day of era [0, 146096]
2963 let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365;
2964 let y = yoe + era * 400;
2965 let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
2966 let mp = (5 * doy + 2) / 153;
2967 let d = doy - (153 * mp + 2) / 5 + 1;
2968 let m = if mp < 10 { mp + 3 } else { mp - 9 };
2969 let y = if m <= 2 { y + 1 } else { y };
2970
2971 (y, m - 1, d, hours, minutes, seconds, ms_part, weekday)
2972}
2973
2974/// Convert UTC components to milliseconds since epoch.
2975fn utc_components_to_ms(year: i64, month: i64, day: i64, h: i64, min: i64, s: i64, ms: i64) -> f64 {
2976 // Normalize month overflow.
2977 let y = year + month.div_euclid(12);
2978 let m = month.rem_euclid(12) + 1; // 1-based
2979
2980 // Civil calendar to days (inverse of above).
2981 let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) };
2982 let era = if y2 >= 0 { y2 / 400 } else { (y2 - 399) / 400 };
2983 let yoe = y2 - era * 400;
2984 let doy = (153 * m2 + 2) / 5 + day - 1;
2985 let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
2986 let days = era * 146_097 + doe - 719_468;
2987
2988 (days * 86_400_000 + h * 3_600_000 + min * 60_000 + s * 1000 + ms) as f64
2989}
2990
2991/// Parse a subset of ISO 8601 date strings (YYYY-MM-DDTHH:MM:SS.sssZ).
2992fn parse_date_string(s: &str) -> Option<f64> {
2993 let s = s.trim();
2994 // Try YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD or YYYY
2995 let parts: Vec<&str> = s.splitn(2, 'T').collect();
2996 let date_part = parts[0];
2997 let time_part = parts.get(1).copied().unwrap_or("");
2998
2999 let date_fields: Vec<&str> = date_part.split('-').collect();
3000 if date_fields.is_empty() {
3001 return None;
3002 }
3003
3004 let year: i64 = date_fields[0].parse().ok()?;
3005 let month: i64 = date_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(1);
3006 let day: i64 = date_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(1);
3007
3008 if !(1..=12).contains(&month) || !(1..=31).contains(&day) {
3009 return None;
3010 }
3011
3012 let (h, min, sec, ms) = if time_part.is_empty() {
3013 (0, 0, 0, 0)
3014 } else {
3015 // Strip trailing Z.
3016 let tp = time_part.strip_suffix('Z').unwrap_or(time_part);
3017 // Split seconds from milliseconds.
3018 let (time_str, ms_val) = if let Some((t, m)) = tp.split_once('.') {
3019 let ms_str = &m[..m.len().min(3)];
3020 let mut ms_val: i64 = ms_str.parse().unwrap_or(0);
3021 // Pad to 3 digits if needed.
3022 for _ in ms_str.len()..3 {
3023 ms_val *= 10;
3024 }
3025 (t, ms_val)
3026 } else {
3027 (tp, 0i64)
3028 };
3029 let time_fields: Vec<&str> = time_str.split(':').collect();
3030 let h: i64 = time_fields
3031 .first()
3032 .and_then(|s| s.parse().ok())
3033 .unwrap_or(0);
3034 let min: i64 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0);
3035 let sec: i64 = time_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(0);
3036 (h, min, sec, ms_val)
3037 };
3038
3039 Some(utc_components_to_ms(year, month - 1, day, h, min, sec, ms))
3040}
3041
3042static DAY_NAMES: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
3043static MONTH_NAMES: [&str; 12] = [
3044 "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
3045];
3046
3047fn init_date_builtins(vm: &mut Vm) {
3048 // Date.prototype.
3049 let mut date_proto_data = ObjectData::new();
3050 if let Some(proto) = vm.object_prototype {
3051 date_proto_data.prototype = Some(proto);
3052 }
3053 let date_proto = vm.gc.alloc(HeapObject::Object(date_proto_data));
3054 init_date_prototype(&mut vm.gc, date_proto);
3055
3056 // Store prototype for constructor access.
3057 vm.date_prototype = Some(date_proto);
3058 DATE_PROTO.with(|cell| cell.set(Some(date_proto)));
3059
3060 // Date constructor function.
3061 let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3062 name: "Date".to_string(),
3063 kind: FunctionKind::Native(NativeFunc {
3064 callback: date_constructor,
3065 }),
3066 prototype_obj: Some(date_proto),
3067 properties: HashMap::new(),
3068 upvalues: Vec::new(),
3069 })));
3070
3071 // Static methods.
3072 let now_fn = make_native(&mut vm.gc, "now", date_now);
3073 set_func_prop(&mut vm.gc, ctor, "now", Value::Function(now_fn));
3074
3075 let parse_fn = make_native(&mut vm.gc, "parse", date_parse);
3076 set_func_prop(&mut vm.gc, ctor, "parse", Value::Function(parse_fn));
3077
3078 let utc_fn = make_native(&mut vm.gc, "UTC", date_utc);
3079 set_func_prop(&mut vm.gc, ctor, "UTC", Value::Function(utc_fn));
3080
3081 vm.set_global("Date", Value::Function(ctor));
3082}
3083
3084/// Internal: create a Date object (plain object with __date_ms__ property).
3085fn make_date_obj(gc: &mut Gc<HeapObject>, ms: f64, proto: Option<GcRef>) -> Value {
3086 let mut data = ObjectData::new();
3087 if let Some(p) = proto {
3088 data.prototype = Some(p);
3089 }
3090 data.properties.insert(
3091 "__date_ms__".to_string(),
3092 Property::builtin(Value::Number(ms)),
3093 );
3094 Value::Object(gc.alloc(HeapObject::Object(data)))
3095}
3096
3097/// Extract timestamp from a Date object.
3098fn date_get_ms(gc: &Gc<HeapObject>, this: &Value) -> f64 {
3099 match this {
3100 Value::Object(r) => match gc.get(*r) {
3101 Some(HeapObject::Object(data)) => data
3102 .properties
3103 .get("__date_ms__")
3104 .map(|p| p.value.to_number())
3105 .unwrap_or(f64::NAN),
3106 _ => f64::NAN,
3107 },
3108 _ => f64::NAN,
3109 }
3110}
3111
3112/// Set timestamp on a Date object and return the new value.
3113fn date_set_ms(gc: &mut Gc<HeapObject>, this: &Value, ms: f64) -> f64 {
3114 if let Value::Object(r) = this {
3115 if let Some(HeapObject::Object(data)) = gc.get_mut(*r) {
3116 if let Some(prop) = data.properties.get_mut("__date_ms__") {
3117 prop.value = Value::Number(ms);
3118 }
3119 }
3120 }
3121 ms
3122}
3123
3124fn date_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3125 let proto = DATE_PROTO.with(|cell| cell.get());
3126 let ms = match args.len() {
3127 0 => now_ms(),
3128 1 => match &args[0] {
3129 Value::String(s) => parse_date_string(s).unwrap_or(f64::NAN),
3130 Value::Number(n) => *n,
3131 v => v.to_number(),
3132 },
3133 _ => {
3134 // new Date(year, month, day?, hours?, min?, sec?, ms?)
3135 let year = args[0].to_number() as i64;
3136 let month = args[1].to_number() as i64;
3137 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1);
3138 let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0);
3139 let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0);
3140 let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0);
3141 let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0);
3142 // Years 0-99 map to 1900-1999 per spec.
3143 let year = if (0..100).contains(&year) {
3144 year + 1900
3145 } else {
3146 year
3147 };
3148 utc_components_to_ms(year, month, day, h, min, sec, ms)
3149 }
3150 };
3151 Ok(make_date_obj(ctx.gc, ms, proto))
3152}
3153
3154fn date_now(_args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3155 Ok(Value::Number(now_ms()))
3156}
3157
3158fn date_parse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3159 let s = args
3160 .first()
3161 .map(|v| v.to_js_string(ctx.gc))
3162 .unwrap_or_default();
3163 Ok(Value::Number(parse_date_string(&s).unwrap_or(f64::NAN)))
3164}
3165
3166fn date_utc(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3167 let year = args.first().map(|v| v.to_number() as i64).unwrap_or(1970);
3168 let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0);
3169 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1);
3170 let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0);
3171 let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0);
3172 let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0);
3173 let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0);
3174 let year = if (0..100).contains(&year) {
3175 year + 1900
3176 } else {
3177 year
3178 };
3179 Ok(Value::Number(utc_components_to_ms(
3180 year, month, day, h, min, sec, ms,
3181 )))
3182}
3183
3184fn init_date_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
3185 let methods: &[NativeMethod] = &[
3186 ("getTime", date_get_time),
3187 ("valueOf", date_get_time), // valueOf === getTime
3188 ("getFullYear", date_get_full_year),
3189 ("getMonth", date_get_month),
3190 ("getDate", date_get_date),
3191 ("getDay", date_get_day),
3192 ("getHours", date_get_hours),
3193 ("getMinutes", date_get_minutes),
3194 ("getSeconds", date_get_seconds),
3195 ("getMilliseconds", date_get_milliseconds),
3196 ("getTimezoneOffset", date_get_timezone_offset),
3197 ("getUTCFullYear", date_get_full_year),
3198 ("getUTCMonth", date_get_month),
3199 ("getUTCDate", date_get_date),
3200 ("getUTCDay", date_get_day),
3201 ("getUTCHours", date_get_hours),
3202 ("getUTCMinutes", date_get_minutes),
3203 ("getUTCSeconds", date_get_seconds),
3204 ("getUTCMilliseconds", date_get_milliseconds),
3205 ("setTime", date_set_time),
3206 ("setFullYear", date_set_full_year),
3207 ("setMonth", date_set_month),
3208 ("setDate", date_set_date),
3209 ("setHours", date_set_hours),
3210 ("setMinutes", date_set_minutes),
3211 ("setSeconds", date_set_seconds),
3212 ("setMilliseconds", date_set_milliseconds),
3213 ("toString", date_to_string),
3214 ("toISOString", date_to_iso_string),
3215 ("toUTCString", date_to_utc_string),
3216 ("toLocaleDateString", date_to_locale_date_string),
3217 ("toJSON", date_to_json),
3218 ];
3219 for &(name, cb) in methods {
3220 let f = make_native(gc, name, cb);
3221 set_builtin_prop(gc, proto, name, Value::Function(f));
3222 }
3223}
3224
3225fn date_get_time(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3226 let _ = args;
3227 Ok(Value::Number(date_get_ms(ctx.gc, &ctx.this)))
3228}
3229
3230fn date_get_full_year(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3231 let ms = date_get_ms(ctx.gc, &ctx.this);
3232 if ms.is_nan() {
3233 return Ok(Value::Number(f64::NAN));
3234 }
3235 let (y, ..) = ms_to_utc_components(ms);
3236 Ok(Value::Number(y as f64))
3237}
3238
3239fn date_get_month(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3240 let ms = date_get_ms(ctx.gc, &ctx.this);
3241 if ms.is_nan() {
3242 return Ok(Value::Number(f64::NAN));
3243 }
3244 let (_, m, ..) = ms_to_utc_components(ms);
3245 Ok(Value::Number(m as f64))
3246}
3247
3248fn date_get_date(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3249 let ms = date_get_ms(ctx.gc, &ctx.this);
3250 if ms.is_nan() {
3251 return Ok(Value::Number(f64::NAN));
3252 }
3253 let (_, _, d, ..) = ms_to_utc_components(ms);
3254 Ok(Value::Number(d as f64))
3255}
3256
3257fn date_get_day(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3258 let ms = date_get_ms(ctx.gc, &ctx.this);
3259 if ms.is_nan() {
3260 return Ok(Value::Number(f64::NAN));
3261 }
3262 let (.., wd) = ms_to_utc_components(ms);
3263 Ok(Value::Number(wd as f64))
3264}
3265
3266fn date_get_hours(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3267 let ms = date_get_ms(ctx.gc, &ctx.this);
3268 if ms.is_nan() {
3269 return Ok(Value::Number(f64::NAN));
3270 }
3271 let (_, _, _, h, ..) = ms_to_utc_components(ms);
3272 Ok(Value::Number(h as f64))
3273}
3274
3275fn date_get_minutes(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3276 let ms = date_get_ms(ctx.gc, &ctx.this);
3277 if ms.is_nan() {
3278 return Ok(Value::Number(f64::NAN));
3279 }
3280 let (_, _, _, _, min, ..) = ms_to_utc_components(ms);
3281 Ok(Value::Number(min as f64))
3282}
3283
3284fn date_get_seconds(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3285 let ms = date_get_ms(ctx.gc, &ctx.this);
3286 if ms.is_nan() {
3287 return Ok(Value::Number(f64::NAN));
3288 }
3289 let (_, _, _, _, _, s, ..) = ms_to_utc_components(ms);
3290 Ok(Value::Number(s as f64))
3291}
3292
3293fn date_get_milliseconds(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3294 let ms = date_get_ms(ctx.gc, &ctx.this);
3295 if ms.is_nan() {
3296 return Ok(Value::Number(f64::NAN));
3297 }
3298 let (_, _, _, _, _, _, ms_part, _) = ms_to_utc_components(ms);
3299 Ok(Value::Number(ms_part as f64))
3300}
3301
3302fn date_get_timezone_offset(
3303 _args: &[Value],
3304 _ctx: &mut NativeContext,
3305) -> Result<Value, RuntimeError> {
3306 // We operate in UTC; return 0.
3307 Ok(Value::Number(0.0))
3308}
3309
3310fn date_set_time(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3311 let ms = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
3312 date_set_ms(ctx.gc, &ctx.this, ms);
3313 Ok(Value::Number(ms))
3314}
3315
3316fn date_set_full_year(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3317 let old = date_get_ms(ctx.gc, &ctx.this);
3318 let (_, m, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3319 let year = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3320 let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(m);
3321 let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(d);
3322 let new_ms = utc_components_to_ms(year, month, day, h, min, s, ms);
3323 date_set_ms(ctx.gc, &ctx.this, new_ms);
3324 Ok(Value::Number(new_ms))
3325}
3326
3327fn date_set_month(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3328 let old = date_get_ms(ctx.gc, &ctx.this);
3329 let (y, _, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3330 let month = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3331 let day = args.get(1).map(|v| v.to_number() as i64).unwrap_or(d);
3332 let new_ms = utc_components_to_ms(y, month, day, h, min, s, ms);
3333 date_set_ms(ctx.gc, &ctx.this, new_ms);
3334 Ok(Value::Number(new_ms))
3335}
3336
3337fn date_set_date(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3338 let old = date_get_ms(ctx.gc, &ctx.this);
3339 let (y, m, _, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3340 let day = args.first().map(|v| v.to_number() as i64).unwrap_or(1);
3341 let new_ms = utc_components_to_ms(y, m, day, h, min, s, ms);
3342 date_set_ms(ctx.gc, &ctx.this, new_ms);
3343 Ok(Value::Number(new_ms))
3344}
3345
3346fn date_set_hours(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3347 let old = date_get_ms(ctx.gc, &ctx.this);
3348 let (y, m, d, _, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3349 let h = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3350 let min_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(min);
3351 let sec = args.get(2).map(|v| v.to_number() as i64).unwrap_or(s);
3352 let ms_v = args.get(3).map(|v| v.to_number() as i64).unwrap_or(ms);
3353 let new_ms = utc_components_to_ms(y, m, d, h, min_v, sec, ms_v);
3354 date_set_ms(ctx.gc, &ctx.this, new_ms);
3355 Ok(Value::Number(new_ms))
3356}
3357
3358fn date_set_minutes(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3359 let old = date_get_ms(ctx.gc, &ctx.this);
3360 let (y, m, d, h, _, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3361 let min = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3362 let sec = args.get(1).map(|v| v.to_number() as i64).unwrap_or(s);
3363 let ms_v = args.get(2).map(|v| v.to_number() as i64).unwrap_or(ms);
3364 let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v);
3365 date_set_ms(ctx.gc, &ctx.this, new_ms);
3366 Ok(Value::Number(new_ms))
3367}
3368
3369fn date_set_seconds(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3370 let old = date_get_ms(ctx.gc, &ctx.this);
3371 let (y, m, d, h, min, _, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3372 let sec = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3373 let ms_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(ms);
3374 let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v);
3375 date_set_ms(ctx.gc, &ctx.this, new_ms);
3376 Ok(Value::Number(new_ms))
3377}
3378
3379fn date_set_milliseconds(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3380 let old = date_get_ms(ctx.gc, &ctx.this);
3381 let (y, m, d, h, min, s, _, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old });
3382 let ms_v = args.first().map(|v| v.to_number() as i64).unwrap_or(0);
3383 let new_ms = utc_components_to_ms(y, m, d, h, min, s, ms_v);
3384 date_set_ms(ctx.gc, &ctx.this, new_ms);
3385 Ok(Value::Number(new_ms))
3386}
3387
3388fn date_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3389 let ms = date_get_ms(ctx.gc, &ctx.this);
3390 if ms.is_nan() {
3391 return Ok(Value::String("Invalid Date".to_string()));
3392 }
3393 let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms);
3394 Ok(Value::String(format!(
3395 "{} {} {:02} {} {:02}:{:02}:{:02} GMT",
3396 DAY_NAMES[wd as usize], MONTH_NAMES[m as usize], d, y, h, min, s
3397 )))
3398}
3399
3400fn date_to_iso_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3401 let ms = date_get_ms(ctx.gc, &ctx.this);
3402 if ms.is_nan() {
3403 return Err(RuntimeError::range_error("Invalid time value"));
3404 }
3405 let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms);
3406 Ok(Value::String(format!(
3407 "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
3408 y,
3409 m + 1,
3410 d,
3411 h,
3412 min,
3413 s,
3414 ms_part
3415 )))
3416}
3417
3418fn date_to_utc_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3419 let ms = date_get_ms(ctx.gc, &ctx.this);
3420 if ms.is_nan() {
3421 return Ok(Value::String("Invalid Date".to_string()));
3422 }
3423 let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms);
3424 Ok(Value::String(format!(
3425 "{}, {:02} {} {} {:02}:{:02}:{:02} GMT",
3426 DAY_NAMES[wd as usize], d, MONTH_NAMES[m as usize], y, h, min, s
3427 )))
3428}
3429
3430fn date_to_locale_date_string(
3431 _args: &[Value],
3432 ctx: &mut NativeContext,
3433) -> Result<Value, RuntimeError> {
3434 // Simplified: same as toISOString date portion.
3435 let ms = date_get_ms(ctx.gc, &ctx.this);
3436 if ms.is_nan() {
3437 return Ok(Value::String("Invalid Date".to_string()));
3438 }
3439 let (y, m, d, ..) = ms_to_utc_components(ms);
3440 Ok(Value::String(format!("{:04}-{:02}-{:02}", y, m + 1, d)))
3441}
3442
3443fn date_to_json(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3444 let ms = date_get_ms(ctx.gc, &ctx.this);
3445 if ms.is_nan() {
3446 return Ok(Value::Null);
3447 }
3448 date_to_iso_string(_args, ctx)
3449}
3450
3451// ── RegExp built-in ──────────────────────────────────────────
3452
3453thread_local! {
3454 static REGEXP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3455}
3456
3457fn init_regexp_builtins(vm: &mut Vm) {
3458 // RegExp.prototype.
3459 let mut regexp_proto_data = ObjectData::new();
3460 if let Some(proto) = vm.object_prototype {
3461 regexp_proto_data.prototype = Some(proto);
3462 }
3463 let regexp_proto = vm.gc.alloc(HeapObject::Object(regexp_proto_data));
3464 init_regexp_prototype(&mut vm.gc, regexp_proto);
3465
3466 vm.regexp_prototype = Some(regexp_proto);
3467 REGEXP_PROTO.with(|cell| cell.set(Some(regexp_proto)));
3468
3469 // RegExp constructor function.
3470 let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3471 name: "RegExp".to_string(),
3472 kind: FunctionKind::Native(NativeFunc {
3473 callback: regexp_constructor,
3474 }),
3475 prototype_obj: Some(regexp_proto),
3476 properties: HashMap::new(),
3477 upvalues: Vec::new(),
3478 })));
3479
3480 vm.set_global("RegExp", Value::Function(ctor));
3481}
3482
3483fn init_regexp_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
3484 let methods: &[NativeMethod] = &[
3485 ("test", regexp_proto_test),
3486 ("exec", regexp_proto_exec),
3487 ("toString", regexp_proto_to_string),
3488 ];
3489 for &(name, callback) in methods {
3490 let f = make_native(gc, name, callback);
3491 set_builtin_prop(gc, proto, name, Value::Function(f));
3492 }
3493}
3494
3495/// Create a RegExp object storing compiled regex state in hidden properties.
3496pub fn make_regexp_obj(
3497 gc: &mut Gc<HeapObject>,
3498 pattern: &str,
3499 flags_str: &str,
3500 proto: Option<GcRef>,
3501) -> Result<Value, String> {
3502 use crate::regex::CompiledRegex;
3503
3504 let compiled = CompiledRegex::new(pattern, flags_str)?;
3505 let flags = compiled.flags;
3506
3507 let mut data = ObjectData::new();
3508 if let Some(p) = proto {
3509 data.prototype = Some(p);
3510 }
3511 // Store pattern and flags as properties.
3512 data.properties.insert(
3513 "source".to_string(),
3514 Property::builtin(Value::String(pattern.to_string())),
3515 );
3516 let flags_string = flags.as_flag_string();
3517 data.properties.insert(
3518 "flags".to_string(),
3519 Property::builtin(Value::String(flags_string)),
3520 );
3521 data.properties.insert(
3522 "global".to_string(),
3523 Property::builtin(Value::Boolean(flags.global)),
3524 );
3525 data.properties.insert(
3526 "ignoreCase".to_string(),
3527 Property::builtin(Value::Boolean(flags.ignore_case)),
3528 );
3529 data.properties.insert(
3530 "multiline".to_string(),
3531 Property::builtin(Value::Boolean(flags.multiline)),
3532 );
3533 data.properties.insert(
3534 "dotAll".to_string(),
3535 Property::builtin(Value::Boolean(flags.dot_all)),
3536 );
3537 data.properties.insert(
3538 "unicode".to_string(),
3539 Property::builtin(Value::Boolean(flags.unicode)),
3540 );
3541 data.properties.insert(
3542 "sticky".to_string(),
3543 Property::builtin(Value::Boolean(flags.sticky)),
3544 );
3545 data.properties
3546 .insert("lastIndex".to_string(), Property::data(Value::Number(0.0)));
3547 // Hidden: serialized pattern for re-compilation.
3548 data.properties.insert(
3549 "__regexp_pattern__".to_string(),
3550 Property::builtin(Value::String(pattern.to_string())),
3551 );
3552 data.properties.insert(
3553 "__regexp_flags__".to_string(),
3554 Property::builtin(Value::String(flags.as_flag_string())),
3555 );
3556
3557 Ok(Value::Object(gc.alloc(HeapObject::Object(data))))
3558}
3559
3560/// Check if a Value is a RegExp object.
3561pub fn is_regexp(gc: &Gc<HeapObject>, val: &Value) -> bool {
3562 match val {
3563 Value::Object(r) => match gc.get(*r) {
3564 Some(HeapObject::Object(data)) => data.properties.contains_key("__regexp_pattern__"),
3565 _ => false,
3566 },
3567 _ => false,
3568 }
3569}
3570
3571/// Extract the pattern from a RegExp object.
3572fn regexp_get_pattern(gc: &Gc<HeapObject>, val: &Value) -> Option<String> {
3573 match val {
3574 Value::Object(r) => match gc.get(*r) {
3575 Some(HeapObject::Object(data)) => {
3576 data.properties
3577 .get("__regexp_pattern__")
3578 .and_then(|p| match &p.value {
3579 Value::String(s) => Some(s.clone()),
3580 _ => None,
3581 })
3582 }
3583 _ => None,
3584 },
3585 _ => None,
3586 }
3587}
3588
3589/// Extract the flags string from a RegExp object.
3590fn regexp_get_flags(gc: &Gc<HeapObject>, val: &Value) -> Option<String> {
3591 match val {
3592 Value::Object(r) => match gc.get(*r) {
3593 Some(HeapObject::Object(data)) => {
3594 data.properties
3595 .get("__regexp_flags__")
3596 .and_then(|p| match &p.value {
3597 Value::String(s) => Some(s.clone()),
3598 _ => None,
3599 })
3600 }
3601 _ => None,
3602 },
3603 _ => None,
3604 }
3605}
3606
3607/// Get lastIndex from a RegExp object.
3608fn regexp_get_last_index(gc: &Gc<HeapObject>, val: &Value) -> f64 {
3609 match val {
3610 Value::Object(r) => match gc.get(*r) {
3611 Some(HeapObject::Object(data)) => data
3612 .properties
3613 .get("lastIndex")
3614 .map(|p| p.value.to_number())
3615 .unwrap_or(0.0),
3616 _ => 0.0,
3617 },
3618 _ => 0.0,
3619 }
3620}
3621
3622/// Set lastIndex on a RegExp object.
3623fn regexp_set_last_index(gc: &mut Gc<HeapObject>, val: &Value, idx: f64) {
3624 if let Value::Object(r) = val {
3625 if let Some(HeapObject::Object(data)) = gc.get_mut(*r) {
3626 if let Some(prop) = data.properties.get_mut("lastIndex") {
3627 prop.value = Value::Number(idx);
3628 }
3629 }
3630 }
3631}
3632
3633/// Execute the regex on a string and return a match result array or null.
3634fn regexp_exec_internal(
3635 gc: &mut Gc<HeapObject>,
3636 this: &Value,
3637 input: &str,
3638) -> Result<Value, RuntimeError> {
3639 use crate::regex::{exec, CompiledRegex};
3640
3641 let pattern = regexp_get_pattern(gc, this)
3642 .ok_or_else(|| RuntimeError::type_error("not a RegExp".to_string()))?;
3643 let flags_str = regexp_get_flags(gc, this).unwrap_or_default();
3644 let compiled = CompiledRegex::new(&pattern, &flags_str).map_err(RuntimeError::syntax_error)?;
3645 let is_global = compiled.flags.global;
3646 let is_sticky = compiled.flags.sticky;
3647
3648 let start_index = if is_global || is_sticky {
3649 let li = regexp_get_last_index(gc, this);
3650 if li < 0.0 {
3651 0
3652 } else {
3653 li as usize
3654 }
3655 } else {
3656 0
3657 };
3658
3659 let chars: Vec<char> = input.chars().collect();
3660 let result = exec(&compiled, input, start_index);
3661
3662 match result {
3663 Some(m) => {
3664 if is_global || is_sticky {
3665 regexp_set_last_index(gc, this, m.end as f64);
3666 }
3667
3668 // Build result array: [fullMatch, ...groups]
3669 let mut items: Vec<Value> = Vec::new();
3670
3671 // Full match (index 0).
3672 let full: String = chars[m.start..m.end].iter().collect();
3673 items.push(Value::String(full));
3674
3675 // Capture groups (index 1..n).
3676 for i in 1..m.captures.len() {
3677 match m.captures[i] {
3678 Some((s, e)) => {
3679 let cap: String = chars[s..e].iter().collect();
3680 items.push(Value::String(cap));
3681 }
3682 None => items.push(Value::Undefined),
3683 }
3684 }
3685
3686 // Build named groups object (if any) before creating the array.
3687 let groups_val = {
3688 let (node, _) =
3689 crate::regex::parse_pattern(&pattern).map_err(RuntimeError::syntax_error)?;
3690 let named = collect_named_groups(&node);
3691 if named.is_empty() {
3692 Value::Undefined
3693 } else {
3694 let mut groups_data = ObjectData::new();
3695 for (name, idx) in &named {
3696 let cap_idx = *idx as usize;
3697 let val = if cap_idx < m.captures.len() {
3698 match m.captures[cap_idx] {
3699 Some((s, e)) => {
3700 let cap: String = chars[s..e].iter().collect();
3701 Value::String(cap)
3702 }
3703 None => Value::Undefined,
3704 }
3705 } else {
3706 Value::Undefined
3707 };
3708 groups_data
3709 .properties
3710 .insert(name.clone(), Property::data(val));
3711 }
3712 Value::Object(gc.alloc(HeapObject::Object(groups_data)))
3713 }
3714 };
3715
3716 let arr = make_value_array(gc, &items);
3717 // Set index, input, and groups properties on the result array.
3718 if let Value::Object(r) = arr {
3719 if let Some(HeapObject::Object(data)) = gc.get_mut(r) {
3720 data.properties.insert(
3721 "index".to_string(),
3722 Property::data(Value::Number(m.start as f64)),
3723 );
3724 data.properties.insert(
3725 "input".to_string(),
3726 Property::data(Value::String(input.to_string())),
3727 );
3728 data.properties
3729 .insert("groups".to_string(), Property::data(groups_val));
3730 }
3731 Ok(Value::Object(r))
3732 } else {
3733 Ok(arr)
3734 }
3735 }
3736 None => {
3737 if is_global || is_sticky {
3738 regexp_set_last_index(gc, this, 0.0);
3739 }
3740 Ok(Value::Null)
3741 }
3742 }
3743}
3744
3745/// Collect named groups from a regex AST node.
3746fn collect_named_groups(node: &crate::regex::Node) -> Vec<(String, u32)> {
3747 use crate::regex::Node;
3748 let mut result = Vec::new();
3749 match node {
3750 Node::Group {
3751 index,
3752 name: Some(name),
3753 node: inner,
3754 } => {
3755 result.push((name.clone(), *index));
3756 result.extend(collect_named_groups(inner));
3757 }
3758 Node::Group { node: inner, .. }
3759 | Node::NonCapturingGroup(inner)
3760 | Node::Lookahead(inner)
3761 | Node::NegativeLookahead(inner) => {
3762 result.extend(collect_named_groups(inner));
3763 }
3764 Node::Quantifier { node: inner, .. } => {
3765 result.extend(collect_named_groups(inner));
3766 }
3767 Node::Sequence(nodes) | Node::Alternation(nodes) => {
3768 for n in nodes {
3769 result.extend(collect_named_groups(n));
3770 }
3771 }
3772 _ => {}
3773 }
3774 result
3775}
3776
3777fn regexp_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3778 let proto = REGEXP_PROTO.with(|cell| cell.get());
3779 let pattern = args
3780 .first()
3781 .map(|v| v.to_js_string(ctx.gc))
3782 .unwrap_or_default();
3783 let flags = args
3784 .get(1)
3785 .map(|v| {
3786 if matches!(v, Value::Undefined) {
3787 String::new()
3788 } else {
3789 v.to_js_string(ctx.gc)
3790 }
3791 })
3792 .unwrap_or_default();
3793
3794 make_regexp_obj(ctx.gc, &pattern, &flags, proto).map_err(RuntimeError::syntax_error)
3795}
3796
3797fn regexp_proto_test(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3798 let input = args
3799 .first()
3800 .map(|v| v.to_js_string(ctx.gc))
3801 .unwrap_or_default();
3802 let result = regexp_exec_internal(ctx.gc, &ctx.this, &input)?;
3803 Ok(Value::Boolean(!matches!(result, Value::Null)))
3804}
3805
3806fn regexp_proto_exec(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3807 let input = args
3808 .first()
3809 .map(|v| v.to_js_string(ctx.gc))
3810 .unwrap_or_default();
3811 regexp_exec_internal(ctx.gc, &ctx.this, &input)
3812}
3813
3814fn regexp_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
3815 let pattern = regexp_get_pattern(ctx.gc, &ctx.this).unwrap_or_default();
3816 let flags = regexp_get_flags(ctx.gc, &ctx.this).unwrap_or_default();
3817 Ok(Value::String(format!("/{}/{}", pattern, flags)))
3818}
3819
3820// ── Map built-in ─────────────────────────────────────────────
3821
3822thread_local! {
3823 static MAP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3824 static SET_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3825 static WEAKMAP_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3826 static WEAKSET_PROTO: std::cell::Cell<Option<GcRef>> = const { std::cell::Cell::new(None) };
3827}
3828
3829fn init_map_set_builtins(vm: &mut Vm) {
3830 // ── Map ──
3831 let mut map_proto_data = ObjectData::new();
3832 if let Some(proto) = vm.object_prototype {
3833 map_proto_data.prototype = Some(proto);
3834 }
3835 let map_proto = vm.gc.alloc(HeapObject::Object(map_proto_data));
3836 init_map_prototype(&mut vm.gc, map_proto);
3837 MAP_PROTO.with(|cell| cell.set(Some(map_proto)));
3838
3839 let map_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3840 name: "Map".to_string(),
3841 kind: FunctionKind::Native(NativeFunc {
3842 callback: map_constructor,
3843 }),
3844 prototype_obj: Some(map_proto),
3845 properties: HashMap::new(),
3846 upvalues: Vec::new(),
3847 })));
3848 vm.set_global("Map", Value::Function(map_ctor));
3849
3850 // ── Set ──
3851 let mut set_proto_data = ObjectData::new();
3852 if let Some(proto) = vm.object_prototype {
3853 set_proto_data.prototype = Some(proto);
3854 }
3855 let set_proto = vm.gc.alloc(HeapObject::Object(set_proto_data));
3856 init_set_prototype(&mut vm.gc, set_proto);
3857 SET_PROTO.with(|cell| cell.set(Some(set_proto)));
3858
3859 let set_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3860 name: "Set".to_string(),
3861 kind: FunctionKind::Native(NativeFunc {
3862 callback: set_constructor,
3863 }),
3864 prototype_obj: Some(set_proto),
3865 properties: HashMap::new(),
3866 upvalues: Vec::new(),
3867 })));
3868 vm.set_global("Set", Value::Function(set_ctor));
3869
3870 // ── WeakMap ──
3871 let mut wm_proto_data = ObjectData::new();
3872 if let Some(proto) = vm.object_prototype {
3873 wm_proto_data.prototype = Some(proto);
3874 }
3875 let wm_proto = vm.gc.alloc(HeapObject::Object(wm_proto_data));
3876 init_weakmap_prototype(&mut vm.gc, wm_proto);
3877 WEAKMAP_PROTO.with(|cell| cell.set(Some(wm_proto)));
3878
3879 let wm_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3880 name: "WeakMap".to_string(),
3881 kind: FunctionKind::Native(NativeFunc {
3882 callback: weakmap_constructor,
3883 }),
3884 prototype_obj: Some(wm_proto),
3885 properties: HashMap::new(),
3886 upvalues: Vec::new(),
3887 })));
3888 vm.set_global("WeakMap", Value::Function(wm_ctor));
3889
3890 // ── WeakSet ──
3891 let mut ws_proto_data = ObjectData::new();
3892 if let Some(proto) = vm.object_prototype {
3893 ws_proto_data.prototype = Some(proto);
3894 }
3895 let ws_proto = vm.gc.alloc(HeapObject::Object(ws_proto_data));
3896 init_weakset_prototype(&mut vm.gc, ws_proto);
3897 WEAKSET_PROTO.with(|cell| cell.set(Some(ws_proto)));
3898
3899 let ws_ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData {
3900 name: "WeakSet".to_string(),
3901 kind: FunctionKind::Native(NativeFunc {
3902 callback: weakset_constructor,
3903 }),
3904 prototype_obj: Some(ws_proto),
3905 properties: HashMap::new(),
3906 upvalues: Vec::new(),
3907 })));
3908 vm.set_global("WeakSet", Value::Function(ws_ctor));
3909}
3910
3911// ── Map / Set internal helpers ───────────────────────────────
3912
3913/// Internal storage key for Map/Set entries.
3914/// We store entries as a hidden object with indexed key/value pairs:
3915/// __entries__: GcRef to an object with "0_k", "0_v", "1_k", "1_v", ...
3916/// __entry_count__: total slots allocated (some may be deleted)
3917/// __live_count__: number of non-deleted entries
3918/// Deleted entries have their key property removed from the entries object.
3919const ENTRIES_KEY: &str = "__entries__";
3920const ENTRY_COUNT_KEY: &str = "__entry_count__";
3921const LIVE_COUNT_KEY: &str = "__live_count__";
3922
3923/// Create a new empty Map/Set internal storage object.
3924fn make_collection_obj(gc: &mut Gc<HeapObject>, proto: Option<GcRef>) -> GcRef {
3925 let entries_obj = gc.alloc(HeapObject::Object(ObjectData::new()));
3926 let mut data = ObjectData::new();
3927 if let Some(p) = proto {
3928 data.prototype = Some(p);
3929 }
3930 data.properties.insert(
3931 ENTRIES_KEY.to_string(),
3932 Property::builtin(Value::Object(entries_obj)),
3933 );
3934 data.properties.insert(
3935 ENTRY_COUNT_KEY.to_string(),
3936 Property::builtin(Value::Number(0.0)),
3937 );
3938 data.properties.insert(
3939 LIVE_COUNT_KEY.to_string(),
3940 Property::builtin(Value::Number(0.0)),
3941 );
3942 // size is a read-only, non-enumerable property.
3943 data.properties
3944 .insert("size".to_string(), Property::builtin(Value::Number(0.0)));
3945 gc.alloc(HeapObject::Object(data))
3946}
3947
3948/// Get the entries object GcRef from a Map/Set object.
3949fn collection_entries(gc: &Gc<HeapObject>, obj: &Value) -> Option<GcRef> {
3950 let gc_ref = obj.gc_ref()?;
3951 let heap = gc.get(gc_ref)?;
3952 if let HeapObject::Object(data) = heap {
3953 if let Some(prop) = data.properties.get(ENTRIES_KEY) {
3954 return prop.value.gc_ref();
3955 }
3956 }
3957 None
3958}
3959
3960/// Get the entry count from a Map/Set object.
3961fn collection_entry_count(gc: &Gc<HeapObject>, obj: &Value) -> usize {
3962 let gc_ref = match obj.gc_ref() {
3963 Some(r) => r,
3964 None => return 0,
3965 };
3966 match gc.get(gc_ref) {
3967 Some(HeapObject::Object(data)) => data
3968 .properties
3969 .get(ENTRY_COUNT_KEY)
3970 .map(|p| p.value.to_number() as usize)
3971 .unwrap_or(0),
3972 _ => 0,
3973 }
3974}
3975
3976/// Get the live count from a Map/Set object.
3977fn collection_live_count(gc: &Gc<HeapObject>, obj: &Value) -> usize {
3978 let gc_ref = match obj.gc_ref() {
3979 Some(r) => r,
3980 None => return 0,
3981 };
3982 match gc.get(gc_ref) {
3983 Some(HeapObject::Object(data)) => data
3984 .properties
3985 .get(LIVE_COUNT_KEY)
3986 .map(|p| p.value.to_number() as usize)
3987 .unwrap_or(0),
3988 _ => 0,
3989 }
3990}
3991
3992/// Set the entry count on a Map/Set object and update the `size` property.
3993fn set_collection_count(gc: &mut Gc<HeapObject>, obj: &Value, entry_count: usize, live: usize) {
3994 let gc_ref = match obj.gc_ref() {
3995 Some(r) => r,
3996 None => return,
3997 };
3998 if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) {
3999 data.properties.insert(
4000 ENTRY_COUNT_KEY.to_string(),
4001 Property::builtin(Value::Number(entry_count as f64)),
4002 );
4003 data.properties.insert(
4004 LIVE_COUNT_KEY.to_string(),
4005 Property::builtin(Value::Number(live as f64)),
4006 );
4007 data.properties.insert(
4008 "size".to_string(),
4009 Property::builtin(Value::Number(live as f64)),
4010 );
4011 }
4012}
4013
4014/// Get the key at index `i` in the entries object (returns None if deleted or missing).
4015fn entry_key_at(gc: &Gc<HeapObject>, entries: GcRef, i: usize) -> Option<Value> {
4016 match gc.get(entries) {
4017 Some(HeapObject::Object(data)) => {
4018 let k = format!("{i}_k");
4019 data.properties.get(&k).map(|p| p.value.clone())
4020 }
4021 _ => None,
4022 }
4023}
4024
4025/// Get the value at index `i` in the entries object.
4026fn entry_value_at(gc: &Gc<HeapObject>, entries: GcRef, i: usize) -> Value {
4027 match gc.get(entries) {
4028 Some(HeapObject::Object(data)) => {
4029 let v = format!("{i}_v");
4030 data.properties
4031 .get(&v)
4032 .map(|p| p.value.clone())
4033 .unwrap_or(Value::Undefined)
4034 }
4035 _ => Value::Undefined,
4036 }
4037}
4038
4039/// Set an entry at index `i`.
4040fn set_entry_at(gc: &mut Gc<HeapObject>, entries: GcRef, i: usize, key: Value, value: Value) {
4041 if let Some(HeapObject::Object(data)) = gc.get_mut(entries) {
4042 data.properties
4043 .insert(format!("{i}_k"), Property::builtin(key));
4044 data.properties
4045 .insert(format!("{i}_v"), Property::builtin(value));
4046 }
4047}
4048
4049/// Mark entry at index `i` as deleted by removing its key and value properties.
4050fn delete_entry_at(gc: &mut Gc<HeapObject>, entries: GcRef, i: usize) {
4051 if let Some(HeapObject::Object(data)) = gc.get_mut(entries) {
4052 data.properties.remove(&format!("{i}_k"));
4053 data.properties.remove(&format!("{i}_v"));
4054 }
4055}
4056
4057/// Find the index of a key in the entries, using SameValueZero.
4058fn find_key_index(gc: &Gc<HeapObject>, entries: GcRef, count: usize, key: &Value) -> Option<usize> {
4059 for i in 0..count {
4060 if let Some(existing) = entry_key_at(gc, entries, i) {
4061 if same_value_zero(&existing, key) {
4062 return Some(i);
4063 }
4064 }
4065 }
4066 None
4067}
4068
4069// ── Map constructor & prototype ──────────────────────────────
4070
4071fn map_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4072 let proto = MAP_PROTO.with(|cell| cell.get());
4073 let obj_ref = make_collection_obj(ctx.gc, proto);
4074 let obj = Value::Object(obj_ref);
4075
4076 // If an iterable argument is provided, add entries from it.
4077 // We support arrays of [key, value] pairs.
4078 if let Some(arg) = args.first() {
4079 if !matches!(arg, Value::Undefined | Value::Null) {
4080 if let Some(arr_ref) = arg.gc_ref() {
4081 let len = array_length(ctx.gc, arr_ref);
4082 for i in 0..len {
4083 let pair = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string());
4084 if let Some(pair_ref) = pair.gc_ref() {
4085 let k = get_property(ctx.gc, &Value::Object(pair_ref), "0");
4086 let v = get_property(ctx.gc, &Value::Object(pair_ref), "1");
4087 map_set_internal(ctx.gc, &obj, k, v);
4088 }
4089 }
4090 }
4091 }
4092 }
4093
4094 Ok(obj)
4095}
4096
4097fn map_set_internal(gc: &mut Gc<HeapObject>, map: &Value, key: Value, value: Value) {
4098 let entries = match collection_entries(gc, map) {
4099 Some(e) => e,
4100 None => return,
4101 };
4102 let count = collection_entry_count(gc, map);
4103 let live = collection_live_count(gc, map);
4104
4105 // Check if key already exists.
4106 if let Some(idx) = find_key_index(gc, entries, count, &key) {
4107 set_entry_at(gc, entries, idx, key, value);
4108 return;
4109 }
4110
4111 // Add new entry.
4112 set_entry_at(gc, entries, count, key, value);
4113 set_collection_count(gc, map, count + 1, live + 1);
4114}
4115
4116fn init_map_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
4117 let methods: &[NativeMethod] = &[
4118 ("set", map_proto_set),
4119 ("get", map_proto_get),
4120 ("has", map_proto_has),
4121 ("delete", map_proto_delete),
4122 ("clear", map_proto_clear),
4123 ("forEach", map_proto_for_each),
4124 ("keys", map_proto_keys),
4125 ("values", map_proto_values),
4126 ("entries", map_proto_entries),
4127 ];
4128 for &(name, callback) in methods {
4129 let f = make_native(gc, name, callback);
4130 set_builtin_prop(gc, proto, name, Value::Function(f));
4131 }
4132}
4133
4134fn map_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4135 let key = args.first().cloned().unwrap_or(Value::Undefined);
4136 let value = args.get(1).cloned().unwrap_or(Value::Undefined);
4137 // Normalize -0 to +0 for key.
4138 let key = normalize_zero(key);
4139 map_set_internal(ctx.gc, &ctx.this, key, value);
4140 Ok(ctx.this.clone())
4141}
4142
4143fn map_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4144 let key = args.first().cloned().unwrap_or(Value::Undefined);
4145 let key = normalize_zero(key);
4146 let entries = match collection_entries(ctx.gc, &ctx.this) {
4147 Some(e) => e,
4148 None => return Ok(Value::Undefined),
4149 };
4150 let count = collection_entry_count(ctx.gc, &ctx.this);
4151 if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) {
4152 return Ok(entry_value_at(ctx.gc, entries, idx));
4153 }
4154 Ok(Value::Undefined)
4155}
4156
4157fn map_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4158 let key = args.first().cloned().unwrap_or(Value::Undefined);
4159 let key = normalize_zero(key);
4160 let entries = match collection_entries(ctx.gc, &ctx.this) {
4161 Some(e) => e,
4162 None => return Ok(Value::Boolean(false)),
4163 };
4164 let count = collection_entry_count(ctx.gc, &ctx.this);
4165 Ok(Value::Boolean(
4166 find_key_index(ctx.gc, entries, count, &key).is_some(),
4167 ))
4168}
4169
4170fn map_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4171 let key = args.first().cloned().unwrap_or(Value::Undefined);
4172 let key = normalize_zero(key);
4173 let entries = match collection_entries(ctx.gc, &ctx.this) {
4174 Some(e) => e,
4175 None => return Ok(Value::Boolean(false)),
4176 };
4177 let count = collection_entry_count(ctx.gc, &ctx.this);
4178 let live = collection_live_count(ctx.gc, &ctx.this);
4179 if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) {
4180 delete_entry_at(ctx.gc, entries, idx);
4181 set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1));
4182 return Ok(Value::Boolean(true));
4183 }
4184 Ok(Value::Boolean(false))
4185}
4186
4187fn map_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4188 clear_collection(ctx.gc, &ctx.this);
4189 Ok(Value::Undefined)
4190}
4191
4192/// Clear all entries from a Map/Set collection.
4193fn clear_collection(gc: &mut Gc<HeapObject>, obj: &Value) {
4194 let new_entries = gc.alloc(HeapObject::Object(ObjectData::new()));
4195 if let Some(gc_ref) = obj.gc_ref() {
4196 if let Some(HeapObject::Object(data)) = gc.get_mut(gc_ref) {
4197 data.properties.insert(
4198 ENTRIES_KEY.to_string(),
4199 Property::builtin(Value::Object(new_entries)),
4200 );
4201 data.properties.insert(
4202 ENTRY_COUNT_KEY.to_string(),
4203 Property::builtin(Value::Number(0.0)),
4204 );
4205 data.properties.insert(
4206 LIVE_COUNT_KEY.to_string(),
4207 Property::builtin(Value::Number(0.0)),
4208 );
4209 data.properties
4210 .insert("size".to_string(), Property::builtin(Value::Number(0.0)));
4211 }
4212 }
4213}
4214
4215fn map_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4216 let callback = match args.first() {
4217 Some(Value::Function(r)) => *r,
4218 _ => {
4219 return Err(RuntimeError::type_error(
4220 "Map.prototype.forEach requires a function argument",
4221 ))
4222 }
4223 };
4224 let entries = match collection_entries(ctx.gc, &ctx.this) {
4225 Some(e) => e,
4226 None => return Ok(Value::Undefined),
4227 };
4228 let count = collection_entry_count(ctx.gc, &ctx.this);
4229 // Collect entries first to avoid borrow issues.
4230 let mut pairs = Vec::new();
4231 for i in 0..count {
4232 if let Some(k) = entry_key_at(ctx.gc, entries, i) {
4233 let v = entry_value_at(ctx.gc, entries, i);
4234 pairs.push((k, v));
4235 }
4236 }
4237 let this_val = ctx.this.clone();
4238 for (k, v) in pairs {
4239 call_native_callback(ctx.gc, callback, &[v, k, this_val.clone()])?;
4240 }
4241 Ok(Value::Undefined)
4242}
4243
4244fn map_proto_keys(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4245 map_proto_iter(args, ctx, IterKind::Keys)
4246}
4247
4248fn map_proto_values(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4249 map_proto_iter(args, ctx, IterKind::Values)
4250}
4251
4252fn map_proto_entries(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4253 map_proto_iter(args, ctx, IterKind::Entries)
4254}
4255
4256enum IterKind {
4257 Keys,
4258 Values,
4259 Entries,
4260}
4261
4262fn map_proto_iter(
4263 _args: &[Value],
4264 ctx: &mut NativeContext,
4265 kind: IterKind,
4266) -> Result<Value, RuntimeError> {
4267 let entries = match collection_entries(ctx.gc, &ctx.this) {
4268 Some(e) => e,
4269 None => return Ok(make_value_array(ctx.gc, &[])),
4270 };
4271 let count = collection_entry_count(ctx.gc, &ctx.this);
4272 let mut items = Vec::new();
4273 for i in 0..count {
4274 if let Some(k) = entry_key_at(ctx.gc, entries, i) {
4275 match kind {
4276 IterKind::Keys => items.push(k),
4277 IterKind::Values => {
4278 let v = entry_value_at(ctx.gc, entries, i);
4279 items.push(v);
4280 }
4281 IterKind::Entries => {
4282 let v = entry_value_at(ctx.gc, entries, i);
4283 let pair = make_value_array(ctx.gc, &[k, v]);
4284 items.push(pair);
4285 }
4286 }
4287 }
4288 }
4289 Ok(make_value_array(ctx.gc, &items))
4290}
4291
4292/// Normalize -0 to +0 for Map/Set key equality.
4293fn normalize_zero(val: Value) -> Value {
4294 if let Value::Number(n) = &val {
4295 if *n == 0.0 {
4296 return Value::Number(0.0);
4297 }
4298 }
4299 val
4300}
4301
4302/// Helper to get a property from an object value by key (used for reading iterable pairs).
4303fn get_property(gc: &Gc<HeapObject>, obj: &Value, key: &str) -> Value {
4304 let gc_ref = match obj.gc_ref() {
4305 Some(r) => r,
4306 None => return Value::Undefined,
4307 };
4308 match gc.get(gc_ref) {
4309 Some(HeapObject::Object(data)) => data
4310 .properties
4311 .get(key)
4312 .map(|p| p.value.clone())
4313 .unwrap_or(Value::Undefined),
4314 _ => Value::Undefined,
4315 }
4316}
4317
4318/// Call a native callback function (for forEach).
4319fn call_native_callback(
4320 gc: &mut Gc<HeapObject>,
4321 func_ref: GcRef,
4322 args: &[Value],
4323) -> Result<Value, RuntimeError> {
4324 match gc.get(func_ref) {
4325 Some(HeapObject::Function(fdata)) => match &fdata.kind {
4326 FunctionKind::Native(native) => {
4327 let cb = native.callback;
4328 let mut ctx = NativeContext {
4329 gc,
4330 this: Value::Undefined,
4331 };
4332 cb(args, &mut ctx)
4333 }
4334 FunctionKind::Bytecode(_) => Err(RuntimeError::type_error(
4335 "bytecode callbacks in forEach not yet supported",
4336 )),
4337 },
4338 _ => Err(RuntimeError::type_error("not a function")),
4339 }
4340}
4341
4342// ── Set constructor & prototype ──────────────────────────────
4343
4344fn set_constructor(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4345 let proto = SET_PROTO.with(|cell| cell.get());
4346 let obj_ref = make_collection_obj(ctx.gc, proto);
4347 let obj = Value::Object(obj_ref);
4348
4349 // If an iterable argument is provided, add values from it.
4350 if let Some(arg) = args.first() {
4351 if !matches!(arg, Value::Undefined | Value::Null) {
4352 if let Some(arr_ref) = arg.gc_ref() {
4353 let len = array_length(ctx.gc, arr_ref);
4354 for i in 0..len {
4355 let v = get_property(ctx.gc, &Value::Object(arr_ref), &i.to_string());
4356 set_add_internal(ctx.gc, &obj, v);
4357 }
4358 }
4359 }
4360 }
4361
4362 Ok(obj)
4363}
4364
4365fn set_add_internal(gc: &mut Gc<HeapObject>, set: &Value, value: Value) {
4366 let entries = match collection_entries(gc, set) {
4367 Some(e) => e,
4368 None => return,
4369 };
4370 let count = collection_entry_count(gc, set);
4371 let live = collection_live_count(gc, set);
4372
4373 // Check if value already exists.
4374 if find_key_index(gc, entries, count, &value).is_some() {
4375 return;
4376 }
4377
4378 // Add new entry (value stored as key, value slot unused).
4379 set_entry_at(gc, entries, count, value, Value::Undefined);
4380 set_collection_count(gc, set, count + 1, live + 1);
4381}
4382
4383fn init_set_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
4384 let methods: &[NativeMethod] = &[
4385 ("add", set_proto_add),
4386 ("has", set_proto_has),
4387 ("delete", set_proto_delete),
4388 ("clear", set_proto_clear),
4389 ("forEach", set_proto_for_each),
4390 ("keys", set_proto_keys),
4391 ("values", set_proto_values),
4392 ("entries", set_proto_entries),
4393 ];
4394 for &(name, callback) in methods {
4395 let f = make_native(gc, name, callback);
4396 set_builtin_prop(gc, proto, name, Value::Function(f));
4397 }
4398}
4399
4400fn set_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4401 let value = args.first().cloned().unwrap_or(Value::Undefined);
4402 let value = normalize_zero(value);
4403 set_add_internal(ctx.gc, &ctx.this, value);
4404 Ok(ctx.this.clone())
4405}
4406
4407fn set_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4408 let value = args.first().cloned().unwrap_or(Value::Undefined);
4409 let value = normalize_zero(value);
4410 let entries = match collection_entries(ctx.gc, &ctx.this) {
4411 Some(e) => e,
4412 None => return Ok(Value::Boolean(false)),
4413 };
4414 let count = collection_entry_count(ctx.gc, &ctx.this);
4415 Ok(Value::Boolean(
4416 find_key_index(ctx.gc, entries, count, &value).is_some(),
4417 ))
4418}
4419
4420fn set_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4421 let value = args.first().cloned().unwrap_or(Value::Undefined);
4422 let value = normalize_zero(value);
4423 let entries = match collection_entries(ctx.gc, &ctx.this) {
4424 Some(e) => e,
4425 None => return Ok(Value::Boolean(false)),
4426 };
4427 let count = collection_entry_count(ctx.gc, &ctx.this);
4428 let live = collection_live_count(ctx.gc, &ctx.this);
4429 if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) {
4430 delete_entry_at(ctx.gc, entries, idx);
4431 set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1));
4432 return Ok(Value::Boolean(true));
4433 }
4434 Ok(Value::Boolean(false))
4435}
4436
4437fn set_proto_clear(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4438 clear_collection(ctx.gc, &ctx.this);
4439 Ok(Value::Undefined)
4440}
4441
4442fn set_proto_for_each(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4443 let callback = match args.first() {
4444 Some(Value::Function(r)) => *r,
4445 _ => {
4446 return Err(RuntimeError::type_error(
4447 "Set.prototype.forEach requires a function argument",
4448 ))
4449 }
4450 };
4451 let entries = match collection_entries(ctx.gc, &ctx.this) {
4452 Some(e) => e,
4453 None => return Ok(Value::Undefined),
4454 };
4455 let count = collection_entry_count(ctx.gc, &ctx.this);
4456 let mut values = Vec::new();
4457 for i in 0..count {
4458 if let Some(k) = entry_key_at(ctx.gc, entries, i) {
4459 values.push(k);
4460 }
4461 }
4462 let this_val = ctx.this.clone();
4463 for v in values {
4464 call_native_callback(ctx.gc, callback, &[v.clone(), v, this_val.clone()])?;
4465 }
4466 Ok(Value::Undefined)
4467}
4468
4469fn set_proto_keys(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4470 set_proto_values(_args, ctx)
4471}
4472
4473fn set_proto_values(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4474 let entries = match collection_entries(ctx.gc, &ctx.this) {
4475 Some(e) => e,
4476 None => return Ok(make_value_array(ctx.gc, &[])),
4477 };
4478 let count = collection_entry_count(ctx.gc, &ctx.this);
4479 let mut items = Vec::new();
4480 for i in 0..count {
4481 if let Some(k) = entry_key_at(ctx.gc, entries, i) {
4482 items.push(k);
4483 }
4484 }
4485 Ok(make_value_array(ctx.gc, &items))
4486}
4487
4488fn set_proto_entries(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4489 let entries = match collection_entries(ctx.gc, &ctx.this) {
4490 Some(e) => e,
4491 None => return Ok(make_value_array(ctx.gc, &[])),
4492 };
4493 let count = collection_entry_count(ctx.gc, &ctx.this);
4494 let mut items = Vec::new();
4495 for i in 0..count {
4496 if let Some(k) = entry_key_at(ctx.gc, entries, i) {
4497 let pair = make_value_array(ctx.gc, &[k.clone(), k]);
4498 items.push(pair);
4499 }
4500 }
4501 Ok(make_value_array(ctx.gc, &items))
4502}
4503
4504// ── WeakMap constructor & prototype ──────────────────────────
4505
4506fn weakmap_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4507 let proto = WEAKMAP_PROTO.with(|cell| cell.get());
4508 let obj_ref = make_collection_obj(ctx.gc, proto);
4509 Ok(Value::Object(obj_ref))
4510}
4511
4512fn is_object_value(val: &Value) -> bool {
4513 matches!(val, Value::Object(_) | Value::Function(_))
4514}
4515
4516fn init_weakmap_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
4517 let methods: &[NativeMethod] = &[
4518 ("set", weakmap_proto_set),
4519 ("get", weakmap_proto_get),
4520 ("has", weakmap_proto_has),
4521 ("delete", weakmap_proto_delete),
4522 ];
4523 for &(name, callback) in methods {
4524 let f = make_native(gc, name, callback);
4525 set_builtin_prop(gc, proto, name, Value::Function(f));
4526 }
4527}
4528
4529fn weakmap_proto_set(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4530 let key = args.first().cloned().unwrap_or(Value::Undefined);
4531 if !is_object_value(&key) {
4532 return Err(RuntimeError::type_error("WeakMap key must be an object"));
4533 }
4534 let value = args.get(1).cloned().unwrap_or(Value::Undefined);
4535 map_set_internal(ctx.gc, &ctx.this, key, value);
4536 Ok(ctx.this.clone())
4537}
4538
4539fn weakmap_proto_get(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4540 let key = args.first().cloned().unwrap_or(Value::Undefined);
4541 if !is_object_value(&key) {
4542 return Ok(Value::Undefined);
4543 }
4544 let entries = match collection_entries(ctx.gc, &ctx.this) {
4545 Some(e) => e,
4546 None => return Ok(Value::Undefined),
4547 };
4548 let count = collection_entry_count(ctx.gc, &ctx.this);
4549 if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) {
4550 return Ok(entry_value_at(ctx.gc, entries, idx));
4551 }
4552 Ok(Value::Undefined)
4553}
4554
4555fn weakmap_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4556 let key = args.first().cloned().unwrap_or(Value::Undefined);
4557 if !is_object_value(&key) {
4558 return Ok(Value::Boolean(false));
4559 }
4560 let entries = match collection_entries(ctx.gc, &ctx.this) {
4561 Some(e) => e,
4562 None => return Ok(Value::Boolean(false)),
4563 };
4564 let count = collection_entry_count(ctx.gc, &ctx.this);
4565 Ok(Value::Boolean(
4566 find_key_index(ctx.gc, entries, count, &key).is_some(),
4567 ))
4568}
4569
4570fn weakmap_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4571 let key = args.first().cloned().unwrap_or(Value::Undefined);
4572 if !is_object_value(&key) {
4573 return Ok(Value::Boolean(false));
4574 }
4575 let entries = match collection_entries(ctx.gc, &ctx.this) {
4576 Some(e) => e,
4577 None => return Ok(Value::Boolean(false)),
4578 };
4579 let count = collection_entry_count(ctx.gc, &ctx.this);
4580 let live = collection_live_count(ctx.gc, &ctx.this);
4581 if let Some(idx) = find_key_index(ctx.gc, entries, count, &key) {
4582 delete_entry_at(ctx.gc, entries, idx);
4583 set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1));
4584 return Ok(Value::Boolean(true));
4585 }
4586 Ok(Value::Boolean(false))
4587}
4588
4589// ── WeakSet constructor & prototype ──────────────────────────
4590
4591fn weakset_constructor(_args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4592 let proto = WEAKSET_PROTO.with(|cell| cell.get());
4593 let obj_ref = make_collection_obj(ctx.gc, proto);
4594 Ok(Value::Object(obj_ref))
4595}
4596
4597fn init_weakset_prototype(gc: &mut Gc<HeapObject>, proto: GcRef) {
4598 let methods: &[NativeMethod] = &[
4599 ("add", weakset_proto_add),
4600 ("has", weakset_proto_has),
4601 ("delete", weakset_proto_delete),
4602 ];
4603 for &(name, callback) in methods {
4604 let f = make_native(gc, name, callback);
4605 set_builtin_prop(gc, proto, name, Value::Function(f));
4606 }
4607}
4608
4609fn weakset_proto_add(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4610 let value = args.first().cloned().unwrap_or(Value::Undefined);
4611 if !is_object_value(&value) {
4612 return Err(RuntimeError::type_error("WeakSet value must be an object"));
4613 }
4614 set_add_internal(ctx.gc, &ctx.this, value);
4615 Ok(ctx.this.clone())
4616}
4617
4618fn weakset_proto_has(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4619 let value = args.first().cloned().unwrap_or(Value::Undefined);
4620 if !is_object_value(&value) {
4621 return Ok(Value::Boolean(false));
4622 }
4623 let entries = match collection_entries(ctx.gc, &ctx.this) {
4624 Some(e) => e,
4625 None => return Ok(Value::Boolean(false)),
4626 };
4627 let count = collection_entry_count(ctx.gc, &ctx.this);
4628 Ok(Value::Boolean(
4629 find_key_index(ctx.gc, entries, count, &value).is_some(),
4630 ))
4631}
4632
4633fn weakset_proto_delete(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
4634 let value = args.first().cloned().unwrap_or(Value::Undefined);
4635 if !is_object_value(&value) {
4636 return Ok(Value::Boolean(false));
4637 }
4638 let entries = match collection_entries(ctx.gc, &ctx.this) {
4639 Some(e) => e,
4640 None => return Ok(Value::Boolean(false)),
4641 };
4642 let count = collection_entry_count(ctx.gc, &ctx.this);
4643 let live = collection_live_count(ctx.gc, &ctx.this);
4644 if let Some(idx) = find_key_index(ctx.gc, entries, count, &value) {
4645 delete_entry_at(ctx.gc, entries, idx);
4646 set_collection_count(ctx.gc, &ctx.this, count, live.saturating_sub(1));
4647 return Ok(Value::Boolean(true));
4648 }
4649 Ok(Value::Boolean(false))
4650}
4651
4652// ── JSON object ──────────────────────────────────────────────
4653
4654fn init_json_object(vm: &mut Vm) {
4655 let mut data = ObjectData::new();
4656 if let Some(proto) = vm.object_prototype {
4657 data.prototype = Some(proto);
4658 }
4659 let json_ref = vm.gc.alloc(HeapObject::Object(data));
4660
4661 let parse_fn = make_native(&mut vm.gc, "parse", json_parse);
4662 set_builtin_prop(&mut vm.gc, json_ref, "parse", Value::Function(parse_fn));
4663
4664 let stringify_fn = make_native(&mut vm.gc, "stringify", json_stringify);
4665 set_builtin_prop(
4666 &mut vm.gc,
4667 json_ref,
4668 "stringify",
4669 Value::Function(stringify_fn),
4670 );
4671
4672 vm.set_global("JSON", Value::Object(json_ref));
4673}
4674
4675// ── JSON.parse ───────────────────────────────────────────────
4676
4677/// Minimal JSON tokenizer.
4678#[derive(Debug, Clone, PartialEq)]
4679enum JsonToken {
4680 LBrace,
4681 RBrace,
4682 LBracket,
4683 RBracket,
4684 Colon,
4685 Comma,
4686 String(String),
4687 Number(f64),
4688 True,
4689 False,
4690 Null,
4691}
4692
4693struct JsonLexer<'a> {
4694 chars: &'a [u8],
4695 pos: usize,
4696}
4697
4698impl<'a> JsonLexer<'a> {
4699 fn new(input: &'a str) -> Self {
4700 Self {
4701 chars: input.as_bytes(),
4702 pos: 0,
4703 }
4704 }
4705
4706 fn skip_ws(&mut self) {
4707 while self.pos < self.chars.len() {
4708 match self.chars[self.pos] {
4709 b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1,
4710 _ => break,
4711 }
4712 }
4713 }
4714
4715 fn next_token(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
4716 self.skip_ws();
4717 if self.pos >= self.chars.len() {
4718 return Ok(None);
4719 }
4720 let ch = self.chars[self.pos];
4721 match ch {
4722 b'{' => {
4723 self.pos += 1;
4724 Ok(Some(JsonToken::LBrace))
4725 }
4726 b'}' => {
4727 self.pos += 1;
4728 Ok(Some(JsonToken::RBrace))
4729 }
4730 b'[' => {
4731 self.pos += 1;
4732 Ok(Some(JsonToken::LBracket))
4733 }
4734 b']' => {
4735 self.pos += 1;
4736 Ok(Some(JsonToken::RBracket))
4737 }
4738 b':' => {
4739 self.pos += 1;
4740 Ok(Some(JsonToken::Colon))
4741 }
4742 b',' => {
4743 self.pos += 1;
4744 Ok(Some(JsonToken::Comma))
4745 }
4746 b'"' => self.read_string(),
4747 b't' => self.read_keyword("true", JsonToken::True),
4748 b'f' => self.read_keyword("false", JsonToken::False),
4749 b'n' => self.read_keyword("null", JsonToken::Null),
4750 b'-' | b'0'..=b'9' => self.read_number(),
4751 _ => Err(RuntimeError::syntax_error(format!(
4752 "Unexpected character '{}' in JSON",
4753 ch as char
4754 ))),
4755 }
4756 }
4757
4758 fn read_string(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
4759 self.pos += 1; // skip opening quote
4760 let mut s = String::new();
4761 while self.pos < self.chars.len() {
4762 let ch = self.chars[self.pos];
4763 self.pos += 1;
4764 if ch == b'"' {
4765 return Ok(Some(JsonToken::String(s)));
4766 }
4767 if ch == b'\\' {
4768 if self.pos >= self.chars.len() {
4769 return Err(RuntimeError::syntax_error("Unterminated string in JSON"));
4770 }
4771 let esc = self.chars[self.pos];
4772 self.pos += 1;
4773 match esc {
4774 b'"' => s.push('"'),
4775 b'\\' => s.push('\\'),
4776 b'/' => s.push('/'),
4777 b'b' => s.push('\u{0008}'),
4778 b'f' => s.push('\u{000C}'),
4779 b'n' => s.push('\n'),
4780 b'r' => s.push('\r'),
4781 b't' => s.push('\t'),
4782 b'u' => {
4783 if self.pos + 4 > self.chars.len() {
4784 return Err(RuntimeError::syntax_error("Invalid unicode escape"));
4785 }
4786 let hex = std::str::from_utf8(&self.chars[self.pos..self.pos + 4])
4787 .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?;
4788 self.pos += 4;
4789 let code = u32::from_str_radix(hex, 16)
4790 .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?;
4791 if let Some(c) = char::from_u32(code) {
4792 s.push(c);
4793 } else {
4794 return Err(RuntimeError::syntax_error("Invalid unicode code point"));
4795 }
4796 }
4797 _ => {
4798 return Err(RuntimeError::syntax_error(format!(
4799 "Invalid escape character '\\{}'",
4800 esc as char
4801 )));
4802 }
4803 }
4804 } else {
4805 s.push(ch as char);
4806 }
4807 }
4808 Err(RuntimeError::syntax_error("Unterminated string in JSON"))
4809 }
4810
4811 fn read_number(&mut self) -> Result<Option<JsonToken>, RuntimeError> {
4812 let start = self.pos;
4813 if self.pos < self.chars.len() && self.chars[self.pos] == b'-' {
4814 self.pos += 1;
4815 }
4816 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
4817 self.pos += 1;
4818 }
4819 if self.pos < self.chars.len() && self.chars[self.pos] == b'.' {
4820 self.pos += 1;
4821 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
4822 self.pos += 1;
4823 }
4824 }
4825 if self.pos < self.chars.len()
4826 && (self.chars[self.pos] == b'e' || self.chars[self.pos] == b'E')
4827 {
4828 self.pos += 1;
4829 if self.pos < self.chars.len()
4830 && (self.chars[self.pos] == b'+' || self.chars[self.pos] == b'-')
4831 {
4832 self.pos += 1;
4833 }
4834 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
4835 self.pos += 1;
4836 }
4837 }
4838 let num_str = std::str::from_utf8(&self.chars[start..self.pos])
4839 .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?;
4840 let n: f64 = num_str
4841 .parse()
4842 .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?;
4843 Ok(Some(JsonToken::Number(n)))
4844 }
4845
4846 fn read_keyword(
4847 &mut self,
4848 keyword: &str,
4849 token: JsonToken,
4850 ) -> Result<Option<JsonToken>, RuntimeError> {
4851 let end = self.pos + keyword.len();
4852 if end <= self.chars.len() && &self.chars[self.pos..end] == keyword.as_bytes() {
4853 self.pos = end;
4854 Ok(Some(token))
4855 } else {
4856 Err(RuntimeError::syntax_error(format!(
4857 "Unexpected token in JSON at position {}",
4858 self.pos
4859 )))
4860 }
4861 }
4862}
4863
4864struct JsonParser<'a> {
4865 lexer: JsonLexer<'a>,
4866 current: Option<JsonToken>,
4867}
4868
4869impl<'a> JsonParser<'a> {
4870 fn new(input: &'a str) -> Result<Self, RuntimeError> {
4871 let mut lexer = JsonLexer::new(input);
4872 let current = lexer.next_token()?;
4873 Ok(Self { lexer, current })
4874 }
4875
4876 fn advance(&mut self) -> Result<(), RuntimeError> {
4877 self.current = self.lexer.next_token()?;
4878 Ok(())
4879 }
4880
4881 fn parse_value(&mut self, gc: &mut Gc<HeapObject>) -> Result<Value, RuntimeError> {
4882 match self.current.take() {
4883 Some(JsonToken::Null) => {
4884 self.advance()?;
4885 Ok(Value::Null)
4886 }
4887 Some(JsonToken::True) => {
4888 self.advance()?;
4889 Ok(Value::Boolean(true))
4890 }
4891 Some(JsonToken::False) => {
4892 self.advance()?;
4893 Ok(Value::Boolean(false))
4894 }
4895 Some(JsonToken::Number(n)) => {
4896 self.advance()?;
4897 Ok(Value::Number(n))
4898 }
4899 Some(JsonToken::String(s)) => {
4900 self.advance()?;
4901 Ok(Value::String(s))
4902 }
4903 Some(JsonToken::LBracket) => self.parse_array(gc),
4904 Some(JsonToken::LBrace) => self.parse_object(gc),
4905 Some(other) => {
4906 // Put it back for error context.
4907 self.current = Some(other);
4908 Err(RuntimeError::syntax_error("Unexpected token in JSON"))
4909 }
4910 None => Err(RuntimeError::syntax_error("Unexpected end of JSON input")),
4911 }
4912 }
4913
4914 fn parse_array(&mut self, gc: &mut Gc<HeapObject>) -> Result<Value, RuntimeError> {
4915 // Current token was LBracket, already consumed via take().
4916 self.advance()?; // move past '['
4917 let mut items: Vec<Value> = Vec::new();
4918 if self.current == Some(JsonToken::RBracket) {
4919 self.advance()?;
4920 let mut obj = ObjectData::new();
4921 obj.properties.insert(
4922 "length".to_string(),
4923 Property {
4924 value: Value::Number(0.0),
4925 writable: true,
4926 enumerable: false,
4927 configurable: false,
4928 },
4929 );
4930 return Ok(Value::Object(gc.alloc(HeapObject::Object(obj))));
4931 }
4932 loop {
4933 let val = self.parse_value(gc)?;
4934 items.push(val);
4935 match &self.current {
4936 Some(JsonToken::Comma) => {
4937 self.advance()?;
4938 }
4939 Some(JsonToken::RBracket) => {
4940 self.advance()?;
4941 break;
4942 }
4943 _ => {
4944 return Err(RuntimeError::syntax_error(
4945 "Expected ',' or ']' in JSON array",
4946 ));
4947 }
4948 }
4949 }
4950 let mut obj = ObjectData::new();
4951 for (i, v) in items.iter().enumerate() {
4952 obj.properties
4953 .insert(i.to_string(), Property::data(v.clone()));
4954 }
4955 obj.properties.insert(
4956 "length".to_string(),
4957 Property {
4958 value: Value::Number(items.len() as f64),
4959 writable: true,
4960 enumerable: false,
4961 configurable: false,
4962 },
4963 );
4964 Ok(Value::Object(gc.alloc(HeapObject::Object(obj))))
4965 }
4966
4967 fn parse_object(&mut self, gc: &mut Gc<HeapObject>) -> Result<Value, RuntimeError> {
4968 self.advance()?; // move past '{'
4969 let mut obj = ObjectData::new();
4970 if self.current == Some(JsonToken::RBrace) {
4971 self.advance()?;
4972 return Ok(Value::Object(gc.alloc(HeapObject::Object(obj))));
4973 }
4974 loop {
4975 let key = match self.current.take() {
4976 Some(JsonToken::String(s)) => s,
4977 _ => {
4978 return Err(RuntimeError::syntax_error(
4979 "Expected string key in JSON object",
4980 ));
4981 }
4982 };
4983 self.advance()?;
4984 if self.current != Some(JsonToken::Colon) {
4985 return Err(RuntimeError::syntax_error(
4986 "Expected ':' after key in JSON object",
4987 ));
4988 }
4989 self.advance()?;
4990 let val = self.parse_value(gc)?;
4991 obj.properties.insert(key, Property::data(val));
4992 match &self.current {
4993 Some(JsonToken::Comma) => {
4994 self.advance()?;
4995 }
4996 Some(JsonToken::RBrace) => {
4997 self.advance()?;
4998 break;
4999 }
5000 _ => {
5001 return Err(RuntimeError::syntax_error(
5002 "Expected ',' or '}}' in JSON object",
5003 ));
5004 }
5005 }
5006 }
5007 Ok(Value::Object(gc.alloc(HeapObject::Object(obj))))
5008 }
5009}
5010
5011fn json_parse(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5012 let text = args
5013 .first()
5014 .map(|v| v.to_js_string(ctx.gc))
5015 .unwrap_or_default();
5016 let mut parser = JsonParser::new(&text)?;
5017 let value = parser.parse_value(ctx.gc)?;
5018 // Ensure no trailing content.
5019 if parser.current.is_some() {
5020 return Err(RuntimeError::syntax_error(
5021 "Unexpected token after JSON value",
5022 ));
5023 }
5024 // TODO: reviver support — skip for now.
5025 Ok(value)
5026}
5027
5028// ── JSON.stringify ───────────────────────────────────────────
5029
5030fn json_stringify(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5031 let value = args.first().cloned().unwrap_or(Value::Undefined);
5032 let indent = match args.get(2) {
5033 Some(Value::Number(n)) => {
5034 let n = *n as usize;
5035 if n > 0 {
5036 " ".repeat(n.min(10))
5037 } else {
5038 String::new()
5039 }
5040 }
5041 Some(Value::String(s)) => s[..s.len().min(10)].to_string(),
5042 _ => String::new(),
5043 };
5044 // Track visited objects for circular reference detection.
5045 let mut visited: Vec<GcRef> = Vec::new();
5046 let result = json_stringify_value(&value, ctx.gc, &indent, "", &mut visited)?;
5047 match result {
5048 Some(s) => Ok(Value::String(s)),
5049 None => Ok(Value::Undefined),
5050 }
5051}
5052
5053fn json_stringify_value(
5054 value: &Value,
5055 gc: &Gc<HeapObject>,
5056 indent: &str,
5057 current_indent: &str,
5058 visited: &mut Vec<GcRef>,
5059) -> Result<Option<String>, RuntimeError> {
5060 match value {
5061 Value::Null => Ok(Some("null".to_string())),
5062 Value::Boolean(true) => Ok(Some("true".to_string())),
5063 Value::Boolean(false) => Ok(Some("false".to_string())),
5064 Value::Number(n) => {
5065 if n.is_nan() || n.is_infinite() {
5066 Ok(Some("null".to_string()))
5067 } else {
5068 Ok(Some(js_number_to_string(*n)))
5069 }
5070 }
5071 Value::String(s) => Ok(Some(json_quote_string(s))),
5072 Value::Undefined | Value::Function(_) => Ok(None),
5073 Value::Object(gc_ref) => {
5074 // Circular reference check.
5075 if visited.contains(gc_ref) {
5076 return Err(RuntimeError::type_error(
5077 "Converting circular structure to JSON",
5078 ));
5079 }
5080 visited.push(*gc_ref);
5081
5082 let result = match gc.get(*gc_ref) {
5083 Some(HeapObject::Object(data)) => {
5084 // Check for toJSON method.
5085 if let Some(prop) = data.properties.get("toJSON") {
5086 if matches!(prop.value, Value::Function(_)) {
5087 // We can't call JS functions from here easily,
5088 // but for Date objects we recognize the __date_ms__ pattern.
5089 if let Some(date_prop) = data.properties.get("__date_ms__") {
5090 let ms = date_prop.value.to_number();
5091 if ms.is_nan() {
5092 visited.pop();
5093 return Ok(Some("null".to_string()));
5094 }
5095 let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms);
5096 visited.pop();
5097 return Ok(Some(format!(
5098 "\"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z\"",
5099 y,
5100 m + 1,
5101 d,
5102 h,
5103 min,
5104 s,
5105 ms_part
5106 )));
5107 }
5108 }
5109 }
5110 if array_length_exists(gc, *gc_ref) {
5111 json_stringify_array(gc, *gc_ref, indent, current_indent, visited)
5112 } else {
5113 json_stringify_object(gc, data, indent, current_indent, visited)
5114 }
5115 }
5116 _ => Ok(Some("{}".to_string())),
5117 };
5118
5119 visited.pop();
5120 result
5121 }
5122 }
5123}
5124
5125fn json_stringify_array(
5126 gc: &Gc<HeapObject>,
5127 arr: GcRef,
5128 indent: &str,
5129 current_indent: &str,
5130 visited: &mut Vec<GcRef>,
5131) -> Result<Option<String>, RuntimeError> {
5132 let len = array_length(gc, arr);
5133 if len == 0 {
5134 return Ok(Some("[]".to_string()));
5135 }
5136 let has_indent = !indent.is_empty();
5137 let next_indent = if has_indent {
5138 format!("{}{}", current_indent, indent)
5139 } else {
5140 String::new()
5141 };
5142 let mut parts: Vec<String> = Vec::new();
5143 for i in 0..len {
5144 let val = array_get(gc, arr, i);
5145 match json_stringify_value(&val, gc, indent, &next_indent, visited)? {
5146 Some(s) => parts.push(s),
5147 None => parts.push("null".to_string()),
5148 }
5149 }
5150 if has_indent {
5151 Ok(Some(format!(
5152 "[\n{}{}\n{}]",
5153 next_indent,
5154 parts.join(&format!(",\n{}", next_indent)),
5155 current_indent
5156 )))
5157 } else {
5158 Ok(Some(format!("[{}]", parts.join(","))))
5159 }
5160}
5161
5162fn json_stringify_object(
5163 gc: &Gc<HeapObject>,
5164 data: &ObjectData,
5165 indent: &str,
5166 current_indent: &str,
5167 visited: &mut Vec<GcRef>,
5168) -> Result<Option<String>, RuntimeError> {
5169 let has_indent = !indent.is_empty();
5170 let next_indent = if has_indent {
5171 format!("{}{}", current_indent, indent)
5172 } else {
5173 String::new()
5174 };
5175 let mut parts: Vec<String> = Vec::new();
5176 // Collect and sort keys for deterministic output.
5177 let mut keys: Vec<&String> = data.properties.keys().collect();
5178 keys.sort();
5179 for key in keys {
5180 let prop = &data.properties[key];
5181 if !prop.enumerable {
5182 continue;
5183 }
5184 if let Some(val_str) = json_stringify_value(&prop.value, gc, indent, &next_indent, visited)?
5185 {
5186 if has_indent {
5187 parts.push(format!("{}: {}", json_quote_string(key), val_str));
5188 } else {
5189 parts.push(format!("{}:{}", json_quote_string(key), val_str));
5190 }
5191 }
5192 }
5193 if parts.is_empty() {
5194 return Ok(Some("{}".to_string()));
5195 }
5196 if has_indent {
5197 Ok(Some(format!(
5198 "{{\n{}{}\n{}}}",
5199 next_indent,
5200 parts.join(&format!(",\n{}", next_indent)),
5201 current_indent
5202 )))
5203 } else {
5204 Ok(Some(format!("{{{}}}", parts.join(","))))
5205 }
5206}
5207
5208fn json_quote_string(s: &str) -> String {
5209 let mut out = String::with_capacity(s.len() + 2);
5210 out.push('"');
5211 for ch in s.chars() {
5212 match ch {
5213 '"' => out.push_str("\\\""),
5214 '\\' => out.push_str("\\\\"),
5215 '\n' => out.push_str("\\n"),
5216 '\r' => out.push_str("\\r"),
5217 '\t' => out.push_str("\\t"),
5218 '\u{0008}' => out.push_str("\\b"),
5219 '\u{000C}' => out.push_str("\\f"),
5220 c if c < '\u{0020}' => {
5221 out.push_str(&format!("\\u{:04x}", c as u32));
5222 }
5223 c => out.push(c),
5224 }
5225 }
5226 out.push('"');
5227 out
5228}
5229
5230// ── Global utility functions ─────────────────────────────────
5231
5232fn init_global_functions(vm: &mut Vm) {
5233 vm.define_native("parseInt", parse_int);
5234 vm.define_native("parseFloat", parse_float);
5235 vm.define_native("isNaN", is_nan);
5236 vm.define_native("isFinite", is_finite);
5237}
5238
5239fn parse_int(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5240 let s = args
5241 .first()
5242 .map(|v| v.to_js_string(ctx.gc))
5243 .unwrap_or_default();
5244 let radix = args.get(1).map(|v| v.to_number() as u32).unwrap_or(0);
5245 let s = s.trim();
5246 if s.is_empty() {
5247 return Ok(Value::Number(f64::NAN));
5248 }
5249 let (neg, s) = if let Some(rest) = s.strip_prefix('-') {
5250 (true, rest)
5251 } else if let Some(rest) = s.strip_prefix('+') {
5252 (false, rest)
5253 } else {
5254 (false, s)
5255 };
5256 // Auto-detect hex.
5257 let (radix, s) = if radix == 0 || radix == 16 {
5258 if let Some(rest) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
5259 (16, rest)
5260 } else {
5261 (if radix == 0 { 10 } else { radix }, s)
5262 }
5263 } else {
5264 (radix, s)
5265 };
5266 if !(2..=36).contains(&radix) {
5267 return Ok(Value::Number(f64::NAN));
5268 }
5269 // Parse digits until invalid.
5270 let mut result: f64 = 0.0;
5271 let mut found = false;
5272 for c in s.chars() {
5273 let digit = match c.to_digit(radix) {
5274 Some(d) => d,
5275 None => break,
5276 };
5277 result = result * radix as f64 + digit as f64;
5278 found = true;
5279 }
5280 if !found {
5281 return Ok(Value::Number(f64::NAN));
5282 }
5283 if neg {
5284 result = -result;
5285 }
5286 Ok(Value::Number(result))
5287}
5288
5289fn parse_float(args: &[Value], ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5290 let s = args
5291 .first()
5292 .map(|v| v.to_js_string(ctx.gc))
5293 .unwrap_or_default();
5294 let s = s.trim();
5295 match s.parse::<f64>() {
5296 Ok(n) => Ok(Value::Number(n)),
5297 Err(_) => Ok(Value::Number(f64::NAN)),
5298 }
5299}
5300
5301fn is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5302 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
5303 Ok(Value::Boolean(n.is_nan()))
5304}
5305
5306fn is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result<Value, RuntimeError> {
5307 let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN);
5308 Ok(Value::Boolean(n.is_finite()))
5309}
5310
5311// ── Strict equality (for Array.indexOf etc.) ─────────────────
5312
5313fn strict_eq_values(a: &Value, b: &Value) -> bool {
5314 match (a, b) {
5315 (Value::Undefined, Value::Undefined) => true,
5316 (Value::Null, Value::Null) => true,
5317 (Value::Number(x), Value::Number(y)) => x == y,
5318 (Value::String(x), Value::String(y)) => x == y,
5319 (Value::Boolean(x), Value::Boolean(y)) => x == y,
5320 (Value::Object(x), Value::Object(y)) => x == y,
5321 (Value::Function(x), Value::Function(y)) => x == y,
5322 _ => false,
5323 }
5324}
5325
5326/// SameValueZero (like === but NaN === NaN is true).
5327fn same_value_zero(a: &Value, b: &Value) -> bool {
5328 match (a, b) {
5329 (Value::Number(x), Value::Number(y)) => {
5330 if x.is_nan() && y.is_nan() {
5331 return true;
5332 }
5333 x == y
5334 }
5335 _ => strict_eq_values(a, b),
5336 }
5337}
5338
5339// ── JS preamble for callback-based methods ───────────────────
5340
5341fn init_js_preamble(vm: &mut Vm) {
5342 // NOTE: All preamble methods capture `this` into a local `_t` variable
5343 // because nested method calls (e.g. result.push()) clobber the global `this`.
5344 let preamble = r#"
5345Array.prototype.forEach = function(cb) {
5346 var _t = this;
5347 for (var i = 0; i < _t.length; i = i + 1) {
5348 cb(_t[i], i, _t);
5349 }
5350};
5351Array.prototype.map = function(cb) {
5352 var _t = this;
5353 var result = [];
5354 for (var i = 0; i < _t.length; i = i + 1) {
5355 result.push(cb(_t[i], i, _t));
5356 }
5357 return result;
5358};
5359Array.prototype.filter = function(cb) {
5360 var _t = this;
5361 var result = [];
5362 for (var i = 0; i < _t.length; i = i + 1) {
5363 if (cb(_t[i], i, _t)) {
5364 result.push(_t[i]);
5365 }
5366 }
5367 return result;
5368};
5369Array.prototype.reduce = function(cb, init) {
5370 var _t = this;
5371 var acc = init;
5372 var start = 0;
5373 if (acc === undefined) {
5374 acc = _t[0];
5375 start = 1;
5376 }
5377 for (var i = start; i < _t.length; i = i + 1) {
5378 acc = cb(acc, _t[i], i, _t);
5379 }
5380 return acc;
5381};
5382Array.prototype.reduceRight = function(cb, init) {
5383 var _t = this;
5384 var acc = init;
5385 var start = _t.length - 1;
5386 if (acc === undefined) {
5387 acc = _t[start];
5388 start = start - 1;
5389 }
5390 for (var i = start; i >= 0; i = i - 1) {
5391 acc = cb(acc, _t[i], i, _t);
5392 }
5393 return acc;
5394};
5395Array.prototype.find = function(cb) {
5396 var _t = this;
5397 for (var i = 0; i < _t.length; i = i + 1) {
5398 if (cb(_t[i], i, _t)) { return _t[i]; }
5399 }
5400 return undefined;
5401};
5402Array.prototype.findIndex = function(cb) {
5403 var _t = this;
5404 for (var i = 0; i < _t.length; i = i + 1) {
5405 if (cb(_t[i], i, _t)) { return i; }
5406 }
5407 return -1;
5408};
5409Array.prototype.some = function(cb) {
5410 var _t = this;
5411 for (var i = 0; i < _t.length; i = i + 1) {
5412 if (cb(_t[i], i, _t)) { return true; }
5413 }
5414 return false;
5415};
5416Array.prototype.every = function(cb) {
5417 var _t = this;
5418 for (var i = 0; i < _t.length; i = i + 1) {
5419 if (!cb(_t[i], i, _t)) { return false; }
5420 }
5421 return true;
5422};
5423Array.prototype.sort = function(cmp) {
5424 var _t = this;
5425 var len = _t.length;
5426 for (var i = 0; i < len; i = i + 1) {
5427 for (var j = 0; j < len - i - 1; j = j + 1) {
5428 var a = _t[j];
5429 var b = _t[j + 1];
5430 var order;
5431 if (cmp) {
5432 order = cmp(a, b);
5433 } else {
5434 var sa = "" + a;
5435 var sb = "" + b;
5436 if (sa > sb) { order = 1; }
5437 else if (sa < sb) { order = -1; }
5438 else { order = 0; }
5439 }
5440 if (order > 0) {
5441 _t[j] = b;
5442 _t[j + 1] = a;
5443 }
5444 }
5445 }
5446 return _t;
5447};
5448Array.prototype.flat = function(depth) {
5449 var _t = this;
5450 if (depth === undefined) { depth = 1; }
5451 var result = [];
5452 for (var i = 0; i < _t.length; i = i + 1) {
5453 var elem = _t[i];
5454 if (depth > 0 && Array.isArray(elem)) {
5455 var sub = elem.flat(depth - 1);
5456 for (var j = 0; j < sub.length; j = j + 1) {
5457 result.push(sub[j]);
5458 }
5459 } else {
5460 result.push(elem);
5461 }
5462 }
5463 return result;
5464};
5465Array.prototype.flatMap = function(cb) {
5466 var _t = this;
5467 return _t.map(cb).flat();
5468};
5469"#;
5470 // Compile and execute the preamble.
5471 let ast = match crate::parser::Parser::parse(preamble) {
5472 Ok(ast) => ast,
5473 Err(_) => return, // Silently skip if preamble fails to parse.
5474 };
5475 let func = match crate::compiler::compile(&ast) {
5476 Ok(func) => func,
5477 Err(_) => return,
5478 };
5479 let _ = vm.execute(&func);
5480}