@tishlang/tish 1.7.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 (95) hide show
  1. package/Cargo.toml +1 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +15 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_resolve/Cargo.toml +13 -0
  64. package/crates/tish_resolve/src/lib.rs +3436 -0
  65. package/crates/tish_resolve/src/pos.rs +133 -0
  66. package/crates/tish_runtime/Cargo.toml +68 -3
  67. package/crates/tish_runtime/src/http.rs +1123 -141
  68. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  69. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  70. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  71. package/crates/tish_runtime/src/lib.rs +159 -29
  72. package/crates/tish_runtime/src/promise.rs +199 -36
  73. package/crates/tish_runtime/src/promise_io.rs +2 -1
  74. package/crates/tish_runtime/src/timers.rs +37 -1
  75. package/crates/tish_runtime/src/ws.rs +26 -28
  76. package/crates/tish_ui/src/jsx.rs +279 -8
  77. package/crates/tish_ui/src/lib.rs +5 -2
  78. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  79. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  80. package/crates/tish_vm/Cargo.toml +15 -5
  81. package/crates/tish_vm/src/vm.rs +506 -259
  82. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  83. package/crates/tish_wasm/src/lib.rs +17 -14
  84. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  85. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  86. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  87. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  88. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  89. package/justfile +8 -0
  90. package/package.json +1 -1
  91. package/platform/darwin-arm64/tish +0 -0
  92. package/platform/darwin-x64/tish +0 -0
  93. package/platform/linux-arm64/tish +0 -0
  94. package/platform/linux-x64/tish +0 -0
  95. package/platform/win32-x64/tish.exe +0 -0
@@ -1,82 +1,423 @@
1
- //! Minimal hook state: `useState` + render flush (Lattish-style cursor reset).
1
+ //! Minimal hook state: `useState`, `useMemo`, and render flush (Lattish-style cursor reset).
2
+ //! Supports multiple independent roots (`RootId`) in one thread.
2
3
 
3
4
  use std::cell::{Cell, RefCell};
5
+ use std::collections::HashMap;
4
6
  use std::rc::Rc;
5
7
 
6
- use tishlang_core::{ObjectMap, Value};
8
+ use tishlang_core::{ObjectMap, Value, VmRef};
7
9
 
8
- use super::ACTIVE_HOST;
10
+ use super::Host;
11
+
12
+ /// Opaque id for one `createRoot().render(App)` tree in this thread.
13
+ pub type RootId = u64;
14
+
15
+ /// First root: `install_thread_local_host` and `native_create_root` without an id argument.
16
+ pub const LEGACY_ROOT_ID: RootId = 1;
9
17
 
10
18
  thread_local! {
11
- pub static HOOK: RefCell<HookState> = RefCell::new(HookState::default());
19
+ static HOOKS: RefCell<HashMap<RootId, HookState>> = RefCell::new(HashMap::new());
20
+ static CURRENT_ROOT: Cell<Option<RootId>> = Cell::new(None);
21
+ static HOSTS: RefCell<HashMap<RootId, Box<dyn Host>>> = RefCell::new(HashMap::new());
22
+ static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = Cell::new(2);
12
23
  static IN_FLUSH: Cell<bool> = Cell::new(false);
13
24
  }
14
25
 
