@tishlang/tish 1.6.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 (113) 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/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. 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};
@@ -35,7 +36,11 @@ impl TishPromise for FetchResponsePromise {
35
36
  }
36
37
 
37
38
  struct FetchAllResponsesPromise {
38
- rx: Mutex<Option<tokio::sync::oneshot::Receiver<Result<Vec<Result<reqwest::Response, String>>, String>>>>,
39
+ rx: Mutex<
40
+ Option<
41
+ tokio::sync::oneshot::Receiver<Result<Vec<Result<reqwest::Response, String>>, String>>,
42
+ >,
43
+ >,
39
44
  }
40
45
 
41
46
  impl TishPromise for FetchAllResponsesPromise {
@@ -47,9 +52,12 @@ impl TishPromise for FetchAllResponsesPromise {
47
52
  Ok(Ok(vec)) => {
48
53
  let out: Vec<Value> = vec
49
54
  .into_iter()
50
- .map(|x| x.map(response_value_from_reqwest).unwrap_or_else(|e| build_error_response(&e)))
55
+ .map(|x| {
56
+ x.map(response_value_from_reqwest)
57
+ .unwrap_or_else(|e| build_error_response(&e))
58
+ })
51
59
  .collect();
52
- Ok(Value::Array(Rc::new(RefCell::new(out))))
60
+ Ok(Value::Array(VmRef::new(out)))
53
61
  }
54
62
  Ok(Err(e)) => Ok(build_error_response(&e)),
55
63
  Err(_) => Err(Value::String("Promise dropped".into())),
@@ -79,22 +87,19 @@ impl TishPromise for ReadChunkPromise {
79
87
  let mut o = ObjectMap::default();
80
88
  o.insert(Arc::from("done"), Value::Bool(true));
81
89
  o.insert(Arc::from("value"), Value::Null);
82
- Ok(Value::Object(Rc::new(RefCell::new(o))))
90
+ Ok(Value::Object(VmRef::new(o)))
83
91
  }
84
92
  Ok(Ok(ReadChunk::Bytes(b))) => {
85
93
  let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
86
94
  let mut o = ObjectMap::default();
87
95
  o.insert(Arc::from("done"), Value::Bool(false));
88
- o.insert(
89
- Arc::from("value"),
90
- Value::Array(Rc::new(RefCell::new(arr))),
91
- );
92
- 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)))
93
98
  }
94
99
  Ok(Err(e)) => Err({
95
100
  let mut obj = ObjectMap::default();
96
101
  obj.insert(Arc::from("error"), Value::String(e.into()));
97
- Value::Object(Rc::new(RefCell::new(obj)))
102
+ Value::Object(VmRef::new(obj))
98
103
  }),
99
104
  Err(_) => Err(Value::String("Promise dropped".into())),
100
105
  }
@@ -119,13 +124,13 @@ impl TishPromise for JsonTextPromise {
119
124
  Err(e) => Err({
120
125
  let mut obj = ObjectMap::default();
121
126
  obj.insert(Arc::from("error"), Value::String(e.into()));
122
- Value::Object(Rc::new(RefCell::new(obj)))
127
+ Value::Object(VmRef::new(obj))
123
128
  }),
124
129
  },
125
130
  Ok(Err(e)) => Err({
126
131
  let mut obj = ObjectMap::default();
127
132
  obj.insert(Arc::from("error"), Value::String(e.into()));
128
- Value::Object(Rc::new(RefCell::new(obj)))
133
+ Value::Object(VmRef::new(obj))
129
134
  }),
130
135
  Err(_) => Err(Value::String("Promise dropped".into())),
131
136
  }
@@ -160,18 +165,23 @@ impl HttpBody {
160
165
  let mut g = self.state.lock().unwrap();
161
166
  match &mut *g {
162
167
  BodyState::Fresh(r) => {
163
- let resp = r.take().ok_or_else(|| "Response body already consumed".to_string())?;
168
+ let resp = r
169
+ .take()
170
+ .ok_or_else(|| "Response body already consumed".to_string())?;
164
171
  *g = BodyState::ReadInProgress;
165
172
  Ok(Box::pin(resp.bytes_stream()))
166
173
  }
167
- BodyState::ReadInProgress => Err("ReadableStream is locked; getReader() already called".into()),
174
+ BodyState::ReadInProgress => {
175
+ Err("ReadableStream is locked; getReader() already called".into())
176
+ }
168
177
  BodyState::Gone => Err("Response body already consumed".into()),
169
178
  }
170
179
  }
171
180
 
