@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.
- package/Cargo.toml +3 -0
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +1 -1
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish_ast/src/ast.rs +10 -0
- package/crates/tish_builtins/src/array.rs +529 -56
- package/crates/tish_builtins/src/collections.rs +114 -40
- package/crates/tish_builtins/src/string.rs +95 -8
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +560 -64
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +154 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +17736 -5269
- package/crates/tish_compile/src/infer.rs +1707 -190
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +66 -5
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +33 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +104 -5
- package/crates/tish_core/src/lib.rs +174 -0
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/value.rs +565 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/src/eval.rs +675 -76
- package/crates/tish_eval/src/natives.rs +19 -0
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +75 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +27 -0
- package/crates/tish_parser/src/parser.rs +302 -20
- package/crates/tish_resolve/src/lib.rs +9 -0
- package/crates/tish_runtime/Cargo.toml +5 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +523 -42
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +3 -0
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1227 -182
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
.
|
|
650
|
-
|
|
651
|
-
|
|
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
|
}
|