@tishlang/tish 1.0.7 → 1.0.11
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 +43 -0
- package/LICENSE +13 -0
- package/README.md +66 -0
- package/crates/js_to_tish/Cargo.toml +9 -0
- package/crates/js_to_tish/README.md +18 -0
- package/crates/js_to_tish/src/error.rs +61 -0
- package/crates/js_to_tish/src/lib.rs +11 -0
- package/crates/js_to_tish/src/span_util.rs +35 -0
- package/crates/js_to_tish/src/transform/expr.rs +608 -0
- package/crates/js_to_tish/src/transform/stmt.rs +474 -0
- package/crates/js_to_tish/src/transform.rs +60 -0
- package/crates/tish/Cargo.toml +44 -0
- package/crates/tish/src/main.rs +585 -0
- package/crates/tish/src/repl_completion.rs +200 -0
- package/crates/tish/tests/integration_test.rs +726 -0
- package/crates/tish_ast/Cargo.toml +7 -0
- package/crates/tish_ast/src/ast.rs +494 -0
- package/crates/tish_ast/src/lib.rs +5 -0
- package/crates/tish_build_utils/Cargo.toml +5 -0
- package/crates/tish_build_utils/src/lib.rs +175 -0
- package/crates/tish_builtins/Cargo.toml +12 -0
- package/crates/tish_builtins/src/array.rs +410 -0
- package/crates/tish_builtins/src/globals.rs +197 -0
- package/crates/tish_builtins/src/helpers.rs +38 -0
- package/crates/tish_builtins/src/lib.rs +14 -0
- package/crates/tish_builtins/src/math.rs +80 -0
- package/crates/tish_builtins/src/object.rs +36 -0
- package/crates/tish_builtins/src/string.rs +253 -0
- package/crates/tish_bytecode/Cargo.toml +15 -0
- package/crates/tish_bytecode/src/chunk.rs +97 -0
- package/crates/tish_bytecode/src/compiler.rs +1361 -0
- package/crates/tish_bytecode/src/encoding.rs +100 -0
- package/crates/tish_bytecode/src/lib.rs +19 -0
- package/crates/tish_bytecode/src/opcode.rs +110 -0
- package/crates/tish_bytecode/src/peephole.rs +159 -0
- package/crates/tish_bytecode/src/serialize.rs +163 -0
- package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
- package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
- package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
- package/crates/tish_compile/Cargo.toml +21 -0
- package/crates/tish_compile/src/codegen.rs +3316 -0
- package/crates/tish_compile/src/lib.rs +71 -0
- package/crates/tish_compile/src/resolve.rs +631 -0
- package/crates/tish_compile/src/types.rs +304 -0
- package/crates/tish_compile_js/Cargo.toml +16 -0
- package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
- package/crates/tish_compile_js/src/codegen.rs +794 -0
- package/crates/tish_compile_js/src/error.rs +20 -0
- package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
- package/crates/tish_compile_js/src/lib.rs +27 -0
- package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
- package/crates/tish_compiler_wasm/Cargo.toml +19 -0
- package/crates/tish_compiler_wasm/src/lib.rs +55 -0
- package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
- package/crates/tish_core/Cargo.toml +11 -0
- package/crates/tish_core/src/console_style.rs +128 -0
- package/crates/tish_core/src/json.rs +327 -0
- package/crates/tish_core/src/lib.rs +15 -0
- package/crates/tish_core/src/macros.rs +37 -0
- package/crates/tish_core/src/uri.rs +115 -0
- package/crates/tish_core/src/value.rs +376 -0
- package/crates/tish_cranelift/Cargo.toml +17 -0
- package/crates/tish_cranelift/src/lib.rs +41 -0
- package/crates/tish_cranelift/src/link.rs +120 -0
- package/crates/tish_cranelift/src/lower.rs +77 -0
- package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
- package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
- package/crates/tish_eval/Cargo.toml +26 -0
- package/crates/tish_eval/src/eval.rs +3205 -0
- package/crates/tish_eval/src/http.rs +122 -0
- package/crates/tish_eval/src/lib.rs +59 -0
- package/crates/tish_eval/src/natives.rs +301 -0
- package/crates/tish_eval/src/promise.rs +173 -0
- package/crates/tish_eval/src/regex.rs +298 -0
- package/crates/tish_eval/src/timers.rs +111 -0
- package/crates/tish_eval/src/value.rs +224 -0
- package/crates/tish_eval/src/value_convert.rs +85 -0
- package/crates/tish_fmt/Cargo.toml +16 -0
- package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
- package/crates/tish_fmt/src/lib.rs +884 -0
- package/crates/tish_jsx_web/Cargo.toml +7 -0
- package/crates/tish_jsx_web/README.md +18 -0
- package/crates/tish_jsx_web/src/lib.rs +157 -0
- package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
- package/crates/tish_lexer/Cargo.toml +7 -0
- package/crates/tish_lexer/src/lib.rs +430 -0
- package/crates/tish_lexer/src/token.rs +155 -0
- package/crates/tish_lint/Cargo.toml +17 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
- package/crates/tish_lint/src/lib.rs +278 -0
- package/crates/tish_llvm/Cargo.toml +11 -0
- package/crates/tish_llvm/src/lib.rs +106 -0
- package/crates/tish_lsp/Cargo.toml +22 -0
- package/crates/tish_lsp/README.md +26 -0
- package/crates/tish_lsp/src/main.rs +615 -0
- package/crates/tish_native/Cargo.toml +14 -0
- package/crates/tish_native/src/build.rs +102 -0
- package/crates/tish_native/src/lib.rs +237 -0
- package/crates/tish_opt/Cargo.toml +11 -0
- package/crates/tish_opt/src/lib.rs +896 -0
- package/crates/tish_parser/Cargo.toml +9 -0
- package/crates/tish_parser/src/lib.rs +123 -0
- package/crates/tish_parser/src/parser.rs +1714 -0
- package/crates/tish_runtime/Cargo.toml +26 -0
- package/crates/tish_runtime/src/http.rs +308 -0
- package/crates/tish_runtime/src/http_fetch.rs +453 -0
- package/crates/tish_runtime/src/lib.rs +1004 -0
- package/crates/tish_runtime/src/native_promise.rs +26 -0
- package/crates/tish_runtime/src/promise.rs +77 -0
- package/crates/tish_runtime/src/promise_io.rs +41 -0
- package/crates/tish_runtime/src/timers.rs +125 -0
- package/crates/tish_runtime/src/ws.rs +725 -0
- package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
- package/crates/tish_vm/Cargo.toml +31 -0
- package/crates/tish_vm/src/lib.rs +39 -0
- package/crates/tish_vm/src/vm.rs +1399 -0
- package/crates/tish_wasm/Cargo.toml +13 -0
- package/crates/tish_wasm/src/lib.rs +358 -0
- package/crates/tish_wasm_runtime/Cargo.toml +25 -0
- package/crates/tish_wasm_runtime/src/lib.rs +36 -0
- package/justfile +260 -0
- package/package.json +8 -3
- 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,298 @@
|
|
|
1
|
+
//! JavaScript-compatible regular expression support for Tish.
|
|
2
|
+
//!
|
|
3
|
+
//! Re-exports core types from tish_core and provides interpreter-specific functionality.
|
|
4
|
+
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::rc::Rc;
|
|
8
|
+
use std::sync::Arc;
|
|
9
|
+
|
|
10
|
+
pub use tish_core::{RegExpFlags, TishRegExp};
|
|
11
|
+
|
|
12
|
+
use crate::value::Value;
|
|
13
|
+
|
|
14
|
+
/// RegExp.prototype.exec(string) - returns match object (array-like with index) or null
|
|
15
|
+
pub fn regexp_exec(re: &mut TishRegExp, input: &str) -> Value {
|
|
16
|
+
let start = if re.flags.global || re.flags.sticky {
|
|
17
|
+
re.last_index
|
|
18
|
+
} else {
|
|
19
|
+
0
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
let char_count = input.chars().count();
|
|
23
|
+
if start > char_count {
|
|
24
|
+
if re.flags.global || re.flags.sticky {
|
|
25
|
+
re.last_index = 0;
|
|
26
|
+
}
|
|
27
|
+
return Value::Null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
let byte_start: usize = input.chars().take(start).map(|c| c.len_utf8()).sum();
|
|
31
|
+
let search_str = &input[byte_start..];
|
|
32
|
+
|
|
33
|
+
match re.regex.captures(search_str) {
|
|
34
|
+
Ok(Some(caps)) => {
|
|
35
|
+
let full_match = caps.get(0).unwrap();
|
|
36
|
+
|
|
37
|
+
if re.flags.sticky && full_match.start() != 0 {
|
|
38
|
+
re.last_index = 0;
|
|
39
|
+
return Value::Null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let match_byte_start = byte_start + full_match.start();
|
|
43
|
+
let match_char_index = input[..match_byte_start].chars().count();
|
|
44
|
+
|
|
45
|
+
let mut obj: HashMap<Arc<str>, Value> = HashMap::new();
|
|
46
|
+
obj.insert(Arc::from("0"), Value::String(full_match.as_str().into()));
|
|
47
|
+
for i in 1..caps.len() {
|
|
48
|
+
let val = match caps.get(i) {
|
|
49
|
+
Some(m) => Value::String(m.as_str().into()),
|
|
50
|
+
None => Value::Null,
|
|
51
|
+
};
|
|
52
|
+
obj.insert(Arc::from(i.to_string().as_str()), val);
|
|
53
|
+
}
|
|
54
|
+
obj.insert(Arc::from("index"), Value::Number(match_char_index as f64));
|
|
55
|
+
|
|
56
|
+
if re.flags.global || re.flags.sticky {
|
|
57
|
+
let match_end_chars = input[..byte_start + full_match.end()].chars().count();
|
|
58
|
+
re.last_index = if full_match.start() == full_match.end() {
|
|
59
|
+
match_end_chars + 1
|
|
60
|
+
} else {
|
|
61
|
+
match_end_chars
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
Value::Object(Rc::new(RefCell::new(obj)))
|
|
66
|
+
}
|
|
67
|
+
Ok(None) | Err(_) => {
|
|
68
|
+
if re.flags.global || re.flags.sticky {
|
|
69
|
+
re.last_index = 0;
|
|
70
|
+
}
|
|
71
|
+
Value::Null
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Create a RegExp Value from pattern and flags
|
|
77
|
+
pub fn create_regexp(pattern: &str, flags: &str) -> Result<Value, String> {
|
|
78
|
+
let re = TishRegExp::new(pattern, flags)?;
|
|
79
|
+
Ok(Value::RegExp(Rc::new(RefCell::new(re))))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// RegExp constructor function - handles `new RegExp(pattern, flags)` or `RegExp(pattern, flags)`
|
|
83
|
+
pub fn regexp_constructor(args: &[Value]) -> Result<Value, String> {
|
|
84
|
+
let pattern = match args.first() {
|
|
85
|
+
Some(Value::String(s)) => s.to_string(),
|
|
86
|
+
Some(Value::RegExp(re)) => {
|
|
87
|
+
if args.get(1).is_none() {
|
|
88
|
+
let re = re.borrow();
|
|
89
|
+
return create_regexp(&re.source, &re.flags_string());
|
|
90
|
+
}
|
|
91
|
+
re.borrow().source.clone()
|
|
92
|
+
}
|
|
93
|
+
Some(v) => v.to_string(),
|
|
94
|
+
None => String::new(),
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let flags = match args.get(1) {
|
|
98
|
+
Some(Value::String(s)) => s.to_string(),
|
|
99
|
+
Some(Value::Null) | None => String::new(),
|
|
100
|
+
Some(v) => v.to_string(),
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
create_regexp(&pattern, &flags)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ============== String methods with regex support ==============
|
|
107
|
+
|
|
108
|
+
/// String.prototype.match(regexp) - returns array of matches or null
|
|
109
|
+
pub fn string_match(input: &str, regexp: &Value) -> Value {
|
|
110
|
+
match regexp {
|
|
111
|
+
Value::RegExp(re) => {
|
|
112
|
+
let mut re = re.borrow_mut();
|
|
113
|
+
|
|
114
|
+
if re.flags.global {
|
|
115
|
+
let mut matches = Vec::new();
|
|
116
|
+
re.last_index = 0;
|
|
117
|
+
|
|
118
|
+
while let Ok(Some(m)) = re.regex.find_from_pos(input, re.last_index) {
|
|
119
|
+
matches.push(Value::String(m.as_str().into()));
|
|
120
|
+
if m.start() == m.end() {
|
|
121
|
+
re.last_index = m.end() + 1;
|
|
122
|
+
} else {
|
|
123
|
+
re.last_index = m.end();
|
|
124
|
+
}
|
|
125
|
+
if re.last_index > input.len() {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
re.last_index = 0;
|
|
131
|
+
|
|
132
|
+
if matches.is_empty() {
|
|
133
|
+
Value::Null
|
|
134
|
+
} else {
|
|
135
|
+
Value::Array(Rc::new(RefCell::new(matches)))
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
regexp_exec(&mut re, input)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
Value::String(pattern) => match TishRegExp::new(pattern, "") {
|
|
142
|
+
Ok(mut re) => regexp_exec(&mut re, input),
|
|
143
|
+
Err(_) => Value::Null,
|
|
144
|
+
},
|
|
145
|
+
_ => Value::Null,
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/// String.prototype.replace(searchValue, replaceValue)
|
|
150
|
+
pub fn string_replace(input: &str, search: &Value, replace: &Value) -> Value {
|
|
151
|
+
let replacement = match replace {
|
|
152
|
+
Value::String(s) => s.to_string(),
|
|
153
|
+
v => v.to_string(),
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
match search {
|
|
157
|
+
Value::RegExp(re) => {
|
|
158
|
+
let re = re.borrow();
|
|
159
|
+
if re.flags.global {
|
|
160
|
+
match re.regex.replace_all(input, replacement.as_str()) {
|
|
161
|
+
std::borrow::Cow::Borrowed(s) => Value::String(s.into()),
|
|
162
|
+
std::borrow::Cow::Owned(s) => Value::String(s.into()),
|
|
163
|
+
}
|
|
164
|
+
} else {
|
|
165
|
+
match re.regex.replace(input, replacement.as_str()) {
|
|
166
|
+
std::borrow::Cow::Borrowed(s) => Value::String(s.into()),
|
|
167
|
+
std::borrow::Cow::Owned(s) => Value::String(s.into()),
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
Value::String(pattern) => {
|
|
172
|
+
Value::String(input.replacen(pattern.as_ref(), &replacement, 1).into())
|
|
173
|
+
}
|
|
174
|
+
_ => Value::String(input.into()),
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/// Replace regex matches using a callback. Callback receives (match, g1, g2, ..., index, fullString).
|
|
179
|
+
pub fn string_replace_regex_with_fn<F>(
|
|
180
|
+
input: &str,
|
|
181
|
+
re: &TishRegExp,
|
|
182
|
+
invoke: &mut F,
|
|
183
|
+
) -> Result<Value, String>
|
|
184
|
+
where
|
|
185
|
+
F: FnMut(&[Value]) -> Result<String, String>,
|
|
186
|
+
{
|
|
187
|
+
let limit = if re.flags.global { usize::MAX } else { 1 };
|
|
188
|
+
let mut result = String::new();
|
|
189
|
+
let mut last_end: usize = 0;
|
|
190
|
+
for (count, cap_result) in re.regex.captures_iter(input).enumerate() {
|
|
191
|
+
if count >= limit {
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
let caps = cap_result.map_err(|e| format!("Regex error: {}", e))?;
|
|
195
|
+
let full = caps.get(0).unwrap();
|
|
196
|
+
let match_str = full.as_str();
|
|
197
|
+
let byte_start = full.start();
|
|
198
|
+
let char_index = input[..byte_start].chars().count();
|
|
199
|
+
|
|
200
|
+
let mut args = vec![Value::String(match_str.into())];
|
|
201
|
+
for i in 1..caps.len() {
|
|
202
|
+
let val = match caps.get(i) {
|
|
203
|
+
Some(m) => Value::String(m.as_str().into()),
|
|
204
|
+
None => Value::Null,
|
|
205
|
+
};
|
|
206
|
+
args.push(val);
|
|
207
|
+
}
|
|
208
|
+
args.push(Value::Number(char_index as f64));
|
|
209
|
+
args.push(Value::String(input.into()));
|
|
210
|
+
|
|
211
|
+
let repl = invoke(&args)?;
|
|
212
|
+
result.push_str(&input[last_end..byte_start]);
|
|
213
|
+
result.push_str(&repl);
|
|
214
|
+
last_end = full.end();
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
result.push_str(&input[last_end..]);
|
|
218
|
+
Ok(Value::String(result.into()))
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/// String.prototype.search(regexp) - returns index of first match or -1
|
|
222
|
+
pub fn string_search(input: &str, regexp: &Value) -> Value {
|
|
223
|
+
match regexp {
|
|
224
|
+
Value::RegExp(re) => {
|
|
225
|
+
let re = re.borrow();
|
|
226
|
+
match re.regex.find(input) {
|
|
227
|
+
Ok(Some(m)) => {
|
|
228
|
+
let char_index = input[..m.start()].chars().count();
|
|
229
|
+
Value::Number(char_index as f64)
|
|
230
|
+
}
|
|
231
|
+
_ => Value::Number(-1.0),
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
Value::String(pattern) => match TishRegExp::new(pattern, "") {
|
|
235
|
+
Ok(re) => match re.regex.find(input) {
|
|
236
|
+
Ok(Some(m)) => {
|
|
237
|
+
let char_index = input[..m.start()].chars().count();
|
|
238
|
+
Value::Number(char_index as f64)
|
|
239
|
+
}
|
|
240
|
+
_ => Value::Number(-1.0),
|
|
241
|
+
},
|
|
242
|
+
Err(_) => Value::Number(-1.0),
|
|
243
|
+
},
|
|
244
|
+
_ => Value::Number(-1.0),
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// String.prototype.split(separator, limit) - split string by regex or string
|
|
249
|
+
pub fn string_split(input: &str, separator: &Value, limit: Option<usize>) -> Value {
|
|
250
|
+
let max = limit.unwrap_or(usize::MAX);
|
|
251
|
+
|
|
252
|
+
if max == 0 {
|
|
253
|
+
return Value::Array(Rc::new(RefCell::new(Vec::new())));
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
match separator {
|
|
257
|
+
Value::RegExp(re) => {
|
|
258
|
+
let re = re.borrow();
|
|
259
|
+
let mut result = Vec::new();
|
|
260
|
+
let mut last_end = 0;
|
|
261
|
+
|
|
262
|
+
for mat in re.regex.find_iter(input) {
|
|
263
|
+
match mat {
|
|
264
|
+
Ok(m) => {
|
|
265
|
+
if result.len() >= max - 1 {
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
result.push(Value::String(input[last_end..m.start()].into()));
|
|
269
|
+
last_end = m.end();
|
|
270
|
+
}
|
|
271
|
+
Err(_) => break,
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if result.len() < max {
|
|
276
|
+
result.push(Value::String(input[last_end..].into()));
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
Value::Array(Rc::new(RefCell::new(result)))
|
|
280
|
+
}
|
|
281
|
+
Value::String(sep) => {
|
|
282
|
+
let parts: Vec<Value> = input
|
|
283
|
+
.splitn(max, sep.as_ref())
|
|
284
|
+
.map(|s| Value::String(s.into()))
|
|
285
|
+
.collect();
|
|
286
|
+
Value::Array(Rc::new(RefCell::new(parts)))
|
|
287
|
+
}
|
|
288
|
+
Value::Null => Value::Array(Rc::new(RefCell::new(vec![Value::String(input.into())]))),
|
|
289
|
+
_ => {
|
|
290
|
+
let sep_str = separator.to_string();
|
|
291
|
+
let parts: Vec<Value> = input
|
|
292
|
+
.splitn(max, &sep_str)
|
|
293
|
+
.map(|s| Value::String(s.into()))
|
|
294
|
+
.collect();
|
|
295
|
+
Value::Array(Rc::new(RefCell::new(parts)))
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
//! setTimeout, setInterval, clearTimeout, clearInterval.
|
|
2
|
+
//! Non-blocking: setTimeout returns immediately; callbacks run in a drain phase
|
|
3
|
+
//! after the script yields (when run() finishes the synchronous program).
|
|
4
|
+
|
|
5
|
+
use std::cell::RefCell;
|
|
6
|
+
use std::collections::HashMap;
|
|
7
|
+
use std::time::{Duration, Instant};
|
|
8
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
9
|
+
|
|
10
|
+
use crate::value::Value;
|
|
11
|
+
|
|
12
|
+
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
13
|
+
|
|
14
|
+
struct TimerEntry {
|
|
15
|
+
due: Instant,
|
|
16
|
+
callback: Value,
|
|
17
|
+
args: Vec<Value>,
|
|
18
|
+
interval_ms: u64,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
thread_local! {
|
|
22
|
+
static REGISTRY: RefCell<HashMap<u64, TimerEntry>> = RefCell::new(HashMap::new());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
fn next_id() -> u64 {
|
|
26
|
+
NEXT_ID.fetch_add(1, Ordering::SeqCst)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/// Register a one-shot timer. Returns immediately with timer id.
|
|
30
|
+
#[allow(non_snake_case)]
|
|
31
|
+
pub fn setTimeout(callback: Value, args: Vec<Value>, delay_ms: u64) -> u64 {
|
|
32
|
+
let id = next_id();
|
|
33
|
+
let due = Instant::now() + Duration::from_millis(delay_ms);
|
|
34
|
+
REGISTRY.with(|r| {
|
|
35
|
+
r.borrow_mut().insert(id, TimerEntry {
|
|
36
|
+
due,
|
|
37
|
+
callback,
|
|
38
|
+
args,
|
|
39
|
+
interval_ms: 0,
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
id
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Register a repeating timer. Returns immediately with timer id.
|
|
46
|
+
#[allow(non_snake_case)]
|
|
47
|
+
pub fn setInterval(callback: Value, args: Vec<Value>, delay_ms: u64) -> u64 {
|
|
48
|
+
let id = next_id();
|
|
49
|
+
let due = Instant::now() + Duration::from_millis(delay_ms);
|
|
50
|
+
REGISTRY.with(|r| {
|
|
51
|
+
r.borrow_mut().insert(id, TimerEntry {
|
|
52
|
+
due,
|
|
53
|
+
callback,
|
|
54
|
+
args,
|
|
55
|
+
interval_ms: delay_ms,
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
id
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Remove a timer. No-op if already fired or invalid.
|
|
62
|
+
#[allow(non_snake_case)]
|
|
63
|
+
pub fn clearTimer(id: u64) {
|
|
64
|
+
REGISTRY.with(|r| {
|
|
65
|
+
r.borrow_mut().remove(&id);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Take all due timers and return (id, callback, args, interval_ms). Removes them from registry.
|
|
70
|
+
/// Caller should run callbacks; for interval_ms > 0, caller should re-register.
|
|
71
|
+
pub fn take_due_timers() -> Vec<(u64, Value, Vec<Value>, u64)> {
|
|
72
|
+
let now = Instant::now();
|
|
73
|
+
REGISTRY.with(|r| {
|
|
74
|
+
let mut reg = r.borrow_mut();
|
|
75
|
+
let due: Vec<_> = reg
|
|
76
|
+
.iter()
|
|
77
|
+
.filter(|(_, e)| e.due <= now)
|
|
78
|
+
.map(|(id, e)| (*id, e.callback.clone(), e.args.clone(), e.interval_ms))
|
|
79
|
+
.collect();
|
|
80
|
+
for (id, _, _, _) in &due {
|
|
81
|
+
reg.remove(id);
|
|
82
|
+
}
|
|
83
|
+
due
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Re-register an interval timer (called after running its callback).
|
|
88
|
+
pub fn re_register_interval(id: u64, callback: Value, args: Vec<Value>, interval_ms: u64) {
|
|
89
|
+
let due = Instant::now() + Duration::from_millis(interval_ms);
|
|
90
|
+
REGISTRY.with(|r| {
|
|
91
|
+
r.borrow_mut().insert(id, TimerEntry {
|
|
92
|
+
due,
|
|
93
|
+
callback,
|
|
94
|
+
args,
|
|
95
|
+
interval_ms,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/// Check if any timers are still pending.
|
|
101
|
+
pub fn has_pending_timers() -> bool {
|
|
102
|
+
REGISTRY.with(|r| !r.borrow().is_empty())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/// Return the instant when the next timer is due, or None if registry is empty.
|
|
106
|
+
pub fn next_due_instant() -> Option<Instant> {
|
|
107
|
+
REGISTRY.with(|r| {
|
|
108
|
+
let reg = r.borrow();
|
|
109
|
+
reg.values().map(|e| e.due).min()
|
|
110
|
+
})
|
|
111
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
//! Runtime values for the Tish interpreter.
|
|
2
|
+
//!
|
|
3
|
+
//! This module defines the interpreter's `Value` type, which includes variants
|
|
4
|
+
//! like `Function`, `Native`, and `Serve` that hold AST or interpreter-specific
|
|
5
|
+
//! data. The compiled runtime uses `tish_core::Value` instead, which has a
|
|
6
|
+
//! different shape (no AST-carrying variants). The split is intentional.
|
|
7
|
+
|
|
8
|
+
use std::cell::RefCell;
|
|
9
|
+
use std::collections::HashMap;
|
|
10
|
+
use std::rc::Rc;
|
|
11
|
+
use std::sync::Arc;
|
|
12
|
+
|
|
13
|
+
use tish_ast::{Expr, Statement};
|
|
14
|
+
#[cfg(any(feature = "http", feature = "ws"))]
|
|
15
|
+
use tish_core::NativeFn as CoreNativeFn;
|
|
16
|
+
#[cfg(feature = "http")]
|
|
17
|
+
use tish_core::TishPromise;
|
|
18
|
+
use tish_core::TishOpaque;
|
|
19
|
+
|
|
20
|
+
#[cfg(feature = "http")]
|
|
21
|
+
pub use crate::promise::PromiseResolver;
|
|
22
|
+
#[cfg(feature = "regex")]
|
|
23
|
+
pub use crate::regex::TishRegExp;
|
|
24
|
+
|
|
25
|
+
/// Native function type - takes args, returns Result<Value, String>
|
|
26
|
+
pub type NativeFn = fn(&[Value]) -> Result<Value, String>;
|
|
27
|
+
|
|
28
|
+
#[derive(Clone)]
|
|
29
|
+
pub enum Value {
|
|
30
|
+
Number(f64),
|
|
31
|
+
String(Arc<str>),
|
|
32
|
+
Bool(bool),
|
|
33
|
+
Null,
|
|
34
|
+
Array(Rc<RefCell<Vec<Value>>>),
|
|
35
|
+
Object(Rc<RefCell<HashMap<Arc<str>, Value>>>),
|
|
36
|
+
/// User-defined function with AST body
|
|
37
|
+
Function {
|
|
38
|
+
params: Arc<[Arc<str>]>,
|
|
39
|
+
defaults: Arc<[Option<Expr>]>,
|
|
40
|
+
rest_param: Option<Arc<str>>,
|
|
41
|
+
body: Arc<Statement>,
|
|
42
|
+
},
|
|
43
|
+
/// Native/builtin function
|
|
44
|
+
Native(NativeFn),
|
|
45
|
+
/// HTTP serve function (needs special handling for callbacks)
|
|
46
|
+
#[cfg(feature = "http")]
|
|
47
|
+
Serve,
|
|
48
|
+
#[cfg(feature = "regex")]
|
|
49
|
+
RegExp(Rc<RefCell<TishRegExp>>),
|
|
50
|
+
/// Promise (ECMA-262 §27.2). Requires http feature for tokio.
|
|
51
|
+
#[cfg(feature = "http")]
|
|
52
|
+
Promise(crate::promise::PromiseRef),
|
|
53
|
+
/// Internal: resolve/reject functions passed to executor. Not user-facing.
|
|
54
|
+
#[cfg(feature = "http")]
|
|
55
|
+
PromiseResolver(PromiseResolver),
|
|
56
|
+
/// Promise constructor: Promise(executor). Requires special call handling.
|
|
57
|
+
#[cfg(feature = "http")]
|
|
58
|
+
PromiseConstructor,
|
|
59
|
+
/// Bound promise method: promise.then/catch/finally - captures the promise for the call.
|
|
60
|
+
#[cfg(feature = "http")]
|
|
61
|
+
BoundPromiseMethod(crate::promise::PromiseRef, std::sync::Arc<str>),
|
|
62
|
+
/// Timer builtins: setTimeout, setInterval. Need evaluator for callback.
|
|
63
|
+
#[cfg(feature = "http")]
|
|
64
|
+
TimerBuiltin(std::sync::Arc<str>),
|
|
65
|
+
/// Native `tish_core` Promise (fetch / reader.read / response.text).
|
|
66
|
+
#[cfg(feature = "http")]
|
|
67
|
+
CorePromise(Arc<dyn TishPromise>),
|
|
68
|
+
/// `tish_core::Value::Function` (e.g. response.text/json, ws send/receive) callable from eval.
|
|
69
|
+
#[cfg(any(feature = "http", feature = "ws"))]
|
|
70
|
+
CoreFn(CoreNativeFn),
|
|
71
|
+
/// Opaque handle to a native Rust type (e.g. Polars DataFrame).
|
|
72
|
+
Opaque(Arc<dyn TishOpaque>),
|
|
73
|
+
/// Bound method on an opaque value (opaque, method_name). Callable.
|
|
74
|
+
OpaqueMethod(Arc<dyn TishOpaque>, Arc<str>),
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
impl std::fmt::Debug for Value {
|
|
78
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
79
|
+
match self {
|
|
80
|
+
Value::Number(n) => write!(f, "Number({})", n),
|
|
81
|
+
Value::String(s) => write!(f, "String({:?})", s.as_ref()),
|
|
82
|
+
Value::Bool(b) => write!(f, "Bool({})", b),
|
|
83
|
+
Value::Null => write!(f, "Null"),
|
|
84
|
+
Value::Array(arr) => write!(f, "Array({:?})", arr.borrow()),
|
|
85
|
+
Value::Object(obj) => write!(f, "Object({:?})", obj.borrow()),
|
|
86
|
+
Value::Function { .. } => write!(f, "Function"),
|
|
87
|
+
Value::Native(_) => write!(f, "Native"),
|
|
88
|
+
#[cfg(feature = "http")]
|
|
89
|
+
Value::Serve => write!(f, "Serve"),
|
|
90
|
+
#[cfg(feature = "regex")]
|
|
91
|
+
Value::RegExp(re) => write!(f, "RegExp(/{}/{})", re.borrow().source, re.borrow().flags_string()),
|
|
92
|
+
#[cfg(feature = "http")]
|
|
93
|
+
Value::Promise(_) => write!(f, "Promise"),
|
|
94
|
+
#[cfg(feature = "http")]
|
|
95
|
+
Value::PromiseResolver(_) => write!(f, "[PromiseResolver]"),
|
|
96
|
+
#[cfg(feature = "http")]
|
|
97
|
+
Value::PromiseConstructor => write!(f, "[Function: Promise]"),
|
|
98
|
+
#[cfg(feature = "http")]
|
|
99
|
+
Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
|
|
100
|
+
#[cfg(feature = "http")]
|
|
101
|
+
Value::CorePromise(_) => write!(f, "Promise"),
|
|
102
|
+
#[cfg(any(feature = "http", feature = "ws"))]
|
|
103
|
+
Value::CoreFn(_) => write!(f, "CoreFn"),
|
|
104
|
+
Value::Opaque(o) => write!(f, "{}(opaque)", o.type_name()),
|
|
105
|
+
Value::OpaqueMethod(_, _) => write!(f, "[Function]"),
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
impl std::fmt::Display for Value {
|
|
111
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
112
|
+
match self {
|
|
113
|
+
Value::Number(n) => {
|
|
114
|
+
if n.is_nan() {
|
|
115
|
+
write!(f, "NaN")
|
|
116
|
+
} else if *n == f64::INFINITY {
|
|
117
|
+
write!(f, "Infinity")
|
|
118
|
+
} else if *n == f64::NEG_INFINITY {
|
|
119
|
+
write!(f, "-Infinity")
|
|
120
|
+
} else {
|
|
121
|
+
write!(f, "{}", n)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
Value::String(s) => write!(f, "{}", s),
|
|
125
|
+
Value::Bool(b) => write!(f, "{}", b),
|
|
126
|
+
Value::Null => write!(f, "null"),
|
|
127
|
+
Value::Array(arr) => {
|
|
128
|
+
let inner: Vec<String> = arr.borrow().iter().map(|v| v.to_string()).collect();
|
|
129
|
+
write!(f, "[{}]", inner.join(", "))
|
|
130
|
+
}
|
|
131
|
+
Value::Object(obj) => {
|
|
132
|
+
let inner: Vec<String> = obj
|
|
133
|
+
.borrow()
|
|
134
|
+
.iter()
|
|
135
|
+
.map(|(k, v)| format!("{}: {}", k.as_ref(), v))
|
|
136
|
+
.collect();
|
|
137
|
+
write!(f, "{{{}}}", inner.join(", "))
|
|
138
|
+
}
|
|
139
|
+
Value::Function { .. } => write!(f, "[Function]"),
|
|
140
|
+
Value::Native(_) => write!(f, "[NativeFunction]"),
|
|
141
|
+
#[cfg(feature = "http")]
|
|
142
|
+
Value::Serve => write!(f, "[NativeFunction: serve]"),
|
|
143
|
+
#[cfg(feature = "regex")]
|
|
144
|
+
Value::RegExp(re) => {
|
|
145
|
+
let re = re.borrow();
|
|
146
|
+
write!(f, "/{}/{}", re.source, re.flags_string())
|
|
147
|
+
}
|
|
148
|
+
#[cfg(feature = "http")]
|
|
149
|
+
Value::Promise(_) => write!(f, "[Promise]"),
|
|
150
|
+
#[cfg(feature = "http")]
|
|
151
|
+
Value::PromiseResolver(_) => write!(f, "[Function]"),
|
|
152
|
+
#[cfg(feature = "http")]
|
|
153
|
+
Value::PromiseConstructor => write!(f, "function Promise() {{ [native code] }}"),
|
|
154
|
+
#[cfg(feature = "http")]
|
|
155
|
+
Value::BoundPromiseMethod(_, _) | Value::TimerBuiltin(_) => write!(f, "[Function]"),
|
|
156
|
+
#[cfg(feature = "http")]
|
|
157
|
+
Value::CorePromise(_) => write!(f, "[Promise]"),
|
|
158
|
+
#[cfg(any(feature = "http", feature = "ws"))]
|
|
159
|
+
Value::CoreFn(_) => write!(f, "[Function]"),
|
|
160
|
+
Value::Opaque(o) => write!(f, "[object {}]", o.type_name()),
|
|
161
|
+
Value::OpaqueMethod(_, _) => write!(f, "[Function]"),
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
impl Value {
|
|
167
|
+
pub fn is_truthy(&self) -> bool {
|
|
168
|
+
match self {
|
|
169
|
+
Value::Null => false,
|
|
170
|
+
Value::Bool(b) => *b,
|
|
171
|
+
Value::Number(n) => *n != 0.0 && !n.is_nan(),
|
|
172
|
+
Value::String(s) => !s.is_empty(),
|
|
173
|
+
_ => true,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
pub fn strict_eq(&self, other: &Value) -> bool {
|
|
178
|
+
match (self, other) {
|
|
179
|
+
(Value::Number(a), Value::Number(b)) => {
|
|
180
|
+
if a.is_nan() || b.is_nan() {
|
|
181
|
+
false
|
|
182
|
+
} else {
|
|
183
|
+
a == b
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
(Value::String(a), Value::String(b)) => a == b,
|
|
187
|
+
(Value::Bool(a), Value::Bool(b)) => a == b,
|
|
188
|
+
(Value::Null, Value::Null) => true,
|
|
189
|
+
(Value::Array(a), Value::Array(b)) => Rc::ptr_eq(a, b),
|
|
190
|
+
(Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
|
|
191
|
+
(Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
|
|
192
|
+
(Value::OpaqueMethod(a, ak), Value::OpaqueMethod(b, bk)) => Arc::ptr_eq(a, b) && ak == bk,
|
|
193
|
+
_ => false,
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/// Create a new array Value from a Vec.
|
|
198
|
+
pub fn array(items: Vec<Value>) -> Self {
|
|
199
|
+
Value::Array(Rc::new(RefCell::new(items)))
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Create a new object Value from a HashMap.
|
|
203
|
+
pub fn object(map: HashMap<Arc<str>, Value>) -> Self {
|
|
204
|
+
Value::Object(Rc::new(RefCell::new(map)))
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/// Create an empty array Value.
|
|
208
|
+
pub fn empty_array() -> Self {
|
|
209
|
+
Value::Array(Rc::new(RefCell::new(Vec::new())))
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/// Create an empty object Value.
|
|
213
|
+
pub fn empty_object() -> Self {
|
|
214
|
+
Value::Object(Rc::new(RefCell::new(HashMap::new())))
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/// Extract the number value, if this is a Number.
|
|
218
|
+
pub fn as_number(&self) -> Option<f64> {
|
|
219
|
+
match self {
|
|
220
|
+
Value::Number(n) => Some(*n),
|
|
221
|
+
_ => None,
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|