@tishlang/tish-lsp 2.16.13 → 2.35.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/bin/tish-lsp +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  3. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  4. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  5. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  6. package/crates/tish/tests/fs_parity.rs +103 -0
  7. package/crates/tish/tests/shortcircuit.rs +3 -3
  8. package/crates/tish_ast/src/ast.rs +77 -3
  9. package/crates/tish_builtins/Cargo.toml +1 -0
  10. package/crates/tish_builtins/src/array.rs +195 -16
  11. package/crates/tish_builtins/src/collections.rs +37 -2
  12. package/crates/tish_builtins/src/globals.rs +215 -25
  13. package/crates/tish_builtins/src/math.rs +65 -7
  14. package/crates/tish_builtins/src/number.rs +173 -0
  15. package/crates/tish_builtins/src/string.rs +168 -15
  16. package/crates/tish_bytecode/src/compiler.rs +56 -4
  17. package/crates/tish_bytecode/src/opcode.rs +4 -2
  18. package/crates/tish_compile/src/codegen.rs +513 -44
  19. package/crates/tish_compile/src/infer.rs +26 -11
  20. package/crates/tish_compile/src/resolve.rs +87 -10
  21. package/crates/tish_compile_js/src/codegen.rs +82 -24
  22. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  23. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +17 -2
  24. package/crates/tish_core/src/json.rs +5 -1
  25. package/crates/tish_core/src/lib.rs +48 -2
  26. package/crates/tish_core/src/uri.rs +45 -0
  27. package/crates/tish_core/src/value.rs +6 -0
  28. package/crates/tish_eval/Cargo.toml +2 -1
  29. package/crates/tish_eval/src/eval.rs +655 -27
  30. package/crates/tish_eval/src/natives.rs +264 -18
  31. package/crates/tish_eval/src/regex.rs +47 -1
  32. package/crates/tish_fmt/src/lib.rs +23 -4
  33. package/crates/tish_lsp/Cargo.toml +1 -1
  34. package/crates/tish_opt/src/lib.rs +20 -1
  35. package/crates/tish_parser/src/lib.rs +41 -0
  36. package/crates/tish_parser/src/parser.rs +180 -11
  37. package/crates/tish_resolve/src/lib.rs +66 -5
  38. package/crates/tish_runtime/Cargo.toml +5 -1
  39. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  40. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  41. package/crates/tish_runtime/src/lib.rs +128 -9
  42. package/crates/tish_ui/src/jsx.rs +18 -3
  43. package/crates/tish_vm/src/jit.rs +1 -1
  44. package/crates/tish_vm/src/vm.rs +270 -60
  45. package/package.json +1 -1
  46. package/platform/darwin-arm64/tish-lsp +0 -0
  47. package/platform/darwin-x64/tish-lsp +0 -0
  48. package/platform/linux-arm64/tish-lsp +0 -0
  49. package/platform/linux-x64/tish-lsp +0 -0
  50. package/platform/win32-x64/tish-lsp.exe +0 -0
@@ -238,6 +238,9 @@ impl Evaluator {
238
238
  );
239
239
  s.set("decodeURI".into(), Value::Native(natives::decode_uri), true);
240
240
  s.set("encodeURI".into(), Value::Native(natives::encode_uri), true);