26
+ /// Allocate an id for an additional in-process window (starts at 2; 1 is legacy primary).
27
+ pub fn alloc_root_id() -> RootId {
28
+ NEXT_DYNAMIC_ROOT_ID.with(|n| {
29
+ let id = n.get();
30
+ n.set(id.saturating_add(1).max(2));
31
+ id
32
+ })
33
+ }
34
+
35
+ fn ensure_hook_entry(root_id: RootId) {
36
+ HOOKS.with(|h| {
37
+ h.borrow_mut().entry(root_id).or_default();
38
+ });
39
+ }
40
+
41
+ /// Install the host for a specific root. Replaces any previous host for that id.
42
+ pub fn install_host_for_root(root_id: RootId, host: Box<dyn Host>) {
43
+ ensure_hook_entry(root_id);
44
+ HOSTS.with(|h| {
45
+ h.borrow_mut().insert(root_id, host);
46
+ });
47
+ }
48
+
49
+ /// Legacy: install host for [`LEGACY_ROOT_ID`] (same as `macos.run` / single-window tools).
50
+ #[allow(dead_code)] // Emitted Rust / hosts call via `tishlang_ui` re-exports; unused inside this crate.
51
+ pub fn install_thread_local_host(host: Box<dyn Host>) {
52
+ install_host_for_root(LEGACY_ROOT_ID, host);
53
+ }
54
+
55
+ /// Remove hook state and run effect cleanups. Safe to call while the host still exists (e.g. before
56
+ /// dropping AppKit objects that might receive async callbacks).
57
+ pub fn unregister_root_hooks_and_effects(root_id: RootId) {
58
+ HOOKS.with(|h| {
59
+ if let Some(st) = h.borrow_mut().remove(&root_id) {
60
+ run_all_effect_cleanups(st.effect_cells.as_ref());
61
+ }
62
+ });
63
+ }
64
+
65
+ /// Drop the [`Host`] for `root_id`. Defer after `windowWillClose:` returns when the host retains the
66
+ /// window delegate object that is still executing that callback.
67
+ pub fn drop_host_for_root(root_id: RootId) {
68
+ HOSTS.with(|h| {
69
+ h.borrow_mut().remove(&root_id);
70
+ });
71
+ }
72
+
73
+ pub fn unregister_root(root_id: RootId) {
74
+ unregister_root_hooks_and_effects(root_id);
75
+ drop_host_for_root(root_id);
76
+ }
77
+
78
+ pub fn with_host_for_root<R>(root_id: RootId, f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
79
+ HOSTS.with(|c| {
80
+ let mut m = c.borrow_mut();
81
+ m.get_mut(&root_id).map(|host| f(host.as_mut()))
82
+ })
83
+ }
84
+
85
+ /// Prefer [`with_host_for_root`]; kept for call sites that assume a single primary root.
86
+ #[allow(dead_code)]
87
+ pub fn with_thread_local_host<R>(f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
88
+ with_host_for_root(LEGACY_ROOT_ID, f)
89
+ }
90
+
91
+ /// Root currently rendering or running hook flush (`None` outside that scope).
92
+ pub fn current_root_id() -> Option<RootId> {
93
+ CURRENT_ROOT.get()
94
+ }
95
+
96
+ /// Sets [`CURRENT_ROOT`] for the duration of `f` so `window.*` and similar APIs target this tree.
97
+ pub fn run_with_current_root<R>(root_id: RootId, f: impl FnOnce() -> R) -> R {
98
+ let prev = CURRENT_ROOT.get();
99
+ CURRENT_ROOT.set(Some(root_id));
100
+ let out = f();
101
+ CURRENT_ROOT.set(prev);
102
+ out
103
+ }
104
+
105
+ /// One `useEffect` slot: committed dependency snapshot and optional cleanup from the last run.
15
106
  #[derive(Default)]
