@tishlang/tish 2.12.0 → 2.16.13

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 (71) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/tests/integration_test.rs +80 -0
  5. package/crates/tish_ast/src/ast.rs +10 -0
  6. package/crates/tish_builtins/src/array.rs +529 -56
  7. package/crates/tish_builtins/src/collections.rs +114 -40
  8. package/crates/tish_builtins/src/string.rs +95 -8
  9. package/crates/tish_bytecode/src/chunk.rs +7 -0
  10. package/crates/tish_bytecode/src/compiler.rs +560 -64
  11. package/crates/tish_bytecode/src/lib.rs +1 -1
  12. package/crates/tish_bytecode/src/opcode.rs +154 -2
  13. package/crates/tish_bytecode/src/serialize.rs +2 -0
  14. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  15. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  16. package/crates/tish_compile/src/codegen.rs +17736 -5269
  17. package/crates/tish_compile/src/infer.rs +1707 -190
  18. package/crates/tish_compile/src/lib.rs +29 -4
  19. package/crates/tish_compile/src/resolve.rs +66 -5
  20. package/crates/tish_compile/src/types.rs +224 -28
  21. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  22. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  23. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  24. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  25. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  26. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  27. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  28. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  29. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  30. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  31. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  32. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  33. package/crates/tish_compile_js/src/codegen.rs +33 -0
  34. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  35. package/crates/tish_core/Cargo.toml +4 -0
  36. package/crates/tish_core/src/json.rs +104 -5
  37. package/crates/tish_core/src/lib.rs +174 -0
  38. package/crates/tish_core/src/shape.rs +4 -2
  39. package/crates/tish_core/src/value.rs +565 -35
  40. package/crates/tish_core/src/vmref.rs +14 -0
  41. package/crates/tish_eval/src/eval.rs +675 -76
  42. package/crates/tish_eval/src/natives.rs +19 -0
  43. package/crates/tish_eval/src/value.rs +76 -21
  44. package/crates/tish_ffi/src/lib.rs +11 -1
  45. package/crates/tish_ffi/tests/double_free.rs +35 -0
  46. package/crates/tish_fmt/src/lib.rs +75 -1
  47. package/crates/tish_lexer/src/lib.rs +76 -0
  48. package/crates/tish_lexer/src/token.rs +4 -0
  49. package/crates/tish_lint/src/lib.rs +126 -0
  50. package/crates/tish_lsp/README.md +2 -1
  51. package/crates/tish_lsp/src/main.rs +378 -28
  52. package/crates/tish_native/src/build.rs +41 -0
  53. package/crates/tish_parser/Cargo.toml +4 -0
  54. package/crates/tish_parser/src/lib.rs +27 -0
  55. package/crates/tish_parser/src/parser.rs +302 -20
  56. package/crates/tish_resolve/src/lib.rs +9 -0
  57. package/crates/tish_runtime/Cargo.toml +5 -0
  58. package/crates/tish_runtime/src/http.rs +28 -10
  59. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  60. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  61. package/crates/tish_runtime/src/lib.rs +523 -42
  62. package/crates/tish_runtime/src/timers.rs +49 -2
  63. package/crates/tish_ui/src/jsx.rs +3 -0
  64. package/crates/tish_vm/src/jit.rs +2514 -117
  65. package/crates/tish_vm/src/vm.rs +1227 -182
  66. package/package.json +1 -1
  67. package/platform/darwin-arm64/tish +0 -0
  68. package/platform/darwin-x64/tish +0 -0
  69. package/platform/linux-arm64/tish +0 -0
  70. package/platform/linux-x64/tish +0 -0
  71. package/platform/win32-x64/tish.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 {
@@ -307,15 +345,20 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
307
345
  // boxed array on the FIRST non-numeric callback result; every element's callback still runs
308
346
  // exactly once, in index order (the deopt resumes at `i + 1`).
309
347
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
348
+ // JS passes `(element, index, array)`; `arr_value` is the receiver, cloned per call (a cheap
349
+ // VmRef refcount bump). A callback that ignores the 3rd param is unaffected.
350
+ let arr_value = arr.clone();
310
351
  let mut nums: Vec<f64> = Vec::with_capacity(data.len());
311
352
  for (i, &n) in data.iter().enumerate() {
312
- match cb.call(&[Value::Number(n), Value::Number(i as f64)]) {
353
+ if tishlang_core::has_pending_throw() { return packed_or_empty(nums); } // #303
354
+ match cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]) {
313
355
  Value::Number(r) => nums.push(r),
314
356
  other => {
315
357
  let mut boxed: Vec<Value> = nums.into_iter().map(Value::Number).collect();
316
358
  boxed.push(other);
317
359
  for (j, &m) in data.iter().enumerate().skip(i + 1) {
318
- boxed.push(cb.call(&[Value::Number(m), Value::Number(j as f64)]));
360
+ if tishlang_core::has_pending_throw() { break; } // #303
361
+ boxed.push(cb.call(&[Value::Number(m), Value::Number(j as f64), arr_value.clone()]));
319
362
  }
320
363
  return Value::Array(VmRef::new(boxed));
321
364
  }
@@ -325,12 +368,17 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
325
368
  }
326
369
  let arr = as_boxed_array(arr); let arr = &*arr;
327
370
  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();
371
+ // #382: snapshot the backing store and DROP the borrow before running any callback — a
372
+ // callback that re-enters this same array (`arr.map(x => arr.length)`) would otherwise
373
+ // deadlock (Mutex, send-values) or panic (RefCell). Matches the packed path's copy semantics.
374
+ let arr_borrow = arr.borrow().clone();
375
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
376
+ // #303: stop on a pending throw from the callback (don't map the rest of the array).
377
+ let mut result: Vec<Value> = Vec::with_capacity(arr_borrow.len());
378
+ for (i, v) in arr_borrow.iter().enumerate() {
379
+ if tishlang_core::has_pending_throw() { break; }
380
+ result.push(cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]));
381
+ }
334
382
  Value::Array(VmRef::new(result))
