@tishlang/tish-format 1.0.12 → 1.0.13

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 (164) hide show
  1. package/Cargo.toml +49 -0
  2. package/LICENSE +13 -0
  3. package/README.md +138 -0
  4. package/bin/tish-format +0 -0
  5. package/crates/js_to_tish/Cargo.toml +11 -0
  6. package/crates/js_to_tish/README.md +18 -0
  7. package/crates/js_to_tish/src/error.rs +55 -0
  8. package/crates/js_to_tish/src/lib.rs +11 -0
  9. package/crates/js_to_tish/src/span_util.rs +35 -0
  10. package/crates/js_to_tish/src/transform/expr.rs +610 -0
  11. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  12. package/crates/js_to_tish/src/transform.rs +60 -0
  13. package/crates/tish/Cargo.toml +54 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +565 -0
  16. package/crates/tish/src/main.rs +781 -0
  17. package/crates/tish/src/repl_completion.rs +200 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  24. package/crates/tish/tests/integration_test.rs +1095 -0
  25. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  26. package/crates/tish/tests/shortcircuit.rs +65 -0
  27. package/crates/tish_ast/Cargo.toml +9 -0
  28. package/crates/tish_ast/src/ast.rs +620 -0
  29. package/crates/tish_ast/src/lib.rs +5 -0
  30. package/crates/tish_build_utils/Cargo.toml +11 -0
  31. package/crates/tish_build_utils/src/lib.rs +577 -0
  32. package/crates/tish_builtins/Cargo.toml +20 -0
  33. package/crates/tish_builtins/src/array.rs +441 -0
  34. package/crates/tish_builtins/src/construct.rs +159 -0
  35. package/crates/tish_builtins/src/globals.rs +213 -0
  36. package/crates/tish_builtins/src/helpers.rs +35 -0
  37. package/crates/tish_builtins/src/lib.rs +16 -0
  38. package/crates/tish_builtins/src/math.rs +89 -0
  39. package/crates/tish_builtins/src/object.rs +36 -0
  40. package/crates/tish_builtins/src/string.rs +647 -0
  41. package/crates/tish_builtins/src/symbol.rs +83 -0
  42. package/crates/tish_bytecode/Cargo.toml +17 -0
  43. package/crates/tish_bytecode/src/chunk.rs +96 -0
  44. package/crates/tish_bytecode/src/compiler.rs +1760 -0
  45. package/crates/tish_bytecode/src/encoding.rs +100 -0
  46. package/crates/tish_bytecode/src/lib.rs +19 -0
  47. package/crates/tish_bytecode/src/opcode.rs +142 -0
  48. package/crates/tish_bytecode/src/peephole.rs +189 -0
  49. package/crates/tish_bytecode/src/serialize.rs +163 -0
  50. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  51. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  52. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  53. package/crates/tish_compile/Cargo.toml +26 -0
  54. package/crates/tish_compile/src/codegen.rs +5332 -0
  55. package/crates/tish_compile/src/infer.rs +292 -0
  56. package/crates/tish_compile/src/lib.rs +164 -0
  57. package/crates/tish_compile/src/resolve.rs +1388 -0
  58. package/crates/tish_compile/src/types.rs +501 -0
  59. package/crates/tish_compile_js/Cargo.toml +18 -0
  60. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  61. package/crates/tish_compile_js/src/codegen.rs +871 -0
  62. package/crates/tish_compile_js/src/error.rs +20 -0
  63. package/crates/tish_compile_js/src/lib.rs +26 -0
  64. package/crates/tish_compile_js/src/tests_jsx.rs +350 -0
  65. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  66. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  67. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  68. package/crates/tish_core/Cargo.toml +26 -0
  69. package/crates/tish_core/src/console_style.rs +160 -0
  70. package/crates/tish_core/src/json.rs +387 -0
  71. package/crates/tish_core/src/lib.rs +17 -0
  72. package/crates/tish_core/src/macros.rs +36 -0
  73. package/crates/tish_core/src/uri.rs +118 -0
  74. package/crates/tish_core/src/value.rs +696 -0
  75. package/crates/tish_core/src/vmref.rs +178 -0
  76. package/crates/tish_cranelift/Cargo.toml +19 -0
  77. package/crates/tish_cranelift/src/lib.rs +43 -0
  78. package/crates/tish_cranelift/src/link.rs +117 -0
  79. package/crates/tish_cranelift/src/lower.rs +85 -0
  80. package/crates/tish_cranelift_runtime/Cargo.toml +25 -0
  81. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  82. package/crates/tish_eval/Cargo.toml +45 -0
  83. package/crates/tish_eval/src/eval.rs +3717 -0
  84. package/crates/tish_eval/src/http.rs +188 -0
  85. package/crates/tish_eval/src/lib.rs +99 -0
  86. package/crates/tish_eval/src/natives.rs +399 -0
  87. package/crates/tish_eval/src/promise.rs +179 -0
  88. package/crates/tish_eval/src/regex.rs +299 -0
  89. package/crates/tish_eval/src/timers.rs +120 -0
  90. package/crates/tish_eval/src/value.rs +318 -0
  91. package/crates/tish_eval/src/value_convert.rs +111 -0
  92. package/crates/tish_fmt/Cargo.toml +16 -0
  93. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  94. package/crates/tish_fmt/src/lib.rs +2101 -0
  95. package/crates/tish_jsx_web/Cargo.toml +9 -0
  96. package/crates/tish_jsx_web/README.md +5 -0
  97. package/crates/tish_jsx_web/src/lib.rs +2 -0
  98. package/crates/tish_lexer/Cargo.toml +9 -0
  99. package/crates/tish_lexer/src/lib.rs +716 -0
  100. package/crates/tish_lexer/src/token.rs +163 -0
  101. package/crates/tish_lint/Cargo.toml +18 -0
  102. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  103. package/crates/tish_lint/src/lib.rs +289 -0
  104. package/crates/tish_llvm/Cargo.toml +13 -0
  105. package/crates/tish_llvm/src/lib.rs +115 -0
  106. package/crates/tish_lsp/Cargo.toml +25 -0
  107. package/crates/tish_lsp/README.md +26 -0
  108. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  109. package/crates/tish_lsp/src/import_goto.rs +562 -0
  110. package/crates/tish_lsp/src/main.rs +1046 -0
  111. package/crates/tish_native/Cargo.toml +16 -0
  112. package/crates/tish_native/src/build.rs +427 -0
  113. package/crates/tish_native/src/config.rs +48 -0
  114. package/crates/tish_native/src/lib.rs +416 -0
  115. package/crates/tish_opt/Cargo.toml +13 -0
  116. package/crates/tish_opt/src/lib.rs +943 -0
  117. package/crates/tish_parser/Cargo.toml +11 -0
  118. package/crates/tish_parser/src/lib.rs +332 -0
  119. package/crates/tish_parser/src/parser.rs +2304 -0
  120. package/crates/tish_pg/Cargo.toml +34 -0
  121. package/crates/tish_pg/README.md +38 -0
  122. package/crates/tish_pg/src/error.rs +52 -0
  123. package/crates/tish_pg/src/lib.rs +955 -0
  124. package/crates/tish_resolve/Cargo.toml +13 -0
  125. package/crates/tish_resolve/src/lib.rs +3561 -0
  126. package/crates/tish_resolve/src/pos.rs +141 -0
  127. package/crates/tish_runtime/Cargo.toml +96 -0
  128. package/crates/tish_runtime/src/http.rs +1298 -0
  129. package/crates/tish_runtime/src/http_fetch.rs +471 -0
  130. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  131. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  132. package/crates/tish_runtime/src/lib.rs +1192 -0
  133. package/crates/tish_runtime/src/native_promise.rs +15 -0
  134. package/crates/tish_runtime/src/promise.rs +248 -0
  135. package/crates/tish_runtime/src/promise_io.rs +38 -0
  136. package/crates/tish_runtime/src/timers.rs +166 -0
  137. package/crates/tish_runtime/src/ws.rs +761 -0
  138. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  139. package/crates/tish_ui/Cargo.toml +17 -0
  140. package/crates/tish_ui/src/jsx.rs +682 -0
  141. package/crates/tish_ui/src/lib.rs +20 -0
  142. package/crates/tish_ui/src/runtime/hooks.rs +569 -0
  143. package/crates/tish_ui/src/runtime/mod.rs +180 -0
  144. package/crates/tish_vm/Cargo.toml +47 -0
  145. package/crates/tish_vm/src/lib.rs +39 -0
  146. package/crates/tish_vm/src/vm.rs +2192 -0
  147. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  148. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  149. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  150. package/crates/tish_wasm/Cargo.toml +15 -0
  151. package/crates/tish_wasm/src/lib.rs +424 -0
  152. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  153. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  154. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  155. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  156. package/crates/tishlang_cargo_bindgen/src/classify.rs +263 -0
  157. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  158. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  159. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  160. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  161. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  162. package/justfile +268 -0
  163. package/package.json +1 -1
  164. package/platform/darwin-arm64/tish-fmt +0 -0
