@tishlang/tish 1.7.0 → 1.8.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 (95) hide show
  1. package/Cargo.toml +1 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +15 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_resolve/Cargo.toml +13 -0
  64. package/crates/tish_resolve/src/lib.rs +3436 -0
  65. package/crates/tish_resolve/src/pos.rs +133 -0
  66. package/crates/tish_runtime/Cargo.toml +68 -3
  67. package/crates/tish_runtime/src/http.rs +1123 -141
  68. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  69. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  70. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  71. package/crates/tish_runtime/src/lib.rs +159 -29
  72. package/crates/tish_runtime/src/promise.rs +199 -36
  73. package/crates/tish_runtime/src/promise_io.rs +2 -1
  74. package/crates/tish_runtime/src/timers.rs +37 -1
  75. package/crates/tish_runtime/src/ws.rs +26 -28
  76. package/crates/tish_ui/src/jsx.rs +279 -8
  77. package/crates/tish_ui/src/lib.rs +5 -2
  78. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  79. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  80. package/crates/tish_vm/Cargo.toml +15 -5
  81. package/crates/tish_vm/src/vm.rs +506 -259
  82. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  83. package/crates/tish_wasm/src/lib.rs +17 -14
  84. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  85. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  86. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  87. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  88. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  89. package/justfile +8 -0
  90. package/package.json +1 -1
  91. package/platform/darwin-arm64/tish +0 -0
  92. package/platform/darwin-x64/tish +0 -0
  93. package/platform/linux-arm64/tish +0 -0
  94. package/platform/linux-x64/tish +0 -0
  95. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,178 @@
