@tishlang/tish 1.9.2 → 1.12.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 (84) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +8 -6
  3. package/crates/js_to_tish/src/transform/stmt.rs +12 -13
  4. package/crates/tish/Cargo.toml +1 -1
  5. package/crates/tish/src/cargo_native_registry.rs +4 -1
  6. package/crates/tish/src/cli_help.rs +9 -1
  7. package/crates/tish/src/main.rs +66 -11
  8. package/crates/tish/tests/integration_test.rs +145 -7
  9. package/crates/tish_ast/src/ast.rs +3 -9
  10. package/crates/tish_build_utils/src/lib.rs +74 -23
  11. package/crates/tish_builtins/src/array.rs +2 -3
  12. package/crates/tish_builtins/src/construct.rs +15 -28
  13. package/crates/tish_builtins/src/globals.rs +18 -16
  14. package/crates/tish_builtins/src/helpers.rs +1 -4
  15. package/crates/tish_builtins/src/lib.rs +1 -0
  16. package/crates/tish_builtins/src/math.rs +7 -0
  17. package/crates/tish_builtins/src/object.rs +10 -10
  18. package/crates/tish_builtins/src/string.rs +27 -3
  19. package/crates/tish_builtins/src/symbol.rs +83 -0
  20. package/crates/tish_compile/src/codegen.rs +324 -158
  21. package/crates/tish_compile/src/lib.rs +39 -7
  22. package/crates/tish_compile/src/resolve.rs +191 -6
  23. package/crates/tish_compile/src/types.rs +6 -6
  24. package/crates/tish_compile_js/src/codegen.rs +8 -5
  25. package/crates/tish_core/src/console_style.rs +9 -0
  26. package/crates/tish_core/src/json.rs +17 -7
  27. package/crates/tish_core/src/macros.rs +2 -2
  28. package/crates/tish_core/src/value.rs +213 -4
  29. package/crates/tish_cranelift/src/link.rs +1 -1
  30. package/crates/tish_cranelift_runtime/Cargo.toml +4 -0
  31. package/crates/tish_eval/src/eval.rs +135 -73
  32. package/crates/tish_eval/src/http.rs +18 -12
  33. package/crates/tish_eval/src/lib.rs +29 -0
  34. package/crates/tish_eval/src/regex.rs +1 -1
  35. package/crates/tish_eval/src/value.rs +89 -4
  36. package/crates/tish_eval/src/value_convert.rs +30 -8
  37. package/crates/tish_fmt/src/lib.rs +4 -1
  38. package/crates/tish_lexer/src/lib.rs +7 -2
  39. package/crates/tish_llvm/src/lib.rs +2 -2
  40. package/crates/tish_lsp/src/builtin_goto.rs +111 -10
  41. package/crates/tish_lsp/src/import_goto.rs +35 -22
  42. package/crates/tish_lsp/src/main.rs +118 -85
  43. package/crates/tish_native/src/build.rs +270 -24
  44. package/crates/tish_native/src/config.rs +48 -0
  45. package/crates/tish_native/src/lib.rs +139 -12
  46. package/crates/tish_parser/src/lib.rs +5 -2
  47. package/crates/tish_parser/src/parser.rs +45 -75
  48. package/crates/tish_pg/src/error.rs +1 -1
  49. package/crates/tish_pg/src/lib.rs +61 -73
  50. package/crates/tish_resolve/src/lib.rs +283 -158
  51. package/crates/tish_resolve/src/pos.rs +10 -2
  52. package/crates/tish_runtime/Cargo.toml +3 -0
  53. package/crates/tish_runtime/src/http.rs +39 -39
  54. package/crates/tish_runtime/src/http_fetch.rs +12 -12
  55. package/crates/tish_runtime/src/lib.rs +35 -44
  56. package/crates/tish_runtime/src/native_promise.rs +0 -11
  57. package/crates/tish_runtime/src/promise.rs +14 -1
  58. package/crates/tish_runtime/src/promise_io.rs +1 -4
  59. package/crates/tish_runtime/src/timers.rs +12 -7
  60. package/crates/tish_runtime/src/ws.rs +40 -27
  61. package/crates/tish_runtime/tests/fetch_readable_stream.rs +10 -8
  62. package/crates/tish_ui/src/jsx.rs +6 -4
  63. package/crates/tish_ui/src/lib.rs +5 -4
  64. package/crates/tish_ui/src/runtime/hooks.rs +123 -37
  65. package/crates/tish_ui/src/runtime/mod.rs +21 -41
  66. package/crates/tish_vm/Cargo.toml +2 -0
  67. package/crates/tish_vm/src/vm.rs +258 -153
  68. package/crates/tish_wasm/src/lib.rs +60 -7
  69. package/crates/tish_wasm_runtime/Cargo.toml +10 -1
  70. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  71. package/crates/tish_wasm_runtime/src/lib.rs +7 -1
  72. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  73. package/crates/tishlang_cargo_bindgen/src/discover.rs +10 -5
  74. package/crates/tishlang_cargo_bindgen/src/infer.rs +18 -8
  75. package/crates/tishlang_cargo_bindgen/src/lib.rs +25 -26
  76. package/crates/tishlang_cargo_bindgen/src/main.rs +41 -38
  77. package/crates/tishlang_cargo_bindgen/src/metadata.rs +4 -1
  78. package/justfile +3 -3
  79. package/package.json +1 -1
  80. package/platform/darwin-arm64/tish +0 -0
  81. package/platform/darwin-x64/tish +0 -0
  82. package/platform/linux-arm64/tish +0 -0
  83. package/platform/linux-x64/tish +0 -0
  84. package/platform/win32-x64/tish.exe +0 -0
