@tishlang/tish-format 2.12.0 → 2.35.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 (88) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  4. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  5. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  6. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/integration_test.rs +80 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +84 -0
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +724 -72
  13. package/crates/tish_builtins/src/collections.rs +149 -40
  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 +263 -23
  18. package/crates/tish_bytecode/src/chunk.rs +7 -0
  19. package/crates/tish_bytecode/src/compiler.rs +611 -63
  20. package/crates/tish_bytecode/src/lib.rs +1 -1
  21. package/crates/tish_bytecode/src/opcode.rs +156 -2
  22. package/crates/tish_bytecode/src/serialize.rs +2 -0
  23. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  24. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  25. package/crates/tish_compile/src/codegen.rs +18219 -5283
  26. package/crates/tish_compile/src/infer.rs +1562 -30
  27. package/crates/tish_compile/src/lib.rs +29 -4
  28. package/crates/tish_compile/src/resolve.rs +151 -13
  29. package/crates/tish_compile/src/types.rs +224 -28
  30. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  31. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  32. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  33. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  34. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  35. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  36. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  37. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  38. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  39. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  40. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  41. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  42. package/crates/tish_compile_js/src/codegen.rs +94 -3
  43. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  44. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
  45. package/crates/tish_core/Cargo.toml +4 -0
  46. package/crates/tish_core/src/json.rs +109 -6
  47. package/crates/tish_core/src/lib.rs +222 -2
  48. package/crates/tish_core/src/shape.rs +4 -2
  49. package/crates/tish_core/src/uri.rs +45 -0
  50. package/crates/tish_core/src/value.rs +571 -35
  51. package/crates/tish_core/src/vmref.rs +14 -0
  52. package/crates/tish_eval/Cargo.toml +2 -1
  53. package/crates/tish_eval/src/eval.rs +1328 -101
  54. package/crates/tish_eval/src/natives.rs +283 -18
  55. package/crates/tish_eval/src/regex.rs +47 -1
  56. package/crates/tish_eval/src/value.rs +76 -21
  57. package/crates/tish_ffi/src/lib.rs +11 -1
  58. package/crates/tish_ffi/tests/double_free.rs +35 -0
  59. package/crates/tish_fmt/Cargo.toml +1 -1
  60. package/crates/tish_fmt/src/lib.rs +94 -1
  61. package/crates/tish_lexer/src/lib.rs +76 -0
  62. package/crates/tish_lexer/src/token.rs +4 -0
  63. package/crates/tish_lint/src/lib.rs +126 -0
  64. package/crates/tish_lsp/README.md +2 -1
  65. package/crates/tish_lsp/src/main.rs +378 -28
  66. package/crates/tish_native/src/build.rs +41 -0
  67. package/crates/tish_opt/src/lib.rs +20 -1
  68. package/crates/tish_parser/Cargo.toml +4 -0
  69. package/crates/tish_parser/src/lib.rs +68 -0
  70. package/crates/tish_parser/src/parser.rs +479 -28
  71. package/crates/tish_resolve/src/lib.rs +75 -5
  72. package/crates/tish_runtime/Cargo.toml +10 -1
  73. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  74. package/crates/tish_runtime/src/http.rs +28 -10
  75. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  76. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  77. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  78. package/crates/tish_runtime/src/lib.rs +651 -51
  79. package/crates/tish_runtime/src/timers.rs +49 -2
  80. package/crates/tish_ui/src/jsx.rs +21 -3
  81. package/crates/tish_vm/src/jit.rs +2514 -117
  82. package/crates/tish_vm/src/vm.rs +1479 -224
  83. package/package.json +1 -1
  84. package/platform/darwin-arm64/tish-fmt +0 -0
  85. package/platform/darwin-x64/tish-fmt +0 -0
  86. package/platform/linux-arm64/tish-fmt +0 -0
  87. package/platform/linux-x64/tish-fmt +0 -0
  88. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -9,6 +9,44 @@ pub fn from_vec(v: Vec<Value>) -> Value {
9
9
  Value::Array(VmRef::new(v))
10
10
  }
11
11
 
12
+ /// Snapshot an array `Value` into an owned `Vec<Value>` (one clone of the backing store),
13
+ /// handling both the boxed [`Value::Array`] and packed [`Value::NumberArray`] reps. Non-array
14
+ /// values snapshot as empty. Used by the fused HOF-chain lowering (#317): the chain iterates this
15
+ /// snapshot once, so a `.filter(...).map(...).reduce(...)` runs as a single native loop with no
16
+ /// per-stage intermediate array and no per-element boxed `value_call`. Cloning up front (rather
17
+ /// than holding a borrow) keeps the fused body free to read the source array safely.
18
+ pub fn snapshot_values(arr: &Value) -> Vec<Value> {
19
+ match arr {
20
+ Value::Array(a) => a.borrow().clone(),
21
+ Value::NumberArray(a) => a.borrow().iter().map(|&n| Value::Number(n)).collect(),
22
+ _ => Vec::new(),
23
+ }
24
+ }
25
+
26
+ /// If `arr` is an array whose every element is a `Value::Number`, return an owned `Vec<f64>`
27
+ /// snapshot; otherwise return `None`. Packed [`Value::NumberArray`] is all-numeric by construction
28
+ /// (one clone, no scan). A boxed [`Value::Array`] is scanned once — built by `[].push(n)`, the
29
+ /// dominant numeric-pipeline source stays a boxed `Array`, so the scan unlocks the fused HOF chain's
30
+ /// unboxed f64 fast path for it too. The `None` result drives the fused lowering back to its boxed
31
+ /// loop, so any non-numeric element is handled with identical `Value` semantics.
32
+ pub fn as_f64_snapshot(arr: &Value) -> Option<Vec<f64>> {
33
+ match arr {
34
+ Value::NumberArray(a) => Some(a.borrow().clone()),
35
+ Value::Array(a) => {
36
+ let b = a.borrow();
37
+ let mut out: Vec<f64> = Vec::with_capacity(b.len());
38
+ for v in b.iter() {
39
+ match v {
40
+ Value::Number(n) => out.push(*n),
41
+ _ => return None,
42
+ }
43
+ }
44
+ Some(out)
45
+ }
46
+ _ => None,
47
+ }
48
+ }
49
+
12
50
  /// Get the length of an array.