335
383
  } else {
336
384
  Value::Null
@@ -341,9 +389,11 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
341
389
  // Packed fast path: `filter` keeps a SUBSET of the input f64s, so the result is always numeric —
342
390
  // build the packed `Vec<f64>` directly, no boxed intermediate, and hand back a `NumberArray`.
343
391
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
392
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
344
393
  let mut out: Vec<f64> = Vec::new();
345
394
  for (i, &n) in data.iter().enumerate() {
346
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
395
+ if tishlang_core::has_pending_throw() { break; } // #303
396
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
347
397
  out.push(n);
348
398
  }
349
399
  }
@@ -351,19 +401,16 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
351
401
  }
352
402
  let arr = as_boxed_array(arr); let arr = &*arr;
353
403
  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();
404
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
405
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
406
+ // #303: stop on a pending throw from the predicate (don't test the rest of the array).
407
+ let mut result: Vec<Value> = Vec::new();
408
+ for (i, v) in arr_borrow.iter().enumerate() {
409
+ if tishlang_core::has_pending_throw() { break; }
410
+ if cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
411
+ result.push(v.clone());
412
+ }
413
+ }
367
414
  Value::Array(VmRef::new(result))
368
415
  } else {
369
416
  Value::Null
@@ -374,6 +421,8 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
374
421
  // Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
375
422
  // path (absent init → first element as the seed, scan from index 1).
376
423
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
424
+ // `reduce` passes `(accumulator, element, index, array)` — the array is the 4th arg.
425
+ let arr_value = arr.clone();
377
426
  let (start_idx, mut acc) = if matches!(initial, Value::Null) && !data.is_empty() {
378
427
  (1usize, Value::Number(data[0]))
379
428
  } else {
@@ -381,13 +430,15 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
381
430
  };
382
431
  // `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
383
432
  for (i, &x) in data.iter().enumerate().skip(start_idx) {
384
- acc = cb.call(&[acc, Value::Number(x), Value::Number(i as f64)]);
433
+ if tishlang_core::has_pending_throw() { return acc; } // #303
434
+ acc = cb.call(&[acc, Value::Number(x), Value::Number(i as f64), arr_value.clone()]);
385
435
  }
386
436
  return acc;
387
437
  }
388
438
  let arr = as_boxed_array(arr); let arr = &*arr;
389
439
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
390
- let arr_borrow = arr.borrow();
440
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
441
+ let arr_value = Value::Array(arr.clone()); // 4th callback arg (JS `array`)
391
442
  let len = arr_borrow.len();
392
443
  let (start_idx, mut acc) = if matches!(initial, Value::Null) && !arr_borrow.is_empty() {
393
444
  // No initial value: use first element as acc, start from index 1
@@ -396,8 +447,9 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
396
447
  (0, initial.clone())
397
448
  };
398
449
  for i in start_idx..len {
450
+ if tishlang_core::has_pending_throw() { return acc; } // #303
399
451
  let v = arr_borrow[i].clone();
400
- acc = cb.call(&[acc, v.clone(), Value::Number(i as f64)]);
452
+ acc = cb.call(&[acc, v.clone(), Value::Number(i as f64), arr_value.clone()]);
401
453
  }
402
454
  acc
403
455
  } else {
@@ -407,16 +459,20 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
407
459
 
408
460
  pub fn for_each(arr: &Value, callback: &Value) -> Value {
409
461
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
462
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
410
463
  for (i, &n) in data.iter().enumerate() {
411
- cb.call(&[Value::Number(n), Value::Number(i as f64)]);
464
+ if tishlang_core::has_pending_throw() { return Value::Null; } // #303
465
+ cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]);
412
466
  }
413
467
  return Value::Null;
414
468
  }
415
469
  let arr = as_boxed_array(arr); let arr = &*arr;
