@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.
Files changed (88) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish-format +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +12 -2
  4. package/crates/tish/tests/fixtures/fs_parity_callbacks.tish +22 -0
  5. package/crates/tish/tests/fixtures/fs_parity_promises.tish +21 -0
  6. package/crates/tish/tests/fixtures/fs_parity_sync.tish +20 -0
  7. package/crates/tish/tests/fs_parity.rs +103 -0
  8. package/crates/tish/tests/integration_test.rs +80 -0
  9. package/crates/tish/tests/shortcircuit.rs +3 -3
  10. package/crates/tish_ast/src/ast.rs +84 -0
  11. package/crates/tish_builtins/Cargo.toml +1 -0
  12. package/crates/tish_builtins/src/array.rs +724 -72
  13. package/crates/tish_builtins/src/collections.rs +149 -40
  14. package/crates/tish_builtins/src/globals.rs +215 -25
  15. package/crates/tish_builtins/src/math.rs +65 -7
  16. package/crates/tish_builtins/src/number.rs +173 -0
  17. package/crates/tish_builtins/src/string.rs +263 -23
  18. package/crates/tish_bytecode/src/chunk.rs +7 -0
  19. package/crates/tish_bytecode/src/compiler.rs +611 -63
  20. package/crates/tish_bytecode/src/lib.rs +1 -1
  21. package/crates/tish_bytecode/src/opcode.rs +156 -2
  22. package/crates/tish_bytecode/src/serialize.rs +2 -0
  23. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  24. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  25. package/crates/tish_compile/src/codegen.rs +18219 -5283
  26. package/crates/tish_compile/src/infer.rs +1562 -30
  27. package/crates/tish_compile/src/lib.rs +29 -4
  28. package/crates/tish_compile/src/resolve.rs +151 -13
  29. package/crates/tish_compile/src/types.rs +224 -28
  30. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  31. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  32. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  33. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  34. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  35. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  36. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  37. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  38. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  39. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  40. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  41. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  42. package/crates/tish_compile_js/src/codegen.rs +94 -3
  43. package/crates/tish_compile_js/src/tests_jsx.rs +27 -0
  44. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +115 -2
  45. package/crates/tish_core/Cargo.toml +4 -0
  46. package/crates/tish_core/src/json.rs +109 -6
  47. package/crates/tish_core/src/lib.rs +222 -2
  48. package/crates/tish_core/src/shape.rs +4 -2
  49. package/crates/tish_core/src/uri.rs +45 -0
  50. package/crates/tish_core/src/value.rs +571 -35
  51. package/crates/tish_core/src/vmref.rs +14 -0
  52. package/crates/tish_eval/Cargo.toml +2 -1
  53. package/crates/tish_eval/src/eval.rs +1328 -101
  54. package/crates/tish_eval/src/natives.rs +283 -18
  55. package/crates/tish_eval/src/regex.rs +47 -1
  56. package/crates/tish_eval/src/value.rs +76 -21
  57. package/crates/tish_ffi/src/lib.rs +11 -1
  58. package/crates/tish_ffi/tests/double_free.rs +35 -0
  59. package/crates/tish_fmt/Cargo.toml +1 -1
  60. package/crates/tish_fmt/src/lib.rs +94 -1
  61. package/crates/tish_lexer/src/lib.rs +76 -0
  62. package/crates/tish_lexer/src/token.rs +4 -0
  63. package/crates/tish_lint/src/lib.rs +126 -0
  64. package/crates/tish_lsp/README.md +2 -1
  65. package/crates/tish_lsp/src/main.rs +378 -28
  66. package/crates/tish_native/src/build.rs +41 -0
  67. package/crates/tish_opt/src/lib.rs +20 -1
  68. package/crates/tish_parser/Cargo.toml +4 -0
  69. package/crates/tish_parser/src/lib.rs +68 -0
  70. package/crates/tish_parser/src/parser.rs +479 -28
  71. package/crates/tish_resolve/src/lib.rs +75 -5
  72. package/crates/tish_runtime/Cargo.toml +10 -1
  73. package/crates/tish_runtime/src/fs_ext.rs +359 -0
  74. package/crates/tish_runtime/src/http.rs +28 -10
  75. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  76. package/crates/tish_runtime/src/http_hyper.rs +41 -18
  77. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  78. package/crates/tish_runtime/src/lib.rs +651 -51
  79. package/crates/tish_runtime/src/timers.rs +49 -2
  80. package/crates/tish_ui/src/jsx.rs +21 -3
  81. package/crates/tish_vm/src/jit.rs +2514 -117
  82. package/crates/tish_vm/src/vm.rs +1479 -224
  83. package/package.json +1 -1
  84. package/platform/darwin-arm64/tish-fmt +0 -0
  85. package/platform/darwin-x64/tish-fmt +0 -0
  86. package/platform/linux-arm64/tish-fmt +0 -0
  87. package/platform/linux-x64/tish-fmt +0 -0
  88. 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 == 0) { return true } return odd(n - 1) }\nfn odd(n) { if (n == 0) { return false } return even(n - 1) }\neven(10)\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
- while let Ok((req_prim, resp_tx)) = rx.recv() {
1114
- let req_value = req_prim.into_value();
1115
- let response_value = handler.call(&[req_value]);
1116
- let resp_prim = ResponsePrimitive::from_value(&response_value);
1117
- let _ = resp_tx.send(resp_prim);
1118
-
1119
- count += 1;
1120
- if max_requests.map(|m| count >= m).unwrap_or(false) {
1121
- stop.store(true, Ordering::Relaxed);
1122
- break;
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