241
+ s.set("encodeURIComponent".into(), Value::Native(natives::encode_uri_component), true);
242
+ s.set("decodeURIComponent".into(), Value::Native(natives::decode_uri_component), true);
243
+ s.set("structuredClone".into(), Value::Native(natives::structured_clone), true);
241
244
  s.set(
242
245
  "htmlEscape".into(),
243
246
  Value::Native(natives::html_escape),
@@ -262,6 +265,7 @@ impl Evaluator {
262
265
  math.insert("round".into(), Value::Native(natives::math_round));
263
266
  math.insert("random".into(), Value::Native(natives::math_random));
264
267
  math.insert("pow".into(), Value::Native(natives::math_pow));
268
+ math.insert("imul".into(), Value::Native(natives::math_imul));
265
269
  math.insert("hypot".into(), Value::Native(natives::math_hypot));
266
270
  math.insert("asin".into(), Value::Native(natives::math_asin));
267
271
  math.insert("acos".into(), Value::Native(natives::math_acos));
@@ -281,6 +285,10 @@ impl Evaluator {
281
285
  math.insert("acosh".into(), Value::Native(natives::math_acosh));
282
286
  math.insert("atanh".into(), Value::Native(natives::math_atanh));
283
287
  math.insert("cbrt".into(), Value::Native(natives::math_cbrt));
288
+ math.insert("expm1".into(), Value::Native(natives::math_expm1));
289
+ math.insert("log1p".into(), Value::Native(natives::math_log1p));
290
+ math.insert("clz32".into(), Value::Native(natives::math_clz32));
291
+ math.insert("fround".into(), Value::Native(natives::math_fround));
284
292
  math.insert("log2".into(), Value::Native(natives::math_log2));
285
293
  math.insert("log10".into(), Value::Native(natives::math_log10));
286
294
  math.insert("PI".into(), Value::Number(std::f64::consts::PI));
@@ -329,6 +337,10 @@ impl Evaluator {
329
337
  object.insert("values".into(), Value::Native(Self::object_values));
330
338
  object.insert("entries".into(), Value::Native(Self::object_entries));
331
339
  object.insert("assign".into(), Value::Native(Self::object_assign));
340
+ object.insert("is".into(), Value::Native(natives::object_is));
341
+ object.insert("hasOwn".into(), Value::Native(natives::object_has_own));
342
+ // getOwnPropertyNames ≈ keys for tish's plain objects (own string keys, insertion order).
343
+ object.insert("getOwnPropertyNames".into(), Value::Native(Self::object_keys));
332
344
  object.insert(
333
345
  "fromEntries".into(),
334
346
  Value::Native(Self::object_from_entries),
@@ -341,6 +353,7 @@ impl Evaluator {
341
353
 
342
354
  let mut array_obj = PropMap::with_capacity(3);
343
355
  array_obj.insert("isArray".into(), Value::Native(natives::array_is_array));
356
+ array_obj.insert("of".into(), Value::Native(natives::array_of));
344
357
  // `Array(n)` and `new Array(n)` constructor (issue #72).
345
358
  array_obj.insert("__call".into(), Value::Native(natives::array_construct));
346
359
  array_obj.insert("__construct".into(), Value::Native(natives::array_construct));
@@ -376,9 +389,23 @@ impl Evaluator {
376
389
  true,
377
390
  );
378
391
 
379
- // `Number(value)` coercion as a callable global (issue #36).
380
- let mut number_obj = PropMap::with_capacity(1);
392
+ // `Number(value)` coercion as a callable global (issue #36) + the `Number.*` statics.
393
+ let mut number_obj = PropMap::with_capacity(15);
381
394
  number_obj.insert("__call".into(), Value::Native(natives::number_convert));
395
+ number_obj.insert("isInteger".into(), Value::Native(natives::number_is_integer));
396
+ number_obj.insert("isSafeInteger".into(), Value::Native(natives::number_is_safe_integer));
397
+ number_obj.insert("isNaN".into(), Value::Native(natives::number_is_nan));
398
+ number_obj.insert("isFinite".into(), Value::Native(natives::number_is_finite));
399
+ number_obj.insert("parseInt".into(), Value::Native(natives::parse_int));
400
+ number_obj.insert("parseFloat".into(), Value::Native(natives::parse_float));
401
+ number_obj.insert("MAX_SAFE_INTEGER".into(), Value::Number(9_007_199_254_740_991.0));
402
+ number_obj.insert("MIN_SAFE_INTEGER".into(), Value::Number(-9_007_199_254_740_991.0));
403
+ number_obj.insert("EPSILON".into(), Value::Number(f64::EPSILON));
404
+ number_obj.insert("MAX_VALUE".into(), Value::Number(f64::MAX));
405
+ number_obj.insert("MIN_VALUE".into(), Value::Number(5e-324));
406
+ number_obj.insert("POSITIVE_INFINITY".into(), Value::Number(f64::INFINITY));
407
+ number_obj.insert("NEGATIVE_INFINITY".into(), Value::Number(f64::NEG_INFINITY));
408
+ number_obj.insert("NaN".into(), Value::Number(f64::NAN));
382
409
  s.set("Number".into(), Value::object(number_obj), true);
383
410
 
384
411
  s.set(
@@ -660,6 +687,51 @@ impl Evaluator {
660
687
  self.scope = outer;
661
688
  ret
662
689
  }
690
+ Statement::ForIn {
691
+ name,
692
+ object,
693
+ body,
694
+ ..
695
+ } => {
696
+ let obj_val = self.eval_expr(object)?;
697
+ // for-in enumerates OWN ENUMERABLE KEYS as strings (JS semantics): objects yield keys
698
+ // in insertion order, arrays yield index strings "0".."len-1", and a non-object
699
+ // (incl. null) yields nothing — a no-op, matching JS `for (k in null)`.
700
+ let keys: Vec<crate::value::Value> = match &obj_val {
701
+ crate::value::Value::Object(obj) => obj
702
+ .borrow()
703
+ .strings
704
+ .keys()
705
+ .map(|k| crate::value::Value::String(Arc::clone(k)))
706
+ .collect(),
707
+ crate::value::Value::Array(arr) => {
708
+ let len = arr.borrow().len();
709
+ (0..len)
710
+ .map(|i| crate::value::Value::String(Arc::from(i.to_string())))
711
+ .collect()
712
+ }
713
+ _ => Vec::new(),
714
+ };
715
+ // Fresh per-iteration binding (ES6 `for (let k in …)`), like the for-of arm above.
716
+ let outer = Rc::clone(&self.scope);
717
+ let mut ret = Ok(Value::Null);
718
+ for key in keys {
719
+ let iter_env = Scope::child(Rc::clone(&outer));
720
+ iter_env.borrow_mut().set(Arc::clone(name), key, true);
721
+ self.scope = Rc::clone(&iter_env);
722
+ match self.eval_statement(body) {
723
+ Ok(_) => {}
724
+ Err(EvalError::Break) => break,
725
+ Err(EvalError::Continue) => continue,
726
+ Err(e) => {
727
+ ret = Err(e);
728
+ break;
729
+ }
730
+ }
731
+ }
732
+ self.scope = outer;
733
+ ret
734
+ }
663
735
  Statement::For {
664
736
  init,
665
737
  cond,
@@ -948,7 +1020,7 @@ impl Evaluator {
948
1020
  ExportDeclaration::ReExport {
949
1021
  specifiers,
950
1022
  all,
951
- from,
1023
+ from: Some(from),
952
1024
  ..
953
1025
  } => {
954
1026
  let dep_val = self.load_module(from.as_ref())?;
@@ -971,6 +1043,11 @@ impl Evaluator {
971
1043
  }
972
1044
  }
973
1045
  }
1046
+ // #415 local named export (`export { a, b as c }`, no `from`): a no-op in the
1047
+ // running (entry) program — the bindings already exist, and an export alias is
1048
+ // visible only to importers, never as a local variable (JS semantics). The module
1049
+ // export table is populated by the module-evaluation path, not here.
1050
+ ExportDeclaration::ReExport { from: None, .. } => {}
974
1051
  }
975
1052
  Ok(Value::Null)
976
1053
  }
@@ -1041,7 +1118,7 @@ impl Evaluator {
1041
1118
  ExportDeclaration::ReExport {
1042
1119
  specifiers,
1043
1120
  all,
1044
- from,
1121
+ from: Some(from),
1045
1122
  ..
1046
1123
  } => {
1047
1124
  let dep_val = self.load_module(from.as_ref())?;
@@ -1066,6 +1143,24 @@ impl Evaluator {
1066
1143
  }
1067
1144
  }
1068
1145
  }
1146
+ // #415 local named export (`export { a, b as c }`, no `from`): the bindings are
1147
+ // already declared in this module's scope; expose each under its export name.
1148
+ ExportDeclaration::ReExport {
1149
+ specifiers,
1150
+ from: None,
1151
+ ..
1152
+ } => {
1153
+ for spec in specifiers {
1154
+ if let ImportSpecifier::Named { name, alias, .. } = spec {
1155
+ let v = self.scope.borrow().get(name.as_ref()).ok_or_else(|| {
1156
+ EvalError::Error(format!("Export '{}' is not defined", name))
1157
+ })?;
1158
+ let bind = alias.as_deref().unwrap_or(name.as_ref());
1159
+ self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
1160
+ export_names.push(bind.to_string());
1161
+ }
1162
+ }
1163
+ }
1069
1164
  }
1070
1165
  } else {
1071
1166
  let _ = self.eval_statement(stmt);
@@ -1132,6 +1227,44 @@ impl Evaluator {
1132
1227
  Value::Native(natives::read_file_bytes),
1133
1228
  );
1134
1229
  exports.insert("mkdir".into(), Value::Native(natives::mkdir));
1230
+ // Node-compatible names + new methods (sync) — issue #122
1231
+ use natives::fsx;
1232
+ exports.insert("readFileSync".into(), Value::Native(fsx::read_file));
1233
+ exports.insert("readFileBytesSync".into(), Value::Native(fsx::read_file_bytes));
1234
+ exports.insert("writeFileSync".into(), Value::Native(fsx::write_file));
1235
+ exports.insert("appendFile".into(), Value::Native(fsx::append_file));
1236
+ exports.insert("appendFileSync".into(), Value::Native(fsx::append_file));
1237
+ exports.insert("existsSync".into(), Value::Native(fsx::exists));
1238
+ exports.insert("mkdirSync".into(), Value::Native(fsx::mkdir));
1239
+ exports.insert("readdir".into(), Value::Native(fsx::readdir));
1240
+ exports.insert("readdirSync".into(), Value::Native(fsx::readdir));
1241
+ exports.insert("stat".into(), Value::Native(fsx::stat));
1242
+ exports.insert("statSync".into(), Value::Native(fsx::stat));
1243
+ exports.insert("lstat".into(), Value::Native(fsx::lstat));
1244
+ exports.insert("lstatSync".into(), Value::Native(fsx::lstat));
1245
+ exports.insert("rm".into(), Value::Native(fsx::rm));
1246
+ exports.insert("rmSync".into(), Value::Native(fsx::rm));
1247
+ exports.insert("rmdir".into(), Value::Native(fsx::rmdir));
1248
+ exports.insert("rmdirSync".into(), Value::Native(fsx::rmdir));
1249
+ exports.insert("unlink".into(), Value::Native(fsx::unlink));
1250
+ exports.insert("unlinkSync".into(), Value::Native(fsx::unlink));
1251
+ exports.insert("rename".into(), Value::Native(fsx::rename));
1252
+ exports.insert("renameSync".into(), Value::Native(fsx::rename));
1253
+ exports.insert("copyFile".into(), Value::Native(fsx::copy_file));
1254
+ exports.insert("copyFileSync".into(), Value::Native(fsx::copy_file));
1255
+ exports.insert("realpath".into(), Value::Native(fsx::realpath));
1256
+ exports.insert("realpathSync".into(), Value::Native(fsx::realpath));
1257
+ exports.insert("readlink".into(), Value::Native(fsx::readlink));
1258
+ exports.insert("readlinkSync".into(), Value::Native(fsx::readlink));
1259
+ exports.insert("truncate".into(), Value::Native(fsx::truncate));
1260
+ exports.insert("truncateSync".into(), Value::Native(fsx::truncate));
1261
+ exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp));
1262
+ exports.insert("mkdtempSync".into(), Value::Native(fsx::mkdtemp));
1263
+ exports.insert("cp".into(), Value::Native(fsx::cp));
1264
+ exports.insert("cpSync".into(), Value::Native(fsx::cp));
1265
+ exports.insert("access".into(), Value::Native(fsx::access));
1266
+ exports.insert("accessSync".into(), Value::Native(fsx::access));
1267
+ exports.insert("constants".into(), fsx::constants());
1135
1268
  Ok(Value::object(exports))
1136
1269
  }
1137
1270
  #[cfg(not(feature = "fs"))]
@@ -1141,6 +1274,41 @@ impl Evaluator {
1141
1274
  ));
1142
1275
  }