172
181
  pub fn take_text_async(
173
182
  &self,
174
- ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + '_>> {
183
+ ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<String, String>> + Send + '_>>
184
+ {
175
185
  let resp = {
176
186
  let mut g = self.state.lock().unwrap();
177
187
  match &mut *g {
@@ -182,9 +192,9 @@ impl HttpBody {
182
192
  }
183
193
  None => Err("Response body already consumed".into()),
184
194
  },
185
- BodyState::ReadInProgress => Err(
186
- "Cannot call text(): body is locked by ReadableStreamDefaultReader".into(),
187
- ),
195
+ BodyState::ReadInProgress => {
196
+ Err("Cannot call text(): body is locked by ReadableStreamDefaultReader".into())
197
+ }
188
198
  BodyState::Gone => Err("Response body already consumed".into()),
189
199
  }
190
200
  };
@@ -220,7 +230,7 @@ impl TishOpaque for HttpReadableStream {
220
230
  return None;
221
231
  }
222
232
  let body = Arc::clone(&self.body);
223
- Some(Rc::new(move |_args: &[Value]| match body.take_stream() {
233
+ Some(Arc::new(move |_args: &[Value]| match body.take_stream() {
224
234
  Ok(stream) => {
225
235
  let inner = Arc::new(tokio::sync::Mutex::new(StreamSlot { stream }));
226
236
  Value::Opaque(Arc::new(HttpStreamReader {
@@ -231,7 +241,7 @@ impl TishOpaque for HttpReadableStream {
231
241
  Err(e) => {
232
242
  let mut m = ObjectMap::default();
233
243
  m.insert(Arc::from("error"), Value::String(e.into()));
234
- Value::Object(Rc::new(RefCell::new(m)))
244
+ Value::Object(VmRef::new(m))
235
245
  }
236
246
  }))
237
247
  }
@@ -261,7 +271,7 @@ impl TishOpaque for HttpStreamReader {
261
271
  }
262
272
  let inner = Arc::clone(&self.inner);
263
273
  let body = Arc::clone(&self.body);
264
- Some(Rc::new(move |_args: &[Value]| {
274
+ Some(Arc::new(move |_args: &[Value]| {
265
275
  let inner = Arc::clone(&inner);
266
276
  let body = Arc::clone(&body);
267
277
  let (tx, rx) = tokio::sync::oneshot::channel();
@@ -296,7 +306,7 @@ fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
296
306
  headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
297
307
  }
298
308
  }
299
- Value::Object(Rc::new(RefCell::new(headers_obj)))
309
+ Value::Object(VmRef::new(headers_obj))
300
310
  }
301
311
 
302
312
  pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
@@ -310,7 +320,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
310
320
  let body_stream_val = Value::Opaque(stream);
311
321
  let bh_text = Arc::clone(&body_holder);
312
322
  let bh_json = Arc::clone(&body_holder);
313
- let text_fn: NativeFn = Rc::new(move |_args: &[Value]| {
323
+ let text_fn: NativeFn = Arc::new(move |_args: &[Value]| {
314
324
  let bh = Arc::clone(&bh_text);
315
325
  let (tx, rx) = tokio::sync::oneshot::channel();
316
326
  crate::http::RUNTIME.with(|rt| {
@@ -321,7 +331,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
321
331
  });
322
332
  crate::promise_io::string_result_promise(rx)
323
333
  });
324
- let json_fn: NativeFn = Rc::new(move |_args: &[Value]| {
334
+ let json_fn: NativeFn = Arc::new(move |_args: &[Value]| {
325
335
  let bh = Arc::clone(&bh_json);
326
336
  let (tx, rx) = tokio::sync::oneshot::channel();
327
337
  crate::http::RUNTIME.with(|rt| {
@@ -341,7 +351,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
341
351
  obj.insert(Arc::from("body"), body_stream_val);
342
352
  obj.insert(Arc::from("text"), Value::Function(text_fn));
343
353
  obj.insert(Arc::from("json"), Value::Function(json_fn));
344
- Value::Object(Rc::new(RefCell::new(obj)))
354
+ Value::Object(VmRef::new(obj))
345
355
  }
346
356
 
347
357
  async fn send_request_parts(
@@ -420,7 +430,8 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
420
430
  Some(u) => (u, Some(req.clone())),
421
431
  None => {
422
432
  let (tx, rx) = tokio::sync::oneshot::channel();
423
- let _ = tx.send(Err("Each request object must have a 'url' property".into()));
433
+ let _ =
434
+ tx.send(Err("Each request object must have a 'url' property".into()));
424
435
  return Value::Promise(Arc::new(FetchAllResponsesPromise {
425
436
  rx: Mutex::new(Some(rx)),
426
437
  }));
@@ -430,7 +441,7 @@ pub fn fetch_all_promise_from_args(args: Vec<Value>) -> Value {
430
441
  _ => {
431
442
  let (tx, rx) = tokio::sync::oneshot::channel();
432
443
  let _ = tx.send(Err(
433
- "Each request must be a string URL or request object".into(),
444
+ "Each request must be a string URL or request object".into()
434
445
  ));
435
446
  return Value::Promise(Arc::new(FetchAllResponsesPromise {
436
447
  rx: Mutex::new(Some(rx)),
@@ -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
+ }