@@ -0,0 +1,418 @@
1
+ //! `hyper`-based HTTP backend for `tish:http`.
2
+ //!
3
+ //! Selectable at runtime via `TISH_HTTP_BACKEND=hyper` when the
4
+ //! `http-hyper` feature is compiled in. When unset, `serve` falls back to
5
+ //! the `tiny_http` path in [`crate::http`].
6
+ //!
7
+ //! ## Architecture
8
+ //!
9
+ //! ```text
10
+ //! N OS threads (one per CPU, pinned via core_affinity)
11
+ //! ├─ single-threaded tokio runtime per thread
12
+ //! ├─ SO_REUSEPORT-bound TcpListener per thread
13
+ //! └─ hyper HTTP/1.1 + HTTP/2 server
14
+ //! │
15
+ //! ├─ async per-connection state machine (no OS thread)
16
+ //! ├─ static-route fast path (lock-free ArcSwap<HashMap> from
17
+ //! │ [`crate::http::register_static_route`])
18
+ //! └─ VM-dispatch slow path: crosses mpsc to VM thread, awaits
19
+ //! oneshot reply, writes response
20
+ //! ```
21
+ //!
22
+ //! ## Why this is broadly useful (beyond the bench)
23
+ //!
24
+ //! * Removes tiny_http's thread-per-connection model (the bottleneck on
25
+ //! every macOS/Linux Tish HTTP server at >50k concurrent connections).
26
+ //! * Gives HTTP/2 + TLS-ready surface for free (hyper handles the state
27
+ //! machine; we only have to convert our `RequestPrimitive` to/from
28
+ //! `http::Request` / `http::Response`).
29
+ //! * Shared async context with `reqwest`, `tokio-postgres`, `tokio` in
30
+ //! general — the tokio runtime is reused for client fetch, db, and
31
+ //! server accept.
32
+ //!
33
+ //! ## Integration with the Tish VM
34
+ //!
35
+ //! The Tish handler returns a synchronous `Value`. We call it from inside
36
+ //! a tokio task via `tokio::task::spawn_blocking`, which:
37
+ //! * detaches onto tokio's blocking thread pool,
38
+ //! * lets `tishlang_pg`'s `block_on` detect no ambient runtime and block
39
+ //! directly (no extra thread spawn per query),
40
+ //! * unblocks hyper's reactor to serve other connections while the VM
41
+ //! runs.
42
+ //!
43
+ //! Multi-VM (one Tish VM per worker thread) is the **next** step layered
44
+ //! on top of this file — see [`WorkerHandler`] below and the `onWorker`
45
+ //! semantics in [`crate::http::serve`]. For now the VM stays single-threaded
46
+ //! and we share it via the mpsc-dispatch pattern; adding per-core VMs is
47
+ //! a drop-in replacement of `dispatch_to_vm` with a per-worker handler
48
+ //! closure.
49
+
50
+ use std::convert::Infallible;
51
+ use std::sync::atomic::{AtomicBool, Ordering};
52
+ use std::sync::{mpsc as std_mpsc, Arc};
53
+ use std::thread;
54
+
55
+ use http_body_util::Full;
56
+ use hyper::body::Bytes;
57
+ use hyper::server::conn::http1;
58
+ use hyper::service::service_fn;
59
+ use hyper::{Request, Response, StatusCode};
60
+ use hyper_util::rt::TokioIo;
61
+
62
+ use crate::http::{
63
+ bind_listeners_reuseport, build_error_response_pub, cached_date_header_arc,
64
+ lookup_static_route_pub, num_workers_pub as num_workers, RequestPrimitive, ResponseBody,
65
+ ResponsePrimitive, StaticRouteSnapshot,
66
+ };
67
+
68
+ /// Bridge from hyper-thread → VM thread and back. Same `Job` shape as the
69
+ /// tiny_http path uses so the VM dispatcher code is identical.
70
+ pub(crate) type Job = (
71
+ RequestPrimitive,
72
+ std_mpsc::SyncSender<ResponsePrimitive>,
73
+ );
74
+
75
+ /// Drop-in replacement for [`crate::http::serve`]. Same arg layout, same
76
+ /// return value. Selected at runtime when `TISH_HTTP_BACKEND=hyper`.
77
+ pub fn serve<F>(args: &[tishlang_core::Value], handler: F) -> tishlang_core::Value
78
+ where
79
+ F: Fn(&[tishlang_core::Value]) -> tishlang_core::Value,
80
+ {
81
+ use tishlang_core::Value;
82
+
83
+ let port = match args.first() {
84
+ Some(Value::Number(n)) => *n as u16,
85
+ _ => return build_error_response_pub("serve requires a port number"),
86
+ };
87
+
88
+ let max_requests: Option<usize> = args.get(2).and_then(|v| match v {
89
+ Value::Number(n) if *n >= 1.0 => Some(*n as usize),
90
+ _ => None,
91
+ });
92
+
93
+ // Kick the Date background thread so the first response has a value.
94
+ let _ = cached_date_header_arc();
95
+
96
+ // Prefork: same semantics as tiny_http path — spawn N-1 subprocesses,
97
+ // each runs its own single-threaded tokio runtime + Tish VM.
98
+ let role = crate::http_prefork::role_from_env();
99
+ let prefork_n = crate::http::num_prefork_workers_pub();
100
+ let mut in_prefork_group = false;
101
+ match role {
102
+ crate::http_prefork::PreforkRole::Parent if prefork_n > 1 => {
103
+ match crate::http_prefork::spawn_children(prefork_n) {
104
+ Ok(c) => {
105
+ let _ = crate::http_prefork::install_parent_signal_handler(c);
106
+ in_prefork_group = true;
107
+ }
108
+ Err(e) => {
109
+ eprintln!(
110
+ "[tish http/hyper] prefork spawn failed, running single process: {}",
111
+ e
112
+ );
113
+ }
114
+ }
115
+ }
116
+ _ => {}
117
+ }
118
+
119
+ let workers = if in_prefork_group { 1 } else { num_workers() };
120
+ let listeners = match bind_listeners_reuseport(port, workers) {
121
+ Ok(l) => l,
122
+ Err(e) => {
123
+ eprintln!("[tish http/hyper] failed to bind: {}", e);
124
+ return build_error_response_pub(&e);
125
+ }
126
+ };
127
+
128
+ let worker_id = match role {
129
+ crate::http_prefork::PreforkRole::Child(id) => id,
130
+ _ => 0,
131
+ };
132
+ if matches!(role, crate::http_prefork::PreforkRole::Parent) && in_prefork_group {
133
+ println!(
134
+ "tish http/hyper: listening on http://0.0.0.0:{} ({} process{} x {} accept thread{})",
135
+ port,
136
+ prefork_n,
137
+ if prefork_n == 1 { "" } else { "es" },
138
+ workers,
139
+ if workers == 1 { "" } else { "s" }
140
+ );
141
+ } else if worker_id == 0 {
142
+ println!(
143
+ "tish http/hyper: listening on http://0.0.0.0:{} ({} worker{})",
144
+ port,
145
+ workers,
146
+ if workers == 1 { "" } else { "s" }
147
+ );
148
+ }
149
+
150
+ // Shared job queue (same shape as tiny_http path).
151
+ let (tx, rx) = std_mpsc::sync_channel::<Job>(workers * 512);
152
+ let stop = Arc::new(AtomicBool::new(false));
153
+
154
+ // Spawn the HTTP worker threads. Each owns a single-threaded tokio
155
+ // runtime and a SO_REUSEPORT-bound listener. Optionally pins to a
156
+ // physical core via core_affinity (opt-in via env).
157
+ let core_ids: Vec<Option<core_affinity::CoreId>> = {
158
+ let pin = std::env::var("TISH_HTTP_PIN_CORES")
159
+ .map(|v| v != "0" && v != "false")
160
+ .unwrap_or(false);
161
+ if pin {
162
+ core_affinity::get_core_ids()
163
+ .unwrap_or_default()
164
+ .into_iter()
165
+ .map(Some)
166
+ .collect()
167
+ } else {
168
+ (0..workers).map(|_| None).collect()
169
+ }
170
+ };
171
+
172
+ let mut worker_handles = Vec::with_capacity(workers);
173
+ for (idx, listener) in listeners.into_iter().enumerate() {
174
+ let tx = tx.clone();
175
+ let stop = Arc::clone(&stop);
176
+ let core = core_ids.get(idx).copied().flatten();
177
+ let handle = thread::Builder::new()
178
+ .name(format!("tish-http-h{}", idx))
179
+ .spawn(move || worker_thread(listener, tx, stop, core))
180
+ .expect("spawn tish-http-hyper worker");
181
+ worker_handles.push(handle);
182
+ }
183
+ drop(tx);
184
+
185
+ // VM-thread dispatcher loop: identical to the tiny_http path.
186
+ let mut count = 0usize;
187
+ while let Ok((req_prim, resp_tx)) = rx.recv() {
188
+ let req_value = req_prim.into_value_pub();
189
+ let response_value = handler(&[req_value]);
190
+ let resp_prim = ResponsePrimitive::from_value_pub(&response_value);
191
+ let _ = resp_tx.send(resp_prim);
192
+
193
+ count += 1;
194
+ if max_requests.map(|m| count >= m).unwrap_or(false) {
195
+ stop.store(true, Ordering::Relaxed);
196
+ break;
197
+ }
198
+ }
199
+
200
+ stop.store(true, Ordering::Relaxed);
201
+ for h in worker_handles {
202
+ let _ = h.join();
203
+ }
204
+
205
+ Value::Null
206
+ }
207
+
208
+ /// Per-OS-thread entry: build a single-threaded tokio runtime, adopt the
209
+ /// pre-bound SO_REUSEPORT listener, serve connections with hyper.
210
+ fn worker_thread(
211
+ listener: std::net::TcpListener,
212
+ dispatch: std_mpsc::SyncSender<Job>,
213
+ stop: Arc<AtomicBool>,
214
+ core: Option<core_affinity::CoreId>,
215
+ ) {
216
+ if let Some(id) = core {
217
+ let _ = core_affinity::set_for_current(id);
218
+ }
219
+ let rt = tokio::runtime::Builder::new_current_thread()
220
+ .enable_all()
221
+ .build()
222
+ .expect("tish-http-hyper: build current-thread runtime");
223
+
224
+ rt.block_on(async move {
225
+ listener
226
+ .set_nonblocking(true)
227
+ .expect("tish-http-hyper: set_nonblocking");
228
+ let tokio_listener = tokio::net::TcpListener::from_std(listener)
229
+ .expect("tish-http-hyper: adopt tokio listener");
230
+
231
+ loop {
232
+ if stop.load(Ordering::Relaxed) {
233
+ break;
234
+ }
235
+ // Accept with a short timeout so we notice the stop flag.
236
+ let accept_fut = tokio_listener.accept();
237
+ let (stream, _peer) = match tokio::time::timeout(
238
+ std::time::Duration::from_millis(250),
239
+ accept_fut,
240
+ )
241
+ .await
242
+ {
243
+ Ok(Ok(s)) => s,
244
+ Ok(Err(e)) => {
245
+ eprintln!("[tish http/hyper] accept error: {}", e);
246
+ continue;
247
+ }
248
+ Err(_) => continue, // timeout; re-check stop flag
249
+ };
250
+ // TCP_NODELAY once, at the socket level.
251
+ let _ = stream.set_nodelay(true);
252
+
253
+ let dispatch = dispatch.clone();
254
+ tokio::task::spawn(async move {
255
+ let io = TokioIo::new(stream);
256
+ let svc = service_fn(move |req: Request<hyper::body::Incoming>| {
257
+ let dispatch = dispatch.clone();
258
+ async move { handle_request(req, dispatch).await }
259
+ });
260
+ if let Err(e) = http1::Builder::new()
261
+ .keep_alive(true)
262
+ .serve_connection(io, svc)
263
+ .await
264
+ {
265
+ // Chatty connection errors (resets, client timeouts) are
266
+ // normal; silently drop unless debug logging is on.
267
+ if std::env::var_os("TISH_HTTP_DEBUG").is_some() {
268
+ eprintln!("[tish http/hyper] conn closed: {}", e);
269
+ }
270
+ }
271
+ });
272
+ }
273
+ });
274
+ }
275
+
276
+ /// Per-request hyper service. Static-route fast path first; otherwise
277
+ /// dispatch via mpsc to the VM thread.
278
+ async fn handle_request(
279
+ req: Request<hyper::body::Incoming>,
280
+ dispatch: std_mpsc::SyncSender<Job>,
281
+ ) -> Result<Response<Full<Bytes>>, Infallible> {
282
+ // Fast path: static routes (pre-baked bodies).
283
+ let uri_path_str = req.uri().path();
284
+ if let Some(route) = lookup_static_route_pub(uri_path_str) {
285
+ return Ok(static_route_response(route));
286
+ }
287
+
288
+ // Slow path: cross the mpsc to the VM thread.
289
+ let (method, url, path, query, headers, body_bytes) = extract_request(req).await;
290
+ let body = String::from_utf8(body_bytes).unwrap_or_default();
291
+ let prim = RequestPrimitive::new_pub(method, url, path, query, headers, body);
292
+
293
+ let (resp_tx, resp_rx) = std_mpsc::sync_channel::<ResponsePrimitive>(1);
294
+ if dispatch.send((prim, resp_tx)).is_err() {
295
+ return Ok(simple_error_response(
296
+ StatusCode::SERVICE_UNAVAILABLE,
297
+ "tish http: dispatch channel closed",
298
+ ));
299
+ }
300
+
301
+ // Block on the VM response from a tokio blocking worker so we don't
302
+ // starve hyper's reactor while the Tish handler runs.
303
+ let resp_prim = tokio::task::spawn_blocking(move || {
304
+ resp_rx
305
+ .recv_timeout(std::time::Duration::from_secs(30))
306
+ .ok()
307
+ })
308
+ .await
309
+ .ok()
310
+ .flatten();
311
+
312
+ match resp_prim {
313
+ Some(r) => Ok(primitive_to_hyper(r)),
314
+ None => Ok(simple_error_response(
315
+ StatusCode::GATEWAY_TIMEOUT,
316
+ "tish handler timed out",
317
+ )),
318
+ }
319
+ }
320
+
321
+ async fn extract_request(
322
+ req: Request<hyper::body::Incoming>,
323
+ ) -> (
324
+ String,
325
+ String,
326
+ String,
327
+ String,
328
+ Vec<(String, String)>,
329
+ Vec<u8>,
330
+ ) {
331
+ let method = req.method().to_string();
332
+ let uri = req.uri().clone();
333
+ let url = uri.to_string();
334
+ let path = uri.path().to_string();
335
+ let query = uri.query().unwrap_or("").to_string();
336
+ let headers: Vec<(String, String)> = req
337
+ .headers()
338
+ .iter()
339
+ .map(|(k, v)| (k.as_str().to_string(), v.to_str().unwrap_or("").to_string()))
340
+ .collect();
341
+ let body_bytes = match http_body_util::BodyExt::collect(req.into_body()).await {
342
+ Ok(c) => c.to_bytes().to_vec(),
343
+ Err(_) => Vec::new(),
344
+ };
345
+ (method, url, path, query, headers, body_bytes)
346
+ }
347
+
348
+ fn primitive_to_hyper(resp: ResponsePrimitive) -> Response<Full<Bytes>> {
349
+ let (body_bytes, default_ct): (Bytes, Option<&str>) = match resp.body {
350
+ ResponseBody::Text(s) => (Bytes::copy_from_slice(s.as_bytes()), Some("text/plain")),
351
+ ResponseBody::Bytes(b) => (Bytes::from(b), Some("application/octet-stream")),
352
+ ResponseBody::File(p) => match std::fs::read(&p) {
353
+ Ok(b) => (Bytes::from(b), Some("application/octet-stream")),
354
+ Err(_) => (Bytes::from_static(b"file not found"), Some("text/plain")),
355
+ },
356
+ };
357
+ let mut builder = Response::builder().status(resp.status);
358
+ let mut has_ct = false;
359
+ let mut has_server = false;
360
+ let mut has_date = false;
361
+ for (k, v) in &resp.headers {
362
+ if k.eq_ignore_ascii_case("content-type") {
363
+ has_ct = true;
364
+ }
365
+ if k.eq_ignore_ascii_case("server") {
366
+ has_server = true;
367
+ }
368
+ if k.eq_ignore_ascii_case("date") {
369
+ has_date = true;
370
+ }
371
+ builder = builder.header(k, v);
372
+ }
373
+ if !has_ct {
374
+ if let Some(ct) = default_ct {
375
+ builder = builder.header("content-type", ct);
376
+ }
377
+ }
378
+ if !has_server {
379
+ builder = builder.header("server", "Tish");
380
+ }
381
+ if !has_date {
382
+ builder = builder.header("date", cached_date_header_arc().as_str());
383
+ }
384
+ builder.body(Full::new(body_bytes)).unwrap_or_else(|_| {
385
+ Response::builder()
386
+ .status(StatusCode::INTERNAL_SERVER_ERROR)
387
+ .body(Full::new(Bytes::new()))
388
+ .unwrap()
389
+ })
390
+ }
391
+
392
+ fn static_route_response(route: StaticRouteSnapshot) -> Response<Full<Bytes>> {
393
+ let body = Bytes::copy_from_slice(&route.body);
394
+ Response::builder()
395
+ .status(200)
396
+ .header("content-type", route.content_type.as_ref())
397
+ .header("server", "Tish")
398
+ .header("date", cached_date_header_arc().as_str())
399
+ .body(Full::new(body))
400
+ .unwrap()
401
+ }
402
+
403
+ fn simple_error_response(status: StatusCode, msg: &str) -> Response<Full<Bytes>> {
404
+ Response::builder()
405
+ .status(status)
406
+ .header("content-type", "text/plain")
407
+ .header("server", "Tish")
408
+ .body(Full::new(Bytes::copy_from_slice(msg.as_bytes())))
409
+ .unwrap()
410
+ }
411
+
412
+ /// Marker used by external callers (like the bench) to gate the backend
413
+ /// choice. Returns true when `TISH_HTTP_BACKEND=hyper` is in the env.
414
+ pub fn is_enabled_via_env() -> bool {
415
+ std::env::var("TISH_HTTP_BACKEND")
416
+ .map(|v| v.eq_ignore_ascii_case("hyper"))
417
+ .unwrap_or(false)
418
+ }
@@ -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
+ }