@tishlang/tish-format 2.16.13 → 2.36.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fixtures/process_capture.tish +16 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/process_capture.rs +40 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +77 -3
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +195 -16
  13. package/crates/tish_builtins/src/collections.rs +37 -2
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +168 -15
  18. package/crates/tish_bytecode/src/compiler.rs +56 -4
  19. package/crates/tish_bytecode/src/opcode.rs +4 -2
  20. package/crates/tish_compile/src/codegen.rs +519 -46
  21. package/crates/tish_compile/src/infer.rs +26 -11
  22. package/crates/tish_compile/src/resolve.rs +87 -10
  23. package/crates/tish_compile_js/src/codegen.rs +82 -24
  24. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  25. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  26. package/crates/tish_core/src/json.rs +5 -1
  27. package/crates/tish_core/src/lib.rs +48 -2
  28. package/crates/tish_core/src/uri.rs +45 -0
  29. package/crates/tish_core/src/value.rs +6 -0
  30. package/crates/tish_eval/Cargo.toml +2 -1
  31. package/crates/tish_eval/src/eval.rs +661 -27
  32. package/crates/tish_eval/src/natives.rs +318 -18
  33. package/crates/tish_eval/src/regex.rs +47 -1
  34. package/crates/tish_fmt/Cargo.toml +1 -1
  35. package/crates/tish_fmt/src/lib.rs +23 -4
  36. package/crates/tish_opt/src/lib.rs +20 -1
  37. package/crates/tish_parser/src/lib.rs +41 -0
  38. package/crates/tish_parser/src/parser.rs +180 -11
  39. package/crates/tish_resolve/src/lib.rs +66 -5
  40. package/crates/tish_runtime/Cargo.toml +5 -1
  41. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  42. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  43. package/crates/tish_runtime/src/lib.rs +236 -10
  44. package/crates/tish_ui/src/jsx.rs +18 -3
  45. package/crates/tish_vm/src/jit.rs +1 -1
  46. package/crates/tish_vm/src/vm.rs +294 -60
  47. package/package.json +1 -1
  48. package/platform/darwin-arm64/tish-fmt +0 -0
  49. package/platform/darwin-x64/tish-fmt +0 -0
  50. package/platform/linux-arm64/tish-fmt +0 -0
  51. package/platform/linux-x64/tish-fmt +0 -0
  52. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -149,11 +149,19 @@ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
149
149
  }
150
150
  }
151
151
 