107
+ struct EffectCell {
108
+ committed_deps: Option<Vec<Value>>,
109
+ cleanup: Option<Value>,
110
+ }
111
+
112
+ struct PendingEffect {
113
+ slot: usize,
114
+ effect_fn: Value,
115
+ new_deps: Vec<Value>,
116
+ }
117
+
118
+ /// Hook storage for one `createRoot().render(App)` tree.
16
119
  pub struct HookState {
17
120
  pub state_slots: Rc<RefCell<Vec<Value>>>,
18
121
  pub cursor: usize,
19
122
  pub root_app: Option<Value>,
20
123
  pub root_vnode: Option<Value>,
21
124
  pub flush_scheduled: bool,
125
+ /// Per-slot: last dependency tuple snapshot and cached value from `useMemo`.
126
+ pub memo_cache: Rc<RefCell<Vec<Option<(Vec<Value>, Value)>>>>,
127
+ pub memo_cursor: usize,
128
+ effect_cells: Rc<RefCell<Vec<EffectCell>>>,
129
+ effect_cursor: usize,
130
+ pending_effects: Rc<RefCell<Vec<PendingEffect>>>,
131
+ }
132
+
133
+ impl Default for HookState {
134
+ fn default() -> Self {
135
+ Self {
136
+ state_slots: Rc::new(RefCell::new(Vec::new())),
137
+ cursor: 0,
138
+ root_app: None,
139
+ root_vnode: None,
140
+ flush_scheduled: false,
141
+ memo_cache: Rc::new(RefCell::new(Vec::new())),
142
+ memo_cursor: 0,
143
+ effect_cells: Rc::new(RefCell::new(Vec::new())),
144
+ effect_cursor: 0,
145
+ pending_effects: Rc::new(RefCell::new(Vec::new())),
146
+ }
147
+ }
148
+ }
149
+
150
+ fn run_all_effect_cleanups(cells: &RefCell<Vec<EffectCell>>) {
151
+ for cell in cells.borrow_mut().iter_mut() {
152
+ if let Some(c) = cell.cleanup.take() {
153
+ if let Value::Function(f) = c {
154
+ let _ = f(&[]);
155
+ }
156
+ }
157
+ }
22
158
  }
23
159
 
24
160
  impl HookState {
25
161
  pub fn reset_for_new_root(&mut self) {
162
+ run_all_effect_cleanups(self.effect_cells.as_ref());
163
+ self.effect_cells.borrow_mut().clear();
164
+ self.effect_cursor = 0;
165
+ self.pending_effects.borrow_mut().clear();
26
166
  self.state_slots.borrow_mut().clear();
27
167
  self.cursor = 0;
28
168
  self.root_vnode = None;
29
169
  self.flush_scheduled = false;
170
+ self.memo_cache.borrow_mut().clear();
171
+ self.memo_cursor = 0;
30
172
  }
31
173
  }
32
174
 
175
+ fn memo_dep_eq(a: &Value, b: &Value) -> bool {
176
+ match (a, b) {
177
+ (Value::Number(x), Value::Number(y)) => {
178
+ if x.is_nan() && y.is_nan() {
179
+ return true;
180
+ }
181
+ x == y
182
+ }
183
+ (Value::String(x), Value::String(y)) => x == y,
184
+ (Value::Bool(x), Value::Bool(y)) => x == y,
185
+ (Value::Null, Value::Null) => true,
186
+ (Value::Array(ax), Value::Array(bx)) => {
187
+ let ab = ax.borrow();
188
+ let bb = bx.borrow();
189
+ if ab.len() != bb.len() {
190
+ return false;
191
+ }
192
+ ab.iter()
193
+ .zip(bb.iter())
194
+ .all(|(x, y)| memo_dep_eq(x, y))
195
+ }
196
+ _ => false,
197
+ }
198
+ }
199
+
200
+ fn memo_deps_unchanged(prev: &[Value], next: &[Value]) -> bool {
201
+ prev.len() == next.len()
202
+ && prev
203
+ .iter()
204
+ .zip(next.iter())
205
+ .all(|(a, b)| memo_dep_eq(a, b))
206
+ }
207
+
208
+ fn root_id_for_hooks() -> RootId {
209
+ CURRENT_ROOT.get().unwrap_or(LEGACY_ROOT_ID)
210
+ }
211
+
33
212
  /// `useState(initial)` → `[state, setState]` as a Tish array.
