@tishlang/tish-lsp 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.
- package/Cargo.toml +3 -0
- package/bin/tish-lsp +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -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 +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- 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 +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- 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 +94 -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/Cargo.toml +1 -1
- 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_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -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_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.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
|
-
|
|
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(©, cmp),
|
|
242
|
+
_ => sort_default(©),
|
|
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(©, 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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
|
|
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
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
(0usize,
|
|
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
|
-
|
|
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) =
|
|
393
|
-
|
|
394
|
-
(1, arr_borrow[0].clone())
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
632
|
-
|
|
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
|
-
|
|
648
|
-
|
|
649
|
-
.
|
|
650
|
-
|
|
651
|
-
|
|
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,
|
|
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
|
}
|