152
- pub fn index_of(arr: &Value, search: &Value) -> Value {
152
+ pub fn index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
153
153
  let arr = as_boxed_array(arr); let arr = &*arr;
154
154
  if let Value::Array(arr) = arr {
155
155
  let arr_borrow = arr.borrow();
156
- for (i, v) in arr_borrow.iter().enumerate() {
156
+ // `fromIndex`: clamp positive to [0, len]; negative counts from the end (`len + n`, floored
157
+ // at 0). Same handling as `includes`. Absent → 0.
158
+ let len = arr_borrow.len() as i64;
159
+ let start = match from {
160
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
161
+ Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
162
+ _ => 0,
163
+ };
164
+ for (i, v) in arr_borrow.iter().enumerate().skip(start) {
157
165
  if v.strict_eq(search) {
158
166
  return Value::Number(i as f64);
159
167
  }
@@ -218,6 +226,57 @@ pub fn reverse(arr: &Value) -> Value {
218
226
  }
219
227
  }
220
228
 
229
+ /// `Array.prototype.toReversed()` — a reversed COPY (ES2023); the receiver is untouched.
230
+ pub fn to_reversed(arr: &Value) -> Value {
231
+ let mut v = snapshot_values(arr);
232
+ v.reverse();
233
+ Value::Array(VmRef::new(v))
234
+ }
235
+
236
+ /// `Array.prototype.toSorted(cmp?)` — a sorted COPY (ES2023); the receiver is untouched. Sorting runs
237
+ /// on the copy via the same comparator/default machinery as `sort`.
238
+ pub fn to_sorted(arr: &Value, comparator: Option<&Value>) -> Value {
239
+ let copy = Value::Array(VmRef::new(snapshot_values(arr)));
240
+ match comparator {
241
+ Some(cmp @ Value::Function(_)) => sort_with_comparator(&copy, cmp),
242
+ _ => sort_default(&copy),
243
+ }
244
+ }
245
+
246
+ /// `Array.prototype.with(index, value)` — a COPY with `index` replaced (ES2023). A negative index
247
+ /// counts from the end; an out-of-range index parks a catchable `RangeError` (matches node's message).
248
+ pub fn with(arr: &Value, index: &Value, value: &Value) -> Value {
249
+ let mut v = snapshot_values(arr);
250
+ let len = v.len() as i64;
251
+ let rel = match index {
252
+ Value::Number(n) if n.is_finite() => *n as i64,
253
+ _ => 0,
254
+ };
255
+ let actual = if rel >= 0 { rel } else { len + rel };
256
+ if actual < 0 || actual >= len {
257
+ tishlang_core::set_pending_throw(tishlang_core::range_error(format!(
258
+ "Invalid index : {}",
259
+ rel
260
+ )));
261
+ return Value::Null;
262
+ }
263
+ v[actual as usize] = value.clone();
264
+ Value::Array(VmRef::new(v))
265
+ }
266
+
267
+ /// `Array.prototype.toSpliced(start, deleteCount?, ...items)` — a COPY with the splice applied (ES2023);
268
+ /// unlike `splice` (which returns the removed elements) this returns the resulting array.
269
+ pub fn to_spliced(
270
+ arr: &Value,
271
+ start: &Value,
272
+ delete_count: Option<&Value>,
273
+ items: &[Value],
274
+ ) -> Value {
275
+ let copy = Value::Array(VmRef::new(snapshot_values(arr)));
276
+ splice(&copy, start, delete_count, items);
277
+ copy
278
+ }
279
+
221
280
  /// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
222
281
  pub fn shuffle(arr: &Value) -> Value {
223
282
  let arr = as_boxed_array(arr); let arr = &*arr;
@@ -273,6 +332,55 @@ pub fn fill(arr: &Value, value: &Value, start: &Value, end: &Value) -> Value {
273
332
  }
274
333
  }
275
334
 
335
+ pub fn last_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
336
+ let arr = as_boxed_array(arr);
337
+ let arr = &*arr;
338
+ if let Value::Array(arr) = arr {
339
+ let arr_borrow = arr.borrow();
340
+ let len = arr_borrow.len() as i64;
341
+ // Default fromIndex is len-1; positive clamps to len-1; negative counts back from the end.
342
+ let start = match from {
343
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
344
+ Some(Value::Number(n)) => len + *n as i64,
345
+ _ => len - 1,
346
+ };
347
+ let mut i = start;
348
+ while i >= 0 {
349
+ if let Some(v) = arr_borrow.get(i as usize) {
350
+ if v.strict_eq(search) {
351
+ return Value::Number(i as f64);
352
+ }
353
+ }
354
+ i -= 1;
355
+ }
356
+ }
357
+ Value::Number(-1.0)
358
+ }
359
+
360
+ /// `copyWithin(target, start, end)` — copy the `[start, end)` block to `target`, in place; returns
361
+ /// the array. Indices are clamped, negatives count from the end.
362
+ pub fn copy_within(arr: &Value, target: &Value, start: &Value, end: &Value) -> Value {
363
+ let arr = as_boxed_array(arr);
364
+ let arr = &*arr;
365
+ if let Value::Array(a) = arr {
366
+ let mut v = a.borrow_mut();
367
+ let len = v.len();
368
+ let t = normalize_index(target, len as i64, 0);
369
+ let s = normalize_index(start, len as i64, 0);
370
+ let e = normalize_index(end, len as i64, len);
371
+ if s < e && t < len {
372
+ let count = (e - s).min(len - t);
373
+ let block: Vec<Value> = v[s..s + count].to_vec();
374
+ for (k, val) in block.into_iter().enumerate() {
375
+ v[t + k] = val;
376
+ }
377
+ }
378
+ drop(v);
379
+ return Value::Array(a.clone());
380
+ }
381
+ Value::Null
382
+ }
383
+
276
384
  pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
277
385
  let arr = as_boxed_array(arr); let arr = &*arr;
278
386
  if let Value::Array(arr) = arr {
@@ -417,16 +525,25 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
417
525
  }
418
526
  }
419
527
 
420
- pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
528
+ /// `initial` is `None` when no initial value was passed (`arr.reduce(fn)`) and `Some` for an explicit
529
+ /// one (`arr.reduce(fn, x)`, INCLUDING `arr.reduce(fn, null)`). This distinction matters: reducing an
530
+ /// empty array with NO initial value is a `TypeError` in JS, while an explicit initial (even `null`)
531
+ /// is returned as-is. A `Value::Null` sentinel can't tell the two apart — hence `Option`.
532
+ pub fn reduce(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
421
533
  // Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
422
534
  // path (absent init → first element as the seed, scan from index 1).
423
535
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
424
536
  // `reduce` passes `(accumulator, element, index, array)` — the array is the 4th arg.
425
537
  let arr_value = arr.clone();
426
- let (start_idx, mut acc) = if matches!(initial, Value::Null) && !data.is_empty() {
427
- (1usize, Value::Number(data[0]))
428
- } else {
429
- (0usize, initial.clone())
538
+ let (start_idx, mut acc) = match initial {
539
+ Some(v) => (0usize, v.clone()),
540
+ None if !data.is_empty() => (1usize, Value::Number(data[0])),
541
+ None => {
542
+ tishlang_core::set_pending_throw(tishlang_core::type_error(
543
+ "Reduce of empty array with no initial value",
544
+ ));
545
+ return Value::Null;
546
+ }
430
547
  };
431
548
  // `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
432
549
  for (i, &x) in data.iter().enumerate().skip(start_idx) {
@@ -440,11 +557,15 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
440
557
  let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
441
558
  let arr_value = Value::Array(arr.clone()); // 4th callback arg (JS `array`)
442
559
  let len = arr_borrow.len();
443
- let (start_idx, mut acc) = if matches!(initial, Value::Null) && !arr_borrow.is_empty() {
444
- // No initial value: use first element as acc, start from index 1
445
- (1, arr_borrow[0].clone())
446
- } else {
447
- (0, initial.clone())
560
+ let (start_idx, mut acc) = match initial {
561
+ Some(v) => (0, v.clone()),
562
+ None if !arr_borrow.is_empty() => (1, arr_borrow[0].clone()),
563
+ None => {
564
+ tishlang_core::set_pending_throw(tishlang_core::type_error(
565
+ "Reduce of empty array with no initial value",
566
+ ));
567
+ return Value::Null;
568
+ }
448
569
  };
449
570
  for i in start_idx..len {
450
571
  if tishlang_core::has_pending_throw() { return acc; } // #303
@@ -457,6 +578,64 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
457
578
  }
458
579
  }
459
580
 
581
+ /// `reduceRight(cb, initial?)` — like [`reduce`] but folds right-to-left. `initial` is `None` when
582
+ /// omitted (empty array + no initial → catchable TypeError).
583
+ pub fn reduce_right(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
584
+ let data = snapshot_values(arr);
585
+ if let Value::Function(cb) = callback {
586
+ let arr_value = arr.clone();
587
+ let len = data.len();
588
+ let (start, mut acc) = match initial {
589
+ Some(v) => (len, v.clone()),
590
+ None if len > 0 => (len - 1, data[len - 1].clone()),
591
+ None => {
592
+ tishlang_core::set_pending_throw(tishlang_core::type_error(
593
+ "Reduce of empty array with no initial value",
594
+ ));
595
+ return Value::Null;
596
+ }
597
+ };
598
+ let mut i = start;
599
+ while i > 0 {
600
+ i -= 1;
601
+ if tishlang_core::has_pending_throw() {
602
+ return acc;
603
+ }
604
+ acc = cb.call(&[
605
+ acc,
606
+ data[i].clone(),
607
+ Value::Number(i as f64),
608
+ arr_value.clone(),
609
+ ]);
610
+ }
611
+ acc
612
+ } else {
613
+ Value::Null
614
+ }
615
+ }
616
+
617
+ /// `arr.keys()` — an iterator over the indices `0..len`.
618
+ pub fn keys(arr: &Value) -> Value {
619
+ let len = snapshot_values(arr).len();
620
+ crate::iterator::array_iterator((0..len).map(|i| Value::Number(i as f64)).collect())
621
+ }
622
+
623
+ /// `arr.values()` — an iterator over the elements.
624
+ pub fn values(arr: &Value) -> Value {
625
+ crate::iterator::array_iterator(snapshot_values(arr))
626
+ }
627
+
628
+ /// `arr.entries()` — an iterator over `[index, element]` pairs.
629
+ pub fn entries(arr: &Value) -> Value {
630
+ crate::iterator::array_iterator(
631
+ snapshot_values(arr)
632
+ .into_iter()
633
+ .enumerate()
634
+ .map(|(i, v)| Value::Array(VmRef::new(vec![Value::Number(i as f64), v])))
635
+ .collect(),
636
+ )
637
+ }
638
+
460
639
  pub fn for_each(arr: &Value, callback: &Value) -> Value {
461
640
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
462
641
  let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
@@ -974,14 +1153,14 @@ mod packed_hof_tests {
974
1153
  let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
975
1154
  let add = cb_num(|acc, x| acc + x);
976
1155
  // With init.
977
- assert_eq!(reduce(&n, &add, &Value::Number(0.0)).as_number(), Some(14.0));
978
- // No init → first element seeds, scan from index 1 (same total here).
979
- assert_eq!(reduce(&n, &add, &Value::Null).as_number(), Some(14.0));
1156
+ assert_eq!(reduce(&n, &add, Some(&Value::Number(0.0))).as_number(), Some(14.0));
1157
+ // No init (None) → first element seeds, scan from index 1 (same total here).
1158
+ assert_eq!(reduce(&n, &add, None).as_number(), Some(14.0));
980
1159
  // Index arg: callback (acc, _elem, index) — sum the indices 0..5 = 10.
981
1160
  let sum_idx = Value::native(|a: &[Value]| {
982
1161
  Value::Number(a[0].as_number().unwrap() + a[2].as_number().unwrap())
983
1162
  });
984
- assert_eq!(reduce(&n, &sum_idx, &Value::Number(0.0)).as_number(), Some(10.0));
1163
+ assert_eq!(reduce(&n, &sum_idx, Some(&Value::Number(0.0))).as_number(), Some(10.0));
985
1164
  }
986
1165
 
987
1166
  #[test]
@@ -175,9 +175,13 @@ pub fn map_get(map: &Value, key: &Value) -> Value {
175
175
  }
176
176
 
177
177
  /// Direct `Map.prototype.set` (mutates in place; returns `undefined` = `Null`).
178
- pub fn map_set(map: &Value, key: Value, val: Value) -> Value {
178
+ ///
179
+ /// Takes the key/val by reference and clones on insert (the map owns its entries). Passing by
180
+ /// reference lets the native emitter BORROW the caller's place instead of moving it, so
181
+ /// `m.set(k, v); …use k…` no longer produces `E0382 use of moved value` in the generated Rust (#442).
182
+ pub fn map_set(map: &Value, key: &Value, val: &Value) -> Value {
179
183
  if let Some(s) = map_store(map) {
180
- s.borrow_mut().insert(Key(key), val);
184
+ s.borrow_mut().insert(Key(key.clone()), val.clone());
181
185
  }
182
186
  Value::Null
183
187
  }
@@ -320,6 +324,20 @@ pub fn set_instance(initial: &[Value]) -> Value {
320
324
  }),
321
325
  );
322
326
  }
327
+ // `__drain__`: the default-iteration array a Set yields (its elements). `drain_iterator`
328
+ // (and the interp/native equivalents) check `__drain__` FIRST, so exposing it here makes a
329
+ // Set directly iterable — `[...set]`, `for (x of set)`, `Array.from(set)` — across all three
330
+ // backends with no changes to any spread/for-of site. (#440)
331
+ {
332
+ let s = store.clone();
333
+ m.insert(
334
+ Arc::from("__drain__"),
335
+ Value::native(move |_args: &[Value]| {
336
+ let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
337
+ Value::Array(VmRef::new(out))
338
+ }),
339
+ );
340
+ }
323
341
 
324
342
  Value::object(m)
325
343
  }
@@ -442,6 +460,23 @@ pub fn map_instance(pairs: &[Value]) -> Value {
442
460
  }),
443
461
  );
