@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,178 @@
|
|
|
1
|
+
//! Shared-mutable reference used by the Tish runtime for `Value::Array`,
|
|
2
|
+
//! `Value::Object`, and `Value::RegExp` payloads.
|
|
3
|
+
//!
|
|
4
|
+
//! ## Why this exists
|
|
5
|
+
//!
|
|
6
|
+
//! Tish's `Value` uses interior mutability for arrays, objects, and regex
|
|
7
|
+
//! state. Historically that was `Rc<RefCell<T>>`, which is fast but
|
|
8
|
+
//! `!Send` — so `Value` couldn't move across threads, which in turn meant
|
|
9
|
+
//! `serve(port, handler)` had to serialise every request through one
|
|
10
|
+
//! VM dispatcher thread.
|
|
11
|
+
//!
|
|
12
|
+
//! `VmRef<T>` lets the build system pick the right trade-off **per
|
|
13
|
+
//! compile target**:
|
|
14
|
+
//!
|
|
15
|
+
//! | feature `send-values` | `VmRef<T>` | `NativeFn` | targets |
|
|
16
|
+
//! |---------------------------|-------------------------|----------------------------------|--------------------------------------------------|
|
|
17
|
+
//! | **off** *(default)* | `Rc<RefCell<T>>` | `Rc<dyn Fn + 'static>` | wasm32, wasi, interpreter, cranelift/llvm VMs |
|
|
18
|
+
//! | **on** | `Arc<Mutex<T>>` | `Arc<dyn Fn + Send + Sync>` | Rust native with `http` enabled (server workloads) |
|
|
19
|
+
//!
|
|
20
|
+
//! The *API* is identical in both configurations (`borrow` / `borrow_mut`
|
|
21
|
+
//! / `ptr_eq` / `Clone`), so every existing call site in the workspace
|
|
22
|
+
//! compiles unchanged. What flips is only the underlying primitive.
|
|
23
|
+
//!
|
|
24
|
+
//! ## Why this matters for performance
|
|
25
|
+
//!
|
|
26
|
+
//! * **wasm / wasi / cranelift / llvm / interpreter**: still pure
|
|
27
|
+
//! `Rc<RefCell<T>>`. Zero atomic ops, no mutex churn, behaviour
|
|
28
|
+
//! bit-identical to the pre-migration baseline.
|
|
29
|
+
//! * **Rust native, non-server**: same — `send-values` only activates
|
|
30
|
+
//! when something in the dependency graph (usually `http`) needs it.
|
|
31
|
+
//! * **Rust native with server**: `Arc<Mutex<T>>` pays ~3–5 ns per
|
|
32
|
+
//! `borrow` in the uncontended case (single atomic CAS). On Tish's
|
|
33
|
+
//! hot paths — roughly 6–12 borrows per request — that's ~30–60 ns of
|
|
34
|
+
//! overhead. In exchange we get `N×` handler scaling across cores,
|
|
35
|
+
//! which recovers orders of magnitude more throughput than it costs.
|
|
36
|
+
//!
|
|
37
|
+
//! ## API surface
|
|
38
|
+
//!
|
|
39
|
+
//! ```ignore
|
|
40
|
+
//! let cell = VmRef::new(42);
|
|
41
|
+
//! *cell.borrow() + 1; // read
|
|
42
|
+
//! *cell.borrow_mut() = 99; // write
|
|
43
|
+
//! VmRef::ptr_eq(&a, &b); // identity
|
|
44
|
+
//! let clone = cell.clone(); // shared ownership
|
|
45
|
+
//! ```
|
|
46
|
+
//!
|
|
47
|
+
//! Returned guard types (`VmReadGuard<'_, T>`, `VmWriteGuard<'_, T>`) are
|
|
48
|
+
//! type aliases that pick `Ref`/`RefMut` or `MutexGuard` depending on the
|
|
49
|
+
//! feature. They both `Deref` (and, for write guards, `DerefMut`) to `T`
|
|
50
|
+
//! just like the underlying types.
|
|
51
|
+
|
|
52
|
+
use std::fmt;
|
|
53
|
+
|
|
54
|
+
// --------------------------------------------------------------------------
|
|
55
|
+
// Single-threaded backing store (default): Rc<RefCell<T>>
|
|
56
|
+
// --------------------------------------------------------------------------
|
|
57
|
+
#[cfg(not(feature = "send-values"))]
|
|
58
|
+
mod imp {
|
|
59
|
+
use std::cell::RefCell;
|
|
60
|
+
use std::rc::Rc;
|
|
61
|
+
|
|
62
|
+
#[derive(Default)]
|
|
63
|
+
pub struct VmRef<T: ?Sized>(pub(super) Rc<RefCell<T>>);
|
|
64
|
+
|
|
65
|
+
/// Read guard alias. On the single-threaded path this is a true
|
|
66
|
+
/// `Ref<'_, T>`, so multiple readers can coexist.
|
|
67
|
+
pub type ReadGuard<'a, T> = std::cell::Ref<'a, T>;
|
|
68
|
+
/// Write guard alias. Exclusive, `DerefMut`.
|
|
69
|
+
pub type WriteGuard<'a, T> = std::cell::RefMut<'a, T>;
|
|
70
|
+
|
|
71
|
+
impl<T> VmRef<T> {
|
|
72
|
+
#[inline]
|
|
73
|
+
pub fn new(value: T) -> Self {
|
|
74
|
+
VmRef(Rc::new(RefCell::new(value)))
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
impl<T: ?Sized> VmRef<T> {
|
|
79
|
+
#[inline]
|
|
80
|
+
pub fn borrow(&self) -> ReadGuard<'_, T> {
|
|
81
|
+
self.0.borrow()
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[inline]
|
|
85
|
+
pub fn borrow_mut(&self) -> WriteGuard<'_, T> {
|
|
86
|
+
self.0.borrow_mut()
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
#[inline]
|
|
90
|
+
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
|
|
91
|
+
Rc::ptr_eq(&a.0, &b.0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
#[inline]
|
|
95
|
+
pub fn strong_count(this: &Self) -> usize {
|
|
96
|
+
Rc::strong_count(&this.0)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
impl<T: ?Sized> Clone for VmRef<T> {
|
|
101
|
+
#[inline]
|
|
102
|
+
fn clone(&self) -> Self {
|
|
103
|
+
VmRef(Rc::clone(&self.0))
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// --------------------------------------------------------------------------
|
|
109
|
+
// Thread-safe backing store (opt-in): Arc<Mutex<T>>
|
|
110
|
+
// --------------------------------------------------------------------------
|
|
111
|
+
#[cfg(feature = "send-values")]
|
|
112
|
+
mod imp {
|
|
113
|
+
use std::sync::{Arc, Mutex};
|
|
114
|
+
|
|
115
|
+
#[derive(Default)]
|
|
116
|
+
pub struct VmRef<T: ?Sized>(pub(super) Arc<Mutex<T>>);
|
|
117
|
+
|
|
118
|
+
/// Read guard alias. On the multi-threaded path both readers and
|
|
119
|
+
/// writers share a single `MutexGuard` (exclusive access).
|
|
120
|
+
pub type ReadGuard<'a, T> = std::sync::MutexGuard<'a, T>;
|
|
121
|
+
/// Write guard alias.
|
|
122
|
+
pub type WriteGuard<'a, T> = std::sync::MutexGuard<'a, T>;
|
|
123
|
+
|
|
124
|
+
impl<T> VmRef<T> {
|
|
125
|
+
#[inline]
|
|
126
|
+
pub fn new(value: T) -> Self {
|
|
127
|
+
VmRef(Arc::new(Mutex::new(value)))
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
impl<T: ?Sized> VmRef<T> {
|
|
132
|
+
/// Acquire the inner mutex. Poisoning is swallowed — a Tish
|
|
133
|
+
/// handler panic already aborts the enclosing thread; there is
|
|
134
|
+
/// no invariant worth preserving past that point.
|
|
135
|
+
#[inline]
|
|
136
|
+
pub fn borrow(&self) -> ReadGuard<'_, T> {
|
|
137
|
+
self.0.lock().unwrap_or_else(|p| p.into_inner())
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
#[inline]
|
|
141
|
+
pub fn borrow_mut(&self) -> WriteGuard<'_, T> {
|
|
142
|
+
self.0.lock().unwrap_or_else(|p| p.into_inner())
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[inline]
|
|
146
|
+
pub fn ptr_eq(a: &Self, b: &Self) -> bool {
|
|
147
|
+
Arc::ptr_eq(&a.0, &b.0)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
#[inline]
|
|
151
|
+
pub fn strong_count(this: &Self) -> usize {
|
|
152
|
+
Arc::strong_count(&this.0)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
impl<T: ?Sized> Clone for VmRef<T> {
|
|
157
|
+
#[inline]
|
|
158
|
+
fn clone(&self) -> Self {
|
|
159
|
+
VmRef(Arc::clone(&self.0))
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
pub use imp::{ReadGuard as VmReadGuard, VmRef, WriteGuard as VmWriteGuard};
|
|
165
|
+
|
|
166
|
+
impl<T: fmt::Debug> fmt::Debug for VmRef<T> {
|
|
167
|
+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
168
|
+
// Match `RefCell`'s debug format so snapshot-test output stays
|
|
169
|
+
// stable across the migration.
|
|
170
|
+
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
|
|
171
|
+
let guard = self.borrow();
|
|
172
|
+
format!("{:?}", &*guard)
|
|
173
|
+
})) {
|
|
174
|
+
Ok(s) => write!(f, "RefCell {{ value: {} }}", s),
|
|
175
|
+
Err(_) => write!(f, "RefCell {{ value: <borrowed> }}"),
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -8,7 +8,22 @@ license-file = { workspace = true }
|
|
|
8
8
|
repository = { workspace = true }
|
|
9
9
|
[features]
|
|
10
10
|
default = []
|
|
11
|
-
|
|
11
|
+
# setTimeout / setInterval / clear* (standalone or with `import { … } from "tish:timers"`).
|
|
12
|
+
timers = []
|
|
13
|
+
http = [
|
|
14
|
+
"timers",
|
|
15
|
+
"tokio",
|
|
16
|
+
"reqwest",
|
|
17
|
+
"futures",
|
|
18
|
+
"tiny_http",
|
|
19
|
+
"tishlang_core/regex",
|
|
20
|
+
"dep:tishlang_runtime",
|
|
21
|
+
"tishlang_runtime/http",
|
|
22
|
+
# Interpreter + http means the runtime's `NativeFn` is `Arc<... + Send>`,
|
|
23
|
+
# so the interpreter's `CoreFn` variant must use the same shape.
|
|
24
|
+
"tishlang_core/send-values",
|
|
25
|
+
"tishlang_builtins/send-values",
|
|
26
|
+
]
|
|
12
27
|
fs = []
|
|
13
28
|
process = []
|
|
14
29
|
regex = ["dep:fancy-regex", "tishlang_core/regex"]
|
|
@@ -17,7 +32,7 @@ ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
|
|
|
17
32
|
|
|
18
33
|
[dependencies]
|
|
19
34
|
ahash = "0.8.12"
|
|
20
|
-
rand = "0.10.
|
|
35
|
+
rand = "0.10.1"
|
|
21
36
|
tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
|
|
22
37
|
tishlang_builtins = { path = "../tish_builtins", version = ">=0.1" }
|
|
23
38
|
tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
|
|
@@ -109,6 +109,7 @@ impl Evaluator {
|
|
|
109
109
|
);
|
|
110
110
|
s.set("decodeURI".into(), Value::Native(natives::decode_uri), true);
|
|
111
111
|
s.set("encodeURI".into(), Value::Native(natives::encode_uri), true);
|
|
112
|
+
s.set("htmlEscape".into(), Value::Native(natives::html_escape), true);
|
|
112
113
|
s.set(
|
|
113
114
|
"Boolean".into(),
|
|
114
115
|
Value::Native(natives::boolean_native),
|
|
@@ -221,7 +222,37 @@ impl Evaluator {
|
|
|
221
222
|
);
|
|
222
223
|
}
|
|
223
224
|
|
|
224
|
-
// fs,
|
|
225
|
+
// fs, process: prefer `import { x } from 'tish:fs'` etc.
|
|
226
|
+
#[cfg(feature = "timers")]
|
|
227
|
+
{
|
|
228
|
+
s.set(
|
|
229
|
+
"setTimeout".into(),
|
|
230
|
+
Value::TimerBuiltin(Arc::from("setTimeout")),
|
|
231
|
+
true,
|
|
232
|
+
);
|
|
233
|
+
s.set(
|
|
234
|
+
"setInterval".into(),
|
|
235
|
+
Value::TimerBuiltin(Arc::from("setInterval")),
|
|
236
|
+
true,
|
|
237
|
+
);
|
|
238
|
+
s.set(
|
|
239
|
+
"clearTimeout".into(),
|
|
240
|
+
Value::Native(Self::clear_timeout_native),
|
|
241
|
+
true,
|
|
242
|
+
);
|
|
243
|
+
s.set(
|
|
244
|
+
"clearInterval".into(),
|
|
245
|
+
Value::Native(Self::clear_interval_native),
|
|
246
|
+
true,
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
#[cfg(feature = "http")]
|
|
250
|
+
{
|
|
251
|
+
s.set("fetch".into(), Value::Native(Self::fetch_native), true);
|
|
252
|
+
s.set("fetchAll".into(), Value::Native(Self::fetch_all_native), true);
|
|
253
|
+
s.set("Promise".into(), Value::PromiseConstructor, true);
|
|
254
|
+
s.set("serve".into(), Value::Serve, true);
|
|
255
|
+
}
|
|
225
256
|
}
|
|
226
257
|
Self {
|
|
227
258
|
scope,
|
|
@@ -556,21 +587,21 @@ impl Evaluator {
|
|
|
556
587
|
let mut scope = self.scope.borrow_mut();
|
|
557
588
|
for spec in specifiers {
|
|
558
589
|
match spec {
|
|
559
|
-
ImportSpecifier::Named { name, alias } => {
|
|
590
|
+
ImportSpecifier::Named { name, alias, .. } => {
|
|
560
591
|
let v = exports.get(name.as_ref()).ok_or_else(|| {
|
|
561
592
|
EvalError::Error(format!("Module does not export '{}'", name))
|
|
562
593
|
})?;
|
|
563
594
|
let bind = alias.as_deref().unwrap_or(name.as_ref());
|
|
564
595
|
scope.set(Arc::from(bind), v.clone(), false);
|
|
565
596
|
}
|
|
566
|
-
ImportSpecifier::Namespace
|
|
567
|
-
scope.set(Arc::clone(
|
|
597
|
+
ImportSpecifier::Namespace { name, .. } => {
|
|
598
|
+
scope.set(Arc::clone(name), exports_val.clone(), false);
|
|
568
599
|
}
|
|
569
|
-
ImportSpecifier::Default
|
|
600
|
+
ImportSpecifier::Default { name, .. } => {
|
|
570
601
|
let v = exports.get("default").ok_or_else(|| {
|
|
571
602
|
EvalError::Error("Module does not have default export".to_string())
|
|
572
603
|
})?;
|
|
573
|
-
scope.set(Arc::clone(
|
|
604
|
+
scope.set(Arc::clone(name), v.clone(), false);
|
|
574
605
|
}
|
|
575
606
|
}
|
|
576
607
|
}
|
|
@@ -588,6 +619,9 @@ impl Evaluator {
|
|
|
588
619
|
}
|
|
589
620
|
Ok(Value::Null)
|
|
590
621
|
}
|
|
622
|
+
Statement::TypeAlias { .. } | Statement::DeclareVar { .. } | Statement::DeclareFun { .. } => {
|
|
623
|
+
Ok(Value::Null)
|
|
624
|
+
}
|
|
591
625
|
}
|
|
592
626
|
}
|
|
593
627
|
|
|
@@ -726,6 +760,19 @@ impl Evaluator {
|
|
|
726
760
|
exports.insert("fetchAll".into(), Value::Native(Self::fetch_all_native));
|
|
727
761
|
exports.insert("serve".into(), Value::Serve);
|
|
728
762
|
exports.insert("Promise".into(), Value::PromiseConstructor);
|
|
763
|
+
return Ok(Value::Object(Rc::new(RefCell::new(exports))));
|
|
764
|
+
}
|
|
765
|
+
#[cfg(not(feature = "http"))]
|
|
766
|
+
{
|
|
767
|
+
return Err(EvalError::Error(
|
|
768
|
+
"tish:http requires the http feature. Rebuild with: cargo build -p tishlang --features http".into(),
|
|
769
|
+
));
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
"tish:timers" => {
|
|
773
|
+
#[cfg(feature = "timers")]
|
|
774
|
+
{
|
|
775
|
+
let mut exports: PropMap = PropMap::default();
|
|
729
776
|
exports.insert(
|
|
730
777
|
"setTimeout".into(),
|
|
731
778
|
Value::TimerBuiltin(Arc::from("setTimeout")),
|
|
@@ -744,10 +791,10 @@ impl Evaluator {
|
|
|
744
791
|
);
|
|
745
792
|
return Ok(Value::Object(Rc::new(RefCell::new(exports))));
|
|
746
793
|
}
|
|
747
|
-
#[cfg(not(feature = "
|
|
794
|
+
#[cfg(not(feature = "timers"))]
|
|
748
795
|
{
|
|
749
796
|
return Err(EvalError::Error(
|
|
750
|
-
"tish:
|
|
797
|
+
"tish:timers requires the timers feature. Rebuild with: cargo build -p tishlang --features timers".into(),
|
|
751
798
|
));
|
|
752
799
|
}
|
|
753
800
|
}
|
|
@@ -815,7 +862,7 @@ impl Evaluator {
|
|
|
815
862
|
}
|
|
816
863
|
_ => {
|
|
817
864
|
return Err(EvalError::Error(format!(
|
|
818
|
-
"Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:process, tish:ws (plus any registered by native modules)",
|
|
865
|
+
"Unknown built-in module: {}. Supported: tish:fs, tish:http, tish:timers, tish:process, tish:ws (plus any registered by native modules)",
|
|
819
866
|
spec
|
|
820
867
|
)));
|
|
821
868
|
}
|
|
@@ -862,7 +909,12 @@ impl Evaluator {
|
|
|
862
909
|
}
|
|
863
910
|
Expr::Call { callee, args, .. } => {
|
|
864
911
|
// Check for built-in method calls on arrays/strings
|
|
865
|
-
if let Expr::Member {
|
|
912
|
+
if let Expr::Member {
|
|
913
|
+
object,
|
|
914
|
+
prop: MemberProp::Name { name: method_name, .. },
|
|
915
|
+
..
|
|
916
|
+
} = callee.as_ref()
|
|
917
|
+
{
|
|
866
918
|
let obj = self.eval_expr(object)?;
|
|
867
919
|
let arg_vals = self.eval_call_args(args)?;
|
|
868
920
|
|
|
@@ -1629,7 +1681,7 @@ impl Evaluator {
|
|
|
1629
1681
|
return Ok(Value::Null);
|
|
1630
1682
|
}
|
|
1631
1683
|
let key = match prop {
|
|
1632
|
-
MemberProp::Name
|
|
1684
|
+
MemberProp::Name { name, .. } => Arc::clone(name),
|
|
1633
1685
|
MemberProp::Expr(e) => {
|
|
1634
1686
|
let v = self.eval_expr(e)?;
|
|
1635
1687
|
match v {
|
|
@@ -1750,7 +1802,9 @@ impl Evaluator {
|
|
|
1750
1802
|
Value::Serve
|
|
1751
1803
|
| Value::PromiseResolver(_)
|
|
1752
1804
|
| Value::PromiseConstructor
|
|
1753
|
-
| Value::BoundPromiseMethod(_, _)
|
|
1805
|
+
| Value::BoundPromiseMethod(_, _) => "function".into(),
|
|
1806
|
+
#[cfg(feature = "timers")]
|
|
1807
|
+
Value::TimerBuiltin(_) => "function".into(),
|
|
1754
1808
|
#[cfg(feature = "http")]
|
|
1755
1809
|
Value::Promise(_) => "object".into(),
|
|
1756
1810
|
#[cfg(feature = "regex")]
|
|
@@ -2291,7 +2345,7 @@ impl Evaluator {
|
|
|
2291
2345
|
_optional: bool,
|
|
2292
2346
|
) -> Option<Result<Value, EvalError>> {
|
|
2293
2347
|
match property {
|
|
2294
|
-
MemberProp::Name
|
|
2348
|
+
MemberProp::Name { name, .. } => match obj {
|
|
2295
2349
|
Value::Object(o) => {
|
|
2296
2350
|
let result = o
|
|
2297
2351
|
.borrow()
|
|
@@ -2323,8 +2377,9 @@ impl Evaluator {
|
|
|
2323
2377
|
#[cfg(feature = "http")]
|
|
2324
2378
|
Value::PromiseConstructor
|
|
2325
2379
|
| Value::Serve
|
|
2326
|
-
| Value::BoundPromiseMethod(_, _)
|
|
2327
|
-
|
|
2380
|
+
| Value::BoundPromiseMethod(_, _) => self.call_func(callee, args),
|
|
2381
|
+
#[cfg(feature = "timers")]
|
|
2382
|
+
Value::TimerBuiltin(_) => self.call_func(callee, args),
|
|
2328
2383
|
Value::OpaqueMethod(_, _) => self.call_func(callee, args),
|
|
2329
2384
|
_ => Ok(Value::Null),
|
|
2330
2385
|
}
|
|
@@ -2411,7 +2466,7 @@ impl Evaluator {
|
|
|
2411
2466
|
Value::BoundPromiseMethod(promise_ref, method) => {
|
|
2412
2467
|
self.run_promise_method(promise_ref, method.as_ref(), args)
|
|
2413
2468
|
}
|
|
2414
|
-
#[cfg(feature = "
|
|
2469
|
+
#[cfg(feature = "timers")]
|
|
2415
2470
|
Value::TimerBuiltin(name) => self.run_timer_builtin(name.as_ref(), args),
|
|
2416
2471
|
Value::OpaqueMethod(opaque, method_name) => {
|
|
2417
2472
|
let method = opaque.get_method(method_name.as_ref()).ok_or_else(|| {
|
|
@@ -2629,7 +2684,7 @@ impl Evaluator {
|
|
|
2629
2684
|
Ok(promise)
|
|
2630
2685
|
}
|
|
2631
2686
|
|
|
2632
|
-
#[cfg(feature = "
|
|
2687
|
+
#[cfg(feature = "timers")]
|
|
2633
2688
|
fn run_timer_builtin(&self, name: &str, args: &[Value]) -> Result<Value, EvalError> {
|
|
2634
2689
|
let callback = args
|
|
2635
2690
|
.first()
|
|
@@ -2650,7 +2705,7 @@ impl Evaluator {
|
|
|
2650
2705
|
Ok(Value::Number(id as f64))
|
|
2651
2706
|
}
|
|
2652
2707
|
|
|
2653
|
-
#[cfg(feature = "
|
|
2708
|
+
#[cfg(feature = "timers")]
|
|
2654
2709
|
fn clear_timeout_native(args: &[Value]) -> Result<Value, String> {
|
|
2655
2710
|
if let Some(Value::Number(n)) = args.first() {
|
|
2656
2711
|
crate::timers::clearTimer(*n as u64);
|
|
@@ -2658,7 +2713,7 @@ impl Evaluator {
|
|
|
2658
2713
|
Ok(Value::Null)
|
|
2659
2714
|
}
|
|
2660
2715
|
|
|
2661
|
-
#[cfg(feature = "
|
|
2716
|
+
#[cfg(feature = "timers")]
|
|
2662
2717
|
fn clear_interval_native(args: &[Value]) -> Result<Value, String> {
|
|
2663
2718
|
if let Some(Value::Number(n)) = args.first() {
|
|
2664
2719
|
crate::timers::clearTimer(*n as u64);
|
|
@@ -2668,7 +2723,7 @@ impl Evaluator {
|
|
|
2668
2723
|
|
|
2669
2724
|
/// Run all due timer callbacks. Called after the script completes so setTimeout/setInterval
|
|
2670
2725
|
/// callbacks run without blocking the main script. Loops until no timers are due.
|
|
2671
|
-
#[cfg(feature = "
|
|
2726
|
+
#[cfg(feature = "timers")]
|
|
2672
2727
|
pub fn run_timer_phase(&mut self) -> Result<(), String> {
|
|
2673
2728
|
const MAX_ITERATIONS: u32 = 1_000_000; // avoid infinite loop if setInterval never cleared
|
|
2674
2729
|
let mut iterations = 0;
|
|
@@ -2760,8 +2815,14 @@ impl Evaluator {
|
|
|
2760
2815
|
}
|
|
2761
2816
|
};
|
|
2762
2817
|
|
|
2763
|
-
let (status, headers,
|
|
2764
|
-
|
|
2818
|
+
if let Some((status, headers, file_path)) =
|
|
2819
|
+
crate::http::extract_file_from_response(&response_value)
|
|
2820
|
+
{
|
|
2821
|
+
crate::http::send_file_response(request, status, headers, file_path);
|
|
2822
|
+
} else {
|
|
2823
|
+
let (status, headers, body) = crate::http::value_to_response(&response_value);
|
|
2824
|
+
crate::http::send_response(request, status, headers, body);
|
|
2825
|
+
}
|
|
2765
2826
|
count += 1;
|
|
2766
2827
|
if max_requests.map(|m| count >= m).unwrap_or(false) {
|
|
2767
2828
|
break;
|
|
@@ -2809,7 +2870,7 @@ impl Evaluator {
|
|
|
2809
2870
|
for (i, elem) in elements.iter().enumerate() {
|
|
2810
2871
|
if let Some(el) = elem {
|
|
2811
2872
|
match el {
|
|
2812
|
-
tishlang_ast::DestructElement::Ident(name) => {
|
|
2873
|
+
tishlang_ast::DestructElement::Ident(name, _) => {
|
|
2813
2874
|
let val = arr.get(i).cloned().unwrap_or(Value::Null);
|
|
2814
2875
|
scope.borrow_mut().set(Arc::clone(name), val, mutable);
|
|
2815
2876
|
}
|
|
@@ -2817,7 +2878,7 @@ impl Evaluator {
|
|
|
2817
2878
|
let val = arr.get(i).cloned().unwrap_or(Value::Null);
|
|
2818
2879
|
Self::bind_destruct_pattern_scoped(scope, nested, &val, mutable)?;
|
|
2819
2880
|
}
|
|
2820
|
-
tishlang_ast::DestructElement::Rest(name) => {
|
|
2881
|
+
tishlang_ast::DestructElement::Rest(name, _) => {
|
|
2821
2882
|
let rest: Vec<Value> = arr.iter().skip(i).cloned().collect();
|
|
2822
2883
|
scope.borrow_mut().set(
|
|
2823
2884
|
Arc::clone(name),
|
|
@@ -2843,13 +2904,13 @@ impl Evaluator {
|
|
|
2843
2904
|
for prop in props {
|
|
2844
2905
|
let val = obj.get(&prop.key).cloned().unwrap_or(Value::Null);
|
|
2845
2906
|
match &prop.value {
|
|
2846
|
-
tishlang_ast::DestructElement::Ident(name) => {
|
|
2907
|
+
tishlang_ast::DestructElement::Ident(name, _) => {
|
|
2847
2908
|
scope.borrow_mut().set(Arc::clone(name), val, mutable);
|
|
2848
2909
|
}
|
|
2849
2910
|
tishlang_ast::DestructElement::Pattern(nested) => {
|
|
2850
2911
|
Self::bind_destruct_pattern_scoped(scope, nested, &val, mutable)?;
|
|
2851
2912
|
}
|
|
2852
|
-
tishlang_ast::DestructElement::Rest(_) => {
|
|
2913
|
+
tishlang_ast::DestructElement::Rest(_, _) => {
|
|
2853
2914
|
return Err(EvalError::Error(
|
|
2854
2915
|
"Rest not supported in object destructuring".to_string(),
|
|
2855
2916
|
));
|
|
@@ -3240,8 +3301,9 @@ impl Evaluator {
|
|
|
3240
3301
|
| Value::Promise(_)
|
|
3241
3302
|
| Value::PromiseResolver(_)
|
|
3242
3303
|
| Value::PromiseConstructor
|
|
3243
|
-
| Value::BoundPromiseMethod(_, _)
|
|
3244
|
-
|
|
3304
|
+
| Value::BoundPromiseMethod(_, _) => "null".to_string(),
|
|
3305
|
+
#[cfg(feature = "timers")]
|
|
3306
|
+
Value::TimerBuiltin(_) => "null".to_string(),
|
|
3245
3307
|
#[cfg(feature = "regex")]
|
|
3246
3308
|
Value::RegExp(_) => "null".to_string(),
|
|
3247
3309
|
Value::Opaque(_) | Value::OpaqueMethod(_, _) => "null".to_string(),
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
//! HTTP server for the Tish interpreter. Client `fetch` uses `tishlang_runtime` from eval.
|
|
2
2
|
|
|
3
3
|
use crate::value::{PropMap, Value};
|
|
4
|
+
use std::fs::File;
|
|
4
5
|
use std::sync::Arc;
|
|
5
6
|
|
|
6
7
|
use tokio::runtime::Runtime;
|
|
@@ -101,6 +102,66 @@ pub fn value_to_response(value: &Value) -> (u16, Vec<(String, String)>, String)
|
|
|
101
102
|
(status, headers, body)
|
|
102
103
|
}
|
|
103
104
|
|
|
105
|
+
/// If the response value has a `file` key, stream that path (binary-safe). Matches `tishlang_runtime` HTTP behavior.
|
|
106
|
+
pub(crate) fn extract_file_from_response(value: &Value) -> Option<(u16, Vec<(String, String)>, String)> {
|
|
107
|
+
let Value::Object(obj) = value else {
|
|
108
|
+
return None;
|
|
109
|
+
};
|
|
110
|
+
let obj_ref = obj.borrow();
|
|
111
|
+
let file_val = obj_ref.get(&Arc::from("file"))?;
|
|
112
|
+
let Value::String(file_path) = file_val else {
|
|
113
|
+
return None;
|
|
114
|
+
};
|
|
115
|
+
let file_path = file_path.to_string();
|
|
116
|
+
let status = obj_ref
|
|
117
|
+
.get(&Arc::from("status"))
|
|
118
|
+
.and_then(|v| match v {
|
|
119
|
+
Value::Number(n) => Some(*n as u16),
|
|
120
|
+
_ => None,
|
|
121
|
+
})
|
|
122
|
+
.unwrap_or(200);
|
|
123
|
+
let headers = obj_ref
|
|
124
|
+
.get(&Arc::from("headers"))
|
|
125
|
+
.and_then(|v| match v {
|
|
126
|
+
Value::Object(h) => Some(
|
|
127
|
+
h.borrow()
|
|
128
|
+
.iter()
|
|
129
|
+
.map(|(k, v)| (k.to_string(), v.to_string()))
|
|
130
|
+
.collect(),
|
|
131
|
+
),
|
|
132
|
+
_ => None,
|
|
133
|
+
})
|
|
134
|
+
.unwrap_or_default();
|
|
135
|
+
Some((status, headers, file_path))
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
pub(crate) fn send_file_response(
|
|
139
|
+
request: tiny_http::Request,
|
|
140
|
+
status: u16,
|
|
141
|
+
headers: Vec<(String, String)>,
|
|
142
|
+
file_path: String,
|
|
143
|
+
) {
|
|
144
|
+
let file = match File::open(&file_path) {
|
|
145
|
+
Ok(f) => f,
|
|
146
|
+
Err(e) => {
|
|
147
|
+
eprintln!("Failed to open file {}: {}", file_path, e);
|
|
148
|
+
let fallback =
|
|
149
|
+
tiny_http::Response::from_string(format!("File not found: {}", file_path))
|
|
150
|
+
.with_status_code(tiny_http::StatusCode(500));
|
|
151
|
+
let _ = request.respond(fallback);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
let status_code = tiny_http::StatusCode(status);
|
|
156
|
+
let mut response = tiny_http::Response::from_file(file).with_status_code(status_code);
|
|
157
|
+
for (key, value) in headers {
|
|
158
|
+
if let Ok(header) = tiny_http::Header::from_bytes(key.as_bytes(), value.as_bytes()) {
|
|
159
|
+
response = response.with_header(header);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
let _ = request.respond(response);
|
|
163
|
+
}
|
|
164
|
+
|
|
104
165
|
/// Send a response using tiny_http.
|
|
105
166
|
pub fn send_response(
|
|
106
167
|
request: tiny_http::Request,
|
|
@@ -8,7 +8,7 @@ mod natives;
|
|
|
8
8
|
mod promise;
|
|
9
9
|
#[cfg(feature = "regex")]
|
|
10
10
|
pub mod regex;
|
|
11
|
-
#[cfg(feature = "
|
|
11
|
+
#[cfg(feature = "timers")]
|
|
12
12
|
mod timers;
|
|
13
13
|
mod value;
|
|
14
14
|
pub mod value_convert;
|
|
@@ -36,7 +36,7 @@ pub fn run(source: &str) -> Result<Value, String> {
|
|
|
36
36
|
let program = tishlang_parser::parse(source)?;
|
|
37
37
|
let mut eval = Evaluator::new();
|
|
38
38
|
let result = eval.eval_program(&program)?;
|
|
39
|
-
#[cfg(feature = "
|
|
39
|
+
#[cfg(feature = "timers")]
|
|
40
40
|
eval.run_timer_phase()?;
|
|
41
41
|
Ok(result)
|
|
42
42
|
}
|
|
@@ -64,7 +64,7 @@ pub fn run_file(
|
|
|
64
64
|
let mut eval = Evaluator::new();
|
|
65
65
|
eval.set_current_dir(project_root.or(path.parent()));
|
|
66
66
|
let result = eval.eval_program(&program)?;
|
|
67
|
-
#[cfg(feature = "
|
|
67
|
+
#[cfg(feature = "timers")]
|
|
68
68
|
eval.run_timer_phase()?;
|
|
69
69
|
Ok(result)
|
|
70
70
|
}
|
|
@@ -115,6 +115,47 @@ pub fn encode_uri(args: &[Value]) -> Result<Value, String> {
|
|
|
115
115
|
Ok(Value::String(tishlang_core::percent_encode(&s).into()))
|
|
116
116
|
}
|
|
117
117
|
|
|
118
|
+
pub fn html_escape(args: &[Value]) -> Result<Value, String> {
|
|
119
|
+
let input = match args.first() {
|
|
120
|
+
Some(Value::String(s)) => s.to_string(),
|
|
121
|
+
Some(v) => v.to_string(),
|
|
122
|
+
None => return Ok(Value::Null),
|
|
123
|
+
};
|
|
124
|
+
let bytes = input.as_bytes();
|
|
125
|
+
let mut extra = 0usize;
|
|
126
|
+
for b in bytes {
|
|
127
|
+
match b {
|
|
128
|
+
b'&' => extra += 4,
|
|
129
|
+
b'<' | b'>' => extra += 3,
|
|
130
|
+
b'"' => extra += 5,
|
|
131
|
+
b'\'' => extra += 4,
|
|
132
|
+
_ => {}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if extra == 0 {
|
|
136
|
+
return Ok(Value::String(input.into()));
|
|
137
|
+
}
|
|
138
|
+
let mut out = String::with_capacity(input.len() + extra);
|
|
139
|
+
let mut last = 0usize;
|
|
140
|
+
for (i, b) in bytes.iter().enumerate() {
|
|
141
|
+
let repl: Option<&'static str> = match b {
|
|
142
|
+
b'&' => Some("&"),
|
|
143
|
+
b'<' => Some("<"),
|
|
144
|
+
b'>' => Some(">"),
|
|
145
|
+
b'"' => Some("""),
|
|
146
|
+
b'\'' => Some("'"),
|
|
147
|
+
_ => None,
|
|
148
|
+
};
|
|
149
|
+
if let Some(r) = repl {
|
|
150
|
+
out.push_str(&input[last..i]);
|
|
151
|
+
out.push_str(r);
|
|
152
|
+
last = i + 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
out.push_str(&input[last..]);
|
|
156
|
+
Ok(Value::String(out.into()))
|
|
157
|
+
}
|
|
158
|
+
|
|
118
159
|
pub fn math_abs(args: &[Value]) -> Result<Value, String> {
|
|
119
160
|
Ok(Value::Number(
|
|
120
161
|
get_num(args.first().unwrap_or(&Value::Null)).abs(),
|
|
@@ -61,7 +61,7 @@ pub enum Value {
|
|
|
61
61
|
#[cfg(feature = "http")]
|
|
62
62
|
BoundPromiseMethod(crate::promise::PromiseRef, std::sync::Arc<str>),
|
|
63
63
|
/// Timer builtins: setTimeout, setInterval. Need evaluator for callback.
|
|
64
|
-
#[cfg(feature = "
|
|
64
|
+
#[cfg(feature = "timers")]
|
|
65
65
|
TimerBuiltin(std::sync::Arc<str>),
|
|
66
66
|
/// Native `tishlang_core` Promise (fetch / reader.read / response.text).
|
|
67
67
|
#[cfg(feature = "http")]
|
|
@@ -101,7 +101,9 @@ impl std::fmt::Debug for Value {
|
|
|
101
101
|
#[cfg(feature = "http")]
|
|
102
102
|
Value::PromiseConstructor => write!(f, "[Function: Promise]"),
|
|
103
103
|
#[cfg(feature = "http")]
|
|
104
|
-
Value::BoundPromiseMethod(_, _)
|
|
104
|
+
Value::BoundPromiseMethod(_, _) => write!(f, "[Function]"),
|
|
105
|
+
#[cfg(feature = "timers")]
|
|
106
|
+
Value::TimerBuiltin(_) => write!(f, "[Function]"),
|
|
105
107
|
#[cfg(feature = "http")]
|
|
106
108
|
Value::CorePromise(_) => write!(f, "Promise"),
|
|
107
109
|
Value::CoreFn(_) => write!(f, "CoreFn"),
|
|
@@ -156,7 +158,9 @@ impl std::fmt::Display for Value {
|
|
|
156
158
|
#[cfg(feature = "http")]
|
|
157
159
|
Value::PromiseConstructor => write!(f, "function Promise() {{ [native code] }}"),
|
|
158
160
|
#[cfg(feature = "http")]
|
|
159
|
-
Value::BoundPromiseMethod(_, _)
|
|
161
|
+
Value::BoundPromiseMethod(_, _) => write!(f, "[Function]"),
|
|
162
|
+
#[cfg(feature = "timers")]
|
|
163
|
+
Value::TimerBuiltin(_) => write!(f, "[Function]"),
|
|
160
164
|
#[cfg(feature = "http")]
|
|
161
165
|
Value::CorePromise(_) => write!(f, "[Promise]"),
|
|
162
166
|
Value::CoreFn(_) => write!(f, "[Function]"),
|