@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
@@ -1,6 +1,7 @@
1
1
  //! Web Fetch–aligned Response, ReadableStream, reader.read(), text()/json().
2
2
 
3
3
  use std::cell::RefCell;
4
+ use tishlang_core::VmRef;
4
5
  use std::pin::Pin;
5
6
  use std::rc::Rc;
6
7
  use std::sync::{Arc, Mutex};
@@ -56,7 +57,7 @@ impl TishPromise for FetchAllResponsesPromise {
56
57
  .unwrap_or_else(|e| build_error_response(&e))
57
58
  })
58
59
  .collect();
59
- Ok(Value::Array(Rc::new(RefCell::new(out))))
60
+ Ok(Value::Array(VmRef::new(out)))
60
61
  }
61
62
  Ok(Err(e)) => Ok(build_error_response(&e)),
62
63
  Err(_) => Err(Value::String("Promise dropped".into())),
@@ -86,19 +87,19 @@ impl TishPromise for ReadChunkPromise {
86
87
  let mut o = ObjectMap::default();
87
88
  o.insert(Arc::from("done"), Value::Bool(true));
88
89
  o.insert(Arc::from("value"), Value::Null);
89
- Ok(Value::Object(Rc::new(RefCell::new(o))))
90
+ Ok(Value::Object(VmRef::new(o)))
90
91
  }
91
92
  Ok(Ok(ReadChunk::Bytes(b))) => {
92
93
  let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
93
94
  let mut o = ObjectMap::default();
94
95
  o.insert(Arc::from("done"), Value::Bool(false));
95
- o.insert(Arc::from("value"), Value::Array(Rc::new(RefCell::new(arr))));
96
- Ok(Value::Object(Rc::new(RefCell::new(o))))
96
+ o.insert(Arc::from("value"), Value::Array(VmRef::new(arr)));
97
+ Ok(Value::Object(VmRef::new(o)))
97
98
  }
98
99
  Ok(Err(e)) => Err({
99
100
  let mut obj = ObjectMap::default();
100
101
  obj.insert(Arc::from("error"), Value::String(e.into()));
101
- Value::Object(Rc::new(RefCell::new(obj)))
102
+ Value::Object(VmRef::new(obj))
102
103
  }),
103
104
  Err(_) => Err(Value::String("Promise dropped".into())),
104
105
  }
@@ -123,13 +124,13 @@ impl TishPromise for JsonTextPromise {
123
124
  Err(e) => Err({
124
125
  let mut obj = ObjectMap::default();
125
126
  obj.insert(Arc::from("error"), Value::String(e.into()));
126
- Value::Object(Rc::new(RefCell::new(obj)))
127
+ Value::Object(VmRef::new(obj))
127
128
  }),
128
129
  },
129
130
  Ok(Err(e)) => Err({
130
131
  let mut obj = ObjectMap::default();
131
132
  obj.insert(Arc::from("error"), Value::String(e.into()));
132
- Value::Object(Rc::new(RefCell::new(obj)))
133
+ Value::Object(VmRef::new(obj))
133
134
  }),
134
135
  Err(_) => Err(Value::String("Promise dropped".into())),
135
136
  }
@@ -229,7 +230,7 @@ impl TishOpaque for HttpReadableStream {
229
230
  return None;
230
231
  }
231
232
  let body = Arc::clone(&self.body);
232
- Some(Rc::new(move |_args: &[Value]| match body.take_stream() {
233
+ Some(Arc::new(move |_args: &[Value]| match body.take_stream() {
233
234
  Ok(stream) => {
234
235
  let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
235
236
  Value::Opaque(Arc::new(HttpStreamReader {
@@ -240,7 +241,7 @@ impl TishOpaque for HttpReadableStream {
240
241
  Err(e) => {
241
242
  let mut m = ObjectMap::default();
242
243
  m.insert(Arc::from("error"), Value::String(e.into()));
243
- Value::Object(Rc::new(RefCell::new(m)))
244
+ Value::Object(VmRef::new(m))
244
245
  }
245
246
  }))
246
247
  }
@@ -270,7 +271,7 @@ impl TishOpaque for HttpStreamReader {
270
271
  }
271
272
  let inner = Arc::clone(&self.inner);
272
273
  let body = Arc::clone(&self.body);
273
- Some(Rc::new(move |_args: &[Value]| {
274
+ Some(Arc::new(move |_args: &[Value]| {
274
275
  let inner = Arc::clone(&inner);
275
276
  let body = Arc::clone(&body);
276
277
  let (tx, rx) = tokio::sync::oneshot::channel();
@@ -305,7 +306,7 @@ fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
305
306
  headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
306
307
  }
307
308
  }
308
- Value::Object(Rc::new(RefCell::new(headers_obj)))
309
+ Value::Object(VmRef::new(headers_obj))
309
310
  }
310
311
 
311
312
  pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
@@ -319,7 +320,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
319
320
  let body_stream_val = Value::Opaque(stream);
320
321
  let bh_text = Arc::clone(&body_holder);
321
322
  let bh_json = Arc::clone(&body_holder);
322
- let text_fn: NativeFn = Rc::new(move |_args: &[Value]| {
323
+ let text_fn: NativeFn = Arc::new(move |_args: &[Value]| {
323
324
  let bh = Arc::clone(&bh_text);
324
325
  let (tx, rx) = tokio::sync::oneshot::channel();
325
326
  crate::http::RUNTIME.with(|rt| {
@@ -330,7 +331,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
330
331
  });
331
332
  crate::promise_io::string_result_promise(rx)
332
333
  });
333
- let json_fn: NativeFn = Rc::new(move |_args: &[Value]| {
334
+ let json_fn: NativeFn = Arc::new(move |_args: &[Value]| {
334
335
  let bh = Arc::clone(&bh_json);
335
336
  let (tx, rx) = tokio::sync::oneshot::channel();
336
337
  crate::http::RUNTIME.with(|rt| {
@@ -350,7 +351,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
350
351
  obj.insert(Arc::from("body"), body_stream_val);
351
352
  obj.insert(Arc::from("text"), Value::Function(text_fn));
352
353
  obj.insert(Arc::from("json"), Value::Function(json_fn));
353
- Value::Object(Rc::new(RefCell::new(obj)))
354
+ Value::Object(VmRef::new(obj))
354
355
  }
355
356
 
356
357
  async fn send_request_parts(
@@ -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 `tish-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
+ }