1
+ //! Shared-mutable reference used by the Tish runtime for `Value::Array`,
2
+ //! `Value::Object`, and `Value::RegExp` payloads.
3
+ //!
4
+ //! ## Why this exists
5
+ //!
6
+ //! Tish's `Value` uses interior mutability for arrays, objects, and regex
7
+ //! state. Historically that was `Rc<RefCell<T>>`, which is fast but
8
+ //! `!Send` — so `Value` couldn't move across threads, which in turn meant
9
+ //! `serve(port, handler)` had to serialise every request through one
10
+ //! VM dispatcher thread.
11
+ //!
12
+ //! `VmRef<T>` lets the build system pick the right trade-off **per
13
+ //! compile target**:
14
+ //!
15
+ //! | feature `send-values` | `VmRef<T>` | `NativeFn` | targets |
16
+ //! |---------------------------|-------------------------|----------------------------------|--------------------------------------------------|
17
+ //! | **off** *(default)* | `Rc<RefCell<T>>` | `Rc<dyn Fn + 'static>` | wasm32, wasi, interpreter, cranelift/llvm VMs |
18
+ //! | **on** | `Arc<Mutex<T>>` | `Arc<dyn Fn + Send + Sync>` | Rust native with `http` enabled (server workloads) |
19
+ //!
20
+ //! The *API* is identical in both configurations (`borrow` / `borrow_mut`
21
+ //! / `ptr_eq` / `Clone`), so every existing call site in the workspace
22
+ //! compiles unchanged. What flips is only the underlying primitive.
23
+ //!
24
+ //! ## Why this matters for performance
25
+ //!
26
+ //! * **wasm / wasi / cranelift / llvm / interpreter**: still pure
27
+ //! `Rc<RefCell<T>>`. Zero atomic ops, no mutex churn, behaviour
28
+ //! bit-identical to the pre-migration baseline.
29
+ //! * **Rust native, non-server**: same — `send-values` only activates
30
+ //! when something in the dependency graph (usually `http`) needs it.
31
+ //! * **Rust native with server**: `Arc<Mutex<T>>` pays ~3–5 ns per
32
+ //! `borrow` in the uncontended case (single atomic CAS). On Tish's
33
+ //! hot paths — roughly 6–12 borrows per request — that's ~30–60 ns of
34
+ //! overhead. In exchange we get `N×` handler scaling across cores,
35
+ //! which recovers orders of magnitude more throughput than it costs.
36
+ //!
37
+ //! ## API surface
38
+ //!
39
+ //! ```ignore
40
+ //! let cell = VmRef::new(42);
41
+ //! *cell.borrow() + 1; // read
42
+ //! *cell.borrow_mut() = 99; // write
43
+ //! VmRef::ptr_eq(&a, &b); // identity
44
+ //! let clone = cell.clone(); // shared ownership
45
+ //! ```
46
+ //!
47
+ //! Returned guard types (`VmReadGuard<'_, T>`, `VmWriteGuard<'_, T>`) are
48
+ //! type aliases that pick `Ref`/`RefMut` or `MutexGuard` depending on the
49
+ //! feature. They both `Deref` (and, for write guards, `DerefMut`) to `T`
50
+ //! just like the underlying types.
51
+
52
+ use std::fmt;
53
+
54
+ // --------------------------------------------------------------------------
55
+ // Single-threaded backing store (default): Rc<RefCell<T>>
56
+ // --------------------------------------------------------------------------
57
+ #[cfg(not(feature = "send-values"))]
58
+ mod imp {
59
+ use std::cell::RefCell;
60
+ use std::rc::Rc;
61
+
62
+ #[derive(Default)]
63
+ pub struct VmRef<T: ?Sized>(pub(super) Rc<RefCell<T>>);
64
+
65
+ /// Read guard alias. On the single-threaded path this is a true
66
+ /// `Ref<'_, T>`, so multiple readers can coexist.
67
+ pub type ReadGuard<'a, T> = std::cell::Ref<'a, T>;
68
+ /// Write guard alias. Exclusive, `DerefMut`.
69
+ pub type WriteGuard<'a, T> = std::cell::RefMut<'a, T>;
70
+
71
+ impl<T> VmRef<T> {
72
+ #[inline]
73
+ pub fn new(value: T) -> Self {
74
+ VmRef(Rc::new(RefCell::new(value)))
75
+ }
76
+ }
77
+
78
+ impl<T: ?Sized> VmRef<T> {
79
+ #[inline]
80
+ pub fn borrow(&self) -> ReadGuard<'_, T> {
81
+ self.0.borrow()
82
+ }
83
+
84
+ #[inline]
85
+ pub fn borrow_mut(&self) -> WriteGuard<'_, T> {
86
+ self.0.borrow_mut()
87
+ }
88
+
89
+ #[inline]
90
+ pub fn ptr_eq(a: &Self, b: &Self) -> bool {
91
+ Rc::ptr_eq(&a.0, &b.0)
92
+ }
93
+
94
+ #[inline]
95
+ pub fn strong_count(this: &Self) -> usize {
96
+ Rc::strong_count(&this.0)
97
+ }
98
+ }
99
+
100
+ impl<T: ?Sized> Clone for VmRef<T> {
101
+ #[inline]
102
+ fn clone(&self) -> Self {
103
+ VmRef(Rc::clone(&self.0))
104
+ }
105
+ }
106
+ }
107
+
108
+ // --------------------------------------------------------------------------
109
+ // Thread-safe backing store (opt-in): Arc<Mutex<T>>
110
+ // --------------------------------------------------------------------------
111
+ #[cfg(feature = "send-values")]
112
+ mod imp {
113
+ use std::sync::{Arc, Mutex};
114
+
115
+ #[derive(Default)]
116
+ pub struct VmRef<T: ?Sized>(pub(super) Arc<Mutex<T>>);
117
+
118
+ /// Read guard alias. On the multi-threaded path both readers and
119
+ /// writers share a single `MutexGuard` (exclusive access).
120
+ pub type ReadGuard<'a, T> = std::sync::MutexGuard<'a, T>;
121
+ /// Write guard alias.
122
+ pub type WriteGuard<'a, T> = std::sync::MutexGuard<'a, T>;
123
+
124
+ impl<T> VmRef<T> {
125
+ #[inline]
126
+ pub fn new(value: T) -> Self {
127
+ VmRef(Arc::new(Mutex::new(value)))
128
+ }
129
+ }
130
+
131
+ impl<T: ?Sized> VmRef<T> {
132
+ /// Acquire the inner mutex. Poisoning is swallowed — a Tish
133
+ /// handler panic already aborts the enclosing thread; there is
134
+ /// no invariant worth preserving past that point.
135
+ #[inline]
136
+ pub fn borrow(&self) -> ReadGuard<'_, T> {
137
+ self.0.lock().unwrap_or_else(|p| p.into_inner())
138
+ }
139
+
140
+ #[inline]
141
+ pub fn borrow_mut(&self) -> WriteGuard<'_, T> {
142
+ self.0.lock().unwrap_or_else(|p| p.into_inner())
143
+ }
144
+
145
+ #[inline]
146
+ pub fn ptr_eq(a: &Self, b: &Self) -> bool {
147
+ Arc::ptr_eq(&a.0, &b.0)
148
+ }
149
+
150
+ #[inline]
151
+ pub fn strong_count(this: &Self) -> usize {
152
+ Arc::strong_count(&this.0)
153
+ }
154
+ }
155
+
156
+ impl<T: ?Sized> Clone for VmRef<T> {
157
+ #[inline]
158
+ fn clone(&self) -> Self {
159
+ VmRef(Arc::clone(&self.0))
160
+ }
161
+ }
162
+ }
163
+
164
+ pub use imp::{ReadGuard as VmReadGuard, VmRef, WriteGuard as VmWriteGuard};
165
+
166
+ impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
167
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168
+ // Match `RefCell`'s debug format so snapshot-test output stays
169
+ // stable across the migration.
170
+ match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
171
+ let guard = self.borrow();
172
+ format!("{:?}", &*guard)
173
+ })) {
174
+ Ok(s) => write!(f, "RefCell {{ value: {} }}", s),
175
+ Err(_) => write!(f, "RefCell {{ value: <borrowed> }}"),
176
+ }
177
+ }
178
+ }
@@ -8,7 +8,22 @@ license-file = { workspace = true }
8
8
  repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
