@tishlang/tish 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. package/platform/win32-x64/tish.exe +0 -0
@@ -8,8 +8,10 @@ mod json;
8
8
  mod macros;
9
9
  mod uri;
10
10
  mod value;
11
+ mod vmref;
11
12
 
12
13
  pub use console_style::{format_value_styled, format_values_for_console, use_console_colors};
13
- pub use value::*;
14
- pub use json::{json_parse, json_stringify};
14
+ pub use json::{json_parse, json_stringify, json_stringify_into};
15
15
  pub use uri::{percent_decode, percent_encode};
16
+ pub use value::*;
17
+ pub use vmref::{VmReadGuard, VmRef, VmWriteGuard};
@@ -23,14 +23,14 @@
23
23
  #[macro_export]
24
24
  macro_rules! tish_module {
25
25
  ($($name:expr => $fn:expr),* $(,)?) => {{
26
- use std::cell::RefCell;
27
- use std::rc::Rc;
28
26
  use std::sync::Arc;
29
- use $crate::{ObjectMap, Value};
27
+ use $crate::{ObjectMap, Value, VmRef};
30
28
  let mut map = ObjectMap::default();
31
29
  $(
32
- map.insert(Arc::from($name), Value::Function(Rc::new($fn)));
30
+ // `Value::native` picks the right Rc / Arc wrapper depending on
31
+ // whether the `send-values` feature is enabled upstream.
32
+ map.insert(Arc::from($name), Value::native($fn));
33
33
  )*
34
- Value::Object(Rc::new(RefCell::new(map)))
34
+ Value::Object(VmRef::new(map))
35
35
  }};
36
36
  }
@@ -10,13 +10,13 @@ pub fn percent_decode(input: &str) -> Result<String, String> {
10
10
  "%2F", "%2f", // /
11
11
  "%3F", "%3f", // ?
12
12
  "%3A", "%3a", // :
13
- "%40", // @
14
- "%26", // &
13
+ "%40", // @
14
+ "%26", // &
15
15
  "%3D", "%3d", // =
16
16
  "%2B", "%2b", // +
17
- "%24", // $
17
+ "%24", // $
18
18
  "%2C", "%2c", // ,
19
- "%23", // #
19
+ "%23", // #
20
20
  ];
21
21
 
22
22
  let mut result = String::with_capacity(input.len());
@@ -46,11 +46,14 @@ pub fn percent_decode(input: &str) -> Result<String, String> {
46
46
  None => return Err("URIError: malformed URI sequence".to_string()),
47
47
  }
48
48
  }
