@tishlang/tish 1.13.1 → 2.0.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 (106) hide show
  1. package/Cargo.toml +2 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/transform/expr.rs +1 -0
  4. package/crates/tish/Cargo.toml +11 -3
  5. package/crates/tish/build.rs +21 -0
  6. package/crates/tish/src/cli_help.rs +15 -4
  7. package/crates/tish/src/main.rs +93 -21
  8. package/crates/tish/src/repl_completion.rs +0 -1
  9. package/crates/tish/tests/error_source_location.rs +36 -0
  10. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  11. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  12. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  13. package/crates/tish/tests/integration_test.rs +402 -91
  14. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  15. package/crates/tish/tests/tty_capability.rs +43 -0
  16. package/crates/tish_ast/src/ast.rs +37 -8
  17. package/crates/tish_builtins/Cargo.toml +2 -0
  18. package/crates/tish_builtins/src/array.rs +375 -13
  19. package/crates/tish_builtins/src/collections.rs +481 -0
  20. package/crates/tish_builtins/src/construct.rs +59 -19
  21. package/crates/tish_builtins/src/date.rs +538 -0
  22. package/crates/tish_builtins/src/globals.rs +86 -6
  23. package/crates/tish_builtins/src/iterator.rs +129 -0
  24. package/crates/tish_builtins/src/lib.rs +5 -0
  25. package/crates/tish_builtins/src/number.rs +96 -0
  26. package/crates/tish_builtins/src/object.rs +2 -2
  27. package/crates/tish_builtins/src/string.rs +19 -20
  28. package/crates/tish_builtins/src/symbol.rs +1 -1
  29. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  30. package/crates/tish_bytecode/src/chunk.rs +69 -1
  31. package/crates/tish_bytecode/src/compiler.rs +933 -89
  32. package/crates/tish_bytecode/src/encoding.rs +2 -0
  33. package/crates/tish_bytecode/src/lib.rs +2 -1
  34. package/crates/tish_bytecode/src/opcode.rs +47 -4
  35. package/crates/tish_bytecode/src/serialize.rs +31 -1
  36. package/crates/tish_compile/Cargo.toml +1 -0
  37. package/crates/tish_compile/src/check.rs +774 -0
  38. package/crates/tish_compile/src/codegen.rs +2334 -349
  39. package/crates/tish_compile/src/infer.rs +1395 -6
  40. package/crates/tish_compile/src/lib.rs +50 -8
  41. package/crates/tish_compile/src/resolve.rs +584 -21
  42. package/crates/tish_compile/src/types.rs +106 -2
  43. package/crates/tish_compile_js/src/codegen.rs +67 -0
  44. package/crates/tish_compile_js/src/tests_jsx.rs +64 -0
  45. package/crates/tish_core/Cargo.toml +7 -1
  46. package/crates/tish_core/src/console_style.rs +11 -1
  47. package/crates/tish_core/src/json.rs +81 -38
  48. package/crates/tish_core/src/lib.rs +3 -0
  49. package/crates/tish_core/src/shape.rs +85 -0
  50. package/crates/tish_core/src/value.rs +679 -25
  51. package/crates/tish_core/src/vmref.rs +13 -8
  52. package/crates/tish_cranelift/src/link.rs +17 -4
  53. package/crates/tish_cranelift_runtime/Cargo.toml +1 -0
  54. package/crates/tish_eval/Cargo.toml +6 -0
  55. package/crates/tish_eval/src/eval.rs +665 -117
  56. package/crates/tish_eval/src/http.rs +4 -1
  57. package/crates/tish_eval/src/natives.rs +165 -13
  58. package/crates/tish_eval/src/value.rs +31 -13
  59. package/crates/tish_eval/src/value_convert.rs +10 -4
  60. package/crates/tish_ffi/Cargo.toml +26 -0
  61. package/crates/tish_ffi/src/lib.rs +518 -0
  62. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  63. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  64. package/crates/tish_ffi/tests/loader.rs +65 -0
  65. package/crates/tish_fmt/src/lib.rs +43 -5
  66. package/crates/tish_lexer/src/lib.rs +397 -9
  67. package/crates/tish_lexer/src/token.rs +7 -0
  68. package/crates/tish_lint/src/lib.rs +2 -10
  69. package/crates/tish_lsp/src/import_goto.rs +2 -0
  70. package/crates/tish_lsp/src/main.rs +439 -26
  71. package/crates/tish_native/src/build.rs +55 -1
  72. package/crates/tish_opt/src/lib.rs +126 -23
  73. package/crates/tish_parser/src/lib.rs +55 -1
  74. package/crates/tish_parser/src/parser.rs +456 -34
  75. package/crates/tish_pg/src/lib.rs +3 -3
  76. package/crates/tish_resolve/src/lib.rs +99 -59
  77. package/crates/tish_runtime/Cargo.toml +4 -0
  78. package/crates/tish_runtime/src/http.rs +66 -17
  79. package/crates/tish_runtime/src/http_fetch.rs +29 -8
  80. package/crates/tish_runtime/src/http_hyper.rs +25 -2
  81. package/crates/tish_runtime/src/lib.rs +299 -44
  82. package/crates/tish_runtime/src/promise.rs +328 -18
  83. package/crates/tish_runtime/src/timers.rs +13 -7
  84. package/crates/tish_runtime/src/tty.rs +226 -0
  85. package/crates/tish_runtime/src/ws.rs +35 -18
  86. package/crates/tish_runtime/tests/fetch_readable_stream.rs +2 -2
  87. package/crates/tish_ui/src/jsx.rs +10 -0
  88. package/crates/tish_ui/src/runtime/hooks.rs +19 -15
  89. package/crates/tish_ui/src/runtime/mod.rs +15 -12
  90. package/crates/tish_vm/Cargo.toml +14 -1
  91. package/crates/tish_vm/src/jit.rs +1050 -0
  92. package/crates/tish_vm/src/lib.rs +2 -0
  93. package/crates/tish_vm/src/vm.rs +1546 -202
  94. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  95. package/crates/tish_wasm/src/lib.rs +6 -2
  96. package/crates/tish_wasm_runtime/src/gpu.rs +17 -1
  97. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  98. package/crates/tishlang_cargo_bindgen/src/lib.rs +2 -2
  99. package/crates/tishlang_cargo_bindgen/src/metadata.rs +1 -1
  100. package/justfile +8 -0
  101. package/package.json +1 -1
  102. package/platform/darwin-arm64/tish +0 -0
  103. package/platform/darwin-x64/tish +0 -0
  104. package/platform/linux-arm64/tish +0 -0
  105. package/platform/linux-x64/tish +0 -0
  106. package/platform/win32-x64/tish.exe +0 -0
