@tishlang/tish-format 1.0.12 → 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 (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,803 @@
1
+ //! Array builtin methods.
2
+
3
+ use crate::helpers::normalize_index;
4
+ use tishlang_core::Value;
5
+ use tishlang_core::VmRef;
6
+
7
+ /// Create a new array Value from a Vec of Values.
8
+ pub fn from_vec(v: Vec<Value>) -> Value {
9
+ Value::Array(VmRef::new(v))
10
+ }
11
+
12
+ /// Get the length of an array.
13
+ pub fn len(arr: &Value) -> Option<usize> {
14
+ match arr {
15
+ Value::Array(a) => Some(a.borrow().len()),
16
+ Value::NumberArray(a) => Some(a.borrow().len()),
17
+ _ => None,
18
+ }
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
+
65
+ pub fn push(arr: &Value, args: &[Value]) -> Value {
66
+ let arr = as_boxed_array(arr); let arr = &*arr;
67
+ if let Value::Array(arr) = arr {
68
+ let mut arr_mut = arr.borrow_mut();
69
+ for v in args {
70
+ arr_mut.push(v.clone());
71
+ }
72
+ Value::Number(arr_mut.len() as f64)
73
+ } else {
74
+ Value::Null
75
+ }
76
+ }
77
+
78
+ pub fn pop(arr: &Value) -> Value {
79
+ let arr = as_boxed_array(arr); let arr = &*arr;
80
+ if let Value::Array(arr) = arr {
81
+ arr.borrow_mut().pop().unwrap_or(Value::Null)
82
+ } else {
83
+ Value::Null
84
+ }
85
+ }
86
+
87
+ pub fn shift(arr: &Value) -> Value {
88
+ let arr = as_boxed_array(arr); let arr = &*arr;
89
+ if let Value::Array(arr) = arr {
90
+ let mut arr_mut = arr.borrow_mut();
91
+ if arr_mut.is_empty() {
92
+ Value::Null
93
+ } else {
94
+ arr_mut.remove(0)
95
+ }
96
+ } else {
97
+ Value::Null
98
+ }
99
+ }
100
+
101
+ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
102
+ let arr = as_boxed_array(arr); let arr = &*arr;
103
+ if let Value::Array(arr) = arr {
104
+ let mut arr_mut = arr.borrow_mut();
105
+ for (i, v) in args.iter().enumerate() {
106
+ arr_mut.insert(i, v.clone());
107
+ }
108
+ Value::Number(arr_mut.len() as f64)
109
+ } else {
110
+ Value::Null
111
+ }
112
+ }
113
+
114
+ pub fn index_of(arr: &Value, search: &Value) -> Value {
115
+ let arr = as_boxed_array(arr); let arr = &*arr;
116
+ if let Value::Array(arr) = arr {
117
+ let arr_borrow = arr.borrow();
118
+ for (i, v) in arr_borrow.iter().enumerate() {
119
+ if v.strict_eq(search) {
120
+ return Value::Number(i as f64);
121
+ }
122
+ }
123
+ }
124
+ Value::Number(-1.0)
125
+ }
126
+
127
+ pub fn includes(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
128
+ let arr = as_boxed_array(arr); let arr = &*arr;
129
+ if let Value::Array(arr) = arr {
130
+ let arr_borrow = arr.borrow();
131
+ let len = arr_borrow.len() as i64;
132
+ let start = match from {
133
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
134
+ Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
135
+ _ => 0,
136
+ };
137
+ for v in arr_borrow.iter().skip(start) {
138
+ if v.strict_eq(search) {
139
+ return Value::Bool(true);
140
+ }
141
+ }
142
+ }
143
+ Value::Bool(false)
144
+ }
145
+
146
+ pub fn join(arr: &Value, sep: &Value) -> Value {
147
+ let arr = as_boxed_array(arr); let arr = &*arr;
148
+ if let Value::Array(arr) = arr {
149
+ let separator = match sep {
150
+ Value::String(s) => s.to_string(),
151
+ _ => ",".to_string(),
152
+ };
153
+ let arr_borrow = arr.borrow();
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();
163
+ Value::String(parts.join(&separator).into())
164
+ } else {
165
+ Value::Null
166
+ }
167
+ }
168
+
169
+ pub fn reverse(arr: &Value) -> Value {
170
+ let arr = as_boxed_array(arr); let arr = &*arr;
171
+ if let Value::Array(arr) = arr {
172
+ arr.borrow_mut().reverse();
173
+ Value::Array(arr.clone())
174
+ } else {
175
+ Value::Null
176
+ }
177
+ }
178
+
179
+ /// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
180
+ pub fn shuffle(arr: &Value) -> Value {
181
+ let arr = as_boxed_array(arr); let arr = &*arr;
182
+ if let Value::Array(arr) = arr {
183
+ let mut v = arr.borrow().clone();
184
+ use rand::seq::SliceRandom;
185
+ v.shuffle(&mut rand::rng());
186
+ Value::Array(VmRef::new(v))
187
+ } else {
188
+ Value::Null
189
+ }
190
+ }
191
+
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;
194
+ if let Value::Array(arr) = arr {
195
+ let mut arr_mut = arr.borrow_mut();
196
+ let len = arr_mut.len() as i64;
197
+ let start_idx = normalize_index(start, len, 0);
198
+ let del_count = match delete_count {
199
+ Some(Value::Number(n)) => (*n as i64).max(0) as usize,
200
+ _ => (len as usize).saturating_sub(start_idx),
201
+ };
202
+ let actual_delete = del_count.min(arr_mut.len().saturating_sub(start_idx));
203
+ let removed: Vec<Value> = arr_mut
204
+ .splice(start_idx..start_idx + actual_delete, items.iter().cloned())
205
+ .collect();
206
+ Value::Array(VmRef::new(removed))
207
+ } else {
208
+ Value::Null
209
+ }
210
+ }
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
+
234
+ pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
235
+ let arr = as_boxed_array(arr); let arr = &*arr;
236
+ if let Value::Array(arr) = arr {
237
+ let arr_borrow = arr.borrow();
238
+ let len = arr_borrow.len() as i64;
239
+ let start_idx = normalize_index(start, len, 0);
240
+ let end_idx = normalize_index(end, len, len as usize);
241
+ let sliced = if start_idx < end_idx {
242
+ arr_borrow[start_idx..end_idx].to_vec()
243
+ } else {
244
+ vec![]
245
+ };
246
+ Value::Array(VmRef::new(sliced))
247
+ } else {
248
+ Value::Null
249
+ }
250
+ }
251
+
252
+ pub fn concat(arr: &Value, args: &[Value]) -> Value {
253
+ let arr = as_boxed_array(arr); let arr = &*arr;
254
+ if let Value::Array(arr) = arr {
255
+ let mut result = arr.borrow().clone();
256
+ for v in args {
257
+ if let Value::Array(other) = v {
258
+ result.extend(other.borrow().iter().cloned());
259
+ } else {
260
+ result.push(v.clone());
261
+ }
262
+ }
263
+ Value::Array(VmRef::new(result))
264
+ } else {
265
+ Value::Null
266
+ }
267
+ }
268
+
269
+ pub fn flat(arr: &Value, depth: &Value) -> Value {
270
+ let arr = as_boxed_array(arr); let arr = &*arr;
271
+ fn flatten(arr: &[Value], depth: i32, result: &mut Vec<Value>) {
272
+ for v in arr {
273
+ if depth > 0 {
274
+ if let Value::Array(inner) = v {
275
+ flatten(&inner.borrow(), depth - 1, result);
276
+ continue;
277
+ }
278
+ }
279
+ result.push(v.clone());
280
+ }
281
+ }
282
+
283
+ if let Value::Array(arr) = arr {
284
+ let d = match depth {
285
+ Value::Number(n) => *n as i32,
286
+ _ => 1,
287
+ };
288
+ let mut result = Vec::new();
289
+ flatten(&arr.borrow(), d, &mut result);
290
+ Value::Array(VmRef::new(result))
291
+ } else {
292
+ Value::Null
293
+ }
294
+ }
295
+
296
+ // Higher-order array methods require a callback function.
297
+ // These take NativeFn from tishlang_core::Value::Function
298
+
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;
323
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
324
+ let arr_borrow = arr.borrow();
325
+ let result: Vec<Value> = arr_borrow
326
+ .iter()
327
+ .enumerate()
328
+ .map(|(i, v)| cb.call(&[v.clone(), Value::Number(i as f64)]))
329
+ .collect();
330
+ Value::Array(VmRef::new(result))
331
+ } else {
332
+ Value::Null
333
+ }
334
+ }
335
+
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;
349
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
350
+ let arr_borrow = arr.borrow();
351
+ let result: Vec<Value> = arr_borrow
352
+ .iter()
353
+ .enumerate()
354
+ .filter_map(|(i, v)| {
355
+ let keep = cb.call(&[v.clone(), Value::Number(i as f64)]);
356
+ if keep.is_truthy() {
357
+ Some(v.clone())
358
+ } else {
359
+ None
360
+ }
361
+ })
362
+ .collect();
363
+ Value::Array(VmRef::new(result))
364
+ } else {
365
+ Value::Null
366
+ }
367
+ }
368
+
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;
385
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
386
+ let arr_borrow = arr.borrow();
387
+ let len = arr_borrow.len();
388
+ let (start_idx, mut acc) = if matches!(initial, Value::Null) && !arr_borrow.is_empty() {
389
+ // No initial value: use first element as acc, start from index 1
390
+ (1, arr_borrow[0].clone())
391
+ } else {
392
+ (0, initial.clone())
393
+ };
394
+ for i in start_idx..len {
395
+ let v = arr_borrow[i].clone();
396
+ acc = cb.call(&[acc, v.clone(), Value::Number(i as f64)]);
397
+ }
398
+ acc
399
+ } else {
400
+ Value::Null
401
+ }
402
+ }
403
+
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;
412
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
413
+ let arr_borrow = arr.borrow();
414
+ for (i, v) in arr_borrow.iter().enumerate() {
415
+ cb.call(&[v.clone(), Value::Number(i as f64)]);
416
+ }
417
+ }
418
+ Value::Null
419
+ }
420
+
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;
431
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
432
+ let arr_borrow = arr.borrow();
433
+ for (i, v) in arr_borrow.iter().enumerate() {
434
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
435
+ if result.is_truthy() {
436
+ return v.clone();
437
+ }
438
+ }
439
+ }
440
+ Value::Null
441
+ }
442
+
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;
453
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
454
+ let arr_borrow = arr.borrow();
455
+ for (i, v) in arr_borrow.iter().enumerate() {
456
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
457
+ if result.is_truthy() {
458
+ return Value::Number(i as f64);
459
+ }
460
+ }
461
+ }
462
+ Value::Number(-1.0)
463
+ }
464
+
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;
475
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
476
+ let arr_borrow = arr.borrow();
477
+ for (i, v) in arr_borrow.iter().enumerate() {
478
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
479
+ if result.is_truthy() {
480
+ return Value::Bool(true);
481
+ }
482
+ }
483
+ }
484
+ Value::Bool(false)
485
+ }
486
+
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;
497
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
498
+ let arr_borrow = arr.borrow();
499
+ for (i, v) in arr_borrow.iter().enumerate() {
500
+ let result = cb.call(&[v.clone(), Value::Number(i as f64)]);
501
+ if !result.is_truthy() {
502
+ return Value::Bool(false);
503
+ }
504
+ }
505
+ Value::Bool(true)
506
+ } else {
507
+ Value::Bool(false)
508
+ }
509
+ }
510
+
511
+ pub fn flat_map(arr: &Value, callback: &Value) -> Value {
512
+ let arr = as_boxed_array(arr); let arr = &*arr;
513
+ if let (Value::Array(arr), Value::Function(cb)) = (arr, callback) {
514
+ let arr_borrow = arr.borrow();
515
+ let mut result: Vec<Value> = Vec::new();
516
+ for (i, v) in arr_borrow.iter().enumerate() {
517
+ let mapped = cb.call(&[v.clone(), Value::Number(i as f64)]);
518
+ if let Value::Array(inner) = mapped {
519
+ result.extend(inner.borrow().iter().cloned());
520
+ } else {
521
+ result.push(mapped);
522
+ }
523
+ }
524
+ Value::Array(VmRef::new(result))
525
+ } else {
526
+ Value::Null
527
+ }
528
+ }
529
+
530
+ fn sort_by_impl<F>(arr: &Value, cmp: F) -> Value
531
+ where
532
+ F: FnMut(&Value, &Value) -> std::cmp::Ordering,
533
+ {
534
+ if let Value::Array(arr) = arr {
535
+ arr.borrow_mut().sort_by(cmp);
536
+ Value::Array(arr.clone())
537
+ } else {
538
+ Value::Null
539
+ }
540
+ }
541
+
542
+ pub fn sort_default(arr: &Value) -> Value {
543
+ sort_by_impl(arr, |a, b| {
544
+ a.to_display_string().cmp(&b.to_display_string())
545
+ })
546
+ }
547
+
548
+ pub fn sort_with_comparator(arr: &Value, comparator: &Value) -> Value {
549
+ if let (Value::Array(arr), Value::Function(cmp_fn)) = (arr, comparator) {
550
+ let mut arr_mut = arr.borrow_mut();
551
+ let len = arr_mut.len();
552
+ let mut indices: Vec<usize> = (0..len).collect();
553
+ let mut elements: Vec<Value> = std::mem::take(&mut *arr_mut);
554
+ let mut args_buf: [Value; 2] = [Value::Null, Value::Null];
555
+
556
+ indices.sort_by(|&a, &b| {
557
+ args_buf[0] = elements[a].clone();
558
+ args_buf[1] = elements[b].clone();
559
+ match cmp_fn.call(&args_buf) {
560
+ Value::Number(n) if n < 0.0 => std::cmp::Ordering::Less,
561
+ Value::Number(n) if n > 0.0 => std::cmp::Ordering::Greater,
562
+ _ => std::cmp::Ordering::Equal,
563
+ }
564
+ });
565
+
566
+ *arr_mut = indices
567
+ .into_iter()
568
+ .map(|i| std::mem::replace(&mut elements[i], Value::Null))
569
+ .collect();
570
+ drop(arr_mut);
571
+ Value::Array(arr.clone())
572
+ } else {
573
+ Value::Null
574
+ }
575
+ }
576
+
577
+ fn num_cmp(a: &Value, b: &Value, asc: bool) -> std::cmp::Ordering {
578
+ let (na, nb) = match (a, b) {
579
+ (Value::Number(a), Value::Number(b)) => (*a, *b),
580
+ _ => (f64::NAN, f64::NAN),
581
+ };
582
+ let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
583
+ if asc {
584
+ cmp
585
+ } else {
586
+ cmp.reverse()
587
+ }
588
+ }
589
+
590
+ pub fn sort_numeric_asc(arr: &Value) -> Value {
591
+ sort_numeric_impl(arr, true)
592
+ }
593
+
594
+ pub fn sort_numeric_desc(arr: &Value) -> Value {
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
+ }
644
+ }
645
+
646
+ /// Sort array of objects by numeric property: arr.sort((a,b)=>a.prop-b.prop)
647
+ pub fn sort_by_property_numeric(arr: &Value, prop: &str, asc: bool) -> Value {
648
+ let prop_arc = std::sync::Arc::from(prop);
649
+ sort_by_impl(arr, move |a, b| {
650
+ let na = get_prop_number(a, &prop_arc);
651
+ let nb = get_prop_number(b, &prop_arc);
652
+ let cmp = na.partial_cmp(&nb).unwrap_or(std::cmp::Ordering::Equal);
653
+ if asc {
654
+ cmp
655
+ } else {
656
+ cmp.reverse()
657
+ }
658
+ })
659
+ }
660
+
661
+ fn get_prop_number(v: &Value, prop: &std::sync::Arc<str>) -> f64 {
662
+ match v {
663
+ Value::Object(o) => o
664
+ .borrow()
665
+ .strings
666
+ .get(prop.as_ref())
667
+ .map(|v| v.as_number().unwrap_or(f64::NAN))
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,
675
+ _ => f64::NAN,
676
+ }
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
+ }