@tishlang/tish 1.6.0 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. package/platform/win32-x64/tish.exe +0 -0
@@ -1,76 +1,235 @@
1
- //! Promise static methods for compiled Tish (resolve, reject, all, race).
1
+ //! Promise helpers for the bytecode VM and native codegen (`Promise.resolve`, etc.).
2
+ //!
3
+ //! The global `Promise` value is an **object** with a `__call` entry so the VM can
4
+ //! invoke `Promise(executor)` like `new Promise(executor)` in JS. Static methods live
5
+ //! on the same object (`resolve`, `reject`, `all`, `race`).
2
6
 
3
- use std::cell::RefCell;
4
- use std::rc::Rc;
5
- use std::sync::Arc;
6
- use tishlang_core::{ObjectMap, Value};
7
+ use std::sync::mpsc;
8
+ use std::sync::{Arc, Mutex};
7
9
 
8
- /// Promise.resolve(value) - returns the value (immediate resolve).
10
+ use tishlang_core::{ObjectMap, TishPromise, Value, VmRef};
11
+
12
+ /// Fulfilled or rejected before anyone awaits — `block_until_settled` consumes the result once.
13
+ pub struct ImmediateSettledPromise {
14
+ slot: Mutex<Option<Result<Value, Value>>>,
15
+ }
16
+
17
+ impl ImmediateSettledPromise {
18
+ fn new(result: Result<Value, Value>) -> Self {
19
+ Self {
20
+ slot: Mutex::new(Some(result)),
21
+ }
22
+ }
23
+ }
24
+
25
+ impl TishPromise for ImmediateSettledPromise {
26
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
27
+ self.slot
28
+ .lock()
29
+ .unwrap()
30
+ .take()
31
+ .unwrap_or(Err(Value::String(
32
+ "Promise already settled or consumed".into(),
33
+ )))
34
+ }
35
+ }
36
+
37
+ fn fulfilled(v: Value) -> Value {
38
+ Value::Promise(Arc::new(ImmediateSettledPromise::new(Ok(v))))
39
+ }
40
+
41
+ fn rejected(v: Value) -> Value {
42
+ Value::Promise(Arc::new(ImmediateSettledPromise::new(Err(v))))
43
+ }
44
+
45
+ fn flatten_chain_out(v: Value) -> std::result::Result<Value, Value> {
46
+ match v {
47
+ Value::Promise(p) => p.block_until_settled(),
48
+ other => Ok(other),
49
+ }
50
+ }
51
+
52
+ /// `Promise(executor)` — executor runs synchronously; `resolve` / `reject` unblock `recv`.
53
+ struct DeferredChannelPromise {
54
+ rx: Mutex<Option<mpsc::Receiver<Result<Value, Value>>>>,
55
+ }
56
+
57
+ impl TishPromise for DeferredChannelPromise {
58
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
59
+ let rx = self.rx.lock().unwrap().take();
60
+ match rx {
61
+ Some(r) => r.recv().unwrap_or(Err(Value::String(
62
+ "Promise executor did not call resolve or reject".into(),
63
+ ))),
64
+ None => Err(Value::String(
65
+ "Promise already consumed or settled".into(),
66
+ )),
67
+ }
68
+ }
69
+ }
70
+
71
+ /// `.then` / `.catch` chain: when awaited, settle the predecessor then optionally invoke a handler.
72
+ pub struct ThenPromise {
73
+ pred: Arc<dyn TishPromise>,
74
+ on_fulfilled: Option<Value>,
75
+ on_rejected: Option<Value>,
76
+ }
77
+
78
+ impl TishPromise for ThenPromise {
79
+ fn block_until_settled(&self) -> std::result::Result<Value, Value> {
80
+ match self.pred.block_until_settled() {
81
+ Ok(v) => {
82
+ if let Some(Value::Function(f)) = &self.on_fulfilled {
83
+ flatten_chain_out(f(&[v]))
84
+ } else {
85
+ Ok(v)
86
+ }
87
+ }
88
+ Err(e) => {
89
+ if let Some(Value::Function(f)) = &self.on_rejected {
90
+ flatten_chain_out(f(&[e]))
91
+ } else {
92
+ Err(e)
93
+ }
94
+ }
95
+ }
96
+ }
97
+ }
98
+
99
+ /// `Promise.resolve(value)` — adopt promises, otherwise wrap in a fulfilled promise.
9
100
  pub fn promise_resolve(args: &[Value]) -> Value {
10
- args.first().cloned().unwrap_or(Value::Null)
101
+ match args.first() {
102
+ Some(Value::Promise(p)) => Value::Promise(Arc::clone(p)),
103
+ Some(v) => fulfilled(v.clone()),
104
+ None => fulfilled(Value::Null),
105
+ }
11
106
  }
