@tishlang/tish 1.13.2 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +61 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -13,11 +13,57 @@ pub fn from_vec(v: Vec<Value>) -> Value {
13
13
  pub fn len(arr: &Value) -> Option<usize> {
14
14
  match arr {
15
15
  Value::Array(a) => Some(a.borrow().len()),
16
+ Value::NumberArray(a) => Some(a.borrow().len()),
16
17
  _ => None,
17
18
  }
18
19
  }
19
20
 
21
+ /// Normalise `NumberArray → Array` so callers that don't have a packed fast path
22
+ /// can use this deopt helper rather than changing every `if let Value::Array` branch.
23
+ /// Returns the original value unchanged for anything that isn't a `NumberArray`.
24
+ #[inline]
25
+ fn as_boxed_array(arr: &Value) -> std::borrow::Cow<'_, Value> {
26
+ match arr {
27
+ Value::NumberArray(na) => std::borrow::Cow::Owned(Value::materialize_number_array(na)),
28
+ other => std::borrow::Cow::Borrowed(other),
29
+ }
30
+ }
31
+
32
+ /// Packed-HOF fast-path gate: when `arr` is a packed [`Value::NumberArray`] and `callback` is
33
+ /// callable, snapshot the `Vec<f64>` so a higher-order method can fold/scan it WITHOUT first
34
+ /// materialising a boxed `Vec<Value>` (the `as_boxed_array` deopt that otherwise allocates a
35
+ /// 24-byte-per-element copy on every call). Snapshotting — rather than holding the `VmRef` borrow
36
+ /// across the callback — matches the boxed path's copy semantics (mutations to the array from inside
37
+ /// the callback aren't observed mid-scan) and can't deadlock if the callback re-enters the same
38
+ /// array. Returns `None` to fall through to the generic boxed path (regular arrays, or a non-callable
39
+ /// second argument).
40
+ #[inline]
41
+ fn packed_snapshot<'c>(
42
+ arr: &Value,
43
+ callback: &'c Value,
44
+ ) -> Option<(Vec<f64>, &'c tishlang_core::NativeFn)> {
45
+ match (arr, callback) {
46
+ (Value::NumberArray(na), Value::Function(cb)) => Some((na.borrow().clone(), cb)),
47
+ _ => None,
48
+ }
49
+ }
50
+
51
+ /// A `Vec<f64>` HOF result → packed [`Value::NumberArray`] so a packed input keeps producing packed
52
+ /// output (memory stays 3× denser and downstream packed fast paths keep firing). Empty results stay
53
+ /// a boxed `Value::Array`, matching the convention that empty arrays are general-purpose containers
54
+ /// whose element type can't be inferred. Only reached from a `NumberArray` input, which already
55
+ /// implies packed arrays are enabled, so no extra flag check is needed.
56
+ #[inline]
57
+ fn packed_or_empty(nums: Vec<f64>) -> Value {
58
+ if nums.is_empty() {
59
+ Value::Array(VmRef::new(Vec::new()))
60
+ } else {
61
+ Value::number_array(nums)
62
+ }
63
+ }
64
+
20
65
  pub fn push(arr: &Value, args: &[Value]) -> Value {
66
+ let arr = as_boxed_array(arr); let arr = &*arr;
21
67
  if let Value::Array(arr) = arr {
22
68
  let mut arr_mut = arr.borrow_mut();
23
69
  for v in args {
@@ -30,6 +76,7 @@ pub fn push(arr: &Value, args: &[Value]) -> Value {
30
76
  }
31
77
 
32
78
  pub fn pop(arr: &Value) -> Value {
79
+ let arr = as_boxed_array(arr); let arr = &*arr;
33
80
  if let Value::Array(arr) = arr {
34
81
  arr.borrow_mut().pop().unwrap_or(Value::Null)
35
82
  } else {
@@ -38,6 +85,7 @@ pub fn pop(arr: &Value) -> Value {
38
85
  }
39
86
 
40
87
  pub fn shift(arr: &Value) -> Value {
88
+ let arr = as_boxed_array(arr); let arr = &*arr;
41
89
  if let Value::Array(arr) = arr {
42
90
  let mut arr_mut = arr.borrow_mut();
43
91
  if arr_mut.is_empty() {
@@ -51,6 +99,7 @@ pub fn shift(arr: &Value) -> Value {
51
99
  }
52
100
 
53
101
  pub fn unshift(arr: &Value, args: &[Value]) -> Value {
102
+ let arr = as_boxed_array(arr); let arr = &*arr;
54
103
  if let Value::Array(arr) = arr {
55
104
  let mut arr_mut = arr.borrow_mut();
56
105
  for (i, v) in args.iter().enumerate() {
@@ -63,6 +112,7 @@ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
63
112
  }
64
113
 
65
114
  pub fn index_of(arr: &Value, search: &Value) -> Value {
115
+ let arr = as_boxed_array(arr); let arr = &*arr;
66
116
  if let Value::Array(arr) = arr {
67
117
  let arr_borrow = arr.borrow();
68
118
  for (i, v) in arr_borrow.iter().enumerate() {
@@ -75,6 +125,7 @@ pub fn index_of(arr: &Value, search: &Value) -> Value {
75
125
  }
76
126
 
77
127
  pub fn includes(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
128
+ let arr = as_boxed_array(arr); let arr = &*arr;
78
129
  if let Value::Array(arr) = arr {
79
130
  let arr_borrow = arr.borrow();
80
131
  let len = arr_borrow.len() as i64;
@@ -93,13 +144,22 @@ pub fn includes(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
93
144
  }
94
145
 
95
146
  pub fn join(arr: &Value, sep: &Value) -> Value {
147
+ let arr = as_boxed_array(arr); let arr = &*arr;
96
148
  if let Value::Array(arr) = arr {
97
149
  let separator = match sep {
98
150
  Value::String(s) => s.to_string(),
99
151
  _ => ",".to_string(),
100
152
  };
101
153
  let arr_borrow = arr.borrow();
102
- let parts: Vec<String> = arr_borrow.iter().map(|v| v.to_display_string()).collect();
154
+ // JS `Array.prototype.join`: null/undefined → "", everything else via JS ToString
155
+ // (nested arrays recurse to a comma-join, objects → "[object Object]").
156
+ let parts: Vec<String> = arr_borrow
157
+ .iter()
158
+ .map(|v| match v {
159
+ Value::Null => String::new(),
160
+ other => other.to_js_string(),
161
+ })
162
+ .collect();
103
163
  Value::String(parts.join(&separator).into())
104
164
  } else {
105
165
  Value::Null
@@ -107,6 +167,7 @@ pub fn join(arr: &Value, sep: &Value) -> Value {
107
167
  }
108
168
 
109
169
  pub fn reverse(arr: &Value) -> Value {
170
+ let arr = as_boxed_array(arr); let arr = &*arr;
110
171
  if let Value::Array(arr) = arr {
111
172
  arr.borrow_mut().reverse();
112
173
  Value::Array(arr.clone())
@@ -117,6 +178,7 @@ pub fn reverse(arr: &Value) -> Value {
117
178
 
118
179
  /// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
119
180
  pub fn shuffle(arr: &Value) -> Value {
181
+ let arr = as_boxed_array(arr); let arr = &*arr;
120
182
  if let Value::Array(arr) = arr {
121
183
  let mut v = arr.borrow().clone();
122
184
  use rand::seq::SliceRandom;
@@ -128,6 +190,7 @@ pub fn shuffle(arr: &Value) -> Value {
128
190
  }
129
191
 
130
192
  pub fn splice(arr: &Value, start: &Value, delete_count: Option<&Value>, items: &[Value]) -> Value {
193
+ let arr = as_boxed_array(arr); let arr = &*arr;
131
194
  if let Value::Array(arr) = arr {
132
195
  let mut arr_mut = arr.borrow_mut();
133
196
  let len = arr_mut.len() as i64;
@@ -146,7 +209,30 @@ pub fn splice(arr: &Value, start: &Value, delete_count: Option<&Value>, items: &
146
209
  }
147
210
  }
148
211
 
212
+ /// `Array.prototype.fill(value, start?, end?)` — overwrites elements in `[start, end)` with
213
+ /// `value`, mutating in place and returning the same array. start/end use JS index
214
+ /// normalization (negatives count from the end; defaults 0 and length). Issue #76.
215
+ pub fn fill(arr: &Value, value: &Value, start: &Value, end: &Value) -> Value {
216
+ let arr = as_boxed_array(arr);
217
+ let arr = &*arr;
218
+ if let Value::Array(arr) = arr {
219
+ let mut arr_mut = arr.borrow_mut();
220
+ let len = arr_mut.len() as i64;
221
+ let start_idx = normalize_index(start, len, 0);
222
+ let end_idx = normalize_index(end, len, len as usize);
223
+ let mut i = start_idx;
224
+ while i < end_idx && i < arr_mut.len() {
225
+ arr_mut[i] = value.clone();
226
+ i += 1;
227
+ }
228
+ Value::Array(arr.clone())
229
+ } else {
230
+ Value::Null
231
+ }
232
+ }
233
+
149
234
  pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
235
+ let arr = as_boxed_array(arr); let arr = &*arr;
150
236
  if let Value::Array(arr) = arr {
151
237
  let arr_borrow = arr.borrow();
152
238
  let len = arr_borrow.len() as i64;
@@ -164,6 +250,7 @@ pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
164
250
  }
165
251
 
166
252
  pub fn concat(arr: &Value, args: &[Value]) -> Value {
253
+ let arr = as_boxed_array(arr); let arr = &*arr;
167
254
  if let Value::Array(arr) = arr {
168
255
  let mut result = arr.borrow().clone();
169
256
  for v in args {
@@ -180,6 +267,7 @@ pub fn concat(arr: &Value, args: &[Value]) -> Value {
180
267
  }
181
268
 
182
269
  pub fn flat(arr: &Value, depth: &Value) -> Value {
270
+ let arr = as_boxed_array(arr); let arr = &*arr;
183
271
  fn flatten(arr: &[Value], depth: i32, result: &mut Vec<Value>) {
184
272
  for v in arr {
185
273
  if depth > 0 {
@@ -209,12 +297,35 @@ pub fn flat(arr: &Value, depth: &Value) -> Value {
209
297
  // These take NativeFn from tishlang_core::Value::Function
210
298
 
211
299
  pub fn map(arr: &Value, callback: &Value) -> Value {
300
+ // Packed fast path: scan the `Vec<f64>` snapshot directly. Speculatively build a packed
301
+ // `Vec<f64>` so a numeric map (the common `x => x * k` case) keeps its result packed with NO
302
+ // boxed `Vec<Value>` intermediate — downstream packed fast paths then keep firing. Deopt to a
303
+ // boxed array on the FIRST non-numeric callback result; every element's callback still runs
304
+ // exactly once, in index order (the deopt resumes at `i + 1`).
305
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
306
+ let mut nums: Vec<f64> = Vec::with_capacity(data.len());
307
+ for (i, &n) in data.iter().enumerate() {
308
+ match cb.call(&[Value::Number(n), Value::Number(i as f64)]) {
309
+ Value::Number(r) => nums.push(r),
310
+ other => {
311
+ let mut boxed: Vec<Value> = nums.into_iter().map(Value::Number).collect();
312
+ boxed.push(other);
313
+ for (j, &m) in data.iter().enumerate().skip(i + 1) {
314
+ boxed.push(cb.call(&[Value::Number(m), Value::Number(j as f64)]));
315
+ }
316
+ return Value::Array(VmRef::new(boxed));
317
+ }
318
+ }
319
+ }
320
+ return packed_or_empty(nums);
321
+ }
322
+ let arr = as_boxed_array(arr); let arr = &*arr;
212
323
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
213
324
  let arr_borrow = arr.borrow();
214
325
  let result: Vec<Value> = arr_borrow
215
326
  .iter()
216
327
  .enumerate()
217
- .map(|(i, v)| cb(&[v.clone(), Value::Number(i as f64)]))
328
+ .map(|(i, v)| cb.call(&[v.clone(), Value::Number(i as f64)]))
218
329
  .collect();
219
330
  Value::Array(VmRef::new(result))
220
331
  } else {
@@ -223,13 +334,25 @@ pub fn map(arr: &Value, callback: &Value) -> Value {
223
334
  }
224
335
 
225
336
  pub fn filter(arr: &Value, callback: &Value) -> Value {
337
+ // Packed fast path: `filter` keeps a SUBSET of the input f64s, so the result is always numeric —
338
+ // build the packed `Vec<f64>` directly, no boxed intermediate, and hand back a `NumberArray`.
339
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
340
+ let mut out: Vec<f64> = Vec::new();
341
+ for (i, &n) in data.iter().enumerate() {
342
+ if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
343
+ out.push(n);
344
+ }
345
+ }
346
+ return packed_or_empty(out);
347
+ }
348
+ let arr = as_boxed_array(arr); let arr = &*arr;
226
349
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
227
350
  let arr_borrow = arr.borrow();
228
351
  let result: Vec<Value> = arr_borrow
229
352
  .iter()
230
353
  .enumerate()
231
354
  .filter_map(|(i, v)| {
232
- let keep = cb(&[v.clone(), Value::Number(i as f64)]);
355
+ let keep = cb.call(&[v.clone(), Value::Number(i as f64)]);
233
356
  if keep.is_truthy() {
234
357
  Some(v.clone())
235
358
  } else {
@@ -244,6 +367,21 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
244
367
  }
245
368
 
246
369
  pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
370
+ // Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
371
+ // path (absent init → first element as the seed, scan from index 1).
372
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
373
+ let (start_idx, mut acc) = if matches!(initial, Value::Null) && !data.is_empty() {
374
+ (1usize, Value::Number(data[0]))
375
+ } else {
376
+ (0usize, initial.clone())
377
+ };
378
+ // `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
379
+ for (i, &x) in data.iter().enumerate().skip(start_idx) {
380
+ acc = cb.call(&[acc, Value::Number(x), Value::Number(i as f64)]);
381
+ }
382
+ return acc;
383
+ }
384
+ let arr = as_boxed_array(arr); let arr = &*arr;
247
385
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
248
386
  let arr_borrow = arr.borrow();
249
387
  let len = arr_borrow.len();
@@ -255,7 +393,7 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
255
393
  };
256
394
  for i in start_idx..len {
257
395
  let v = arr_borrow[i].clone();
258
- acc = cb(&[acc, v.clone(), Value::Number(i as f64)]);
396
+ acc = cb.call(&[acc, v.clone(), Value::Number(i as f64)]);
259
397
  }
260
398
  acc
261
399
  } else {
@@ -264,20 +402,36 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
264
402
  }
265
403
 
266
404
  pub fn for_each(arr: &Value, callback: &Value) -> Value {
405
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
406
+ for (i, &n) in data.iter().enumerate() {
407
+ cb.call(&[Value::Number(n), Value::Number(i as f64)]);
408
+ }
409
+ return Value::Null;
410
+ }
411
+ let arr = as_boxed_array(arr); let arr = &*arr;
267
412
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
268
413
  let arr_borrow = arr.borrow();
269
414
  for (i, v) in arr_borrow.iter().enumerate() {
270
- cb(&[v.clone(), Value::Number(i as f64)]);
415
+ cb.call(&[v.clone(), Value::Number(i as f64)]);
271
416
  }
272
417
  }
273
418
  Value::Null
274
419
  }
275
420
 
276
421
  pub fn find(arr: &Value, callback: &Value) -> Value {
422
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
423
+ for (i, &n) in data.iter().enumerate() {
424
+ if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
425
+ return Value::Number(n);
426
+ }
427
+ }
428
+ return Value::Null;
429
+ }
430
+ let arr = as_boxed_array(arr); let arr = &*arr;
277
431
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
278
432
  let arr_borrow = arr.borrow();
279
433
  for (i, v) in arr_borrow.iter().enumerate() {
280
- let result = cb(&[v.clone(), Value::Number(i as f64)]);
434
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
281
435
  if result.is_truthy() {
282
436
  return v.clone();
283
437
  }
@@ -287,10 +441,19 @@ pub fn find(arr: &Value, callback: &Value) -> Value {
287
441
  }
288
442
 
289
443
  pub fn find_index(arr: &Value, callback: &Value) -> Value {
444
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
445
+ for (i, &n) in data.iter().enumerate() {
446
+ if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
447
+ return Value::Number(i as f64);
448
+ }
449
+ }
450
+ return Value::Number(-1.0);
451
+ }
452
+ let arr = as_boxed_array(arr); let arr = &*arr;
290
453
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
291
454
  let arr_borrow = arr.borrow();
292
455
  for (i, v) in arr_borrow.iter().enumerate() {
293
- let result = cb(&[v.clone(), Value::Number(i as f64)]);
456
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
294
457
  if result.is_truthy() {
295
458
  return Value::Number(i as f64);
296
459
  }
@@ -300,10 +463,19 @@ pub fn find_index(arr: &Value, callback: &Value) -> Value {
300
463
  }
301
464
 
302
465
  pub fn some(arr: &Value, callback: &Value) -> Value {
466
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
467
+ for (i, &n) in data.iter().enumerate() {
468
+ if cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
469
+ return Value::Bool(true);
470
+ }
471
+ }
472
+ return Value::Bool(false);
473
+ }
474
+ let arr = as_boxed_array(arr); let arr = &*arr;
303
475
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
304
476
  let arr_borrow = arr.borrow();
305
477
  for (i, v) in arr_borrow.iter().enumerate() {
306
- let result = cb(&[v.clone(), Value::Number(i as f64)]);
478
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
307
479
  if result.is_truthy() {
308
480
  return Value::Bool(true);
309
481
  }
@@ -313,10 +485,19 @@ pub fn some(arr: &Value, callback: &Value) -> Value {
313
485
  }
314
486
 
315
487
  pub fn every(arr: &Value, callback: &Value) -> Value {
488
+ if let Some((data, cb)) = packed_snapshot(arr, callback) {
489
+ for (i, &n) in data.iter().enumerate() {
490
+ if !cb.call(&[Value::Number(n), Value::Number(i as f64)]).is_truthy() {
491
+ return Value::Bool(false);
492
+ }
493
+ }
494
+ return Value::Bool(true);
495
+ }
496
+ let arr = as_boxed_array(arr); let arr = &*arr;
316
497
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
317
498
  let arr_borrow = arr.borrow();
318
499
  for (i, v) in arr_borrow.iter().enumerate() {
319
- let result = cb(&[v.clone(), Value::Number(i as f64)]);
500
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
320
501
  if !result.is_truthy() {
321
502
  return Value::Bool(false);
322
503
  }
@@ -328,11 +509,12 @@ pub fn every(arr: &Value, callback: &Value) -> Value {
328
509
  }
329
510
 
330
511
  pub fn flat_map(arr: &Value, callback: &Value) -> Value {
512
+ let arr = as_boxed_array(arr); let arr = &*arr;
331
513
  if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
332
514
  let arr_borrow = arr.borrow();
333
515
  let mut result: Vec<Value> = Vec::new();
334
516
  for (i, v) in arr_borrow.iter().enumerate() {
335
- let mapped = cb(&[v.clone(), Value::Number(i as f64)]);
517
+ let mapped = cb.call(&[v.clone(), Value::Number(i as f64)]);
336
518
  if let Value::Array(inner) = mapped {
337
519
  result.extend(inner.borrow().iter().cloned());
338
520
  } else {
@@ -374,7 +556,7 @@ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
374
556
  indices.sort_by(|&a, &b| {
375
557
  args_buf[0] = elements[a].clone();
376
558
  args_buf[1] = elements[b].clone();
377
- match cmp_fn(&args_buf) {
559
+ match cmp_fn.call(&args_buf) {
378
560
  Value::Number(n) if n < 0.0 => std::cmp::Ordering::Less,
379
561
  Value::Number(n) if n > 0.0 => std::cmp::Ordering::Greater,
380
562
  _ => std::cmp::Ordering::Equal,
@@ -406,11 +588,59 @@ fn num_cmp(a: &Value, b: &Value, asc: bool) -> std::cmp::Ordering {
406
588
  }
407
589
 
408
590
  pub fn sort_numeric_asc(arr: &Value) -> Value {
409
- sort_by_impl(arr, |a, b| num_cmp(a, b, true))
591
+ sort_numeric_impl(arr, true)
410
592
  }
411
593
 
412
594
  pub fn sort_numeric_desc(arr: &Value) -> Value {
413
- sort_by_impl(arr, |a, b| num_cmp(a, b, false))
595
+ sort_numeric_impl(arr, false)
596
+ }
597
+
598
+ /// Numeric sort. When every element is a number, extract to unboxed `f64`,
599
+ /// `sort_unstable` (faster than the stable comparator sort, and stability is
600
+ /// irrelevant for equal numbers), then write back — no per-comparison `Value`
601
+ /// match. Mixed arrays fall back to the comparator path.
602
+ fn sort_numeric_impl(arr: &Value, asc: bool) -> Value {
603
+ // NumberArray fast path: sort the Vec<f64> directly — no unbox pass, no rebox.
604
+ if let Value::NumberArray(a) = arr {
605
+ let mut g = a.borrow_mut();
606
+ if asc {
607
+ g.sort_unstable_by(|x, y| x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal));
608
+ } else {
609
+ g.sort_unstable_by(|x, y| y.partial_cmp(x).unwrap_or(std::cmp::Ordering::Equal));
610
+ }
611
+ return Value::NumberArray(a.clone());
612
+ }
613
+ if let Value::Array(a) = arr {
614
+ {
615
+ let mut g = a.borrow_mut();
616
+ if g.iter().all(|v| matches!(v, Value::Number(_))) {
617
+ let mut nums: Vec<f64> = g
618
+ .iter()
619
+ .map(|v| match v {
620
+ Value::Number(n) => *n,
621
+ _ => f64::NAN,
622
+ })
623
+ .collect();
624
+ if asc {
625
+ nums.sort_unstable_by(|x, y| {
626
+ x.partial_cmp(y).unwrap_or(std::cmp::Ordering::Equal)
627
+ });
628
+ } else {
629
+ nums.sort_unstable_by(|x, y| {
630
+ y.partial_cmp(x).unwrap_or(std::cmp::Ordering::Equal)
631
+ });
632
+ }
633
+ for (slot, n) in g.iter_mut().zip(nums) {
634
+ *slot = Value::Number(n);
635
+ }
636
+ return Value::Array(a.clone());
637
+ }
638
+ g.sort_by(|x, y| num_cmp(x, y, asc));
639
+ }
640
+ Value::Array(a.clone())
641
+ } else {
642
+ Value::Null
643
+ }
414
644
  }
415
645
 
416
646
  /// Sort array of objects by numeric property: arr.sort((a,b)=>a.prop-b.prop)
@@ -436,6 +666,138 @@ fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
436
666
  .get(prop.as_ref())
437
667
  .map(|v| v.as_number().unwrap_or(f64::NAN))
438
668
  .unwrap_or(f64::NAN),
669
+ // `.length` is a *computed* property (not a stored map entry) for strings and arrays.
670
+ // The fused `(a,b)=>a.length-b.length` sort path must compute it the same way
671
+ // `get_member` does, otherwise it returns NaN, every comparison collapses to Equal,
672
+ // and the array is left unsorted. Mirror get_member's length semantics here.
673
+ Value::String(s) if prop.as_ref() == "length" => s.chars().count() as f64,
674
+ Value::Array(a) if prop.as_ref() == "length" => a.borrow().len() as f64,
439
675
  _ => f64::NAN,
440
676
  }
441
677
  }
678
+
679
+ #[cfg(test)]
680
+ mod packed_hof_tests {
681
+ //! The packed (`NumberArray`) HOF fast paths must be observably IDENTICAL to the boxed path —
682
+ //! same element + index callback args, same result shape — since cross-backend parity depends
683
+ //! on it. These run with packing semantics directly (the helpers don't read the env flag; a
684
+ //! `NumberArray` value is enough to take the fast path).
685
+ use super::*;
686
+ use tishlang_core::Value;
687
+
688
+ fn na(xs: &[f64]) -> Value {
689
+ Value::NumberArray(VmRef::new(xs.to_vec()))
690
+ }
691
+ fn nums(v: &Value) -> Vec<f64> {
692
+ match v {
693
+ Value::Array(a) => a.borrow().iter().map(|e| e.as_number().unwrap_or(f64::NAN)).collect(),
694
+ Value::NumberArray(a) => a.borrow().clone(),
695
+ _ => vec![],
696
+ }
697
+ }
698
+ fn cb_num(f: fn(f64, f64) -> f64) -> Value {
699
+ Value::native(move |a: &[Value]| {
700
+ Value::Number(f(a[0].as_number().unwrap_or(0.0), a[1].as_number().unwrap_or(0.0)))
701
+ })
702
+ }
703
+ fn cb_pred(f: fn(f64, f64) -> bool) -> Value {
704
+ Value::native(move |a: &[Value]| {
705
+ Value::Bool(f(a[0].as_number().unwrap_or(0.0), a[1].as_number().unwrap_or(0.0)))
706
+ })
707
+ }
708
+
709
+ #[test]
710
+ fn reduce_packed() {
711
+ let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
712
+ let add = cb_num(|acc, x| acc + x);
713
+ // With init.
714
+ assert_eq!(reduce(&n, &add, &Value::Number(0.0)).as_number(), Some(14.0));
715
+ // No init → first element seeds, scan from index 1 (same total here).
716
+ assert_eq!(reduce(&n, &add, &Value::Null).as_number(), Some(14.0));
717
+ // Index arg: callback (acc, _elem, index) — sum the indices 0..5 = 10.
718
+ let sum_idx = Value::native(|a: &[Value]| {
719
+ Value::Number(a[0].as_number().unwrap() + a[2].as_number().unwrap())
720
+ });
721
+ assert_eq!(reduce(&n, &sum_idx, &Value::Number(0.0)).as_number(), Some(10.0));
722
+ }
723
+
724
+ #[test]
725
+ fn map_filter_stay_packed() {
726
+ let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
727
+ // Numeric map → packed NumberArray result (chains stay packed), with correct values.
728
+ let m = map(&n, &cb_num(|x, _i| x * 2.0));
729
+ assert!(matches!(m, Value::NumberArray(_)), "numeric map should stay packed");
730
+ assert_eq!(nums(&m), vec![6.0, 2.0, 8.0, 2.0, 10.0]);
731
+ // filter keeps a subset of the input f64s → always packed.
732
+ let f = filter(&n, &cb_pred(|x, _i| x > 2.0));
733
+ assert!(matches!(f, Value::NumberArray(_)), "filter should stay packed");
734
+ assert_eq!(nums(&f), vec![3.0, 4.0, 5.0]);
735
+ }
736
+
737
+ #[test]
738
+ fn map_deopts_to_boxed_on_non_numeric() {
739
+ let n = na(&[1.0, 2.0, 3.0]);
740
+ // Callback returns a string for the middle element → deopt to a boxed array, preserving order
741
+ // (callback runs once per element).
742
+ let cb = Value::native(|a: &[Value]| {
743
+ let x = a[0].as_number().unwrap();
744
+ if x == 2.0 { Value::String("two".into()) } else { Value::Number(x * 10.0) }
745
+ });
746
+ match &map(&n, &cb) {
747
+ Value::Array(a) => {
748
+ let b = a.borrow();
749
+ assert_eq!(b.len(), 3);
750
+ assert_eq!(b[0].as_number(), Some(10.0));
751
+ assert!(matches!(&b[1], Value::String(s) if s.as_str() == "two"));
752
+ assert_eq!(b[2].as_number(), Some(30.0));
753
+ }
754
+ _ => panic!("mixed-result map must be a boxed array"),
755
+ }
756
+ }
757
+
758
+ #[test]
759
+ fn map_filter_empty_stays_boxed() {
760
+ let n = na(&[1.0, 2.0, 3.0]);
761
+ // All rejected → empty boxed array (empty arrays stay general-purpose containers).
762
+ assert!(matches!(filter(&n, &cb_pred(|_x, _i| false)), Value::Array(_)));
763
+ // Empty input → empty boxed array.
764
+ assert!(matches!(map(&na(&[]), &cb_num(|x, _i| x)), Value::Array(_)));
765
+ }
766
+
767
+ #[test]
768
+ fn scan_packed() {
769
+ let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
770
+ assert!(matches!(some(&n, &cb_pred(|x, _i| x > 4.0)), Value::Bool(true)));
771
+ assert!(matches!(some(&n, &cb_pred(|x, _i| x > 9.0)), Value::Bool(false)));
772
+ assert!(matches!(every(&n, &cb_pred(|x, _i| x > 0.0)), Value::Bool(true)));
773
+ assert!(matches!(every(&n, &cb_pred(|x, _i| x > 2.0)), Value::Bool(false)));
774
+ // first element > 3 is 4.0 at index 2.
775
+ assert_eq!(find(&n, &cb_pred(|x, _i| x > 3.0)).as_number(), Some(4.0));
776
+ assert_eq!(find_index(&n, &cb_pred(|x, _i| x > 3.0)).as_number(), Some(2.0));
777
+ assert_eq!(find_index(&n, &cb_pred(|x, _i| x > 99.0)).as_number(), Some(-1.0));
778
+ }
779
+
780
+ #[test]
781
+ fn for_each_packed_passes_element_and_index() {
782
+ use std::sync::{Arc, Mutex};
783
+ let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
784
+ let acc = Arc::new(Mutex::new(0.0f64));
785
+ let a2 = acc.clone();
786
+ let collect = Value::native(move |a: &[Value]| {
787
+ *a2.lock().unwrap() += a[0].as_number().unwrap() + a[1].as_number().unwrap();
788
+ Value::Null
789
+ });
790
+ assert!(matches!(for_each(&n, &collect), Value::Null));
791
+ // sum(elems)=14 + sum(idx 0..5)=10.
792
+ assert_eq!(*acc.lock().unwrap(), 24.0);
793
+ }
794
+
795
+ #[test]
796
+ fn non_function_callback_falls_through() {
797
+ // A NumberArray with a non-callable 2nd arg must not take the fast path; mirrors the boxed
798
+ // path's `Value::Null` (map/filter) without panicking.
799
+ let n = na(&[1.0, 2.0]);
800
+ assert!(matches!(map(&n, &Value::Number(1.0)), Value::Null));
801
+ assert!(matches!(filter(&n, &Value::Null), Value::Null));
802
+ }
803
+ }