416
470
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
417
- let arr_borrow = arr.borrow();
471
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
472
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
418
473
  for (i, v) in arr_borrow.iter().enumerate() {
419
- cb.call(&[v.clone(), Value::Number(i as f64)]);
474
+ if tishlang_core::has_pending_throw() { break; } // #303
475
+ cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
420
476
  }
421
477
  }
422
478
  Value::Null
@@ -424,8 +480,10 @@ pub fn for_each(arr: &Value, callback: &Value) -> Value {
424
480
 
425
481
  pub fn find(arr: &Value, callback: &Value) -> Value {
426
482
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
483
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
427
484
  for (i, &n) in data.iter().enumerate() {
428
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
485
+ if tishlang_core::has_pending_throw() { break; } // #303
486
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
429
487
  return Value::Number(n);
430
488
  }
431
489
  }
@@ -433,9 +491,11 @@ pub fn find(arr: &Value, callback: &Value) -> Value {
433
491
  }
434
492
  let arr = as_boxed_array(arr); let arr = &*arr;
435
493
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
436
- let arr_borrow = arr.borrow();
494
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
495
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
437
496
  for (i, v) in arr_borrow.iter().enumerate() {
438
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
497
+ if tishlang_core::has_pending_throw() { break; } // #303
498
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
439
499
  if result.is_truthy() {
440
500
  return v.clone();
441
501
  }
@@ -446,8 +506,10 @@ pub fn find(arr: &Value, callback: &Value) -> Value {
446
506
 
447
507
  pub fn find_index(arr: &Value, callback: &Value) -> Value {
448
508
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
509
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
449
510
  for (i, &n) in data.iter().enumerate() {
450
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
511
+ if tishlang_core::has_pending_throw() { break; } // #303
512
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
451
513
  return Value::Number(i as f64);
452
514
  }
453
515
  }
@@ -455,9 +517,11 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
455
517
  }
456
518
  let arr = as_boxed_array(arr); let arr = &*arr;
457
519
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
458
- let arr_borrow = arr.borrow();
520
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
521
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
459
522
  for (i, v) in arr_borrow.iter().enumerate() {
460
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
523
+ if tishlang_core::has_pending_throw() { break; } // #303
524
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
461
525
  if result.is_truthy() {
462
526
  return Value::Number(i as f64);
463
527
  }
@@ -470,9 +534,11 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
470
534
  /// the original index. Returns `null` (JS `undefined`) when nothing matches. #247
471
535
  pub fn find_last(arr: &Value, callback: &Value) -> Value {
472
536
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
537
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
473
538
  for i in (0..data.len()).rev() {
539
+ if tishlang_core::has_pending_throw() { break; } // #303
474
540
  if cb
475
- .call(&[Value::Number(data[i]), Value::Number(i as f64)])
541
+ .call(&[Value::Number(data[i]), Value::Number(i as f64), arr_value.clone()])
476
542
  .is_truthy()
477
543
  {
478
544
  return Value::Number(data[i]);
@@ -483,10 +549,12 @@ pub fn find_last(arr: &Value, callback: &Value) -> Value {
483
549
  let arr = as_boxed_array(arr);
484
550
  let arr = &*arr;
485
551
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
486
- let arr_borrow = arr.borrow();
552
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
553
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
487
554
  for i in (0..arr_borrow.len()).rev() {
555
+ if tishlang_core::has_pending_throw() { break; } // #303
488
556
  let v = &arr_borrow[i];
489
- if cb.call(&[v.clone(), Value::Number(i as f64)]).is_truthy() {
557
+ if cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
490
558
  return v.clone();
491
559
  }
492
560
  }
@@ -497,9 +565,11 @@ pub fn find_last(arr: &Value, callback: &Value) -> Value {
497
565
  /// `Array.prototype.findLastIndex` — like [`find_index`] but from the end; `-1` if no match. #247
498
566
  pub fn find_last_index(arr: &Value, callback: &Value) -> Value {
499
567
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
568
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
500
569
  for i in (0..data.len()).rev() {
570
+ if tishlang_core::has_pending_throw() { break; } // #303
501
571
  if cb
502
- .call(&[Value::Number(data[i]), Value::Number(i as f64)])
572
+ .call(&[Value::Number(data[i]), Value::Number(i as f64), arr_value.clone()])
503
573
  .is_truthy()
504
574
  {
505
575
  return Value::Number(i as f64);
@@ -510,10 +580,12 @@ pub fn find_last_index(arr: &Value, callback: &Value) -> Value {
510
580
  let arr = as_boxed_array(arr);
511
581
  let arr = &*arr;
512
582
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
513
- let arr_borrow = arr.borrow();
583
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
584
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
514
585
  for i in (0..arr_borrow.len()).rev() {
586
+ if tishlang_core::has_pending_throw() { break; } // #303
515
587
  if cb
516
- .call(&[arr_borrow[i].clone(), Value::Number(i as f64)])
588
+ .call(&[arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()])
517
589
  .is_truthy()
518
590
  {
519
591
  return Value::Number(i as f64);
@@ -545,8 +617,10 @@ pub fn at(arr: &Value, index: &Value) -> Value {
545
617
 
546
618
  pub fn some(arr: &Value, callback: &Value) -> Value {
547
619
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
620
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
548
621
  for (i, &n) in data.iter().enumerate() {
549
- if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
622
+ if tishlang_core::has_pending_throw() { break; } // #303
623
+ if cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
550
624
  return Value::Bool(true);
551
625
  }
552
626
  }
@@ -554,9 +628,11 @@ pub fn some(arr: &Value, callback: &Value) -> Value {
554
628
  }
555
629
  let arr = as_boxed_array(arr); let arr = &*arr;
556
630
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
557
- let arr_borrow = arr.borrow();
631
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
632
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
558
633
  for (i, v) in arr_borrow.iter().enumerate() {
559
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
634
+ if tishlang_core::has_pending_throw() { break; } // #303
635
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
560
636
  if result.is_truthy() {
561
637
  return Value::Bool(true);
562
638
  }
@@ -567,8 +643,10 @@ pub fn some(arr: &Value, callback: &Value) -> Value {
567
643
 
568
644
  pub fn every(arr: &Value, callback: &Value) -> Value {
569
645
  if let Some((data, cb)) = packed_snapshot(arr, callback) {
646
+ let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
570
647
  for (i, &n) in data.iter().enumerate() {
571
- if !cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
648
+ if tishlang_core::has_pending_throw() { break; } // #303
649
+ if !cb.call(&[Value::Number(n), Value::Number(i as f64), arr_value.clone()]).is_truthy() {
572
650
  return Value::Bool(false);
573
651
  }
574
652
  }
@@ -576,9 +654,11 @@ pub fn every(arr: &Value, callback: &Value) -> Value {
576
654
  }
577
655
  let arr = as_boxed_array(arr); let arr = &*arr;
578
656
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
579
- let arr_borrow = arr.borrow();
657
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
658
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
580
659
  for (i, v) in arr_borrow.iter().enumerate() {
581
- let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
660
+ if tishlang_core::has_pending_throw() { break; } // #303
661
+ let result = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
582
662
  if !result.is_truthy() {
583
663
  return Value::Bool(false);
584
664
  }
@@ -592,10 +672,12 @@ pub fn every(arr: &Value, callback: &Value) -> Value {
592
672
  pub fn flat_map(arr: &Value, callback: &Value) -> Value {
593
673
  let arr = as_boxed_array(arr); let arr = &*arr;
594
674
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
595
- let arr_borrow = arr.borrow();
675
+ let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
676
+ let arr_value = Value::Array(arr.clone()); // 3rd callback arg (JS `array`)
596
677
  let mut result: Vec<Value> = Vec::new();
597
678
  for (i, v) in arr_borrow.iter().enumerate() {
598
- let mapped = cb.call(&[v.clone(), Value::Number(i as f64)]);
679
+ if tishlang_core::has_pending_throw() { break; } // #303
680
+ let mapped = cb.call(&[v.clone(), Value::Number(i as f64), arr_value.clone()]);
599
681
  if let Value::Array(inner) = mapped {
600
682
  result.extend(inner.borrow().iter().cloned());
601
683
  } else {
@@ -628,13 +710,23 @@ pub fn sort_default(arr: &Value) -> Value {
628
710
 
629
711
  pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
630
712
  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();
713
+ // #382: move the elements out under a short-lived borrow, then DROP the guard before invoking
714
+ // the comparator. A comparator that reads the array being sorted (the common
715
+ // `arr.sort((a,b) => arr.indexOf(a) - arr.indexOf(b))` idiom) would otherwise re-lock this same
716
+ // Mutex under send-values and self-deadlock the worker (or panic under RefCell) — the exact
717
+ // reentrant-lock class fixed for the captured-cell path in #218/#338. `mem::take` leaves the
718
+ // array empty for the duration of the sort; the final result is written back below.
719
+ let mut elements: Vec<Value> = std::mem::take(&mut *arr.borrow_mut());
720
+ let len = elements.len();
633
721
  let mut indices: Vec<usize> = (0..len).collect();
634
- let mut elements: Vec<Value> = std::mem::take(&mut *arr_mut);
635
722
  let mut args_buf: [Value; 2] = [Value::Null, Value::Null];
636
723
 
637
724
  indices.sort_by(|&a, &b| {
725
+ // #303: once the comparator has thrown, stop invoking it — return Equal so the sort can
726
+ // unwind. Avoids extra comparator calls (and their side effects) after the throw.
727
+ if tishlang_core::has_pending_throw() {
728
+ return std::cmp::Ordering::Equal;
729
+ }
638
730
  args_buf[0] = elements[a].clone();
639
731
  args_buf[1] = elements[b].clone();
640
732
  match cmp_fn.call(&args_buf) {
@@ -644,11 +736,18 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
644
736
  }
645
737
  });
646
738
 
647
- *arr_mut = indices
648
- .into_iter()
649
- .map(|i| std::mem::replace(&mut elements[i], Value::Null))
650
- .collect();
651
- drop(arr_mut);
739
+ // Re-acquire the guard only to write the result back — no user code runs past this point.
740
+ if tishlang_core::has_pending_throw() {
741
+ // #303: the comparator threw — do NOT write the partial/bogus reordering back. Restore the
742
+ // original element order (leave the array unmodified) and let the caller re-raise the throw.
743
+ *arr.borrow_mut() = elements;
744
+ } else {
745
+ let sorted: Vec<Value> = indices
746
+ .into_iter()
747
+ .map(|i| std::mem::replace(&mut elements[i], Value::Null))
748
+ .collect();
749
+ *arr.borrow_mut() = sorted;
750
+ }
652
751
  Value::Array(arr.clone())
653
752
  } else {
654
753
  Value::Null
@@ -757,6 +856,89 @@ fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
757
856
  }
758
857
  }
759
858
 
859
+ /// Read a numeric key from an element following a (possibly empty) property path.
860
+ /// An empty path means the element itself is the numeric key (`(a,b)=>a-b`).
861
+ /// `["k"]` reads `el.k`; `["a","b"]` reads `el.a.b`. Missing fields collapse to
862
+ /// NaN, mirroring what `el.k - el.k` would produce in the boxed comparator, so the
863
+ /// decorate-sort path stays order-identical to the dynamic comparator path.
864
+ fn read_key_number(el: &Value, path: &[&str]) -> f64 {
865
+ if path.is_empty() {
866
+ return el.as_number().unwrap_or(f64::NAN);
867
+ }
868
+ let mut cur = el.clone();
869
+ for (i, seg) in path.iter().enumerate() {
870
+ let is_last = i + 1 == path.len();
871
+ match &cur {
872
+ Value::Object(o) => {
873
+ let next = o.borrow().strings.get(*seg).cloned();
874
+ match next {
875
+ Some(v) if is_last => return v.as_number().unwrap_or(f64::NAN),
876
+ Some(v) => cur = v,
877
+ None => return f64::NAN,
878
+ }
879
+ }
880
+ // `.length` is computed (not stored) for strings/arrays — mirror get_member.
881
+ Value::String(s) if *seg == "length" && is_last => return s.chars().count() as f64,
882
+ Value::Array(a) if *seg == "length" && is_last => return a.borrow().len() as f64,
883
+ _ => return f64::NAN,
884
+ }
885
+ }
886
+ f64::NAN
887
+ }
888
+
889
+ /// GENERAL multi-key numeric sort (decorate-sort-undecorate / Schwartzian transform).
890
+ ///
891
+ /// The native codegen fuses any comparator that reduces to a lexicographic comparison
892
+ /// of numeric keys — `(a,b)=>a.k-b.k`, `(a,b)=>b.k-a.k`, or the canonical multi-key
893
+ /// `(a,b)=>{ if(a.k!==b.k) return a.k-b.k; return a.j-b.j }` — into a call here, with
894
+ /// `keys` describing each `(property-path, ascending)` key in priority order.
895
+ ///
896
+ /// Each element's keys are extracted ONCE into an unboxed `f64` vector, then the index
897
+ /// permutation is sorted by plain `f64` comparison: no per-comparison hash lookups, no
898
+ /// `Value` boxing, no closure dispatch. The boxed comparator path does `key_a - key_b`
899
+ /// and returns its sign, so a lexicographic `partial_cmp` over the same keys (NaN →
900
+ /// Equal) yields the IDENTICAL ordering — backend-identical with the dynamic path.
901
+ pub fn sort_by_keys(arr: &Value, keys: &[(&[&str], bool)]) -> Value {
902
+ let arr = as_boxed_array(arr);
903
+ if let Value::Array(a) = &*arr {
904
+ let k = keys.len();
905
+ let mut g = a.borrow_mut();
906
+ let len = g.len();
907
+ // Decorate: pull every numeric key out of the boxed element exactly once.
908
+ let mut decorated: Vec<(usize, Vec<f64>)> = Vec::with_capacity(len);
909
+ for (idx, el) in g.iter().enumerate() {
910
+ let mut row = Vec::with_capacity(k);
911
+ for (path, _) in keys {
912
+ row.push(read_key_number(el, path));
913
+ }
914
+ decorated.push((idx, row));
915
+ }
916
+ // Sort the permutation by lexicographic f64 comparison of the extracted keys.
917
+ decorated.sort_by(|(_, ka), (_, kb)| {
918
+ for (col, (_, asc)) in keys.iter().enumerate() {
919
+ let ord = ka[col]
920
+ .partial_cmp(&kb[col])
921
+ .unwrap_or(std::cmp::Ordering::Equal);
922
+ let ord = if *asc { ord } else { ord.reverse() };
923
+ if ord != std::cmp::Ordering::Equal {
924
+ return ord;
925
+ }
926
+ }
927
+ std::cmp::Ordering::Equal
928
+ });
929
+ // Undecorate: apply the permutation in place.
930
+ let mut elements: Vec<Value> = std::mem::take(&mut *g);
931
+ *g = decorated
932
+ .into_iter()
933
+ .map(|(i, _)| std::mem::replace(&mut elements[i], Value::Null))
934
+ .collect();
935
+ drop(g);
936
+ Value::Array(a.clone())
937
+ } else {
938
+ Value::Null
939
+ }
940
+ }
941
+
760
942
  #[cfg(test)]
761
943
  mod packed_hof_tests {
762
944
  //! The packed (`NumberArray`) HOF fast paths must be observably IDENTICAL to the boxed path —
@@ -881,4 +1063,295 @@ mod packed_hof_tests {
881
1063
  assert!(matches!(map(&n, &Value::Number(1.0)), Value::Null));
882
1064
  assert!(matches!(filter(&n, &Value::Null), Value::Null));
883
1065
  }
1066
+
1067
+ #[test]
1068
+ fn for_each_stops_on_pending_throw() {
1069
+ use std::sync::{Arc, Mutex};
1070
+ // #303: once the callback parks a throw, for_each must stop invoking it (no extra iterations).
1071
+ let _ = tishlang_core::take_pending_throw(); // start with a clean slot
1072
+ let arr = from_vec(vec![
1073
+ Value::Number(1.0),
1074
+ Value::Number(2.0),
1075
+ Value::Number(3.0),
1076
+ Value::Number(4.0),
1077
+ ]);
1078
+ let calls = Arc::new(Mutex::new(0usize));
1079
+ let c2 = calls.clone();
1080
+ let cb = Value::native(move |a: &[Value]| {
1081
+ *c2.lock().unwrap() += 1;
1082
+ if a[0].as_number() == Some(2.0) {
1083
+ tishlang_core::set_pending_throw(Value::Null);
1084
+ }
1085
+ Value::Null
1086
+ });
1087
+ for_each(&arr, &cb);
1088
+ assert_eq!(
1089
+ *calls.lock().unwrap(),
1090
+ 2,
1091
+ "for_each should stop after the throwing element, not run all 4"
1092
+ );
1093
+ let _ = tishlang_core::take_pending_throw(); // drain so it doesn't leak into other tests
1094
+ }
1095
+
1096
+ #[test]
1097
+ fn sort_with_comparator_throw_restores_original_order() {
1098
+ // #303: a comparator that throws must NOT corrupt the array — sort_with_comparator restores the
1099
+ // original order and leaves the throw parked for the caller to re-raise.
1100
+ let _ = tishlang_core::take_pending_throw();
1101
+ let arr = from_vec(vec![
1102
+ Value::Number(5.0),
1103
+ Value::Number(4.0),
1104
+ Value::Number(3.0),
1105
+ Value::Number(2.0),
1106
+ Value::Number(1.0),
1107
+ ]);
1108
+ // Park a throw on the first comparison and return a dummy ordering value.
1109
+ let cmp = Value::native(|_a: &[Value]| {
1110
+ tishlang_core::set_pending_throw(Value::String("cmp".into()));
1111
+ Value::Null
1112
+ });
1113
+ let _ = sort_with_comparator(&arr, &cmp);
1114
+ if let Value::Array(a) = &arr {
1115
+ let got: Vec<f64> = a
1116
+ .borrow()
1117
+ .iter()
1118
+ .map(|v| v.as_number().unwrap_or(f64::NAN))
1119
+ .collect();
1120
+ assert_eq!(
1121
+ got,
1122
+ vec![5.0, 4.0, 3.0, 2.0, 1.0],
1123
+ "array must be left in its original order after a comparator throw"
1124
+ );
1125
+ } else {
1126
+ panic!("expected a boxed array");
1127
+ }
1128
+ assert!(
1129
+ tishlang_core::take_pending_throw().is_some(),
1130
+ "the parked throw must survive for the caller to re-raise"
1131
+ );
1132
+ }
1133
+ }
1134
+
1135
+ #[cfg(test)]
1136
+ mod sort_by_keys_tests {
1137
+ //! `sort_by_keys` (the fused multi-key numeric comparator path) must produce the
1138
+ //! EXACT same order as running the equivalent boxed comparator through
1139
+ //! `sort_with_comparator`, since cross-backend parity depends on it.
1140
+ use super::*;
1141
+ use std::sync::Arc;
1142
+ use tishlang_core::Value;
1143
+
1144
+ fn rec(key: f64, idx: f64) -> Value {
1145
+ Value::object_from_pairs([
1146
+ (Arc::from("key"), Value::Number(key)),
1147
+ (Arc::from("idx"), Value::Number(idx)),
1148
+ ])
1149
+ }
1150
+
1151
+ fn keys_of(arr: &Value) -> Vec<(f64, f64)> {
1152
+ match arr {
1153
+ Value::Array(a) => a
1154
+ .borrow()
1155
+ .iter()
1156
+ .map(|v| match v {
1157
+ Value::Object(o) => {
1158
+ let b = o.borrow();
1159
+ let k = b.strings.get("key").and_then(|x| x.as_number()).unwrap();
1160
+ let i = b.strings.get("idx").and_then(|x| x.as_number()).unwrap();
1161
+ (k, i)
1162
+ }
1163
+ _ => panic!("expected object element"),
1164
+ })
1165
+ .collect(),
1166
+ _ => panic!("expected array"),
1167
+ }
1168
+ }
1169
+
1170
+ /// A reference boxed comparator with the canonical `key then idx` total order.
1171
+ fn boxed_key_then_idx() -> Value {
1172
+ Value::native(|args: &[Value]| {
1173
+ let a = &args[0];
1174
+ let b = &args[1];
1175
+ let read = |v: &Value, p: &str| match v {
1176
+ Value::Object(o) => o.borrow().strings.get(p).and_then(|x| x.as_number()).unwrap(),
1177
+ _ => f64::NAN,
1178
+ };
1179
+ let (ak, bk) = (read(a, "key"), read(b, "key"));
1180
+ if ak != bk {
1181
+ Value::Number(ak - bk)
1182
+ } else {
1183
+ Value::Number(read(a, "idx") - read(b, "idx"))
1184
+ }
1185
+ })
1186
+ }
1187
+
1188
+ #[test]
1189
+ fn multi_key_matches_boxed_comparator() {
1190
+ // Same unsorted input through both paths; the orders must be identical.
1191
+ let input = || {
1192
+ from_vec(vec![
1193
+ rec(7.0, 0.0),
1194
+ rec(3.0, 1.0),
1195
+ rec(7.0, 2.0),
1196
+ rec(1.0, 3.0),
1197
+ rec(3.0, 4.0),
1198
+ rec(7.0, 5.0),
1199
+ ])
1200
+ };
1201
+ let fused = input();
1202
+ sort_by_keys(
1203
+ &fused,
1204
+ &[(&["key"] as &[&str], true), (&["idx"] as &[&str], true)],
1205
+ );
1206
+ let boxed = input();
1207
+ sort_with_comparator(&boxed, &boxed_key_then_idx());
1208
+ assert_eq!(
1209
+ keys_of(&fused),
1210
+ keys_of(&boxed),
1211
+ "fused multi-key sort must match the boxed comparator order exactly"
1212
+ );
1213
+ // And it is the expected total order.
1214
+ assert_eq!(
1215
+ keys_of(&fused),
1216
+ 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)]
1217
+ );
1218
+ }
1219
+
1220
+ #[test]
1221
+ fn single_key_descending() {
1222
+ let arr = from_vec(vec![rec(2.0, 0.0), rec(9.0, 1.0), rec(5.0, 2.0)]);
1223
+ sort_by_keys(&arr, &[(&["key"] as &[&str], false)]);
1224
+ let ks: Vec<f64> = keys_of(&arr).into_iter().map(|(k, _)| k).collect();
1225
+ assert_eq!(ks, vec![9.0, 5.0, 2.0]);
1226
+ }
1227
+
1228
+ #[test]
1229
+ fn bare_element_key_sorts_numbers() {
1230
+ // Empty path = the element itself is the numeric key (`(a,b)=>a-b`).
1231
+ let arr = from_vec(vec![
1232
+ Value::Number(4.0),
1233
+ Value::Number(1.0),
1234
+ Value::Number(3.0),
1235
+ Value::Number(2.0),
1236
+ ]);
1237
+ sort_by_keys(&arr, &[(&[] as &[&str], true)]);
1238
+ let got: Vec<f64> = match &arr {
1239
+ Value::Array(a) => a.borrow().iter().map(|v| v.as_number().unwrap()).collect(),
1240
+ _ => panic!(),
1241
+ };
1242
+ assert_eq!(got, vec![1.0, 2.0, 3.0, 4.0]);
1243
+ }
1244
+
1245
+ #[test]
1246
+ fn missing_field_reads_nan_and_does_not_panic() {
1247
+ // An element missing the primary key reads NaN; NaN comparisons collapse to
1248
+ // Equal (`partial_cmp -> None -> Equal`), mirroring `a.key - b.key == NaN`
1249
+ // (sign neither <0 nor >0) in the boxed path. The sort must still complete
1250
+ // and return a valid permutation of the input (length + idx-set preserved).
1251
+ let arr = from_vec(vec![
1252
+ rec(5.0, 0.0),
1253
+ Value::object_from_pairs([(Arc::from("idx"), Value::Number(1.0))]), // no "key"
1254
+ rec(2.0, 2.0),
1255
+ ]);
1256
+ sort_by_keys(
1257
+ &arr,
1258
+ &[(&["key"] as &[&str], true), (&["idx"] as &[&str], true)],
1259
+ );
1260
+ let idxs: Vec<f64> = match &arr {
1261
+ Value::Array(a) => a
1262
+ .borrow()
1263
+ .iter()
1264
+ .map(|e| match e {
1265
+ Value::Object(o) => {
1266
+ o.borrow().strings.get("idx").and_then(|x| x.as_number()).unwrap()
1267
+ }
1268
+ _ => f64::NAN,
1269
+ })
1270
+ .collect(),
1271
+ _ => panic!(),
1272
+ };
1273
+ let mut sorted = idxs.clone();
1274
+ sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
1275
+ assert_eq!(sorted, vec![0.0, 1.0, 2.0], "no element lost or duplicated");
1276
+ }
1277
+ }
1278
+
1279
+ #[cfg(test)]
1280
+ mod reentrancy_tests_382 {
1281
+ //! #382: a boxed-array HOF / comparator must snapshot and DROP the array's borrow before running
1282
+ //! any user callback, so a callback that re-enters the SAME array does not deadlock (Mutex, under
1283
+ //! send-values) or panic "already borrowed" (RefCell, the default). Follow-up to #218/#338.
1284
+ use super::*;
1285
+ use tishlang_core::{native_fn, Value, VmRef};
1286
+
1287
+ fn arr3() -> Value {
1288
+ Value::Array(VmRef::new(vec![
1289
+ Value::Number(10.0),
1290
+ Value::Number(20.0),
1291
+ Value::Number(30.0),
1292
+ ]))
1293
+ }
1294
+
1295
+ #[test]
1296
+ fn map_callback_can_mutate_same_array_without_panic() {
1297
+ let arr = arr3();
1298
+ let same = arr.clone(); // shares the VmRef → callback re-enters the same cell
1299
+ let cb = Value::Function(native_fn(move |args| {
1300
+ // borrow_mut() the array being mapped: pre-fix this panics ("already borrowed") because
1301
+ // map still holds arr.borrow(); post-fix map has snapshotted+dropped, so this succeeds.
1302
+ if let Value::Array(a) = &same {
1303
+ a.borrow_mut().push(Value::Number(999.0));
1304
+ }
1305
+ args[0].clone()
1306
+ }));
1307
+ let out = map(&arr, &cb);
1308
+ match out {
1309
+ Value::Array(o) => assert_eq!(o.borrow().len(), 3, "map iterates the 3-element snapshot"),
1310
+ _ => panic!("map should return an array"),
1311
+ }
1312
+ }
1313
+
1314
+ #[test]
1315
+ fn foreach_callback_can_read_same_array_length() {
1316
+ let arr = arr3();
1317
+ let same = arr.clone();
1318
+ let seen = VmRef::new(Vec::<f64>::new());
1319
+ let seen_cb = seen.clone();
1320
+ let cb = Value::Function(native_fn(move |args| {
1321
+ if let Value::Array(a) = &same {
1322
+ // read length from inside the callback (re-enters the same cell)
1323
+ let len = a.borrow().len() as f64;
1324
+ seen_cb.borrow_mut().push(len);
1325
+ }
1326
+ let _ = args;
1327
+ Value::Null
1328
+ }));
1329
+ for_each(&arr, &cb);
1330
+ assert_eq!(seen.borrow().len(), 3, "callback ran once per element without deadlock/panic");
1331
+ }
1332
+
1333
+ #[test]
1334
+ fn sort_comparator_can_read_same_array_without_panic() {
1335
+ let arr = arr3();
1336
+ let same = arr.clone();
1337
+ let cmp = Value::Function(native_fn(move |args| {
1338
+ // A comparator that reads the array being sorted (the arr.indexOf(a) idiom): pre-fix this
1339
+ // re-locks the held guard (deadlock/panic); post-fix the guard is dropped during sorting.
1340
+ if let Value::Array(a) = &same {
1341
+ let _ = a.borrow().len();
1342
+ }
1343
+ match (&args[0], &args[1]) {
1344
+ (Value::Number(x), Value::Number(y)) => Value::Number(x - y),
1345
+ _ => Value::Number(0.0),
1346
+ }
1347
+ }));
1348
+ let out = sort_with_comparator(&arr, &cmp);
1349
+ match out {
1350
+ Value::Array(o) => {
1351
+ let got: Vec<f64> = o.borrow().iter().filter_map(|v| v.as_number()).collect();
1352
+ assert_eq!(got, vec![10.0, 20.0, 30.0], "ascending sort completes, no element lost");
1353
+ }
1354
+ _ => panic!("sort should return an array"),
1355
+ }
1356
+ }
884
1357
  }