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