444
462
  }
463
+ // `__drain__`: a Map's default iteration yields `[key, value]` entries. Exposed so the shared
464
+ // `drain_iterator` (checked first) makes a Map directly iterable — `[...map]`, `for (x of map)`,
465
+ // `Array.from(map)` — across interp/vm/native with no spread/for-of site changes. (#440)
466
+ {
467
+ let s = store.clone();
468
+ m.insert(
469
+ Arc::from("__drain__"),
470
+ Value::native(move |_args: &[Value]| {
471
+ let pairs: Vec<Value> = s
472
+ .borrow()
473
+ .iter()
474
+ .map(|(k, v)| Value::Array(VmRef::new(vec![k.0.clone(), v.clone()])))
475
+ .collect();
476
+ Value::Array(VmRef::new(pairs))
477
+ }),
478
+ );
479
+ }
445
480
 
446
481
  Value::object(m)
447
482
  }
@@ -5,7 +5,7 @@
5
5
 
6
6
  use std::sync::Arc;
7
7
  use tishlang_core::VmRef;
8
- use tishlang_core::{percent_decode, percent_encode, ObjectMap, Value};
8
+ use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
9
9
 
10
10
  /// Boolean(value) - coerce to bool
11
11
  pub fn boolean(args: &[Value]) -> Value {
@@ -13,7 +13,7 @@ pub fn boolean(args: &[Value]) -> Value {
13
13
  Value::Bool(v.is_truthy())
14
14
  }
15
15
 
16
- /// decodeURI(str)
16
+ /// decodeURI(str) — preserves the URI reserved delimiters (the inverse of encodeURI).
17
17
  pub fn decode_uri(args: &[Value]) -> Value {
18
18
  let s = args
19
19
  .first()
@@ -31,19 +31,34 @@ pub fn encode_uri(args: &[Value]) -> Value {
31
31
  Value::String(percent_encode(&s).into())
32
32
  }
33
33
 
34
- /// isFinite(value)
34
+ /// encodeURIComponent(str) — stricter than encodeURI (also escapes the URI reserved delimiters).
35
+ pub fn encode_uri_component(args: &[Value]) -> Value {
36
+ let s = args
37
+ .first()
38
+ .map(Value::to_display_string)
39
+ .unwrap_or_default();
40
+ Value::String(percent_encode_component(&s).into())
41
+ }
42
+
43
+ /// decodeURIComponent(str) — decodes ALL `%XX` escapes (including the reserved delimiters).
44
+ pub fn decode_uri_component(args: &[Value]) -> Value {
45
+ let s = args
46
+ .first()
47
+ .map(Value::to_display_string)
48
+ .unwrap_or_default();
49
+ Value::String(percent_decode_component(&s).unwrap_or(s).into())
50
+ }
51
+
52
+ /// isFinite(value) — coerces via ToNumber (like `Number()`), then tests finiteness. `isFinite("3")`
53
+ /// is `true`, `isFinite("x")`/`isFinite()` is `false`. (The non-coercing form is `Number.isFinite`.)
35
54
  pub fn is_finite(args: &[Value]) -> Value {
36
- Value::Bool(
37
- args.first()
38
- .is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
39
- )
55
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_finite())
40
56
  }
41
57
 
42
- /// isNaN(value)
58
+ /// isNaN(value) — coerces via ToNumber, then tests NaN. `isNaN("3")` is `false`; an absent arg
59
+ /// (undefined) coerces to NaN → `true`. (The non-coercing form is `Number.isNaN`.)
43
60
  pub fn is_nan(args: &[Value]) -> Value {
44
- Value::Bool(args.first().is_none_or(|v| {
45
- matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
46
- }))
61
+ Value::Bool(args.first().map_or(f64::NAN, to_number).is_nan())
47
62
  }
48
63
 
49
64
  /// Array.isArray(value)
@@ -51,6 +66,142 @@ pub fn array_is_array(args: &[Value]) -> Value {
51
66
  Value::Bool(matches!(args.first(), Some(Value::Array(_)) | Some(Value::NumberArray(_))))
52
67
  }
53
68
 
69
+ /// `Array.of(...items)` — build an array from the args (unlike `Array(n)`, a single number is an
70
+ /// element, not a length: `Array.of(7)` is `[7]`).
71
+ pub fn array_of(args: &[Value]) -> Value {
72
+ Value::Array(VmRef::new(args.to_vec()))
73
+ }
74
+
75
+ /// `Array.from(source, mapFn?)` — build an array from an iterable (array, string, Set/Map, or any
76
+ /// `__drain__`/`next` iterator — via the shared iterator protocol) or an array-like `{ length }`
77
+ /// object, optionally mapping each element through `mapFn(item, index)`.
78
+ pub fn array_from(args: &[Value]) -> Value {
79
+ let source = args.first().cloned().unwrap_or(Value::Null);
80
+ let items: Vec<Value> = match &source {
81
+ Value::Array(a) => a.borrow().clone(),
82
+ Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
83
+ // `drain_iterator` covers String (chars), Set/Map, and any iterator object.
84
+ other => {
85
+ if let Some(d) = tishlang_core::drain_iterator(other) {
86
+ d
87
+ } else if matches!(other, Value::Object(_)) {
88
+ let len = tishlang_core::object_get(other, &Value::String("length".into()))
89
+ .and_then(|v| match v {
90
+ Value::Number(n) if n.is_finite() && n >= 0.0 => Some(n as usize),
91
+ _ => None,
92
+ })
93
+ .unwrap_or(0);
94
+ (0..len)
95
+ .map(|i| {
96
+ tishlang_core::object_get(other, &Value::String(i.to_string().into()))
97
+ .unwrap_or(Value::Null)
98
+ })
99
+ .collect()
100
+ } else {
101
+ Vec::new()
102
+ }
103
+ }
104
+ };
105
+ let out: Vec<Value> = match args.get(1) {
106
+ Some(Value::Function(f)) => items
107
+ .into_iter()
108
+ .enumerate()
109
+ .map(|(i, x)| f.call(&[x, Value::Number(i as f64)]))
110
+ .collect(),
111
+ _ => items,
112
+ };
113
+ Value::Array(VmRef::new(out))
114
+ }
115
+
116
+ /// `structuredClone(value)` — a recursive deep copy of arrays, objects, and primitives, with cycles
117
+ /// preserved (a repeated reference clones to the same new node). Functions and exotic types (Date,
118
+ /// RegExp, Map, Set) are shared/shallow — a pragmatic subset covering plain data structures.
119
+ pub fn structured_clone(args: &[Value]) -> Value {
120
+ let mut seen: Vec<(*const (), Value)> = Vec::new();
121
+ deep_clone(args.first().unwrap_or(&Value::Null), &mut seen)
122
+ }
123
+
124
+ fn deep_clone(v: &Value, seen: &mut Vec<(*const (), Value)>) -> Value {
125
+ match v {
126
+ Value::Array(a) => {
127
+ let ptr = a.as_ptr();
128
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
129
+ return c.clone();
130
+ }
131
+ let new: VmRef<Vec<Value>> = VmRef::new(Vec::new());
132
+ seen.push((ptr, Value::Array(new.clone())));
133
+ let src = a.borrow().clone();
134
+ let cloned: Vec<Value> = src.iter().map(|e| deep_clone(e, seen)).collect();
135
+ *new.borrow_mut() = cloned;
136
+ Value::Array(new)
137
+ }
138
+ Value::NumberArray(a) => Value::NumberArray(VmRef::new(a.borrow().clone())),
139
+ Value::Object(o) => {
140
+ let ptr = o.as_ptr();
141
+ if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
142
+ return c.clone();
143
+ }
144
+ let entries: Vec<(Arc<str>, Value)> = o
145
+ .borrow()
146
+ .strings
147
+ .iter()
148
+ .map(|(k, val)| (k.clone(), val.clone()))
149
+ .collect();
150
+ let new_obj = Value::object(ObjectMap::default());
151
+ // register the (empty) new object before recursing so cycles resolve back to it
152
+ if let Value::Object(no) = &new_obj {
153
+ seen.push((ptr, new_obj.clone()));
154
+ for (k, val) in entries {
155
+ let cloned = deep_clone(&val, seen);
156
+ no.borrow_mut().strings.insert(k, cloned);
157
+ }
158
+ }
159
+ new_obj
160
+ }
161
+ other => other.clone(),
162
+ }
163
+ }
164
+
165
+ /// `Object.hasOwn(obj, key)` (ES2022) — true if `key` is an OWN property of `obj`. For arrays an
166
+ /// in-range integer index or `"length"` counts; for plain objects, a key present in the string map.
167
+ pub fn object_has_own(args: &[Value]) -> Value {
168
+ let key = match args.get(1) {
169
+ Some(Value::String(s)) => s.to_string(),
170
+ Some(Value::Number(n)) => tishlang_core::js_number_to_string(*n),
171
+ Some(v) => v.to_display_string(),
172
+ None => return Value::Bool(false),
173
+ };
174
+ match args.first() {
175
+ Some(Value::Object(obj)) => Value::Bool(obj.borrow().strings.contains_key(key.as_str())),
176
+ Some(Value::Array(arr)) => Value::Bool(
177
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
178
+ ),
179
+ Some(Value::NumberArray(arr)) => Value::Bool(
180
+ key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
181
+ ),
182
+ _ => Value::Bool(false),
183
+ }
184
+ }
185
+
186
+ /// `Object.is(a, b)` — SameValue: like `===`, but NaN equals NaN and `+0` is NOT equal to `-0`.
187
+ pub fn object_is(args: &[Value]) -> Value {
188
+ let a = args.first().unwrap_or(&Value::Null);
189
+ let b = args.get(1).unwrap_or(&Value::Null);
190
+ let same = match (a, b) {
191
+ (Value::Number(x), Value::Number(y)) => {
192
+ if x.is_nan() && y.is_nan() {
193
+ true
194
+ } else if *x == 0.0 && *y == 0.0 {
195
+ x.is_sign_negative() == y.is_sign_negative()
196
+ } else {
197
+ x == y
198
+ }
199
+ }
200
+ _ => a.strict_eq(b),
201
+ };
202
+ Value::Bool(same)
203
+ }
204
+
54
205
  /// String(value) — convert value to string (JS String constructor as function).
