@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.
- package/Cargo.toml +2 -0
- package/README.md +2 -0
- package/bin/tish +0 -0
- package/crates/js_to_tish/src/transform/expr.rs +28 -8
- package/crates/js_to_tish/src/transform/stmt.rs +49 -22
- package/crates/tish/Cargo.toml +14 -5
- package/crates/tish/src/cargo_native_registry.rs +29 -0
- package/crates/tish/src/cli_help.rs +16 -10
- package/crates/tish/src/main.rs +87 -32
- package/crates/tish/src/repl_completion.rs +3 -3
- package/crates/tish/tests/cargo_example_compile.rs +1 -1
- package/crates/tish/tests/integration_test.rs +19 -7
- package/crates/tish/tests/shortcircuit.rs +1 -1
- package/crates/tish_ast/src/ast.rs +80 -9
- package/crates/tish_build_utils/Cargo.toml +4 -0
- package/crates/tish_build_utils/src/lib.rs +105 -2
- package/crates/tish_builtins/Cargo.toml +5 -1
- package/crates/tish_builtins/src/array.rs +13 -12
- package/crates/tish_builtins/src/construct.rs +34 -33
- package/crates/tish_builtins/src/globals.rs +12 -11
- package/crates/tish_builtins/src/helpers.rs +2 -1
- package/crates/tish_builtins/src/object.rs +3 -2
- package/crates/tish_builtins/src/string.rs +73 -3
- package/crates/tish_bytecode/src/compiler.rs +12 -14
- package/crates/tish_bytecode/src/opcode.rs +12 -3
- package/crates/tish_compile/Cargo.toml +1 -0
- package/crates/tish_compile/src/codegen.rs +745 -199
- package/crates/tish_compile/src/infer.rs +6 -0
- package/crates/tish_compile/src/lib.rs +4 -3
- package/crates/tish_compile/src/resolve.rs +180 -82
- package/crates/tish_compile/src/types.rs +175 -11
- package/crates/tish_compile_js/Cargo.toml +1 -0
- package/crates/tish_compile_js/src/codegen.rs +152 -29
- package/crates/tish_compile_js/src/lib.rs +3 -1
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
- package/crates/tish_core/Cargo.toml +8 -0
- package/crates/tish_core/src/json.rs +102 -53
- package/crates/tish_core/src/lib.rs +3 -1
- package/crates/tish_core/src/macros.rs +5 -5
- package/crates/tish_core/src/value.rs +53 -15
- package/crates/tish_core/src/vmref.rs +178 -0
- package/crates/tish_eval/Cargo.toml +17 -2
- package/crates/tish_eval/src/eval.rs +90 -28
- package/crates/tish_eval/src/http.rs +61 -0
- package/crates/tish_eval/src/lib.rs +3 -3
- package/crates/tish_eval/src/natives.rs +41 -0
- package/crates/tish_eval/src/value.rs +7 -3
- package/crates/tish_eval/src/value_convert.rs +13 -5
- package/crates/tish_fmt/src/lib.rs +120 -30
- package/crates/tish_lexer/src/lib.rs +20 -5
- package/crates/tish_lexer/src/token.rs +4 -0
- package/crates/tish_llvm/src/lib.rs +3 -1
- package/crates/tish_lsp/Cargo.toml +4 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_lsp/src/builtin_goto.rs +261 -0
- package/crates/tish_lsp/src/import_goto.rs +549 -0
- package/crates/tish_lsp/src/main.rs +502 -102
- package/crates/tish_native/src/build.rs +3 -2
- package/crates/tish_native/src/lib.rs +6 -2
- package/crates/tish_opt/src/lib.rs +17 -2
- package/crates/tish_parser/src/lib.rs +10 -3
- package/crates/tish_parser/src/parser.rs +346 -56
- package/crates/tish_pg/Cargo.toml +34 -0
- package/crates/tish_pg/README.md +38 -0
- package/crates/tish_pg/src/error.rs +52 -0
- package/crates/tish_pg/src/lib.rs +967 -0
- package/crates/tish_resolve/Cargo.toml +13 -0
- package/crates/tish_resolve/src/lib.rs +3436 -0
- package/crates/tish_resolve/src/pos.rs +133 -0
- package/crates/tish_runtime/Cargo.toml +68 -3
- package/crates/tish_runtime/src/http.rs +1123 -141
- package/crates/tish_runtime/src/http_fetch.rs +15 -14
- package/crates/tish_runtime/src/http_hyper.rs +418 -0
- package/crates/tish_runtime/src/http_prefork.rs +189 -0
- package/crates/tish_runtime/src/lib.rs +159 -29
- package/crates/tish_runtime/src/promise.rs +199 -36
- package/crates/tish_runtime/src/promise_io.rs +2 -1
- package/crates/tish_runtime/src/timers.rs +37 -1
- package/crates/tish_runtime/src/ws.rs +26 -28
- package/crates/tish_ui/src/jsx.rs +279 -8
- package/crates/tish_ui/src/lib.rs +5 -2
- package/crates/tish_ui/src/runtime/hooks.rs +406 -45
- package/crates/tish_ui/src/runtime/mod.rs +36 -9
- package/crates/tish_vm/Cargo.toml +15 -5
- package/crates/tish_vm/src/vm.rs +506 -259
- package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
- package/crates/tish_wasm/src/lib.rs +17 -14
- package/crates/tish_wasm_runtime/Cargo.toml +2 -1
- package/crates/tish_wasm_runtime/src/lib.rs +1 -1
- package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
- package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
- package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
- package/justfile +8 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- 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,
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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(
|
|
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
|
-
|
|
582
|
-
|
|
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 =
|
|
655
|
+
let re = re.clone();
|
|
603
656
|
if key == "exec" {
|
|
604
|
-
Value::
|
|
657
|
+
Value::native(move |args: &[Value]| {
|
|
605
658
|
let input = args.first().unwrap_or(&Value::Null);
|
|
606
|
-
regexp_exec(&Value::RegExp(
|
|
607
|
-
})
|
|
659
|
+
regexp_exec(&Value::RegExp(re.clone()), input)
|
|
660
|
+
})
|
|
608
661
|
} else if key == "test" {
|
|
609
|
-
Value::
|
|
662
|
+
Value::native(move |args: &[Value]| {
|
|
610
663
|
let input = args.first().unwrap_or(&Value::Null);
|
|
611
|
-
regexp_test(&Value::RegExp(
|
|
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
|
-
|
|
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,
|
|
829
|
+
await_fetch as http_await_fetch, await_fetch_all as http_await_fetch_all,
|
|
830
|
+
register_static_route,
|
|
761
831
|
};
|
|
762
832
|
|
|
763
|
-
|
|
764
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
1055
|
+
Value::Array(VmRef::new(parts))
|
|
926
1056
|
}
|
|
927
|
-
_ => Value::Array(
|
|
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(
|
|
1093
|
+
Value::Array(VmRef::new(matches))
|
|
964
1094
|
}
|
|
965
1095
|
} else {
|
|
966
1096
|
regexp_exec_impl(&mut re, input)
|