@tishlang/tish-format 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.
- 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/fs_parity.rs +103 -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 +513 -44
- 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 +655 -27
- package/crates/tish_eval/src/natives.rs +264 -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 +128 -9
- 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 +270 -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
|
@@ -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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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, ®exp) {
|
|
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) =>
|
|
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) =>
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
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
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
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
|
|