12
107
 
13
- /// Promise.reject(value) - returns value as "rejected" placeholder.
14
- /// Note: await on this in native compile may not throw; use try/catch in interpreter for full support.
108
+ /// `Promise.reject(reason)` always a rejected promise.
15
109
  pub fn promise_reject(args: &[Value]) -> Value {
16
- args.first().cloned().unwrap_or(Value::Null)
110
+ rejected(
111
+ args.first()
112
+ .cloned()
113
+ .unwrap_or(Value::Null),
114
+ )
17
115
  }
18
116
 
19
- /// Promise.all(iterable) - awaits each Promise in the array, returns array of resolved values.
117
+ /// `Promise.all(iterable)` block on each promise in order; non-promises pass through.
20
118
  pub fn promise_all(args: &[Value]) -> Value {
21
119
  match args.first() {
22
120
  Some(Value::Array(arr)) => {
23
- let arr = arr.borrow();
24
- let resolved: Vec<Value> = arr
25
- .iter()
26
- .map(|v| {
27
- if let Value::Promise(p) = v {
28
- match tishlang_core::TishPromise::block_until_settled(p.as_ref()) {
29
- Ok(val) => val,
30
- Err(rejection) => rejection,
31
- }
32
- } else {
33
- v.clone()
121
+ let mut out: Vec<Value> = Vec::new();
122
+ for v in arr.borrow().iter() {
123
+ let item = if let Value::Promise(p) = v {
124
+ match p.block_until_settled() {
125
+ Ok(x) => x,
126
+ Err(rej) => return rejected(rej),
34
127
  }
35
- })
36
- .collect();
37
- Value::Array(Rc::new(RefCell::new(resolved)))
128
+ } else {
129
+ v.clone()
130
+ };
131
+ out.push(item);
132
+ }
133
+ fulfilled(Value::Array(VmRef::new(out)))
38
134
  }
39
- Some(v) => v.clone(),
40
- None => Value::Null,
135
+ Some(v) => fulfilled(v.clone()),
136
+ None => fulfilled(Value::Null),
41
137
  }
42
138
  }
43
139
 
44
- /// Promise.race(iterable) - returns first element of array.
140
+ /// `Promise.race(iterable)` first element wins (blocking first promise if it is one).
45
141
  pub fn promise_race(args: &[Value]) -> Value {
46
142
  match args.first() {
47
- Some(Value::Array(arr)) => arr
48
- .borrow()
49
- .first()
50
- .cloned()
51
- .unwrap_or(Value::Null),
52
- _ => Value::Null,
143
+ Some(Value::Array(arr)) => {
144
+ let borrowed = arr.borrow();
145
+ for v in borrowed.iter() {
146
+ if let Value::Promise(p) = v {
147
+ return match p.block_until_settled() {
148
+ Ok(x) => fulfilled(x),
149
+ Err(e) => rejected(e),
150
+ };
151
+ }
152
+ return fulfilled(v.clone());
153
+ }
154
+ Value::Null
155
+ }
156
+ Some(v) => fulfilled(v.clone()),
157
+ None => Value::Null,
53
158
  }
54
159
  }
55
160
 
56
- /// Build the Promise object with resolve, reject, all, race static methods.
161
+ /// Build the global `Promise` object: `__call` (constructor) + static methods.
57
162
  pub fn promise_object() -> Value {
58
163
  let mut map: ObjectMap = ObjectMap::default();
164
+
165
+ let ctor = Value::native(|args: &[Value]| match args.first() {
166
+ Some(Value::Function(f)) => {
167
+ let (tx, rx) = mpsc::channel();
168
+ let tx_cell = Arc::new(Mutex::new(Some(tx)));
169
+ let resolve = Value::native({
170
+ let tx_cell = Arc::clone(&tx_cell);
171
+ move |a: &[Value]| {
172
+ if let Some(t) = tx_cell.lock().unwrap().take() {
173
+ let _ = t.send(Ok(
174
+ a.first().cloned().unwrap_or(Value::Null),
175
+ ));
176
+ }
177
+ Value::Null
178
+ }
179
+ });
180
+ let reject = Value::native({
181
+ let tx_cell = Arc::clone(&tx_cell);
182
+ move |a: &[Value]| {
183
+ if let Some(t) = tx_cell.lock().unwrap().take() {
184
+ let _ = t.send(Err(
185
+ a.first().cloned().unwrap_or(Value::Null),
186
+ ));
187
+ }
188
+ Value::Null
189
+ }
190
+ });
191
+ let _ = f(&[resolve, reject]);
192
+ Value::Promise(Arc::new(DeferredChannelPromise {
193
+ rx: Mutex::new(Some(rx)),
194
+ }))
195
+ }
196
+ _ => Value::Null,
197
+ });
198
+
199
+ map.insert(Arc::from("__call"), ctor);
59
200
  map.insert(
60
201
  Arc::from("resolve"),
61
- Value::Function(Rc::new(|args: &[Value]| promise_resolve(args))),
202
+ Value::native(|args: &[Value]| promise_resolve(args)),
62
203
  );
63
204
  map.insert(
64
205
  Arc::from("reject"),
65
- Value::Function(Rc::new(|args: &[Value]| promise_reject(args))),
206
+ Value::native(|args: &[Value]| promise_reject(args)),
66
207
  );
67
208
  map.insert(
68
209
  Arc::from("all"),
69
- Value::Function(Rc::new(|args: &[Value]| promise_all(args))),
210
+ Value::native(|args: &[Value]| promise_all(args)),
70
211
  );
71
212
  map.insert(
72
213
  Arc::from("race"),
73
- Value::Function(Rc::new(|args: &[Value]| promise_race(args))),
214
+ Value::native(|args: &[Value]| promise_race(args)),
74
215
  );
75
- Value::Object(Rc::new(RefCell::new(map)))
216
+ Value::Object(VmRef::new(map))
217
+ }
218
+
219
+ /// `.then(onFulfilled, onRejected)` for a `Value::Promise` instance (VM `GetMember`).
220
+ pub fn promise_instance_then(p: &Arc<dyn TishPromise>, args: &[Value]) -> Value {
221
+ Value::Promise(Arc::new(ThenPromise {
222
+ pred: Arc::clone(p),
223
+ on_fulfilled: args.first().cloned(),
224
+ on_rejected: args.get(1).cloned(),
225
+ }))
226
+ }
227
+
228
+ /// `.catch(onRejected)` for a `Value::Promise` instance.
229
+ pub fn promise_instance_catch(p: &Arc<dyn TishPromise>, args: &[Value]) -> Value {
230
+ Value::Promise(Arc::new(ThenPromise {
231
+ pred: Arc::clone(p),
232
+ on_fulfilled: None,
233
+ on_rejected: args.first().cloned(),
234
+ }))
76
235
  }
@@ -1,6 +1,7 @@
1
1
  //! Promises carrying only Send payloads (string results for text(), etc.).
2
2
 
3
3
  use std::cell::RefCell;
4
+ use tishlang_core::VmRef;
4
5
  use std::rc::Rc;
5
6
  use std::sync::{Arc, Mutex};
6
7
  use tishlang_core::{ObjectMap, TishPromise, Value};
@@ -10,7 +11,7 @@ fn error_value(msg: String) -> Value {
10
11
  let mut obj: ObjectMap = ObjectMap::with_capacity(2);
11
12
  obj.insert(Arc::from("error"), Value::String(msg.into()));
12
13
  obj.insert(Arc::from("ok"), Value::Bool(false));
13
- Value::Object(Rc::new(RefCell::new(obj)))
14
+ Value::Object(VmRef::new(obj))
14
15
  }
15
16
 
16
17
  pub struct StringResultPromise {
@@ -1,4 +1,4 @@
1
- //! setTimeout, clearTimeout for compiled Tish and VM.
1
+ //! setTimeout, setInterval, clearTimeout, clearInterval for compiled Tish and VM.
2
2
  //! Callbacks run when blocking ops (e.g. ws.receiveTimeout) yield in their poll loop.
3
3
 
4
4
  use std::cell::RefCell;
@@ -35,11 +35,18 @@ fn extract_num(v: Option<&Value>) -> u64 {
35
35
 
36
36
  /// Sleep for ms, running due timers before sleeping. Use this instead of thread::sleep
37
37
  /// in blocking loops so setTimeout callbacks can fire.
38
+ #[allow(dead_code)] // Used by embedders with blocking poll loops; AppKit uses [`drain_timers`] instead.
38
39
  pub fn sleep_with_drain(ms: u64) {
39
40
  run_due_timers();
40
41
  std::thread::sleep(Duration::from_millis(ms));
41
42
  }
42
43
 
44
+ /// Run all due timer callbacks (e.g. from an AppKit / GUI event pump).
45
+ #[inline]
46
+ pub fn drain_timers() {
47
+ run_due_timers();
48
+ }
49
+
43
50
  /// Run all due timer callbacks.
44
51
  fn run_due_timers() {
45
52
  let due = take_due_timers();
@@ -109,6 +116,30 @@ pub fn set_timeout(args: &[Value]) -> Value {
109
116
  Value::Number(id as f64)
110
117
  }
111
118
 
119
+ /// setInterval(callback, intervalMs, ...args) — first run after `intervalMs`, then repeats.
120
+ pub fn set_interval(args: &[Value]) -> Value {
121
+ let callback = args.first().cloned().unwrap_or(Value::Null);
122
+ let interval_ms = extract_num(args.get(1)).min(3600_000);
123
+ let extra_args: Vec<Value> = args.iter().skip(2).cloned().collect();
124
+ if matches!(callback, Value::Null) {
125
+ return Value::Number(next_id() as f64);
126
+ }
127
+ let id = next_id();
128
+ let due = Instant::now() + Duration::from_millis(interval_ms);
129
+ REGISTRY.with(|r| {
130
+ r.borrow_mut().insert(
131
+ id,
132
+ TimerEntry {
133
+ due,
134
+ callback,
135
+ args: extra_args,
136
+ interval_ms,
137
+ },
138
+ );
139
+ });
140
+ Value::Number(id as f64)
141
+ }
142
+
112
143
  /// clearTimeout(id) - removes timer.
113
144
  pub fn clear_timeout(args: &[Value]) -> Value {
114
145
  let id = args
@@ -123,3 +154,8 @@ pub fn clear_timeout(args: &[Value]) -> Value {
123
154
  });
124
155
  Value::Null
125
156
  }
157
+
158
+ /// clearInterval(id) — same registry as clearTimeout.
159
+ pub fn clear_interval(args: &[Value]) -> Value {
160
+ clear_timeout(args)
161
+ }
@@ -6,6 +6,7 @@
6
6
  //! - **Broadcast** (Node pattern): `server.clients.forEach(ws => ws.send(data))` or iterate room conns and `wsSend(ws, data)` (same as `ws.send(data)`)
7
7
 
8
8
  use std::cell::RefCell;
9
+ use tishlang_core::VmRef;
9
10
  use std::collections::HashMap;
10
11
  use std::rc::Rc;
11
12
  use std::sync::atomic::{AtomicU32, Ordering};
@@ -16,8 +17,8 @@ use std::time::{Duration, Instant};
16
17
  use futures_util::{SinkExt, StreamExt};
17
18
  use lazy_static::lazy_static;
18
19
  use tishlang_core::{ObjectMap, Value};
19
- use tokio::sync::mpsc as tokio_mpsc;
20
20
  use tokio::runtime::Runtime;
21
+ use tokio::sync::mpsc as tokio_mpsc;
21
22
 
22
23
  thread_local! {
23
24
  /// Multi-thread runtime so `tokio::spawn` I/O tasks keep running after `block_on` returns.
@@ -199,39 +200,42 @@ fn conn_object(id: u32) -> Value {
199
200
  obj.insert(Arc::from("readyState"), Value::Number(1.0)); // OPEN
200
201
  obj.insert(
201
202
  Arc::from("send"),
202
- Value::Function(Rc::new(move |args: &[Value]| {
203
- let data = args.first().map(|v| v.to_display_string()).unwrap_or_default();
203
+ Value::native(move |args: &[Value]| {
204
+ let data = args
205
+ .first()
206
+ .map(|v| v.to_display_string())
207
+ .unwrap_or_default();
204
208
  Value::Bool(conn_send(id, data))
205
- })),
209
+ }),
206
210
  );
207
211
  obj.insert(
208
212
  Arc::from("close"),
209
- Value::Function(Rc::new(move |_args: &[Value]| {
213
+ Value::native(move |_args: &[Value]| {
210
214
  unregister(id);
211
215
  Value::Null
212
- })),
216
+ }),
213
217
  );
214
218
  obj.insert(
215
219
  Arc::from("receive"),
216
- Value::Function(Rc::new(move |_args: &[Value]| {
217
- match conn_receive(id) {
218
- Some(s) => {
219
- let mut ev: ObjectMap = ObjectMap::default();
220
- ev.insert(Arc::from("data"), Value::String(s.into()));
221
- Value::Object(Rc::new(RefCell::new(ev)))
222
- }
223
- None => Value::Null,
220
+ Value::native(move |_args: &[Value]| match conn_receive(id) {
221
+ Some(s) => {
222
+ let mut ev: ObjectMap = ObjectMap::default();
223
+ ev.insert(Arc::from("data"), Value::String(s.into()));
224
+ Value::Object(VmRef::new(ev))
224
225
  }
225
- })),
226
+ None => Value::Null,
227
+ }),
226
228
  );
227
229
  let id_timeout = id;
228
230
  obj.insert(
229
231
  Arc::from("receiveTimeout"),
230
- Value::Function(Rc::new(move |args: &[Value]| {
232
+ Value::native(move |args: &[Value]| {
231
233
  let timeout_ms = args
232
234
  .first()
233
235
  .and_then(|v| match v {
234
- Value::Number(n) if n.is_finite() && *n >= 0.0 => Some((*n as u64).min(3600_000)),
236
+ Value::Number(n) if n.is_finite() && *n >= 0.0 => {
237
+ Some((*n as u64).min(3600_000))
238
+ }
235
239
  _ => None,
236
240
  })
237
241
  .unwrap_or(1000);
@@ -239,13 +243,13 @@ fn conn_object(id: u32) -> Value {
239
243
  Some(s) => {
240
244
  let mut ev: ObjectMap = ObjectMap::default();
241
245
  ev.insert(Arc::from("data"), Value::String(s.into()));
242
- Value::Object(Rc::new(RefCell::new(ev)))
246
+ Value::Object(VmRef::new(ev))
243
247
  }
244
248
  None => Value::Null,
245
249
  }
246
- })),
250
+ }),
247
251
  );
248
- Value::Object(Rc::new(RefCell::new(obj)))
252
+ Value::Object(VmRef::new(obj))
249
253
  }
250
254
 
251
255
  fn parse_port(args: &[Value]) -> Option<u16> {
@@ -366,11 +370,17 @@ pub fn web_socket_server_listen(args: &[Value]) -> Value {
366
370
  };
367
371
  let ws_stream = match tokio_tungstenite::accept_async(stream).await {
368
372
  Ok(ws) => {
369
- eprintln!("[tish ws] server accepted connection (handshake OK): port {}", port);
373
+ eprintln!(
374
+ "[tish ws] server accepted connection (handshake OK): port {}",
375
+ port
376
+ );
370
377
  ws
371
378
  }
372
379
  Err(e) => {
373
- eprintln!("[tish ws] server accept_async failed: {} (port {})", e, port);
380
+ eprintln!(
381
+ "[tish ws] server accept_async failed: {} (port {})",
382
+ e, port
383
+ );
374
384
  continue;
375
385
  }
376
386
  };
@@ -463,9 +473,9 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
463
473
  }
464
474
 
465
475
  // Node.js-compatible: server.clients is array of connected WebSocket instances
466
- let clients: Rc<RefCell<Vec<Value>>> = Rc::new(RefCell::new(Vec::new()));
476
+ let clients: VmRef<Vec<Value>> = VmRef::new(Vec::new());
467
477
 
468
- let on_fn = Rc::new(|args: &[Value]| {
478
+ let on_fn = Value::native(|args: &[Value]| {
469
479
  let Some(Value::Object(so)) = args.first() else {
470
480
  return Value::Null;
471
481
  };
@@ -475,14 +485,13 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
475
485
  .unwrap_or_default();
476
486
  let cb = args.get(2).cloned().unwrap_or(Value::Null);
477
487
  if event == "connection" {
478
- so.borrow_mut()
479
- .insert(Arc::from("_onConnection"), cb);
488
+ so.borrow_mut().insert(Arc::from("_onConnection"), cb);
480
489
  }
481
490
  Value::Null
482
491
  });
483
492
 
484
- let clients_listen = Rc::clone(&clients);
485
- let listen_fn = Rc::new(move |args: &[Value]| {
493
+ let clients_listen = clients.clone();
494
+ let listen_fn = Value::native(move |args: &[Value]| {
486
495
  let Some(Value::Object(so)) = args.first() else {
487
496
  return Value::Null;
488
497
  };
@@ -511,8 +520,8 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
511
520
  Value::Null
512
521
  });
513
522
 
514
- let clients_accept = Rc::clone(&clients);
515
- let accept_timeout_fn = Rc::new(move |args: &[Value]| {
523
+ let clients_accept = clients.clone();
524
+ let accept_timeout_fn = Value::native(move |args: &[Value]| {
516
525
  let Some(Value::Object(so)) = args.first() else {
517
526
  return Value::Null;
518
527
  };
@@ -533,10 +542,10 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
533
542
  m.insert(Arc::from("_handle"), handle_val);
534
543
  m.insert(Arc::from("_onConnection"), Value::Null);
535
544
  m.insert(Arc::from("clients"), Value::Array(clients));
536
- m.insert(Arc::from("on"), Value::Function(on_fn));
537
- m.insert(Arc::from("listen"), Value::Function(listen_fn));
538
- m.insert(Arc::from("acceptTimeout"), Value::Function(accept_timeout_fn));
539
- Value::Object(Rc::new(RefCell::new(m)))
545
+ m.insert(Arc::from("on"), on_fn);
546
+ m.insert(Arc::from("listen"), listen_fn);
547
+ m.insert(Arc::from("acceptTimeout"), accept_timeout_fn);
548
+ Value::Object(VmRef::new(m))
540
549
  }
541
550
 
542
551
  #[cfg(test)]
@@ -551,7 +560,7 @@ mod tests {
551
560
  let opts = {
552
561
  let mut m: ObjectMap = ObjectMap::default();
553
562
  m.insert(Arc::from("port"), Value::Number(port as f64));
554
- Value::Object(Rc::new(RefCell::new(m)))
563
+ Value::Object(VmRef::new(m))
555
564
  };
556
565
 
557
566
  let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
@@ -578,7 +587,9 @@ mod tests {
578
587
  .unwrap_or_default(),
579
588
  _ => String::new(),
580
589
  };
581
- if let Some(Value::Function(sf)) = wso.borrow().get(&Arc::from("send")).cloned() {
590
+ if let Some(Value::Function(sf)) =
591
+ wso.borrow().get(&Arc::from("send")).cloned()
592
+ {
582
593
  let _ = sf(&[Value::String(data.into())]);
583
594
  }
584
595
  break;
@@ -634,7 +645,7 @@ mod tests {
634
645
  let opts = {
635
646
  let mut m: ObjectMap = ObjectMap::default();
636
647
  m.insert(Arc::from("port"), Value::Number(port as f64));
637
- Value::Object(Rc::new(RefCell::new(m)))
648
+ Value::Object(VmRef::new(m))
638
649
  };
639
650
 
640
651
  let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
@@ -666,8 +677,8 @@ mod tests {
666
677
  if data.contains("\"type\":\"join\"") || data.contains("\"type\": \"join\"") {
667
678
  let joined = r#"{"type":"joined","sessionId":"default"}"#;
668
679
  let presence = r#"{"type":"presence","agentLanes":["ai-a"]}"#;
669
- ws_send_native(&Value::Object(Rc::clone(&wso)), joined);
670
- ws_send_native(&Value::Object(Rc::clone(&wso)), presence);
680
+ ws_send_native(&Value::Object(wso.clone()), joined);
681
+ ws_send_native(&Value::Object(wso.clone()), presence);
671
682
  return;
672
683
  }
673
684
  }
@@ -692,7 +703,11 @@ mod tests {
692
703
  let _ = send_f(&[Value::String(join_msg.into())]);
693
704
 
694
705
  // Client uses receiveTimeout like the agent
695
- let recv_timeout = co.borrow().get(&Arc::from("receiveTimeout")).cloned().unwrap();
706
+ let recv_timeout = co
707
+ .borrow()
708
+ .get(&Arc::from("receiveTimeout"))
709
+ .cloned()
710
+ .unwrap();
696
711
  let Value::Function(recv_timeout_f) = recv_timeout else {
697
712
  panic!("no receiveTimeout");
698
713
  };
@@ -707,7 +722,11 @@ mod tests {
707
722
  .get(&Arc::from("data"))
708
723
  .map(|v| v.to_display_string())
709
724
  .unwrap_or_default();
710
- assert!(data1.contains("\"type\":\"joined\""), "expected joined, got {}", data1);
725
+ assert!(
726
+ data1.contains("\"type\":\"joined\""),
727
+ "expected joined, got {}",
728
+ data1
729
+ );
711
730
 
712
731
  let got2 = recv_timeout_f(&[timeout_arg]);
713
732
  let Value::Object(ev2) = got2 else {
@@ -718,7 +737,11 @@ mod tests {
718
737
  .get(&Arc::from("data"))
719
738
  .map(|v| v.to_display_string())
720
739
  .unwrap_or_default();
721
- assert!(data2.contains("\"type\":\"presence\""), "expected presence, got {}", data2);
740
+ assert!(
741
+ data2.contains("\"type\":\"presence\""),
742
+ "expected presence, got {}",
743
+ data2
744
+ );
722
745
 
723
746
  let _ = server.join();
724
747
  }
@@ -51,16 +51,17 @@ fn fetch_readable_stream_read_chunks() {
51
51
  Value::Object(o) => o.borrow(),
52
52
  _ => panic!("expected object response, got {:?}", resp),
53
53
  };
54
- assert!(obj.get(&std::sync::Arc::from("ok")).map(|v| matches!(v, Value::Bool(true))).unwrap_or(false));
54
+ assert!(obj
55
+ .get(&std::sync::Arc::from("ok"))
56
+ .map(|v| matches!(v, Value::Bool(true)))
57
+ .unwrap_or(false));
55
58
 
56
59
  let body = obj.get(&std::sync::Arc::from("body")).expect("body");
57
60
  let stream = match body {
58
61
  Value::Opaque(s) => s.as_ref(),
59
62
  _ => panic!("expected ReadableStream opaque"),
60
63
  };
61
- let reader_val = stream
62
- .get_method("getReader")
63
- .expect("getReader")(&[]);
64
+ let reader_val = stream.get_method("getReader").expect("getReader")(&[]);
64
65
  let reader = match reader_val {
65
66
  Value::Opaque(r) => r,
66
67
  _ => panic!("expected reader opaque, got {:?}", reader_val),