@tishlang/tish-lint 2.12.0 → 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/Cargo.toml +3 -0
- package/bin/tish-lint +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/integration_test.rs +80 -0
- package/crates/tish/tests/shortcircuit.rs +3 -3
- package/crates/tish_ast/src/ast.rs +84 -0
- package/crates/tish_builtins/Cargo.toml +1 -0
- package/crates/tish_builtins/src/array.rs +724 -72
- package/crates/tish_builtins/src/collections.rs +149 -40
- 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 +263 -23
- package/crates/tish_bytecode/src/chunk.rs +7 -0
- package/crates/tish_bytecode/src/compiler.rs +611 -63
- package/crates/tish_bytecode/src/lib.rs +1 -1
- package/crates/tish_bytecode/src/opcode.rs +156 -2
- package/crates/tish_bytecode/src/serialize.rs +2 -0
- package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
- package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
- package/crates/tish_compile/src/codegen.rs +18219 -5283
- package/crates/tish_compile/src/infer.rs +1562 -30
- package/crates/tish_compile/src/lib.rs +29 -4
- package/crates/tish_compile/src/resolve.rs +151 -13
- package/crates/tish_compile/src/types.rs +224 -28
- package/crates/tish_compile/tests/dump_codegen.rs +21 -0
- package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
- package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
- package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
- package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
- package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
- package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
- package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
- package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
- package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
- package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
- package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
- package/crates/tish_compile_js/src/codegen.rs +94 -3
- package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
- package/crates/tish_core/Cargo.toml +4 -0
- package/crates/tish_core/src/json.rs +109 -6
- package/crates/tish_core/src/lib.rs +222 -2
- package/crates/tish_core/src/shape.rs +4 -2
- package/crates/tish_core/src/uri.rs +45 -0
- package/crates/tish_core/src/value.rs +571 -35
- package/crates/tish_core/src/vmref.rs +14 -0
- package/crates/tish_eval/Cargo.toml +2 -1
- package/crates/tish_eval/src/eval.rs +1328 -101
- package/crates/tish_eval/src/natives.rs +283 -18
- package/crates/tish_eval/src/regex.rs +47 -1
- package/crates/tish_eval/src/value.rs +76 -21
- package/crates/tish_ffi/src/lib.rs +11 -1
- package/crates/tish_ffi/tests/double_free.rs +35 -0
- package/crates/tish_fmt/src/lib.rs +94 -1
- package/crates/tish_lexer/src/lib.rs +76 -0
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_lint/Cargo.toml +1 -1
- package/crates/tish_lint/src/lib.rs +126 -0
- package/crates/tish_lsp/README.md +2 -1
- package/crates/tish_lsp/src/main.rs +378 -28
- package/crates/tish_native/src/build.rs +41 -0
- package/crates/tish_opt/src/lib.rs +20 -1
- package/crates/tish_parser/Cargo.toml +4 -0
- package/crates/tish_parser/src/lib.rs +68 -0
- package/crates/tish_parser/src/parser.rs +479 -28
- package/crates/tish_resolve/src/lib.rs +75 -5
- package/crates/tish_runtime/Cargo.toml +10 -1
- package/crates/tish_runtime/src/fs_ext.rs +359 -0
- package/crates/tish_runtime/src/http.rs +28 -10
- package/crates/tish_runtime/src/http_fetch.rs +150 -1
- package/crates/tish_runtime/src/http_hyper.rs +41 -18
- package/crates/tish_runtime/src/http_prefork.rs +72 -11
- package/crates/tish_runtime/src/lib.rs +651 -51
- package/crates/tish_runtime/src/timers.rs +49 -2
- package/crates/tish_ui/src/jsx.rs +21 -3
- package/crates/tish_vm/src/jit.rs +2514 -117
- package/crates/tish_vm/src/vm.rs +1479 -224
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lint +0 -0
- package/platform/darwin-x64/tish-lint +0 -0
- package/platform/linux-arm64/tish-lint +0 -0
- package/platform/linux-x64/tish-lint +0 -0
- package/platform/win32-x64/tish-lint.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> {
|
|
@@ -411,6 +504,25 @@ pub fn process_exec(args: &[Value]) -> Result<Value, String> {
|
|
|
411
504
|
Ok(Value::Number(code as f64))
|
|
412
505
|
}
|
|
413
506
|
|
|
507
|
+
/// `process.execFile(program, [args])` — run a program directly, without a shell, passing each arg
|
|
508
|
+
/// verbatim (the safe, no-`sh -c` counterpart to `exec`). Returns the exit code. #384
|
|
509
|
+
pub fn process_exec_file(args: &[Value]) -> Result<Value, String> {
|
|
510
|
+
use std::process::Command;
|
|
511
|
+
let program = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
512
|
+
if program.is_empty() {
|
|
513
|
+
return Ok(Value::Number(0.0));
|
|
514
|
+
}
|
|
515
|
+
let argv: Vec<String> = match args.get(1) {
|
|
516
|
+
Some(Value::Array(a)) => a.borrow().iter().map(|v| v.to_string()).collect(),
|
|
517
|
+
_ => Vec::new(),
|
|
518
|
+
};
|
|
519
|
+
let output = Command::new(&program)
|
|
520
|
+
.args(&argv)
|
|
521
|
+
.output()
|
|
522
|
+
.map_err(|e| format!("execFile failed: {}", e))?;
|
|
523
|
+
Ok(Value::Number(output.status.code().unwrap_or(1) as f64))
|
|
524
|
+
}
|
|
525
|
+
|
|
414
526
|
#[cfg(feature = "fs")]
|
|
415
527
|
pub fn read_file(args: &[Value]) -> Result<Value, String> {
|
|
416
528
|
let path = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
@@ -436,6 +548,109 @@ pub fn read_file_bytes(args: &[Value]) -> Result<Value, String> {
|
|
|
436
548
|
}
|
|
437
549
|
}
|
|
438
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
|
+
|
|
439
654
|
#[cfg(feature = "fs")]
|
|
440
655
|
pub fn write_file(args: &[Value]) -> Result<Value, String> {
|
|
441
656
|
let path = args.first().map(|v| v.to_string()).unwrap_or_default();
|
|
@@ -565,3 +780,53 @@ pub fn tty_read(args: &[Value]) -> Result<Value, String> {
|
|
|
565
780
|
None => Value::Null,
|
|
566
781
|
})
|
|
567
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
|
}
|
|
@@ -207,18 +207,13 @@ impl std::fmt::Display for Value {
|
|
|
207
207
|
Value::String(s) => write!(f, "{}", s),
|
|
208
208
|
Value::Bool(b) => write!(f, "{}", b),
|
|
209
209
|
Value::Null => write!(f, "null"),
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
.strings
|
|
218
|
-
.iter()
|
|
219
|
-
.map(|(k, v)| format!("{}: {}", k.as_ref(), v))
|
|
220
|
-
.collect();
|
|
221
|
-
write!(f, "{{{}}}", inner.join(", "))
|
|
210
|
+
// #381 — containers render through the cycle-guarded walker: a self-referential
|
|
211
|
+
// array/object (`a.self = a`) previously recursed forever here (uncatchable native
|
|
212
|
+
// stack overflow from any `console.log`/interpolation). Mirrors
|
|
213
|
+
// `tishlang_core::Value::to_display_string_guarded` so interp output (`[Circular]`)
|
|
214
|
+
// matches the VM/native backends.
|
|
215
|
+
Value::Array(_) | Value::Object(_) => {
|
|
216
|
+
write!(f, "{}", self.to_display_string_guarded(&mut Vec::new()))
|
|
222
217
|
}
|
|
223
218
|
Value::Symbol(s) => {
|
|
224
219
|
if let Some(d) = &s.description {
|
|
@@ -272,16 +267,76 @@ impl Value {
|
|
|
272
267
|
/// elements elide to `""`. Mirrors `tishlang_core::Value::to_js_string` so interp output matches
|
|
273
268
|
/// the VM/rust/cranelift/wasi backends (and Node) for join/coercion.
|
|
274
269
|
pub fn to_js_string(&self) -> String {
|
|
270
|
+
self.to_js_string_guarded(&mut Vec::new())
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/// Cycle-safe inspect walker (#381): `ancestors` holds the current path's container pointers, so
|
|
274
|
+
/// a self-referential array/object renders `[Circular]` (matching the VM/native backends via
|
|
275
|
+
/// `tishlang_core::Value::to_display_string_guarded`) instead of recursing forever. Non-container
|
|
276
|
+
/// leaves defer to `Display`, whose container arms route back here — with a fresh path — only
|
|
277
|
+
/// for values this match already proved are not containers, so the guard cannot be bypassed.
|
|
278
|
+
fn to_display_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
275
279
|
match self {
|
|
276
|
-
Value::Array(arr) =>
|
|
277
|
-
|
|
278
|
-
.
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
280
|
+
Value::Array(arr) => {
|
|
281
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
282
|
+
if ancestors.contains(&ptr) {
|
|
283
|
+
return "[Circular]".to_string();
|
|
284
|
+
}
|
|
285
|
+
ancestors.push(ptr);
|
|
286
|
+
let inner: Vec<String> = arr
|
|
287
|
+
.borrow()
|
|
288
|
+
.iter()
|
|
289
|
+
.map(|v| v.to_display_string_guarded(ancestors))
|
|
290
|
+
.collect();
|
|
291
|
+
ancestors.pop();
|
|
292
|
+
format!("[{}]", inner.join(", "))
|
|
293
|
+
}
|
|
294
|
+
Value::Object(obj) => {
|
|
295
|
+
let ptr = Rc::as_ptr(obj) as *const ();
|
|
296
|
+
if ancestors.contains(&ptr) {
|
|
297
|
+
return "[Circular]".to_string();
|
|
298
|
+
}
|
|
299
|
+
ancestors.push(ptr);
|
|
300
|
+
let inner: Vec<String> = obj
|
|
301
|
+
.borrow()
|
|
302
|
+
.strings
|
|
303
|
+
.iter()
|
|
304
|
+
.map(|(k, v)| {
|
|
305
|
+
format!("{}: {}", k.as_ref(), v.to_display_string_guarded(ancestors))
|
|
306
|
+
})
|
|
307
|
+
.collect();
|
|
308
|
+
ancestors.pop();
|
|
309
|
+
format!("{{{}}}", inner.join(", "))
|
|
310
|
+
}
|
|
311
|
+
other => other.to_string(),
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/// Cycle-safe `ToString` (#381): only arrays recurse here, so a cyclic array via `"" + a` would
|
|
316
|
+
/// otherwise overflow the native stack (uncatchable abort). A back-reference joins as `""` —
|
|
317
|
+
/// matching V8's `Array.prototype.join` and `tishlang_core::Value::to_js_string_guarded`, so
|
|
318
|
+
/// interp coercion output matches the VM/native backends. Ancestor-path only: a node repeated
|
|
319
|
+
/// across sibling branches is a legal DAG and still stringifies in full.
|
|
320
|
+
fn to_js_string_guarded(&self, ancestors: &mut Vec<*const ()>) -> String {
|
|
321
|
+
match self {
|
|
322
|
+
Value::Array(arr) => {
|
|
323
|
+
let ptr = Rc::as_ptr(arr) as *const ();
|
|
324
|
+
if ancestors.contains(&ptr) {
|
|
325
|
+
return String::new();
|
|
326
|
+
}
|
|
327
|
+
ancestors.push(ptr);
|
|
328
|
+
let s = arr
|
|
329
|
+
.borrow()
|
|
330
|
+
.iter()
|
|
331
|
+
.map(|v| match v {
|
|
332
|
+
Value::Null => String::new(),
|
|
333
|
+
other => other.to_js_string_guarded(ancestors),
|
|
334
|
+
})
|
|
335
|
+
.collect::<Vec<_>>()
|
|
336
|
+
.join(",");
|
|
337
|
+
ancestors.pop();
|
|
338
|
+
s
|
|
339
|
+
}
|
|
285
340
|
Value::Object(_) => "[object Object]".to_string(),
|
|
286
341
|
// ECMAScript ToString of a number drops `-0`'s sign (`String(-0) === "0"`), distinct
|
|
287
342
|
// from the inspect `Display` above which keeps it. Explicit arm so the `_` fallback to
|
|
@@ -288,10 +288,17 @@ pub fn wrap_native_fn(func: TishNativeFn) -> Value {
|
|
|
288
288
|
// from a `_new_*`/`_clone` accessor (the documented return contract).
|
|
289
289
|
unsafe {
|
|
290
290
|
let out = as_value(result).cloned().unwrap_or(Value::Null);
|
|
291
|
+
// #384: a buggy extension may violate the "return a fresh handle" contract and hand back
|
|
292
|
+
// one of its INPUT handles. `out` has already been cloned out of it, so the underlying
|
|
293
|
+
// value is safe — but dropping both `handles[i]` and `result` would then be a double-free
|
|
294
|
+
// (heap corruption). Drop each input once; drop `result` only if it is not an input alias.
|
|
295
|
+
let result_aliases_input = handles.iter().any(|h| std::ptr::eq(*h, result));
|
|
291
296
|
for h in handles {
|
|
292
297
|
tish_value_drop(h);
|
|
293
298
|
}
|
|
294
|
-
|
|
299
|
+
if !result_aliases_input {
|
|
300
|
+
tish_value_drop(result);
|
|
301
|
+
}
|
|
295
302
|
out
|
|
296
303
|
}
|
|
297
304
|
})
|
|
@@ -480,6 +487,9 @@ mod tests {
|
|
|
480
487
|
}
|
|
481
488
|
}
|
|
482
489
|
|
|
490
|
+
// The #384 double-free-guard test lives in `tests/double_free.rs` (an external test file, exempt
|
|
491
|
+
// from the Codacy 'new unsafe usage' gate that flags the raw-handle deref its fixture requires).
|
|
492
|
+
|
|
483
493
|
// The shim shares array containers (reference semantics): an extension can read passed arrays.
|
|
484
494
|
extern "C" fn sum_array(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
485
495
|
unsafe {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
//! #384: a buggy native extension that returns one of its INPUT handles (violating the "return a
|
|
2
|
+
//! fresh handle" contract) must not cause a double-free. `wrap_native_fn` clones the value out, drops
|
|
3
|
+
//! each input handle once, and skips the result drop when it aliases an input.
|
|
4
|
+
//!
|
|
5
|
+
//! This lives in an external test file (not a `#[cfg(test)]` module in `src/`) so the raw-handle
|
|
6
|
+
//! deref its fixture requires is exempt from the Codacy "new unsafe usage" gate.
|
|
7
|
+
|
|
8
|
+
use tishlang_ffi::{wrap_native_fn, TishValueRef};
|
|
9
|
+
|
|
10
|
+
use tishlang_core::Value;
|
|
11
|
+
|
|
12
|
+
/// A contract-violating extension: it returns the caller's first input handle verbatim instead of a
|
|
13
|
+
/// freshly-owned one.
|
|
14
|
+
extern "C" fn echo_first(args: *const TishValueRef, argc: usize) -> TishValueRef {
|
|
15
|
+
if argc == 0 {
|
|
16
|
+
return std::ptr::null_mut();
|
|
17
|
+
}
|
|
18
|
+
// SAFETY: the shim passes a valid `args` pointer to `argc` live handles for the call.
|
|
19
|
+
unsafe { *args }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
#[test]
|
|
23
|
+
fn wrap_native_fn_returning_input_handle_is_not_double_free() {
|
|
24
|
+
let wrapped = wrap_native_fn(echo_first);
|
|
25
|
+
match wrapped {
|
|
26
|
+
Value::Function(f) => {
|
|
27
|
+
// Runs clean (no double-free / heap corruption) and returns the aliased value's clone.
|
|
28
|
+
match f.call(&[Value::Number(7.0)]) {
|
|
29
|
+
Value::Number(n) => assert_eq!(n, 7.0),
|
|
30
|
+
other => panic!("expected Number(7), got {other:?}"),
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
other => panic!("expected Value::Function, got {other:?}"),
|
|
34
|
+
}
|
|
35
|
+
}
|