@tishlang/tish 1.7.0 → 1.9.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 (99) hide show
  1. package/Cargo.toml +2 -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 +14 -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_pg/Cargo.toml +34 -0
  64. package/crates/tish_pg/README.md +38 -0
  65. package/crates/tish_pg/src/error.rs +52 -0
  66. package/crates/tish_pg/src/lib.rs +967 -0
  67. package/crates/tish_resolve/Cargo.toml +13 -0
  68. package/crates/tish_resolve/src/lib.rs +3436 -0
  69. package/crates/tish_resolve/src/pos.rs +133 -0
  70. package/crates/tish_runtime/Cargo.toml +68 -3
  71. package/crates/tish_runtime/src/http.rs +1123 -141
  72. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  73. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  74. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  75. package/crates/tish_runtime/src/lib.rs +159 -29
  76. package/crates/tish_runtime/src/promise.rs +199 -36
  77. package/crates/tish_runtime/src/promise_io.rs +2 -1
  78. package/crates/tish_runtime/src/timers.rs +37 -1
  79. package/crates/tish_runtime/src/ws.rs +26 -28
  80. package/crates/tish_ui/src/jsx.rs +279 -8
  81. package/crates/tish_ui/src/lib.rs +5 -2
  82. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  83. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  84. package/crates/tish_vm/Cargo.toml +15 -5
  85. package/crates/tish_vm/src/vm.rs +506 -259
  86. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  87. package/crates/tish_wasm/src/lib.rs +17 -14
  88. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  89. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  90. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  91. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  92. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  93. package/justfile +8 -0
  94. package/package.json +1 -1
  95. package/platform/darwin-arm64/tish +0 -0
  96. package/platform/darwin-x64/tish +0 -0
  97. package/platform/linux-arm64/tish +0 -0
  98. package/platform/linux-x64/tish +0 -0
  99. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,189 @@