- http = ["tokio", "reqwest", "futures", "tiny_http", "tishlang_core/regex", "dep:tishlang_runtime", "tishlang_runtime/http"]
11
+ # setTimeout / setInterval / clear* (standalone or with `import { … } from "tish:timers"`).
12
+ timers = []
13
+ http = [
14
+ "timers",
15
+ "tokio",
16
+ "reqwest",
17
+ "futures",
18
+ "tiny_http",
19
+ "tishlang_core/regex",
20
+ "dep:tishlang_runtime",
21
+ "tishlang_runtime/http",
22
+ # Interpreter + http means the runtime's `NativeFn` is `Arc<... + Send>`,
23
+ # so the interpreter's `CoreFn` variant must use the same shape.
24
+ "tishlang_core/send-values",
25
+ "tishlang_builtins/send-values",
26
+ ]
12
27
  fs = []
13
28
  process = []
14
29
  regex = ["dep:fancy-regex", "tishlang_core/regex"]
@@ -17,7 +32,7 @@ ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
17
32
 
18
33
  [dependencies]
19
34
  ahash = "0.8.12"
20
- rand = "0.10.0"
35
+ rand = "0.10.1"
21
36
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
22
37
  tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
23
38
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
@@ -109,6 +109,7 @@ impl Evaluator {
109
109
  );
110
110
  s.set("decodeURI".into(), Value::Native(natives::decode_uri), true);
111
111
  s.set("encodeURI".into(), Value::Native(natives::encode_uri), true);
112
+ s.set("htmlEscape".into(), Value::Native(natives::html_escape), true);
112
113
  s.set(
113
114
  "Boolean".into(),
114
115
  Value::Native(natives::boolean_native),
@@ -221,7 +222,37 @@ impl Evaluator {
221
222
  );
222
223
  }
223
224
 
224
- // fs, http, process: use import { x } from 'tish:fs' etc. No globals.
225
+ // fs, process: prefer `import { x } from 'tish:fs'` etc.
226
+ #[cfg(feature = "timers")]
227
+ {
228
+ s.set(
229
+ "setTimeout".into(),
230
+ Value::TimerBuiltin(Arc::from("setTimeout")),
231
+ true,
232
+ );
233
+ s.set(
234
+ "setInterval".into(),
235
+ Value::TimerBuiltin(Arc::from("setInterval")),
236
+ true,
237
+ );
238
+ s.set(
239
+ "clearTimeout".into(),
240
+ Value::Native(Self::clear_timeout_native),
241
+ true,
242
+ );
243
+ s.set(
244
+ "clearInterval".into(),
245
+ Value::Native(Self::clear_interval_native),
246
+ true,
247
+ );
248
+ }
249
+ #[cfg(feature = "http")]
250
+ {
251
+ s.set("fetch".into(), Value::Native(Self::fetch_native), true);
252
+ s.set("fetchAll".into(), Value::Native(Self::fetch_all_native), true);
253
+ s.set("Promise".into(), Value::PromiseConstructor, true);
254
+ s.set("serve".into(), Value::Serve, true);
255
+ }
225
256
  }