1143
1276
  }
1277
+ "tish:fs/promises" => {
1278
+ #[cfg(feature = "fs")]
1279
+ {
1280
+ use natives::fsx;
1281
+ let mut exports: PropMap = PropMap::default();
1282
+ exports.insert("readFile".into(), Value::Native(fsx::read_file_p));
1283
+ exports.insert("readFileBytes".into(), Value::Native(fsx::read_file_bytes_p));
1284
+ exports.insert("writeFile".into(), Value::Native(fsx::write_file_p));
1285
+ exports.insert("appendFile".into(), Value::Native(fsx::append_file_p));
1286
+ exports.insert("exists".into(), Value::Native(fsx::exists_p));
1287
+ exports.insert("mkdir".into(), Value::Native(fsx::mkdir_p));
1288
+ exports.insert("readdir".into(), Value::Native(fsx::readdir_p));
1289
+ exports.insert("stat".into(), Value::Native(fsx::stat_p));
1290
+ exports.insert("lstat".into(), Value::Native(fsx::lstat_p));
1291
+ exports.insert("rm".into(), Value::Native(fsx::rm_p));
1292
+ exports.insert("rmdir".into(), Value::Native(fsx::rmdir_p));
1293
+ exports.insert("unlink".into(), Value::Native(fsx::unlink_p));
1294
+ exports.insert("rename".into(), Value::Native(fsx::rename_p));
1295
+ exports.insert("copyFile".into(), Value::Native(fsx::copy_file_p));
1296
+ exports.insert("realpath".into(), Value::Native(fsx::realpath_p));
1297
+ exports.insert("readlink".into(), Value::Native(fsx::readlink_p));
1298
+ exports.insert("truncate".into(), Value::Native(fsx::truncate_p));
1299
+ exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp_p));
1300
+ exports.insert("cp".into(), Value::Native(fsx::cp_p));
1301
+ exports.insert("access".into(), Value::Native(fsx::access_p));
1302
+ exports.insert("constants".into(), fsx::constants());
1303
+ Ok(Value::object(exports))
1304
+ }
1305
+ #[cfg(not(feature = "fs"))]
1306
+ {
1307
+ return Err(EvalError::Error(
1308
+ "tish:fs/promises requires the fs feature. Rebuild with: cargo build -p tishlang --features fs".into(),
1309
+ ));
1310
+ }
1311
+ }
1144
1312
  "tish:http" => {
1145
1313
  #[cfg(feature = "http")]
1146
1314
  {
@@ -1538,7 +1706,58 @@ impl Evaluator {
1538
1706
  {
1539
1707
  let obj = self.eval_expr(object)?;
1540
1708
  let arg_vals = self.eval_call_args(args)?;
1541
-
1709
+
1710
+ // `Array.from(source, mapFn?)` — handled inline so the mapFn callback runs with the
1711
+ // interpreter context (a plain native has no `self`). Covers array / string / Set /
1712
+ // Map / iterator (via `__drain__`) and array-like `{ length }`. (#440)
1713
+ if let Expr::Ident { name, .. } = object.as_ref() {
1714
+ if name.as_ref() == "Array" && method_name.as_ref() == "from" {
1715
+ let source = arg_vals.first().cloned().unwrap_or(Value::Null);
1716
+ let items: Vec<Value> = match &source {
1717
+ Value::Array(a) => a.borrow().clone(),
1718
+ Value::String(s) => s
1719
+ .chars()
1720
+ .map(|c| Value::String(std::sync::Arc::from(c.to_string())))
1721
+ .collect(),
1722
+ _ => {
1723
+ if let Some(d) = self.drain_eval_iterator(&source) {
1724
+ d
1725
+ } else if matches!(&source, Value::Object(_)) {
1726
+ let len = self
1727
+ .get_prop(&source, "length")
1728
+ .ok()
1729
+ .and_then(|v| match v {
1730
+ Value::Number(n) if n.is_finite() && n >= 0.0 => {
1731
+ Some(n as usize)
1732
+ }
1733
+ _ => None,
1734
+ })
1735
+ .unwrap_or(0);
1736
+ (0..len)
1737
+ .map(|i| {
1738
+ self.get_prop(&source, &i.to_string())
1739
+ .unwrap_or(Value::Null)
1740
+ })
1741
+ .collect()
1742
+ } else {
1743
+ Vec::new()
1744
+ }
1745
+ }
1746
+ };
1747
+ let out = match arg_vals.get(1) {
1748
+ Some(f) if !matches!(f, Value::Null) => {
1749
+ let mut o = Vec::with_capacity(items.len());
1750
+ for (i, item) in items.into_iter().enumerate() {
1751
+ o.push(self.call_func(f, &[item, Value::Number(i as f64)])?);
1752
+ }
1753
+ o
1754
+ }
1755
+ _ => items,
1756
+ };
1757
+ return Ok(Value::array(out));
1758
+ }
1759
+ }
1760
+
1542
1761
  // Array methods
1543
1762
  if let Value::Array(arr) = &obj {
1544
1763
  match method_name.as_ref() {
@@ -1569,13 +1788,66 @@ impl Evaluator {
1569
1788
  "indexOf" => {
1570
1789
  let search = arg_vals.first().cloned().unwrap_or(Value::Null);
1571
1790
  let arr_borrow = arr.borrow();
1572
- for (i, v) in arr_borrow.iter().enumerate() {
1791
+ // fromIndex: positive clamps to [0, len]; negative counts from the end.
1792
+ let len = arr_borrow.len() as i64;
1793
+ let start = match arg_vals.get(1) {
1794
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
1795
+ Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
1796
+ _ => 0,
1797
+ };
1798
+ for (i, v) in arr_borrow.iter().enumerate().skip(start) {
1573
1799
  if v.strict_eq(&search) {
1574
1800
  return Ok(Value::Number(i as f64));
1575
1801
  }
1576
1802
  }
1577
1803
  return Ok(Value::Number(-1.0));
1578
1804
  }
1805
+ "lastIndexOf" => {
1806
+ let search = arg_vals.first().cloned().unwrap_or(Value::Null);
1807
+ let arr_borrow = arr.borrow();
1808
+ let len = arr_borrow.len() as i64;
1809
+ let start = match arg_vals.get(1) {
1810
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
1811
+ Some(Value::Number(n)) => len + *n as i64,
1812
+ _ => len - 1,
1813
+ };
1814
+ let mut i = start;
1815
+ while i >= 0 {
1816
+ if let Some(v) = arr_borrow.get(i as usize) {
1817
+ if v.strict_eq(&search) {
1818
+ return Ok(Value::Number(i as f64));
1819
+ }
1820
+ }
1821
+ i -= 1;
1822
+ }
1823
+ return Ok(Value::Number(-1.0));
1824
+ }
1825
+ "copyWithin" => {
1826
+ let mut v = arr.borrow_mut();
1827
+ let len = v.len() as i64;
1828
+ let norm = |x: Option<&Value>, dflt: i64| -> usize {
1829
+ match x {
1830
+ Some(Value::Number(n)) => {
1831
+ let n = *n as i64;
1832
+ (if n < 0 { len + n } else { n }).clamp(0, len) as usize
1833
+ }
1834
+ _ => dflt.clamp(0, len) as usize,
1835
+ }
1836
+ };
1837
+ let t = norm(arg_vals.first(), 0);
1838
+ let s = norm(arg_vals.get(1), 0);
1839
+ let e = norm(arg_vals.get(2), len);
1840
+ let lu = len as usize;
1841
+ if s < e && t < lu {
1842
+ let count = (e - s).min(lu - t);
1843
+ let block: Vec<Value> = v[s..s + count].to_vec();
1844
+ for (k, val) in block.into_iter().enumerate() {
1845
+ v[t + k] = val;
1846
+ }
1847
+ }
1848
+ drop(v);
1849
+ return Ok(Value::Array(arr.clone()));
1850
+ }
1579
1851
  "includes" => {
1580
1852
  let search = arg_vals.first().cloned().unwrap_or(Value::Null);
1581
1853
  let arr_borrow = arr.borrow();
@@ -1617,6 +1889,91 @@ impl Evaluator {
1617
1889
  arr.borrow_mut().reverse();
1618
1890
  return Ok(obj.clone());
1619
1891
  }
1892
+ "toReversed" => {
1893
+ // ES2023: a reversed COPY; the receiver is untouched.
1894
+ let mut v = arr.borrow().clone();
1895
+ v.reverse();
1896
+ return Ok(Value::Array(Rc::new(RefCell::new(v))));
1897
+ }
1898
+ "toSorted" => {
1899
+ // ES2023: a sorted COPY. Uses the general comparator/default path (the
1900
+ // numeric fast-paths in `sort` are speed-only and give identical order).
1901
+ let comparator = arg_vals.into_iter().next();
1902
+ let arr_values: Vec<Value> = arr.borrow().clone();
1903
+ let len = arr_values.len();
1904
+ let mut indices: Vec<usize> = (0..len).collect();
1905
+ if let Some(cmp_fn) = comparator {
1906
+ let mut pending: Option<EvalError> = None;
1907
+ indices.sort_by(|&i, &j| {
1908
+ if pending.is_some() {
1909
+ return std::cmp::Ordering::Equal;
1910
+ }
1911
+ match self.call_func(&cmp_fn, &[arr_values[i].clone(), arr_values[j].clone()]) {
1912
+ Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
1913
+ Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
1914
+ Ok(_) => std::cmp::Ordering::Equal,
1915
+ Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
1916
+ }
1917
+ });
1918
+ if let Some(e) = pending {
1919
+ return Err(e);
1920
+ }
1921
+ } else {
1922
+ indices.sort_by(|&i, &j| {
1923
+ arr_values[i].to_string().cmp(&arr_values[j].to_string())
1924
+ });
1925
+ }
1926
+ let sorted: Vec<Value> =
1927
+ indices.into_iter().map(|i| arr_values[i].clone()).collect();
1928
+ return Ok(Value::Array(Rc::new(RefCell::new(sorted))));
1929
+ }
1930
+ "with" => {
1931
+ // ES2023: a COPY with one index replaced; negative index counts from the
1932
+ // end; out-of-range throws a catchable RangeError (matches vm/native/node).
1933
+ let index = arg_vals.first().cloned().unwrap_or(Value::Null);
1934
+ let value = arg_vals.get(1).cloned().unwrap_or(Value::Null);
1935
+ let mut v = arr.borrow().clone();
1936
+ let len = v.len() as i64;
1937
+ let rel = match index {
1938
+ Value::Number(n) if n.is_finite() => n as i64,
1939
+ _ => 0,
1940
+ };
1941
+ let actual = if rel >= 0 { rel } else { len + rel };
1942
+ if actual < 0 || actual >= len {
1943
+ let err = crate::natives::range_error_construct(&[Value::String(
1944
+ format!("Invalid index : {}", rel).into(),
1945
+ )])
1946
+ .unwrap_or(Value::Null);
1947
+ return Err(EvalError::Throw(err));
1948
+ }
1949
+ v[actual as usize] = value;
1950
+ return Ok(Value::Array(Rc::new(RefCell::new(v))));
1951
+ }
1952
+ "toSpliced" => {
1953
+ // ES2023: a COPY with the splice applied; returns the resulting array
1954
+ // (splice returns the removed elements). Same normalization as `splice`.
1955
+ let mut result = arr.borrow().clone();
1956
+ let len = result.len() as i64;
1957
+ let start = match arg_vals.first() {
1958
+ Some(Value::Number(n)) => {
1959
+ let n = *n as i64;
1960
+ if n < 0 { (len + n).max(0) as usize } else { n.min(len) as usize }
1961
+ }
1962
+ _ => 0,
1963
+ };
1964
+ let delete_count = match arg_vals.get(1) {
1965
+ Some(Value::Number(n)) => (*n as i64).max(0) as usize,
1966
+ _ => (len as usize).saturating_sub(start),
1967
+ };
1968
+ let actual_delete = delete_count.min(result.len().saturating_sub(start));
1969
+ result.drain(start..start + actual_delete);
1970
+ if arg_vals.len() > 2 {
1971
+ for (i, item) in arg_vals[2..].iter().cloned().enumerate() {
1972
+ result.insert(start + i, item);
1973
+ }
1974
+ }
1975
+ return Ok(Value::Array(Rc::new(RefCell::new(result))));
1976
+ }
1620
1977
  "fill" => {
1621
1978
  // Array.prototype.fill(value, start?, end?) — in place (issue #76).
1622
1979
  let value = arg_vals.first().cloned().unwrap_or(Value::Null);
@@ -1912,6 +2269,62 @@ impl Evaluator {
1912
2269
  }
1913
2270
  return Ok(acc);
1914
2271
  }
2272
+ "reduceRight" => {
2273
+ let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
2274
+ let arr_borrow = arr.borrow().clone();
2275
+ let arr_value = Value::Array(arr.clone());
2276
+ let len = arr_borrow.len();
2277
+ // With an initial value, fold indices len-1..0; without, seed from the
2278
+ // last element and fold len-2..0. Empty + no init → TypeError.
2279
+ let (mut acc, start) = if arg_vals.len() > 1 {
2280
+ (arg_vals[1].clone(), len)
2281
+ } else if len > 0 {
2282
+ (arr_borrow[len - 1].clone(), len - 1)
2283
+ } else {
2284
+ return Err(EvalError::Error("Reduce of empty array with no initial value".to_string()));
2285
+ };
2286
+ let mut i = start;
2287
+ while i > 0 {
2288
+ i -= 1;
2289
+ acc = self.call_func(&callback, &[acc, arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()])?;
2290
+ }
2291
+ return Ok(acc);
2292
+ }
2293
+ "keys" => {
2294
+ let len = arr.borrow().len();
2295
+ let it = tishlang_builtins::iterator::array_iterator(
2296
+ (0..len).map(|i| tishlang_core::Value::Number(i as f64)).collect(),
2297
+ );
2298
+ return Ok(crate::value_convert::core_to_eval(it));
2299
+ }
2300
+ "values" => {
2301
+ let items: Vec<tishlang_core::Value> = arr
2302
+ .borrow()
2303
+ .iter()
2304
+ .map(|v| {
2305
+ crate::value_convert::eval_to_core(v)
2306
+ .unwrap_or(tishlang_core::Value::Null)
2307
+ })
2308
+ .collect();
2309
+ let it = tishlang_builtins::iterator::array_iterator(items);
2310
+ return Ok(crate::value_convert::core_to_eval(it));
2311
+ }
2312
+ "entries" => {
2313
+ let items: Vec<tishlang_core::Value> = arr
2314
+ .borrow()
2315
+ .iter()
2316
+ .enumerate()
2317
+ .map(|(i, v)| {
2318
+ tishlang_core::Value::Array(tishlang_core::VmRef::new(vec![
2319
+ tishlang_core::Value::Number(i as f64),
2320
+ crate::value_convert::eval_to_core(v)
2321
+ .unwrap_or(tishlang_core::Value::Null),
2322
+ ]))
2323
+ })
2324
+ .collect();
2325
+ let it = tishlang_builtins::iterator::array_iterator(items);
2326
+ return Ok(crate::value_convert::core_to_eval(it));
2327
+ }
1915
2328
  "find" => {
1916
2329
  let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
1917
2330
  let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
@@ -2227,6 +2640,32 @@ impl Evaluator {
2227
2640
  "trim" => {
2228
2641
  return Ok(Value::String(s.trim().into()));
2229
2642
  }
2643
+ "trimStart" => {
2644
+ return Ok(Value::String(s.trim_start().into()));
2645
+ }
2646
+ "trimEnd" => {
2647
+ return Ok(Value::String(s.trim_end().into()));
2648
+ }
2649
+ "normalize" => {
2650
+ let form = match arg_vals.first() {
2651
+ None | Some(Value::Null) => "NFC".to_string(),
2652
+ Some(Value::String(f)) => f.to_string(),
2653
+ Some(v) => v.to_string(),
2654
+ };
2655
+ match tishlang_builtins::string::normalize_form(s, &form) {
2656
+ Some(out) => return Ok(Value::String(out.into())),
2657
+ None => {
2658
+ let err = crate::natives::range_error_construct(&[
2659
+ Value::String(
2660
+ "The normalization form should be one of NFC, NFD, NFKC, NFKD."
2661
+ .into(),
2662
+ ),
2663
+ ])
2664
+ .unwrap_or(Value::Null);
2665
+ return Err(EvalError::Throw(err));
2666
+ }
2667
+ }
2668
+ }
2230
2669
  "toUpperCase" => {
2231
2670
  return Ok(Value::String(s.to_uppercase().into()));
2232
2671
  }
@@ -2238,14 +2677,28 @@ impl Evaluator {
2238
2677
  Some(Value::String(ss)) => ss.as_ref(),
2239
2678
  _ => return Ok(Value::Bool(false)),
2240
2679
  };
2241
- return Ok(Value::Bool(s.starts_with(search)));
2680
+ // position: test as if the string began at char `position`.
2681
+ let pos = match arg_vals.get(1) {
2682
+ Some(Value::Number(n)) if *n > 0.0 => *n as usize,
2683
+ _ => 0,
2684
+ };
2685
+ let byte_start = s.char_indices().nth(pos).map(|(b, _)| b).unwrap_or(s.len());
2686
+ return Ok(Value::Bool(s[byte_start..].starts_with(search)));
2242
2687
  }
2243
2688
  "endsWith" => {
2244
2689
  let search = match arg_vals.first() {
2245
2690
  Some(Value::String(ss)) => ss.as_ref(),
2246
2691
  _ => return Ok(Value::Bool(false)),
2247
2692
  };
2248
- return Ok(Value::Bool(s.ends_with(search)));
2693
+ // endPosition: test as if the string ended at char `endPosition`.
2694
+ let char_count = s.chars().count();
2695
+ let end = match arg_vals.get(1) {
2696
+ Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(char_count),
2697
+ Some(Value::Number(_)) => 0,
2698
+ _ => char_count,
2699
+ };
2700
+ let byte_end = s.char_indices().nth(end).map(|(b, _)| b).unwrap_or(s.len());
2701
+ return Ok(Value::Bool(s[..byte_end].ends_with(search)));
2249
2702
  }
2250
2703
  "replace" => {
2251
2704
  #[cfg(feature = "regex")]
@@ -2285,6 +2738,17 @@ impl Evaluator {
2285
2738
  Some(Value::String(ss)) => ss.to_string(),
2286
2739
  _ => String::new(),
2287
2740
  };
2741
+ if replacement.contains('$') && !search.is_empty() {
2742
+ let sref = s.as_ref();
2743
+ if let Some(m) = sref.find(&search) {
2744
+ let mut out = String::with_capacity(sref.len());
2745
+ out.push_str(&sref[..m]);
2746
+ out.push_str(&tishlang_builtins::string::expand_replacement(
2747
+ &replacement, &search, &sref[..m], &sref[m + search.len()..]));
2748
+ out.push_str(&sref[m + search.len()..]);
2749
+ return Ok(Value::String(out.into()));
2750
+ }
2751
+ }
2288
2752
  return Ok(Value::String(s.replacen(&search, &replacement, 1).into()));
2289
2753
  }
2290
2754
  #[cfg(feature = "regex")]
@@ -2299,6 +2763,21 @@ impl Evaluator {
2299
2763
  Some(Value::String(ss)) => ss.to_string(),
2300
2764
  _ => String::new(),
2301
2765
  };
2766
+ if replacement.contains('$') && !search.is_empty() {
2767
+ let sref = s.as_ref();
2768
+ let mut out = String::with_capacity(sref.len());
2769
+ let (mut last, mut start) = (0usize, 0usize);
2770
+ while let Some(pos) = sref[start..].find(&search) {
2771
+ let m = start + pos;
2772
+ out.push_str(&sref[last..m]);
2773
+ out.push_str(&tishlang_builtins::string::expand_replacement(
2774
+ &replacement, &search, &sref[..m], &sref[m + search.len()..]));
2775
+ last = m + search.len();
2776
+ start = last;
2777
+ }
2778
+ out.push_str(&sref[last..]);
2779
+ return Ok(Value::String(out.into()));
2780
+ }
2302
2781
  return Ok(Value::String(s.replace(&search, &replacement).into()));
2303
2782
  }
2304
2783
  "charAt" => {
@@ -2334,6 +2813,35 @@ impl Evaluator {
2334
2813
  .map(|c| Value::Number(c as u32 as f64))
2335
2814
  .unwrap_or(Value::Number(f64::NAN)));
2336
2815
  }
2816
+ "codePointAt" => {
2817
+ let idx = match arg_vals.first() {
2818
+ Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
2819
+ _ => return Ok(Value::Null),
2820
+ };
2821
+ return Ok(nth_char_cached(s, idx)
2822
+ .map(|c| Value::Number(c as u32 as f64))
2823
+ .unwrap_or(Value::Null));
2824
+ }
2825
+ "substr" => {
2826
+ // (interp lacked substr; vm/native had it — this closes the gap.)
2827
+ let chars: Vec<char> = s.chars().collect();
2828
+ let len = chars.len() as i64;
2829
+ let mut start = match arg_vals.first() {
2830
+ Some(Value::Number(n)) => *n as i64,
2831
+ _ => 0,
2832
+ };
2833
+ if start < 0 {
2834
+ start = (len + start).max(0);
2835
+ }
2836
+ let start = (start as usize).min(chars.len());
2837
+ let count = match arg_vals.get(1) {
2838
+ Some(Value::Number(n)) => (*n as i64).max(0) as usize,
2839
+ _ => chars.len() - start,
2840
+ };
2841
+ let out: String =
2842
+ chars[start..(start + count).min(chars.len())].iter().collect();
2843
+ return Ok(Value::String(out.into()));
2844
+ }
2337
2845
  "repeat" => {
2338
2846
  let count = match arg_vals.first() {
2339
2847
  Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
@@ -2383,6 +2891,35 @@ impl Evaluator {
2383
2891
  return Ok(Value::Null);
2384
2892
  }
2385
2893
  #[cfg(feature = "regex")]
2894
+ "matchAll" => {
2895
+ let regexp = arg_vals.first().cloned().unwrap_or(Value::Null);
2896
+ match crate::regex::string_match_all(s, &regexp) {
2897
+ Ok(matches) => {
2898
+ let core_matches: Vec<tishlang_core::Value> = matches
2899
+ .iter()
2900
+ .map(|m| {
2901
+ crate::value_convert::eval_to_core(m)
2902
+ .unwrap_or(tishlang_core::Value::Null)
2903
+ })
2904
+ .collect();
2905
+ let it = tishlang_builtins::iterator::array_iterator(
2906
+ core_matches,
2907
+ );
2908
+ return Ok(crate::value_convert::core_to_eval(it));
2909
+ }
2910
+ Err(()) => {
2911
+ let err = crate::natives::type_error_construct(&[
2912
+ Value::String(
2913
+ "String.prototype.matchAll called with a non-global RegExp argument"
2914
+ .into(),
2915
+ ),
2916
+ ])
2917
+ .unwrap_or(Value::Null);
2918
+ return Err(EvalError::Throw(err));
2919
+ }
2920
+ }
2921
+ }
2922
+ #[cfg(feature = "regex")]
2386
2923
  "search" => {
2387
2924
  if let Some(regexp) = arg_vals.first() {
2388
2925
  return Ok(crate::regex::string_search(s, regexp));
@@ -2408,6 +2945,40 @@ impl Evaluator {
2408
2945
  let formatted = tishlang_builtins::number::to_fixed_str(*n, digits as usize);
2409
2946
  return Ok(Value::String(formatted.into()));
2410
2947
  }
2948
+ if method_name.as_ref() == "toExponential" {
2949
+ let digits = arg_vals.first().and_then(|v| match v {
2950
+ Value::Number(d) => Some((*d as i32).clamp(0, 100) as usize),
2951
+ _ => None,
2952
+ });
2953
+ return Ok(Value::String(
2954
+ tishlang_builtins::number::to_exponential_str(*n, digits).into(),
2955
+ ));
2956
+ }
2957
+ if method_name.as_ref() == "toPrecision" {
2958
+ match arg_vals.first() {
2959
+ Some(Value::Number(p)) => {
2960
+ let pi = *p as i32;
2961
+ if !(1..=100).contains(&pi) && n.is_finite() {
2962
+ let err = crate::natives::range_error_construct(&[
2963
+ Value::String(
2964
+ "toPrecision() argument must be between 1 and 100"
2965
+ .into(),
2966
+ ),
2967
+ ])
2968
+ .unwrap_or(Value::Null);
2969
+ return Err(EvalError::Throw(err));
2970
+ }
2971
+ return Ok(Value::String(
2972
+ tishlang_builtins::number::to_precision_str(*n, pi).into(),
2973
+ ));
2974
+ }
2975
+ _ => {
2976
+ return Ok(Value::String(
2977
+ tishlang_core::js_number_to_string(*n).into(),
2978
+ ))
2979
+ }
2980
+ }
2981
+ }
2411
2982
  if method_name.as_ref() == "toString" {
2412
2983
  // Shares the VM/native formatting via the backend-agnostic helper
2413
2984
  // (issue #59). Radix defaults to 10; 2–36 supported, else RangeError.
@@ -2821,6 +3392,12 @@ impl Evaluator {
2821
3392
  Value::Array(arr) => {
2822
3393
  let idx = match &idx_val {
2823
3394
  Value::Number(n) => *n as usize,
3395
+ // Canonical integer string key → index (#432); a non-index string key
3396
+ // can't live in a Vec-backed array, so the write is a no-op.
3397
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
3398
+ Some(i) => i,
3399
+ None => return Ok(val),
3400
+ },
2824
3401
  _ => return Err(EvalError::Error(format!(
2825
3402
  "Array index must be a number, got {:?}",
2826
3403
  idx_val
@@ -3331,7 +3908,31 @@ impl Evaluator {
3331
3908
  }
3332
3909
  Err(EvalError::Error("Not a function".to_string()))
3333
3910
  }
3334
- Value::Native(native_fn) => native_fn(args).map_err(EvalError::Error),
3911
+ Value::Native(native_fn) => {
3912
+ // #122: Node fs callback form — `readFile(path, (err, data) => …)`. The self-less
3913
+ // `Value::Native` bridge can't invoke an eval callback (a core `Callable` can't
3914
+ // re-enter the interpreter), so handle it here where `&self` is available: run the
3915
+ // op's core on the non-callback args, split its Result into `(err, data)`, and call
3916
+ // the callback. Matches the VM/native backends (fs_ext runs the callback in-core).
3917
+ #[cfg(feature = "fs")]
3918
+ if let Some(cb @ Value::Function { .. }) = args.last() {
3919
+ if let Some(core_op) = natives::fsx::callback_core(*native_fn) {
3920
+ let cb = cb.clone();
3921
+ let core_args = args[..args.len() - 1]
3922
+ .iter()
3923
+ .map(crate::value_convert::eval_to_core)
3924
+ .collect::<Result<Vec<_>, String>>()
3925
+ .map_err(EvalError::Error)?;
3926
+ let (err, data) = match core_op(&core_args) {
3927
+ Ok(v) => (Value::Null, crate::value_convert::core_to_eval(v)),
3928
+ Err(e) => (crate::value_convert::core_to_eval(e), Value::Null),
3929
+ };
3930
+ self.call_func(&cb, &[err, data])?;
3931
+ return Ok(Value::Null);
3932
+ }
3933
+ }
3934
+ native_fn(args).map_err(EvalError::Error)
3935
+ }
3335
3936
  #[cfg(feature = "http")]
3336
3937
  Value::PromiseResolver(r) => {
3337
3938
  let value = args.first().cloned().unwrap_or(Value::Null);
@@ -3959,6 +4560,15 @@ impl Evaluator {
3959
4560
  /// `.entries()` — into a `Vec` by calling `next()` until `done`. Returns `None` when `v`
3960
4561
  /// is not such an object. Shared by `for…of` and spread so both treat iterators like JS.
3961
4562
  fn drain_eval_iterator(&self, v: &Value) -> Option<Vec<Value>> {
4563
+ // A string spreads as its characters (`[...s]`, `f(...s)`); for-of over a string is handled
4564
+ // separately at the loop site, but spread routes through here. (#440)
4565
+ if let Value::String(s) = v {
4566
+ return Some(
4567
+ s.chars()
4568
+ .map(|c| Value::String(std::sync::Arc::from(c.to_string())))
4569
+ .collect(),
4570
+ );
4571
+ }
3962
4572
  if !matches!(v, Value::Object(_)) {
3963
4573
  return None;
3964
4574
  }
@@ -4085,6 +4695,12 @@ impl Evaluator {
4085
4695
  Value::Array(arr) => {
4086
4696
  let idx = match index {
4087
4697
  Value::Number(n) => *n as usize,
4698
+ // A canonical integer string key ("0","1",…) indexes the array (#432), so
4699
+ // `arr["0"] === arr[0]`; any other key reads Null.
4700
+ Value::String(s) => match tishlang_core::str_to_array_index(s) {
4701
+ Some(i) => i,
4702
+ None => return Ok(Value::Null),
4703
+ },
4088
4704
  _ => return Ok(Value::Null),
4089
4705
  };
4090
4706
  Ok(arr.borrow().get(idx).cloned().unwrap_or(Value::Null))
@@ -4148,14 +4764,16 @@ impl Evaluator {
4148
4764
  tishlang_core::write_json_number(&mut s, *n);
4149
4765
  s
4150
4766
  }
4151
- Value::String(s) => format!(
4152
- "\"{}\"",
4153
- s.replace('\\', "\\\\")
4154
- .replace('"', "\\\"")
4155
- .replace('\n', "\\n")
4156
- .replace('\r', "\\r")
4157
- .replace('\t', "\\t")
4158
- ),
4767
+ Value::String(s) => {
4768
+ // Use the shared core escaper so control chars (`\b`, `\f`, `\u00xx`) are escaped
4769
+ // exactly like the vm/native path — the old hand-rolled `.replace()` chain missed
4770
+ // them and produced invalid JSON (raw control bytes). #437
4771
+ let mut out = String::with_capacity(s.len() + 2);
4772
+ out.push('"');
4773
+ tishlang_core::escape_json_string_into(&mut out, s);
4774
+ out.push('"');
4775
+ out
4776
+ }
4159
4777
  Value::Array(arr) => {
4160
4778
  let ptr = Rc::as_ptr(arr) as *const ();
4161
4779
  if ancestors.contains(&ptr) {
@@ -4229,16 +4847,26 @@ impl Evaluator {
4229
4847
  }
4230
4848
 
4231
4849
  fn object_keys(args: &[Value]) -> Result<Value, String> {
4232
- if let Some(Value::Object(obj)) = args.first() {
4233
- let keys: Vec<Value> = obj
4234
- .borrow()
4235
- .strings
4236
- .keys()
4237
- .map(|k| Value::String(Arc::clone(k)))
4238
- .collect();
4239
- Ok(Value::Array(Rc::new(RefCell::new(keys))))
4240
- } else {
4241
- Ok(Value::Array(Rc::new(RefCell::new(Vec::new()))))
4850
+ match args.first() {
4851
+ Some(Value::Object(obj)) => {
4852
+ let keys: Vec<Value> = obj
4853
+ .borrow()
4854
+ .strings
4855
+ .keys()
4856
+ .map(|k| Value::String(Arc::clone(k)))
4857
+ .collect();
4858
+ Ok(Value::Array(Rc::new(RefCell::new(keys))))
4859
+ }
4860
+ // `Object.keys(array)` → index strings "0".."len-1" (own enumerable keys are the indices),
4861
+ // matching node and the shared builtin.
4862
+ Some(Value::Array(arr)) => {
4863
+ let len = arr.borrow().len();
4864
+ let keys: Vec<Value> = (0..len)
4865
+ .map(|i| Value::String(Arc::from(i.to_string())))
4866
+ .collect();
4867
+ Ok(Value::Array(Rc::new(RefCell::new(keys))))
4868
+ }
4869
+ _ => Ok(Value::Array(Rc::new(RefCell::new(Vec::new())))),
4242
4870
  }
4243
4871
  }
4244
4872