@tishlang/tish-format 2.16.13 → 2.36.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/tish-format +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +12 -2
- package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
- package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
- package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
- package/crates/tish/tests/fixtures/process_capture.tish +16 -0
- package/crates/tish/tests/fs_parity.rs +103 -0
- package/crates/tish/tests/process_capture.rs +40 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +77 -3
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +195 -16
- package/crates/tish_builtins/src/collections.rs +37 -2
- package/crates/tish_builtins/src/globals.rs +215 -25
- package/crates/tish_builtins/src/math.rs +65 -7
- package/crates/tish_builtins/src/number.rs +173 -0
- package/crates/tish_builtins/src/string.rs +168 -15
- package/crates/tish_bytecode/src/compiler.rs +56 -4
- package/crates/tish_bytecode/src/opcode.rs +4 -2
- package/crates/tish_compile/src/codegen.rs +519 -46
- package/crates/tish_compile/src/infer.rs +26 -11
- package/crates/tish_compile/src/resolve.rs +87 -10
- package/crates/tish_compile_js/src/codegen.rs +82 -24
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
- package/crates/tish_core/src/json.rs +5 -1
- package/crates/tish_core/src/lib.rs +48 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +6 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +661 -27
- package/crates/tish_eval/src/natives.rs +318 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_fmt/src/lib.rs +23 -4
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/src/lib.rs +41 -0
- package/crates/tish_parser/src/parser.rs +180 -11
- package/crates/tish_resolve/src/lib.rs +66 -5
- package/crates/tish_runtime/Cargo.toml +5 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/lib.rs +236 -10
- package/crates/tish_ui/src/jsx.rs +18 -3
- package/crates/tish_vm/src/jit.rs +1 -1
- package/crates/tish_vm/src/vm.rs +294 -60
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -149,11 +149,19 @@ pub fn unshift(arr: &Value, args: &[Value]) -> Value {
|
|
|
149
149
|
}
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
pub fn index_of(arr: &Value, search: &Value) -> Value {
|
|
152
|
+
pub fn index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
153
153
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
154
154
|
if let Value::Array(arr) = arr {
|
|
155
155
|
let arr_borrow = arr.borrow();
|
|
156
|
-
|
|
156
|
+
// `fromIndex`: clamp positive to [0, len]; negative counts from the end (`len + n`, floored
|
|
157
|
+
// at 0). Same handling as `includes`. Absent → 0.
|
|
158
|
+
let len = arr_borrow.len() as i64;
|
|
159
|
+
let start = match from {
|
|
160
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
|
|
161
|
+
Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
|
|
162
|
+
_ => 0,
|
|
163
|
+
};
|
|
164
|
+
for (i, v) in arr_borrow.iter().enumerate().skip(start) {
|
|
157
165
|
if v.strict_eq(search) {
|
|
158
166
|
return Value::Number(i as f64);
|
|
159
167
|
}
|
|
@@ -218,6 +226,57 @@ pub fn reverse(arr: &Value) -> Value {
|
|
|
218
226
|
}
|
|
219
227
|
}
|
|
220
228
|
|
|
229
|
+
/// `Array.prototype.toReversed()` — a reversed COPY (ES2023); the receiver is untouched.
|
|
230
|
+
pub fn to_reversed(arr: &Value) -> Value {
|
|
231
|
+
let mut v = snapshot_values(arr);
|
|
232
|
+
v.reverse();
|
|
233
|
+
Value::Array(VmRef::new(v))
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// `Array.prototype.toSorted(cmp?)` — a sorted COPY (ES2023); the receiver is untouched. Sorting runs
|
|
237
|
+
/// on the copy via the same comparator/default machinery as `sort`.
|
|
238
|
+
pub fn to_sorted(arr: &Value, comparator: Option<&Value>) -> Value {
|
|
239
|
+
let copy = Value::Array(VmRef::new(snapshot_values(arr)));
|
|
240
|
+
match comparator {
|
|
241
|
+
Some(cmp @ Value::Function(_)) => sort_with_comparator(©, cmp),
|
|
242
|
+
_ => sort_default(©),
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// `Array.prototype.with(index, value)` — a COPY with `index` replaced (ES2023). A negative index
|
|
247
|
+
/// counts from the end; an out-of-range index parks a catchable `RangeError` (matches node's message).
|
|
248
|
+
pub fn with(arr: &Value, index: &Value, value: &Value) -> Value {
|
|
249
|
+
let mut v = snapshot_values(arr);
|
|
250
|
+
let len = v.len() as i64;
|
|
251
|
+
let rel = match index {
|
|
252
|
+
Value::Number(n) if n.is_finite() => *n as i64,
|
|
253
|
+
_ => 0,
|
|
254
|
+
};
|
|
255
|
+
let actual = if rel >= 0 { rel } else { len + rel };
|
|
256
|
+
if actual < 0 || actual >= len {
|
|
257
|
+
tishlang_core::set_pending_throw(tishlang_core::range_error(format!(
|
|
258
|
+
"Invalid index : {}",
|
|
259
|
+
rel
|
|
260
|
+
)));
|
|
261
|
+
return Value::Null;
|
|
262
|
+
}
|
|
263
|
+
v[actual as usize] = value.clone();
|
|
264
|
+
Value::Array(VmRef::new(v))
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/// `Array.prototype.toSpliced(start, deleteCount?, ...items)` — a COPY with the splice applied (ES2023);
|
|
268
|
+
/// unlike `splice` (which returns the removed elements) this returns the resulting array.
|
|
269
|
+
pub fn to_spliced(
|
|
270
|
+
arr: &Value,
|
|
271
|
+
start: &Value,
|
|
272
|
+
delete_count: Option<&Value>,
|
|
273
|
+
items: &[Value],
|
|
274
|
+
) -> Value {
|
|
275
|
+
let copy = Value::Array(VmRef::new(snapshot_values(arr)));
|
|
276
|
+
splice(©, start, delete_count, items);
|
|
277
|
+
copy
|
|
278
|
+
}
|
|
279
|
+
|
|
221
280
|
/// Fisher-Yates shuffle. Returns a new shuffled array (does not mutate).
|
|
222
281
|
pub fn shuffle(arr: &Value) -> Value {
|
|
223
282
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
@@ -273,6 +332,55 @@ pub fn fill(arr: &Value, value: &Value, start: &Value, end: &Value) -> Value {
|
|
|
273
332
|
}
|
|
274
333
|
}
|
|
275
334
|
|
|
335
|
+
pub fn last_index_of(arr: &Value, search: &Value, from: Option<&Value>) -> Value {
|
|
336
|
+
let arr = as_boxed_array(arr);
|
|
337
|
+
let arr = &*arr;
|
|
338
|
+
if let Value::Array(arr) = arr {
|
|
339
|
+
let arr_borrow = arr.borrow();
|
|
340
|
+
let len = arr_borrow.len() as i64;
|
|
341
|
+
// Default fromIndex is len-1; positive clamps to len-1; negative counts back from the end.
|
|
342
|
+
let start = match from {
|
|
343
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
|
|
344
|
+
Some(Value::Number(n)) => len + *n as i64,
|
|
345
|
+
_ => len - 1,
|
|
346
|
+
};
|
|
347
|
+
let mut i = start;
|
|
348
|
+
while i >= 0 {
|
|
349
|
+
if let Some(v) = arr_borrow.get(i as usize) {
|
|
350
|
+
if v.strict_eq(search) {
|
|
351
|
+
return Value::Number(i as f64);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
i -= 1;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
Value::Number(-1.0)
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/// `copyWithin(target, start, end)` — copy the `[start, end)` block to `target`, in place; returns
|
|
361
|
+
/// the array. Indices are clamped, negatives count from the end.
|
|
362
|
+
pub fn copy_within(arr: &Value, target: &Value, start: &Value, end: &Value) -> Value {
|
|
363
|
+
let arr = as_boxed_array(arr);
|
|
364
|
+
let arr = &*arr;
|
|
365
|
+
if let Value::Array(a) = arr {
|
|
366
|
+
let mut v = a.borrow_mut();
|
|
367
|
+
let len = v.len();
|
|
368
|
+
let t = normalize_index(target, len as i64, 0);
|
|
369
|
+
let s = normalize_index(start, len as i64, 0);
|
|
370
|
+
let e = normalize_index(end, len as i64, len);
|
|
371
|
+
if s < e && t < len {
|
|
372
|
+
let count = (e - s).min(len - t);
|
|
373
|
+
let block: Vec<Value> = v[s..s + count].to_vec();
|
|
374
|
+
for (k, val) in block.into_iter().enumerate() {
|
|
375
|
+
v[t + k] = val;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
drop(v);
|
|
379
|
+
return Value::Array(a.clone());
|
|
380
|
+
}
|
|
381
|
+
Value::Null
|
|
382
|
+
}
|
|
383
|
+
|
|
276
384
|
pub fn slice(arr: &Value, start: &Value, end: &Value) -> Value {
|
|
277
385
|
let arr = as_boxed_array(arr); let arr = &*arr;
|
|
278
386
|
if let Value::Array(arr) = arr {
|
|
@@ -417,16 +525,25 @@ pub fn filter(arr: &Value, callback: &Value) -> Value {
|
|
|
417
525
|
}
|
|
418
526
|
}
|
|
419
527
|
|
|
420
|
-
|
|
528
|
+
/// `initial` is `None` when no initial value was passed (`arr.reduce(fn)`) and `Some` for an explicit
|
|
529
|
+
/// one (`arr.reduce(fn, x)`, INCLUDING `arr.reduce(fn, null)`). This distinction matters: reducing an
|
|
530
|
+
/// empty array with NO initial value is a `TypeError` in JS, while an explicit initial (even `null`)
|
|
531
|
+
/// is returned as-is. A `Value::Null` sentinel can't tell the two apart — hence `Option`.
|
|
532
|
+
pub fn reduce(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
|
|
421
533
|
// Packed fast path: fold the `Vec<f64>` snapshot directly. Same no-initial rule as the boxed
|
|
422
534
|
// path (absent init → first element as the seed, scan from index 1).
|
|
423
535
|
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
424
536
|
// `reduce` passes `(accumulator, element, index, array)` — the array is the 4th arg.
|
|
425
537
|
let arr_value = arr.clone();
|
|
426
|
-
let (start_idx, mut acc) =
|
|
427
|
-
(
|
|
428
|
-
|
|
429
|
-
|
|
538
|
+
let (start_idx, mut acc) = match initial {
|
|
539
|
+
Some(v) => (0usize, v.clone()),
|
|
540
|
+
None if !data.is_empty() => (1usize, Value::Number(data[0])),
|
|
541
|
+
None => {
|
|
542
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
543
|
+
"Reduce of empty array with no initial value",
|
|
544
|
+
));
|
|
545
|
+
return Value::Null;
|
|
546
|
+
}
|
|
430
547
|
};
|
|
431
548
|
// `skip(start_idx)` preserves the true element index for the callback's 3rd arg.
|
|
432
549
|
for (i, &x) in data.iter().enumerate().skip(start_idx) {
|
|
@@ -440,11 +557,15 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
|
|
|
440
557
|
let arr_borrow = arr.borrow().clone(); // #382: snapshot; drop the guard before any callback
|
|
441
558
|
let arr_value = Value::Array(arr.clone()); // 4th callback arg (JS `array`)
|
|
442
559
|
let len = arr_borrow.len();
|
|
443
|
-
let (start_idx, mut acc) =
|
|
444
|
-
|
|
445
|
-
(1, arr_borrow[0].clone())
|
|
446
|
-
|
|
447
|
-
|
|
560
|
+
let (start_idx, mut acc) = match initial {
|
|
561
|
+
Some(v) => (0, v.clone()),
|
|
562
|
+
None if !arr_borrow.is_empty() => (1, arr_borrow[0].clone()),
|
|
563
|
+
None => {
|
|
564
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
565
|
+
"Reduce of empty array with no initial value",
|
|
566
|
+
));
|
|
567
|
+
return Value::Null;
|
|
568
|
+
}
|
|
448
569
|
};
|
|
449
570
|
for i in start_idx..len {
|
|
450
571
|
if tishlang_core::has_pending_throw() { return acc; } // #303
|
|
@@ -457,6 +578,64 @@ pub fn reduce(arr: &Value, callback: &Value, initial: &Value) -> Value {
|
|
|
457
578
|
}
|
|
458
579
|
}
|
|
459
580
|
|
|
581
|
+
/// `reduceRight(cb, initial?)` — like [`reduce`] but folds right-to-left. `initial` is `None` when
|
|
582
|
+
/// omitted (empty array + no initial → catchable TypeError).
|
|
583
|
+
pub fn reduce_right(arr: &Value, callback: &Value, initial: Option<&Value>) -> Value {
|
|
584
|
+
let data = snapshot_values(arr);
|
|
585
|
+
if let Value::Function(cb) = callback {
|
|
586
|
+
let arr_value = arr.clone();
|
|
587
|
+
let len = data.len();
|
|
588
|
+
let (start, mut acc) = match initial {
|
|
589
|
+
Some(v) => (len, v.clone()),
|
|
590
|
+
None if len > 0 => (len - 1, data[len - 1].clone()),
|
|
591
|
+
None => {
|
|
592
|
+
tishlang_core::set_pending_throw(tishlang_core::type_error(
|
|
593
|
+
"Reduce of empty array with no initial value",
|
|
594
|
+
));
|
|
595
|
+
return Value::Null;
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
let mut i = start;
|
|
599
|
+
while i > 0 {
|
|
600
|
+
i -= 1;
|
|
601
|
+
if tishlang_core::has_pending_throw() {
|
|
602
|
+
return acc;
|
|
603
|
+
}
|
|
604
|
+
acc = cb.call(&[
|
|
605
|
+
acc,
|
|
606
|
+
data[i].clone(),
|
|
607
|
+
Value::Number(i as f64),
|
|
608
|
+
arr_value.clone(),
|
|
609
|
+
]);
|
|
610
|
+
}
|
|
611
|
+
acc
|
|
612
|
+
} else {
|
|
613
|
+
Value::Null
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/// `arr.keys()` — an iterator over the indices `0..len`.
|
|
618
|
+
pub fn keys(arr: &Value) -> Value {
|
|
619
|
+
let len = snapshot_values(arr).len();
|
|
620
|
+
crate::iterator::array_iterator((0..len).map(|i| Value::Number(i as f64)).collect())
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
/// `arr.values()` — an iterator over the elements.
|
|
624
|
+
pub fn values(arr: &Value) -> Value {
|
|
625
|
+
crate::iterator::array_iterator(snapshot_values(arr))
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
/// `arr.entries()` — an iterator over `[index, element]` pairs.
|
|
629
|
+
pub fn entries(arr: &Value) -> Value {
|
|
630
|
+
crate::iterator::array_iterator(
|
|
631
|
+
snapshot_values(arr)
|
|
632
|
+
.into_iter()
|
|
633
|
+
.enumerate()
|
|
634
|
+
.map(|(i, v)| Value::Array(VmRef::new(vec![Value::Number(i as f64), v])))
|
|
635
|
+
.collect(),
|
|
636
|
+
)
|
|
637
|
+
}
|
|
638
|
+
|
|
460
639
|
pub fn for_each(arr: &Value, callback: &Value) -> Value {
|
|
461
640
|
if let Some((data, cb)) = packed_snapshot(arr, callback) {
|
|
462
641
|
let arr_value = arr.clone(); // 3rd callback arg (JS `array`)
|
|
@@ -974,14 +1153,14 @@ mod packed_hof_tests {
|
|
|
974
1153
|
let n = na(&[3.0, 1.0, 4.0, 1.0, 5.0]);
|
|
975
1154
|
let add = cb_num(|acc, x| acc + x);
|
|
976
1155
|
// With init.
|
|
977
|
-
assert_eq!(reduce(&n, &add, &Value::Number(0.0)).as_number(), Some(14.0));
|
|
978
|
-
// No init → first element seeds, scan from index 1 (same total here).
|
|
979
|
-
assert_eq!(reduce(&n, &add,
|
|
1156
|
+
assert_eq!(reduce(&n, &add, Some(&Value::Number(0.0))).as_number(), Some(14.0));
|
|
1157
|
+
// No init (None) → first element seeds, scan from index 1 (same total here).
|
|
1158
|
+
assert_eq!(reduce(&n, &add, None).as_number(), Some(14.0));
|
|
980
1159
|
// Index arg: callback (acc, _elem, index) — sum the indices 0..5 = 10.
|
|
981
1160
|
let sum_idx = Value::native(|a: &[Value]| {
|
|
982
1161
|
Value::Number(a[0].as_number().unwrap() + a[2].as_number().unwrap())
|
|
983
1162
|
});
|
|
984
|
-
assert_eq!(reduce(&n, &sum_idx, &Value::Number(0.0)).as_number(), Some(10.0));
|
|
1163
|
+
assert_eq!(reduce(&n, &sum_idx, Some(&Value::Number(0.0))).as_number(), Some(10.0));
|
|
985
1164
|
}
|
|
986
1165
|
|
|
987
1166
|
#[test]
|
|
@@ -175,9 +175,13 @@ pub fn map_get(map: &Value, key: &Value) -> Value {
|
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
/// Direct `Map.prototype.set` (mutates in place; returns `undefined` = `Null`).
|
|
178
|
-
|
|
178
|
+
///
|
|
179
|
+
/// Takes the key/val by reference and clones on insert (the map owns its entries). Passing by
|
|
180
|
+
/// reference lets the native emitter BORROW the caller's place instead of moving it, so
|
|
181
|
+
/// `m.set(k, v); …use k…` no longer produces `E0382 use of moved value` in the generated Rust (#442).
|
|
182
|
+
pub fn map_set(map: &Value, key: &Value, val: &Value) -> Value {
|
|
179
183
|
if let Some(s) = map_store(map) {
|
|
180
|
-
s.borrow_mut().insert(Key(key), val);
|
|
184
|
+
s.borrow_mut().insert(Key(key.clone()), val.clone());
|
|
181
185
|
}
|
|
182
186
|
Value::Null
|
|
183
187
|
}
|
|
@@ -320,6 +324,20 @@ pub fn set_instance(initial: &[Value]) -> Value {
|
|
|
320
324
|
}),
|
|
321
325
|
);
|
|
322
326
|
}
|
|
327
|
+
// `__drain__`: the default-iteration array a Set yields (its elements). `drain_iterator`
|
|
328
|
+
// (and the interp/native equivalents) check `__drain__` FIRST, so exposing it here makes a
|
|
329
|
+
// Set directly iterable — `[...set]`, `for (x of set)`, `Array.from(set)` — across all three
|
|
330
|
+
// backends with no changes to any spread/for-of site. (#440)
|
|
331
|
+
{
|
|
332
|
+
let s = store.clone();
|
|
333
|
+
m.insert(
|
|
334
|
+
Arc::from("__drain__"),
|
|
335
|
+
Value::native(move |_args: &[Value]| {
|
|
336
|
+
let out: Vec<Value> = s.borrow().keys().map(|k| k.0.clone()).collect();
|
|
337
|
+
Value::Array(VmRef::new(out))
|
|
338
|
+
}),
|
|
339
|
+
);
|
|
340
|
+
}
|
|
323
341
|
|
|
324
342
|
Value::object(m)
|
|
325
343
|
}
|
|
@@ -442,6 +460,23 @@ pub fn map_instance(pairs: &[Value]) -> Value {
|
|
|
442
460
|
}),
|
|
443
461
|
);
|
|
444
462
|
}
|
|
463
|
+
// `__drain__`: a Map's default iteration yields `[key, value]` entries. Exposed so the shared
|
|
464
|
+
// `drain_iterator` (checked first) makes a Map directly iterable — `[...map]`, `for (x of map)`,
|
|
465
|
+
// `Array.from(map)` — across interp/vm/native with no spread/for-of site changes. (#440)
|
|
466
|
+
{
|
|
467
|
+
let s = store.clone();
|
|
468
|
+
m.insert(
|
|
469
|
+
Arc::from("__drain__"),
|
|
470
|
+
Value::native(move |_args: &[Value]| {
|
|
471
|
+
let pairs: Vec<Value> = s
|
|
472
|
+
.borrow()
|
|
473
|
+
.iter()
|
|
474
|
+
.map(|(k, v)| Value::Array(VmRef::new(vec![k.0.clone(), v.clone()])))
|
|
475
|
+
.collect();
|
|
476
|
+
Value::Array(VmRef::new(pairs))
|
|
477
|
+
}),
|
|
478
|
+
);
|
|
479
|
+
}
|
|
445
480
|
|
|
446
481
|
Value::object(m)
|
|
447
482
|
}
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
use std::sync::Arc;
|
|
7
7
|
use tishlang_core::VmRef;
|
|
8
|
-
use tishlang_core::{percent_decode, percent_encode, ObjectMap, Value};
|
|
8
|
+
use tishlang_core::{percent_decode, percent_decode_component, percent_encode, percent_encode_component, ObjectMap, Value};
|
|
9
9
|
|
|
10
10
|
/// Boolean(value) - coerce to bool
|
|
11
11
|
pub fn boolean(args: &[Value]) -> Value {
|
|
@@ -13,7 +13,7 @@ pub fn boolean(args: &[Value]) -> Value {
|
|
|
13
13
|
Value::Bool(v.is_truthy())
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
-
/// decodeURI(str)
|
|
16
|
+
/// decodeURI(str) — preserves the URI reserved delimiters (the inverse of encodeURI).
|
|
17
17
|
pub fn decode_uri(args: &[Value]) -> Value {
|
|
18
18
|
let s = args
|
|
19
19
|
.first()
|
|
@@ -31,19 +31,34 @@ pub fn encode_uri(args: &[Value]) -> Value {
|
|
|
31
31
|
Value::String(percent_encode(&s).into())
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
///
|
|
34
|
+
/// encodeURIComponent(str) — stricter than encodeURI (also escapes the URI reserved delimiters).
|
|
35
|
+
pub fn encode_uri_component(args: &[Value]) -> Value {
|
|
36
|
+
let s = args
|
|
37
|
+
.first()
|
|
38
|
+
.map(Value::to_display_string)
|
|
39
|
+
.unwrap_or_default();
|
|
40
|
+
Value::String(percent_encode_component(&s).into())
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// decodeURIComponent(str) — decodes ALL `%XX` escapes (including the reserved delimiters).
|
|
44
|
+
pub fn decode_uri_component(args: &[Value]) -> Value {
|
|
45
|
+
let s = args
|
|
46
|
+
.first()
|
|
47
|
+
.map(Value::to_display_string)
|
|
48
|
+
.unwrap_or_default();
|
|
49
|
+
Value::String(percent_decode_component(&s).unwrap_or(s).into())
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/// isFinite(value) — coerces via ToNumber (like `Number()`), then tests finiteness. `isFinite("3")`
|
|
53
|
+
/// is `true`, `isFinite("x")`/`isFinite()` is `false`. (The non-coercing form is `Number.isFinite`.)
|
|
35
54
|
pub fn is_finite(args: &[Value]) -> Value {
|
|
36
|
-
Value::Bool(
|
|
37
|
-
args.first()
|
|
38
|
-
.is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite())),
|
|
39
|
-
)
|
|
55
|
+
Value::Bool(args.first().map_or(f64::NAN, to_number).is_finite())
|
|
40
56
|
}
|
|
41
57
|
|
|
42
|
-
/// isNaN(value)
|
|
58
|
+
/// isNaN(value) — coerces via ToNumber, then tests NaN. `isNaN("3")` is `false`; an absent arg
|
|
59
|
+
/// (undefined) coerces to NaN → `true`. (The non-coercing form is `Number.isNaN`.)
|
|
43
60
|
pub fn is_nan(args: &[Value]) -> Value {
|
|
44
|
-
Value::Bool(args.first().
|
|
45
|
-
matches!(v, Value::Number(n) if n.is_nan()) || !matches!(v, Value::Number(_))
|
|
46
|
-
}))
|
|
61
|
+
Value::Bool(args.first().map_or(f64::NAN, to_number).is_nan())
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
/// Array.isArray(value)
|
|
@@ -51,6 +66,142 @@ pub fn array_is_array(args: &[Value]) -> Value {
|
|
|
51
66
|
Value::Bool(matches!(args.first(), Some(Value::Array(_)) | Some(Value::NumberArray(_))))
|
|
52
67
|
}
|
|
53
68
|
|
|
69
|
+
/// `Array.of(...items)` — build an array from the args (unlike `Array(n)`, a single number is an
|
|
70
|
+
/// element, not a length: `Array.of(7)` is `[7]`).
|
|
71
|
+
pub fn array_of(args: &[Value]) -> Value {
|
|
72
|
+
Value::Array(VmRef::new(args.to_vec()))
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// `Array.from(source, mapFn?)` — build an array from an iterable (array, string, Set/Map, or any
|
|
76
|
+
/// `__drain__`/`next` iterator — via the shared iterator protocol) or an array-like `{ length }`
|
|
77
|
+
/// object, optionally mapping each element through `mapFn(item, index)`.
|
|
78
|
+
pub fn array_from(args: &[Value]) -> Value {
|
|
79
|
+
let source = args.first().cloned().unwrap_or(Value::Null);
|
|
80
|
+
let items: Vec<Value> = match &source {
|
|
81
|
+
Value::Array(a) => a.borrow().clone(),
|
|
82
|
+
Value::NumberArray(a) => a.borrow().iter().map(|n| Value::Number(*n)).collect(),
|
|
83
|
+
// `drain_iterator` covers String (chars), Set/Map, and any iterator object.
|
|
84
|
+
other => {
|
|
85
|
+
if let Some(d) = tishlang_core::drain_iterator(other) {
|
|
86
|
+
d
|
|
87
|
+
} else if matches!(other, Value::Object(_)) {
|
|
88
|
+
let len = tishlang_core::object_get(other, &Value::String("length".into()))
|
|
89
|
+
.and_then(|v| match v {
|
|
90
|
+
Value::Number(n) if n.is_finite() && n >= 0.0 => Some(n as usize),
|
|
91
|
+
_ => None,
|
|
92
|
+
})
|
|
93
|
+
.unwrap_or(0);
|
|
94
|
+
(0..len)
|
|
95
|
+
.map(|i| {
|
|
96
|
+
tishlang_core::object_get(other, &Value::String(i.to_string().into()))
|
|
97
|
+
.unwrap_or(Value::Null)
|
|
98
|
+
})
|
|
99
|
+
.collect()
|
|
100
|
+
} else {
|
|
101
|
+
Vec::new()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
let out: Vec<Value> = match args.get(1) {
|
|
106
|
+
Some(Value::Function(f)) => items
|
|
107
|
+
.into_iter()
|
|
108
|
+
.enumerate()
|
|
109
|
+
.map(|(i, x)| f.call(&[x, Value::Number(i as f64)]))
|
|
110
|
+
.collect(),
|
|
111
|
+
_ => items,
|
|
112
|
+
};
|
|
113
|
+
Value::Array(VmRef::new(out))
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/// `structuredClone(value)` — a recursive deep copy of arrays, objects, and primitives, with cycles
|
|
117
|
+
/// preserved (a repeated reference clones to the same new node). Functions and exotic types (Date,
|
|
118
|
+
/// RegExp, Map, Set) are shared/shallow — a pragmatic subset covering plain data structures.
|
|
119
|
+
pub fn structured_clone(args: &[Value]) -> Value {
|
|
120
|
+
let mut seen: Vec<(*const (), Value)> = Vec::new();
|
|
121
|
+
deep_clone(args.first().unwrap_or(&Value::Null), &mut seen)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
fn deep_clone(v: &Value, seen: &mut Vec<(*const (), Value)>) -> Value {
|
|
125
|
+
match v {
|
|
126
|
+
Value::Array(a) => {
|
|
127
|
+
let ptr = a.as_ptr();
|
|
128
|
+
if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
|
|
129
|
+
return c.clone();
|
|
130
|
+
}
|
|
131
|
+
let new: VmRef<Vec<Value>> = VmRef::new(Vec::new());
|
|
132
|
+
seen.push((ptr, Value::Array(new.clone())));
|
|
133
|
+
let src = a.borrow().clone();
|
|
134
|
+
let cloned: Vec<Value> = src.iter().map(|e| deep_clone(e, seen)).collect();
|
|
135
|
+
*new.borrow_mut() = cloned;
|
|
136
|
+
Value::Array(new)
|
|
137
|
+
}
|
|
138
|
+
Value::NumberArray(a) => Value::NumberArray(VmRef::new(a.borrow().clone())),
|
|
139
|
+
Value::Object(o) => {
|
|
140
|
+
let ptr = o.as_ptr();
|
|
141
|
+
if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
|
|
142
|
+
return c.clone();
|
|
143
|
+
}
|
|
144
|
+
let entries: Vec<(Arc<str>, Value)> = o
|
|
145
|
+
.borrow()
|
|
146
|
+
.strings
|
|
147
|
+
.iter()
|
|
148
|
+
.map(|(k, val)| (k.clone(), val.clone()))
|
|
149
|
+
.collect();
|
|
150
|
+
let new_obj = Value::object(ObjectMap::default());
|
|
151
|
+
// register the (empty) new object before recursing so cycles resolve back to it
|
|
152
|
+
if let Value::Object(no) = &new_obj {
|
|
153
|
+
seen.push((ptr, new_obj.clone()));
|
|
154
|
+
for (k, val) in entries {
|
|
155
|
+
let cloned = deep_clone(&val, seen);
|
|
156
|
+
no.borrow_mut().strings.insert(k, cloned);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
new_obj
|
|
160
|
+
}
|
|
161
|
+
other => other.clone(),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/// `Object.hasOwn(obj, key)` (ES2022) — true if `key` is an OWN property of `obj`. For arrays an
|
|
166
|
+
/// in-range integer index or `"length"` counts; for plain objects, a key present in the string map.
|
|
167
|
+
pub fn object_has_own(args: &[Value]) -> Value {
|
|
168
|
+
let key = match args.get(1) {
|
|
169
|
+
Some(Value::String(s)) => s.to_string(),
|
|
170
|
+
Some(Value::Number(n)) => tishlang_core::js_number_to_string(*n),
|
|
171
|
+
Some(v) => v.to_display_string(),
|
|
172
|
+
None => return Value::Bool(false),
|
|
173
|
+
};
|
|
174
|
+
match args.first() {
|
|
175
|
+
Some(Value::Object(obj)) => Value::Bool(obj.borrow().strings.contains_key(key.as_str())),
|
|
176
|
+
Some(Value::Array(arr)) => Value::Bool(
|
|
177
|
+
key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
|
|
178
|
+
),
|
|
179
|
+
Some(Value::NumberArray(arr)) => Value::Bool(
|
|
180
|
+
key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
|
|
181
|
+
),
|
|
182
|
+
_ => Value::Bool(false),
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/// `Object.is(a, b)` — SameValue: like `===`, but NaN equals NaN and `+0` is NOT equal to `-0`.
|
|
187
|
+
pub fn object_is(args: &[Value]) -> Value {
|
|
188
|
+
let a = args.first().unwrap_or(&Value::Null);
|
|
189
|
+
let b = args.get(1).unwrap_or(&Value::Null);
|
|
190
|
+
let same = match (a, b) {
|
|
191
|
+
(Value::Number(x), Value::Number(y)) => {
|
|
192
|
+
if x.is_nan() && y.is_nan() {
|
|
193
|
+
true
|
|
194
|
+
} else if *x == 0.0 && *y == 0.0 {
|
|
195
|
+
x.is_sign_negative() == y.is_sign_negative()
|
|
196
|
+
} else {
|
|
197
|
+
x == y
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
_ => a.strict_eq(b),
|
|
201
|
+
};
|
|
202
|
+
Value::Bool(same)
|
|
203
|
+
}
|
|
204
|
+
|
|
54
205
|
/// String(value) — convert value to string (JS String constructor as function).
|
|
55
206
|
/// Uses JS `ToString` (arrays comma-join recursively, objects → "[object Object]"),
|
|
56
207
|
/// not the inspect/display form.
|
|
@@ -64,8 +215,14 @@ pub fn string_convert(args: &[Value]) -> Value {
|
|
|
64
215
|
/// otherwise NaN); arrays/objects go via their string form (so `Number([5])` → 5,
|
|
65
216
|
/// `Number([])` → 0, objects → NaN).
|
|
66
217
|
pub fn number_convert(args: &[Value]) -> Value {
|
|
67
|
-
|
|
68
|
-
|
|
218
|
+
Value::Number(to_number(args.first().unwrap_or(&Value::Null)))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// JS `ToNumber(v)`: Number → itself; Bool → 1/0; Null → 0; String → parsed (trimmed, `0x`/`0b`/`0o`
|
|
222
|
+
/// and `Infinity`; `""` → 0, else NaN); anything else via its string form. The single ToNumber path
|
|
223
|
+
/// shared by `Number()`, `isNaN`/`isFinite`, etc.
|
|
224
|
+
pub fn to_number(v: &Value) -> f64 {
|
|
225
|
+
match v {
|
|
69
226
|
Value::Number(n) => *n,
|
|
70
227
|
Value::Bool(b) => {
|
|
71
228
|
if *b {
|
|
@@ -77,8 +234,29 @@ pub fn number_convert(args: &[Value]) -> Value {
|
|
|
77
234
|
Value::Null => 0.0,
|
|
78
235
|
Value::String(s) => parse_numeric_string(s),
|
|
79
236
|
other => parse_numeric_string(&other.to_js_string()),
|
|
80
|
-
}
|
|
81
|
-
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/// `Number.isInteger(v)` — true iff `v` IS a finite Number with no fractional part. No coercion
|
|
241
|
+
/// (`Number.isInteger("3")` is `false`, unlike a bare `isNaN`-style coercion).
|
|
242
|
+
pub fn number_is_integer(args: &[Value]) -> Value {
|
|
243
|
+
Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0))
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// `Number.isSafeInteger(v)` — `isInteger` AND exactly representable (|v| ≤ 2^53 − 1).
|
|
247
|
+
pub fn number_is_safe_integer(args: &[Value]) -> Value {
|
|
248
|
+
Value::Bool(matches!(args.first(),
|
|
249
|
+
Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_991.0))
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/// `Number.isNaN(v)` — true iff `v` IS the number NaN. No coercion (unlike the global `isNaN`).
|
|
253
|
+
pub fn number_is_nan(args: &[Value]) -> Value {
|
|
254
|
+
Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_nan()))
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/// `Number.isFinite(v)` — true iff `v` is a finite Number. No coercion (unlike the global `isFinite`).
|
|
258
|
+
pub fn number_is_finite(args: &[Value]) -> Value {
|
|
259
|
+
Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite()))
|
|
82
260
|
}
|
|
83
261
|
|
|
84
262
|
/// Parse a string as JS `Number` does: trimmed; `""` → 0; `0x`/`0o`/`0b` radix prefixes;
|
|
@@ -120,16 +298,28 @@ pub fn string_from_char_code(args: &[Value]) -> Value {
|
|
|
120
298
|
|
|
121
299
|
/// Object.keys(obj)
|
|
122
300
|
pub fn object_keys(args: &[Value]) -> Value {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
301
|
+
match args.first() {
|
|
302
|
+
Some(Value::Object(obj)) => {
|
|
303
|
+
let obj_borrow = obj.borrow();
|
|
304
|
+
let keys: Vec<Value> = obj_borrow
|
|
305
|
+
.strings
|
|
306
|
+
.keys()
|
|
307
|
+
.map(|k| Value::String(tishlang_core::ArcStr::from(k.as_ref())))
|
|
308
|
+
.collect();
|
|
309
|
+
Value::Array(VmRef::new(keys))
|
|
310
|
+
}
|
|
311
|
+
// `Object.keys(array)` yields the index strings "0".."len-1" (an array's own enumerable keys
|
|
312
|
+
// are its indices, per JS). Previously returned `[]` — a node divergence, and the reason
|
|
313
|
+
// `for (k in array)` enumerated nothing on the vm/native backends (they lower for-in to
|
|
314
|
+
// for-of over `Object.keys`).
|
|
315
|
+
Some(Value::Array(arr)) => {
|
|
316
|
+
let len = arr.borrow().len();
|
|
317
|
+
let keys: Vec<Value> = (0..len)
|
|
318
|
+
.map(|i| Value::String(tishlang_core::ArcStr::from(i.to_string().as_str())))
|
|
319
|
+
.collect();
|
|
320
|
+
Value::Array(VmRef::new(keys))
|
|
321
|
+
}
|
|
322
|
+
_ => Value::Array(VmRef::new(Vec::new())),
|
|
133
323
|
}
|
|
134
324
|
}
|
|
135
325
|
|