13
51
  pub fn len(arr: &Value) -> Option<usize> {
14
52
  match arr {
@@ -111,11 +149,19 @@ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
111
149
  }
112
150
  }
113
151
 
114
- pub fn index_of(arr: &Value, search: &Value) -> Value {
152
+ pub fn index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
115
153
  let arr = as_boxed_array(arr); let arr = &*arr;
116
154
  if let Value::Array(arr) = arr {
117
155
  let arr_borrow = arr.borrow();
118
- 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) {
119
165
  if v.strict_eq(search) {
120
166
  return Value::Number(i as f64);
121
167
  }
@@ -180,6 +226,57 @@ pub fn reverse(arr: &Value) -> Value {
180
226
  }
181
227
  }
182
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
+
183
280
  /// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
184
281
  pub fn shuffle(arr: &Value) -> Value {
185
282
  let arr = as_boxed_array(arr); let arr = &*arr;
@@ -235,6 +332,55 @@ pub fn fill(arr: &Value, value: &Value, start: &Value, end: &Value) -> Value {
235
332
  }
236
333
  }
237
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
+
238
384
  pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
239
385
  let arr = as_boxed_array(arr); let arr = &*arr;
240
386
  if let Value::Array(arr) = arr {
@@ -307,15 +453,20 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
307
453
  // boxed array on the FIRST non-numeric callback result; every element's callback still runs
308
454
  // exactly once, in index order (the deopt resumes at `i + 1`).
309
455
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
456
+ // JS passes `(element, index, array)`; `arr_value` is the receiver, cloned per call (a cheap
457
+ // VmRef refcount bump). A callback that ignores the 3rd param is unaffected.
458
+ let arr_value = arr.clone();
310
459
  let mut nums: Vec<f64> = Vec::with_capacity(data.len());
311
460
  for (i, &n) in data.iter().enumerate() {
312
- match cb.call(&[Value::Number(n), Value::Number(i as f64)]) {
461
+ if tishlang_core::has_pending_throw() { return packed_or_empty(nums); } // #303
462
+ match cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]) {
313
463
  Value::Number(r) => nums.push(r),
314
464
  other => {
315
465
  let mut boxed: Vec<Value> = nums.into_iter().map(Value::Number).collect();
316
466
  boxed.push(other);
317
467
  for (j, &m) in data.iter().enumerate().skip(i + 1) {
318
- boxed.push(cb.call(&[Value::Number(m), Value::Number(j as f64)]));
468
+ if tishlang_core::has_pending_throw() { break; } // #303
469
+ boxed.push(cb.call(&[Value::Number(m), Value::Number(j as f64), arr_value.clone()]));
319
470
  }
320
471
  return Value::Array(VmRef::new(boxed));
321
472
  }
@@ -325,12 +476,17 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
325
476
  }
326
477
  let arr = as_boxed_array(arr); let arr = &*arr;
327
478
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
328
- let arr_borrow = arr.borrow();
329
- let result: Vec<Value> = arr_borrow
330
- .iter()
331
- .enumerate()
332
- .map(|(i, v)| cb.call(&[v.clone(), Value::Number(i as f64)]))
333
- .collect();
479
+ // #382: snapshot the backing store and DROP the borrow before running any callback — a
480
+ // callback that re-enters this same array (`arr.map(x => arr.length)`) would otherwise
481
+ // deadlock (Mutex, send-values) or panic (RefCell). Matches the packed path's copy semantics.
482
+ let arr_borrow = arr.borrow().clone();
483
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
484
+ // #303: stop on a pending throw from the callback (don't map the rest of the array).
485
+ let mut result: Vec<Value> = Vec::with_capacity(arr_borrow.len());
486
+ for (i, v) in arr_borrow.iter().enumerate() {
487
+ if tishlang_core::has_pending_throw() { break; }
488
+ result.push(cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]));
489
+ }
334
490
  Value::Array(VmRef::new(result))
335
491
  } else {
336
492
  Value::Null
@@ -341,9 +497,11 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
341
497
  // Packed fast path: `filter` keeps a SUBSET of the input f64s, so the result is always numeric —
342
498
  // build the packed `Vec<f64>` directly, no boxed intermediate, and hand back a `NumberArray`.
343
499
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
500
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
344
501
  let mut out: Vec<f64> = Vec::new();
345
502
  for (i, &n) in data.iter().enumerate() {
346
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
503
+ if tishlang_core::has_pending_throw() { break; } // #303
504
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
347
505
  out.push(n);
348
506
  }
349
507
  }
@@ -351,53 +509,68 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
351
509
  }
352
510
  let arr = as_boxed_array(arr); let arr = &*arr;