55
206
  /// Uses JS `ToString` (arrays comma-join recursively, objects → "[object Object]"),
56
207
  /// not the inspect/display form.
@@ -64,8 +215,14 @@ pub fn string_convert(args: &[Value]) -> Value {
64
215
  /// otherwise NaN); arrays/objects go via their string form (so `Number([5])` → 5,
65
216
  /// `Number([])` → 0, objects → NaN).
66
217
  pub fn number_convert(args: &[Value]) -> Value {
67
- let v = args.first().unwrap_or(&Value::Null);
68
- let n = match v {
218
+ Value::Number(to_number(args.first().unwrap_or(&Value::Null)))
219
+ }
220
+
221
+ /// JS `ToNumber(v)`: Number → itself; Bool → 1/0; Null → 0; String → parsed (trimmed, `0x`/`0b`/`0o`
222
+ /// and `Infinity`; `""` → 0, else NaN); anything else via its string form. The single ToNumber path
223
+ /// shared by `Number()`, `isNaN`/`isFinite`, etc.
224
+ pub fn to_number(v: &Value) -> f64 {
225
+ match v {
69
226
  Value::Number(n) => *n,
70
227
  Value::Bool(b) => {
71
228
  if *b {
@@ -77,8 +234,29 @@ pub fn number_convert(args: &[Value]) -> Value {
77
234
  Value::Null => 0.0,
78
235
  Value::String(s) => parse_numeric_string(s),
79
236
  other => parse_numeric_string(&other.to_js_string()),
80
- };
81
- Value::Number(n)
237
+ }
238
+ }
239
+
240
+ /// `Number.isInteger(v)` — true iff `v` IS a finite Number with no fractional part. No coercion
241
+ /// (`Number.isInteger("3")` is `false`, unlike a bare `isNaN`-style coercion).
242
+ pub fn number_is_integer(args: &[Value]) -> Value {
243
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0))
244
+ }
245
+
246
+ /// `Number.isSafeInteger(v)` — `isInteger` AND exactly representable (|v| ≤ 2^53 − 1).
247
+ pub fn number_is_safe_integer(args: &[Value]) -> Value {
248
+ Value::Bool(matches!(args.first(),
249
+ Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_991.0))
250
+ }
251
+
252
+ /// `Number.isNaN(v)` — true iff `v` IS the number NaN. No coercion (unlike the global `isNaN`).
253
+ pub fn number_is_nan(args: &[Value]) -> Value {
254
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_nan()))
255
+ }
256
+
257
+ /// `Number.isFinite(v)` — true iff `v` is a finite Number. No coercion (unlike the global `isFinite`).
258
+ pub fn number_is_finite(args: &[Value]) -> Value {
259
+ Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite()))
82
260
  }
