@tishlang/tish-format 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-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/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/Cargo.toml +1 -1
- 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/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-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
|
@@ -177,6 +177,13 @@ fn collect_stmt(
|
|
|
177
177
|
iterable,
|
|
178
178
|
body,
|
|
179
179
|
..
|
|
180
|
+
}
|
|
181
|
+
| Statement::ForIn {
|
|
182
|
+
name,
|
|
183
|
+
name_span,
|
|
184
|
+
object: iterable,
|
|
185
|
+
body,
|
|
186
|
+
..
|
|
180
187
|
} => {
|
|
181
188
|
consider(source, lsp_line, lsp_char, name_span, name.clone(), best);
|
|
182
189
|
collect_expr(iterable, source, lsp_line, lsp_char, best);
|
|
@@ -286,6 +293,7 @@ fn collect_stmt(
|
|
|
286
293
|
collect_stmt(inner, source, lsp_line, lsp_char, best)
|
|
287
294
|
}
|
|
288
295
|
ExportDeclaration::Default(e) => collect_expr(e, source, lsp_line, lsp_char, best),
|
|
296
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
289
297
|
},
|
|
290
298
|
Statement::TypeAlias {
|
|
291
299
|
name, name_span, ..
|
|
@@ -938,7 +946,12 @@ fn member_chain_collect_stmt(
|
|
|
938
946
|
}
|
|
939
947
|
member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
|
|
940
948
|
}
|
|
941
|
-
Statement::ForOf { iterable, body, .. }
|
|
949
|
+
Statement::ForOf { iterable, body, .. }
|
|
950
|
+
| Statement::ForIn {
|
|
951
|
+
object: iterable,
|
|
952
|
+
body,
|
|
953
|
+
..
|
|
954
|
+
} => {
|
|
942
955
|
member_chain_collect_expr(iterable, source, lsp_line, lsp_char, best);
|
|
943
956
|
member_chain_collect_stmt(body, source, lsp_line, lsp_char, best);
|
|
944
957
|
}
|
|
@@ -1009,6 +1022,7 @@ fn member_chain_collect_stmt(
|
|
|
1009
1022
|
ExportDeclaration::Default(e) => {
|
|
1010
1023
|
member_chain_collect_expr(e, source, lsp_line, lsp_char, best)
|
|
1011
1024
|
}
|
|
1025
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
1012
1026
|
},
|
|
1013
1027
|
Statement::Import { .. }
|
|
1014
1028
|
| Statement::Break { .. }
|
|
@@ -1412,6 +1426,13 @@ fn walk_stmt_resolve(
|
|
|
1412
1426
|
iterable,
|
|
1413
1427
|
body,
|
|
1414
1428
|
..
|
|
1429
|
+
}
|
|
1430
|
+
| Statement::ForIn {
|
|
1431
|
+
name,
|
|
1432
|
+
name_span,
|
|
1433
|
+
object: iterable,
|
|
1434
|
+
body,
|
|
1435
|
+
..
|
|
1415
1436
|
} => {
|
|
1416
1437
|
if *name_span == tgt_span && name.as_ref() == target.name.as_ref() {
|
|
1417
1438
|
return Some(*name_span);
|
|
@@ -1598,6 +1619,7 @@ fn walk_stmt_resolve(
|
|
|
1598
1619
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
1599
1620
|
ExportDeclaration::Named(inner) => walk_stmt_resolve(inner, scopes, target),
|
|
1600
1621
|
ExportDeclaration::Default(e) => walk_expr_resolve(e, scopes, target),
|
|
1622
|
+
ExportDeclaration::ReExport { .. } => None,
|
|
1601
1623
|
},
|
|
1602
1624
|
Statement::TypeAlias {
|
|
1603
1625
|
name, name_span, ..
|
|
@@ -2025,6 +2047,13 @@ fn check_unresolved_stmt(
|
|
|
2025
2047
|
iterable,
|
|
2026
2048
|
body,
|
|
2027
2049
|
..
|
|
2050
|
+
}
|
|
2051
|
+
| Statement::ForIn {
|
|
2052
|
+
name,
|
|
2053
|
+
name_span,
|
|
2054
|
+
object: iterable,
|
|
2055
|
+
body,
|
|
2056
|
+
..
|
|
2028
2057
|
} => {
|
|
2029
2058
|
check_unresolved_expr(iterable, scopes, out);
|
|
2030
2059
|
scopes.push();
|
|
@@ -2156,6 +2185,7 @@ fn check_unresolved_stmt(
|
|
|
2156
2185
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
2157
2186
|
ExportDeclaration::Named(inner) => check_unresolved_stmt(inner, scopes, out),
|
|
2158
2187
|
ExportDeclaration::Default(e) => check_unresolved_expr(e, scopes, out),
|
|
2188
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
2159
2189
|
},
|
|
2160
2190
|
Statement::TypeAlias {
|
|
2161
2191
|
name, name_span, ..
|
|
@@ -2489,6 +2519,7 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
|
|
|
2489
2519
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
2490
2520
|
ExportDeclaration::Named(inner) => enumerate_stmt(inner, true, out),
|
|
2491
2521
|
ExportDeclaration::Default(e) => enumerate_expr(e, exported, out),
|
|
2522
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
2492
2523
|
},
|
|
2493
2524
|
Statement::VarDecl {
|
|
2494
2525
|
name,
|
|
@@ -2551,6 +2582,13 @@ fn enumerate_stmt(stmt: &Statement, exported: bool, out: &mut Vec<BindingSite>)
|
|
|
2551
2582
|
iterable,
|
|
2552
2583
|
body,
|
|
2553
2584
|
..
|
|
2585
|
+
}
|
|
2586
|
+
| Statement::ForIn {
|
|
2587
|
+
name,
|
|
2588
|
+
name_span,
|
|
2589
|
+
object: iterable,
|
|
2590
|
+
body,
|
|
2591
|
+
..
|
|
2554
2592
|
} => {
|
|
2555
2593
|
out.push(BindingSite {
|
|
2556
2594
|
name: name.clone(),
|
|
@@ -2726,7 +2764,12 @@ fn jsx_tags_stmt(s: &Statement, out: &mut std::collections::HashSet<Arc<str>>) {
|
|
|
2726
2764
|
}
|
|
2727
2765
|
jsx_tags_stmt(body, out);
|
|
2728
2766
|
}
|
|
2729
|
-
Statement::ForOf { iterable, body, .. }
|
|
2767
|
+
Statement::ForOf { iterable, body, .. }
|
|
2768
|
+
| Statement::ForIn {
|
|
2769
|
+
object: iterable,
|
|
2770
|
+
body,
|
|
2771
|
+
..
|
|
2772
|
+
} => {
|
|
2730
2773
|
jsx_tags_expr(iterable, out);
|
|
2731
2774
|
jsx_tags_stmt(body, out);
|
|
2732
2775
|
}
|
|
@@ -2788,6 +2831,7 @@ fn jsx_tags_stmt(s: &Statement, out: &mut std::collections::HashSet<Arc<str>>) {
|
|
|
2788
2831
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
2789
2832
|
ExportDeclaration::Named(inner) => jsx_tags_stmt(inner, out),
|
|
2790
2833
|
ExportDeclaration::Default(e) => jsx_tags_expr(e, out),
|
|
2834
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
2791
2835
|
},
|
|
2792
2836
|
Statement::Import { .. }
|
|
2793
2837
|
| Statement::Break { .. }
|
|
@@ -3051,6 +3095,13 @@ fn collect_block_locals(
|
|
|
3051
3095
|
body,
|
|
3052
3096
|
span,
|
|
3053
3097
|
..
|
|
3098
|
+
}
|
|
3099
|
+
| Statement::ForIn {
|
|
3100
|
+
name,
|
|
3101
|
+
name_span,
|
|
3102
|
+
body,
|
|
3103
|
+
span,
|
|
3104
|
+
..
|
|
3054
3105
|
} => {
|
|
3055
3106
|
if pos::span_contains_lsp_position(source, span, lsp_line, lsp_char)
|
|
3056
3107
|
&& pos::span_contains_lsp_position(
|
|
@@ -3422,6 +3473,12 @@ fn walk_stmt_completion(
|
|
|
3422
3473
|
iterable,
|
|
3423
3474
|
body,
|
|
3424
3475
|
..
|
|
3476
|
+
}
|
|
3477
|
+
| Statement::ForIn {
|
|
3478
|
+
name,
|
|
3479
|
+
object: iterable,
|
|
3480
|
+
body,
|
|
3481
|
+
..
|
|
3425
3482
|
} => {
|
|
3426
3483
|
walk_expr_completion(iterable, source, lsp_line, lsp_char, stack, best);
|
|
3427
3484
|
let body_sp = body.as_ref().span();
|
|
@@ -3532,6 +3589,7 @@ fn walk_stmt_completion(
|
|
|
3532
3589
|
ExportDeclaration::Default(e) => {
|
|
3533
3590
|
walk_expr_completion(e, source, lsp_line, lsp_char, stack, best);
|
|
3534
3591
|
}
|
|
3592
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
3535
3593
|
},
|
|
3536
3594
|
}
|
|
3537
3595
|
}
|
|
@@ -3893,7 +3951,12 @@ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
|
|
|
3893
3951
|
}
|
|
3894
3952
|
tref_stmt(body, out);
|
|
3895
3953
|
}
|
|
3896
|
-
Statement::ForOf { iterable, body, .. }
|
|
3954
|
+
Statement::ForOf { iterable, body, .. }
|
|
3955
|
+
| Statement::ForIn {
|
|
3956
|
+
object: iterable,
|
|
3957
|
+
body,
|
|
3958
|
+
..
|
|
3959
|
+
} => {
|
|
3897
3960
|
tref_expr(iterable, out);
|
|
3898
3961
|
tref_stmt(body, out);
|
|
3899
3962
|
}
|
|
@@ -3945,6 +4008,7 @@ fn tref_stmt(stmt: &Statement, out: &mut Vec<TypeOcc>) {
|
|
|
3945
4008
|
Statement::Export { declaration, .. } => match declaration.as_ref() {
|
|
3946
4009
|
ExportDeclaration::Named(inner) => tref_stmt(inner, out),
|
|
3947
4010
|
ExportDeclaration::Default(e) => tref_expr(e, out),
|
|
4011
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
3948
4012
|
},
|
|
3949
4013
|
_ => {}
|
|
3950
4014
|
}
|
|
@@ -4053,7 +4117,12 @@ fn refs_stmt(
|
|
|
4053
4117
|
}
|
|
4054
4118
|
refs_stmt(body, program, source, name, def_span, out);
|
|
4055
4119
|
}
|
|
4056
|
-
Statement::ForOf { iterable, body, .. }
|
|
4120
|
+
Statement::ForOf { iterable, body, .. }
|
|
4121
|
+
| Statement::ForIn {
|
|
4122
|
+
object: iterable,
|
|
4123
|
+
body,
|
|
4124
|
+
..
|
|
4125
|
+
} => {
|
|
4057
4126
|
refs_expr(iterable, program, source, name, def_span, out);
|
|
4058
4127
|
refs_stmt(body, program, source, name, def_span, out);
|
|
4059
4128
|
}
|
|
@@ -4132,6 +4201,7 @@ fn refs_stmt(
|
|
|
4132
4201
|
refs_stmt(inner, program, source, name, def_span, out)
|
|
4133
4202
|
}
|
|
4134
4203
|
ExportDeclaration::Default(e) => refs_expr(e, program, source, name, def_span, out),
|
|
4204
|
+
ExportDeclaration::ReExport { .. } => {}
|
|
4135
4205
|
},
|
|
4136
4206
|
Statement::Import { .. }
|
|
4137
4207
|
| Statement::Break { .. }
|
|
@@ -4648,7 +4718,7 @@ mod tests {
|
|
|
4648
4718
|
|
|
4649
4719
|
#[test]
|
|
4650
4720
|
fn mutual_recursion_not_unresolved_or_unused() {
|
|
4651
|
-
let src = "fn even(n) { if (n
|
|
4721
|
+
let src = "fn even(n) { if (n === 0) { return true } return odd(n - 1) }\nfn odd(n) { if (n === 0) { return false } return even(n - 1) }\neven(10)\n";
|
|
4652
4722
|
let program = parse(src).expect("parse");
|
|
4653
4723
|
assert!(
|
|
4654
4724
|
collect_unresolved_identifiers(&program).is_empty(),
|
|
@@ -52,7 +52,9 @@ http-hyper = [
|
|
|
52
52
|
"tokio/rt",
|
|
53
53
|
"tokio/io-util",
|
|
54
54
|
]
|
|
55
|
-
fs
|
|
55
|
+
# `fs/promises` returns settled promises, so fs pulls in the promise machinery.
|
|
56
|
+
# `mkdtemp` needs a random suffix (predictable/timestamp names are a temp-dir security smell).
|
|
57
|
+
fs = ["promise", "dep:rand"]
|
|
56
58
|
process = []
|
|
57
59
|
# Interactive terminal I/O (issue #101): raw mode, key/resize events, size, alt screen.
|
|
58
60
|
tty = ["dep:crossterm"]
|
|
@@ -81,6 +83,8 @@ reqwest = { version = "0.13.2", default-features = false, features = ["rustls",
|
|
|
81
83
|
futures = { version = "0.3.32", optional = true }
|
|
82
84
|
bytes = { version = "1", optional = true }
|
|
83
85
|
tiny_http = { version = "0.12.0", optional = true }
|
|
86
|
+
# fs feature only: random suffix for `mkdtemp`.
|
|
87
|
+
rand = { version = "0.10.1", optional = true }
|
|
84
88
|
socket2 = { version = "0.5", features = ["all"], optional = true }
|
|
85
89
|
libc = { version = "0.2", optional = true }
|
|
86
90
|
arc-swap = { version = "1", optional = true }
|
|
@@ -94,3 +98,8 @@ core_affinity = { version = "0.8", optional = true }
|
|
|
94
98
|
tokio-tungstenite = { version = "0.29", optional = true }
|
|
95
99
|
futures-util = { version = "0.3", optional = true }
|
|
96
100
|
lazy_static = { version = "1", optional = true }
|
|
101
|
+
|
|
102
|
+
# #381 native-codegen recursion guard: stack-pressure probe for guarded self-recursive fns.
|
|
103
|
+
# Non-wasm only — wasm traps on overflow inside its own sandbox, so the guard is a no-op there.
|
|
104
|
+
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
|
105
|
+
stacker = "0.1"
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
//! Node-compatible `fs` surface for `tish:fs` and the async `tish:fs/promises` module
|
|
2
|
+
//! (issue #122). Each operation has a single `*_core` returning `Result<Value, Value>`
|
|
3
|
+
//! (Ok = result, Err = an error value); a macro derives the synchronous export (returns the
|
|
4
|
+
//! value, or the error object on failure — tish's sync convention) and the promise export
|
|
5
|
+
//! (a fulfilled/rejected Promise — Node's `fs/promises` convention).
|
|
6
|
+
//!
|
|
7
|
+
//! Node names are primary (`readFileSync`, `statSync`, …); the existing tish names
|
|
8
|
+
//! (`readFile`, `readDir`, `fileExists`, `isDir`) are kept as aliases in the backends.
|
|
9
|
+
#![cfg(feature = "fs")]
|
|
10
|
+
|
|
11
|
+
use crate::promise::{promise_reject, promise_resolve};
|
|
12
|
+
use std::time::{SystemTime, UNIX_EPOCH};
|
|
13
|
+
use tishlang_builtins::helpers::make_error_value;
|
|
14
|
+
use tishlang_core::{ObjectMap, Value, VmRef};
|
|
15
|
+
|
|
16
|
+
fn path_arg(args: &[Value], i: usize) -> String {
|
|
17
|
+
args.get(i).map(|v| v.to_display_string()).unwrap_or_default()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/// Map an io::Error to the same error value `read_file` already produces.
|
|
21
|
+
fn io_err(e: std::io::Error) -> Value {
|
|
22
|
+
make_error_value(e)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn unwrap(r: Result<Value, Value>) -> Value {
|
|
26
|
+
match r {
|
|
27
|
+
Ok(v) => v,
|
|
28
|
+
Err(e) => e,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
fn settle(r: Result<Value, Value>) -> Value {
|
|
33
|
+
match r {
|
|
34
|
+
Ok(v) => promise_resolve(&[v]),
|
|
35
|
+
Err(e) => promise_reject(&[e]),
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// Node callback form: the trailing arg is `(err, result) => …`. Run the op on the remaining
|
|
40
|
+
/// args, invoke the callback with `(null, value)` on success or `(err, null)` on failure, and
|
|
41
|
+
/// return null. The op is synchronous under the hood, so the callback fires synchronously.
|
|
42
|
+
fn run_callback(core: fn(&[Value]) -> Result<Value, Value>, args: &[Value]) -> Value {
|
|
43
|
+
let cb = args.last().cloned().unwrap_or(Value::Null);
|
|
44
|
+
let op_args: &[Value] = if args.len() > 1 { &args[..args.len() - 1] } else { &[] };
|
|
45
|
+
let (err, data) = match core(op_args) {
|
|
46
|
+
Ok(v) => (Value::Null, v),
|
|
47
|
+
Err(e) => (e, Value::Null),
|
|
48
|
+
};
|
|
49
|
+
tishlang_core::value_call(&cb, &[err, data]);
|
|
50
|
+
Value::Null
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/// Generate the sync export (dual: a trailing function arg switches to the Node callback form)
|
|
54
|
+
/// and the promise export over a `*_core`.
|
|
55
|
+
macro_rules! fs_method {
|
|
56
|
+
($sync:ident, $promise:ident, $core:ident) => {
|
|
57
|
+
pub fn $sync(args: &[Value]) -> Value {
|
|
58
|
+
if let Some(Value::Function(_)) = args.last() {
|
|
59
|
+
return run_callback($core, args);
|
|
60
|
+
}
|
|
61
|
+
unwrap($core(args))
|
|
62
|
+
}
|
|
63
|
+
pub fn $promise(args: &[Value]) -> Value {
|
|
64
|
+
settle($core(args))
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
fn ms_since_epoch(t: std::io::Result<SystemTime>) -> f64 {
|
|
70
|
+
t.ok()
|
|
71
|
+
.and_then(|st| st.duration_since(UNIX_EPOCH).ok())
|
|
72
|
+
.map(|d| d.as_secs_f64() * 1000.0)
|
|
73
|
+
.unwrap_or(0.0)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Build a Node-like `Stats` object from metadata (predicate methods + size + times + mode).
|
|
77
|
+
fn stats_object(md: &std::fs::Metadata) -> Value {
|
|
78
|
+
let mut m = ObjectMap::default();
|
|
79
|
+
let is_file = md.is_file();
|
|
80
|
+
let is_dir = md.is_dir();
|
|
81
|
+
let is_symlink = md.file_type().is_symlink();
|
|
82
|
+
m.insert("isFile".into(), Value::native(move |_| Value::Bool(is_file)));
|
|
83
|
+
m.insert("isDirectory".into(), Value::native(move |_| Value::Bool(is_dir)));
|
|
84
|
+
m.insert("isSymbolicLink".into(), Value::native(move |_| Value::Bool(is_symlink)));
|
|
85
|
+
m.insert("isBlockDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
86
|
+
m.insert("isCharacterDevice".into(), Value::native(|_| Value::Bool(false)));
|
|
87
|
+
m.insert("isFIFO".into(), Value::native(|_| Value::Bool(false)));
|
|
88
|
+
m.insert("isSocket".into(), Value::native(|_| Value::Bool(false)));
|
|
89
|
+
m.insert("size".into(), Value::Number(md.len() as f64));
|
|
90
|
+
m.insert("mtimeMs".into(), Value::Number(ms_since_epoch(md.modified())));
|
|
91
|
+
m.insert("atimeMs".into(), Value::Number(ms_since_epoch(md.accessed())));
|
|
92
|
+
m.insert("birthtimeMs".into(), Value::Number(ms_since_epoch(md.created())));
|
|
93
|
+
#[cfg(unix)]
|
|
94
|
+
{
|
|
95
|
+
use std::os::unix::fs::MetadataExt;
|
|
96
|
+
m.insert("mode".into(), Value::Number(md.mode() as f64));
|
|
97
|
+
m.insert("uid".into(), Value::Number(md.uid() as f64));
|
|
98
|
+
m.insert("gid".into(), Value::Number(md.gid() as f64));
|
|
99
|
+
m.insert("ino".into(), Value::Number(md.ino() as f64));
|
|
100
|
+
}
|
|
101
|
+
Value::object(m)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ── cores ─────────────────────────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
pub fn read_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
107
|
+
std::fs::read_to_string(path_arg(args, 0))
|
|
108
|
+
.map(|s| Value::String(s.into()))
|
|
109
|
+
.map_err(io_err)
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
pub fn read_file_bytes_core(args: &[Value]) -> Result<Value, Value> {
|
|
113
|
+
std::fs::read(path_arg(args, 0))
|
|
114
|
+
.map(|b| Value::Array(VmRef::new(b.into_iter().map(|x| Value::Number(x as f64)).collect())))
|
|
115
|
+
.map_err(io_err)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Write a string, or a byte array (numbers 0–255), to a file.
|
|
119
|
+
pub fn write_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
120
|
+
let path = path_arg(args, 0);
|
|
121
|
+
let res = match args.get(1) {
|
|
122
|
+
Some(Value::Array(a)) => {
|
|
123
|
+
let bytes: Vec<u8> = a
|
|
124
|
+
.borrow()
|
|
125
|
+
.iter()
|
|
126
|
+
.map(|v| if let Value::Number(n) = v { *n as u8 } else { 0 })
|
|
127
|
+
.collect();
|
|
128
|
+
std::fs::write(&path, bytes)
|
|
129
|
+
}
|
|
130
|
+
Some(v) => std::fs::write(&path, v.to_display_string()),
|
|
131
|
+
None => std::fs::write(&path, ""),
|
|
132
|
+
};
|
|
133
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
pub fn append_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
137
|
+
use std::io::Write;
|
|
138
|
+
let path = path_arg(args, 0);
|
|
139
|
+
let data = args.get(1).map(|v| v.to_display_string()).unwrap_or_default();
|
|
140
|
+
std::fs::OpenOptions::new()
|
|
141
|
+
.create(true)
|
|
142
|
+
.append(true)
|
|
143
|
+
.open(&path)
|
|
144
|
+
.and_then(|mut f| f.write_all(data.as_bytes()))
|
|
145
|
+
.map(|_| Value::Null)
|
|
146
|
+
.map_err(io_err)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
pub fn stat_core(args: &[Value]) -> Result<Value, Value> {
|
|
150
|
+
std::fs::metadata(path_arg(args, 0)).map(|m| stats_object(&m)).map_err(io_err)
|
|
151
|
+
}
|
|
152
|
+
pub fn lstat_core(args: &[Value]) -> Result<Value, Value> {
|
|
153
|
+
std::fs::symlink_metadata(path_arg(args, 0)).map(|m| stats_object(&m)).map_err(io_err)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
pub fn readdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
157
|
+
std::fs::read_dir(path_arg(args, 0))
|
|
158
|
+
.map(|entries| {
|
|
159
|
+
let names: Vec<Value> = entries
|
|
160
|
+
.filter_map(|e| e.ok())
|
|
161
|
+
.map(|e| Value::String(e.file_name().to_string_lossy().into()))
|
|
162
|
+
.collect();
|
|
163
|
+
Value::Array(VmRef::new(names))
|
|
164
|
+
})
|
|
165
|
+
.map_err(io_err)
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/// `mkdir(path[, { recursive }])` — recursive creates parents.
|
|
169
|
+
pub fn mkdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
170
|
+
let path = path_arg(args, 0);
|
|
171
|
+
let recursive = opt_bool(args.get(1), "recursive");
|
|
172
|
+
let res = if recursive {
|
|
173
|
+
std::fs::create_dir_all(&path)
|
|
174
|
+
} else {
|
|
175
|
+
std::fs::create_dir(&path)
|
|
176
|
+
};
|
|
177
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/// `rm(path[, { recursive }])` — file, or whole tree when recursive.
|
|
181
|
+
pub fn rm_core(args: &[Value]) -> Result<Value, Value> {
|
|
182
|
+
let path = path_arg(args, 0);
|
|
183
|
+
let recursive = opt_bool(args.get(1), "recursive");
|
|
184
|
+
let md = std::fs::symlink_metadata(&path);
|
|
185
|
+
let res = match md {
|
|
186
|
+
Ok(m) if m.is_dir() => {
|
|
187
|
+
if recursive {
|
|
188
|
+
std::fs::remove_dir_all(&path)
|
|
189
|
+
} else {
|
|
190
|
+
std::fs::remove_dir(&path)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
_ => std::fs::remove_file(&path),
|
|
194
|
+
};
|
|
195
|
+
res.map(|_| Value::Null).map_err(io_err)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
pub fn rmdir_core(args: &[Value]) -> Result<Value, Value> {
|
|
199
|
+
std::fs::remove_dir(path_arg(args, 0)).map(|_| Value::Null).map_err(io_err)
|
|
200
|
+
}
|
|
201
|
+
pub fn unlink_core(args: &[Value]) -> Result<Value, Value> {
|
|
202
|
+
std::fs::remove_file(path_arg(args, 0)).map(|_| Value::Null).map_err(io_err)
|
|
203
|
+
}
|
|
204
|
+
pub fn rename_core(args: &[Value]) -> Result<Value, Value> {
|
|
205
|
+
std::fs::rename(path_arg(args, 0), path_arg(args, 1)).map(|_| Value::Null).map_err(io_err)
|
|
206
|
+
}
|
|
207
|
+
pub fn copy_file_core(args: &[Value]) -> Result<Value, Value> {
|
|
208
|
+
std::fs::copy(path_arg(args, 0), path_arg(args, 1)).map(|_| Value::Null).map_err(io_err)
|
|
209
|
+
}
|
|
210
|
+
pub fn realpath_core(args: &[Value]) -> Result<Value, Value> {
|
|
211
|
+
std::fs::canonicalize(path_arg(args, 0))
|
|
212
|
+
.map(|p| Value::String(p.to_string_lossy().into()))
|
|
213
|
+
.map_err(io_err)
|
|
214
|
+
}
|
|
215
|
+
pub fn readlink_core(args: &[Value]) -> Result<Value, Value> {
|
|
216
|
+
std::fs::read_link(path_arg(args, 0))
|
|
217
|
+
.map(|p| Value::String(p.to_string_lossy().into()))
|
|
218
|
+
.map_err(io_err)
|
|
219
|
+
}
|
|
220
|
+
pub fn truncate_core(args: &[Value]) -> Result<Value, Value> {
|
|
221
|
+
let len = match args.get(1) {
|
|
222
|
+
Some(Value::Number(n)) => *n as u64,
|
|
223
|
+
_ => 0,
|
|
224
|
+
};
|
|
225
|
+
std::fs::OpenOptions::new()
|
|
226
|
+
.write(true)
|
|
227
|
+
.open(path_arg(args, 0))
|
|
228
|
+
.and_then(|f| f.set_len(len))
|
|
229
|
+
.map(|_| Value::Null)
|
|
230
|
+
.map_err(io_err)
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// `mkdtemp(prefix)` — create a uniquely-named temp dir and return its path. Node appends 6 RANDOM
|
|
234
|
+
/// characters; a timestamp suffix is both predictable (a temp-dir security smell) and collision-prone
|
|
235
|
+
/// under rapid calls. Use a random suffix and rely on `create_dir`'s exclusive semantics, retrying on
|
|
236
|
+
/// the astronomically-rare collision.
|
|
237
|
+
pub fn mkdtemp_core(args: &[Value]) -> Result<Value, Value> {
|
|
238
|
+
let prefix = path_arg(args, 0);
|
|
239
|
+
for _ in 0..16 {
|
|
240
|
+
// 10 base-36 chars derived from a random u64 (~51 bits of entropy).
|
|
241
|
+
let mut n: u64 = rand::random();
|
|
242
|
+
let mut suffix = String::with_capacity(10);
|
|
243
|
+
for _ in 0..10 {
|
|
244
|
+
suffix.push(char::from_digit((n % 36) as u32, 36).unwrap());
|
|
245
|
+
n /= 36;
|
|
246
|
+
}
|
|
247
|
+
let path = format!("{}{}", prefix, suffix);
|
|
248
|
+
match std::fs::create_dir(&path) {
|
|
249
|
+
Ok(()) => return Ok(Value::String(path.into())),
|
|
250
|
+
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => continue,
|
|
251
|
+
Err(e) => return Err(io_err(e)),
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
Err(io_err(std::io::Error::new(
|
|
255
|
+
std::io::ErrorKind::AlreadyExists,
|
|
256
|
+
"mkdtemp: could not create a unique temporary directory",
|
|
257
|
+
)))
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/// `cp(src, dest[, { recursive }])` — copy a file, or a directory tree when recursive.
|
|
261
|
+
pub fn cp_core(args: &[Value]) -> Result<Value, Value> {
|
|
262
|
+
let src = path_arg(args, 0);
|
|
263
|
+
let dest = path_arg(args, 1);
|
|
264
|
+
let recursive = opt_bool(args.get(2), "recursive");
|
|
265
|
+
copy_recursive(std::path::Path::new(&src), std::path::Path::new(&dest), recursive)
|
|
266
|
+
.map(|_| Value::Null)
|
|
267
|
+
.map_err(io_err)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn copy_recursive(src: &std::path::Path, dest: &std::path::Path, recursive: bool) -> std::io::Result<()> {
|
|
271
|
+
if src.is_dir() {
|
|
272
|
+
if !recursive {
|
|
273
|
+
return Err(std::io::Error::new(
|
|
274
|
+
std::io::ErrorKind::InvalidInput,
|
|
275
|
+
"cp on a directory requires { recursive: true }",
|
|
276
|
+
));
|
|
277
|
+
}
|
|
278
|
+
std::fs::create_dir_all(dest)?;
|
|
279
|
+
for entry in std::fs::read_dir(src)? {
|
|
280
|
+
let entry = entry?;
|
|
281
|
+
copy_recursive(&entry.path(), &dest.join(entry.file_name()), true)?;
|
|
282
|
+
}
|
|
283
|
+
Ok(())
|
|
284
|
+
} else {
|
|
285
|
+
if let Some(parent) = dest.parent() {
|
|
286
|
+
std::fs::create_dir_all(parent)?;
|
|
287
|
+
}
|
|
288
|
+
std::fs::copy(src, dest).map(|_| ())
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
fn opt_bool(v: Option<&Value>, key: &str) -> bool {
|
|
293
|
+
match v {
|
|
294
|
+
Some(Value::Object(o)) => o
|
|
295
|
+
.borrow()
|
|
296
|
+
.strings
|
|
297
|
+
.get(key)
|
|
298
|
+
.map(|b| b.is_truthy())
|
|
299
|
+
.unwrap_or(false),
|
|
300
|
+
_ => false,
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// ── sync + promise exports ──────────────────────────────────────────────────────────────────
|
|
305
|
+
|
|
306
|
+
fs_method!(read_file, read_file_promise, read_file_core);
|
|
307
|
+
fs_method!(read_file_bytes, read_file_bytes_promise, read_file_bytes_core);
|
|
308
|
+
fs_method!(write_file, write_file_promise, write_file_core);
|
|
309
|
+
fs_method!(append_file, append_file_promise, append_file_core);
|
|
310
|
+
fs_method!(stat, stat_promise, stat_core);
|
|
311
|
+
fs_method!(lstat, lstat_promise, lstat_core);
|
|
312
|
+
fs_method!(readdir, readdir_promise, readdir_core);
|
|
313
|
+
fs_method!(mkdir, mkdir_promise, mkdir_core);
|
|
314
|
+
fs_method!(rm, rm_promise, rm_core);
|
|
315
|
+
fs_method!(rmdir, rmdir_promise, rmdir_core);
|
|
316
|
+
fs_method!(unlink, unlink_promise, unlink_core);
|
|
317
|
+
fs_method!(rename, rename_promise, rename_core);
|
|
318
|
+
fs_method!(copy_file, copy_file_promise, copy_file_core);
|
|
319
|
+
fs_method!(realpath, realpath_promise, realpath_core);
|
|
320
|
+
fs_method!(readlink, readlink_promise, readlink_core);
|
|
321
|
+
fs_method!(truncate, truncate_promise, truncate_core);
|
|
322
|
+
fs_method!(mkdtemp, mkdtemp_promise, mkdtemp_core);
|
|
323
|
+
fs_method!(cp, cp_promise, cp_core);
|
|
324
|
+
|
|
325
|
+
// `exists` / `access` never error — they answer a boolean.
|
|
326
|
+
pub fn exists(args: &[Value]) -> Value {
|
|
327
|
+
Value::Bool(std::path::Path::new(&path_arg(args, 0)).exists())
|
|
328
|
+
}
|
|
329
|
+
pub fn exists_promise(args: &[Value]) -> Value {
|
|
330
|
+
promise_resolve(&[exists(args)])
|
|
331
|
+
}
|
|
332
|
+
/// `accessSync(path)` → true if the path exists (tish-friendly); the promise form resolves
|
|
333
|
+
/// `true` / rejects with an error, matching `fs/promises.access`.
|
|
334
|
+
pub fn access(args: &[Value]) -> Value {
|
|
335
|
+
exists(args)
|
|
336
|
+
}
|
|
337
|
+
pub fn access_promise(args: &[Value]) -> Value {
|
|
338
|
+
let path = path_arg(args, 0);
|
|
339
|
+
if std::path::Path::new(&path).exists() {
|
|
340
|
+
promise_resolve(&[Value::Null])
|
|
341
|
+
} else {
|
|
342
|
+
promise_reject(&[Value::String(format!("ENOENT: no such file '{}'", path).into())])
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/// `isDir(path)` — tish convenience kept for back-compat (≈ `statSync().isDirectory()`).
|
|
347
|
+
pub fn is_dir(args: &[Value]) -> Value {
|
|
348
|
+
Value::Bool(std::path::Path::new(&path_arg(args, 0)).is_dir())
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/// `fs.constants` — the access-mode flags.
|
|
352
|
+
pub fn constants() -> Value {
|
|
353
|
+
let mut m = ObjectMap::default();
|
|
354
|
+
m.insert("F_OK".into(), Value::Number(0.0));
|
|
355
|
+
m.insert("R_OK".into(), Value::Number(4.0));
|
|
356
|
+
m.insert("W_OK".into(), Value::Number(2.0));
|
|
357
|
+
m.insert("X_OK".into(), Value::Number(1.0));
|
|
358
|
+
Value::object(m)
|
|
359
|
+
}
|
|
@@ -1110,16 +1110,30 @@ where
|
|
|
1110
1110
|
drop(tx);
|
|
1111
1111
|
|
|
1112
1112
|
let mut count = 0usize;
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1113
|
+
// #384: `recv_timeout` (not a blocking `recv`) so a handler's scheduled timers still fire when the
|
|
1114
|
+
// server is idle. Handler calls all run on this single VM thread, so the timer registry is here.
|
|
1115
|
+
loop {
|
|
1116
|
+
match rx.recv_timeout(Duration::from_millis(250)) {
|
|
1117
|
+
Ok((req_prim, resp_tx)) => {
|
|
1118
|
+
let req_value = req_prim.into_value();
|
|
1119
|
+
let response_value = handler.call(&[req_value]);
|
|
1120
|
+
crate::timers::drain_timers();
|
|
1121
|
+
let resp_prim = ResponsePrimitive::from_value(&response_value);
|
|
1122
|
+
let _ = resp_tx.send(resp_prim);
|
|
1123
|
+
|
|
1124
|
+
count += 1;
|
|
1125
|
+
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
1126
|
+
stop.store(true, Ordering::Relaxed);
|
|
1127
|
+
break;
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
Err(mpsc::RecvTimeoutError::Timeout) => {
|
|
1131
|
+
crate::timers::drain_timers();
|
|
1132
|
+
if stop.load(Ordering::Relaxed) {
|
|
1133
|
+
break;
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
Err(mpsc::RecvTimeoutError::Disconnected) => break,
|
|
1123
1137
|
}
|
|
1124
1138
|
}
|
|
1125
1139
|
|
|
@@ -1181,6 +1195,10 @@ fn worker_loop_direct(
|
|
|
1181
1195
|
Ok(None) => {} // timeout; re-check stop flag
|
|
1182
1196
|
Err(_) => break,
|
|
1183
1197
|
}
|
|
1198
|
+
// #384: fire any timers a handler scheduled. The handler runs on this accept thread, so its
|
|
1199
|
+
// `setTimeout`/`setInterval` callbacks live in this thread's registry; the ≤250ms `recv_timeout`
|
|
1200
|
+
// above means they fire within that window even when no new requests arrive.
|
|
1201
|
+
crate::timers::drain_timers();
|
|
1184
1202
|
}
|
|
1185
1203
|
}
|
|
1186
1204
|
|