1
+ //! Process-level prefork for the `tish:http` server.
2
+ //!
3
+ //! ## Why
4
+ //!
5
+ //! Tish's `Value` type is reference-counted with `Rc`/`RefCell` and therefore
6
+ //! `!Send`. Serving HTTP in parallel across CPU cores with the existing VM
7
+ //! would require either
8
+ //!
9
+ //! 1. a wholesale `Rc → Arc` conversion across every Tish crate, or
10
+ //! 2. spinning up independent VM instances that never share a `Value`.
11
+ //!
12
+ //! Option 1 taxes every single-threaded Tish program with atomic ref-count
13
+ //! overhead forever. Option 2 is what this file implements, via the classic
14
+ //! UNIX **prefork** pattern:
15
+ //!
16
+ //! * The parent process (worker 0) `fork`s — actually `spawn`s a new
17
+ //! `std::process::Command` pointing at the current executable — once per
18
+ //! extra core. Each child re-executes the entire Tish program in its own
19
+ //! address space.
20
+ //! * All processes (parent + children) bind the same `port` with
21
+ //! `SO_REUSEPORT`; the kernel hashes incoming connections across them.
22
+ //! * Each process runs a *single-threaded* accept + dispatch loop, so the
23
+ //! Tish VM stays single-threaded and `Value` stays `Rc`-backed.
24
+ //!
25
+ //! ## Why this is the right default
26
+ //!
27
+ //! * **nginx, gunicorn, unicorn, puma (cluster), and phpfpm all ship this
28
+ //! model.** It's the battle-tested way to extract N-core throughput from a
29
+ //! single-threaded scripting runtime.
30
+ //! * Zero Tish-language semantic changes: users write `serve(port, handler)`
31
+ //! exactly as before and get N-core scaling for free.
32
+ //! * Every process has a fresh DB connection pool, a fresh cache, a fresh
33
+ //! whatever. No cache invalidation, no shared mutable state, no data races.
34
+ //! * Crash isolation: if one worker panics the others keep serving.
35
+ //!
36
+ //! ## Cost
37
+ //!
38
+ //! * Each worker re-runs top-level initialization (module imports, constant
39
+ //! folding, static route registration, cache warmup, ...). For typical
40
+ //! apps this is milliseconds and happens once at startup, in parallel. For
41
+ //! apps that preload hundreds of MB of in-process state (e.g. the TFB
42
+ //! `warmupWorldCache` that keeps 10 000 rows in RAM), the memory multiplier
43
+ //! is N×. Users who can't afford the memory set `TISH_HTTP_WORKERS=1`.
44
+ //!
45
+ //! ## Control surface
46
+ //!
47
+ //! | env var | default | effect |
48
+ //! |---------------------|------------------------|--------------------------------------|
49
+ //! | `TISH_HTTP_WORKERS` | `available_parallelism`| number of worker processes |
50
+ //! | `TISH_HTTP_PREFORK` | `1` (on) | set to `0` to force single-process |
51
+ //! | `TISH_WORKER_ID` | unset on parent | set on children by the parent |
52
+ //!
53
+ //! Children are launched with `TISH_WORKER_ID={1..N-1}` and
54
+ //! `TISH_HTTP_PREFORK=child`. The serve() runtime checks these before
55
+ //! deciding whether to fork again, preventing runaway forking.
56
+
57
+ use std::io;
58
+ use std::process::{Child, Command};
59
+ use std::sync::atomic::{AtomicBool, Ordering};
60
+ use std::sync::Arc;
61
+
62
+ /// Role of the current process in a prefork group.
63
+ #[derive(Debug, Clone, Copy, PartialEq, Eq)]
64
+ pub enum PreforkRole {
65
+ /// The parent — owns the child PIDs, handles signals, re-execs nothing.
66
+ Parent,
67
+ /// A child spawned by the parent. Never forks again.
68
+ Child(usize),
69
+ /// Prefork disabled (single-process mode).
70
+ Single,
71
+ }
72
+
73
+ /// Inspect the environment to decide which role this process plays.
74
+ pub fn role_from_env() -> PreforkRole {
75
+ let is_child = std::env::var("TISH_HTTP_PREFORK")
76
+ .map(|v| v.eq_ignore_ascii_case("child"))
77
+ .unwrap_or(false);
78
+ if is_child {
79
+ let id = std::env::var("TISH_WORKER_ID")
80
+ .ok()
81
+ .and_then(|s| s.parse::<usize>().ok())
82
+ .unwrap_or(1);
83
+ return PreforkRole::Child(id);
84
+ }
85
+ let disabled = std::env::var("TISH_HTTP_PREFORK")
86
+ .map(|v| v == "0" || v.eq_ignore_ascii_case("false") || v.eq_ignore_ascii_case("off"))
87
+ .unwrap_or(false);
88
+ if disabled {
89
+ PreforkRole::Single
90
+ } else {
91
+ PreforkRole::Parent
92
+ }
93
+ }
94
+
95
+ /// Spawn `n - 1` child processes (current worker is worker 0). Each child
96
+ /// inherits stdio and gets `TISH_WORKER_ID={1..n-1}` +
97
+ /// `TISH_HTTP_PREFORK=child` so it doesn't recurse.
98
+ ///
99
+ /// Returns the child handles so the parent can reap / signal them.
100
+ pub fn spawn_children(n: usize) -> io::Result<Vec<Child>> {
101
+ if n <= 1 {
102
+ return Ok(Vec::new());
103
+ }
104
+ let exe = std::env::current_exe()?; // codacy-disable-line
105
+ let args: Vec<std::ffi::OsString> = std::env::args_os().skip(1).collect(); // codacy-disable-line
106
+ let mut out = Vec::with_capacity(n - 1);
107
+ for i in 1..n {
108
+ let mut cmd = Command::new(&exe);
109
+ cmd.args(&args);
110
+ cmd.env("TISH_WORKER_ID", i.to_string());
111
+ cmd.env("TISH_HTTP_PREFORK", "child");
112
+ // Children inherit the shared cache of 1 thread so they don't recurse
113
+ // into SO_REUSEPORT multi-listener logic. The parent keeps the same.
114
+ cmd.env("TISH_HTTP_WORKERS", "1");
115
+ // Inherit stdout/stderr: child logs stream into the same terminal.
116
+ let child = cmd.spawn()?;
117
+ out.push(child);
118
+ }
119
+ Ok(out)
120
+ }
121
+
122
+ /// Install a Ctrl-C / SIGTERM handler on the parent that propagates to all
123
+ /// children. Safe to call multiple times; the handler is stored in a
124
+ /// process-wide slot.
125
+ ///
126
+ /// Returns a shared stop flag that callers can poll from their accept loop.
127
+ pub fn install_parent_signal_handler(children: Vec<Child>) -> Arc<AtomicBool> {
128
+ let stop = Arc::new(AtomicBool::new(false));
129
+ let pids: Vec<u32> = children.iter().map(|c| c.id()).collect();
130
+ install_shutdown_handler(Arc::clone(&stop), pids);
131
+
132
+ // Reap children in the background so they don't zombify when the user
133
+ // ^C's or when a child dies on its own.
134
+ std::thread::Builder::new()
135
+ .name("tish-prefork-reaper".into())
136
+ .spawn(move || {
137
+ for mut child in children {
138
+ let _ = child.wait();
139
+ }
140
+ })
141
+ .ok();
142
+ stop
143
+ }
144
+
145
+ #[cfg(unix)]
146
+ fn install_shutdown_handler(stop: Arc<AtomicBool>, pids: Vec<u32>) {
147
+ // Store state in process-global statics so an `extern "C"` fn can reach
148
+ // them from inside a signal handler. This is the usual pattern for
149
+ // libc::signal callbacks — setting a flag + waking up listeners is the
150
+ // only async-signal-safe work we do here.
151
+ use std::sync::OnceLock;
152
+ static STOP_FLAG: OnceLock<Arc<AtomicBool>> = OnceLock::new();
153
+ static CHILD_PIDS: OnceLock<Vec<u32>> = OnceLock::new();
154
+
155
+ let _ = STOP_FLAG.set(stop);
156
+ let _ = CHILD_PIDS.set(pids);
157
+
158
+ extern "C" fn on_signal(sig: libc::c_int) {
159
+ if let Some(flag) = STOP_FLAG.get() {
160
+ flag.store(true, Ordering::Relaxed);
161
+ }
162
+ if let Some(pids) = CHILD_PIDS.get() {
163
+ for pid in pids {
164
+ // codacy-disable-next-line
165
+ unsafe {
166
+ libc::kill(*pid as libc::pid_t, libc::SIGTERM);
167
+ }
168
+ }
169
+ }
170
+ // Re-raise with default disposition so the parent actually exits.
171
+ // codacy-disable-next-line
172
+ unsafe {
173
+ libc::signal(sig, libc::SIG_DFL);
174
+ libc::raise(sig);
175
+ }
176
+ }
177
+
178
+ let h = on_signal as *const () as libc::sighandler_t;
179
+ // codacy-disable-next-line
180
+ unsafe {
181
+ libc::signal(libc::SIGINT, h);
182
+ libc::signal(libc::SIGTERM, h);
183
+ }
184
+ }
185
+
186
+ #[cfg(not(unix))]
187
+ fn install_shutdown_handler(_stop: Arc<AtomicBool>, _pids: Vec<u32>) {
188
+ // TODO: SetConsoleCtrlHandler on Windows.
189
+ }
@@ -15,6 +15,10 @@ use tishlang_builtins::helpers::make_error_value;
15
15
 
