@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.
- package/Cargo.toml +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +1 -0
- package/crates/tish/Cargo.toml +11 -3
- package/crates/tish/build.rs +21 -0
- package/crates/tish/src/cli_help.rs +15 -4
- package/crates/tish/src/main.rs +93 -21
- package/crates/tish/src/repl_completion.rs +0 -1
- package/crates/tish/tests/error_source_location.rs +36 -0
- package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
- package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
- package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
- package/crates/tish/tests/integration_test.rs +402 -91
- package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
- package/crates/tish/tests/tty_capability.rs +43 -0
- package/crates/tish_ast/src/ast.rs +37 -8
- package/crates/tish_builtins/Cargo.toml +2 -0
- package/crates/tish_builtins/src/array.rs +375 -13
- package/crates/tish_builtins/src/collections.rs +481 -0
- package/crates/tish_builtins/src/construct.rs +59 -19
- package/crates/tish_builtins/src/date.rs +538 -0
- package/crates/tish_builtins/src/globals.rs +86 -6
- package/crates/tish_builtins/src/iterator.rs +129 -0
- package/crates/tish_builtins/src/lib.rs +5 -0
- package/crates/tish_builtins/src/number.rs +96 -0
- package/crates/tish_builtins/src/object.rs +2 -2
- package/crates/tish_builtins/src/string.rs +19 -20
- package/crates/tish_builtins/src/symbol.rs +1 -1
- package/crates/tish_builtins/src/typedarrays.rs +298 -0
- package/crates/tish_bytecode/src/chunk.rs +69 -1
- package/crates/tish_bytecode/src/compiler.rs +933 -89
- package/crates/tish_bytecode/src/encoding.rs +2 -0
- package/crates/tish_bytecode/src/lib.rs +2 -1
- package/crates/tish_bytecode/src/opcode.rs +47 -4
- package/crates/tish_bytecode/src/serialize.rs +31 -1
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/check.rs +774 -0
- package/crates/tish_compile/src/codegen.rs +2334 -349
- package/crates/tish_compile/src/infer.rs +1395 -6
- package/crates/tish_compile/src/lib.rs +50 -8
- package/crates/tish_compile/src/resolve.rs +584 -21
- package/crates/tish_compile/src/types.rs +106 -2
- package/crates/tish_compile_js/src/codegen.rs +67 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
- package/crates/tish_core/Cargo.toml +7 -1
- package/crates/tish_core/src/console_style.rs +11 -1
- package/crates/tish_core/src/json.rs +81 -38
- package/crates/tish_core/src/lib.rs +3 -0
- package/crates/tish_core/src/shape.rs +85 -0
- package/crates/tish_core/src/value.rs +679 -25
- package/crates/tish_core/src/vmref.rs +13 -8
- package/crates/tish_cranelift/src/link.rs +17 -4
- package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
- package/crates/tish_eval/Cargo.toml +6 -0
- package/crates/tish_eval/src/eval.rs +665 -117
- package/crates/tish_eval/src/http.rs +4 -1
- package/crates/tish_eval/src/natives.rs +165 -13
- package/crates/tish_eval/src/value.rs +31 -13
- package/crates/tish_eval/src/value_convert.rs +10 -4
- package/crates/tish_ffi/Cargo.toml +26 -0
- package/crates/tish_ffi/src/lib.rs +518 -0
- package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
- package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
- package/crates/tish_ffi/tests/loader.rs +65 -0
- package/crates/tish_fmt/src/lib.rs +61 -5
- package/crates/tish_lexer/src/lib.rs +397 -9
- package/crates/tish_lexer/src/token.rs +7 -0
- package/crates/tish_lint/src/lib.rs +2 -10
- package/crates/tish_lsp/src/import_goto.rs +2 -0
- package/crates/tish_lsp/src/main.rs +439 -26
- package/crates/tish_native/src/build.rs +55 -1
- package/crates/tish_opt/src/lib.rs +126 -23
- package/crates/tish_parser/src/lib.rs +55 -1
- package/crates/tish_parser/src/parser.rs +456 -34
- package/crates/tish_pg/src/lib.rs +3 -3
- package/crates/tish_resolve/src/lib.rs +99 -59
- package/crates/tish_runtime/Cargo.toml +4 -0
- package/crates/tish_runtime/src/http.rs +66 -17
- package/crates/tish_runtime/src/http_fetch.rs +29 -8
- package/crates/tish_runtime/src/http_hyper.rs +25 -2
- package/crates/tish_runtime/src/lib.rs +299 -44
- package/crates/tish_runtime/src/promise.rs +328 -18
- package/crates/tish_runtime/src/timers.rs +13 -7
- package/crates/tish_runtime/src/tty.rs +226 -0
- package/crates/tish_runtime/src/ws.rs +35 -18
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
- package/crates/tish_ui/src/jsx.rs +10 -0
- package/crates/tish_ui/src/runtime/hooks.rs +19 -15
- package/crates/tish_ui/src/runtime/mod.rs +15 -12
- package/crates/tish_vm/Cargo.toml +14 -1
- package/crates/tish_vm/src/jit.rs +1050 -0
- package/crates/tish_vm/src/lib.rs +2 -0
- package/crates/tish_vm/src/vm.rs +1546 -202
- package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
- package/crates/tish_wasm/src/lib.rs +6 -2
- package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
- package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
- package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
- package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
591
|
+
sort_numeric_impl(arr, true)
|
|
410
592
|
}
|
|
411
593
|
|
|
412
594
|
pub fn sort_numeric_desc(arr: &Value) -> Value {
|
|
413
|
-
|
|
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
|
+
}
|