49
-
49
+
50
50
  if hex.len() == 2 {
51
51
  let encoded = format!("%{}", hex);
52
52
  // Check if this is a reserved character that should NOT be decoded
53
- if RESERVED_ENCODED.iter().any(|r| r.eq_ignore_ascii_case(&encoded)) {
53
+ if RESERVED_ENCODED
54
+ .iter()
55
+ .any(|r| r.eq_ignore_ascii_case(&encoded))
56
+ {
54
57
  result.push_str(&encoded);
55
58
  } else if let Ok(byte) = u8::from_str_radix(&hex, 16) {
56
59
  result.push(byte as char);
@@ -1,11 +1,11 @@
1
1
  //! Unified Value type for Tish runtime values.
2
2
 
3
- use std::cell::RefCell;
4
- use std::rc::Rc;
5
3
  use std::sync::Arc;
6
4
 
7
5
  use ahash::AHashMap;
8
6
 
7
+ use crate::vmref::VmRef;
8
+
9
9
  /// Property map for objects and other `Arc<str>` → `Value` tables (VM globals, scopes).
10
10
  /// Uses a faster hasher than `std::collections::HashMap` for string-heavy workloads.
11
11
  pub type ObjectMap = AHashMap<Arc<str>, Value>;
@@ -14,8 +14,17 @@ pub type ObjectMap = AHashMap<Arc<str>, Value>;
14
14
  use fancy_regex::Regex;
15
15
 
16
16
  /// Native function signature.
17
- /// Returns Value directly (not Result) for simplicity and backward compatibility.
18
- pub type NativeFn = Rc<dyn Fn(&[Value]) -> Value>;
17
+ ///
18
+ /// When the `send-values` feature is enabled this is
19
+ /// `Arc<dyn Fn + Send + Sync>`, so handler closures can be dispatched across
20
+ /// HTTP worker threads (`tishlang_runtime::http::serve`). Otherwise it stays
21
+ /// `Rc<dyn Fn>` for zero-overhead single-threaded execution (wasm / wasi /
22
+ /// interpreter / cranelift / llvm VMs and any Rust native build without
23
+ /// `http`).
24
+ #[cfg(feature = "send-values")]
25
+ pub type NativeFn = Arc<dyn Fn(&[Value]) -> Value + Send + Sync>;
26
+ #[cfg(not(feature = "send-values"))]
27
+ pub type NativeFn = std::rc::Rc<dyn Fn(&[Value]) -> Value>;
19
28
 
20
29
  /// Trait for opaque Rust types exposed to Tish (e.g. Polars DataFrame).
21
30
  /// Implementors provide method dispatch so Tish can call methods on the value.
@@ -54,29 +63,70 @@ impl RegExpFlags {
54
63
  let mut result = Self::default();
55
64
  for c in flags.chars() {
56
65
  match c {
57
- 'g' => { if result.global { return Err(format!("duplicate flag '{}'", c)); } result.global = true; }
58
- 'i' => { if result.ignore_case { return Err(format!("duplicate flag '{}'", c)); } result.ignore_case = true; }
59
- 'm' => { if result.multiline { return Err(format!("duplicate flag '{}'", c)); } result.multiline = true; }
60
- 's' => { if result.dot_all { return Err(format!("duplicate flag '{}'", c)); } result.dot_all = true; }
61
- 'u' => { if result.unicode { return Err(format!("duplicate flag '{}'", c)); } result.unicode = true; }
62
- 'y' => { if result.sticky { return Err(format!("duplicate flag '{}'", c)); } result.sticky = true; }
66
+ 'g' => {
67
+ if result.global {
68
+ return Err(format!("duplicate flag '{}'", c));
69
+ }
70
+ result.global = true;
71
+ }
72
+ 'i' => {
73
+ if result.ignore_case {
74
+ return Err(format!("duplicate flag '{}'", c));
75
+ }
76
+ result.ignore_case = true;
77
+ }
78
+ 'm' => {
79
+ if result.multiline {
80
+ return Err(format!("duplicate flag '{}'", c));
81
+ }
82
+ result.multiline = true;
83
+ }
84
+ 's' => {
85
+ if result.dot_all {
86
+ return Err(format!("duplicate flag '{}'", c));
87
+ }
88
+ result.dot_all = true;
89
+ }
90
+ 'u' => {
91
+ if result.unicode {
92
+ return Err(format!("duplicate flag '{}'", c));
93
+ }
94
+ result.unicode = true;
95
+ }
96
+ 'y' => {
97
+ if result.sticky {
98
+ return Err(format!("duplicate flag '{}'", c));
99
+ }
100
+ result.sticky = true;
101
+ }
63
102
  _ => return Err(format!("unknown flag '{}'", c)),
64
103
  }
65
104
  }
66
105
  Ok(result)
67
106
  }
68
-
69
107
  }
70
108
 
71
109
  #[cfg(feature = "regex")]
72
110
  impl std::fmt::Display for RegExpFlags {
73
111
  fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74
- if self.global { f.write_str("g")?; }
75
- if self.ignore_case { f.write_str("i")?; }
76
- if self.multiline { f.write_str("m")?; }
77
- if self.dot_all { f.write_str("s")?; }
78
- if self.unicode { f.write_str("u")?; }
79
- if self.sticky { f.write_str("y")?; }
112
+ if self.global {
113
+ f.write_str("g")?;
114
+ }
115
+ if self.ignore_case {
116
+ f.write_str("i")?;
117
+ }
118
+ if self.multiline {
119
+ f.write_str("m")?;
120
+ }
121
+ if self.dot_all {
122
+ f.write_str("s")?;
123
+ }
124
+ if self.unicode {
125
+ f.write_str("u")?;
126
+ }
127
+ if self.sticky {
128
+ f.write_str("y")?;
129
+ }
80
130
  Ok(())
81
131
  }
82
132
  }
@@ -96,23 +146,36 @@ impl TishRegExp {
96
146
  pub fn new(pattern: &str, flags_str: &str) -> Result<Self, String> {
97
147
  let flags = RegExpFlags::from_string(flags_str)?;
98
148
  let mut regex_pattern = pattern.to_string();
99
-
149
+
100
150
  if flags.ignore_case || flags.multiline || flags.dot_all {
101
151
  let mut flag_prefix = String::from("(?");
102
- if flags.ignore_case { flag_prefix.push('i'); }
103
- if flags.multiline { flag_prefix.push('m'); }
104
- if flags.dot_all { flag_prefix.push('s'); }
152
+ if flags.ignore_case {
153
+ flag_prefix.push('i');
154
+ }
155
+ if flags.multiline {
156
+ flag_prefix.push('m');
157
+ }
158
+ if flags.dot_all {
159
+ flag_prefix.push('s');
160
+ }
105
161
  flag_prefix.push(')');
106
162
  regex_pattern = format!("{}{}", flag_prefix, regex_pattern);
107
163
  }
108
-
109
- let regex = Regex::new(&regex_pattern)
110
- .map_err(|e| format!("Invalid regular expression: {}", e))?;
111
-
112
- Ok(Self { source: pattern.to_string(), flags, regex: Arc::new(regex), last_index: 0 })
164
+
165
+ let regex =
166
+ Regex::new(&regex_pattern).map_err(|e| format!("Invalid regular expression: {}", e))?;
167
+
168
+ Ok(Self {
169
+ source: pattern.to_string(),
170
+ flags,
171
+ regex: Arc::new(regex),
172
+ last_index: 0,
173
+ })
113
174
  }
114
175
 
115
- pub fn flags_string(&self) -> String { self.flags.to_string() }
176
+ pub fn flags_string(&self) -> String {
177
+ self.flags.to_string()
178
+ }
116
179
 
117
180
  pub fn test(&mut self, input: &str) -> bool {
118
181
  if self.flags.global || self.flags.sticky {
@@ -121,10 +184,10 @@ impl TishRegExp {
121
184
  self.last_index = 0;
122
185
  return false;
123
186
  }
124
-
187
+
125
188
  let byte_start: usize = input.chars().take(start).map(|c| c.len_utf8()).sum();
126
189
  let search_str = &input[byte_start..];
127
-
190
+
128
191
  match self.regex.find(search_str) {
129
192
  Ok(Some(m)) => {
130
193
  if self.flags.sticky && m.start() != 0 {
@@ -148,17 +211,21 @@ impl TishRegExp {
148
211
 
149
212
  /// Runtime value for Tish programs.
150
213
  /// Used by both interpreter and compiled code.
214
+ ///
215
+ /// **Thread safety**: `Value: Send + Sync`. Mutable payloads live inside
216
+ /// [`VmRef`], a `Send + Sync` `Arc<Mutex<T>>` wrapper that preserves the
217
+ /// `RefCell`-style borrow API. Functions are `Arc<dyn Fn + Send + Sync>`.
151
218
  #[derive(Clone)]
152
219
  pub enum Value {
153
220
  Number(f64),
154
221
  String(Arc<str>),
155
222
  Bool(bool),
156
223
  Null,
157
- Array(Rc<RefCell<Vec<Value>>>),
158
- Object(Rc<RefCell<ObjectMap>>),
224
+ Array(VmRef<Vec<Value>>),
225
+ Object(VmRef<ObjectMap>),
159
226
  Function(NativeFn),
160
227
  #[cfg(feature = "regex")]
161
- RegExp(Rc<RefCell<TishRegExp>>),
228
+ RegExp(VmRef<TishRegExp>),
162
229
  /// Promise (for native compile). Interpreter uses tishlang_eval::Value::Promise.
163
230
  Promise(Arc<dyn TishPromise>),
164
231
  /// Opaque handle to a native Rust type (e.g. Polars DataFrame).
@@ -176,7 +243,12 @@ impl std::fmt::Debug for Value {
176
243
  Value::Object(obj) => write!(f, "Object({:?})", obj.borrow()),
177
244
  Value::Function(_) => write!(f, "Function"),
178
245
  #[cfg(feature = "regex")]
179
- Value::RegExp(re) => write!(f, "RegExp(/{}/{})", re.borrow().source, re.borrow().flags_string()),
246
+ Value::RegExp(re) => write!(
247
+ f,
248
+ "RegExp(/{}/{})",
249
+ re.borrow().source,
250
+ re.borrow().flags_string()
251
+ ),
180
252
  Value::Promise(_) => write!(f, "Promise"),
181
253
  Value::Opaque(o) => write!(f, "{}(opaque)", o.type_name()),
182
254
  }
@@ -202,7 +274,8 @@ impl Value {
202
274
  Value::Bool(b) => b.to_string(),
203
275
  Value::Null => "null".to_string(),
204
276
  Value::Array(arr) => {
205
- let inner: Vec<String> = arr.borrow().iter().map(|v| v.to_display_string()).collect();
277
+ let inner: Vec<String> =
278
+ arr.borrow().iter().map(|v| v.to_display_string()).collect();
206
279
  format!("[{}]", inner.join(", "))
207
280
  }
208
281
  Value::Object(obj) => {
@@ -248,35 +321,60 @@ impl Value {
248
321
  (Value::String(a), Value::String(b)) => a == b,
249
322
  (Value::Bool(a), Value::Bool(b)) => a == b,
250
323
  (Value::Null, Value::Null) => true,
251
- (Value::Array(a), Value::Array(b)) => Rc::ptr_eq(a, b),
252
- (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
253
- (Value::Function(a), Value::Function(b)) => Rc::ptr_eq(a, b),
324
+ (Value::Array(a), Value::Array(b)) => VmRef::ptr_eq(a, b),
325
+ (Value::Object(a), Value::Object(b)) => VmRef::ptr_eq(a, b),
326
+ #[cfg(feature = "send-values")]
327
+ (Value::Function(a), Value::Function(b)) => Arc::ptr_eq(a, b),
328
+ #[cfg(not(feature = "send-values"))]
329
+ (Value::Function(a), Value::Function(b)) => std::rc::Rc::ptr_eq(a, b),
254
330
  #[cfg(feature = "regex")]
255
- (Value::RegExp(a), Value::RegExp(b)) => Rc::ptr_eq(a, b),
331
+ (Value::RegExp(a), Value::RegExp(b)) => VmRef::ptr_eq(a, b),
256
332
  (Value::Promise(a), Value::Promise(b)) => Arc::ptr_eq(a, b),
257
333
  (Value::Opaque(a), Value::Opaque(b)) => Arc::ptr_eq(a, b),
258
334
  _ => false,
259
335
  }
260
336
  }
261
337
 
338
+ /// Wrap a Rust closure in a `Value::Function`. Automatically picks
339
+ /// `Rc<dyn Fn>` or `Arc<dyn Fn + Send + Sync>` based on the
340
+ /// `send-values` feature, so callers don't have to `cfg`-gate their
341
+ /// code. The input bound tracks the feature too: when `send-values`
342
+ /// is enabled the closure must be `Send + Sync`, otherwise any `Fn`
343
+ /// is accepted.
344
+ #[cfg(feature = "send-values")]
345
+ pub fn native<F>(f: F) -> Self
346
+ where
347
+ F: Fn(&[Value]) -> Value + Send + Sync + 'static,
348
+ {
349
+ Value::Function(Arc::new(f))
350
+ }
351
+
352
+ #[cfg(not(feature = "send-values"))]
353
+ pub fn native<F>(f: F) -> Self
354
+ where
355
+ F: Fn(&[Value]) -> Value + 'static,
356
+ {
357
+ Value::Function(std::rc::Rc::new(f))
358
+ }
359
+
262
360
  /// Create a new array Value from a Vec.
263
361
  pub fn array(items: Vec<Value>) -> Self {
264
- Value::Array(Rc::new(RefCell::new(items)))
362
+ Value::Array(VmRef::new(items))
265
363
  }
266
364
 
267
365
  /// Create a new object Value from a property map.
268
366
  pub fn object(map: ObjectMap) -> Self {
269
- Value::Object(Rc::new(RefCell::new(map)))
367
+ Value::Object(VmRef::new(map))
270
368
  }
271
369
 
272
370
  /// Create an empty array Value.
273
371
  pub fn empty_array() -> Self {
274
- Value::Array(Rc::new(RefCell::new(Vec::new())))
372
+ Value::Array(VmRef::new(Vec::new()))
275
373
  }
276
374
 
277
375
  /// Create an empty object Value.
278
376
  pub fn empty_object() -> Self {
279
- Value::Object(Rc::new(RefCell::new(ObjectMap::default())))
377
+ Value::Object(VmRef::new(ObjectMap::default()))
280
378
  }
281
379
 
282
380
  /// Extract the number value, if this is a Number.
@@ -378,7 +476,11 @@ impl Value {
378
476
  "trim".into(),
379
477
  ]
380
478
  }
381
- Value::Number(_) => vec!["toFixed".into(), "toExponential".into(), "toPrecision".into()],
479
+ Value::Number(_) => vec![
480
+ "toFixed".into(),
481
+ "toExponential".into(),
482
+ "toPrecision".into(),
483
+ ],
382
484
  _ => vec![],
383
485
  }
384
486
  }
@@ -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
+ }
@@ -12,9 +12,8 @@ pub fn link_to_binary(
12
12
  output_path: &Path,
13
13
  features: &[String],
14
14
  ) -> Result<(), CraneliftError> {
15
- let workspace_root = tishlang_build_utils::find_workspace_root().map_err(|e| CraneliftError {
16
- message: e,
17
- })?;
15
+ let workspace_root =
16
+ tishlang_build_utils::find_workspace_root().map_err(|e| CraneliftError { message: e })?;
18
17
  let out_name = output_path
19
18
  .file_stem()
20
19
  .and_then(|s| s.to_str())
@@ -104,17 +103,15 @@ fn main() {{
104
103
  message: format!("Cannot write build.rs: {}", e),
105
104
  })?;
106
105
 
107
- tishlang_build_utils::run_cargo_build(&build_dir, None).map_err(|e| CraneliftError { message: e })?;
106
+ tishlang_build_utils::run_cargo_build(&build_dir, None)
107
+ .map_err(|e| CraneliftError { message: e })?;
108
108
 
109
109
  let binary_dir = build_dir.join("target").join("release");
110
- let binary =
111
- tishlang_build_utils::find_release_binary(&binary_dir, out_name)
112
- .map_err(|e| CraneliftError { message: e })?;
110
+ let binary = tishlang_build_utils::find_release_binary(&binary_dir, out_name)
111
+ .map_err(|e| CraneliftError { message: e })?;
113
112
  let target = tishlang_build_utils::resolve_output_path(output_path, out_name);
114
113
  tishlang_build_utils::copy_binary_to_output(&binary, &target)
115
114
  .map_err(|e| CraneliftError { message: e })?;
116
115
 
117
116
  Ok(())
118
117
  }
119
-
120
-
@@ -7,8 +7,8 @@
7
7
 
8
8
  use std::path::Path;
9
9
 
10
- use cranelift::codegen::settings::Configurable;
11
10
  use cranelift::codegen::settings;
11
+ use cranelift::codegen::settings::Configurable;
12
12
  use cranelift_module::{DataDescription, Linkage, Module};
13
13
  use cranelift_object::{ObjectBuilder, ObjectModule};
14
14
 
@@ -18,9 +18,11 @@ use crate::CraneliftError;
18
18
 
19
19
  pub fn lower_and_emit(chunk: &Chunk, object_path: &Path) -> Result<(), CraneliftError> {
20
20
  let mut settings_builder = settings::builder();
21
- settings_builder.set("opt_level", "speed").map_err(|_| CraneliftError {
22
- message: "Failed to set opt_level".to_string(),
23
- })?;
21
+ settings_builder
22
+ .set("opt_level", "speed")
23
+ .map_err(|_| CraneliftError {
24
+ message: "Failed to set opt_level".to_string(),
25
+ })?;
24
26
  let flags = settings::Flags::new(settings_builder);
25
27
 
26
28
  let isa_builder = cranelift_native::builder().map_err(|e| CraneliftError {
@@ -30,10 +32,14 @@ pub fn lower_and_emit(chunk: &Chunk, object_path: &Path) -> Result<(), Cranelift
30
32
  message: format!("Failed to finish ISA: {}", e),
31
33
  })?;
32
34
 
33
- let object_builder = ObjectBuilder::new(isa, "tishlang_cranelift", cranelift_module::default_libcall_names())
34
- .map_err(|e| CraneliftError {
35
- message: format!("Failed to create ObjectBuilder: {}", e),
36
- })?;
35
+ let object_builder = ObjectBuilder::new(
36
+ isa,
37
+ "tishlang_cranelift",
38
+ cranelift_module::default_libcall_names(),
39
+ )
40
+ .map_err(|e| CraneliftError {
41
+ message: format!("Failed to create ObjectBuilder: {}", e),
42
+ })?;
37
43
  let mut module = ObjectModule::new(object_builder);
38
44
 
39
45
  // Serialize chunk and emit as data - link step will build a Rust binary that reads it
@@ -8,7 +8,22 @@ license-file = { workspace = true }
8
8
  repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
- http = ["tokio", "reqwest", "futures", "tiny_http", "tishlang_core/regex", "dep:tishlang_runtime", "tishlang_runtime/http"]
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.0"
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" }