83
261
 
84
262
  /// Parse a string as JS `Number` does: trimmed; `""` → 0; `0x`/`0o`/`0b` radix prefixes;
@@ -120,16 +298,28 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
120
298
 
121
299
  /// Object.keys(obj)
122
300
  pub fn object_keys(args: &[Value]) -> Value {
123
- if let Some(Value::Object(obj)) = args.first() {
124
- let obj_borrow = obj.borrow();
125
- let keys: Vec<Value> = obj_borrow
126
- .strings
127
- .keys()
128
- .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
129
- .collect();
130
- Value::Array(VmRef::new(keys))
131
- } else {
132
- Value::Array(VmRef::new(Vec::new()))
301
+ match args.first() {
302
+ Some(Value::Object(obj)) => {
303
+ let obj_borrow = obj.borrow();
304
+ let keys: Vec<Value> = obj_borrow
305
+ .strings
306
+ .keys()
307
+ .map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
308
+ .collect();
309
+ Value::Array(VmRef::new(keys))
310
+ }
311
+ // `Object.keys(array)` yields the index strings "0".."len-1" (an array's own enumerable keys
312
+ // are its indices, per JS). Previously returned `[]` — a node divergence, and the reason
313
+ // `for (k in array)` enumerated nothing on the vm/native backends (they lower for-in to
314
+ // for-of over `Object.keys`).
315
+ Some(Value::Array(arr)) => {
316
+ let len = arr.borrow().len();
317
+ let keys: Vec<Value> = (0..len)
318
+ .map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
319
+ .collect();
320
+ Value::Array(VmRef::new(keys))
321
+ }
322
+ _ => Value::Array(VmRef::new(Vec::new())),
133
323
  }
134
324
  }
135
325