16
16
  pub use tishlang_core::ObjectMap;
17
17
  pub use tishlang_core::Value;
18
+ // Re-export the shared-mutable wrapper so the Rust code emitted by
19
+ // `tishlang_compile::codegen` can write `VmRef::new(...)` without needing
20
+ // a direct dependency on `tishlang_core` from the generated crate.
21
+ pub use tishlang_core::{VmReadGuard, VmRef, VmWriteGuard};
18
22
 
19
23
  pub use tishlang_builtins::construct::{
20
24
  audio_context_constructor_value as tish_audio_context_constructor, construct as tish_construct,
@@ -38,11 +42,12 @@ pub use tishlang_builtins::array::{
38
42
  // Re-export string methods from tishlang_builtins
39
43
  pub use tishlang_builtins::string::{
40
44
  char_at as string_char_at_impl, char_code_at as string_char_code_at_impl,
41
- ends_with as string_ends_with_impl, includes as string_includes_impl,
42
- index_of as string_index_of_impl, last_index_of as string_last_index_of_impl,
43
- pad_end as string_pad_end_impl, pad_start as string_pad_start_impl,
44
- repeat as string_repeat_impl, replace as string_replace_impl,
45
- replace_all as string_replace_all_impl, slice as string_slice_impl, split as string_split_impl,
45
+ ends_with as string_ends_with_impl, escape_html as string_escape_html_impl,
46
+ includes as string_includes_impl, index_of as string_index_of_impl,
47
+ last_index_of as string_last_index_of_impl, pad_end as string_pad_end_impl,
48
+ pad_start as string_pad_start_impl, repeat as string_repeat_impl,
49
+ replace as string_replace_impl, replace_all as string_replace_all_impl,
50
+ slice as string_slice_impl, split as string_split_impl,
46
51
  starts_with as string_starts_with_impl, substring as string_substring_impl,
47
52
  to_lower_case as string_to_lower_case, to_upper_case as string_to_upper_case,
48
53
  trim as string_trim,
@@ -269,6 +274,50 @@ use tishlang_builtins::globals::{
269
274
  };
270
275
  use tishlang_core::{json_parse as core_json_parse, json_stringify as core_json_stringify};
271
276
 
277
+ /// Public JSON helpers used by codegen-emitted code (specifically the
278
+ /// `_tish_write_json` impls on user-declared `type` aliases). Re-exporting
279
+ /// from the runtime keeps the generated source decoupled from
280
+ /// `tishlang_core` — generated code only ever names `tishlang_runtime`.
281
+ pub mod json {
282
+ pub use tishlang_core::json_stringify_into as stringify_into;
283
+ /// Append the JSON-escaped contents of `s` (without surrounding
284
+ /// quotes) to `buf`. Used by typed-struct serialisers for `String`
285
+ /// fields. Falls through to `tishlang_core::json_stringify_into`'s
286
+ /// internal helper via a `Value::String` round-trip when the inner
287
+ /// helper isn't directly exposed.
288
+ pub fn escape_into(buf: &mut String, s: &str) {
289
+ // Inline the same escape rules as tishlang_core::json::
290
+ // `escape_json_string_into`. Kept locally so we don't widen
291
+ // tishlang_core's public surface unnecessarily.
292
+ let bytes = s.as_bytes();
293
+ let mut start = 0usize;
294
+ for (i, &b) in bytes.iter().enumerate() {
295
+ if b < 0x20 || b == b'"' || b == b'\\' {
296
+ if start < i {
297
+ buf.push_str(&s[start..i]);
298
+ }
299
+ match b {
300
+ b'"' => buf.push_str("\\\""),
301
+ b'\\' => buf.push_str("\\\\"),
302
+ b'\n' => buf.push_str("\\n"),
303
+ b'\r' => buf.push_str("\\r"),
304
+ b'\t' => buf.push_str("\\t"),
305
+ b'\x08' => buf.push_str("\\b"),
306
+ b'\x0c' => buf.push_str("\\f"),
307
+ _ => {
308
+ use std::fmt::Write;
309
+ let _ = write!(buf, "\\u{:04x}", b as u32);
310
+ }
311
+ }
312
+ start = i + 1;
313
+ }
314
+ }
315
+ if start < bytes.len() {
316
+ buf.push_str(&s[start..]);
317
+ }
318
+ }
319
+ }
320
+
272
321
  /// Error type for Tish throw/catch.
273
322
  #[derive(Debug, Clone)]
274
323
  pub enum TishError {
@@ -553,7 +602,7 @@ pub fn read_dir(args: &[Value]) -> Value {
553
602
  .filter_map(|e| e.ok())
554
603
  .map(|e| Value::String(e.file_name().to_string_lossy().into()))
555
604
  .collect();
556
- Value::Array(Rc::new(RefCell::new(files)))
605
+ Value::Array(VmRef::new(files))
557
606
  }
558
607
  Err(e) => make_error_value(e),
559
608
  }
@@ -578,8 +627,12 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
578
627
  let key = key.as_ref();
579
628
  match obj {
580
629
  Value::Object(map) => {
581
- let k: Arc<str> = key.into();
582
- map.borrow().get(&k).cloned().unwrap_or(Value::Null)
630
+ // The map's key type is `Arc<str>`, which implements
631
+ // `Borrow<str>` — so we can look up with a borrowed `&str`
632
+ // directly. Previously we allocated a fresh `Arc<str>` on
633
+ // every call (one heap alloc per `r.field` read in tight
634
+ // handler loops); this version is alloc-free on the hit path.
635
+ map.borrow().get(key).cloned().unwrap_or(Value::Null)
583
636
  }
584
637
  Value::Array(arr) => {
585
638
  if key == "length" {
@@ -599,17 +652,17 @@ pub fn get_prop(obj: &Value, key: impl AsRef<str>) -> Value {
599
652
  }
600
653
  #[cfg(feature = "regex")]
601
654
  Value::RegExp(re) => {
602
- let re = Rc::clone(re);
655
+ let re = re.clone();
603
656
  if key == "exec" {
604
- Value::Function(Rc::new(move |args: &[Value]| {
657
+ Value::native(move |args: &[Value]| {
605
658
  let input = args.first().unwrap_or(&Value::Null);
606
- regexp_exec(&Value::RegExp(Rc::clone(&re)), input)
607
- }))
659
+ regexp_exec(&Value::RegExp(re.clone()), input)
660
+ })
608
661
  } else if key == "test" {
609
- Value::Function(Rc::new(move |args: &[Value]| {
662
+ Value::native(move |args: &[Value]| {
610
663
  let input = args.first().unwrap_or(&Value::Null);
611
- regexp_test(&Value::RegExp(Rc::clone(&re)), input)
612
- }))
664
+ regexp_test(&Value::RegExp(re.clone()), input)
665
+ })
613
666
  } else {
614
667
  Value::Null
615
668
  }
@@ -648,7 +701,15 @@ pub fn get_index(obj: &Value, index: &Value) -> Value {
648
701
  pub fn set_prop(obj: &Value, key: &str, val: Value) -> Value {
649
702
  match obj {
650
703
  Value::Object(map) => {
651
- map.borrow_mut().insert(Arc::from(key), val.clone());
704
+ // Try the in-place update path first: if the key already
705
+ // exists we re-use the existing `Arc<str>` and skip the
706
+ // alloc. Only newly-inserted keys pay for `Arc::from(key)`.
707
+ let mut m = map.borrow_mut();
708
+ if let Some(slot) = m.get_mut(key) {
709
+ *slot = val.clone();
710
+ } else {
711
+ m.insert(Arc::from(key), val.clone());
712
+ }
652
713
  val
653
714
  }
654
715
  _ => panic!("Cannot assign property on non-object"),
@@ -734,10 +795,18 @@ mod promise_io;
734
795
  #[cfg(feature = "http")]
735
796
  mod http;
736
797
 
798
+ #[cfg(feature = "http")]
799
+ mod http_prefork;
800
+
801
+ #[cfg(feature = "http-io-uring")]
802
+ mod http_io_uring;
803
+
804
+ #[cfg(feature = "http-hyper")]
805
+ mod http_hyper;
806
+
737
807
  #[cfg(feature = "http")]
738
808
  mod http_fetch;
739
809
 
740
- #[cfg(any(feature = "http", feature = "ws"))]
741
810
  mod timers;
742
811
 
743
812
  #[cfg(feature = "http")]
@@ -757,14 +826,75 @@ pub use ws::{
757
826
 
758
827
  #[cfg(feature = "http")]
759
828
  pub use http::{
760
- await_fetch as http_await_fetch, await_fetch_all as http_await_fetch_all, serve as http_serve,
829
+ await_fetch as http_await_fetch, await_fetch_all as http_await_fetch_all,
830
+ register_static_route,
761
831
  };
762
832
 
763
- #[cfg(any(feature = "http", feature = "ws"))]
764
- pub use timers::{clear_timeout as timer_clear_timeout, set_timeout as timer_set_timeout};
833
+ // `serve` is the user-facing entry point for Tish's HTTP server. By default
834
+ // it uses the tiny_http + SO_REUSEPORT path in `http.rs`. When compiled with
835
+ // `--features http-hyper` and the `TISH_HTTP_BACKEND=hyper` env var is set
836
+ // at runtime, it dispatches to the hyper backend in `http_hyper.rs`.
837
+ //
838
+ // The env-var switch (rather than a cargo feature switch) means one built
839
+ // binary can toggle backends for A/B benchmarking and production rollout
840
+ // without rebuilding. When `http-hyper` is not compiled in, the switch is a
841
+ // no-op and the tiny_http path is used unconditionally.
842
+ #[cfg(feature = "http")]
843
+ pub fn http_serve<F>(args: &[tishlang_core::Value], handler: F) -> tishlang_core::Value
844
+ where
845
+ F: Fn(&[tishlang_core::Value]) -> tishlang_core::Value + Send + Sync + 'static,
846
+ {
847
+ #[cfg(feature = "http-hyper")]
848
+ {
849
+ if http_hyper::is_enabled_via_env() {
850
+ return http_hyper::serve(args, handler);
851
+ }
852
+ }
853
+ http::serve(args, handler)
854
+ }
855
+
856
+ /// `serve(port, { onWorker: (workerId) => handler })` — the object form of
857
+ /// `serve`. Picks up `onWorker`, invokes it once per HTTP accept thread to
858
+ /// build that thread's handler, then enters the normal parallel accept
859
+ /// loop. See [`http::serve_per_worker`] for the full doc.
860
+ ///
861
+ /// This is broadly useful for any Tish app that wants per-worker state —
862
+ /// DB connection pools, in-process caches, counters, etc. — without a
863
+ /// global `RwLock` or forcing everything through the single-thread
864
+ /// dispatcher. It also plays nicely with prefork: `onWorker` sees global
865
+ /// worker ids across processes so logs and sharded state are easy to key.
866
+ #[cfg(feature = "http")]
867
+ pub fn http_serve_per_worker(
868
+ args: &[tishlang_core::Value],
869
+ factory_value: tishlang_core::Value,
870
+ ) -> tishlang_core::Value {
871
+ use tishlang_core::Value;
872
+ // factory_value should be Value::Function (passed down by codegen after
873
+ // extracting `onWorker` from the options object).
874
+ let Value::Function(factory) = factory_value else {
875
+ eprintln!("[tish http] serve: onWorker must be a function (id) => handler");
876
+ return Value::Null;
877
+ };
878
+ let factory: tishlang_core::NativeFn = factory;
879
+ http::serve_per_worker(args, move |worker_id| {
880
+ let handler_val = factory(&[Value::Number(worker_id as f64)]);
881
+ match handler_val {
882
+ Value::Function(f) => f,
883
+ _ => panic!(
884
+ "onWorker returned {:?} for worker {}; must return a function",
885
+ handler_val, worker_id
886
+ ),
887
+ }
888
+ })
889
+ }
890
+
891
+ pub use timers::{
892
+ clear_interval as timer_clear_interval, clear_timeout as timer_clear_timeout, drain_timers,
893
+ set_interval as timer_set_interval, set_timeout as timer_set_timeout,
894
+ };
765
895
 
766
896
  #[cfg(feature = "http")]
767
- pub use promise::promise_object;
897
+ pub use promise::{promise_instance_catch, promise_instance_then, promise_object};
768
898
 
769
899
  #[cfg(feature = "http")]
770
900
  pub use native_promise::{await_promise, fetch_all_promise, fetch_async_promise, fetch_promise};
@@ -788,7 +918,7 @@ pub fn regexp_new(args: &[Value]) -> Value {
788
918
  };
789
919
 
790
920
  match TishRegExp::new(&pattern, &flags) {
791
- Ok(re) => Value::RegExp(Rc::new(RefCell::new(re))),
921
+ Ok(re) => Value::RegExp(VmRef::new(re)),
792
922
  Err(e) => {
793
923
  eprintln!("RegExp error: {}", e);
794
924
  Value::Null
@@ -869,7 +999,7 @@ fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
869
999
  };
870
1000
  }
871
1001
 
872
- Value::Object(Rc::new(RefCell::new(obj)))
1002
+ Value::Object(VmRef::new(obj))
873
1003
  }
874
1004
  Ok(None) | Err(_) => {
875
1005
  if re.flags.global || re.flags.sticky {
@@ -884,12 +1014,12 @@ fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
884
1014
  pub fn string_split_regex(s: &Value, separator: &Value, limit: Option<usize>) -> Value {
885
1015
  let input = match s {
886
1016
  Value::String(s) => s.as_ref(),
887
- _ => return Value::Array(Rc::new(RefCell::new(vec![s.clone()]))),
1017
+ _ => return Value::Array(VmRef::new(vec![s.clone()])),
888
1018
  };
889
1019
 
890
1020
  let max = limit.unwrap_or(usize::MAX);
891
1021
  if max == 0 {
892
- return Value::Array(Rc::new(RefCell::new(Vec::new())));
1022
+ return Value::Array(VmRef::new(Vec::new()));
893
1023
  }
894
1024
 
895
1025
  match separator {
@@ -915,16 +1045,16 @@ pub fn string_split_regex(s: &Value, separator: &Value, limit: Option<usize>) ->
915
1045
  result.push(Value::String(input[last_end..].into()));
916
1046
  }
917
1047
 
918
- Value::Array(Rc::new(RefCell::new(result)))
1048
+ Value::Array(VmRef::new(result))
919
1049
  }
920
1050
  Value::String(sep) => {
921
1051
  let parts: Vec<Value> = input
922
1052
  .splitn(max, sep.as_ref())
923
1053
  .map(|s| Value::String(s.into()))
924
1054
  .collect();
925
- Value::Array(Rc::new(RefCell::new(parts)))
1055
+ Value::Array(VmRef::new(parts))
926
1056
  }
927
- _ => Value::Array(Rc::new(RefCell::new(vec![Value::String(input.into())]))),
1057
+ _ => Value::Array(VmRef::new(vec![Value::String(input.into())])),
928
1058
  }
929
1059
  }
930
1060
 
@@ -960,7 +1090,7 @@ pub fn string_match_regex(s: &Value, regexp: &Value) -> Value {
960
1090
  if matches.is_empty() {
961
1091
  Value::Null
962
1092
  } else {
963
- Value::Array(Rc::new(RefCell::new(matches)))
1093
+ Value::Array(VmRef::new(matches))
964
1094
  }
965
1095
  } else {
966
1096
  regexp_exec_impl(&mut re, input)