@tishlang/tish 1.0.29 → 1.0.33

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 (60) hide show
  1. package/Cargo.toml +1 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +15 -6
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/main.rs +1 -1
  5. package/crates/tish/tests/integration_test.rs +4 -3
  6. package/crates/tish_ast/src/ast.rs +65 -2
  7. package/crates/tish_build_utils/src/lib.rs +10 -2
  8. package/crates/tish_builtins/src/construct.rs +177 -0
  9. package/crates/tish_builtins/src/globals.rs +3 -5
  10. package/crates/tish_builtins/src/helpers.rs +2 -3
  11. package/crates/tish_builtins/src/lib.rs +1 -0
  12. package/crates/tish_builtins/src/object.rs +3 -4
  13. package/crates/tish_bytecode/src/compiler.rs +85 -11
  14. package/crates/tish_bytecode/src/opcode.rs +7 -3
  15. package/crates/tish_compile/Cargo.toml +1 -0
  16. package/crates/tish_compile/src/codegen.rs +233 -71
  17. package/crates/tish_compile/src/lib.rs +35 -0
  18. package/crates/tish_compile_js/Cargo.toml +1 -0
  19. package/crates/tish_compile_js/src/codegen.rs +38 -94
  20. package/crates/tish_compile_js/src/lib.rs +0 -1
  21. package/crates/tish_compile_js/src/tests_jsx.rs +68 -0
  22. package/crates/tish_core/Cargo.toml +4 -0
  23. package/crates/tish_core/src/console_style.rs +7 -1
  24. package/crates/tish_core/src/json.rs +1 -2
  25. package/crates/tish_core/src/macros.rs +2 -3
  26. package/crates/tish_core/src/value.rs +10 -5
  27. package/crates/tish_eval/Cargo.toml +2 -0
  28. package/crates/tish_eval/src/eval.rs +149 -72
  29. package/crates/tish_eval/src/http.rs +3 -4
  30. package/crates/tish_eval/src/regex.rs +3 -2
  31. package/crates/tish_eval/src/value.rs +11 -13
  32. package/crates/tish_eval/src/value_convert.rs +4 -8
  33. package/crates/tish_fmt/src/lib.rs +49 -10
  34. package/crates/tish_lexer/src/token.rs +2 -0
  35. package/crates/tish_lint/src/lib.rs +9 -0
  36. package/crates/tish_lsp/README.md +1 -1
  37. package/crates/tish_native/src/build.rs +16 -2
  38. package/crates/tish_opt/src/lib.rs +15 -0
  39. package/crates/tish_parser/src/lib.rs +101 -1
  40. package/crates/tish_parser/src/parser.rs +161 -50
  41. package/crates/tish_runtime/src/http.rs +4 -5
  42. package/crates/tish_runtime/src/http_fetch.rs +9 -10
  43. package/crates/tish_runtime/src/lib.rs +9 -2
  44. package/crates/tish_runtime/src/promise.rs +2 -3
  45. package/crates/tish_runtime/src/promise_io.rs +2 -3
  46. package/crates/tish_runtime/src/ws.rs +7 -7
  47. package/crates/tish_ui/Cargo.toml +17 -0
  48. package/crates/tish_ui/src/jsx.rs +390 -0
  49. package/crates/tish_ui/src/lib.rs +16 -0
  50. package/crates/tish_ui/src/runtime/hooks.rs +122 -0
  51. package/crates/tish_ui/src/runtime/mod.rs +173 -0
  52. package/crates/tish_vm/src/vm.rs +121 -27
  53. package/justfile +3 -3
  54. package/package.json +1 -1
  55. package/platform/darwin-arm64/tish +0 -0
  56. package/platform/darwin-x64/tish +0 -0
  57. package/platform/linux-arm64/tish +0 -0
  58. package/platform/linux-x64/tish +0 -0
  59. package/platform/win32-x64/tish.exe +0 -0
  60. package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
@@ -1,7 +1,6 @@
1
1
  //! Web Fetch–aligned Response, ReadableStream, reader.read(), text()/json().
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::collections::HashMap;
5
4
  use std::pin::Pin;
6
5
  use std::rc::Rc;
7
6
  use std::sync::{Arc, Mutex};
@@ -9,7 +8,7 @@ use std::sync::{Arc, Mutex};
9
8
  use bytes::Bytes;
10
9
  use futures::Stream;
11
10
  use futures::StreamExt;