@@ -82,6 +82,20 @@ fn unregister(id: u32) {
82
82
  CONNS.lock().unwrap().remove(&id);
83
83
  }
84
84
 
85
+ /// Max simultaneous WebSocket connections. Each accepted conn registers in `CONNS` and
86
+ /// spawns tasks + channels, freed only on close — unbounded accepts exhaust tasks/memory.
87
+ /// Override with `TISH_WS_MAX_CONNS`; default 10000.
88
+ fn max_ws_connections() -> usize {
89
+ use std::sync::OnceLock;
90
+ static MAX: OnceLock<usize> = OnceLock::new();
91
+ *MAX.get_or_init(|| {
92
+ std::env::var("TISH_WS_MAX_CONNS")
93
+ .ok()
94
+ .and_then(|v| v.parse().ok())
95
+ .unwrap_or(10_000)
96
+ })
97
+ }
98
+
85
99
  fn conn_send(id: u32, data: String) -> bool {
86
100
  let guard = match CONNS.lock() {
87
101
  Ok(g) => g,
@@ -105,7 +119,7 @@ fn conn_receive(id: u32) -> Option<String> {
105
119
  /// Uses try_recv in a loop to avoid holding CONNS lock while blocking (prevents deadlock
106
120
  /// when connection closes and tokio task needs to unregister).
107
121
  fn conn_receive_timeout(id: u32, timeout_ms: u64) -> Option<String> {
108
- let timeout_ms = timeout_ms.min(3600_000);
122
+ let timeout_ms = timeout_ms.min(3_600_000);
109
123
  let deadline = Instant::now() + Duration::from_millis(timeout_ms);
110
124
  let poll_interval = Duration::from_millis(50);
111
125
  loop {
@@ -149,11 +163,9 @@ fn conn_id_from_value(v: &Value) -> Option<u32> {
149
163
  Value::Object(o) => {
150
164
  let b = o.borrow();
151
165
  // Direct conn: { _id, send, ... }
152
- if let Some(idv) = b.strings.get("_id") {
153
- if let Value::Number(n) = idv {
154
- if n.is_finite() && *n >= 0.0 {
155
- return Some(*n as u32);
156
- }
166
+ if let Some(Value::Number(n)) = b.strings.get("_id") {
167
+ if n.is_finite() && *n >= 0.0 {
168
+ return Some(*n as u32);
157
169
  }
158
170
  }
159
171
  // Wrapper: { ws: conn, ... }
@@ -168,7 +180,7 @@ fn conn_id_from_value(v: &Value) -> Option<u32> {
168
180
 
169
181
  /// Native broadcast: send data to all conns in array except `except`. Avoids Tish-side method calls.
170
182
  pub fn ws_broadcast_native(args: &[Value]) -> Value {
171
- let conns = match args.get(0) {
183
+ let conns = match args.first() {
172
184
  Some(Value::Array(a)) => a.borrow().clone(),
173
185
  _ => return Value::Null,
174
186
  };
@@ -232,7 +244,7 @@ fn conn_object(id: u32) -> Value {
232
244
  .first()
233
245
  .and_then(|v| match v {
234
246
  Value::Number(n) if n.is_finite() && *n >= 0.0 => {
235
- Some((*n as u64).min(3600_000))
247
+ Some((*n as u64).min(3_600_000))
236
248
  }
237
249
  _ => None,
238
250
  })
@@ -366,6 +378,11 @@ pub fn web_socket_server_listen(args: &[Value]) -> Value {
366
378
  Ok(s) => s,
367
379
  Err(_) => break,
368
380
  };
381
+ // Cap total connections — unbounded accepts would exhaust tasks/memory.
382
+ if CONNS.lock().map(|c| c.len()).unwrap_or(0) >= max_ws_connections() {
383
+ drop(stream);
384
+ continue;
385
+ }
369
386
  let ws_stream = match tokio_tungstenite::accept_async(stream).await {
370
387
  Ok(ws) => {
371
388
  eprintln!(
@@ -446,7 +463,7 @@ pub fn web_socket_server_accept_timeout(args: &[Value]) -> Value {
446
463
  _ => return Value::Null,
447
464
  };
448
465
  let timeout_ms = match args.get(1) {
449
- Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => (*n as u64).min(3600_000),
466
+ Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => (*n as u64).min(3_600_000),
450
467
  _ => 100,
451
468
  };
452
469
  let mut map = match SERVER_RECV.lock() {
@@ -520,7 +537,7 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
520
537
  }
521
538
  clients_listen.borrow_mut().push(ws.clone());
522
539
  if let Value::Function(f) = cb {
523
- let _ = f(&[ws]);
540
+ let _ = f.call(&[ws]);
524
541
  }
525
542
  }
526
543
  Value::Null
@@ -584,7 +601,7 @@ mod tests {
584
601
  for _ in 0..50 {
585
602
  let recv_fn = wso.borrow().strings.get("receive").cloned();
586
603
  if let Some(Value::Function(rf)) = recv_fn {
587
- let msg = rf(&[]);
604
+ let msg = rf.call(&[]);
588
605
  if !matches!(msg, Value::Null) {
589
606
  let data = match msg {
590
607
  Value::Object(ev) => ev
@@ -598,7 +615,7 @@ mod tests {
598
615
  if let Some(Value::Function(sf)) =
599
616
  wso.borrow().strings.get("send").cloned()
600
617
  {
601
- let _ = sf(&[Value::String(data.into())]);
618
+ let _ = sf.call(&[Value::String(data.into())]);
602
619
  }
603
620
  break;
604
621
  }
@@ -619,7 +636,7 @@ mod tests {
619
636
  let Value::Function(send_f) = send else {
620
637
  panic!("no send");
621
638
  };
622
- let _ = send_f(&[Value::String("hello".into())]);
639
+ let _ = send_f.call(&[Value::String("hello".into())]);
623
640
 
624
641
  let recv = co.borrow().strings.get("receive").cloned().unwrap();
625
642
  let Value::Function(recv_f) = recv else {
@@ -627,7 +644,7 @@ mod tests {
627
644
  };
628
645
  let mut got = Value::Null;
629
646
  for _ in 0..100 {
630
- got = recv_f(&[]);
647
+ got = recv_f.call(&[]);
631
648
  if !matches!(got, Value::Null) {
632
649
  break;
633
650
  }
@@ -673,7 +690,7 @@ mod tests {
673
690
  };
674
691
  // Poll until we get join
675
692
  for _ in 0..200 {
676
- let msg = rf(&[]);
693
+ let msg = rf.call(&[]);
677
694
  if !matches!(msg, Value::Null) {
678
695
  let data = match &msg {
679
696
  Value::Object(ev) => ev
@@ -710,7 +727,7 @@ mod tests {
710
727
  panic!("no send");
711
728
  };
712
729
  let join_msg = r#"{"type":"join","sessionId":"default","role":"agent","laneId":"ai-a"}"#;
713
- let _ = send_f(&[Value::String(join_msg.into())]);
730
+ let _ = send_f.call(&[Value::String(join_msg.into())]);
714
731
 
715
732
  // Client uses receiveTimeout like the agent
716
733
  let recv_timeout = co
@@ -724,7 +741,7 @@ mod tests {
724
741
  };
725
742
  let timeout_arg = Value::Number(2000.0);
726
743
 
727
- let got1 = recv_timeout_f(&[timeout_arg.clone()]);
744
+ let got1 = recv_timeout_f.call(&[timeout_arg.clone()]);
728
745
  let Value::Object(ev1) = got1 else {
729
746
  panic!("first recv: expected object, got {:?}", got1);
730
747
  };
@@ -740,7 +757,7 @@ mod tests {
740
757
  data1
741
758
  );
742
759
 
743
- let got2 = recv_timeout_f(&[timeout_arg]);
760
+ let got2 = recv_timeout_f.call(&[timeout_arg]);
744
761
  let Value::Object(ev2) = got2 else {
745
762
  panic!("second recv: expected object, got {:?}", got2);
746
763
  };
@@ -65,7 +65,7 @@ fn fetch_readable_stream_read_chunks() {
65
65
  Value::Opaque(s) => s.as_ref(),
66
66
  _ => panic!("expected ReadableStream opaque"),
67
67
  };
68
- let reader_val = stream.get_method("getReader").expect("getReader")(&[]);
68
+ let reader_val = stream.get_method("getReader").expect("getReader").call(&[]);
69
69
  let reader = match reader_val {
70
70
  Value::Opaque(r) => r,
71
71
  _ => panic!("expected reader opaque, got {:?}", reader_val),
@@ -73,7 +73,7 @@ fn fetch_readable_stream_read_chunks() {
73
73
 
74
74
  let mut acc = Vec::new();
75
75
  loop {
76
- let read_p = reader.get_method("read").expect("read")(&[]);
76
+ let read_p = reader.get_method("read").expect("read").call(&[]);
77
77
  let chunk = await_promise(read_p);
78
78
  let (done, chunk_bytes) = match chunk {
79
79
  Value::Object(o) => {
@@ -142,6 +142,11 @@ fn collect_fun_decl_names_stmt(stmt: &Statement, names: &mut HashSet<String>) {
142
142
  collect_fun_decl_names_stmt(s, names);
143
143
  }
144
144
  }
145
+ Statement::Multi { statements, .. } => {
146
+ for s in statements {
147
+ collect_fun_decl_names_stmt(s, names);
148
+ }
149
+ }
145
150
  Statement::VarDecl { init, .. } => {
146
151
  if let Some(e) = init {
147
152
  collect_fun_decl_names_expr(e, names);
@@ -315,6 +320,9 @@ fn collect_fun_decl_names_expr(expr: &Expr, names: &mut HashSet<String>) {
315
320
  Expr::Await { operand, .. } | Expr::TypeOf { operand, .. } => {
316
321
  collect_fun_decl_names_expr(operand, names);
317
322
  }
323
+ Expr::Delete { target, .. } => {
324
+ collect_fun_decl_names_expr(target, names);
325
+ }
318
326
  Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
319
327
  collect_fun_decl_names_expr(value, names);
320
328
  }
@@ -537,6 +545,7 @@ fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
537
545
  use tishlang_ast::{ExportDeclaration, Statement};
538
546
  match stmt {
539
547
  Statement::Block { statements, .. } => statements.iter().any(stmt_contains_jsx),
548
+ Statement::Multi { statements, .. } => statements.iter().any(stmt_contains_jsx),
540
549
  Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_contains_jsx),
541
550
  Statement::VarDeclDestructure { init, .. } => expr_contains_jsx(init),
542
551
  Statement::ExprStmt { expr, .. } => expr_contains_jsx(expr),
@@ -653,6 +662,7 @@ fn expr_contains_jsx(expr: &Expr) -> bool {
653
662
  Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(expr_contains_jsx),
654
663
  Expr::Await { operand, .. } => expr_contains_jsx(operand),
655
664
  Expr::TypeOf { operand, .. } => expr_contains_jsx(operand),
665
+ Expr::Delete { target, .. } => expr_contains_jsx(target),
656
666
  Expr::PostfixInc { .. }
657
667
  | Expr::PrefixInc { .. }
658
668
  | Expr::PostfixDec { .. }
@@ -17,13 +17,16 @@ pub type RootId = u64;
17
17
  /// First root: `install_thread_local_host` and `native_create_root` without an id argument.
18
18
  pub const LEGACY_ROOT_ID: RootId = 1;
19
19
 
20
+ /// Shared, interior-mutable handle to one root's host backend.
21
+ type HostHandle = Rc<RefCell<Box<dyn Host>>>;
22
+
20
23
  thread_local! {
21
24
  static HOOKS: RefCell<HashMap<RootId, HookState>> = RefCell::new(HashMap::new());
22
- static CURRENT_ROOT: Cell<Option<RootId>> = Cell::new(None);
23
- static HOSTS: RefCell<HashMap<RootId, Rc<RefCell<Box<dyn Host>>>>> = RefCell::new(HashMap::new());
24
- static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = Cell::new(2);
25
- static IN_FLUSH: Cell<bool> = Cell::new(false);
26
- static IN_EFFECT_FLUSH: Cell<bool> = Cell::new(false);
25
+ static CURRENT_ROOT: Cell<Option<RootId>> = const { Cell::new(None) };
26
+ static HOSTS: RefCell<HashMap<RootId, HostHandle>> = RefCell::new(HashMap::new());
27
+ static NEXT_DYNAMIC_ROOT_ID: Cell<RootId> = const { Cell::new(2) };
28
+ static IN_FLUSH: Cell<bool> = const { Cell::new(false) };
29
+ static IN_EFFECT_FLUSH: Cell<bool> = const { Cell::new(false) };
27
30
  }
28
31
 
29
32
  /// Allocate an id for an additional in-process window (starts at 2; 1 is legacy primary).
@@ -121,6 +124,9 @@ struct PendingEffect {
121
124
  new_deps: Vec<Value>,
122
125
  }
123
126
 
127
+ /// Per-slot last dependency snapshot and cached value for `useMemo`.
128
+ type MemoCache = Rc<RefCell<Vec<Option<(Vec<Value>, Value)>>>>;
129
+
124
130
  /// Hook storage for one `createRoot().render(App)` tree.
125
131
  pub struct HookState {
126
132
  pub state_slots: Rc<RefCell<Vec<Value>>>,
@@ -129,7 +135,7 @@ pub struct HookState {
129
135
  pub root_vnode: Option<Value>,
130
136
  pub flush_scheduled: bool,
131
137
  /// Per-slot: last dependency tuple snapshot and cached value from `useMemo`.
132
- pub memo_cache: Rc<RefCell<Vec<Option<(Vec<Value>, Value)>>>>,
138
+ pub memo_cache: MemoCache,
133
139
  pub memo_cursor: usize,
134
140
  effect_cells: Rc<RefCell<Vec<EffectCell>>>,
135
141
  effect_cursor: usize,
@@ -165,10 +171,8 @@ impl Default for HookState {
165
171
 
166
172
  fn run_all_effect_cleanups(cells: &RefCell<Vec<EffectCell>>) {
167
173
  for cell in cells.borrow_mut().iter_mut() {
168
- if let Some(c) = cell.cleanup.take() {
169
- if let Value::Function(f) = c {
170
- let _ = f(&[]);
171
- }
174
+ if let Some(Value::Function(f)) = cell.cleanup.take() {
175
+ let _ = f.call(&[]);
172
176
  }
173
177
  }
174
178
  }
@@ -252,7 +256,7 @@ pub fn native_use_state(args: &[Value]) -> Value {
252
256
  }
253
257
  let prev = st.state_slots.borrow()[idx].clone();
254
258
  let new_v = match &arg {
255
- Value::Function(f) => f(&[prev.clone()]),
259
+ Value::Function(f) => f.call(std::slice::from_ref(&prev)),
256
260
  _ => arg.clone(),
257
261
  };
258
262
  if memo_dep_eq(&prev, &new_v) {
@@ -301,7 +305,7 @@ pub fn native_use_memo(args: &[Value]) -> Value {
301
305
  if reuse {
302
306
  return c[i].as_ref().unwrap().1.clone();
303
307
  }
304
- let produced = factory(&[]);
308
+ let produced = factory.call(&[]);
305
309
  c[i] = Some((deps, produced.clone()));
306
310
  produced
307
311
  })
@@ -419,10 +423,10 @@ fn flush_pending_effects_for(
419
423
  cells[p.slot].cleanup.take()
420
424
  };
421
425
  if let Some(Value::Function(f)) = cleanup_fn {
422
- let _ = f(&[]);
426
+ let _ = f.call(&[]);
423
427
  }
424
428
  let run_result = if let Value::Function(f) = &p.effect_fn {
425
- f(&[])
429
+ f.call(&[])
426
430
  } else {
427
431
  Value::Null
428
432
  };
@@ -548,7 +552,7 @@ fn drain_flush_queue_on_current_thread() {
548
552
  });
549
553
 
550
554
  if let Some(f) = app_fn {
551
- let tree = f(&[]);
555
+ let tree = f.call(&[]);
552
556
  HOOKS.with(|h| {
553
557
  if let Some(st) = h.borrow_mut().get_mut(&root_id) {
554
558
  st.root_vnode = Some(tree.clone());
@@ -29,7 +29,7 @@ pub fn fragment_value() -> Value {
29
29
  /// Returns true if `tag` refers to [`fragment_value`].
30
30
  pub fn is_fragment_tag(tag: &Value) -> bool {
31
31
  match tag {
32
- Value::String(s) => s.as_ref() == FRAGMENT_SENTINEL,
32
+ Value::String(s) => s.as_str() == FRAGMENT_SENTINEL,
33
33
  Value::Symbol(s) => s.registry_key.as_deref() == Some("tish.fragment"),
34
34
  _ => false,
35
35
  }
@@ -46,17 +46,24 @@ pub fn ui_text(args: &[Value]) -> Value {
46
46
 
47
47
  /// Vnode factory: `h(tag, props, children)` (Lattish-compatible shape).
48
48
  pub fn ui_h(args: &[Value]) -> Value {
49
- let tag = args.get(0).cloned().unwrap_or(Value::Null);
49
+ let tag = args.first().cloned().unwrap_or(Value::Null);
50
50
  let props = args.get(1).cloned().unwrap_or(Value::Null);
51
51
  let children_arg = args.get(2).cloned().unwrap_or(Value::Null);
52
52
 
53
53
  let children_vec = normalize_children_list(children_arg);
54
54
 
55
55
  if let Value::Function(f) = &tag {
56
- let mut merged = if matches!(props, Value::Null) {
56
+ let mut merged: ObjectMap = if matches!(props, Value::Null) {
57
57
  ObjectMap::default()
58
58
  } else if let Value::Object(obj) = props {
59
- obj.borrow().strings.clone()
59
+ // `ObjectData.strings` is an insertion-ordered `PropMap`; `Value::object` takes an
60
+ // `ObjectMap` (`AHashMap`). Copy the entries across (object property order is not
61
+ // observable for a merged props map here).
62
+ obj.borrow()
63
+ .strings
64
+ .iter()
65
+ .map(|(k, v)| (Arc::clone(k), v.clone()))
66
+ .collect()
60
67
  } else {
61
68
  ObjectMap::default()
62
69
  };
@@ -66,7 +73,7 @@ pub fn ui_h(args: &[Value]) -> Value {
66
73
  Value::Array(VmRef::new(children_vec.clone())),
67
74
  );
68
75
  }
69
- return f(&[Value::object(merged)]);
76
+ return f.call(&[Value::object(merged)]);
70
77
  }
71
78
 
72
79
  if is_fragment_tag(&tag) {
@@ -74,7 +81,7 @@ pub fn ui_h(args: &[Value]) -> Value {
74
81
  }
75
82
 
76
83
  let tag_str: Arc<str> = match tag {
77
- Value::String(s) => s,
84
+ Value::String(s) => Arc::from(s.as_str()),
78
85
  _ => return Value::Null,
79
86
  };
80
87
 
@@ -105,7 +112,7 @@ fn flatten_vnode_children(items: &[Value]) -> Vec<Value> {
105
112
 
106
113
  fn vnode_element(tag: Arc<str>, props: Value, children: Vec<Value>) -> Value {
107
114
  let mut m = ObjectMap::default();
108
- m.insert(Arc::from("tag"), Value::String(tag));
115
+ m.insert(Arc::from("tag"), Value::String(tag.as_ref().into()));
109
116
  m.insert(
110
117
  Arc::from("props"),
111
118
  if matches!(props, Value::Null) {
@@ -143,15 +150,11 @@ pub trait Host {
143
150
  }
144
151
 
145
152
  /// No-op / test host that only stores the last committed tree.
153
+ #[derive(Default)]
146
154
  pub struct HeadlessHost {
147
155
  pub last: Option<Value>,
148
156
  }
149
157
 
150
- impl Default for HeadlessHost {
151
- fn default() -> Self {
152
- Self { last: None }
153
- }
154
- }
155
158
 
156
159
  impl Host for HeadlessHost {
157
160
  fn commit_root(&mut self, vnode: &Value) {
@@ -8,7 +8,10 @@ license-file = { workspace = true }
8
8
  repository = { workspace = true }
9
9
  [features]
10
10
  default = []
11
- regex = ["tishlang_core/regex"]
11
+ # RegExp needs the runtime's regex impls (regexp_new / regexp_test / regexp_exec and the
12
+ # regex-aware String methods) so the VM routes through the SAME code as the rust backend.
13
+ # Mirror the fs/http pattern: pull in the optional runtime crate + its regex feature.
14
+ regex = ["tishlang_core/regex", "dep:tishlang_runtime", "tishlang_runtime/regex"]
12
15
  # Propagate `send-values` so that every native-function closure we build
13
16
  # in the VM (array / string / object methods) picks up the right
14
17
  # `Rc<dyn Fn>` vs `Arc<dyn Fn + Send + Sync>` wrapper at compile time.
@@ -20,6 +23,7 @@ wasm = ["dep:wasm-bindgen"]
20
23
  # (some Cargo/cache combinations only saw `tishlang_runtime/http` and built VM stubs without http).
21
24
  fs = ["dep:tishlang_runtime", "tishlang_runtime/fs"]
22
25
  process = ["dep:tishlang_runtime", "tishlang_runtime/process"]
26
+ tty = ["dep:tishlang_runtime", "tishlang_runtime/tty"]
23
27
  # Timer globals + `tish:timers` LoadNativeExport (uses tishlang_runtime TLS timer registry).
24
28
  timers = ["dep:tishlang_runtime"]
25
29
  # Any HTTP build needs Send-safe values so handlers can be dispatched
@@ -45,3 +49,12 @@ tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
45
49
 
46
50
  [target.'cfg(target_arch = "wasm32")'.dependencies]
47
51
  getrandom = { version = "0.4", features = ["wasm_js"] }
52
+
53
+ # Numeric JIT (native codegen slice 1). Not built for wasm — cranelift-jit emits host
54
+ # machine code and cannot target wasm32. The wasm/wasi VM keeps the interpreter path.
55
+ [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
56
+ cranelift = "0.130"
57
+ cranelift-jit = "0.130"
58
+ cranelift-module = "0.130"
59
+ cranelift-native = "0.130"
60
+ stacker = "0.1"