353
511
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
354
- let arr_borrow = arr.borrow();
355
- let result: Vec<Value> = arr_borrow
356
- .iter()
357
- .enumerate()
358
- .filter_map(|(i, v)| {
359
- let keep = cb.call(&[v.clone(), Value::Number(i as f64)]);
360
- if keep.is_truthy() {
361
- Some(v.clone())
362
- } else {
363
- None
364
- }
365
- })
366
- .collect();
512
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
513
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
514
+ // #303: stop on a pending throw from the predicate (don't test the rest of the array).
515
+ let mut result: Vec<Value> = Vec::new();
516
+ for (i, v) in arr_borrow.iter().enumerate() {
517
+ if tishlang_core::has_pending_throw() { break; }
518
+ if cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
519
+ result.push(v.clone());
520
+ }
521
+ }
367
522
  Value::Array(VmRef::new(result))
368
523
  } else {
369
524
  Value::Null
370
525
  }
371
526
  }
372
527
 
373
- 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 {
374
533
  // Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
375
534
  // path (absent init → first element as the seed, scan from index 1).
376
535
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
377
- let (start_idx, mut acc) = if matches!(initial, Value::Null) && !data.is_empty() {
378
- (1usize, Value::Number(data[0]))
379
- } else {
380
- (0usize, initial.clone())
536
+ // `reduce` passes `(accumulator, element, index, array)` — the array is the 4th arg.
537
+ let arr_value = arr.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
+ }
381
547
  };
382
548
  // `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
383
549
  for (i, &x) in data.iter().enumerate().skip(start_idx) {
384
- acc = cb.call(&[acc, Value::Number(x), Value::Number(i as f64)]);
550
+ if tishlang_core::has_pending_throw() { return acc; } // #303
551
+ acc = cb.call(&[acc, Value::Number(x), Value::Number(i as f64), arr_value.clone()]);
385
552
  }
386
553
  return acc;
387
554
  }
388
555
  let arr = as_boxed_array(arr); let arr = &*arr;
389
556
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
390
- let arr_borrow = arr.borrow();
557
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
558
+ let arr_value = Value::Array(arr.clone()); // 4th callback arg (JS `array`)
391
559
  let len = arr_borrow.len();
392
- let (start_idx, mut acc) = if matches!(initial, Value::Null) && !arr_borrow.is_empty() {
393
- // No initial value: use first element as acc, start from index 1
394
- (1, arr_borrow[0].clone())
395
- } else {
396
- (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
+ }
397
569
  };
398
570
  for i in start_idx..len {
571
+ if tishlang_core::has_pending_throw() { return acc; } // #303
399
572
  let v = arr_borrow[i].clone();
400
- acc = cb.call(&[acc, v.clone(), Value::Number(i as f64)]);
573
+ acc = cb.call(&[acc, v.clone(), Value::Number(i as f64), arr_value.clone()]);
401
574
  }
402
575
  acc
403
576
  } else {
@@ -405,18 +578,80 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
405
578
  }
406
579
  }
407
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
+
408
639
  pub fn for_each(arr: &Value, callback: &Value) -> Value {
409
640
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
641
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
410
642
  for (i, &n) in data.iter().enumerate() {
411
- cb.call(&[Value::Number(n), Value::Number(i as f64)]);
643
+ if tishlang_core::has_pending_throw() { return Value::Null; } // #303
644
+ cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]);
412
645
  }
413
646
  return Value::Null;
414
647
  }
415
648
  let arr = as_boxed_array(arr); let arr = &*arr;
416
649
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
417
- let arr_borrow = arr.borrow();
650
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
651
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
418
652
  for (i, v) in arr_borrow.iter().enumerate() {
419
- cb.call(&[v.clone(), Value::Number(i as f64)]);
653
+ if tishlang_core::has_pending_throw() { break; } // #303
654
+ cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
420
655
  }
421
656
  }
422
657
  Value::Null
