@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
|
@@ -73,17 +73,13 @@ pub fn parse_float(args: &[Value]) -> Result<Value, String> {
|
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
pub fn is_finite(args: &[Value]) -> Result<Value, String> {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
.is_some_and(|v| matches!(v, Value::Number(n) if n.is_finite()));
|
|
79
|
-
Ok(Value::Bool(b))
|
|
76
|
+
// Global isFinite coerces via ToNumber (like Number()); absent arg (undefined) → NaN → false.
|
|
77
|
+
Ok(Value::Bool(args.first().map_or(f64::NAN, to_number).is_finite()))
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
pub fn is_nan(args: &[Value]) -> Result<Value, String> {
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
});
|
|
86
|
-
Ok(Value::Bool(b))
|
|
81
|
+
// Global isNaN coerces via ToNumber; absent arg (undefined) → NaN → true.
|
|
82
|
+
Ok(Value::Bool(args.first().map_or(f64::NAN, to_number).is_nan()))
|
|
87
83
|
}
|
|
88
84
|
|
|
89
85
|
pub fn boolean_native(args: &[Value]) -> Result<Value, String> {
|
|
@@ -103,6 +99,20 @@ pub fn encode_uri(args: &[Value]) -> Result<Value, String> {
|
|
|
103
99
|
Ok(Value::String(tishlang_core::percent_encode(&s).into()))
|
|
104
100
|
}
|
|
105
101
|
|
|
102
|
+
pub fn encode_uri_component(args: &[Value]) -> Result<Value, String> {
|
|
103
|
+
let s = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
104
|
+
Ok(Value::String(
|
|
105
|
+
tishlang_core::percent_encode_component(&s).into(),
|
|
106
|
+
))
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
pub fn decode_uri_component(args: &[Value]) -> Result<Value, String> {
|
|
110
|
+
let s = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
111
|
+
Ok(Value::String(
|
|
112
|
+
tishlang_core::percent_decode_component(&s).unwrap_or(s).into(),
|
|
113
|
+
))
|
|
114
|
+
}
|
|
115
|
+
|
|
106
116
|
pub fn html_escape(args: &[Value]) -> Result<Value, String> {
|
|
107
117
|
let input = match args.first() {
|
|
108
118
|
Some(Value::String(s)) => s.to_string(),
|
|
@@ -170,12 +180,12 @@ pub fn math_max(args: &[Value]) -> Result<Value, String> {
|
|
|
170
180
|
}
|
|
171
181
|
|
|
172
182
|
// hypot/asin/acos/atan/atan2 existed on the vm's Math but not the interpreter's — a direct interp≠vm
|
|
173
|
-
// gap (#247).
|
|
174
|
-
//
|
|
183
|
+
// gap (#247). asin/acos/atan/atan2 are 1:1 with Rust f64 methods, so compute directly. hypot is
|
|
184
|
+
// VARIADIC in JS and must propagate NaN/Infinity, so it delegates to the shared `hypot_f64` (exactly
|
|
185
|
+
// like min/max) — a 2-arg `x.hypot(y)` gave `Math.hypot(3,4,12)` = 5 instead of 13.
|
|
175
186
|
pub fn math_hypot(args: &[Value]) -> Result<Value, String> {
|
|
176
|
-
let
|
|
177
|
-
|
|
178
|
-
Ok(Value::Number(x.hypot(y)))
|
|
187
|
+
let nums: Vec<f64> = args.iter().map(get_num).collect();
|
|
188
|
+
Ok(Value::Number(tishlang_builtins::math::hypot_f64(&nums)))
|
|
179
189
|
}
|
|
180
190
|
|
|
181
191
|
pub fn math_asin(args: &[Value]) -> Result<Value, String> {
|
|
@@ -226,6 +236,17 @@ pub fn math_pow(args: &[Value]) -> Result<Value, String> {
|
|
|
226
236
|
Ok(Value::Number(base.powf(exp)))
|
|
227
237
|
}
|
|
228
238
|
|
|
239
|
+
/// ES6 `Math.imul`: an exact 32-bit integer multiply. Both operands go through JS `ToInt32`
|
|
240
|
+
/// (modulo 2³², NaN/±Infinity → 0), NOT a saturating `as i32` cast, then a wrapping i32 multiply —
|
|
241
|
+
/// bit-for-bit identical to the shared `tishlang_builtins::math::imul`, the vm, the native path, and
|
|
242
|
+
/// V8. Was previously absent from the interpreter's `Math` object, so `Math.imul(...)` threw
|
|
243
|
+
/// "Not a function" in `--backend interp` while working natively (a cross-backend divergence).
|
|
244
|
+
pub fn math_imul(args: &[Value]) -> Result<Value, String> {
|
|
245
|
+
let a = tishlang_core::to_int32(get_num(args.first().unwrap_or(&Value::Null)));
|
|
246
|
+
let b = tishlang_core::to_int32(get_num(args.get(1).unwrap_or(&Value::Null)));
|
|
247
|
+
Ok(Value::Number(a.wrapping_mul(b) as f64))
|
|
248
|
+
}
|
|
249
|
+
|
|
229
250
|
pub fn math_sin(args: &[Value]) -> Result<Value, String> {
|
|
230
251
|
Ok(Value::Number(
|
|
231
252
|
get_num(args.first().unwrap_or(&Value::Null)).sin(),
|
|
@@ -265,7 +286,8 @@ pub fn math_sign(args: &[Value]) -> Result<Value, String> {
|
|
|
265
286
|
} else if n < 0.0 {
|
|
266
287
|
-1.0
|
|
267
288
|
} else {
|
|
268
|
-
0.0
|
|
289
|
+
// ±0 → return the input so `Math.sign(-0)` is `-0` (per ES); bare `0.0` drops the sign.
|
|
290
|
+
n
|
|
269
291
|
};
|
|
270
292
|
Ok(Value::Number(sign))
|
|
271
293
|
}
|
|
@@ -297,6 +319,17 @@ math_unary!(math_atanh, atanh);
|
|
|
297
319
|
math_unary!(math_cbrt, cbrt);
|
|
298
320
|
math_unary!(math_log2, log2);
|
|
299
321
|
math_unary!(math_log10, log10);
|
|
322
|
+
math_unary!(math_expm1, exp_m1);
|
|
323
|
+
math_unary!(math_log1p, ln_1p);
|
|
324
|
+
|
|
325
|
+
pub fn math_clz32(args: &[Value]) -> Result<Value, String> {
|
|
326
|
+
let n = tishlang_core::to_uint32(get_num(args.first().unwrap_or(&Value::Null)));
|
|
327
|
+
Ok(Value::Number(n.leading_zeros() as f64))
|
|
328
|
+
}
|
|
329
|
+
pub fn math_fround(args: &[Value]) -> Result<Value, String> {
|
|
330
|
+
let x = get_num(args.first().unwrap_or(&Value::Null));
|
|
331
|
+
Ok(Value::Number(x as f32 as f64))
|
|
332
|
+
}
|
|
300
333
|
|
|
301
334
|
pub fn array_is_array(args: &[Value]) -> Result<Value, String> {
|
|
302
335
|
Ok(Value::Bool(matches!(args.first(), Some(Value::Array(_)))))
|
|
@@ -337,6 +370,47 @@ pub fn array_construct(args: &[Value]) -> Result<Value, String> {
|
|
|
337
370
|
Ok(Value::array(args.to_vec()))
|
|
338
371
|
}
|
|
339
372
|
|
|
373
|
+
/// `Array.of(...items)` — args become elements (a single number is NOT a length, unlike `Array(n)`).
|
|
374
|
+
pub fn array_of(args: &[Value]) -> Result<Value, String> {
|
|
375
|
+
Ok(Value::array(args.to_vec()))
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/// `Object.is(a, b)` — SameValue: like `===`, but NaN equals NaN and `+0` is not `-0`.
|
|
379
|
+
pub fn object_is(args: &[Value]) -> Result<Value, String> {
|
|
380
|
+
let a = args.first().unwrap_or(&Value::Null);
|
|
381
|
+
let b = args.get(1).unwrap_or(&Value::Null);
|
|
382
|
+
let same = match (a, b) {
|
|
383
|
+
(Value::Number(x), Value::Number(y)) => {
|
|
384
|
+
if x.is_nan() && y.is_nan() {
|
|
385
|
+
true
|
|
386
|
+
} else if *x == 0.0 && *y == 0.0 {
|
|
387
|
+
x.is_sign_negative() == y.is_sign_negative()
|
|
388
|
+
} else {
|
|
389
|
+
x == y
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
_ => a.strict_eq(b),
|
|
393
|
+
};
|
|
394
|
+
Ok(Value::Bool(same))
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/// `Object.hasOwn(obj, key)` (ES2022) — true if `key` is an OWN property of `obj`. Mirrors the shared
|
|
398
|
+
/// `tishlang_builtins::globals::object_has_own` on the interpreter's `Value` type.
|
|
399
|
+
pub fn object_has_own(args: &[Value]) -> Result<Value, String> {
|
|
400
|
+
let key = match args.get(1) {
|
|
401
|
+
Some(Value::String(s)) => s.to_string(),
|
|
402
|
+
Some(v) => v.to_string(),
|
|
403
|
+
None => return Ok(Value::Bool(false)),
|
|
404
|
+
};
|
|
405
|
+
Ok(match args.first() {
|
|
406
|
+
Some(Value::Object(obj)) => Value::Bool(obj.borrow().strings.contains_key(key.as_str())),
|
|
407
|
+
Some(Value::Array(arr)) => Value::Bool(
|
|
408
|
+
key == "length" || matches!(key.parse::<usize>(), Ok(i) if i < arr.borrow().len()),
|
|
409
|
+
),
|
|
410
|
+
_ => Value::Bool(false),
|
|
411
|
+
})
|
|
412
|
+
}
|
|
413
|
+
|
|
340
414
|
/// `String(value)` as a function: JS `ToString` coercion (arrays comma-join recursively,
|
|
341
415
|
/// objects → "[object Object]", null → "null"), matching the VM/native `string_convert`.
|
|
342
416
|
pub fn string_convert(args: &[Value]) -> Result<Value, String> {
|
|
@@ -347,8 +421,13 @@ pub fn string_convert(args: &[Value]) -> Result<Value, String> {
|
|
|
347
421
|
/// `Number(value)` coercion (issue #36) — shares the string parser with the VM/native
|
|
348
422
|
/// backend so the result is byte-identical.
|
|
349
423
|
pub fn number_convert(args: &[Value]) -> Result<Value, String> {
|
|
350
|
-
|
|
351
|
-
|
|
424
|
+
Ok(Value::Number(to_number(args.first().unwrap_or(&Value::Null))))
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/// JS `ToNumber(v)` for the interpreter's Value — shares the string parser with the VM/native path
|
|
428
|
+
/// so `Number()`, `isNaN`, `isFinite` are byte-identical across backends.
|
|
429
|
+
pub fn to_number(v: &Value) -> f64 {
|
|
430
|
+
match v {
|
|
352
431
|
Value::Number(n) => *n,
|
|
353
432
|
Value::Bool(b) => {
|
|
354
433
|
if *b {
|
|
@@ -360,8 +439,22 @@ pub fn number_convert(args: &[Value]) -> Result<Value, String> {
|
|
|
360
439
|
Value::Null => 0.0,
|
|
361
440
|
Value::String(s) => tishlang_builtins::globals::parse_numeric_string(s),
|
|
362
441
|
other => tishlang_builtins::globals::parse_numeric_string(&other.to_js_string()),
|
|
363
|
-
}
|
|
364
|
-
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// `Number.*` static predicates — no coercion (the coercing forms are the bare globals).
|
|
446
|
+
pub fn number_is_integer(args: &[Value]) -> Result<Value, String> {
|
|
447
|
+
Ok(Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0)))
|
|
448
|
+
}
|
|
449
|
+
pub fn number_is_safe_integer(args: &[Value]) -> Result<Value, String> {
|
|
450
|
+
Ok(Value::Bool(matches!(args.first(),
|
|
451
|
+
Some(Value::Number(n)) if n.is_finite() && n.fract() == 0.0 && n.abs() <= 9_007_199_254_740_991.0)))
|
|
452
|
+
}
|
|
453
|
+
pub fn number_is_nan(args: &[Value]) -> Result<Value, String> {
|
|
454
|
+
Ok(Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_nan())))
|
|
455
|
+
}
|
|
456
|
+
pub fn number_is_finite(args: &[Value]) -> Result<Value, String> {
|
|
457
|
+
Ok(Value::Bool(matches!(args.first(), Some(Value::Number(n)) if n.is_finite())))
|
|
365
458
|
}
|
|
366
459
|
|
|
367
460
|
pub fn string_from_char_code(args: &[Value]) -> Result<Value, String> {
|
|
@@ -455,6 +548,109 @@ pub fn read_file_bytes(args: &[Value]) -> Result<Value, String> {
|
|
|
455
548
|
}
|
|
456
549
|
}
|
|
457
550
|
|
|
551
|
+
// ── Node-compatible fs surface (#122) — bridged to tishlang_runtime::fs_ext so the interpreter
|
|
552
|
+
// shares one implementation with the VM/native backends. eval args -> core, call, core -> eval.
|
|
553
|
+
#[cfg(feature = "fs")]
|
|
554
|
+
macro_rules! fs_bridge {
|
|
555
|
+
($name:ident => $core:path) => {
|
|
556
|
+
pub fn $name(args: &[Value]) -> Result<Value, String> {
|
|
557
|
+
let core_args = args
|
|
558
|
+
.iter()
|
|
559
|
+
.map(crate::value_convert::eval_to_core)
|
|
560
|
+
.collect::<Result<Vec<_>, String>>()?;
|
|
561
|
+
Ok(crate::value_convert::core_to_eval($core(&core_args)))
|
|
562
|
+
}
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
#[cfg(feature = "fs")]
|
|
567
|
+
pub mod fsx {
|
|
568
|
+
use super::Value;
|
|
569
|
+
use tishlang_runtime::fs_ext as fx;
|
|
570
|
+
// sync
|
|
571
|
+
fs_bridge!(read_file => fx::read_file);
|
|
572
|
+
fs_bridge!(read_file_bytes => fx::read_file_bytes);
|
|
573
|
+
fs_bridge!(write_file => fx::write_file);
|
|
574
|
+
fs_bridge!(append_file => fx::append_file);
|
|
575
|
+
fs_bridge!(exists => fx::exists);
|
|
576
|
+
fs_bridge!(mkdir => fx::mkdir);
|
|
577
|
+
fs_bridge!(readdir => fx::readdir);
|
|
578
|
+
fs_bridge!(stat => fx::stat);
|
|
579
|
+
fs_bridge!(lstat => fx::lstat);
|
|
580
|
+
fs_bridge!(rm => fx::rm);
|
|
581
|
+
fs_bridge!(rmdir => fx::rmdir);
|
|
582
|
+
fs_bridge!(unlink => fx::unlink);
|
|
583
|
+
fs_bridge!(rename => fx::rename);
|
|
584
|
+
fs_bridge!(copy_file => fx::copy_file);
|
|
585
|
+
fs_bridge!(realpath => fx::realpath);
|
|
586
|
+
fs_bridge!(readlink => fx::readlink);
|
|
587
|
+
fs_bridge!(truncate => fx::truncate);
|
|
588
|
+
fs_bridge!(mkdtemp => fx::mkdtemp);
|
|
589
|
+
fs_bridge!(cp => fx::cp);
|
|
590
|
+
fs_bridge!(access => fx::access);
|
|
591
|
+
// promises
|
|
592
|
+
fs_bridge!(read_file_p => fx::read_file_promise);
|
|
593
|
+
fs_bridge!(read_file_bytes_p => fx::read_file_bytes_promise);
|
|
594
|
+
fs_bridge!(write_file_p => fx::write_file_promise);
|
|
595
|
+
fs_bridge!(append_file_p => fx::append_file_promise);
|
|
596
|
+
fs_bridge!(exists_p => fx::exists_promise);
|
|
597
|
+
fs_bridge!(mkdir_p => fx::mkdir_promise);
|
|
598
|
+
fs_bridge!(readdir_p => fx::readdir_promise);
|
|
599
|
+
fs_bridge!(stat_p => fx::stat_promise);
|
|
600
|
+
fs_bridge!(lstat_p => fx::lstat_promise);
|
|
601
|
+
fs_bridge!(rm_p => fx::rm_promise);
|
|
602
|
+
fs_bridge!(rmdir_p => fx::rmdir_promise);
|
|
603
|
+
fs_bridge!(unlink_p => fx::unlink_promise);
|
|
604
|
+
fs_bridge!(rename_p => fx::rename_promise);
|
|
605
|
+
fs_bridge!(copy_file_p => fx::copy_file_promise);
|
|
606
|
+
fs_bridge!(realpath_p => fx::realpath_promise);
|
|
607
|
+
fs_bridge!(readlink_p => fx::readlink_promise);
|
|
608
|
+
fs_bridge!(truncate_p => fx::truncate_promise);
|
|
609
|
+
fs_bridge!(mkdtemp_p => fx::mkdtemp_promise);
|
|
610
|
+
fs_bridge!(cp_p => fx::cp_promise);
|
|
611
|
+
fs_bridge!(access_p => fx::access_promise);
|
|
612
|
+
pub fn constants() -> Value {
|
|
613
|
+
crate::value_convert::core_to_eval(fx::constants())
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/// Core op behind a Node-callback-style fs function (Ok = data, Err = error value), if `f` is one.
|
|
617
|
+
/// The interpreter binds fs to self-less `Value::Native`s that can't invoke an eval callback, so
|
|
618
|
+
/// `call_func` uses this to run the sync op and split its Result into `(err, data)` for the
|
|
619
|
+
/// callback form (#122). Covers both the old tish aliases (`readFile` → `natives::read_file`) and
|
|
620
|
+
/// the `fsx::*` bridges. `exists`/`access` are omitted — they answer a boolean and never error.
|
|
621
|
+
pub type CoreOp = fn(&[tishlang_core::Value]) -> Result<tishlang_core::Value, tishlang_core::Value>;
|
|
622
|
+
pub fn callback_core(f: crate::value::NativeFn) -> Option<CoreOp> {
|
|
623
|
+
use std::ptr::fn_addr_eq;
|
|
624
|
+
type NF = crate::value::NativeFn;
|
|
625
|
+
// Node names bound to the old tish aliases (natives::*).
|
|
626
|
+
if fn_addr_eq(f, super::read_file as NF) { return Some(fx::read_file_core); }
|
|
627
|
+
if fn_addr_eq(f, super::write_file as NF) { return Some(fx::write_file_core); }
|
|
628
|
+
if fn_addr_eq(f, super::read_file_bytes as NF) { return Some(fx::read_file_bytes_core); }
|
|
629
|
+
if fn_addr_eq(f, super::read_dir as NF) { return Some(fx::readdir_core); }
|
|
630
|
+
if fn_addr_eq(f, super::mkdir as NF) { return Some(fx::mkdir_core); }
|
|
631
|
+
// The `fsx::*` bridges (Node *Sync names and the ops with no old alias).
|
|
632
|
+
if fn_addr_eq(f, read_file as NF) { return Some(fx::read_file_core); }
|
|
633
|
+
if fn_addr_eq(f, write_file as NF) { return Some(fx::write_file_core); }
|
|
634
|
+
if fn_addr_eq(f, append_file as NF) { return Some(fx::append_file_core); }
|
|
635
|
+
if fn_addr_eq(f, read_file_bytes as NF) { return Some(fx::read_file_bytes_core); }
|
|
636
|
+
if fn_addr_eq(f, readdir as NF) { return Some(fx::readdir_core); }
|
|
637
|
+
if fn_addr_eq(f, stat as NF) { return Some(fx::stat_core); }
|
|
638
|
+
if fn_addr_eq(f, lstat as NF) { return Some(fx::lstat_core); }
|
|
639
|
+
if fn_addr_eq(f, mkdir as NF) { return Some(fx::mkdir_core); }
|
|
640
|
+
if fn_addr_eq(f, rm as NF) { return Some(fx::rm_core); }
|
|
641
|
+
if fn_addr_eq(f, rmdir as NF) { return Some(fx::rmdir_core); }
|
|
642
|
+
if fn_addr_eq(f, unlink as NF) { return Some(fx::unlink_core); }
|
|
643
|
+
if fn_addr_eq(f, rename as NF) { return Some(fx::rename_core); }
|
|
644
|
+
if fn_addr_eq(f, copy_file as NF) { return Some(fx::copy_file_core); }
|
|
645
|
+
if fn_addr_eq(f, cp as NF) { return Some(fx::cp_core); }
|
|
646
|
+
if fn_addr_eq(f, realpath as NF) { return Some(fx::realpath_core); }
|
|
647
|
+
if fn_addr_eq(f, readlink as NF) { return Some(fx::readlink_core); }
|
|
648
|
+
if fn_addr_eq(f, truncate as NF) { return Some(fx::truncate_core); }
|
|
649
|
+
if fn_addr_eq(f, mkdtemp as NF) { return Some(fx::mkdtemp_core); }
|
|
650
|
+
None
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
458
654
|
#[cfg(feature = "fs")]
|
|
459
655
|
pub fn write_file(args: &[Value]) -> Result<Value, String> {
|
|
460
656
|
let path = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
@@ -584,3 +780,53 @@ pub fn tty_read(args: &[Value]) -> Result<Value, String> {
|
|
|
584
780
|
None => Value::Null,
|
|
585
781
|
})
|
|
586
782
|
}
|
|
783
|
+
|
|
784
|
+
/// `structuredClone(value)` — recursive deep copy of arrays/objects/primitives with cycles preserved
|
|
785
|
+
/// (a repeated reference clones to the same new node). Matches the shared vm/native impl in
|
|
786
|
+
/// `tishlang_builtins::globals::structured_clone`; functions and exotic types are shared/shallow.
|
|
787
|
+
pub fn structured_clone(args: &[Value]) -> Result<Value, String> {
|
|
788
|
+
let mut seen: Vec<(*const (), Value)> = Vec::new();
|
|
789
|
+
Ok(deep_clone(args.first().unwrap_or(&Value::Null), &mut seen))
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
fn deep_clone(v: &Value, seen: &mut Vec<(*const (), Value)>) -> Value {
|
|
793
|
+
use std::rc::Rc;
|
|
794
|
+
match v {
|
|
795
|
+
Value::Array(a) => {
|
|
796
|
+
let ptr = Rc::as_ptr(a) as *const ();
|
|
797
|
+
if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
|
|
798
|
+
return c.clone();
|
|
799
|
+
}
|
|
800
|
+
let new = Value::array(Vec::new());
|
|
801
|
+
if let Value::Array(na) = &new {
|
|
802
|
+
seen.push((ptr, new.clone()));
|
|
803
|
+
let src = a.borrow().clone();
|
|
804
|
+
let cloned: Vec<Value> = src.iter().map(|e| deep_clone(e, seen)).collect();
|
|
805
|
+
*na.borrow_mut() = cloned;
|
|
806
|
+
}
|
|
807
|
+
new
|
|
808
|
+
}
|
|
809
|
+
Value::Object(o) => {
|
|
810
|
+
let ptr = Rc::as_ptr(o) as *const ();
|
|
811
|
+
if let Some((_, c)) = seen.iter().find(|(p, _)| *p == ptr) {
|
|
812
|
+
return c.clone();
|
|
813
|
+
}
|
|
814
|
+
let entries: Vec<(std::sync::Arc<str>, Value)> = o
|
|
815
|
+
.borrow()
|
|
816
|
+
.strings
|
|
817
|
+
.iter()
|
|
818
|
+
.map(|(k, val)| (k.clone(), val.clone()))
|
|
819
|
+
.collect();
|
|
820
|
+
let new = Value::object(crate::value::PropMap::default());
|
|
821
|
+
if let Value::Object(no) = &new {
|
|
822
|
+
seen.push((ptr, new.clone()));
|
|
823
|
+
for (k, val) in entries {
|
|
824
|
+
let cloned = deep_clone(&val, seen);
|
|
825
|
+
no.borrow_mut().strings.insert(k, cloned);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
new
|
|
829
|
+
}
|
|
830
|
+
other => other.clone(),
|
|
831
|
+
}
|
|
832
|
+
}
|
|
@@ -147,6 +147,34 @@ pub fn string_match(input: &str, regexp: &Value) -> Value {
|
|
|
147
147
|
}
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/// `String.prototype.matchAll(regexp)` — collects every match as an exec-style match object (reusing
|
|
151
|
+
/// `regexp_exec`). Returns `Err(())` if a non-global RegExp is passed (the caller raises a TypeError);
|
|
152
|
+
/// a non-RegExp argument is coerced to a global regex. The caller wraps the results in an iterator.
|
|
153
|
+
pub fn string_match_all(input: &str, regexp: &Value) -> Result<Vec<Value>, ()> {
|
|
154
|
+
let re_cell = match regexp {
|
|
155
|
+
Value::RegExp(re) => re.clone(),
|
|
156
|
+
other => match TishRegExp::new(&other.to_string(), "g") {
|
|
157
|
+
Ok(re) => Rc::new(RefCell::new(re)),
|
|
158
|
+
Err(_) => return Ok(Vec::new()),
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
if !re_cell.borrow().flags.global {
|
|
162
|
+
return Err(());
|
|
163
|
+
}
|
|
164
|
+
let mut results = Vec::new();
|
|
165
|
+
let mut re = re_cell.borrow_mut();
|
|
166
|
+
re.last_index = 0;
|
|
167
|
+
loop {
|
|
168
|
+
let m = regexp_exec(&mut re, input);
|
|
169
|
+
if matches!(m, Value::Null) {
|
|
170
|
+
break;
|
|
171
|
+
}
|
|
172
|
+
results.push(m);
|
|
173
|
+
}
|
|
174
|
+
re.last_index = 0;
|
|
175
|
+
Ok(results)
|
|
176
|
+
}
|
|
177
|
+
|
|
150
178
|
/// String.prototype.replace(searchValue, replaceValue)
|
|
151
179
|
pub fn string_replace(input: &str, search: &Value, replace: &Value) -> Value {
|
|
152
180
|
let replacement = match replace {
|
|
@@ -170,7 +198,25 @@ pub fn string_replace(input: &str, search: &Value, replace: &Value) -> Value {
|
|
|
170
198
|
}
|
|
171
199
|
}
|
|
172
200
|
Value::String(pattern) => {
|
|
173
|
-
|
|
201
|
+
let p = pattern.as_ref();
|
|
202
|
+
if replacement.contains('$') && !p.is_empty() {
|
|
203
|
+
if let Some(m) = input.find(p) {
|
|
204
|
+
let mut out = String::with_capacity(input.len());
|
|
205
|
+
out.push_str(&input[..m]);
|
|
206
|
+
out.push_str(&tishlang_builtins::string::expand_replacement(
|
|
207
|
+
&replacement,
|
|
208
|
+
p,
|
|
209
|
+
&input[..m],
|
|
210
|
+
&input[m + p.len()..],
|
|
211
|
+
));
|
|
212
|
+
out.push_str(&input[m + p.len()..]);
|
|
213
|
+
Value::String(out.into())
|
|
214
|
+
} else {
|
|
215
|
+
Value::String(input.into())
|
|
216
|
+
}
|
|
217
|
+
} else {
|
|
218
|
+
Value::String(input.replacen(p, &replacement, 1).into())
|
|
219
|
+
}
|
|
174
220
|
}
|
|
175
221
|
_ => Value::String(input.into()),
|
|
176
222
|
}
|
|
@@ -613,6 +613,20 @@ impl Printer {
|
|
|
613
613
|
self.buf.push_str(") ");
|
|
614
614
|
self.stmt_inline_or_block(body, level);
|
|
615
615
|
}
|
|
616
|
+
Statement::ForIn {
|
|
617
|
+
name,
|
|
618
|
+
object,
|
|
619
|
+
body,
|
|
620
|
+
..
|
|
621
|
+
} => {
|
|
622
|
+
self.indent(level);
|
|
623
|
+
self.buf.push_str("for (let ");
|
|
624
|
+
self.buf.push_str(name);
|
|
625
|
+
self.buf.push_str(" in ");
|
|
626
|
+
self.expr(object);
|
|
627
|
+
self.buf.push_str(") ");
|
|
628
|
+
self.stmt_inline_or_block(body, level);
|
|
629
|
+
}
|
|
616
630
|
Statement::Return { value, .. } => {
|
|
617
631
|
self.indent(level);
|
|
618
632
|
self.buf.push_str("return");
|
|
@@ -825,8 +839,9 @@ impl Printer {
|
|
|
825
839
|
..
|
|
826
840
|
} => {
|
|
827
841
|
if *all {
|
|
842
|
+
// `export *` always has a source.
|
|
828
843
|
self.buf.push_str("* from \"");
|
|
829
|
-
self.buf.push_str(from);
|
|
844
|
+
self.buf.push_str(from.as_deref().unwrap_or(""));
|
|
830
845
|
self.buf.push('"');
|
|
831
846
|
} else {
|
|
832
847
|
self.buf.push_str("{ ");
|
|
@@ -844,9 +859,13 @@ impl Printer {
|
|
|
844
859
|
}
|
|
845
860
|
}
|
|
846
861
|
}
|
|
847
|
-
self.buf.push_str(" }
|
|
848
|
-
|
|
849
|
-
|
|
862
|
+
self.buf.push_str(" }");
|
|
863
|
+
// #415: a local named export (`export { a }`) has no `from` clause.
|
|
864
|
+
if let Some(from) = from {
|
|
865
|
+
self.buf.push_str(" from \"");
|
|
866
|
+
self.buf.push_str(from);
|
|
867
|
+
self.buf.push('"');
|
|
868
|
+
}
|
|
850
869
|
}
|
|
851
870
|
}
|
|
852
871
|
}
|
|
@@ -118,6 +118,19 @@ fn optimize_statement(stmt: &Statement) -> Statement {
|
|
|
118
118
|
body: Box::new(optimize_statement(body)),
|
|
119
119
|
span: *span,
|
|
120
120
|
},
|
|
121
|
+
Statement::ForIn {
|
|
122
|
+
name,
|
|
123
|
+
name_span,
|
|
124
|
+
object,
|
|
125
|
+
body,
|
|
126
|
+
span,
|
|
127
|
+
} => Statement::ForIn {
|
|
128
|
+
name: Arc::clone(name),
|
|
129
|
+
name_span: *name_span,
|
|
130
|
+
object: optimize_expr(object),
|
|
131
|
+
body: Box::new(optimize_statement(body)),
|
|
132
|
+
span: *span,
|
|
133
|
+
},
|
|
121
134
|
Statement::Return { value, span } => Statement::Return {
|
|
122
135
|
value: value.as_ref().map(optimize_expr),
|
|
123
136
|
span: *span,
|
|
@@ -500,12 +513,18 @@ fn optimize_expr(expr: &Expr) -> Expr {
|
|
|
500
513
|
value: Box::new(optimize_expr(value)),
|
|
501
514
|
span: *span,
|
|
502
515
|
},
|
|
503
|
-
Expr::ArrowFunction {
|
|
516
|
+
Expr::ArrowFunction {
|
|
517
|
+
async_,
|
|
518
|
+
params,
|
|
519
|
+
body,
|
|
520
|
+
span,
|
|
521
|
+
} => {
|
|
504
522
|
let opt_body = match body {
|
|
505
523
|
ArrowBody::Expr(e) => ArrowBody::Expr(Box::new(optimize_expr(e))),
|
|
506
524
|
ArrowBody::Block(s) => ArrowBody::Block(Box::new(optimize_statement(s))),
|
|
507
525
|
};
|
|
508
526
|
Expr::ArrowFunction {
|
|
527
|
+
async_: *async_,
|
|
509
528
|
params: params.clone(),
|
|
510
529
|
body: opt_body,
|
|
511
530
|
span: *span,
|
|
@@ -60,6 +60,47 @@ mod tests {
|
|
|
60
60
|
assert!(parse("fn f() { if (a) { if (b) { return 1 } } }").is_ok());
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
/// #244: loose equality `==` / `!=` is rejected uniformly at parse (across every backend), with an
|
|
64
|
+
/// actionable message pointing at the strict form. `===` / `!==` still parse.
|
|
65
|
+
#[test]
|
|
66
|
+
fn loose_equality_is_rejected_at_parse() {
|
|
67
|
+
let eq = parse("let b = 1 == 1").unwrap_err();
|
|
68
|
+
assert!(eq.contains("'=='") && eq.contains("'==='"), "got: {eq}");
|
|
69
|
+
let ne = parse("let b = 1 != 2").unwrap_err();
|
|
70
|
+
assert!(ne.contains("'!='") && ne.contains("'!=='"), "got: {ne}");
|
|
71
|
+
// Rejected wherever an expression is parsed, not just at top level.
|
|
72
|
+
assert!(parse("fn f(x) { if (x == 0) { return 1 } return 0 }").is_err());
|
|
73
|
+
assert!(parse("let y = a != null && a.b").is_err());
|
|
74
|
+
// Strict forms are unaffected.
|
|
75
|
+
assert!(parse("let b = 1 === 1").is_ok());
|
|
76
|
+
assert!(parse("let b = 1 !== 2").is_ok());
|
|
77
|
+
assert!(parse("let y = a !== null && a.b").is_ok());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/// #428: `async` in expression position parses as an async arrow (paren and single-param forms);
|
|
81
|
+
/// non-async arrows keep `async_: false`.
|
|
82
|
+
#[test]
|
|
83
|
+
fn async_arrow_expressions_parse() {
|
|
84
|
+
use tishlang_ast::Expr;
|
|
85
|
+
let arrow_async = |src: &str| -> bool {
|
|
86
|
+
let p = parse(src).expect("parse");
|
|
87
|
+
let Some(Statement::VarDecl { init: Some(e), .. }) = p.statements.first() else {
|
|
88
|
+
panic!("expected `let x = <arrow>`");
|
|
89
|
+
};
|
|
90
|
+
match e {
|
|
91
|
+
Expr::ArrowFunction { async_, .. } => *async_,
|
|
92
|
+
other => panic!("expected ArrowFunction, got {other:?}"),
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
assert!(arrow_async("let f = async () => 1"), "async paren arrow");
|
|
96
|
+
assert!(arrow_async("let g = async x => x + 1"), "async single-param arrow");
|
|
97
|
+
assert!(arrow_async("let h = async (a, b) => a + b"), "async multi-param arrow");
|
|
98
|
+
assert!(!arrow_async("let f = () => 1"), "plain arrow is not async");
|
|
99
|
+
assert!(!arrow_async("let g = x => x"), "plain single-param arrow is not async");
|
|
100
|
+
// `async` not followed by an arrow in expr position is an error, not a silent identifier.
|
|
101
|
+
assert!(parse("let x = async 5").is_err());
|
|
102
|
+
}
|
|
103
|
+
|
|
63
104
|
#[test]
|
|
64
105
|
fn test_async_fn_parse() {
|
|
65
106
|
let program = parse("async fn foo() { }").expect("parse async fn");
|