@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
|
@@ -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));
|
|
@@ -312,6 +320,8 @@ impl Evaluator {
|
|
|
312
320
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
313
321
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
314
322
|
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
323
|
+
process_obj.insert("execCapture".into(), Value::Native(natives::process_exec_capture));
|
|
324
|
+
process_obj.insert("execFileCapture".into(), Value::Native(natives::process_exec_file_capture));
|
|
315
325
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
316
326
|
.into_iter()
|
|
317
327
|
.map(|s| Value::String(s.into()))
|
|
@@ -329,6 +339,10 @@ impl Evaluator {
|
|
|
329
339
|
object.insert("values".into(), Value::Native(Self::object_values));
|
|
330
340
|
object.insert("entries".into(), Value::Native(Self::object_entries));
|
|
331
341
|
object.insert("assign".into(), Value::Native(Self::object_assign));
|
|
342
|
+
object.insert("is".into(), Value::Native(natives::object_is));
|
|
343
|
+
object.insert("hasOwn".into(), Value::Native(natives::object_has_own));
|
|
344
|
+
// getOwnPropertyNames ≈ keys for tish's plain objects (own string keys, insertion order).
|
|
345
|
+
object.insert("getOwnPropertyNames".into(), Value::Native(Self::object_keys));
|
|
332
346
|
object.insert(
|
|
333
347
|
"fromEntries".into(),
|
|
334
348
|
Value::Native(Self::object_from_entries),
|
|
@@ -341,6 +355,7 @@ impl Evaluator {
|
|
|
341
355
|
|
|
342
356
|
let mut array_obj = PropMap::with_capacity(3);
|
|
343
357
|
array_obj.insert("isArray".into(), Value::Native(natives::array_is_array));
|
|
358
|
+
array_obj.insert("of".into(), Value::Native(natives::array_of));
|
|
344
359
|
// `Array(n)` and `new Array(n)` constructor (issue #72).
|
|
345
360
|
array_obj.insert("__call".into(), Value::Native(natives::array_construct));
|
|
346
361
|
array_obj.insert("__construct".into(), Value::Native(natives::array_construct));
|
|
@@ -376,9 +391,23 @@ impl Evaluator {
|
|
|
376
391
|
true,
|
|
377
392
|
);
|
|
378
393
|
|
|
379
|
-
// `Number(value)` coercion as a callable global (issue #36).
|
|
380
|
-
let mut number_obj = PropMap::with_capacity(
|
|
394
|
+
// `Number(value)` coercion as a callable global (issue #36) + the `Number.*` statics.
|
|
395
|
+
let mut number_obj = PropMap::with_capacity(15);
|
|
381
396
|
number_obj.insert("__call".into(), Value::Native(natives::number_convert));
|
|
397
|
+
number_obj.insert("isInteger".into(), Value::Native(natives::number_is_integer));
|
|
398
|
+
number_obj.insert("isSafeInteger".into(), Value::Native(natives::number_is_safe_integer));
|
|
399
|
+
number_obj.insert("isNaN".into(), Value::Native(natives::number_is_nan));
|
|
400
|
+
number_obj.insert("isFinite".into(), Value::Native(natives::number_is_finite));
|
|
401
|
+
number_obj.insert("parseInt".into(), Value::Native(natives::parse_int));
|
|
402
|
+
number_obj.insert("parseFloat".into(), Value::Native(natives::parse_float));
|
|
403
|
+
number_obj.insert("MAX_SAFE_INTEGER".into(), Value::Number(9_007_199_254_740_991.0));
|
|
404
|
+
number_obj.insert("MIN_SAFE_INTEGER".into(), Value::Number(-9_007_199_254_740_991.0));
|
|
405
|
+
number_obj.insert("EPSILON".into(), Value::Number(f64::EPSILON));
|
|
406
|
+
number_obj.insert("MAX_VALUE".into(), Value::Number(f64::MAX));
|
|
407
|
+
number_obj.insert("MIN_VALUE".into(), Value::Number(5e-324));
|
|
408
|
+
number_obj.insert("POSITIVE_INFINITY".into(), Value::Number(f64::INFINITY));
|
|
409
|
+
number_obj.insert("NEGATIVE_INFINITY".into(), Value::Number(f64::NEG_INFINITY));
|
|
410
|
+
number_obj.insert("NaN".into(), Value::Number(f64::NAN));
|
|
382
411
|
s.set("Number".into(), Value::object(number_obj), true);
|
|
383
412
|
|
|
384
413
|
s.set(
|
|
@@ -660,6 +689,51 @@ impl Evaluator {
|
|
|
660
689
|
self.scope = outer;
|
|
661
690
|
ret
|
|
662
691
|
}
|
|
692
|
+
Statement::ForIn {
|
|
693
|
+
name,
|
|
694
|
+
object,
|
|
695
|
+
body,
|
|
696
|
+
..
|
|
697
|
+
} => {
|
|
698
|
+
let obj_val = self.eval_expr(object)?;
|
|
699
|
+
// for-in enumerates OWN ENUMERABLE KEYS as strings (JS semantics): objects yield keys
|
|
700
|
+
// in insertion order, arrays yield index strings "0".."len-1", and a non-object
|
|
701
|
+
// (incl. null) yields nothing — a no-op, matching JS `for (k in null)`.
|
|
702
|
+
let keys: Vec<crate::value::Value> = match &obj_val {
|
|
703
|
+
crate::value::Value::Object(obj) => obj
|
|
704
|
+
.borrow()
|
|
705
|
+
.strings
|
|
706
|
+
.keys()
|
|
707
|
+
.map(|k| crate::value::Value::String(Arc::clone(k)))
|
|
708
|
+
.collect(),
|
|
709
|
+
crate::value::Value::Array(arr) => {
|
|
710
|
+
let len = arr.borrow().len();
|
|
711
|
+
(0..len)
|
|
712
|
+
.map(|i| crate::value::Value::String(Arc::from(i.to_string())))
|
|
713
|
+
.collect()
|
|
714
|
+
}
|
|
715
|
+
_ => Vec::new(),
|
|
716
|
+
};
|
|
717
|
+
// Fresh per-iteration binding (ES6 `for (let k in …)`), like the for-of arm above.
|
|
718
|
+
let outer = Rc::clone(&self.scope);
|
|
719
|
+
let mut ret = Ok(Value::Null);
|
|
720
|
+
for key in keys {
|
|
721
|
+
let iter_env = Scope::child(Rc::clone(&outer));
|
|
722
|
+
iter_env.borrow_mut().set(Arc::clone(name), key, true);
|
|
723
|
+
self.scope = Rc::clone(&iter_env);
|
|
724
|
+
match self.eval_statement(body) {
|
|
725
|
+
Ok(_) => {}
|
|
726
|
+
Err(EvalError::Break) => break,
|
|
727
|
+
Err(EvalError::Continue) => continue,
|
|
728
|
+
Err(e) => {
|
|
729
|
+
ret = Err(e);
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
self.scope = outer;
|
|
735
|
+
ret
|
|
736
|
+
}
|
|
663
737
|
Statement::For {
|
|
664
738
|
init,
|
|
665
739
|
cond,
|
|
@@ -948,7 +1022,7 @@ impl Evaluator {
|
|
|
948
1022
|
ExportDeclaration::ReExport {
|
|
949
1023
|
specifiers,
|
|
950
1024
|
all,
|
|
951
|
-
from,
|
|
1025
|
+
from: Some(from),
|
|
952
1026
|
..
|
|
953
1027
|
} => {
|
|
954
1028
|
let dep_val = self.load_module(from.as_ref())?;
|
|
@@ -971,6 +1045,11 @@ impl Evaluator {
|
|
|
971
1045
|
}
|
|
972
1046
|
}
|
|
973
1047
|
}
|
|
1048
|
+
// #415 local named export (`export { a, b as c }`, no `from`): a no-op in the
|
|
1049
|
+
// running (entry) program — the bindings already exist, and an export alias is
|
|
1050
|
+
// visible only to importers, never as a local variable (JS semantics). The module
|
|
1051
|
+
// export table is populated by the module-evaluation path, not here.
|
|
1052
|
+
ExportDeclaration::ReExport { from: None, .. } => {}
|
|
974
1053
|
}
|
|
975
1054
|
Ok(Value::Null)
|
|
976
1055
|
}
|
|
@@ -1041,7 +1120,7 @@ impl Evaluator {
|
|
|
1041
1120
|
ExportDeclaration::ReExport {
|
|
1042
1121
|
specifiers,
|
|
1043
1122
|
all,
|
|
1044
|
-
from,
|
|
1123
|
+
from: Some(from),
|
|
1045
1124
|
..
|
|
1046
1125
|
} => {
|
|
1047
1126
|
let dep_val = self.load_module(from.as_ref())?;
|
|
@@ -1066,6 +1145,24 @@ impl Evaluator {
|
|
|
1066
1145
|
}
|
|
1067
1146
|
}
|
|
1068
1147
|
}
|
|
1148
|
+
// #415 local named export (`export { a, b as c }`, no `from`): the bindings are
|
|
1149
|
+
// already declared in this module's scope; expose each under its export name.
|
|
1150
|
+
ExportDeclaration::ReExport {
|
|
1151
|
+
specifiers,
|
|
1152
|
+
from: None,
|
|
1153
|
+
..
|
|
1154
|
+
} => {
|
|
1155
|
+
for spec in specifiers {
|
|
1156
|
+
if let ImportSpecifier::Named { name, alias, .. } = spec {
|
|
1157
|
+
let v = self.scope.borrow().get(name.as_ref()).ok_or_else(|| {
|
|
1158
|
+
EvalError::Error(format!("Export '{}' is not defined", name))
|
|
1159
|
+
})?;
|
|
1160
|
+
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
1161
|
+
self.scope.borrow_mut().set(Arc::from(bind), v.clone(), false);
|
|
1162
|
+
export_names.push(bind.to_string());
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1069
1166
|
}
|
|
1070
1167
|
} else {
|
|
1071
1168
|
let _ = self.eval_statement(stmt);
|
|
@@ -1132,6 +1229,44 @@ impl Evaluator {
|
|
|
1132
1229
|
Value::Native(natives::read_file_bytes),
|
|
1133
1230
|
);
|
|
1134
1231
|
exports.insert("mkdir".into(), Value::Native(natives::mkdir));
|
|
1232
|
+
// Node-compatible names + new methods (sync) — issue #122
|
|
1233
|
+
use natives::fsx;
|
|
1234
|
+
exports.insert("readFileSync".into(), Value::Native(fsx::read_file));
|
|
1235
|
+
exports.insert("readFileBytesSync".into(), Value::Native(fsx::read_file_bytes));
|
|
1236
|
+
exports.insert("writeFileSync".into(), Value::Native(fsx::write_file));
|
|
1237
|
+
exports.insert("appendFile".into(), Value::Native(fsx::append_file));
|
|
1238
|
+
exports.insert("appendFileSync".into(), Value::Native(fsx::append_file));
|
|
1239
|
+
exports.insert("existsSync".into(), Value::Native(fsx::exists));
|
|
1240
|
+
exports.insert("mkdirSync".into(), Value::Native(fsx::mkdir));
|
|
1241
|
+
exports.insert("readdir".into(), Value::Native(fsx::readdir));
|
|
1242
|
+
exports.insert("readdirSync".into(), Value::Native(fsx::readdir));
|
|
1243
|
+
exports.insert("stat".into(), Value::Native(fsx::stat));
|
|
1244
|
+
exports.insert("statSync".into(), Value::Native(fsx::stat));
|
|
1245
|
+
exports.insert("lstat".into(), Value::Native(fsx::lstat));
|
|
1246
|
+
exports.insert("lstatSync".into(), Value::Native(fsx::lstat));
|
|
1247
|
+
exports.insert("rm".into(), Value::Native(fsx::rm));
|
|
1248
|
+
exports.insert("rmSync".into(), Value::Native(fsx::rm));
|
|
1249
|
+
exports.insert("rmdir".into(), Value::Native(fsx::rmdir));
|
|
1250
|
+
exports.insert("rmdirSync".into(), Value::Native(fsx::rmdir));
|
|
1251
|
+
exports.insert("unlink".into(), Value::Native(fsx::unlink));
|
|
1252
|
+
exports.insert("unlinkSync".into(), Value::Native(fsx::unlink));
|
|
1253
|
+
exports.insert("rename".into(), Value::Native(fsx::rename));
|
|
1254
|
+
exports.insert("renameSync".into(), Value::Native(fsx::rename));
|
|
1255
|
+
exports.insert("copyFile".into(), Value::Native(fsx::copy_file));
|
|
1256
|
+
exports.insert("copyFileSync".into(), Value::Native(fsx::copy_file));
|
|
1257
|
+
exports.insert("realpath".into(), Value::Native(fsx::realpath));
|
|
1258
|
+
exports.insert("realpathSync".into(), Value::Native(fsx::realpath));
|
|
1259
|
+
exports.insert("readlink".into(), Value::Native(fsx::readlink));
|
|
1260
|
+
exports.insert("readlinkSync".into(), Value::Native(fsx::readlink));
|
|
1261
|
+
exports.insert("truncate".into(), Value::Native(fsx::truncate));
|
|
1262
|
+
exports.insert("truncateSync".into(), Value::Native(fsx::truncate));
|
|
1263
|
+
exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp));
|
|
1264
|
+
exports.insert("mkdtempSync".into(), Value::Native(fsx::mkdtemp));
|
|
1265
|
+
exports.insert("cp".into(), Value::Native(fsx::cp));
|
|
1266
|
+
exports.insert("cpSync".into(), Value::Native(fsx::cp));
|
|
1267
|
+
exports.insert("access".into(), Value::Native(fsx::access));
|
|
1268
|
+
exports.insert("accessSync".into(), Value::Native(fsx::access));
|
|
1269
|
+
exports.insert("constants".into(), fsx::constants());
|
|
1135
1270
|
Ok(Value::object(exports))
|
|
1136
1271
|
}
|
|
1137
1272
|
#[cfg(not(feature = "fs"))]
|
|
@@ -1141,6 +1276,41 @@ impl Evaluator {
|
|
|
1141
1276
|
));
|
|
1142
1277
|
}
|
|
1143
1278
|
}
|
|
1279
|
+
"tish:fs/promises" => {
|
|
1280
|
+
#[cfg(feature = "fs")]
|
|
1281
|
+
{
|
|
1282
|
+
use natives::fsx;
|
|
1283
|
+
let mut exports: PropMap = PropMap::default();
|
|
1284
|
+
exports.insert("readFile".into(), Value::Native(fsx::read_file_p));
|
|
1285
|
+
exports.insert("readFileBytes".into(), Value::Native(fsx::read_file_bytes_p));
|
|
1286
|
+
exports.insert("writeFile".into(), Value::Native(fsx::write_file_p));
|
|
1287
|
+
exports.insert("appendFile".into(), Value::Native(fsx::append_file_p));
|
|
1288
|
+
exports.insert("exists".into(), Value::Native(fsx::exists_p));
|
|
1289
|
+
exports.insert("mkdir".into(), Value::Native(fsx::mkdir_p));
|
|
1290
|
+
exports.insert("readdir".into(), Value::Native(fsx::readdir_p));
|
|
1291
|
+
exports.insert("stat".into(), Value::Native(fsx::stat_p));
|
|
1292
|
+
exports.insert("lstat".into(), Value::Native(fsx::lstat_p));
|
|
1293
|
+
exports.insert("rm".into(), Value::Native(fsx::rm_p));
|
|
1294
|
+
exports.insert("rmdir".into(), Value::Native(fsx::rmdir_p));
|
|
1295
|
+
exports.insert("unlink".into(), Value::Native(fsx::unlink_p));
|
|
1296
|
+
exports.insert("rename".into(), Value::Native(fsx::rename_p));
|
|
1297
|
+
exports.insert("copyFile".into(), Value::Native(fsx::copy_file_p));
|
|
1298
|
+
exports.insert("realpath".into(), Value::Native(fsx::realpath_p));
|
|
1299
|
+
exports.insert("readlink".into(), Value::Native(fsx::readlink_p));
|
|
1300
|
+
exports.insert("truncate".into(), Value::Native(fsx::truncate_p));
|
|
1301
|
+
exports.insert("mkdtemp".into(), Value::Native(fsx::mkdtemp_p));
|
|
1302
|
+
exports.insert("cp".into(), Value::Native(fsx::cp_p));
|
|
1303
|
+
exports.insert("access".into(), Value::Native(fsx::access_p));
|
|
1304
|
+
exports.insert("constants".into(), fsx::constants());
|
|
1305
|
+
Ok(Value::object(exports))
|
|
1306
|
+
}
|
|
1307
|
+
#[cfg(not(feature = "fs"))]
|
|
1308
|
+
{
|
|
1309
|
+
return Err(EvalError::Error(
|
|
1310
|
+
"tish:fs/promises requires the fs feature. Rebuild with: cargo build -p tishlang --features fs".into(),
|
|
1311
|
+
));
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1144
1314
|
"tish:http" => {
|
|
1145
1315
|
#[cfg(feature = "http")]
|
|
1146
1316
|
{
|
|
@@ -1244,6 +1414,8 @@ impl Evaluator {
|
|
|
1244
1414
|
exports.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1245
1415
|
exports.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1246
1416
|
exports.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1417
|
+
exports.insert("execCapture".into(), Value::Native(natives::process_exec_capture));
|
|
1418
|
+
exports.insert("execFileCapture".into(), Value::Native(natives::process_exec_file_capture));
|
|
1247
1419
|
let argv: Vec<Value> = tishlang_core::process_argv()
|
|
1248
1420
|
.into_iter()
|
|
1249
1421
|
.map(|s| Value::String(s.into()))
|
|
@@ -1264,6 +1436,8 @@ impl Evaluator {
|
|
|
1264
1436
|
process_obj.insert("cwd".into(), Value::Native(natives::process_cwd));
|
|
1265
1437
|
process_obj.insert("exec".into(), Value::Native(natives::process_exec));
|
|
1266
1438
|
process_obj.insert("execFile".into(), Value::Native(natives::process_exec_file));
|
|
1439
|
+
process_obj.insert("execCapture".into(), Value::Native(natives::process_exec_capture));
|
|
1440
|
+
process_obj.insert("execFileCapture".into(), Value::Native(natives::process_exec_file_capture));
|
|
1267
1441
|
process_obj.insert("argv".into(), Value::Array(Rc::new(RefCell::new(argv))));
|
|
1268
1442
|
process_obj.insert("env".into(), Value::object(env_obj));
|
|
1269
1443
|
exports.insert(
|
|
@@ -1538,7 +1712,58 @@ impl Evaluator {
|
|
|
1538
1712
|
{
|
|
1539
1713
|
let obj = self.eval_expr(object)?;
|
|
1540
1714
|
let arg_vals = self.eval_call_args(args)?;
|
|
1541
|
-
|
|
1715
|
+
|
|
1716
|
+
// `Array.from(source, mapFn?)` — handled inline so the mapFn callback runs with the
|
|
1717
|
+
// interpreter context (a plain native has no `self`). Covers array / string / Set /
|
|
1718
|
+
// Map / iterator (via `__drain__`) and array-like `{ length }`. (#440)
|
|
1719
|
+
if let Expr::Ident { name, .. } = object.as_ref() {
|
|
1720
|
+
if name.as_ref() == "Array" && method_name.as_ref() == "from" {
|
|
1721
|
+
let source = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1722
|
+
let items: Vec<Value> = match &source {
|
|
1723
|
+
Value::Array(a) => a.borrow().clone(),
|
|
1724
|
+
Value::String(s) => s
|
|
1725
|
+
.chars()
|
|
1726
|
+
.map(|c| Value::String(std::sync::Arc::from(c.to_string())))
|
|
1727
|
+
.collect(),
|
|
1728
|
+
_ => {
|
|
1729
|
+
if let Some(d) = self.drain_eval_iterator(&source) {
|
|
1730
|
+
d
|
|
1731
|
+
} else if matches!(&source, Value::Object(_)) {
|
|
1732
|
+
let len = self
|
|
1733
|
+
.get_prop(&source, "length")
|
|
1734
|
+
.ok()
|
|
1735
|
+
.and_then(|v| match v {
|
|
1736
|
+
Value::Number(n) if n.is_finite() && n >= 0.0 => {
|
|
1737
|
+
Some(n as usize)
|
|
1738
|
+
}
|
|
1739
|
+
_ => None,
|
|
1740
|
+
})
|
|
1741
|
+
.unwrap_or(0);
|
|
1742
|
+
(0..len)
|
|
1743
|
+
.map(|i| {
|
|
1744
|
+
self.get_prop(&source, &i.to_string())
|
|
1745
|
+
.unwrap_or(Value::Null)
|
|
1746
|
+
})
|
|
1747
|
+
.collect()
|
|
1748
|
+
} else {
|
|
1749
|
+
Vec::new()
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
};
|
|
1753
|
+
let out = match arg_vals.get(1) {
|
|
1754
|
+
Some(f) if !matches!(f, Value::Null) => {
|
|
1755
|
+
let mut o = Vec::with_capacity(items.len());
|
|
1756
|
+
for (i, item) in items.into_iter().enumerate() {
|
|
1757
|
+
o.push(self.call_func(f, &[item, Value::Number(i as f64)])?);
|
|
1758
|
+
}
|
|
1759
|
+
o
|
|
1760
|
+
}
|
|
1761
|
+
_ => items,
|
|
1762
|
+
};
|
|
1763
|
+
return Ok(Value::array(out));
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1542
1767
|
// Array methods
|
|
1543
1768
|
if let Value::Array(arr) = &obj {
|
|
1544
1769
|
match method_name.as_ref() {
|
|
@@ -1569,13 +1794,66 @@ impl Evaluator {
|
|
|
1569
1794
|
"indexOf" => {
|
|
1570
1795
|
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1571
1796
|
let arr_borrow = arr.borrow();
|
|
1572
|
-
|
|
1797
|
+
// fromIndex: positive clamps to [0, len]; negative counts from the end.
|
|
1798
|
+
let len = arr_borrow.len() as i64;
|
|
1799
|
+
let start = match arg_vals.get(1) {
|
|
1800
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len).max(0) as usize,
|
|
1801
|
+
Some(Value::Number(n)) if *n < 0.0 => ((len + *n as i64).max(0)) as usize,
|
|
1802
|
+
_ => 0,
|
|
1803
|
+
};
|
|
1804
|
+
for (i, v) in arr_borrow.iter().enumerate().skip(start) {
|
|
1573
1805
|
if v.strict_eq(&search) {
|
|
1574
1806
|
return Ok(Value::Number(i as f64));
|
|
1575
1807
|
}
|
|
1576
1808
|
}
|
|
1577
1809
|
return Ok(Value::Number(-1.0));
|
|
1578
1810
|
}
|
|
1811
|
+
"lastIndexOf" => {
|
|
1812
|
+
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1813
|
+
let arr_borrow = arr.borrow();
|
|
1814
|
+
let len = arr_borrow.len() as i64;
|
|
1815
|
+
let start = match arg_vals.get(1) {
|
|
1816
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as i64).min(len - 1),
|
|
1817
|
+
Some(Value::Number(n)) => len + *n as i64,
|
|
1818
|
+
_ => len - 1,
|
|
1819
|
+
};
|
|
1820
|
+
let mut i = start;
|
|
1821
|
+
while i >= 0 {
|
|
1822
|
+
if let Some(v) = arr_borrow.get(i as usize) {
|
|
1823
|
+
if v.strict_eq(&search) {
|
|
1824
|
+
return Ok(Value::Number(i as f64));
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
i -= 1;
|
|
1828
|
+
}
|
|
1829
|
+
return Ok(Value::Number(-1.0));
|
|
1830
|
+
}
|
|
1831
|
+
"copyWithin" => {
|
|
1832
|
+
let mut v = arr.borrow_mut();
|
|
1833
|
+
let len = v.len() as i64;
|
|
1834
|
+
let norm = |x: Option<&Value>, dflt: i64| -> usize {
|
|
1835
|
+
match x {
|
|
1836
|
+
Some(Value::Number(n)) => {
|
|
1837
|
+
let n = *n as i64;
|
|
1838
|
+
(if n < 0 { len + n } else { n }).clamp(0, len) as usize
|
|
1839
|
+
}
|
|
1840
|
+
_ => dflt.clamp(0, len) as usize,
|
|
1841
|
+
}
|
|
1842
|
+
};
|
|
1843
|
+
let t = norm(arg_vals.first(), 0);
|
|
1844
|
+
let s = norm(arg_vals.get(1), 0);
|
|
1845
|
+
let e = norm(arg_vals.get(2), len);
|
|
1846
|
+
let lu = len as usize;
|
|
1847
|
+
if s < e && t < lu {
|
|
1848
|
+
let count = (e - s).min(lu - t);
|
|
1849
|
+
let block: Vec<Value> = v[s..s + count].to_vec();
|
|
1850
|
+
for (k, val) in block.into_iter().enumerate() {
|
|
1851
|
+
v[t + k] = val;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
drop(v);
|
|
1855
|
+
return Ok(Value::Array(arr.clone()));
|
|
1856
|
+
}
|
|
1579
1857
|
"includes" => {
|
|
1580
1858
|
let search = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1581
1859
|
let arr_borrow = arr.borrow();
|
|
@@ -1617,6 +1895,91 @@ impl Evaluator {
|
|
|
1617
1895
|
arr.borrow_mut().reverse();
|
|
1618
1896
|
return Ok(obj.clone());
|
|
1619
1897
|
}
|
|
1898
|
+
"toReversed" => {
|
|
1899
|
+
// ES2023: a reversed COPY; the receiver is untouched.
|
|
1900
|
+
let mut v = arr.borrow().clone();
|
|
1901
|
+
v.reverse();
|
|
1902
|
+
return Ok(Value::Array(Rc::new(RefCell::new(v))));
|
|
1903
|
+
}
|
|
1904
|
+
"toSorted" => {
|
|
1905
|
+
// ES2023: a sorted COPY. Uses the general comparator/default path (the
|
|
1906
|
+
// numeric fast-paths in `sort` are speed-only and give identical order).
|
|
1907
|
+
let comparator = arg_vals.into_iter().next();
|
|
1908
|
+
let arr_values: Vec<Value> = arr.borrow().clone();
|
|
1909
|
+
let len = arr_values.len();
|
|
1910
|
+
let mut indices: Vec<usize> = (0..len).collect();
|
|
1911
|
+
if let Some(cmp_fn) = comparator {
|
|
1912
|
+
let mut pending: Option<EvalError> = None;
|
|
1913
|
+
indices.sort_by(|&i, &j| {
|
|
1914
|
+
if pending.is_some() {
|
|
1915
|
+
return std::cmp::Ordering::Equal;
|
|
1916
|
+
}
|
|
1917
|
+
match self.call_func(&cmp_fn, &[arr_values[i].clone(), arr_values[j].clone()]) {
|
|
1918
|
+
Ok(Value::Number(n)) if n < 0.0 => std::cmp::Ordering::Less,
|
|
1919
|
+
Ok(Value::Number(n)) if n > 0.0 => std::cmp::Ordering::Greater,
|
|
1920
|
+
Ok(_) => std::cmp::Ordering::Equal,
|
|
1921
|
+
Err(e) => { pending = Some(e); std::cmp::Ordering::Equal }
|
|
1922
|
+
}
|
|
1923
|
+
});
|
|
1924
|
+
if let Some(e) = pending {
|
|
1925
|
+
return Err(e);
|
|
1926
|
+
}
|
|
1927
|
+
} else {
|
|
1928
|
+
indices.sort_by(|&i, &j| {
|
|
1929
|
+
arr_values[i].to_string().cmp(&arr_values[j].to_string())
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
let sorted: Vec<Value> =
|
|
1933
|
+
indices.into_iter().map(|i| arr_values[i].clone()).collect();
|
|
1934
|
+
return Ok(Value::Array(Rc::new(RefCell::new(sorted))));
|
|
1935
|
+
}
|
|
1936
|
+
"with" => {
|
|
1937
|
+
// ES2023: a COPY with one index replaced; negative index counts from the
|
|
1938
|
+
// end; out-of-range throws a catchable RangeError (matches vm/native/node).
|
|
1939
|
+
let index = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1940
|
+
let value = arg_vals.get(1).cloned().unwrap_or(Value::Null);
|
|
1941
|
+
let mut v = arr.borrow().clone();
|
|
1942
|
+
let len = v.len() as i64;
|
|
1943
|
+
let rel = match index {
|
|
1944
|
+
Value::Number(n) if n.is_finite() => n as i64,
|
|
1945
|
+
_ => 0,
|
|
1946
|
+
};
|
|
1947
|
+
let actual = if rel >= 0 { rel } else { len + rel };
|
|
1948
|
+
if actual < 0 || actual >= len {
|
|
1949
|
+
let err = crate::natives::range_error_construct(&[Value::String(
|
|
1950
|
+
format!("Invalid index : {}", rel).into(),
|
|
1951
|
+
)])
|
|
1952
|
+
.unwrap_or(Value::Null);
|
|
1953
|
+
return Err(EvalError::Throw(err));
|
|
1954
|
+
}
|
|
1955
|
+
v[actual as usize] = value;
|
|
1956
|
+
return Ok(Value::Array(Rc::new(RefCell::new(v))));
|
|
1957
|
+
}
|
|
1958
|
+
"toSpliced" => {
|
|
1959
|
+
// ES2023: a COPY with the splice applied; returns the resulting array
|
|
1960
|
+
// (splice returns the removed elements). Same normalization as `splice`.
|
|
1961
|
+
let mut result = arr.borrow().clone();
|
|
1962
|
+
let len = result.len() as i64;
|
|
1963
|
+
let start = match arg_vals.first() {
|
|
1964
|
+
Some(Value::Number(n)) => {
|
|
1965
|
+
let n = *n as i64;
|
|
1966
|
+
if n < 0 { (len + n).max(0) as usize } else { n.min(len) as usize }
|
|
1967
|
+
}
|
|
1968
|
+
_ => 0,
|
|
1969
|
+
};
|
|
1970
|
+
let delete_count = match arg_vals.get(1) {
|
|
1971
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
1972
|
+
_ => (len as usize).saturating_sub(start),
|
|
1973
|
+
};
|
|
1974
|
+
let actual_delete = delete_count.min(result.len().saturating_sub(start));
|
|
1975
|
+
result.drain(start..start + actual_delete);
|
|
1976
|
+
if arg_vals.len() > 2 {
|
|
1977
|
+
for (i, item) in arg_vals[2..].iter().cloned().enumerate() {
|
|
1978
|
+
result.insert(start + i, item);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
return Ok(Value::Array(Rc::new(RefCell::new(result))));
|
|
1982
|
+
}
|
|
1620
1983
|
"fill" => {
|
|
1621
1984
|
// Array.prototype.fill(value, start?, end?) — in place (issue #76).
|
|
1622
1985
|
let value = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
@@ -1912,6 +2275,62 @@ impl Evaluator {
|
|
|
1912
2275
|
}
|
|
1913
2276
|
return Ok(acc);
|
|
1914
2277
|
}
|
|
2278
|
+
"reduceRight" => {
|
|
2279
|
+
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
2280
|
+
let arr_borrow = arr.borrow().clone();
|
|
2281
|
+
let arr_value = Value::Array(arr.clone());
|
|
2282
|
+
let len = arr_borrow.len();
|
|
2283
|
+
// With an initial value, fold indices len-1..0; without, seed from the
|
|
2284
|
+
// last element and fold len-2..0. Empty + no init → TypeError.
|
|
2285
|
+
let (mut acc, start) = if arg_vals.len() > 1 {
|
|
2286
|
+
(arg_vals[1].clone(), len)
|
|
2287
|
+
} else if len > 0 {
|
|
2288
|
+
(arr_borrow[len - 1].clone(), len - 1)
|
|
2289
|
+
} else {
|
|
2290
|
+
return Err(EvalError::Error("Reduce of empty array with no initial value".to_string()));
|
|
2291
|
+
};
|
|
2292
|
+
let mut i = start;
|
|
2293
|
+
while i > 0 {
|
|
2294
|
+
i -= 1;
|
|
2295
|
+
acc = self.call_func(&callback, &[acc, arr_borrow[i].clone(), Value::Number(i as f64), arr_value.clone()])?;
|
|
2296
|
+
}
|
|
2297
|
+
return Ok(acc);
|
|
2298
|
+
}
|
|
2299
|
+
"keys" => {
|
|
2300
|
+
let len = arr.borrow().len();
|
|
2301
|
+
let it = tishlang_builtins::iterator::array_iterator(
|
|
2302
|
+
(0..len).map(|i| tishlang_core::Value::Number(i as f64)).collect(),
|
|
2303
|
+
);
|
|
2304
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2305
|
+
}
|
|
2306
|
+
"values" => {
|
|
2307
|
+
let items: Vec<tishlang_core::Value> = arr
|
|
2308
|
+
.borrow()
|
|
2309
|
+
.iter()
|
|
2310
|
+
.map(|v| {
|
|
2311
|
+
crate::value_convert::eval_to_core(v)
|
|
2312
|
+
.unwrap_or(tishlang_core::Value::Null)
|
|
2313
|
+
})
|
|
2314
|
+
.collect();
|
|
2315
|
+
let it = tishlang_builtins::iterator::array_iterator(items);
|
|
2316
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2317
|
+
}
|
|
2318
|
+
"entries" => {
|
|
2319
|
+
let items: Vec<tishlang_core::Value> = arr
|
|
2320
|
+
.borrow()
|
|
2321
|
+
.iter()
|
|
2322
|
+
.enumerate()
|
|
2323
|
+
.map(|(i, v)| {
|
|
2324
|
+
tishlang_core::Value::Array(tishlang_core::VmRef::new(vec![
|
|
2325
|
+
tishlang_core::Value::Number(i as f64),
|
|
2326
|
+
crate::value_convert::eval_to_core(v)
|
|
2327
|
+
.unwrap_or(tishlang_core::Value::Null),
|
|
2328
|
+
]))
|
|
2329
|
+
})
|
|
2330
|
+
.collect();
|
|
2331
|
+
let it = tishlang_builtins::iterator::array_iterator(items);
|
|
2332
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2333
|
+
}
|
|
1915
2334
|
"find" => {
|
|
1916
2335
|
let callback = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
1917
2336
|
let arr_borrow = arr.borrow().clone(); // snapshot; drop borrow before callbacks
|
|
@@ -2227,6 +2646,32 @@ impl Evaluator {
|
|
|
2227
2646
|
"trim" => {
|
|
2228
2647
|
return Ok(Value::String(s.trim().into()));
|
|
2229
2648
|
}
|
|
2649
|
+
"trimStart" => {
|
|
2650
|
+
return Ok(Value::String(s.trim_start().into()));
|
|
2651
|
+
}
|
|
2652
|
+
"trimEnd" => {
|
|
2653
|
+
return Ok(Value::String(s.trim_end().into()));
|
|
2654
|
+
}
|
|
2655
|
+
"normalize" => {
|
|
2656
|
+
let form = match arg_vals.first() {
|
|
2657
|
+
None | Some(Value::Null) => "NFC".to_string(),
|
|
2658
|
+
Some(Value::String(f)) => f.to_string(),
|
|
2659
|
+
Some(v) => v.to_string(),
|
|
2660
|
+
};
|
|
2661
|
+
match tishlang_builtins::string::normalize_form(s, &form) {
|
|
2662
|
+
Some(out) => return Ok(Value::String(out.into())),
|
|
2663
|
+
None => {
|
|
2664
|
+
let err = crate::natives::range_error_construct(&[
|
|
2665
|
+
Value::String(
|
|
2666
|
+
"The normalization form should be one of NFC, NFD, NFKC, NFKD."
|
|
2667
|
+
.into(),
|
|
2668
|
+
),
|
|
2669
|
+
])
|
|
2670
|
+
.unwrap_or(Value::Null);
|
|
2671
|
+
return Err(EvalError::Throw(err));
|
|
2672
|
+
}
|
|
2673
|
+
}
|
|
2674
|
+
}
|
|
2230
2675
|
"toUpperCase" => {
|
|
2231
2676
|
return Ok(Value::String(s.to_uppercase().into()));
|
|
2232
2677
|
}
|
|
@@ -2238,14 +2683,28 @@ impl Evaluator {
|
|
|
2238
2683
|
Some(Value::String(ss)) => ss.as_ref(),
|
|
2239
2684
|
_ => return Ok(Value::Bool(false)),
|
|
2240
2685
|
};
|
|
2241
|
-
|
|
2686
|
+
// position: test as if the string began at char `position`.
|
|
2687
|
+
let pos = match arg_vals.get(1) {
|
|
2688
|
+
Some(Value::Number(n)) if *n > 0.0 => *n as usize,
|
|
2689
|
+
_ => 0,
|
|
2690
|
+
};
|
|
2691
|
+
let byte_start = s.char_indices().nth(pos).map(|(b, _)| b).unwrap_or(s.len());
|
|
2692
|
+
return Ok(Value::Bool(s[byte_start..].starts_with(search)));
|
|
2242
2693
|
}
|
|
2243
2694
|
"endsWith" => {
|
|
2244
2695
|
let search = match arg_vals.first() {
|
|
2245
2696
|
Some(Value::String(ss)) => ss.as_ref(),
|
|
2246
2697
|
_ => return Ok(Value::Bool(false)),
|
|
2247
2698
|
};
|
|
2248
|
-
|
|
2699
|
+
// endPosition: test as if the string ended at char `endPosition`.
|
|
2700
|
+
let char_count = s.chars().count();
|
|
2701
|
+
let end = match arg_vals.get(1) {
|
|
2702
|
+
Some(Value::Number(n)) if *n >= 0.0 => (*n as usize).min(char_count),
|
|
2703
|
+
Some(Value::Number(_)) => 0,
|
|
2704
|
+
_ => char_count,
|
|
2705
|
+
};
|
|
2706
|
+
let byte_end = s.char_indices().nth(end).map(|(b, _)| b).unwrap_or(s.len());
|
|
2707
|
+
return Ok(Value::Bool(s[..byte_end].ends_with(search)));
|
|
2249
2708
|
}
|
|
2250
2709
|
"replace" => {
|
|
2251
2710
|
#[cfg(feature = "regex")]
|
|
@@ -2285,6 +2744,17 @@ impl Evaluator {
|
|
|
2285
2744
|
Some(Value::String(ss)) => ss.to_string(),
|
|
2286
2745
|
_ => String::new(),
|
|
2287
2746
|
};
|
|
2747
|
+
if replacement.contains('$') && !search.is_empty() {
|
|
2748
|
+
let sref = s.as_ref();
|
|
2749
|
+
if let Some(m) = sref.find(&search) {
|
|
2750
|
+
let mut out = String::with_capacity(sref.len());
|
|
2751
|
+
out.push_str(&sref[..m]);
|
|
2752
|
+
out.push_str(&tishlang_builtins::string::expand_replacement(
|
|
2753
|
+
&replacement, &search, &sref[..m], &sref[m + search.len()..]));
|
|
2754
|
+
out.push_str(&sref[m + search.len()..]);
|
|
2755
|
+
return Ok(Value::String(out.into()));
|
|
2756
|
+
}
|
|
2757
|
+
}
|
|
2288
2758
|
return Ok(Value::String(s.replacen(&search, &replacement, 1).into()));
|
|
2289
2759
|
}
|
|
2290
2760
|
#[cfg(feature = "regex")]
|
|
@@ -2299,6 +2769,21 @@ impl Evaluator {
|
|
|
2299
2769
|
Some(Value::String(ss)) => ss.to_string(),
|
|
2300
2770
|
_ => String::new(),
|
|
2301
2771
|
};
|
|
2772
|
+
if replacement.contains('$') && !search.is_empty() {
|
|
2773
|
+
let sref = s.as_ref();
|
|
2774
|
+
let mut out = String::with_capacity(sref.len());
|
|
2775
|
+
let (mut last, mut start) = (0usize, 0usize);
|
|
2776
|
+
while let Some(pos) = sref[start..].find(&search) {
|
|
2777
|
+
let m = start + pos;
|
|
2778
|
+
out.push_str(&sref[last..m]);
|
|
2779
|
+
out.push_str(&tishlang_builtins::string::expand_replacement(
|
|
2780
|
+
&replacement, &search, &sref[..m], &sref[m + search.len()..]));
|
|
2781
|
+
last = m + search.len();
|
|
2782
|
+
start = last;
|
|
2783
|
+
}
|
|
2784
|
+
out.push_str(&sref[last..]);
|
|
2785
|
+
return Ok(Value::String(out.into()));
|
|
2786
|
+
}
|
|
2302
2787
|
return Ok(Value::String(s.replace(&search, &replacement).into()));
|
|
2303
2788
|
}
|
|
2304
2789
|
"charAt" => {
|
|
@@ -2334,6 +2819,35 @@ impl Evaluator {
|
|
|
2334
2819
|
.map(|c| Value::Number(c as u32 as f64))
|
|
2335
2820
|
.unwrap_or(Value::Number(f64::NAN)));
|
|
2336
2821
|
}
|
|
2822
|
+
"codePointAt" => {
|
|
2823
|
+
let idx = match arg_vals.first() {
|
|
2824
|
+
Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
|
|
2825
|
+
_ => return Ok(Value::Null),
|
|
2826
|
+
};
|
|
2827
|
+
return Ok(nth_char_cached(s, idx)
|
|
2828
|
+
.map(|c| Value::Number(c as u32 as f64))
|
|
2829
|
+
.unwrap_or(Value::Null));
|
|
2830
|
+
}
|
|
2831
|
+
"substr" => {
|
|
2832
|
+
// (interp lacked substr; vm/native had it — this closes the gap.)
|
|
2833
|
+
let chars: Vec<char> = s.chars().collect();
|
|
2834
|
+
let len = chars.len() as i64;
|
|
2835
|
+
let mut start = match arg_vals.first() {
|
|
2836
|
+
Some(Value::Number(n)) => *n as i64,
|
|
2837
|
+
_ => 0,
|
|
2838
|
+
};
|
|
2839
|
+
if start < 0 {
|
|
2840
|
+
start = (len + start).max(0);
|
|
2841
|
+
}
|
|
2842
|
+
let start = (start as usize).min(chars.len());
|
|
2843
|
+
let count = match arg_vals.get(1) {
|
|
2844
|
+
Some(Value::Number(n)) => (*n as i64).max(0) as usize,
|
|
2845
|
+
_ => chars.len() - start,
|
|
2846
|
+
};
|
|
2847
|
+
let out: String =
|
|
2848
|
+
chars[start..(start + count).min(chars.len())].iter().collect();
|
|
2849
|
+
return Ok(Value::String(out.into()));
|
|
2850
|
+
}
|
|
2337
2851
|
"repeat" => {
|
|
2338
2852
|
let count = match arg_vals.first() {
|
|
2339
2853
|
Some(Value::Number(n)) if *n >= 0.0 => *n as usize,
|
|
@@ -2383,6 +2897,35 @@ impl Evaluator {
|
|
|
2383
2897
|
return Ok(Value::Null);
|
|
2384
2898
|
}
|
|
2385
2899
|
#[cfg(feature = "regex")]
|
|
2900
|
+
"matchAll" => {
|
|
2901
|
+
let regexp = arg_vals.first().cloned().unwrap_or(Value::Null);
|
|
2902
|
+
match crate::regex::string_match_all(s, ®exp) {
|
|
2903
|
+
Ok(matches) => {
|
|
2904
|
+
let core_matches: Vec<tishlang_core::Value> = matches
|
|
2905
|
+
.iter()
|
|
2906
|
+
.map(|m| {
|
|
2907
|
+
crate::value_convert::eval_to_core(m)
|
|
2908
|
+
.unwrap_or(tishlang_core::Value::Null)
|
|
2909
|
+
})
|
|
2910
|
+
.collect();
|
|
2911
|
+
let it = tishlang_builtins::iterator::array_iterator(
|
|
2912
|
+
core_matches,
|
|
2913
|
+
);
|
|
2914
|
+
return Ok(crate::value_convert::core_to_eval(it));
|
|
2915
|
+
}
|
|
2916
|
+
Err(()) => {
|
|
2917
|
+
let err = crate::natives::type_error_construct(&[
|
|
2918
|
+
Value::String(
|
|
2919
|
+
"String.prototype.matchAll called with a non-global RegExp argument"
|
|
2920
|
+
.into(),
|
|
2921
|
+
),
|
|
2922
|
+
])
|
|
2923
|
+
.unwrap_or(Value::Null);
|
|
2924
|
+
return Err(EvalError::Throw(err));
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
}
|
|
2928
|
+
#[cfg(feature = "regex")]
|
|
2386
2929
|
"search" => {
|
|
2387
2930
|
if let Some(regexp) = arg_vals.first() {
|
|
2388
2931
|
return Ok(crate::regex::string_search(s, regexp));
|
|
@@ -2408,6 +2951,40 @@ impl Evaluator {
|
|
|
2408
2951
|
let formatted = tishlang_builtins::number::to_fixed_str(*n, digits as usize);
|
|
2409
2952
|
return Ok(Value::String(formatted.into()));
|
|
2410
2953
|
}
|
|
2954
|
+
if method_name.as_ref() == "toExponential" {
|
|
2955
|
+
let digits = arg_vals.first().and_then(|v| match v {
|
|
2956
|
+
Value::Number(d) => Some((*d as i32).clamp(0, 100) as usize),
|
|
2957
|
+
_ => None,
|
|
2958
|
+
});
|
|
2959
|
+
return Ok(Value::String(
|
|
2960
|
+
tishlang_builtins::number::to_exponential_str(*n, digits).into(),
|
|
2961
|
+
));
|
|
2962
|
+
}
|
|
2963
|
+
if method_name.as_ref() == "toPrecision" {
|
|
2964
|
+
match arg_vals.first() {
|
|
2965
|
+
Some(Value::Number(p)) => {
|
|
2966
|
+
let pi = *p as i32;
|
|
2967
|
+
if !(1..=100).contains(&pi) && n.is_finite() {
|
|
2968
|
+
let err = crate::natives::range_error_construct(&[
|
|
2969
|
+
Value::String(
|
|
2970
|
+
"toPrecision() argument must be between 1 and 100"
|
|
2971
|
+
.into(),
|
|
2972
|
+
),
|
|
2973
|
+
])
|
|
2974
|
+
.unwrap_or(Value::Null);
|
|
2975
|
+
return Err(EvalError::Throw(err));
|
|
2976
|
+
}
|
|
2977
|
+
return Ok(Value::String(
|
|
2978
|
+
tishlang_builtins::number::to_precision_str(*n, pi).into(),
|
|
2979
|
+
));
|
|
2980
|
+
}
|
|
2981
|
+
_ => {
|
|
2982
|
+
return Ok(Value::String(
|
|
2983
|
+
tishlang_core::js_number_to_string(*n).into(),
|
|
2984
|
+
))
|
|
2985
|
+
}
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2411
2988
|
if method_name.as_ref() == "toString" {
|
|
2412
2989
|
// Shares the VM/native formatting via the backend-agnostic helper
|
|
2413
2990
|
// (issue #59). Radix defaults to 10; 2–36 supported, else RangeError.
|
|
@@ -2821,6 +3398,12 @@ impl Evaluator {
|
|
|
2821
3398
|
Value::Array(arr) => {
|
|
2822
3399
|
let idx = match &idx_val {
|
|
2823
3400
|
Value::Number(n) => *n as usize,
|
|
3401
|
+
// Canonical integer string key → index (#432); a non-index string key
|
|
3402
|
+
// can't live in a Vec-backed array, so the write is a no-op.
|
|
3403
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
3404
|
+
Some(i) => i,
|
|
3405
|
+
None => return Ok(val),
|
|
3406
|
+
},
|
|
2824
3407
|
_ => return Err(EvalError::Error(format!(
|
|
2825
3408
|
"Array index must be a number, got {:?}",
|
|
2826
3409
|
idx_val
|
|
@@ -3331,7 +3914,31 @@ impl Evaluator {
|
|
|
3331
3914
|
}
|
|
3332
3915
|
Err(EvalError::Error("Not a function".to_string()))
|
|
3333
3916
|
}
|
|
3334
|
-
Value::Native(native_fn) =>
|
|
3917
|
+
Value::Native(native_fn) => {
|
|
3918
|
+
// #122: Node fs callback form — `readFile(path, (err, data) => …)`. The self-less
|
|
3919
|
+
// `Value::Native` bridge can't invoke an eval callback (a core `Callable` can't
|
|
3920
|
+
// re-enter the interpreter), so handle it here where `&self` is available: run the
|
|
3921
|
+
// op's core on the non-callback args, split its Result into `(err, data)`, and call
|
|
3922
|
+
// the callback. Matches the VM/native backends (fs_ext runs the callback in-core).
|
|
3923
|
+
#[cfg(feature = "fs")]
|
|
3924
|
+
if let Some(cb @ Value::Function { .. }) = args.last() {
|
|
3925
|
+
if let Some(core_op) = natives::fsx::callback_core(*native_fn) {
|
|
3926
|
+
let cb = cb.clone();
|
|
3927
|
+
let core_args = args[..args.len() - 1]
|
|
3928
|
+
.iter()
|
|
3929
|
+
.map(crate::value_convert::eval_to_core)
|
|
3930
|
+
.collect::<Result<Vec<_>, String>>()
|
|
3931
|
+
.map_err(EvalError::Error)?;
|
|
3932
|
+
let (err, data) = match core_op(&core_args) {
|
|
3933
|
+
Ok(v) => (Value::Null, crate::value_convert::core_to_eval(v)),
|
|
3934
|
+
Err(e) => (crate::value_convert::core_to_eval(e), Value::Null),
|
|
3935
|
+
};
|
|
3936
|
+
self.call_func(&cb, &[err, data])?;
|
|
3937
|
+
return Ok(Value::Null);
|
|
3938
|
+
}
|
|
3939
|
+
}
|
|
3940
|
+
native_fn(args).map_err(EvalError::Error)
|
|
3941
|
+
}
|
|
3335
3942
|
#[cfg(feature = "http")]
|
|
3336
3943
|
Value::PromiseResolver(r) => {
|
|
3337
3944
|
let value = args.first().cloned().unwrap_or(Value::Null);
|
|
@@ -3959,6 +4566,15 @@ impl Evaluator {
|
|
|
3959
4566
|
/// `.entries()` — into a `Vec` by calling `next()` until `done`. Returns `None` when `v`
|
|
3960
4567
|
/// is not such an object. Shared by `for…of` and spread so both treat iterators like JS.
|
|
3961
4568
|
fn drain_eval_iterator(&self, v: &Value) -> Option<Vec<Value>> {
|
|
4569
|
+
// A string spreads as its characters (`[...s]`, `f(...s)`); for-of over a string is handled
|
|
4570
|
+
// separately at the loop site, but spread routes through here. (#440)
|
|
4571
|
+
if let Value::String(s) = v {
|
|
4572
|
+
return Some(
|
|
4573
|
+
s.chars()
|
|
4574
|
+
.map(|c| Value::String(std::sync::Arc::from(c.to_string())))
|
|
4575
|
+
.collect(),
|
|
4576
|
+
);
|
|
4577
|
+
}
|
|
3962
4578
|
if !matches!(v, Value::Object(_)) {
|
|
3963
4579
|
return None;
|
|
3964
4580
|
}
|
|
@@ -4085,6 +4701,12 @@ impl Evaluator {
|
|
|
4085
4701
|
Value::Array(arr) => {
|
|
4086
4702
|
let idx = match index {
|
|
4087
4703
|
Value::Number(n) => *n as usize,
|
|
4704
|
+
// A canonical integer string key ("0","1",…) indexes the array (#432), so
|
|
4705
|
+
// `arr["0"] === arr[0]`; any other key reads Null.
|
|
4706
|
+
Value::String(s) => match tishlang_core::str_to_array_index(s) {
|
|
4707
|
+
Some(i) => i,
|
|
4708
|
+
None => return Ok(Value::Null),
|
|
4709
|
+
},
|
|
4088
4710
|
_ => return Ok(Value::Null),
|
|
4089
4711
|
};
|
|
4090
4712
|
Ok(arr.borrow().get(idx).cloned().unwrap_or(Value::Null))
|
|
@@ -4148,14 +4770,16 @@ impl Evaluator {
|
|
|
4148
4770
|
tishlang_core::write_json_number(&mut s, *n);
|
|
4149
4771
|
s
|
|
4150
4772
|
}
|
|
4151
|
-
Value::String(s) =>
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4773
|
+
Value::String(s) => {
|
|
4774
|
+
// Use the shared core escaper so control chars (`\b`, `\f`, `\u00xx`) are escaped
|
|
4775
|
+
// exactly like the vm/native path — the old hand-rolled `.replace()` chain missed
|
|
4776
|
+
// them and produced invalid JSON (raw control bytes). #437
|
|
4777
|
+
let mut out = String::with_capacity(s.len() + 2);
|
|
4778
|
+
out.push('"');
|
|
4779
|
+
tishlang_core::escape_json_string_into(&mut out, s);
|
|
4780
|
+
out.push('"');
|
|
4781
|
+
out
|
|
4782
|
+
}
|
|
4159
4783
|
Value::Array(arr) => {
|
|
4160
4784
|
let ptr = Rc::as_ptr(arr) as *const ();
|
|
4161
4785
|
if ancestors.contains(&ptr) {
|
|
@@ -4229,16 +4853,26 @@ impl Evaluator {
|
|
|
4229
4853
|
}
|
|
4230
4854
|
|
|
4231
4855
|
fn object_keys(args: &[Value]) -> Result<Value, String> {
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
4235
|
-
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4856
|
+
match args.first() {
|
|
4857
|
+
Some(Value::Object(obj)) => {
|
|
4858
|
+
let keys: Vec<Value> = obj
|
|
4859
|
+
.borrow()
|
|
4860
|
+
.strings
|
|
4861
|
+
.keys()
|
|
4862
|
+
.map(|k| Value::String(Arc::clone(k)))
|
|
4863
|
+
.collect();
|
|
4864
|
+
Ok(Value::Array(Rc::new(RefCell::new(keys))))
|
|
4865
|
+
}
|
|
4866
|
+
// `Object.keys(array)` → index strings "0".."len-1" (own enumerable keys are the indices),
|
|
4867
|
+
// matching node and the shared builtin.
|
|
4868
|
+
Some(Value::Array(arr)) => {
|
|
4869
|
+
let len = arr.borrow().len();
|
|
4870
|
+
let keys: Vec<Value> = (0..len)
|
|
4871
|
+
.map(|i| Value::String(Arc::from(i.to_string())))
|
|
4872
|
+
.collect();
|
|
4873
|
+
Ok(Value::Array(Rc::new(RefCell::new(keys))))
|
|
4874
|
+
}
|
|
4875
|
+
_ => Ok(Value::Array(Rc::new(RefCell::new(Vec::new())))),
|
|
4242
4876
|
}
|
|
4243
4877
|
}
|
|
4244
4878
|
|