@@ -5,10 +5,8 @@
5
5
  //! - **Connection**: `send(data)`, `close()`, `readyState` (1=OPEN), `receive()` / `receiveTimeout(ms)`
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
- use std::cell::RefCell;
9
8
  use tishlang_core::VmRef;
10
9
  use std::collections::HashMap;
11
- use std::rc::Rc;
12
10
  use std::sync::atomic::{AtomicU32, Ordering};
13
11
  use std::sync::mpsc;
14
12
  use std::sync::{Arc, Mutex};
@@ -151,7 +149,7 @@ fn conn_id_from_value(v: &Value) -> Option<u32> {
151
149
  Value::Object(o) => {
152
150
  let b = o.borrow();
153
151
  // Direct conn: { _id, send, ... }
154
- if let Some(idv) = b.get(&Arc::from("_id")) {
152
+ if let Some(idv) = b.strings.get("_id") {
155
153
  if let Value::Number(n) = idv {
156
154
  if n.is_finite() && *n >= 0.0 {
157
155
  return Some(*n as u32);
@@ -159,7 +157,7 @@ fn conn_id_from_value(v: &Value) -> Option<u32> {
159
157
  }
160
158
  }
161
159
  // Wrapper: { ws: conn, ... }
162
- if let Some(ws) = b.get(&Arc::from("ws")) {
160
+ if let Some(ws) = b.strings.get("ws") {
163
161
  return conn_id_from_value(ws);
164
162
  }
165
163
  None
@@ -221,7 +219,7 @@ fn conn_object(id: u32) -> Value {
221
219
  Some(s) => {
222
220
  let mut ev: ObjectMap = ObjectMap::default();
223
221
  ev.insert(Arc::from("data"), Value::String(s.into()));
224
- Value::Object(VmRef::new(ev))
222
+ Value::object(ev)
225
223
  }
226
224
  None => Value::Null,
227
225
  }),
@@ -243,18 +241,18 @@ fn conn_object(id: u32) -> Value {
243
241
  Some(s) => {
244
242
  let mut ev: ObjectMap = ObjectMap::default();
245
243
  ev.insert(Arc::from("data"), Value::String(s.into()));
246
- Value::Object(VmRef::new(ev))
244
+ Value::object(ev)
247
245
  }
248
246
  None => Value::Null,
249
247
  }
250
248
  }),
251
249
  );
252
- Value::Object(VmRef::new(obj))
250
+ Value::object(obj)
253
251
  }
254
252
 
255
253
  fn parse_port(args: &[Value]) -> Option<u16> {
256
254
  args.first().and_then(|v| match v {
257
- Value::Object(o) => o.borrow().get(&Arc::from("port")).and_then(|v| match v {
255
+ Value::Object(o) => o.borrow().strings.get("port").and_then(|v| match v {
258
256
  Value::Number(n) if n.is_finite() && *n >= 0.0 => Some(*n as u16),
259
257
  _ => None,
260
258
  }),
@@ -485,7 +483,9 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
485
483
  .unwrap_or_default();
486
484
  let cb = args.get(2).cloned().unwrap_or(Value::Null);
487
485
  if event == "connection" {
488
- so.borrow_mut().insert(Arc::from("_onConnection"), cb);
486
+ so.borrow_mut()
487
+ .strings
488
+ .insert(Arc::from("_onConnection"), cb);
489
489
  }
490
490
  Value::Null
491
491
  });
@@ -498,14 +498,20 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
498
498
  loop {
499
499
  let handle_n = {
500
500
  let b = so.borrow();
501
- match b.get(&Arc::from("_handle")).cloned().unwrap_or(Value::Null) {
501
+ match b
502
+ .strings
503
+ .get("_handle")
504
+ .cloned()
505
+ .unwrap_or(Value::Null)
506
+ {
502
507
  Value::Number(n) if n.is_finite() && n >= 0.0 => n,
503
508
  _ => break,
504
509
  }
505
510
  };
506
511
  let cb = so
507
512
  .borrow()
508
- .get(&Arc::from("_onConnection"))
513
+ .strings
514
+ .get("_onConnection")
509
515
  .cloned()
510
516
  .unwrap_or(Value::Null);
511
517
  let ws = web_socket_server_accept(&[Value::Number(handle_n)]);
@@ -527,7 +533,8 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
527
533
  };
528
534
  let handle_n = so
529
535
  .borrow()
530
- .get(&Arc::from("_handle"))
536
+ .strings
537
+ .get("_handle")
531
538
  .cloned()
532
539
  .unwrap_or(Value::Null);
533
540
  let timeout_ms = args.get(1).cloned().unwrap_or(Value::Number(100.0));
@@ -545,7 +552,7 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
545
552
  m.insert(Arc::from("on"), on_fn);
546
553
  m.insert(Arc::from("listen"), listen_fn);
547
554
  m.insert(Arc::from("acceptTimeout"), accept_timeout_fn);
548
- Value::Object(VmRef::new(m))
555
+ Value::object(m)
549
556
  }
550
557
 
551
558
  #[cfg(test)]
@@ -560,7 +567,7 @@ mod tests {
560
567
  let opts = {
561
568
  let mut m: ObjectMap = ObjectMap::default();
562
569
  m.insert(Arc::from("port"), Value::Number(port as f64));
563
- Value::Object(VmRef::new(m))
570
+ Value::object(m)
564
571
  };
565
572
 
566
573
  let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
@@ -575,20 +582,21 @@ mod tests {
575
582
  };
576
583
  // Echo one message
577
584
  for _ in 0..50 {
578
- let recv_fn = wso.borrow().get(&Arc::from("receive")).cloned();
585
+ let recv_fn = wso.borrow().strings.get("receive").cloned();
579
586
  if let Some(Value::Function(rf)) = recv_fn {
580
587
  let msg = rf(&[]);
581
588
  if !matches!(msg, Value::Null) {
582
589
  let data = match msg {
583
590
  Value::Object(ev) => ev
584
591
  .borrow()
585
- .get(&Arc::from("data"))
592
+ .strings
593
+ .get("data")
586
594
  .map(|v| v.to_display_string())
587
595
  .unwrap_or_default(),
588
596
  _ => String::new(),
589
597
  };
590
598
  if let Some(Value::Function(sf)) =
591
- wso.borrow().get(&Arc::from("send")).cloned()
599
+ wso.borrow().strings.get("send").cloned()
592
600
  {
593
601
  let _ = sf(&[Value::String(data.into())]);
594
602
  }
@@ -607,13 +615,13 @@ mod tests {
607
615
  let Value::Object(co) = client else {
608
616
  panic!("client not object");
609
617
  };
610
- let send = co.borrow().get(&Arc::from("send")).cloned().unwrap();
618
+ let send = co.borrow().strings.get("send").cloned().unwrap();
611
619
  let Value::Function(send_f) = send else {
612
620
  panic!("no send");
613
621
  };
614
622
  let _ = send_f(&[Value::String("hello".into())]);
615
623
 
616
- let recv = co.borrow().get(&Arc::from("receive")).cloned().unwrap();
624
+ let recv = co.borrow().strings.get("receive").cloned().unwrap();
617
625
  let Value::Function(recv_f) = recv else {
618
626
  panic!("no receive");
619
627
  };
@@ -630,7 +638,8 @@ mod tests {
630
638
  };
631
639
  let data = ev
632
640
  .borrow()
633
- .get(&Arc::from("data"))
641
+ .strings
642
+ .get("data")
634
643
  .map(|v| v.to_display_string())
635
644
  .unwrap_or_default();
636
645
  assert_eq!(data, "hello");
@@ -645,7 +654,7 @@ mod tests {
645
654
  let opts = {
646
655
  let mut m: ObjectMap = ObjectMap::default();
647
656
  m.insert(Arc::from("port"), Value::Number(port as f64));
648
- Value::Object(VmRef::new(m))
657
+ Value::object(m)
649
658
  };
650
659
 
651
660
  let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
@@ -658,7 +667,7 @@ mod tests {
658
667
  let Value::Object(wso) = ws else {
659
668
  panic!("accept failed");
660
669
  };
661
- let recv_fn = wso.borrow().get(&Arc::from("receive")).cloned();
670
+ let recv_fn = wso.borrow().strings.get("receive").cloned();
662
671
  let Value::Function(rf) = recv_fn.unwrap() else {
663
672
  panic!("no receive");
664
673
  };
@@ -669,7 +678,8 @@ mod tests {
669
678
  let data = match &msg {
670
679
  Value::Object(ev) => ev
671
680
  .borrow()
672
- .get(&Arc::from("data"))
681
+ .strings
682
+ .get("data")
673
683
  .map(|v| v.to_display_string())
674
684
  .unwrap_or_default(),
675
685
  _ => String::new(),
@@ -695,7 +705,7 @@ mod tests {
695
705
  let Value::Object(co) = client else {
696
706
  panic!("client not object");
697
707
  };
698
- let send = co.borrow().get(&Arc::from("send")).cloned().unwrap();
708
+ let send = co.borrow().strings.get("send").cloned().unwrap();
699
709
  let Value::Function(send_f) = send else {
700
710
  panic!("no send");
701
711
  };
@@ -705,7 +715,8 @@ mod tests {
705
715
  // Client uses receiveTimeout like the agent
706
716
  let recv_timeout = co
707
717
  .borrow()
708
- .get(&Arc::from("receiveTimeout"))
718
+ .strings
719
+ .get("receiveTimeout")
709
720
  .cloned()
710
721
  .unwrap();
711
722
  let Value::Function(recv_timeout_f) = recv_timeout else {
@@ -719,7 +730,8 @@ mod tests {
719
730
  };
720
731
  let data1 = ev1
721
732
  .borrow()
722
- .get(&Arc::from("data"))
733
+ .strings
734
+ .get("data")
723
735
  .map(|v| v.to_display_string())
724
736
  .unwrap_or_default();
725
737
  assert!(
@@ -734,7 +746,8 @@ mod tests {
734
746
  };
735
747
  let data2 = ev2
736
748
  .borrow()
737
- .get(&Arc::from("data"))
749
+ .strings
750
+ .get("data")
738
751
  .map(|v| v.to_display_string())
739
752
  .unwrap_or_default();
740
753
  assert!(
@@ -52,11 +52,15 @@ fn fetch_readable_stream_read_chunks() {
52
52
  _ => panic!("expected object response, got {:?}", resp),
53
53
  };
54
54
  assert!(obj
55
+ .strings
55
56
  .get(&std::sync::Arc::from("ok"))
56
57
  .map(|v| matches!(v, Value::Bool(true)))
57
58
  .unwrap_or(false));
58
59
 
59
- let body = obj.get(&std::sync::Arc::from("body")).expect("body");
60
+ let body = obj
61
+ .strings
62
+ .get(&std::sync::Arc::from("body"))
63
+ .expect("body");
60
64
  let stream = match body {
61
65
  Value::Opaque(s) => s.as_ref(),
62
66
  _ => panic!("expected ReadableStream opaque"),
@@ -74,14 +78,12 @@ fn fetch_readable_stream_read_chunks() {
74
78
  let (done, chunk_bytes) = match chunk {
75
79
  Value::Object(o) => {
76
80
  let m = o.borrow();
77
- let done = m
78
- .get(&std::sync::Arc::from("done"))
79
- .and_then(|v| match v {
80
- Value::Bool(b) => Some(*b),
81
- _ => None,
82
- })
83
- .unwrap_or(true);
81
+ let done = match m.strings.get(&std::sync::Arc::from("done")) {
82
+ Some(Value::Bool(b)) => *b,
83
+ _ => true,
84
+ };
84
85
  let bytes = m
86
+ .strings
85
87
  .get(&std::sync::Arc::from("value"))
86
88
  .map(|v| byte_array_to_vec(v))
87
89
  .unwrap_or_default();
@@ -344,7 +344,9 @@ fn collect_fun_decl_names_expr(expr: &Expr, names: &mut HashSet<String>) {
344
344
  | Expr::PrefixInc { .. }
345
345
  | Expr::PostfixDec { .. }
346
346
  | Expr::PrefixDec { .. } => {}
347
- Expr::JsxElement { props, children, .. } => {
347
+ Expr::JsxElement {
348
+ props, children, ..
349
+ } => {
348
350
  for p in props {
349
351
  match p {
350
352
  JsxProp::Attr { value, .. } => {
@@ -466,14 +468,14 @@ where
466
468
  JsxProp::Spread(e) => {
467
469
  let val = emit_expr(e)?;
468
470
  parts.push(format!(
469
- "if let Value::Object(ref _spread) = {} {{ for (k, v) in _spread.borrow().iter() {{ _obj.insert(Arc::clone(k), v.clone()); }} }}",
471
+ "if let Value::Object(ref _spread) = {} {{ for (k, v) in _spread.borrow().strings.iter() {{ _obj.insert(Arc::clone(k), v.clone()); }} }}",
470
472
  val
471
473
  ));
472
474
  }
473
475
  }
474
476
  }
475
477
  Ok(format!(
476
- "{{ let mut _obj: ObjectMap = ObjectMap::default(); {} Value::Object(VmRef::new(_obj)) }}",
478
+ "{{ let mut _obj: ObjectMap = ObjectMap::default(); {} Value::object(_obj) }}",
477
479
  parts.join(" ")
478
480
  ))
479
481
  } else {
@@ -495,7 +497,7 @@ where
495
497
  }
496
498
  }
497
499
  Ok(format!(
498
- "Value::Object(VmRef::new(ObjectMap::from([{}])))",
500
+ "Value::object(ObjectMap::from([{}]))",
499
501
  kv.join(", ")
500
502
  ))
501
503
  }
@@ -12,8 +12,9 @@ pub mod runtime;
12
12
  #[cfg(feature = "runtime")]
13
13
  pub use runtime::{
14
14
  alloc_root_id, current_root_id, drop_host_for_root, fragment_value, install_host_for_root,
15
- install_thread_local_host, native_create_root, native_use_effect, native_use_memo,
16
- native_use_state, run_with_current_root, ui_h, ui_text, unregister_root,
17
- unregister_root_hooks_and_effects, with_host_for_root, with_thread_local_host, HeadlessHost, Host,
18
- FRAGMENT_SENTINEL, LEGACY_ROOT_ID, RootId,
15
+ install_thread_local_host, native_create_root,
16
+ native_use_effect, native_use_layout_effect, native_use_memo, native_use_ref,
17
+ native_use_state, run_with_current_root, ui_h, ui_text,
18
+ unregister_root, unregister_root_hooks_and_effects, with_host_for_root,
19
+ with_thread_local_host, HeadlessHost, Host, RootId, FRAGMENT_SENTINEL, LEGACY_ROOT_ID,
19
20
  };
@@ -5,6 +5,8 @@ use std::cell::{Cell, RefCell};
5
5
  use std::collections::HashMap;
6
6
  use std::rc::Rc;
7
7
 
8
+ use std::sync::Arc;
9
+
8
10
  use tishlang_core::{ObjectMap, Value, VmRef};
9
11
 
10
12
  use super::Host;
@@ -18,9 +20,10 @@ pub const LEGACY_ROOT_ID: RootId = 1;
18
20
  thread_local! {
19
21
  static HOOKS: RefCell<HashMap<RootId, HookState>> = RefCell::new(HashMap::new());
20
22
  static CURRENT_ROOT: Cell<Option<RootId>> = Cell::new(None);
21
- static HOSTS: RefCell<HashMap<RootId, Box<dyn Host>>> = RefCell::new(HashMap::new());
23
+ static HOSTS: RefCell<HashMap<RootId, Rc<RefCell<Box<dyn Host>>>>> = RefCell::new(HashMap::new());
22
24
  static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = Cell::new(2);
23
25
  static IN_FLUSH: Cell<bool> = Cell::new(false);
26
+ static IN_EFFECT_FLUSH: Cell<bool> = Cell::new(false);
24
27
  }
25
28
 
26
29
  /// Allocate an id for an additional in-process window (starts at 2; 1 is legacy primary).
@@ -42,7 +45,8 @@ fn ensure_hook_entry(root_id: RootId) {
42
45
  pub fn install_host_for_root(root_id: RootId, host: Box<dyn Host>) {
43
46
  ensure_hook_entry(root_id);
44
47
  HOSTS.with(|h| {
45
- h.borrow_mut().insert(root_id, host);
48
+ h.borrow_mut()
49
+ .insert(root_id, Rc::new(RefCell::new(host)));
46
50
  });
47
51
  }
48
52
 
@@ -77,8 +81,10 @@ pub fn unregister_root(root_id: RootId) {
77
81
 
78
82
  pub fn with_host_for_root<R>(root_id: RootId, f: impl FnOnce(&mut dyn Host) -> R) -> Option<R> {
79
83
  HOSTS.with(|c| {
80
- let mut m = c.borrow_mut();
81
- m.get_mut(&root_id).map(|host| f(host.as_mut()))
84
+ let m = c.borrow();
85
+ let host = m.get(&root_id)?;
86
+ let mut host = host.try_borrow_mut().ok()?;
87
+ Some(f(host.as_mut()))
82
88
  })
83
89
  }
84
90
 
@@ -128,6 +134,11 @@ pub struct HookState {
128
134
  effect_cells: Rc<RefCell<Vec<EffectCell>>>,
129
135
  effect_cursor: usize,
130
136
  pending_effects: Rc<RefCell<Vec<PendingEffect>>>,
137
+ layout_effect_cells: Rc<RefCell<Vec<EffectCell>>>,
138
+ layout_effect_cursor: usize,
139
+ pending_layout_effects: Rc<RefCell<Vec<PendingEffect>>>,
140
+ ref_slots: Rc<RefCell<Vec<Value>>>,
141
+ ref_cursor: usize,
131
142
  }
132
143
 
133
144
  impl Default for HookState {
@@ -143,6 +154,11 @@ impl Default for HookState {
143
154
  effect_cells: Rc::new(RefCell::new(Vec::new())),
144
155
  effect_cursor: 0,
145
156
  pending_effects: Rc::new(RefCell::new(Vec::new())),
157
+ layout_effect_cells: Rc::new(RefCell::new(Vec::new())),
158
+ layout_effect_cursor: 0,
159
+ pending_layout_effects: Rc::new(RefCell::new(Vec::new())),
160
+ ref_slots: Rc::new(RefCell::new(Vec::new())),
161
+ ref_cursor: 0,
146
162
  }
147
163
  }
148
164
  }
@@ -160,11 +176,17 @@ fn run_all_effect_cleanups(cells: &RefCell<Vec<EffectCell>>) {
160
176
  impl HookState {
161
177
  pub fn reset_for_new_root(&mut self) {
162
178
  run_all_effect_cleanups(self.effect_cells.as_ref());
179
+ run_all_effect_cleanups(self.layout_effect_cells.as_ref());
163
180
  self.effect_cells.borrow_mut().clear();
181
+ self.layout_effect_cells.borrow_mut().clear();
164
182
  self.effect_cursor = 0;
183
+ self.layout_effect_cursor = 0;
165
184
  self.pending_effects.borrow_mut().clear();
185
+ self.pending_layout_effects.borrow_mut().clear();
166
186
  self.state_slots.borrow_mut().clear();
167
187
  self.cursor = 0;
188
+ self.ref_slots.borrow_mut().clear();
189
+ self.ref_cursor = 0;
168
190
  self.root_vnode = None;
169
191
  self.flush_scheduled = false;
170
192
  self.memo_cache.borrow_mut().clear();
@@ -189,20 +211,15 @@ fn memo_dep_eq(a: &Value, b: &Value) -> bool {
189
211
  if ab.len() != bb.len() {
190
212
  return false;
191
213
  }
192
- ab.iter()
193
- .zip(bb.iter())
194
- .all(|(x, y)| memo_dep_eq(x, y))
214
+ ab.iter().zip(bb.iter()).all(|(x, y)| memo_dep_eq(x, y))
195
215
  }
216
+ (Value::Object(a), Value::Object(b)) => tishlang_core::VmRef::ptr_eq(a, b),
196
217
  _ => false,
197
218
  }
198
219
  }
199
220
 
200
221
  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))
222
+ prev.len() == next.len() && prev.iter().zip(next.iter()).all(|(a, b)| memo_dep_eq(a, b))
206
223
  }
207
224
 
208
225
  fn root_id_for_hooks() -> RootId {
@@ -245,7 +262,7 @@ pub fn native_use_state(args: &[Value]) -> Value {
245
262
  st.flush_scheduled = true;
246
263
  }
247
264
  });
248
- if !IN_FLUSH.get() {
265
+ if !IN_FLUSH.get() && !IN_EFFECT_FLUSH.get() {
249
266
  drain_flush_queue();
250
267
  }
251
268
  Value::Null
@@ -290,9 +307,29 @@ pub fn native_use_memo(args: &[Value]) -> Value {
290
307
  })
291
308
  }
292
309
 
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 {
310
+ /// `useRef(initial)` `{ current: initial }` (stable object identity per slot).
311
+ pub fn native_use_ref(args: &[Value]) -> Value {
312
+ let initial = args.first().cloned().unwrap_or(Value::Null);
313
+ let root_id = root_id_for_hooks();
314
+ HOOKS.with(|h| {
315
+ let mut map = h.borrow_mut();
316
+ let st = map.entry(root_id).or_default();
317
+ let i = st.ref_cursor;
318
+ st.ref_cursor += 1;
319
+ while i >= st.ref_slots.borrow().len() {
320
+ let mut m = ObjectMap::default();
321
+ m.insert(Arc::from("current"), initial.clone());
322
+ st.ref_slots.borrow_mut().push(Value::object(m));
323
+ }
324
+ let out = st.ref_slots.borrow()[i].clone();
325
+ out
326
+ })
327
+ }
328
+
329
+ fn queue_effect(
330
+ args: &[Value],
331
+ layout: bool,
332
+ ) -> Value {
296
333
  let Some(Value::Function(effect_fn)) = args.first() else {
297
334
  return Value::Null;
298
335
  };
@@ -307,9 +344,22 @@ pub fn native_use_effect(args: &[Value]) -> Value {
307
344
  HOOKS.with(|h| {
308
345
  let mut map = h.borrow_mut();
309
346
  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();
347
+ let (cursor, cells, pending) = if layout {
348
+ (
349
+ &mut st.layout_effect_cursor,
350
+ &st.layout_effect_cells,
351
+ &st.pending_layout_effects,
352
+ )
353
+ } else {
354
+ (
355
+ &mut st.effect_cursor,
356
+ &st.effect_cells,
357
+ &st.pending_effects,
358
+ )
359
+ };
360
+ let i = *cursor;
361
+ *cursor += 1;
362
+ let cells = cells.clone();
313
363
  let mut cells_b = cells.borrow_mut();
314
364
  while cells_b.len() <= i {
315
365
  cells_b.push(EffectCell::default());
@@ -321,7 +371,7 @@ pub fn native_use_effect(args: &[Value]) -> Value {
321
371
  drop(cells_b);
322
372
 
323
373
  if should_run {
324
- st.pending_effects.borrow_mut().push(PendingEffect {
374
+ pending.borrow_mut().push(PendingEffect {
325
375
  slot: i,
326
376
  effect_fn: Value::Function(effect_fn),
327
377
  new_deps: deps,
@@ -331,18 +381,29 @@ pub fn native_use_effect(args: &[Value]) -> Value {
331
381
  })
332
382
  }
333
383
 
334
- fn flush_pending_effects(root_id: RootId) {
384
+ /// `useLayoutEffect(effect, deps?)` — runs synchronously after commit, before `useEffect`.
385
+ pub fn native_use_layout_effect(args: &[Value]) -> Value {
386
+ queue_effect(args, true)
387
+ }
388
+
389
+ /// `useEffect(effect, deps?)` — runs `effect` after the host commits the tree; compares `deps` like `useMemo`.
390
+ /// If `effect` returns a function, it is called before the next run or on root teardown (`render` replacement / [`unregister_root`]).
391
+ pub fn native_use_effect(args: &[Value]) -> Value {
392
+ queue_effect(args, false)
393
+ }
394
+
395
+ fn flush_pending_effects_for(
396
+ root_id: RootId,
397
+ pending_key: impl FnOnce(&mut HookState) -> &Rc<RefCell<Vec<PendingEffect>>>,
398
+ cells_key: impl FnOnce(&HookState) -> Rc<RefCell<Vec<EffectCell>>>,
399
+ ) {
335
400
  let pending: Vec<PendingEffect> = HOOKS.with(|h| {
336
401
  h.borrow_mut()
337
402
  .get_mut(&root_id)
338
- .map(|st| std::mem::take(&mut *st.pending_effects.borrow_mut()))
403
+ .map(|st| std::mem::take(&mut *pending_key(st).borrow_mut()))
339
404
  .unwrap_or_default()
340
405
  });
341
- let cells_rc = HOOKS.with(|h| {
342
- h.borrow()
343
- .get(&root_id)
344
- .map(|st| st.effect_cells.clone())
345
- });
406
+ let cells_rc = HOOKS.with(|h| h.borrow().get(&root_id).map(cells_key));
346
407
  let Some(cells_rc) = cells_rc else {
347
408
  return;
348
409
  };
@@ -380,6 +441,26 @@ fn flush_pending_effects(root_id: RootId) {
380
441
  }
381
442
  }
382
443
 
444
+ fn flush_pending_layout_effects(root_id: RootId) {
445
+ IN_EFFECT_FLUSH.set(true);
446
+ flush_pending_effects_for(
447
+ root_id,
448
+ |st| &st.pending_layout_effects,
449
+ |st| st.layout_effect_cells.clone(),
450
+ );
451
+ IN_EFFECT_FLUSH.set(false);
452
+ }
453
+
454
+ fn flush_pending_effects(root_id: RootId) {
455
+ IN_EFFECT_FLUSH.set(true);
456
+ flush_pending_effects_for(
457
+ root_id,
458
+ |st| &st.pending_effects,
459
+ |st| st.effect_cells.clone(),
460
+ );
461
+ IN_EFFECT_FLUSH.set(false);
462
+ }
463
+
383
464
  fn parse_root_id_arg(args: &[Value]) -> RootId {
384
465
  match args.first() {
385
466
  Some(Value::Number(n)) if n.is_finite() && *n >= 1.0 && n.fract() == 0.0 => *n as u64,
@@ -405,10 +486,10 @@ pub fn native_create_root(args: &[Value]) -> Value {
405
486
  drain_flush_queue();
406
487
  Value::Null
407
488
  });
408
- Value::Object(VmRef::new(ObjectMap::from([(
489
+ Value::object(ObjectMap::from([(
409
490
  std::sync::Arc::from("render"),
410
491
  render_fn,
411
- )])))
492
+ )]))
412
493
  }
413
494
 
414
495
  /// Request a re-render (coalesced; safe if called during flush).
@@ -426,6 +507,10 @@ pub fn schedule_flush() {
426
507
  }
427
508
 
428
509
  fn drain_flush_queue() {
510
+ drain_flush_queue_on_current_thread();
511
+ }
512
+
513
+ fn drain_flush_queue_on_current_thread() {
429
514
  loop {
430
515
  let root_id = HOOKS.with(|h| {
431
516
  h.borrow()
@@ -450,8 +535,11 @@ fn drain_flush_queue() {
450
535
  let st = map.get_mut(&root_id)?;
451
536
  st.cursor = 0;
452
537
  st.memo_cursor = 0;
538
+ st.ref_cursor = 0;
453
539
  st.effect_cursor = 0;
540
+ st.layout_effect_cursor = 0;
454
541
  st.pending_effects.borrow_mut().clear();
542
+ st.pending_layout_effects.borrow_mut().clear();
455
543
  let app = st.root_app.clone()?;
456
544
  let Value::Function(f) = app else {
457
545
  return None;
@@ -462,18 +550,16 @@ fn drain_flush_queue() {
462
550
  if let Some(f) = app_fn {
463
551
  let tree = f(&[]);
464
552
  HOOKS.with(|h| {
465
- let mut map = h.borrow_mut();
466
- if let Some(st) = map.get_mut(&root_id) {
553
+ if let Some(st) = h.borrow_mut().get_mut(&root_id) {
467
554
  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
- });
474
555
  }
475
556
  });
557
+ let host = HOSTS.with(|hosts| hosts.borrow().get(&root_id).cloned());
558
+ if let Some(host) = host {
559
+ host.borrow_mut().commit_root(&tree);
560
+ }
476
561
  IN_FLUSH.set(false);
562
+ flush_pending_layout_effects(root_id);
477
563
  flush_pending_effects(root_id);
478
564
  }
479
565