12
- use tishlang_core::{NativeFn, TishOpaque, TishPromise, Value};
11
+ use tishlang_core::{NativeFn, ObjectMap, TishOpaque, TishPromise, Value};
13
12
 
14
13
  use crate::http::{build_error_response, extract_body, extract_headers, extract_method};
15
14
 
@@ -77,14 +76,14 @@ impl TishPromise for ReadChunkPromise {
77
76
  let r = crate::http::block_on_http(rx);
78
77
  match r {
79
78
  Ok(Ok(ReadChunk::Done)) => {
80
- let mut o = HashMap::new();
79
+ let mut o = ObjectMap::default();
81
80
  o.insert(Arc::from("done"), Value::Bool(true));
82
81
  o.insert(Arc::from("value"), Value::Null);
83
82
  Ok(Value::Object(Rc::new(RefCell::new(o))))
84
83
  }
85
84
  Ok(Ok(ReadChunk::Bytes(b))) => {
86
85
  let arr: Vec<Value> = b.iter().map(|u| Value::Number(*u as f64)).collect();
87
- let mut o = HashMap::new();
86
+ let mut o = ObjectMap::default();
88
87
  o.insert(Arc::from("done"), Value::Bool(false));
89
88
  o.insert(
90
89
  Arc::from("value"),
@@ -93,7 +92,7 @@ impl TishPromise for ReadChunkPromise {
93
92
  Ok(Value::Object(Rc::new(RefCell::new(o))))
94
93
  }
95
94
  Ok(Err(e)) => Err({
96
- let mut obj = HashMap::new();
95
+ let mut obj = ObjectMap::default();
97
96
  obj.insert(Arc::from("error"), Value::String(e.into()));
98
97
  Value::Object(Rc::new(RefCell::new(obj)))
99
98
  }),
@@ -118,13 +117,13 @@ impl TishPromise for JsonTextPromise {
118
117
  Ok(Ok(s)) => match tishlang_core::json_parse(&s) {
119
118
  Ok(v) => Ok(v),
120
119
  Err(e) => Err({
121
- let mut obj = HashMap::new();
120
+ let mut obj = ObjectMap::default();
122
121
  obj.insert(Arc::from("error"), Value::String(e.into()));
123
122
  Value::Object(Rc::new(RefCell::new(obj)))
124
123
  }),
125
124
  },
126
125
  Ok(Err(e)) => Err({
127
- let mut obj = HashMap::new();
126
+ let mut obj = ObjectMap::default();
128
127
  obj.insert(Arc::from("error"), Value::String(e.into()));
129
128
  Value::Object(Rc::new(RefCell::new(obj)))
130
129
  }),
@@ -226,7 +225,7 @@ impl TishOpaque for HttpReadableStream {
226
225
  }))
227
226
  }
228
227
  Err(e) => {
229
- let mut m = HashMap::new();
228
+ let mut m = ObjectMap::default();
230
229
  m.insert(Arc::from("error"), Value::String(e.into()));
231
230
  Value::Object(Rc::new(RefCell::new(m)))
232
231
  }
@@ -283,7 +282,7 @@ impl TishOpaque for HttpStreamReader {
283
282
  }
284
283
 
285
284
  fn headers_to_value(headers: &reqwest::header::HeaderMap) -> Value {
286
- let mut headers_obj: HashMap<Arc<str>, Value> = HashMap::with_capacity(headers.len());
285
+ let mut headers_obj: ObjectMap = ObjectMap::with_capacity(headers.len());
287
286
  for (key, value) in headers.iter() {
288
287
  if let Ok(v) = value.to_str() {
289
288
  headers_obj.insert(Arc::from(key.as_str()), Value::String(v.into()));
@@ -327,7 +326,7 @@ pub fn response_value_from_reqwest(response: reqwest::Response) -> Value {
327
326
  rx: Mutex::new(Some(rx)),
328
327
  }))
329
328
  });
330
- let mut obj: HashMap<Arc<str>, Value> = HashMap::new();
329
+ let mut obj: ObjectMap = ObjectMap::default();
331
330
  obj.insert(Arc::from("status"), Value::Number(status));
332
331
  obj.insert(Arc::from("ok"), Value::Bool(ok));
333
332
  obj.insert(Arc::from("headers"), headers_val);
@@ -13,8 +13,15 @@ use tishlang_builtins::helpers::extract_num;
13
13
  #[cfg(feature = "fs")]
14
14
  use tishlang_builtins::helpers::make_error_value;
15
15
 
16
+ pub use tishlang_core::ObjectMap;
16
17
  pub use tishlang_core::Value;
17
18
 
19
+ pub use tishlang_builtins::construct::{
20
+ audio_context_constructor_value as tish_audio_context_constructor,
21
+ construct as tish_construct,
22
+ uint8_array_constructor_value as tish_uint8_array_constructor,
23
+ };
24
+
18
25
  // Re-export array methods from tishlang_builtins
19
26
  pub use tishlang_builtins::array::{
20
27
  push as array_push_impl,
@@ -754,7 +761,7 @@ pub fn regexp_exec(re: &Value, input: &Value) -> Value {
754
761
 
755
762
  #[cfg(feature = "regex")]
756
763
  fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
757
- use std::collections::HashMap;
764
+ use tishlang_core::ObjectMap;
758
765
 
759
766
  let start = if re.flags.global || re.flags.sticky {
760
767
  re.last_index
@@ -785,7 +792,7 @@ fn regexp_exec_impl(re: &mut tishlang_core::TishRegExp, input: &str) -> Value {
785
792
  let match_byte_start = byte_start + full_match.start();
786
793
  let match_char_index = input[..match_byte_start].chars().count();
787
794
 
788
- let mut obj: HashMap<std::sync::Arc<str>, Value> = HashMap::new();
795
+ let mut obj: ObjectMap = ObjectMap::default();
789
796
  obj.insert(Arc::from("0"), Value::String(full_match.as_str().into()));
790
797
  for i in 1..caps.len() {
791
798
  let val = match caps.get(i) {
@@ -1,10 +1,9 @@
1
1
  //! Promise static methods for compiled Tish (resolve, reject, all, race).
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::collections::HashMap;
5
4
  use std::rc::Rc;
6
5
  use std::sync::Arc;
7
- use tishlang_core::Value;
6
+ use tishlang_core::{ObjectMap, Value};
8
7
 
9
8
  /// Promise.resolve(value) - returns the value (immediate resolve).
10
9
  pub fn promise_resolve(args: &[Value]) -> Value {
@@ -56,7 +55,7 @@ pub fn promise_race(args: &[Value]) -> Value {
56
55
 
57
56
  /// Build the Promise object with resolve, reject, all, race static methods.
58
57
  pub fn promise_object() -> Value {
59
- let mut map: HashMap<Arc<str>, Value> = HashMap::new();
58
+ let mut map: ObjectMap = ObjectMap::default();
60
59
  map.insert(
61
60
  Arc::from("resolve"),
62
61
  Value::Function(Rc::new(|args: &[Value]| promise_resolve(args))),
@@ -1,14 +1,13 @@
1
1
  //! Promises carrying only Send payloads (string results for text(), etc.).
2
2
 
3
3
  use std::cell::RefCell;
4
- use std::collections::HashMap;
5
4
  use std::rc::Rc;
6
5
  use std::sync::{Arc, Mutex};
7
- use tishlang_core::{Value, TishPromise};
6
+ use tishlang_core::{ObjectMap, TishPromise, Value};
8
7
  use tokio::sync::oneshot;
9
8
 
10
9
  fn error_value(msg: String) -> Value {
11
- let mut obj: HashMap<Arc<str>, Value> = HashMap::with_capacity(2);
10
+ let mut obj: ObjectMap = ObjectMap::with_capacity(2);
12
11
  obj.insert(Arc::from("error"), Value::String(msg.into()));
13
12
  obj.insert(Arc::from("ok"), Value::Bool(false));
14
13
  Value::Object(Rc::new(RefCell::new(obj)))
@@ -15,7 +15,7 @@ use std::time::{Duration, Instant};
15
15
 
16
16
  use futures_util::{SinkExt, StreamExt};
17
17
  use lazy_static::lazy_static;
18
- use tishlang_core::Value;
18
+ use tishlang_core::{ObjectMap, Value};
19
19
  use tokio::sync::mpsc as tokio_mpsc;
20
20
  use tokio::runtime::Runtime;
21
21
 
@@ -194,7 +194,7 @@ pub fn ws_broadcast_native(args: &[Value]) -> Value {
194
194
 
195
195
  /// Build connection object: { _id, send, close, readyState, receive }. JS-like.
196
196
  fn conn_object(id: u32) -> Value {
197
- let mut obj: HashMap<Arc<str>, Value> = HashMap::new();
197
+ let mut obj: ObjectMap = ObjectMap::default();
198
198
  obj.insert(Arc::from("_id"), Value::Number(id as f64));
199
199
  obj.insert(Arc::from("readyState"), Value::Number(1.0)); // OPEN
200
200
  obj.insert(
@@ -216,7 +216,7 @@ fn conn_object(id: u32) -> Value {
216
216
  Value::Function(Rc::new(move |_args: &[Value]| {
217
217
  match conn_receive(id) {
218
218
  Some(s) => {
219
- let mut ev: HashMap<Arc<str>, Value> = HashMap::new();
219
+ let mut ev: ObjectMap = ObjectMap::default();
220
220
  ev.insert(Arc::from("data"), Value::String(s.into()));
221
221
  Value::Object(Rc::new(RefCell::new(ev)))
222
222
  }
@@ -237,7 +237,7 @@ fn conn_object(id: u32) -> Value {
237
237
  .unwrap_or(1000);
238
238
  match conn_receive_timeout(id_timeout, timeout_ms) {
239
239
  Some(s) => {
240
- let mut ev: HashMap<Arc<str>, Value> = HashMap::new();
240
+ let mut ev: ObjectMap = ObjectMap::default();
241
241
  ev.insert(Arc::from("data"), Value::String(s.into()));
242
242
  Value::Object(Rc::new(RefCell::new(ev)))
243
243
  }
@@ -529,7 +529,7 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
529
529
  ws
530
530
  });
531
531
 
532
- let mut m: HashMap<Arc<str>, Value> = HashMap::new();
532
+ let mut m: ObjectMap = ObjectMap::default();
533
533
  m.insert(Arc::from("_handle"), handle_val);
534
534
  m.insert(Arc::from("_onConnection"), Value::Null);
535
535
  m.insert(Arc::from("clients"), Value::Array(clients));
@@ -549,7 +549,7 @@ mod tests {
549
549
  fn ws_echo_roundtrip() {
550
550
  let port: u16 = 18_742;
551
551
  let opts = {
552
- let mut m: HashMap<Arc<str>, Value> = HashMap::new();
552
+ let mut m: ObjectMap = ObjectMap::default();
553
553
  m.insert(Arc::from("port"), Value::Number(port as f64));
554
554
  Value::Object(Rc::new(RefCell::new(m)))
555
555
  };
@@ -632,7 +632,7 @@ mod tests {
632
632
  fn ws_gateway_agent_flow() {
633
633
  let port: u16 = 18_743;
634
634
  let opts = {
635
- let mut m: HashMap<Arc<str>, Value> = HashMap::new();
635
+ let mut m: ObjectMap = ObjectMap::default();
636
636
  m.insert(Arc::from("port"), Value::Number(port as f64));
637
637
  Value::Object(Rc::new(RefCell::new(m)))
638
638
  };
@@ -0,0 +1,17 @@
1
+ [package]
2
+ name = "tishlang_ui"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Shared JSX lowering and UI runtime (vnode, hooks, host protocol) for Tish"
6
+
7
+ license-file = { workspace = true }
8
+ repository = { workspace = true }
9
+
10
+ [features]
11
+ default = ["runtime"]
12
+ compiler = ["dep:tishlang_ast"]
13
+ runtime = ["dep:tishlang_core"]
14
+
15
+ [dependencies]
16
+ tishlang_ast = { path = "../tish_ast", version = ">=0.1", optional = true }
17
+ tishlang_core = { path = "../tish_core", version = ">=0.1", optional = true }
@@ -0,0 +1,390 @@
1
+ //! Shared JSX lowering: emit `h(tag, props, children)` as JavaScript or Rust (`Value`) source.
2
+
3
+ use tishlang_ast::{
4
+ ArrayElement, Expr, JsxAttrValue, JsxChild, JsxProp, Literal, ObjectProp,
5
+ };
6
+
7
+ /// Escape a Tish identifier for Rust output (matches `tishlang_compile` conventions).
8
+ pub fn escape_ident_rust(s: &str) -> String {
9
+ if s == "await" || s == "default" {
10
+ format!("_{}", s)
11
+ } else {
12
+ s.to_string()
13
+ }
14
+ }
15
+
16
+ /// Emit JSX expression as JavaScript (same rules as legacy `tishlang_compile_js`).
17
+ pub fn emit_jsx_js<F, E>(expr: &Expr, emit_expr: &mut F) -> Result<String, E>
18
+ where
19
+ F: FnMut(&Expr) -> Result<String, E>,
20
+ E: From<String>,
21
+ {
22
+ match expr {
23
+ Expr::JsxElement {
24
+ tag,
25
+ props,
26
+ children,
27
+ ..
28
+ } => {
29
+ let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
30
+ tag.as_ref().to_string()
31
+ } else {
32
+ format!("{:?}", tag.as_ref())
33
+ };
34
+ let props_str = emit_jsx_props_js(props, emit_expr)?;
35
+ let children_strs: Result<Vec<_>, _> =
36
+ children.iter().map(|c| emit_jsx_child_js(c, emit_expr)).collect();
37
+ let children_str = children_strs?.join(", ");
38
+ Ok(format!("h({}, {}, [{}])", tag_str, props_str, children_str))
39
+ }
40
+ Expr::JsxFragment { children, .. } => {
41
+ let children_strs: Result<Vec<_>, _> =
42
+ children.iter().map(|c| emit_jsx_child_js(c, emit_expr)).collect();
43
+ let children_str = children_strs?.join(", ");
44
+ Ok(format!("h(Fragment, null, [{}])", children_str))
45
+ }
46
+ _ => Err(emit_err("emit_jsx_js: not a JSX expression")),
47
+ }
48
+ }
49
+
50
+ fn emit_err<E>(msg: &str) -> E
51
+ where
52
+ E: From<String>,
53
+ {
54
+ E::from(msg.to_string())
55
+ }
56
+
57
+ fn emit_jsx_props_js<F, E>(props: &[JsxProp], emit_expr: &mut F) -> Result<String, E>
58
+ where
59
+ F: FnMut(&Expr) -> Result<String, E>,
60
+ {
61
+ if props.is_empty() {
62
+ return Ok("null".to_string());
63
+ }
64
+ let parts: Result<Vec<_>, _> = props
65
+ .iter()
66
+ .map(|p| match p {
67
+ JsxProp::Attr { name, value } => {
68
+ let val = match value {
69
+ JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
70
+ JsxAttrValue::Expr(e) => emit_expr(e)?,
71
+ JsxAttrValue::ImplicitTrue => "true".to_string(),
72
+ };
73
+ let key = name.as_ref();
74
+ Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
75
+ format!("{}: {}", key, val)
76
+ } else {
77
+ format!("{:?}: {}", key, val)
78
+ })
79
+ }
80
+ JsxProp::Spread(e) => Ok(format!("...{}", emit_expr(e)?)),
81
+ })
82
+ .collect();
83
+ Ok(format!("{{ {} }}", parts?.join(", ")))
84
+ }
85
+
86
+ fn emit_jsx_child_js<F, E>(child: &JsxChild, emit_expr: &mut F) -> Result<String, E>
87
+ where
88
+ F: FnMut(&Expr) -> Result<String, E>,
89
+ {
90
+ match child {
91
+ JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
92
+ JsxChild::Expr(e) => {
93
+ let inner = emit_expr(e)?;
94
+ let needs_string = matches!(
95
+ e,
96
+ Expr::Literal {
97
+ value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
98
+ ..
99
+ }
100
+ );
101
+ Ok(if needs_string {
102
+ format!("String({})", inner)
103
+ } else {
104
+ inner
105
+ })
106
+ }
107
+ }
108
+ }
109
+
110
+ /// Emit JSX as Rust `Value` by calling `tishlang_ui::ui_h` directly (no closure capture of a local `h` binding).
111
+ pub fn emit_jsx_rust<F, E>(expr: &Expr, emit_expr: &mut F) -> Result<String, E>
112
+ where
113
+ F: FnMut(&Expr) -> Result<String, E>,
114
+ E: From<String>,
115
+ {
116
+ match expr {
117
+ Expr::JsxElement {
118
+ tag,
119
+ props,
120
+ children,
121
+ ..
122
+ } => {
123
+ let is_component = tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false);
124
+ let tag_rust = if is_component {
125
+ escape_ident_rust(tag.as_ref())
126
+ } else {
127
+ format!("Value::String({:?}.into())", tag.as_ref())
128
+ };
129
+ let props_rust = emit_jsx_props_rust(props, emit_expr)?;
130
+ let child_parts: Result<Vec<_>, _> = children
131
+ .iter()
132
+ .map(|c| emit_jsx_child_rust(c, emit_expr))
133
+ .collect();
134
+ let children_rust = format!(
135
+ "Value::Array(Rc::new(RefCell::new(vec![{}])))",
136
+ child_parts?.join(", ")
137
+ );
138
+ Ok(wrap_h_call_rust(&tag_rust, &props_rust, &children_rust))
139
+ }
140
+ Expr::JsxFragment { children, .. } => {
141
+ let child_parts: Result<Vec<_>, _> = children
142
+ .iter()
143
+ .map(|c| emit_jsx_child_rust(c, emit_expr))
144
+ .collect();
145
+ let children_rust = format!(
146
+ "Value::Array(Rc::new(RefCell::new(vec![{}])))",
147
+ child_parts?.join(", ")
148
+ );
149
+ Ok(wrap_h_call_rust(
150
+ "Fragment",
151
+ "Value::Null",
152
+ &children_rust,
153
+ ))
154
+ }
155
+ _ => Err(E::from("emit_jsx_rust: not a JSX expression".to_string())),
156
+ }
157
+ }
158
+
159
+ fn wrap_h_call_rust(tag: &str, props: &str, children: &str) -> String {
160
+ format!(
161
+ "tishlang_ui::ui_h(&[({}).clone(), ({}).clone(), ({}).clone()])",
162
+ tag, props, children
163
+ )
164
+ }
165
+
166
+ fn emit_jsx_props_rust<F, E>(props: &[JsxProp], emit_expr: &mut F) -> Result<String, E>
167
+ where
168
+ F: FnMut(&Expr) -> Result<String, E>,
169
+ E: From<String>,
170
+ {
171
+ if props.is_empty() {
172
+ return Ok("Value::Null".to_string());
173
+ }
174
+ let has_spread = props.iter().any(|p| matches!(p, JsxProp::Spread(_)));
175
+ if has_spread {
176
+ let mut parts = Vec::new();
177
+ for prop in props {
178
+ match prop {
179
+ JsxProp::Attr { name, value } => {
180
+ let val = match value {
181
+ JsxAttrValue::String(s) => {
182
+ format!("Value::String({:?}.into())", s.as_ref())
183
+ }
184
+ JsxAttrValue::Expr(e) => emit_expr(e)?,
185
+ JsxAttrValue::ImplicitTrue => "Value::Bool(true)".to_string(),
186
+ };
187
+ parts.push(format!(
188
+ "_obj.insert(Arc::from({:?}), ({}).clone());",
189
+ name.as_ref(),
190
+ val
191
+ ));
192
+ }
193
+ JsxProp::Spread(e) => {
194
+ let val = emit_expr(e)?;
195
+ parts.push(format!(
196
+ "if let Value::Object(ref _spread) = {} {{ for (k, v) in _spread.borrow().iter() {{ _obj.insert(Arc::clone(k), v.clone()); }} }}",
197
+ val
198
+ ));
199
+ }
200
+ }
201
+ }
202
+ Ok(format!(
203
+ "{{ let mut _obj: ObjectMap = ObjectMap::default(); {} Value::Object(Rc::new(RefCell::new(_obj))) }}",
204
+ parts.join(" ")
205
+ ))
206
+ } else {
207
+ let mut kv = Vec::new();
208
+ for prop in props {
209
+ if let JsxProp::Attr { name, value } = prop {
210
+ let val = match value {
211
+ JsxAttrValue::String(s) => {
212
+ format!("Value::String({:?}.into())", s.as_ref())
213
+ }
214
+ JsxAttrValue::Expr(e) => emit_expr(e)?,
215
+ JsxAttrValue::ImplicitTrue => "Value::Bool(true)".to_string(),
216
+ };
217
+ kv.push(format!(
218
+ "(Arc::from({:?}), ({}).clone())",
219
+ name.as_ref(),
220
+ val
221
+ ));
222
+ }
223
+ }
224
+ Ok(format!(
225
+ "Value::Object(Rc::new(RefCell::new(ObjectMap::from([{}]))))",
226
+ kv.join(", ")
227
+ ))
228
+ }
229
+ }
230
+
231
+ fn emit_jsx_child_rust<F, E>(child: &JsxChild, emit_expr: &mut F) -> Result<String, E>
232
+ where
233
+ F: FnMut(&Expr) -> Result<String, E>,
234
+ E: From<String>,
235
+ {
236
+ match child {
237
+ JsxChild::Text(s) => Ok(format!("Value::String({:?}.into())", s.as_ref())),
238
+ JsxChild::Expr(e) => {
239
+ let inner = emit_expr(e)?;
240
+ let needs_string = matches!(
241
+ e,
242
+ Expr::Literal {
243
+ value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
244
+ ..
245
+ }
246
+ );
247
+ Ok(if needs_string {
248
+ format!("Value::String(({}).to_display_string().into())", inner)
249
+ } else {
250
+ format!("({}).clone()", inner)
251
+ })
252
+ }
253
+ }
254
+ }
255
+
256
+ /// Whether the program contains any JSX syntax (for conditional native UI globals).
257
+ pub fn program_contains_jsx(program: &tishlang_ast::Program) -> bool {
258
+ program.statements.iter().any(stmt_contains_jsx)
259
+ }
260
+
261
+ fn stmt_contains_jsx(stmt: &tishlang_ast::Statement) -> bool {
262
+ use tishlang_ast::{ExportDeclaration, Statement};
263
+ match stmt {
264
+ Statement::Block { statements, .. } => statements.iter().any(stmt_contains_jsx),
265
+ Statement::VarDecl { init, .. } => init.as_ref().is_some_and(expr_contains_jsx),
266
+ Statement::VarDeclDestructure { init, .. } => expr_contains_jsx(init),
267
+ Statement::ExprStmt { expr, .. } => expr_contains_jsx(expr),
268
+ Statement::Return { value, .. } => value.as_ref().is_some_and(expr_contains_jsx),
269
+ Statement::If {
270
+ cond,
271
+ then_branch,
272
+ else_branch,
273
+ ..
274
+ } => {
275
+ expr_contains_jsx(cond)
276
+ || stmt_contains_jsx(then_branch)
277
+ || else_branch.as_ref().is_some_and(|s| stmt_contains_jsx(s))
278
+ }
279
+ Statement::While { cond, body, .. } | Statement::DoWhile { body, cond, .. } => {
280
+ expr_contains_jsx(cond) || stmt_contains_jsx(body)
281
+ }
282
+ Statement::For { init, cond, update, body, .. } => {
283
+ init.as_ref().is_some_and(|s| stmt_contains_jsx(s))
284
+ || cond.as_ref().is_some_and(expr_contains_jsx)
285
+ || update.as_ref().is_some_and(expr_contains_jsx)
286
+ || stmt_contains_jsx(body)
287
+ }
288
+ Statement::ForOf { iterable, body, .. } => {
289
+ expr_contains_jsx(iterable) || stmt_contains_jsx(body)
290
+ }
291
+ Statement::Switch { expr, cases, default_body, .. } => {
292
+ expr_contains_jsx(expr)
293
+ || cases.iter().any(|(e, ss)| {
294
+ e.as_ref().is_some_and(expr_contains_jsx) || ss.iter().any(stmt_contains_jsx)
295
+ })
296
+ || default_body
297
+ .as_ref()
298
+ .is_some_and(|ss| ss.iter().any(stmt_contains_jsx))
299
+ }
300
+ Statement::Try {
301
+ body,
302
+ catch_body,
303
+ finally_body,
304
+ ..
305
+ } => {
306
+ stmt_contains_jsx(body)
307
+ || catch_body.as_ref().is_some_and(|s| stmt_contains_jsx(s))
308
+ || finally_body.as_ref().is_some_and(|s| stmt_contains_jsx(s))
309
+ }
310
+ Statement::FunDecl { body, .. } => stmt_contains_jsx(body),
311
+ Statement::Throw { value, .. } => expr_contains_jsx(value),
312
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
313
+ ExportDeclaration::Named(inner) => stmt_contains_jsx(inner),
314
+ ExportDeclaration::Default(e) => expr_contains_jsx(e),
315
+ },
316
+ Statement::Import { .. } | Statement::Break { .. } | Statement::Continue { .. } => false,
317
+ }
318
+ }
319
+
320
+ fn expr_contains_jsx(expr: &Expr) -> bool {
321
+ match expr {
322
+ Expr::JsxElement { .. } | Expr::JsxFragment { .. } => true,
323
+ Expr::Binary { left, right, .. } => expr_contains_jsx(left) || expr_contains_jsx(right),
324
+ Expr::Unary { operand, .. } => expr_contains_jsx(operand),
325
+ Expr::Assign { value, .. } => expr_contains_jsx(value),
326
+ Expr::Call { callee, args, .. } => {
327
+ expr_contains_jsx(callee)
328
+ || args.iter().any(|a| match a {
329
+ tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
330
+ expr_contains_jsx(e)
331
+ }
332
+ })
333
+ }
334
+ Expr::Member { object, prop, .. } => {
335
+ expr_contains_jsx(object)
336
+ || matches!(prop, tishlang_ast::MemberProp::Expr(e) if expr_contains_jsx(e))
337
+ }
338
+ Expr::Index { object, index, .. } => expr_contains_jsx(object) || expr_contains_jsx(index),
339
+ Expr::Conditional {
340
+ cond,
341
+ then_branch,
342
+ else_branch,
343
+ ..
344
+ } => {
345
+ expr_contains_jsx(cond)
346
+ || expr_contains_jsx(then_branch)
347
+ || expr_contains_jsx(else_branch)
348
+ }
349
+ Expr::Array { elements, .. } => elements.iter().any(|el| match el {
350
+ ArrayElement::Expr(e) | ArrayElement::Spread(e) => expr_contains_jsx(e),
351
+ }),
352
+ Expr::Object { props, .. } => props.iter().any(|p| match p {
353
+ ObjectProp::KeyValue(_, e) | ObjectProp::Spread(e) => expr_contains_jsx(e),
354
+ }),
355
+ Expr::ArrowFunction { body, .. } => match body {
356
+ tishlang_ast::ArrowBody::Expr(e) => expr_contains_jsx(e),
357
+ tishlang_ast::ArrowBody::Block(s) => stmt_contains_jsx(s),
358
+ },
359
+ Expr::NullishCoalesce { left, right, .. } => {
360
+ expr_contains_jsx(left) || expr_contains_jsx(right)
361
+ }
362
+ Expr::TemplateLiteral { exprs, .. } => exprs.iter().any(expr_contains_jsx),
363
+ Expr::Await { operand, .. } => expr_contains_jsx(operand),
364
+ Expr::TypeOf { operand, .. } => expr_contains_jsx(operand),
365
+ Expr::PostfixInc { .. }
366
+ | Expr::PrefixInc { .. }
367
+ | Expr::PostfixDec { .. }
368
+ | Expr::PrefixDec { .. } => false,
369
+ Expr::CompoundAssign { value, .. } | Expr::LogicalAssign { value, .. } => {
370
+ expr_contains_jsx(value)
371
+ }
372
+ Expr::MemberAssign { object, value, .. } => {
373
+ expr_contains_jsx(object) || expr_contains_jsx(value)
374
+ }
375
+ Expr::IndexAssign { object, index, value, .. } => {
376
+ expr_contains_jsx(object) || expr_contains_jsx(index) || expr_contains_jsx(value)
377
+ }
378
+ Expr::New { callee, args, .. } => {
379
+ expr_contains_jsx(callee)
380
+ || args.iter().any(|a| match a {
381
+ tishlang_ast::CallArg::Expr(e) | tishlang_ast::CallArg::Spread(e) => {
382
+ expr_contains_jsx(e)
383
+ }
384
+ })
385
+ }
386
+ Expr::Literal { .. }
387
+ | Expr::Ident { .. }
388
+ | Expr::NativeModuleLoad { .. } => false,
389
+ }
390
+ }
@@ -0,0 +1,16 @@
1
+ //! JSX lowering (compiler) and UI runtime (vnode + hooks + host) for cross-target Tish UI.
2
+ //!
3
+ //! - Feature **`compiler`**: AST → JS / Rust `h(...)` emission helpers (depends on `tishlang_ast`).
4
+ //! - Feature **`runtime`**: `Value`-based `h`, `Fragment`, hooks, and [`Host`] (depends on `tishlang_core`).
5
+
6
+ #[cfg(feature = "compiler")]
7
+ pub mod jsx;
8
+
9
+ #[cfg(feature = "runtime")]
10
+ pub mod runtime;
11
+
12
+ #[cfg(feature = "runtime")]
13
+ pub use runtime::{
14
+ fragment_value, install_thread_local_host, native_create_root, native_use_state, ui_h,
15
+ ui_text, with_thread_local_host, Host, HeadlessHost, FRAGMENT_SENTINEL,
16
+ };