@@ -424,8 +659,10 @@ pub fn for_each(arr: &Value, callback: &Value) -> Value {
424
659
 
425
660
  pub fn find(arr: &Value, callback: &Value) -> Value {
426
661
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
662
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
427
663
  for (i, &n) in data.iter().enumerate() {
428
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
664
+ if tishlang_core::has_pending_throw() { break; } // #303
665
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
429
666
  return Value::Number(n);
430
667
  }
431
668
  }
@@ -433,9 +670,11 @@ pub fn find(arr: &Value, callback: &Value) -> Value {
433
670
  }
434
671
  let arr = as_boxed_array(arr); let arr = &*arr;
435
672
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
436
- let arr_borrow = arr.borrow();
673
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
674
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
437
675
  for (i, v) in arr_borrow.iter().enumerate() {
438
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
676
+ if tishlang_core::has_pending_throw() { break; } // #303
677
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
439
678
  if result.is_truthy() {
440
679
  return v.clone();
441
680
  }
@@ -446,8 +685,10 @@ pub fn find(arr: &Value, callback: &Value) -> Value {
446
685
 
447
686
  pub fn find_index(arr: &Value, callback: &Value) -> Value {
448
687
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
688
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
449
689
  for (i, &n) in data.iter().enumerate() {
450
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
690
+ if tishlang_core::has_pending_throw() { break; } // #303
691
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
451
692
  return Value::Number(i as f64);
452
693
  }
453
694
  }
@@ -455,9 +696,11 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
455
696
  }
456
697
  let arr = as_boxed_array(arr); let arr = &*arr;
457
698
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
458
- let arr_borrow = arr.borrow();
699
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
700
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
459
701
  for (i, v) in arr_borrow.iter().enumerate() {
460
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
702
+ if tishlang_core::has_pending_throw() { break; } // #303
703
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
461
704
  if result.is_truthy() {
462
705
  return Value::Number(i as f64);
463
706
  }
@@ -470,9 +713,11 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
470
713
  /// the original index. Returns `null` (JS `undefined`) when nothing matches. #247
471
714
  pub fn find_last(arr: &Value, callback: &Value) -> Value {
472
715
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
716
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
473
717
  for i in (0..data.len()).rev() {
718
+ if tishlang_core::has_pending_throw() { break; } // #303
474
719
  if cb
475
- .call(&[Value::Number(data[i]), Value::Number(i as f64)])
720
+ .call(&[Value::Number(data[i]), Value::Number(i as f64), arr_value.clone()])
476
721
  .is_truthy()
477
722
  {
478
723
  return Value::Number(data[i]);
@@ -483,10 +728,12 @@ pub fn find_last(arr: &Value, callback: &Value) -> Value {
483
728
  let arr = as_boxed_array(arr);
484
729
  let arr = &*arr;
485
730
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
486
- let arr_borrow = arr.borrow();
731
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
732
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
487
733
  for i in (0..arr_borrow.len()).rev() {
734
+ if tishlang_core::has_pending_throw() { break; } // #303
488
735
  let v = &arr_borrow[i];
489
- if cb.call(&[v.clone(), Value::Number(i as f64)]).is_truthy() {
736
+ if cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
490
737
  return v.clone();
491
738
  }
492
739
  }
@@ -497,9 +744,11 @@ pub fn find_last(arr: &Value, callback: &Value) -> Value {
497
744
  /// `Array.prototype.findLastIndex` — like [`find_index`] but from the end; `-1` if no match. #247
498
745
  pub fn find_last_index(arr: &Value, callback: &Value) -> Value {
499
746
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
747
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
500
748
  for i in (0..data.len()).rev() {
749
+ if tishlang_core::has_pending_throw() { break; } // #303
501
750
  if cb
502
- .call(&[Value::Number(data[i]), Value::Number(i as f64)])
751
+ .call(&[Value::Number(data[i]), Value::Number(i as f64), arr_value.clone()])
503
752
  .is_truthy()
504
753
  {
505
754
  return Value::Number(i as f64);
@@ -510,10 +759,12 @@ pub fn find_last_index(arr: &Value, callback: &Value) -> Value {
510
759
  let arr = as_boxed_array(arr);
511
760
  let arr = &*arr;
512
761
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
513
- let arr_borrow = arr.borrow();
762
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
763
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
514
764
  for i in (0..arr_borrow.len()).rev() {
765
+ if tishlang_core::has_pending_throw() { break; } // #303
515
766
  if cb
516
- .call(&[arr_borrow[i].clone(), Value::Number(i as f64)])
767
+ .call(&[arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()])
517
768
  .is_truthy()
518
769
  {
519
770
  return Value::Number(i as f64);
@@ -545,8 +796,10 @@ pub fn at(arr: &Value, index: &Value) -> Value {
545
796
 
546
797
  pub fn some(arr: &Value, callback: &Value) -> Value {
547
798
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
799
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
548
800
  for (i, &n) in data.iter().enumerate() {
549
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
801
+ if tishlang_core::has_pending_throw() { break; } // #303
802
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
550
803
  return Value::Bool(true);
551
804
  }
552
805
  }
@@ -554,9 +807,11 @@ pub fn some(arr: &Value, callback: &Value) -> Value {
554
807
  }
555
808
  let arr = as_boxed_array(arr); let arr = &*arr;
556
809
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
557
- let arr_borrow = arr.borrow();
810
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
811
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
558
812
  for (i, v) in arr_borrow.iter().enumerate() {
559
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
813
+ if tishlang_core::has_pending_throw() { break; } // #303
814
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
560
815
  if result.is_truthy() {
561
816
  return Value::Bool(true);
562
817
  }
@@ -567,8 +822,10 @@ pub fn some(arr: &Value, callback: &Value) -> Value {
567
822
 
568
823
  pub fn every(arr: &Value, callback: &Value) -> Value {
569
824
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
825
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
570
826
  for (i, &n) in data.iter().enumerate() {
571
- if !cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
827
+ if tishlang_core::has_pending_throw() { break; } // #303
828
+ if !cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
572
829
  return Value::Bool(false);
573
830
  }
574
831
  }
@@ -576,9 +833,11 @@ pub fn every(arr: &Value, callback: &Value) -> Value {
576
833
  }
577
834
  let arr = as_boxed_array(arr); let arr = &*arr;
578
835
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
579
- let arr_borrow = arr.borrow();
836
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
837
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
580
838
  for (i, v) in arr_borrow.iter().enumerate() {
581
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
839
+ if tishlang_core::has_pending_throw() { break; } // #303
840
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
582
841
  if !result.is_truthy() {
583
842
  return Value::Bool(false);
584
843
  }
@@ -592,10 +851,12 @@ pub fn every(arr: &Value, callback: &Value) -> Value {
592
851
  pub fn flat_map(arr: &Value, callback: &Value) -> Value {
593
852
  let arr = as_boxed_array(arr); let arr = &*arr;
594
853
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
595
- let arr_borrow = arr.borrow();
854
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
855
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
596
856
  let mut result: Vec<Value> = Vec::new();
597
857
  for (i, v) in arr_borrow.iter().enumerate() {
598
- let mapped = cb.call(&[v.clone(), Value::Number(i as f64)]);
858
+ if tishlang_core::has_pending_throw() { break; } // #303
859
+ let mapped = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
599
860
  if let Value::Array(inner) = mapped {
600
861
  result.extend(inner.borrow().iter().cloned());
601
862
  } else {
@@ -628,13 +889,23 @@ pub fn sort_default(arr: &Value) -> Value {
628
889
 
629
890
  pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
630
891
  if let (Value::Array(arr), Value::Function(cmp_fn)) = (arr, comparator) {
631
- let mut arr_mut = arr.borrow_mut();
632
- let len = arr_mut.len();
892
+ // #382: move the elements out under a short-lived borrow, then DROP the guard before invoking
893
+ // the comparator. A comparator that reads the array being sorted (the common
894
+ // `arr.sort((a,b) => arr.indexOf(a) - arr.indexOf(b))` idiom) would otherwise re-lock this same
895
+ // Mutex under send-values and self-deadlock the worker (or panic under RefCell) — the exact
896
+ // reentrant-lock class fixed for the captured-cell path in #218/#338. `mem::take` leaves the
897
+ // array empty for the duration of the sort; the final result is written back below.
898
+ let mut elements: Vec<Value> = std::mem::take(&mut *arr.borrow_mut());
899
+ let len = elements.len();
633
900
  let mut indices: Vec<usize> = (0..len).collect();
634
- let mut elements: Vec<Value> = std::mem::take(&mut *arr_mut);
635
901
  let mut args_buf: [Value; 2] = [Value::Null, Value::Null];
636
902
 
637
903
  indices.sort_by(|&a, &b| {
904
+ // #303: once the comparator has thrown, stop invoking it — return Equal so the sort can
905
+ // unwind. Avoids extra comparator calls (and their side effects) after the throw.
906
+ if tishlang_core::has_pending_throw() {
907
+ return std::cmp::Ordering::Equal;
908
+ }
638
909
  args_buf[0] = elements[a].clone();
639
910
  args_buf[1] = elements[b].clone();
640
911
  match cmp_fn.call(&args_buf) {
@@ -644,11 +915,18 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
644
915
  }
645
916
  });
646
917
 
647
- *arr_mut = indices
648
- .into_iter()
649
- .map(|i| std::mem::replace(&mut elements[i], Value::Null))
650
- .collect();
651
- drop(arr_mut);
918
+ // Re-acquire the guard only to write the result back — no user code runs past this point.
919
+ if tishlang_core::has_pending_throw() {
920
+ // #303: the comparator threw — do NOT write the partial/bogus reordering back. Restore the
921
+ // original element order (leave the array unmodified) and let the caller re-raise the throw.
922
+ *arr.borrow_mut() = elements;
923
+ } else {
924
+ let sorted: Vec<Value> = indices
925
+ .into_iter()
926
+ .map(|i| std::mem::replace(&mut elements[i], Value::Null))
927
+ .collect();
928
+ *arr.borrow_mut() = sorted;
929
+ }
652
930
  Value::Array(arr.clone())
653
931
  } else {
654
932
  Value::Null
@@ -757,6 +1035,89 @@ fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
757
1035
  }
758
1036
  }
759
1037
 
1038
+ /// Read a numeric key from an element following a (possibly empty) property path.
1039
+ /// An empty path means the element itself is the numeric key (`(a,b)=>a-b`).
1040
+ /// `["k"]` reads `el.k`; `["a","b"]` reads `el.a.b`. Missing fields collapse to
1041
+ /// NaN, mirroring what `el.k - el.k` would produce in the boxed comparator, so the
1042
+ /// decorate-sort path stays order-identical to the dynamic comparator path.
1043
+ fn read_key_number(el: &Value, path: &[&str]) -> f64 {
1044
+ if path.is_empty() {
1045
+ return el.as_number().unwrap_or(f64::NAN);
1046
+ }
1047
+ let mut cur = el.clone();
1048
+ for (i, seg) in path.iter().enumerate() {
1049
+ let is_last = i + 1 == path.len();
1050
+ match &cur {
1051
+ Value::Object(o) => {
1052
+ let next = o.borrow().strings.get(*seg).cloned();
1053
+ match next {
1054
+ Some(v) if is_last => return v.as_number().unwrap_or(f64::NAN),
1055
+ Some(v) => cur = v,
1056
+ None => return f64::NAN,
1057
+ }
1058
+ }
1059
+ // `.length` is computed (not stored) for strings/arrays — mirror get_member.
1060
+ Value::String(s) if *seg == "length" && is_last => return s.chars().count() as f64,
1061
+ Value::Array(a) if *seg == "length" && is_last => return a.borrow().len() as f64,
1062
+ _ => return f64::NAN,
1063
+ }
1064
+ }
1065
+ f64::NAN
1066
+ }
1067
+
1068
+ /// GENERAL multi-key numeric sort (decorate-sort-undecorate / Schwartzian transform).
1069
+ ///
1070
+ /// The native codegen fuses any comparator that reduces to a lexicographic comparison
1071
+ /// of numeric keys — `(a,b)=>a.k-b.k`, `(a,b)=>b.k-a.k`, or the canonical multi-key
1072
+ /// `(a,b)=>{ if(a.k!==b.k) return a.k-b.k; return a.j-b.j }` — into a call here, with
1073
+ /// `keys` describing each `(property-path, ascending)` key in priority order.
1074
+ ///
1075
+ /// Each element's keys are extracted ONCE into an unboxed `f64` vector, then the index
1076
+ /// permutation is sorted by plain `f64` comparison: no per-comparison hash lookups, no
1077
+ /// `Value` boxing, no closure dispatch. The boxed comparator path does `key_a - key_b`
1078
+ /// and returns its sign, so a lexicographic `partial_cmp` over the same keys (NaN →
1079
+ /// Equal) yields the IDENTICAL ordering — backend-identical with the dynamic path.
1080
+ pub fn sort_by_keys(arr: &Value, keys: &[(&[&str], bool)]) -> Value {
1081
+ let arr = as_boxed_array(arr);
1082
+ if let Value::Array(a) = &*arr {
1083
+ let k = keys.len();
1084
+ let mut g = a.borrow_mut();
1085
+ let len = g.len();
1086
+ // Decorate: pull every numeric key out of the boxed element exactly once.
1087
+ let mut decorated: Vec<(usize, Vec<f64>)> = Vec::with_capacity(len);
1088
+ for (idx, el) in g.iter().enumerate() {
1089
+ let mut row = Vec::with_capacity(k);
1090
+ for (path, _) in keys {
1091
+ row.push(read_key_number(el, path));
1092
+ }
1093
+ decorated.push((idx, row));
1094
+ }
1095
+ // Sort the permutation by lexicographic f64 comparison of the extracted keys.
1096
+ decorated.sort_by(|(_, ka), (_, kb)| {
1097
+ for (col, (_, asc)) in keys.iter().enumerate() {
1098
+ let ord = ka[col]
1099
+ .partial_cmp(&kb[col])
1100
+ .unwrap_or(std::cmp::Ordering::Equal);
1101
+ let ord = if *asc { ord } else { ord.reverse() };
1102
+ if ord != std::cmp::Ordering::Equal {
1103
+ return ord;
1104
+ }
1105
+ }
1106
+ std::cmp::Ordering::Equal
1107
+ });
1108
+ // Undecorate: apply the permutation in place.
1109
+ let mut elements: Vec<Value> = std::mem::take(&mut *g);
1110
+ *g = decorated
1111
+ .into_iter()
1112
+ .map(|(i, _)| std::mem::replace(&mut elements[i], Value::Null))
1113
+ .collect();
1114
+ drop(g);
1115
+ Value::Array(a.clone())
1116
+ } else {
1117
+ Value::Null
1118
+ }
1119
+ }
1120
+
760
1121
  #[cfg(test)]
761
1122
  mod packed_hof_tests {
762
1123
  //! The packed (`NumberArray`) HOF fast paths must be observably IDENTICAL to the boxed path —
@@ -792,14 +1153,14 @@ mod packed_hof_tests {
792
1153
  let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
793
1154
  let add = cb_num(|acc, x| acc + x);
794
1155
  // With init.
795
- assert_eq!(reduce(&n, &add, &Value::Number(0.0)).as_number(), Some(14.0));
796
- // No init → first element seeds, scan from index 1 (same total here).
797
- 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));
798
1159
  // Index arg: callback (acc, _elem, index) — sum the indices 0..5 = 10.
799
1160
  let sum_idx = Value::native(|a: &[Value]| {
800
1161
  Value::Number(a[0].as_number().unwrap() + a[2].as_number().unwrap())
801
1162
  });
802
- 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));
803
1164
  }
804
1165
 
805
1166
  #[test]
@@ -881,4 +1242,295 @@ mod packed_hof_tests {
881
1242
  assert!(matches!(map(&n, &Value::Number(1.0)), Value::Null));
882
1243
  assert!(matches!(filter(&n, &Value::Null), Value::Null));
883
1244
  }
1245
+
1246
+ #[test]
1247
+ fn for_each_stops_on_pending_throw() {
1248
+ use std::sync::{Arc, Mutex};
1249
+ // #303: once the callback parks a throw, for_each must stop invoking it (no extra iterations).
1250
+ let _ = tishlang_core::take_pending_throw(); // start with a clean slot
1251
+ let arr = from_vec(vec![
1252
+ Value::Number(1.0),
1253
+ Value::Number(2.0),
1254
+ Value::Number(3.0),
1255
+ Value::Number(4.0),
1256
+ ]);
1257
+ let calls = Arc::new(Mutex::new(0usize));
1258
+ let c2 = calls.clone();
1259
+ let cb = Value::native(move |a: &[Value]| {
1260
+ *c2.lock().unwrap() += 1;
1261
+ if a[0].as_number() == Some(2.0) {
1262
+ tishlang_core::set_pending_throw(Value::Null);
1263
+ }
1264
+ Value::Null
1265
+ });
1266
+ for_each(&arr, &cb);
1267
+ assert_eq!(
1268
+ *calls.lock().unwrap(),
1269
+ 2,
1270
+ "for_each should stop after the throwing element, not run all 4"
1271
+ );
1272
+ let _ = tishlang_core::take_pending_throw(); // drain so it doesn't leak into other tests
1273
+ }
1274
+
1275
+ #[test]
1276
+ fn sort_with_comparator_throw_restores_original_order() {
1277
+ // #303: a comparator that throws must NOT corrupt the array — sort_with_comparator restores the
1278
+ // original order and leaves the throw parked for the caller to re-raise.
1279
+ let _ = tishlang_core::take_pending_throw();
1280
+ let arr = from_vec(vec![
1281
+ Value::Number(5.0),
1282
+ Value::Number(4.0),
1283
+ Value::Number(3.0),
1284
+ Value::Number(2.0),
1285
+ Value::Number(1.0),
1286
+ ]);
1287
+ // Park a throw on the first comparison and return a dummy ordering value.
1288
+ let cmp = Value::native(|_a: &[Value]| {
1289
+ tishlang_core::set_pending_throw(Value::String("cmp".into()));
1290
+ Value::Null
1291
+ });
1292
+ let _ = sort_with_comparator(&arr, &cmp);
1293
+ if let Value::Array(a) = &arr {
1294
+ let got: Vec<f64> = a
1295
+ .borrow()
1296
+ .iter()
1297
+ .map(|v| v.as_number().unwrap_or(f64::NAN))
1298
+ .collect();
1299
+ assert_eq!(
1300
+ got,
1301
+ vec![5.0, 4.0, 3.0, 2.0, 1.0],
1302
+ "array must be left in its original order after a comparator throw"
1303
+ );
1304
+ } else {
1305
+ panic!("expected a boxed array");
1306
+ }
1307
+ assert!(
1308
+ tishlang_core::take_pending_throw().is_some(),
1309
+ "the parked throw must survive for the caller to re-raise"
1310
+ );
1311
+ }
1312
+ }
1313
+
1314
+ #[cfg(test)]
1315
+ mod sort_by_keys_tests {
1316
+ //! `sort_by_keys` (the fused multi-key numeric comparator path) must produce the
1317
+ //! EXACT same order as running the equivalent boxed comparator through
1318
+ //! `sort_with_comparator`, since cross-backend parity depends on it.
1319
+ use super::*;
1320
+ use std::sync::Arc;
1321
+ use tishlang_core::Value;
1322
+
1323
+ fn rec(key: f64, idx: f64) -> Value {
1324
+ Value::object_from_pairs([
1325
+ (Arc::from("key"), Value::Number(key)),
1326
+ (Arc::from("idx"), Value::Number(idx)),
1327
+ ])
1328
+ }
1329
+
1330
+ fn keys_of(arr: &Value) -> Vec<(f64, f64)> {
1331
+ match arr {
1332
+ Value::Array(a) => a
1333
+ .borrow()
1334
+ .iter()
1335
+ .map(|v| match v {
1336
+ Value::Object(o) => {
1337
+ let b = o.borrow();
1338
+ let k = b.strings.get("key").and_then(|x| x.as_number()).unwrap();
1339
+ let i = b.strings.get("idx").and_then(|x| x.as_number()).unwrap();
1340
+ (k, i)
1341
+ }
1342
+ _ => panic!("expected object element"),
1343
+ })
1344
+ .collect(),
1345
+ _ => panic!("expected array"),
1346
+ }
1347
+ }
1348
+
1349
+ /// A reference boxed comparator with the canonical `key then idx` total order.
1350
+ fn boxed_key_then_idx() -> Value {
1351
+ Value::native(|args: &[Value]| {
1352
+ let a = &args[0];
1353
+ let b = &args[1];
1354
+ let read = |v: &Value, p: &str| match v {
1355
+ Value::Object(o) => o.borrow().strings.get(p).and_then(|x| x.as_number()).unwrap(),
1356
+ _ => f64::NAN,
1357
+ };
1358
+ let (ak, bk) = (read(a, "key"), read(b, "key"));
1359
+ if ak != bk {
1360
+ Value::Number(ak - bk)
1361
+ } else {
1362
+ Value::Number(read(a, "idx") - read(b, "idx"))
1363
+ }
1364
+ })
1365
+ }
1366
+
1367
+ #[test]
1368
+ fn multi_key_matches_boxed_comparator() {
1369
+ // Same unsorted input through both paths; the orders must be identical.
1370
+ let input = || {
1371
+ from_vec(vec![
1372
+ rec(7.0, 0.0),
1373
+ rec(3.0, 1.0),
1374
+ rec(7.0, 2.0),
1375
+ rec(1.0, 3.0),
1376
+ rec(3.0, 4.0),
1377
+ rec(7.0, 5.0),
1378
+ ])
1379
+ };
1380
+ let fused = input();
1381
+ sort_by_keys(
1382
+ &fused,
1383
+ &[(&["key"] as &[&str], true), (&["idx"] as &[&str], true)],
1384
+ );
1385
+ let boxed = input();
1386
+ sort_with_comparator(&boxed, &boxed_key_then_idx());
1387
+ assert_eq!(
1388
+ keys_of(&fused),
1389
+ keys_of(&boxed),
1390
+ "fused multi-key sort must match the boxed comparator order exactly"
1391
+ );
1392
+ // And it is the expected total order.
1393
+ assert_eq!(
1394
+ keys_of(&fused),
1395
+ vec![(1.0, 3.0), (3.0, 1.0), (3.0, 4.0), (7.0, 0.0), (7.0, 2.0), (7.0, 5.0)]
1396
+ );
1397
+ }
1398
+
1399
+ #[test]
1400
+ fn single_key_descending() {
1401
+ let arr = from_vec(vec![rec(2.0, 0.0), rec(9.0, 1.0), rec(5.0, 2.0)]);
1402
+ sort_by_keys(&arr, &[(&["key"] as &[&str], false)]);
1403
+ let ks: Vec<f64> = keys_of(&arr).into_iter().map(|(k, _)| k).collect();
1404
+ assert_eq!(ks, vec![9.0, 5.0, 2.0]);
1405
+ }
1406
+
1407
+ #[test]
1408
+ fn bare_element_key_sorts_numbers() {
1409
+ // Empty path = the element itself is the numeric key (`(a,b)=>a-b`).
1410
+ let arr = from_vec(vec![
1411
+ Value::Number(4.0),
1412
+ Value::Number(1.0),
1413
+ Value::Number(3.0),
1414
+ Value::Number(2.0),
1415
+ ]);
1416
+ sort_by_keys(&arr, &[(&[] as &[&str], true)]);
1417
+ let got: Vec<f64> = match &arr {
1418
+ Value::Array(a) => a.borrow().iter().map(|v| v.as_number().unwrap()).collect(),
1419
+ _ => panic!(),
1420
+ };
1421
+ assert_eq!(got, vec![1.0, 2.0, 3.0, 4.0]);
1422
+ }
1423
+
1424
+ #[test]
1425
+ fn missing_field_reads_nan_and_does_not_panic() {
1426
+ // An element missing the primary key reads NaN; NaN comparisons collapse to
1427
+ // Equal (`partial_cmp -> None -> Equal`), mirroring `a.key - b.key == NaN`
1428
+ // (sign neither <0 nor >0) in the boxed path. The sort must still complete
1429
+ // and return a valid permutation of the input (length + idx-set preserved).
1430
+ let arr = from_vec(vec![
1431
+ rec(5.0, 0.0),
1432
+ Value::object_from_pairs([(Arc::from("idx"), Value::Number(1.0))]), // no "key"
1433
+ rec(2.0, 2.0),
1434
+ ]);
1435
+ sort_by_keys(
1436
+ &arr,
1437
+ &[(&["key"] as &[&str], true), (&["idx"] as &[&str], true)],
1438
+ );
1439
+ let idxs: Vec<f64> = match &arr {
1440
+ Value::Array(a) => a
1441
+ .borrow()
1442
+ .iter()
1443
+ .map(|e| match e {
1444
+ Value::Object(o) => {
1445
+ o.borrow().strings.get("idx").and_then(|x| x.as_number()).unwrap()
1446
+ }
1447
+ _ => f64::NAN,
1448
+ })
1449
+ .collect(),
1450
+ _ => panic!(),
1451
+ };
1452
+ let mut sorted = idxs.clone();
1453
+ sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
1454
+ assert_eq!(sorted, vec![0.0, 1.0, 2.0], "no element lost or duplicated");
1455
+ }
1456
+ }
1457
+
1458
+ #[cfg(test)]
1459
+ mod reentrancy_tests_382 {
1460
+ //! #382: a boxed-array HOF / comparator must snapshot and DROP the array's borrow before running
1461
+ //! any user callback, so a callback that re-enters the SAME array does not deadlock (Mutex, under
1462
+ //! send-values) or panic "already borrowed" (RefCell, the default). Follow-up to #218/#338.
1463
+ use super::*;
1464
+ use tishlang_core::{native_fn, Value, VmRef};
1465
+
1466
+ fn arr3() -> Value {
1467
+ Value::Array(VmRef::new(vec![
1468
+ Value::Number(10.0),
1469
+ Value::Number(20.0),
1470
+ Value::Number(30.0),
1471
+ ]))
1472
+ }
1473
+
1474
+ #[test]
1475
+ fn map_callback_can_mutate_same_array_without_panic() {
1476
+ let arr = arr3();
1477
+ let same = arr.clone(); // shares the VmRef → callback re-enters the same cell
1478
+ let cb = Value::Function(native_fn(move |args| {
1479
+ // borrow_mut() the array being mapped: pre-fix this panics ("already borrowed") because
1480
+ // map still holds arr.borrow(); post-fix map has snapshotted+dropped, so this succeeds.
1481
+ if let Value::Array(a) = &same {
1482
+ a.borrow_mut().push(Value::Number(999.0));
1483
+ }
1484
+ args[0].clone()
1485
+ }));
1486
+ let out = map(&arr, &cb);
1487
+ match out {
1488
+ Value::Array(o) => assert_eq!(o.borrow().len(), 3, "map iterates the 3-element snapshot"),
1489
+ _ => panic!("map should return an array"),
1490
+ }
1491
+ }
1492
+
1493
+ #[test]
1494
+ fn foreach_callback_can_read_same_array_length() {
1495
+ let arr = arr3();
1496
+ let same = arr.clone();
1497
+ let seen = VmRef::new(Vec::<f64>::new());
1498
+ let seen_cb = seen.clone();
1499
+ let cb = Value::Function(native_fn(move |args| {
1500
+ if let Value::Array(a) = &same {
1501
+ // read length from inside the callback (re-enters the same cell)
1502
+ let len = a.borrow().len() as f64;
1503
+ seen_cb.borrow_mut().push(len);
1504
+ }
1505
+ let _ = args;
1506
+ Value::Null
1507
+ }));
1508
+ for_each(&arr, &cb);
1509
+ assert_eq!(seen.borrow().len(), 3, "callback ran once per element without deadlock/panic");
1510
+ }
1511
+
1512
+ #[test]
1513
+ fn sort_comparator_can_read_same_array_without_panic() {
1514
+ let arr = arr3();
1515
+ let same = arr.clone();
1516
+ let cmp = Value::Function(native_fn(move |args| {
1517
+ // A comparator that reads the array being sorted (the arr.indexOf(a) idiom): pre-fix this
1518
+ // re-locks the held guard (deadlock/panic); post-fix the guard is dropped during sorting.
1519
+ if let Value::Array(a) = &same {
1520
+ let _ = a.borrow().len();
1521
+ }
1522
+ match (&args[0], &args[1]) {
1523
+ (Value::Number(x), Value::Number(y)) => Value::Number(x - y),
1524
+ _ => Value::Number(0.0),
1525
+ }
1526
+ }));
1527
+ let out = sort_with_comparator(&arr, &cmp);
1528
+ match out {
1529
+ Value::Array(o) => {
1530
+ let got: Vec<f64> = o.borrow().iter().filter_map(|v| v.as_number()).collect();
1531
+ assert_eq!(got, vec![10.0, 20.0, 30.0], "ascending sort completes, no element lost");
1532
+ }
1533
+ _ => panic!("sort should return an array"),
1534
+ }
1535
+ }
884
1536
  }