226
257
  Self {
227
258
  scope,
@@ -556,21 +587,21 @@ impl Evaluator {
556
587
  let mut scope = self.scope.borrow_mut();
557
588
  for spec in specifiers {
558
589
  match spec {
559
- ImportSpecifier::Named { name, alias } => {
590
+ ImportSpecifier::Named { name, alias, .. } => {
560
591
  let v = exports.get(name.as_ref()).ok_or_else(|| {
561
592
  EvalError::Error(format!("Module does not export '{}'", name))
562
593
  })?;
563
594
  let bind = alias.as_deref().unwrap_or(name.as_ref());
564
595
  scope.set(Arc::from(bind), v.clone(), false);
565
596
  }
566
- ImportSpecifier::Namespace(ns) => {
567
- scope.set(Arc::clone(ns), exports_val.clone(), false);
597
+ ImportSpecifier::Namespace { name, .. } => {
598
+ scope.set(Arc::clone(name), exports_val.clone(), false);
568
599
  }
569
- ImportSpecifier::Default(bind) => {
600
+ ImportSpecifier::Default { name, .. } => {
570
601
  let v = exports.get("default").ok_or_else(|| {
571
602
  EvalError::Error("Module does not have default export".to_string())
572
603
  })?;
573
- scope.set(Arc::clone(bind), v.clone(), false);
604
+ scope.set(Arc::clone(name), v.clone(), false);
574
605
  }
575
606
  }
576
607
  }
@@ -588,6 +619,9 @@ impl Evaluator {
588
619
  }
589
620
  Ok(Value::Null)
590
621
  }
622
+ Statement::TypeAlias { .. } | Statement::DeclareVar { .. } | Statement::DeclareFun { .. } => {
623
+ Ok(Value::Null)
624
+ }
591
625
  }
592
626
  }
593
627
 
@@ -726,6 +760,19 @@ impl Evaluator {
726
760
  exports.insert("fetchAll".into(), Value::Native(Self::fetch_all_native));
727
761
  exports.insert("serve".into(), Value::Serve);
728
762
  exports.insert("Promise".into(), Value::PromiseConstructor);
763
+ return Ok(Value::Object(Rc::new(RefCell::new(exports))));
764
+ }
765
+ #[cfg(not(feature = "http"))]
766
+ {
767
+ return Err(EvalError::Error(
768
+ "tish:http requires the http feature. Rebuild with: cargo build -p tishlang --features http".into(),
769
+ ));
770
+ }
771
+ }
772
+ "tish:timers" => {
773
+ #[cfg(feature = "timers")]
774
+ {
775
+ let mut exports: PropMap = PropMap::default();
729
776
  exports.insert(
730
777
  "setTimeout".into(),
731
778
  Value::TimerBuiltin(Arc::from("setTimeout")),
@@ -744,10 +791,10 @@ impl Evaluator {
744
791
  );
745
792
  return Ok(Value::Object(Rc::new(RefCell::new(exports))));
746
793
  }
747
- #[cfg(not(feature = "http"))]
794
+ #[cfg(not(feature = "timers"))]
748
795
  {
749
796
  return Err(EvalError::Error(
750
- "tish:http requires the http feature. Rebuild with: cargo build -p tishlang --features http".into(),
797
+ "tish:timers requires the timers feature. Rebuild with: cargo build -p tishlang --features timers".into(),
751
798
  ));
752
799
  }
753
800
  }
@@ -815,7 +862,7 @@ impl Evaluator {
815
862
  }
816
863
  _ => {
817
864
  return Err(EvalError::Error(format!(
818
- "Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:process, tish:ws (plus any registered by native modules)",
865
+ "Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:timers, tish:process, tish:ws (plus any registered by native modules)",
819
866
  spec
820
867
  )));
821
868
  }
@@ -862,7 +909,12 @@ impl Evaluator {
862
909
  }
863
910
  Expr::Call { callee, args, .. } => {
864
911
  // Check for built-in method calls on arrays/strings
865
- if let Expr::Member { object, prop: MemberProp::Name(method_name), .. } = callee.as_ref() {
912
+ if let Expr::Member {
913
+ object,
914
+ prop: MemberProp::Name { name: method_name, .. },
915
+ ..
916
+ } = callee.as_ref()
917
+ {
866
918
  let obj = self.eval_expr(object)?;
867
919
  let arg_vals = self.eval_call_args(args)?;
868
920
 
@@ -1629,7 +1681,7 @@ impl Evaluator {
1629
1681
  return Ok(Value::Null);
1630
1682
  }
1631
1683
  let key = match prop {
1632
- MemberProp::Name(n) => Arc::clone(n),
1684
+ MemberProp::Name { name, .. } => Arc::clone(name),
1633
1685
  MemberProp::Expr(e) => {
1634
1686
  let v = self.eval_expr(e)?;
1635
1687
  match v {
@@ -1750,7 +1802,9 @@ impl Evaluator {
1750
1802
  Value::Serve
1751
1803
  | Value::PromiseResolver(_)
1752
1804
  | Value::PromiseConstructor
1753
- | Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => "function".into(),
1805
+ | Value::BoundPromiseMethod(_, _) => "function".into(),
1806
+ #[cfg(feature = "timers")]
1807
+ Value::TimerBuiltin(_) => "function".into(),
1754
1808
  #[cfg(feature = "http")]
1755
1809
  Value::Promise(_) => "object".into(),
1756
1810
  #[cfg(feature = "regex")]
@@ -2291,7 +2345,7 @@ impl Evaluator {
2291
2345
  _optional: bool,
2292
2346
  ) -> Option<Result<Value, EvalError>> {
2293
2347
  match property {
2294
- MemberProp::Name(name) => match obj {
2348
+ MemberProp::Name { name, .. } => match obj {
2295
2349
  Value::Object(o) => {
2296
2350
  let result = o
2297
2351
  .borrow()
@@ -2323,8 +2377,9 @@ impl Evaluator {
2323
2377
  #[cfg(feature = "http")]
2324
2378
  Value::PromiseConstructor
2325
2379
  | Value::Serve
2326
- | Value::BoundPromiseMethod(_, _)
2327
- | Value::TimerBuiltin(_) => self.call_func(callee, args),
2380
+ | Value::BoundPromiseMethod(_, _) => self.call_func(callee, args),
2381
+ #[cfg(feature = "timers")]
2382
+ Value::TimerBuiltin(_) => self.call_func(callee, args),
2328
2383
  Value::OpaqueMethod(_, _) => self.call_func(callee, args),
2329
2384
  _ => Ok(Value::Null),
2330
2385
  }
@@ -2411,7 +2466,7 @@ impl Evaluator {
2411
2466
  Value::BoundPromiseMethod(promise_ref, method) => {
2412
2467
  self.run_promise_method(promise_ref, method.as_ref(), args)
2413
2468
  }
2414
- #[cfg(feature = "http")]
2469
+ #[cfg(feature = "timers")]
2415
2470
  Value::TimerBuiltin(name) => self.run_timer_builtin(name.as_ref(), args),
2416
2471
  Value::OpaqueMethod(opaque, method_name) => {
2417
2472
  let method = opaque.get_method(method_name.as_ref()).ok_or_else(|| {
@@ -2629,7 +2684,7 @@ impl Evaluator {
2629
2684
  Ok(promise)
2630
2685
  }
2631
2686
 
2632
- #[cfg(feature = "http")]
2687
+ #[cfg(feature = "timers")]
2633
2688
  fn run_timer_builtin(&self, name: &str, args: &[Value]) -> Result<Value, EvalError> {
2634
2689
  let callback = args
2635
2690
  .first()
@@ -2650,7 +2705,7 @@ impl Evaluator {
2650
2705
  Ok(Value::Number(id as f64))
2651
2706
  }
2652
2707
 
2653
- #[cfg(feature = "http")]
2708
+ #[cfg(feature = "timers")]
2654
2709
  fn clear_timeout_native(args: &[Value]) -> Result<Value, String> {
2655
2710
  if let Some(Value::Number(n)) = args.first() {
2656
2711
  crate::timers::clearTimer(*n as u64);
@@ -2658,7 +2713,7 @@ impl Evaluator {
2658
2713
  Ok(Value::Null)
2659
2714
  }
2660
2715
 
2661
- #[cfg(feature = "http")]
2716
+ #[cfg(feature = "timers")]
2662
2717
  fn clear_interval_native(args: &[Value]) -> Result<Value, String> {
2663
2718
  if let Some(Value::Number(n)) = args.first() {
2664
2719
  crate::timers::clearTimer(*n as u64);
@@ -2668,7 +2723,7 @@ impl Evaluator {
2668
2723
 
2669
2724
  /// Run all due timer callbacks. Called after the script completes so setTimeout/setInterval
2670
2725
  /// callbacks run without blocking the main script. Loops until no timers are due.
2671
- #[cfg(feature = "http")]
2726
+ #[cfg(feature = "timers")]
2672
2727
  pub fn run_timer_phase(&mut self) -> Result<(), String> {
2673
2728
  const MAX_ITERATIONS: u32 = 1_000_000; // avoid infinite loop if setInterval never cleared
2674
2729
  let mut iterations = 0;
@@ -2760,8 +2815,14 @@ impl Evaluator {
2760
2815
  }
2761
2816
  };
2762
2817
 
2763
- let (status, headers, body) = crate::http::value_to_response(&response_value);
2764
- crate::http::send_response(request, status, headers, body);
2818
+ if let Some((status, headers, file_path)) =
2819
+ crate::http::extract_file_from_response(&response_value)
2820
+ {
2821
+ crate::http::send_file_response(request, status, headers, file_path);
2822
+ } else {
2823
+ let (status, headers, body) = crate::http::value_to_response(&response_value);
2824
+ crate::http::send_response(request, status, headers, body);
2825
+ }
2765
2826
  count += 1;
2766
2827
  if max_requests.map(|m| count >= m).unwrap_or(false) {
2767
2828
  break;
@@ -2809,7 +2870,7 @@ impl Evaluator {
2809
2870
  for (i, elem) in elements.iter().enumerate() {
2810
2871
  if let Some(el) = elem {
2811
2872
  match el {
2812
- tishlang_ast::DestructElement::Ident(name) => {
2873
+ tishlang_ast::DestructElement::Ident(name, _) => {
2813
2874
  let val = arr.get(i).cloned().unwrap_or(Value::Null);
2814
2875
  scope.borrow_mut().set(Arc::clone(name), val, mutable);
2815
2876
  }
@@ -2817,7 +2878,7 @@ impl Evaluator {
2817
2878
  let val = arr.get(i).cloned().unwrap_or(Value::Null);
2818
2879
  Self::bind_destruct_pattern_scoped(scope, nested, &val, mutable)?;
2819
2880
  }
2820
- tishlang_ast::DestructElement::Rest(name) => {
2881
+ tishlang_ast::DestructElement::Rest(name, _) => {
2821
2882
  let rest: Vec<Value> = arr.iter().skip(i).cloned().collect();
2822
2883
  scope.borrow_mut().set(
2823
2884
  Arc::clone(name),
@@ -2843,13 +2904,13 @@ impl Evaluator {
2843
2904
  for prop in props {
2844
2905
  let val = obj.get(&prop.key).cloned().unwrap_or(Value::Null);
2845
2906
  match &prop.value {
2846
- tishlang_ast::DestructElement::Ident(name) => {
2907
+ tishlang_ast::DestructElement::Ident(name, _) => {
2847
2908
  scope.borrow_mut().set(Arc::clone(name), val, mutable);
2848
2909
  }
2849
2910
  tishlang_ast::DestructElement::Pattern(nested) => {
2850
2911
  Self::bind_destruct_pattern_scoped(scope, nested, &val, mutable)?;
2851
2912
  }
2852
- tishlang_ast::DestructElement::Rest(_) => {
2913
+ tishlang_ast::DestructElement::Rest(_, _) => {
2853
2914
  return Err(EvalError::Error(
2854
2915
  "Rest not supported in object destructuring".to_string(),
2855
2916
  ));
@@ -3240,8 +3301,9 @@ impl Evaluator {
3240
3301
  | Value::Promise(_)
3241
3302
  | Value::PromiseResolver(_)
3242
3303
  | Value::PromiseConstructor
3243
- | Value::BoundPromiseMethod(_, _)
3244
- | Value::TimerBuiltin(_) => "null".to_string(),
3304
+ | Value::BoundPromiseMethod(_, _) => "null".to_string(),
3305
+ #[cfg(feature = "timers")]
3306
+ Value::TimerBuiltin(_) => "null".to_string(),
3245
3307
  #[cfg(feature = "regex")]
3246
3308
  Value::RegExp(_) => "null".to_string(),
3247
3309
  Value::Opaque(_) | Value::OpaqueMethod(_, _) => "null".to_string(),
@@ -1,6 +1,7 @@
1
1
  //! HTTP server for the Tish interpreter. Client `fetch` uses `tishlang_runtime` from eval.
2
2
 
3
3
  use crate::value::{PropMap, Value};
4
+ use std::fs::File;
4
5
  use std::sync::Arc;
5
6
 
6
7
  use tokio::runtime::Runtime;
@@ -101,6 +102,66 @@ pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String)
101
102
  (status, headers, body)
102
103
  }
103
104
 
105
+ /// If the response value has a `file` key, stream that path (binary-safe). Matches `tishlang_runtime` HTTP behavior.
106
+ pub(crate) fn extract_file_from_response(value: &Value) -> Option<(u16, Vec<(String, String)>, String)> {
107
+ let Value::Object(obj) = value else {
108
+ return None;
109
+ };
110
+ let obj_ref = obj.borrow();
111
+ let file_val = obj_ref.get(&Arc::from("file"))?;
112
+ let Value::String(file_path) = file_val else {
113
+ return None;
114
+ };
115
+ let file_path = file_path.to_string();
116
+ let status = obj_ref
117
+ .get(&Arc::from("status"))
118
+ .and_then(|v| match v {
119
+ Value::Number(n) => Some(*n as u16),
120
+ _ => None,
121
+ })
122
+ .unwrap_or(200);
123
+ let headers = obj_ref
124
+ .get(&Arc::from("headers"))
125
+ .and_then(|v| match v {
126
+ Value::Object(h) => Some(
127
+ h.borrow()
128
+ .iter()
129
+ .map(|(k, v)| (k.to_string(), v.to_string()))
130
+ .collect(),
131
+ ),
132
+ _ => None,
133
+ })
134
+ .unwrap_or_default();
135
+ Some((status, headers, file_path))
136
+ }
137
+
138
+ pub(crate) fn send_file_response(
139
+ request: tiny_http::Request,
140
+ status: u16,
141
+ headers: Vec<(String, String)>,
142
+ file_path: String,
143
+ ) {
144
+ let file = match File::open(&file_path) {
145
+ Ok(f) => f,
146
+ Err(e) => {
147
+ eprintln!("Failed to open file {}: {}", file_path, e);
148
+ let fallback =
149
+ tiny_http::Response::from_string(format!("File not found: {}", file_path))
150
+ .with_status_code(tiny_http::StatusCode(500));
151
+ let _ = request.respond(fallback);
152
+ return;
153
+ }
154
+ };
155
+ let status_code = tiny_http::StatusCode(status);
156
+ let mut response = tiny_http::Response::from_file(file).with_status_code(status_code);
157
+ for (key, value) in headers {
158
+ if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
159
+ response = response.with_header(header);
160
+ }
161
+ }
162
+ let _ = request.respond(response);
163
+ }
164
+
104
165
  /// Send a response using tiny_http.
105
166
  pub fn send_response(
106
167
  request: tiny_http::Request,
@@ -8,7 +8,7 @@ mod natives;
8
8
  mod promise;
9
9
  #[cfg(feature = "regex")]
10
10
  pub mod regex;
11
- #[cfg(feature = "http")]
11
+ #[cfg(feature = "timers")]
12
12
  mod timers;
13
13
  mod value;
14
14
  pub mod value_convert;
@@ -36,7 +36,7 @@ pub fn run(source: &str) -> Result<Value, String> {
36
36
  let program = tishlang_parser::parse(source)?;
37
37
  let mut eval = Evaluator::new();
38
38
  let result = eval.eval_program(&program)?;
39
- #[cfg(feature = "http")]
39
+ #[cfg(feature = "timers")]
40
40
  eval.run_timer_phase()?;
41
41
  Ok(result)
42
42
  }
@@ -64,7 +64,7 @@ pub fn run_file(
64
64
  let mut eval = Evaluator::new();
65
65
  eval.set_current_dir(project_root.or(path.parent()));
66
66
  let result = eval.eval_program(&program)?;
67
- #[cfg(feature = "http")]
67
+ #[cfg(feature = "timers")]
68
68
  eval.run_timer_phase()?;
69
69
  Ok(result)
70
70
  }
@@ -115,6 +115,47 @@ pub fn encode_uri(args: &[Value]) -> Result<Value, String> {
115
115
  Ok(Value::String(tishlang_core::percent_encode(&s).into()))
116
116
  }
117
117
 
118
+ pub fn html_escape(args: &[Value]) -> Result<Value, String> {
119
+ let input = match args.first() {
120
+ Some(Value::String(s)) => s.to_string(),
121
+ Some(v) => v.to_string(),
122
+ None => return Ok(Value::Null),
123
+ };
124
+ let bytes = input.as_bytes();
125
+ let mut extra = 0usize;
126
+ for b in bytes {
127
+ match b {
128
+ b'&' => extra += 4,
129
+ b'<' | b'>' => extra += 3,
130
+ b'"' => extra += 5,
131
+ b'\'' => extra += 4,
132
+ _ => {}
133
+ }
134
+ }
135
+ if extra == 0 {
136
+ return Ok(Value::String(input.into()));
137
+ }
138
+ let mut out = String::with_capacity(input.len() + extra);
139
+ let mut last = 0usize;
140
+ for (i, b) in bytes.iter().enumerate() {
141
+ let repl: Option<&'static str> = match b {
142
+ b'&' => Some("&amp;"),
143
+ b'<' => Some("&lt;"),
144
+ b'>' => Some("&gt;"),
145
+ b'"' => Some("&quot;"),
146
+ b'\'' => Some("&#39;"),
147
+ _ => None,
148
+ };
149
+ if let Some(r) = repl {
150
+ out.push_str(&input[last..i]);
151
+ out.push_str(r);
152
+ last = i + 1;
153
+ }
154
+ }
155
+ out.push_str(&input[last..]);
156
+ Ok(Value::String(out.into()))
157
+ }
158
+
118
159
  pub fn math_abs(args: &[Value]) -> Result<Value, String> {
119
160
  Ok(Value::Number(
120
161
  get_num(args.first().unwrap_or(&Value::Null)).abs(),
@@ -61,7 +61,7 @@ pub enum Value {
61
61
  #[cfg(feature = "http")]
62
62
  BoundPromiseMethod(crate::promise::PromiseRef, std::sync::Arc<str>),
63
63
  /// Timer builtins: setTimeout, setInterval. Need evaluator for callback.
64
- #[cfg(feature = "http")]
64
+ #[cfg(feature = "timers")]
65
65
  TimerBuiltin(std::sync::Arc<str>),
66
66
  /// Native `tishlang_core` Promise (fetch / reader.read / response.text).
67
67
  #[cfg(feature = "http")]
@@ -101,7 +101,9 @@ impl std::fmt::Debug for Value {
101
101
  #[cfg(feature = "http")]
102
102
  Value::PromiseConstructor => write!(f, "[Function: Promise]"),
103
103
  #[cfg(feature = "http")]
104
- Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
104
+ Value::BoundPromiseMethod(_, _) => write!(f, "[Function]"),
105
+ #[cfg(feature = "timers")]
106
+ Value::TimerBuiltin(_) => write!(f, "[Function]"),
105
107
  #[cfg(feature = "http")]
106
108
  Value::CorePromise(_) => write!(f, "Promise"),
107
109
  Value::CoreFn(_) => write!(f, "CoreFn"),
@@ -156,7 +158,9 @@ impl std::fmt::Display for Value {
156
158
  #[cfg(feature = "http")]
157
159
  Value::PromiseConstructor => write!(f, "function Promise() {{ [native code] }}"),
158
160
  #[cfg(feature = "http")]
159
- Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
161
+ Value::BoundPromiseMethod(_, _) => write!(f, "[Function]"),
162
+ #[cfg(feature = "timers")]
163
+ Value::TimerBuiltin(_) => write!(f, "[Function]"),
160
164
  #[cfg(feature = "http")]
161
165
  Value::CorePromise(_) => write!(f, "[Promise]"),
162
166
  Value::CoreFn(_) => write!(f, "[Function]"),