34
213
  pub fn native_use_state(args: &[Value]) -> Value {
35
214
  let initial = args.first().cloned().unwrap_or(Value::Null);
36
- HOOK.with(|h| {
37
- let mut st = h.borrow_mut();
215
+ let root_id = root_id_for_hooks();
216
+ HOOKS.with(|h| {
217
+ let mut map = h.borrow_mut();
218
+ let st = map.entry(root_id).or_default();
38
219
  let i = st.cursor;
39
220
  st.cursor += 1;
40
- let slots = Rc::clone(&st.state_slots);
221
+ let slots = &st.state_slots.clone();
41
222
  while i >= slots.borrow().len() {
42
223
  slots.borrow_mut().push(initial.clone());
43
224
  }
44
225
  let current = slots.borrow()[i].clone();
45
226
  let idx = i;
46
- let setter = Value::Function(Rc::new(move |a: &[Value]| {
47
- let new_v = a.first().cloned().unwrap_or(Value::Null);
48
- slots.borrow_mut()[idx] = new_v;
49
- schedule_flush();
227
+ let setter = Value::native(move |a: &[Value]| {
228
+ let arg = a.first().cloned().unwrap_or(Value::Null);
229
+ HOOKS.with(|hooks| {
230
+ if let Some(st) = hooks.borrow_mut().get_mut(&root_id) {
231
+ let slot_len = st.state_slots.borrow().len();
232
+ if idx >= slot_len {
233
+ st.flush_scheduled = true;
234
+ return;
235
+ }
236
+ let prev = st.state_slots.borrow()[idx].clone();
237
+ let new_v = match &arg {
238
+ Value::Function(f) => f(&[prev.clone()]),
239
+ _ => arg.clone(),
240
+ };
241
+ if memo_dep_eq(&prev, &new_v) {
242
+ return;
243
+ }
244
+ st.state_slots.borrow_mut()[idx] = new_v;
245
+ st.flush_scheduled = true;
246
+ }
247
+ });
248
+ if !IN_FLUSH.get() {
249
+ drain_flush_queue();
250
+ }
50
251
  Value::Null
51
- }));
52
- Value::Array(Rc::new(RefCell::new(vec![current, setter])))
252
+ });
253
+ Value::Array(VmRef::new(vec![current, setter]))
254
+ })
255
+ }
256
+
257
+ /// `useMemo(factory, deps?)` — caches `factory()` until `deps` changes (shallow compare per slot).
258
+ pub fn native_use_memo(args: &[Value]) -> Value {
259
+ let Some(Value::Function(factory)) = args.first() else {
260
+ return Value::Null;
261
+ };
262
+ let factory = factory.clone();
263
+ let deps: Vec<Value> = match args.get(1) {
264
+ Some(Value::Array(a)) => a.borrow().clone(),
265
+ Some(other) => vec![other.clone()],
266
+ None => vec![],
267
+ };
268
+
269
+ let root_id = root_id_for_hooks();
270
+ HOOKS.with(|h| {
271
+ let mut map = h.borrow_mut();
272
+ let st = map.entry(root_id).or_default();
273
+ let i = st.memo_cursor;
274
+ st.memo_cursor += 1;
275
+ let cache = &st.memo_cache.clone();
276
+ let mut c = cache.borrow_mut();
277
+ while c.len() <= i {
278
+ c.push(None);
279
+ }
280
+ let reuse = match &c[i] {
281
+ Some((old_deps, _)) => memo_deps_unchanged(old_deps, &deps),
282
+ None => false,
283
+ };
284
+ if reuse {
285
+ return c[i].as_ref().unwrap().1.clone();
286
+ }
287
+ let produced = factory(&[]);
288
+ c[i] = Some((deps, produced.clone()));
289
+ produced
53
290
  })
54
291
  }
55
292
 
56
- /// `createRoot(container)` `{ render: (App) => { ... } }` (container ignored for headless native).
293
+ /// `useEffect(effect, deps?)` runs `effect` after the host commits the tree; compares `deps` like `useMemo`.
294
+ /// If `effect` returns a function, it is called before the next run or on root teardown (`render` replacement / [`unregister_root`]).
295
+ pub fn native_use_effect(args: &[Value]) -> Value {
296
+ let Some(Value::Function(effect_fn)) = args.first() else {
297
+ return Value::Null;
298
+ };
299
+ let effect_fn = effect_fn.clone();
300
+ let deps: Vec<Value> = match args.get(1) {
301
+ Some(Value::Array(a)) => a.borrow().clone(),
302
+ Some(other) => vec![other.clone()],
303
+ None => vec![],
304
+ };
305
+
306
+ let root_id = root_id_for_hooks();
307
+ HOOKS.with(|h| {
308
+ let mut map = h.borrow_mut();
309
+ let st = map.entry(root_id).or_default();
310
+ let i = st.effect_cursor;
311
+ st.effect_cursor += 1;
312
+ let cells = &st.effect_cells.clone();
313
+ let mut cells_b = cells.borrow_mut();
314
+ while cells_b.len() <= i {
315
+ cells_b.push(EffectCell::default());
316
+ }
317
+ let should_run = match &cells_b[i].committed_deps {
318
+ None => true,
319
+ Some(old) => !memo_deps_unchanged(old, &deps),
320
+ };
321
+ drop(cells_b);
322
+
323
+ if should_run {
324
+ st.pending_effects.borrow_mut().push(PendingEffect {
325
+ slot: i,
326
+ effect_fn: Value::Function(effect_fn),
327
+ new_deps: deps,
328
+ });
329
+ }
330
+ Value::Null
331
+ })
332
+ }
333
+
334
+ fn flush_pending_effects(root_id: RootId) {
335
+ let pending: Vec<PendingEffect> = HOOKS.with(|h| {
336
+ h.borrow_mut()
337
+ .get_mut(&root_id)
338
+ .map(|st| std::mem::take(&mut *st.pending_effects.borrow_mut()))
339
+ .unwrap_or_default()
340
+ });
341
+ let cells_rc = HOOKS.with(|h| {
342
+ h.borrow()
343
+ .get(&root_id)
344
+ .map(|st| st.effect_cells.clone())
345
+ });
346
+ let Some(cells_rc) = cells_rc else {
347
+ return;
348
+ };
349
+
350
+ for p in pending {
351
+ // Never hold `effect_cells` across user cleanup/effect: they may call `setState` →
352
+ // `drain_flush_queue` → `native_use_effect`, which must `borrow_mut` the same RefCell.
353
+ let cleanup_fn = {
354
+ let mut cells = cells_rc.borrow_mut();
355
+ while cells.len() <= p.slot {
356
+ cells.push(EffectCell::default());
357
+ }
358
+ cells[p.slot].cleanup.take()
359
+ };
360
+ if let Some(Value::Function(f)) = cleanup_fn {
361
+ let _ = f(&[]);
362
+ }
363
+ let run_result = if let Value::Function(f) = &p.effect_fn {
364
+ f(&[])
365
+ } else {
366
+ Value::Null
367
+ };
368
+ {
369
+ let mut cells = cells_rc.borrow_mut();
370
+ while cells.len() <= p.slot {
371
+ cells.push(EffectCell::default());
372
+ }
373
+ let cell = &mut cells[p.slot];
374
+ cell.cleanup = match run_result {
375
+ Value::Function(f) => Some(Value::Function(f)),
376
+ _ => None,
377
+ };
378
+ cell.committed_deps = Some(p.new_deps);
379
+ }
380
+ }
381
+ }
382
+
383
+ fn parse_root_id_arg(args: &[Value]) -> RootId {
384
+ match args.first() {
385
+ Some(Value::Number(n)) if n.is_finite() && *n >= 1.0 && n.fract() == 0.0 => *n as u64,
386
+ _ => LEGACY_ROOT_ID,
387
+ }
388
+ }
389
+
390
+ /// `createRoot(container?)` or `createRoot(rootId)` → `{ render: (App) => { ... } }`.
391
+ /// Pass a positive integer as the first argument to bind this root to a host installed via
392
+ /// [`install_host_for_root`].
57
393
  pub fn native_create_root(args: &[Value]) -> Value {
58
- let _container = args.first();
59
- let render_fn = Value::Function(Rc::new(|app_args: &[Value]| {
394
+ let root_id = parse_root_id_arg(args);
395
+ ensure_hook_entry(root_id);
396
+ let render_fn = Value::native(move |app_args: &[Value]| {
60
397
  let app = app_args.first().cloned().unwrap_or(Value::Null);
61
- HOOK.with(|h| {
62
- let mut st = h.borrow_mut();
398
+ HOOKS.with(|h| {
399
+ let mut map = h.borrow_mut();
400
+ let st = map.entry(root_id).or_default();
63
401
  st.reset_for_new_root();
64
402
  st.root_app = Some(app);
65
403
  st.flush_scheduled = true;
66
404
  });
67
405
  drain_flush_queue();
68
406
  Value::Null
69
- }));
70
- Value::Object(Rc::new(RefCell::new(ObjectMap::from([(
407
+ });
408
+ Value::Object(VmRef::new(ObjectMap::from([(
71
409
  std::sync::Arc::from("render"),
72
410
  render_fn,
73
- )]))))
411
+ )])))
74
412
  }
75
413
 
76
414
  /// Request a re-render (coalesced; safe if called during flush).
77
415
  pub fn schedule_flush() {
78
- HOOK.with(|h| {
79
- h.borrow_mut().flush_scheduled = true;
416
+ let root_id = root_id_for_hooks();
417
+ HOOKS.with(|h| {
418
+ if let Some(st) = h.borrow_mut().get_mut(&root_id) {
419
+ st.flush_scheduled = true;
420
+ }
80
421
  });
81
422
  if IN_FLUSH.get() {
82
423
  return;
@@ -86,37 +427,57 @@ pub fn schedule_flush() {
86
427
 
87
428
  fn drain_flush_queue() {
88
429
  loop {
89
- let run = HOOK.with(|h| {
90
- let mut st = h.borrow_mut();
91
- if st.flush_scheduled {
92
- st.flush_scheduled = false;
93
- true
94
- } else {
95
- false
96
- }
430
+ let root_id = HOOKS.with(|h| {
431
+ h.borrow()
432
+ .iter()
433
+ .find(|(_, st)| st.flush_scheduled)
434
+ .map(|(id, _)| *id)
97
435
  });
98
- if !run {
436
+ let Some(root_id) = root_id else {
99
437
  break;
100
- }
438
+ };
439
+
101
440
  IN_FLUSH.set(true);
102
- HOOK.with(|h| {
103
- let mut st = h.borrow_mut();
441
+ CURRENT_ROOT.set(Some(root_id));
442
+ HOOKS.with(|h| {
443
+ if let Some(st) = h.borrow_mut().get_mut(&root_id) {
444
+ st.flush_scheduled = false;
445
+ }
446
+ });
447
+
448
+ let app_fn = HOOKS.with(|h| {
449
+ let mut map = h.borrow_mut();
450
+ let st = map.get_mut(&root_id)?;
104
451
  st.cursor = 0;
105
- let Some(app) = st.root_app.clone() else {
106
- return;
107
- };
452
+ st.memo_cursor = 0;
453
+ st.effect_cursor = 0;
454
+ st.pending_effects.borrow_mut().clear();
455
+ let app = st.root_app.clone()?;
108
456
  let Value::Function(f) = app else {
109
- return;
457
+ return None;
110
458
  };
459
+ Some(f)
460
+ });
461
+
462
+ if let Some(f) = app_fn {
111
463
  let tree = f(&[]);
112
- st.root_vnode = Some(tree.clone());
113
- ACTIVE_HOST.with(|host_cell| {
114
- let mut host_opt = host_cell.borrow_mut();
115
- if let Some(host) = host_opt.as_deref_mut() {
116
- host.commit_root(&tree);
464
+ HOOKS.with(|h| {
465
+ let mut map = h.borrow_mut();
466
+ if let Some(st) = map.get_mut(&root_id) {
467
+ st.root_vnode = Some(tree.clone());
468
+ HOSTS.with(|hosts| {
469
+ let mut hm = hosts.borrow_mut();
470
+ if let Some(host) = hm.get_mut(&root_id) {
471
+ host.commit_root(&tree);
472
+ }
473
+ });
117
474
  }
118
475
  });
119
- });
476
+ IN_FLUSH.set(false);
477
+ flush_pending_effects(root_id);
478
+ }
479
+
480
+ CURRENT_ROOT.set(None);
120
481
  IN_FLUSH.set(false);
121
482
  }
122
483
  }
@@ -6,9 +6,14 @@ use std::cell::RefCell;
6
6
  use std::rc::Rc;
7
7
  use std::sync::Arc;
8
8
 
9
- pub use hooks::{native_create_root, native_use_state, schedule_flush, HookState, HOOK};
9
+ pub use hooks::{
10
+ alloc_root_id, current_root_id, drop_host_for_root, install_host_for_root, native_create_root,
11
+ native_use_effect, native_use_memo, native_use_state, run_with_current_root, schedule_flush,
12
+ unregister_root, unregister_root_hooks_and_effects, with_host_for_root, HookState,
13
+ LEGACY_ROOT_ID, RootId,
14
+ };
10
15
 
11
- use tishlang_core::{ObjectMap, Value};
16
+ use tishlang_core::{ObjectMap, Value, VmRef};
12
17
 
13
18
  /// Sentinel string for `Fragment` (native). JS/Lattish uses `Symbol`; hosts compare via equality.
14
19
  pub const FRAGMENT_SENTINEL: &str = "__tish_ui_Fragment__";
@@ -51,10 +56,10 @@ pub fn ui_h(args: &[Value]) -> Value {
51
56
  if !children_vec.is_empty() {
52
57
  merged.insert(
53
58
  Arc::from("children"),
54
- Value::Array(Rc::new(RefCell::new(children_vec.clone()))),
59
+ Value::Array(VmRef::new(children_vec.clone())),
55
60
  );
56
61
  }
57
- return f(&[Value::Object(Rc::new(RefCell::new(merged)))]);
62
+ return f(&[Value::Object(VmRef::new(merged))]);
58
63
  }
59
64
 
60
65
  if is_fragment_tag(&tag) {
@@ -72,11 +77,25 @@ pub fn ui_h(args: &[Value]) -> Value {
72
77
  fn normalize_children_list(children_arg: Value) -> Vec<Value> {
73
78
  match children_arg {
74
79
  Value::Null => vec![],
75
- Value::Array(a) => a.borrow().clone(),
80
+ Value::Array(a) => flatten_vnode_children(&a.borrow()),
76
81
  other => vec![other],
77
82
  }
78
83
  }
79
84
 
85
+ /// JSX often passes `{items.map(...)}` as one slot in the children array. Treat nested `Value::Array`
86
+ /// like React: splice them into the parent list so hosts never see a raw array vnode.
87
+ fn flatten_vnode_children(items: &[Value]) -> Vec<Value> {
88
+ let mut out = Vec::new();
89
+ for c in items {
90
+ match c {
91
+ Value::Array(inner) => out.extend(flatten_vnode_children(&inner.borrow())),
92
+ Value::Null => {}
93
+ _ => out.push(c.clone()),
94
+ }
95
+ }
96
+ out
97
+ }
98
+
80
99
  fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
81
100
  let mut m = ObjectMap::default();
82
101
  m.insert(Arc::from("tag"), Value::String(tag));
@@ -90,10 +109,10 @@ fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
90
109
  );
91
110
  m.insert(
92
111
  Arc::from("children"),
93
- Value::Array(Rc::new(RefCell::new(children))),
112
+ Value::Array(VmRef::new(children)),
94
113
  );
95
114
  m.insert(Arc::from("_el"), Value::Null);
96
- Value::Object(Rc::new(RefCell::new(m)))
115
+ Value::Object(VmRef::new(m))
97
116
  }
98
117
 
99
118
  fn vnode_fragment(children: Vec<Value>) -> Value {
@@ -102,16 +121,24 @@ fn vnode_fragment(children: Vec<Value>) -> Value {
102
121
  m.insert(Arc::from("props"), Value::Null);
103
122
  m.insert(
104
123
  Arc::from("children"),
105
- Value::Array(Rc::new(RefCell::new(children))),
124
+ Value::Array(VmRef::new(children)),
106
125
  );
107
126
  m.insert(Arc::from("_el"), Value::Null);
108
- Value::Object(Rc::new(RefCell::new(m)))
127
+ Value::Object(VmRef::new(m))
109
128
  }
110
129
 
111
130
  /// Pluggable UI backend (Floem, DOM, SwiftUI, …). Main-thread / single-threaded by default.
112
131
  pub trait Host {
113
132
  /// Apply a new root vnode (after each render flush).
114
133
  fn commit_root(&mut self, vnode: &Value);
134
+ /// Content area width changed (e.g. window resize); default no-op.
135
+ fn content_width_changed(&mut self, _width: f64) {}
136
+ /// Called once from the main queue shortly after the window is ordered on-screen. Split /
137
+ /// sidebar hosts can use this to re-layout when pane bounds were still provisional during the
138
+ /// first commit.
139
+ fn after_window_shown(&mut self) {}
140
+ /// Clear native control target/action (and similar) before the host is dropped — e.g. window close.
141
+ fn detach_native_actions(&mut self) {}
115
142
  }
116
143
 
117
144
  /// No-op / test host that only stores the last committed tree.
@@ -9,13 +9,23 @@ repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
11
  regex = ["tishlang_core/regex"]
12
+ # Propagate `send-values` so that every native-function closure we build
13
+ # in the VM (array / string / object methods) picks up the right
14
+ # `Rc<dyn Fn>` vs `Arc<dyn Fn + Send + Sync>` wrapper at compile time.
15
+ send-values = ["tishlang_core/send-values", "tishlang_builtins/send-values"]
12
16
  # For wasm32 target: use web_sys console for output
13
17
  wasm = ["dep:wasm-bindgen"]
14
- # Built-in modules: fs, http, process (for bytecode LoadNativeExport)
15
- fs = ["tishlang_runtime/fs"]
16
- process = ["tishlang_runtime/process"]
17
- http = ["tishlang_runtime/http"]
18
- ws = ["tishlang_runtime/ws"]
18
+ # Built-in modules: fs, http, process (for bytecode LoadNativeExport).
19
+ # `dep:tishlang_runtime` is required so enabling e.g. `http` always activates the optional runtime crate
20
+ # (some Cargo/cache combinations only saw `tishlang_runtime/http` and built VM stubs without http).
21
+ fs = ["dep:tishlang_runtime", "tishlang_runtime/fs"]
22
+ process = ["dep:tishlang_runtime", "tishlang_runtime/process"]
23
+ # Timer globals + `tish:timers` LoadNativeExport (uses tishlang_runtime TLS timer registry).
24
+ timers = ["dep:tishlang_runtime"]
25
+ # Any HTTP build needs Send-safe values so handlers can be dispatched
26
+ # across worker threads or processes. HTTP implies timers (fetch/Promise often pair with setTimeout).
27
+ http = ["dep:tishlang_runtime", "tishlang_runtime/http", "send-values", "timers"]
28
+ ws = ["dep:tishlang_runtime", "tishlang_runtime/ws"]
19
29
 
20
30
  [dependencies]
21
31
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }