@tishlang/tish 1.0.29 → 1.0.34

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 (68) 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 +604 -106
  17. package/crates/tish_compile/src/infer.rs +236 -0
  18. package/crates/tish_compile/src/lib.rs +52 -5
  19. package/crates/tish_compile/src/types.rs +42 -5
  20. package/crates/tish_compile_js/Cargo.toml +1 -0
  21. package/crates/tish_compile_js/src/codegen.rs +38 -94
  22. package/crates/tish_compile_js/src/lib.rs +0 -1
  23. package/crates/tish_compile_js/src/tests_jsx.rs +68 -0
  24. package/crates/tish_core/Cargo.toml +4 -0
  25. package/crates/tish_core/src/console_style.rs +7 -1
  26. package/crates/tish_core/src/json.rs +1 -2
  27. package/crates/tish_core/src/macros.rs +2 -3
  28. package/crates/tish_core/src/value.rs +13 -5
  29. package/crates/tish_cranelift/src/lib.rs +6 -4
  30. package/crates/tish_cranelift/src/lower.rs +5 -3
  31. package/crates/tish_cranelift_runtime/src/lib.rs +4 -2
  32. package/crates/tish_eval/Cargo.toml +2 -0
  33. package/crates/tish_eval/src/eval.rs +172 -79
  34. package/crates/tish_eval/src/http.rs +3 -4
  35. package/crates/tish_eval/src/lib.rs +7 -0
  36. package/crates/tish_eval/src/regex.rs +3 -2
  37. package/crates/tish_eval/src/value.rs +11 -13
  38. package/crates/tish_eval/src/value_convert.rs +4 -8
  39. package/crates/tish_fmt/src/lib.rs +49 -10
  40. package/crates/tish_lexer/src/token.rs +2 -0
  41. package/crates/tish_lint/src/lib.rs +9 -0
  42. package/crates/tish_llvm/src/lib.rs +4 -4
  43. package/crates/tish_lsp/README.md +1 -1
  44. package/crates/tish_native/src/build.rs +16 -2
  45. package/crates/tish_native/src/lib.rs +10 -11
  46. package/crates/tish_opt/src/lib.rs +15 -0
  47. package/crates/tish_parser/src/lib.rs +101 -1
  48. package/crates/tish_parser/src/parser.rs +168 -51
  49. package/crates/tish_runtime/src/http.rs +4 -5
  50. package/crates/tish_runtime/src/http_fetch.rs +17 -10
  51. package/crates/tish_runtime/src/lib.rs +9 -2
  52. package/crates/tish_runtime/src/promise.rs +2 -3
  53. package/crates/tish_runtime/src/promise_io.rs +2 -3
  54. package/crates/tish_runtime/src/ws.rs +7 -7
  55. package/crates/tish_ui/Cargo.toml +17 -0
  56. package/crates/tish_ui/src/jsx.rs +390 -0
  57. package/crates/tish_ui/src/lib.rs +16 -0
  58. package/crates/tish_ui/src/runtime/hooks.rs +122 -0
  59. package/crates/tish_ui/src/runtime/mod.rs +173 -0
  60. package/crates/tish_vm/src/vm.rs +121 -27
  61. package/justfile +3 -3
  62. package/package.json +1 -1
  63. package/platform/darwin-arm64/tish +0 -0
  64. package/platform/darwin-x64/tish +0 -0
  65. package/platform/linux-arm64/tish +0 -0
  66. package/platform/linux-x64/tish +0 -0
  67. package/platform/win32-x64/tish.exe +0 -0
  68. package/crates/tish_compile_js/src/js_intrinsics.rs +0 -82
@@ -0,0 +1,236 @@
1
+ //! Type inference pass: annotates `VarDecl` nodes with inferred `TypeAnnotation`s
2
+ //! where the user hasn't provided them, enabling codegen to emit native Rust types.
3
+ //!
4
+ //! Rules (conservative — only infer when unambiguous):
5
+ //! - Number literal init → `number`
6
+ //! - String literal init → `string`
7
+ //! - Bool literal init → `boolean`
8
+ //! - Arithmetic of two `number` expressions → `number`
9
+ //! - Comparison of two `number` expressions → `boolean`
10
+ //! - Already-annotated vars are left unchanged.
11
+
12
+ use std::collections::HashMap;
13
+ use tishlang_ast::{
14
+ ArrowBody, BinOp, CallArg, Expr, FunParam, Literal, Program, Statement, TypeAnnotation,
15
+ };
16
+
17
+ /// Scoped type environment used during inference.
18
+ #[derive(Default)]
19
+ pub struct InferCtx {
20
+ scopes: Vec<HashMap<String, TypeAnnotation>>,
21
+ }
22
+
23
+ impl InferCtx {
24
+ pub fn new() -> Self {
25
+ Self { scopes: vec![HashMap::new()] }
26
+ }
27
+
28
+ fn push_scope(&mut self) {
29
+ self.scopes.push(HashMap::new());
30
+ }
31
+
32
+ fn pop_scope(&mut self) {
33
+ self.scopes.pop();
34
+ }
35
+
36
+ fn define(&mut self, name: &str, ty: TypeAnnotation) {
37
+ if let Some(s) = self.scopes.last_mut() {
38
+ s.insert(name.to_string(), ty);
39
+ }
40
+ }
41
+
42
+ pub fn lookup(&self, name: &str) -> Option<&TypeAnnotation> {
43
+ for s in self.scopes.iter().rev() {
44
+ if let Some(t) = s.get(name) {
45
+ return Some(t);
46
+ }
47
+ }
48
+ None
49
+ }
50
+ }
51
+
52
+ fn is_number(ann: &TypeAnnotation) -> bool {
53
+ matches!(ann, TypeAnnotation::Simple(s) if s.as_ref() == "number")
54
+ }
55
+
56
+ fn number_ann() -> TypeAnnotation {
57
+ TypeAnnotation::Simple("number".into())
58
+ }
59
+
60
+ fn string_ann() -> TypeAnnotation {
61
+ TypeAnnotation::Simple("string".into())
62
+ }
63
+
64
+ fn bool_ann() -> TypeAnnotation {
65
+ TypeAnnotation::Simple("boolean".into())
66
+ }
67
+
68
+ /// Infer the `TypeAnnotation` for an expression, if unambiguous.
69
+ pub fn infer_expr_type(expr: &Expr, ctx: &InferCtx) -> Option<TypeAnnotation> {
70
+ match expr {
71
+ Expr::Literal { value, .. } => match value {
72
+ Literal::Number(_) => Some(number_ann()),
73
+ Literal::String(_) => Some(string_ann()),
74
+ Literal::Bool(_) => Some(bool_ann()),
75
+ Literal::Null => None,
76
+ },
77
+ Expr::Ident { name, .. } => ctx.lookup(name.as_ref()).cloned(),
78
+ Expr::Binary { left, op, right, .. } => {
79
+ let lt = infer_expr_type(left, ctx)?;
80
+ let rt = infer_expr_type(right, ctx)?;
81
+ if is_number(&lt) && is_number(&rt) {
82
+ match op {
83
+ BinOp::Add
84
+ | BinOp::Sub
85
+ | BinOp::Mul
86
+ | BinOp::Div
87
+ | BinOp::Mod
88
+ | BinOp::Pow => Some(number_ann()),
89
+ BinOp::Lt
90
+ | BinOp::Le
91
+ | BinOp::Gt
92
+ | BinOp::Ge
93
+ | BinOp::StrictEq
94
+ | BinOp::StrictNe => Some(bool_ann()),
95
+ _ => None,
96
+ }
97
+ } else {
98
+ None
99
+ }
100
+ }
101
+ Expr::Unary { op, operand, .. } => {
102
+ use tishlang_ast::UnaryOp;
103
+ match op {
104
+ UnaryOp::Neg | UnaryOp::Pos => {
105
+ let t = infer_expr_type(operand, ctx)?;
106
+ if is_number(&t) { Some(number_ann()) } else { None }
107
+ }
108
+ UnaryOp::Not => Some(bool_ann()),
109
+ _ => None,
110
+ }
111
+ }
112
+ _ => None,
113
+ }
114
+ }
115
+
116
+ /// Run inference over a program, returning a modified Program with additional
117
+ /// type annotations filled in on `VarDecl` nodes.
118
+ pub fn infer_program(program: &Program) -> Program {
119
+ let mut ctx = InferCtx::new();
120
+ Program { statements: infer_statements(&program.statements, &mut ctx) }
121
+ }
122
+
123
+ fn infer_statements(stmts: &[Statement], ctx: &mut InferCtx) -> Vec<Statement> {
124
+ stmts.iter().map(|s| infer_statement(s, ctx)).collect()
125
+ }
126
+
127
+ fn infer_statement(stmt: &Statement, ctx: &mut InferCtx) -> Statement {
128
+ match stmt {
129
+ Statement::VarDecl { name, mutable, type_ann, init, span } => {
130
+ // Already annotated — propagate into ctx but don't change the node.
131
+ if let Some(ann) = type_ann {
132
+ ctx.define(name.as_ref(), ann.clone());
133
+ return stmt.clone();
134
+ }
135
+ // Try to infer from init expression.
136
+ let inferred = init.as_ref().and_then(|e| infer_expr_type(e, ctx));
137
+ if let Some(ref ann) = inferred {
138
+ ctx.define(name.as_ref(), ann.clone());
139
+ }
140
+ Statement::VarDecl {
141
+ name: name.clone(),
142
+ mutable: *mutable,
143
+ type_ann: inferred,
144
+ init: init.clone(),
145
+ span: *span,
146
+ }
147
+ }
148
+ Statement::Block { statements, span } => {
149
+ ctx.push_scope();
150
+ let stmts = infer_statements(statements, ctx);
151
+ ctx.pop_scope();
152
+ Statement::Block { statements: stmts, span: *span }
153
+ }
154
+ Statement::For { init, cond, update, body, span } => {
155
+ // Scope for loop variable
156
+ ctx.push_scope();
157
+ let new_init = init.as_ref().map(|i| Box::new(infer_statement(i, ctx)));
158
+ let new_body = Box::new(infer_statement(body, ctx));
159
+ ctx.pop_scope();
160
+ Statement::For {
161
+ init: new_init,
162
+ cond: cond.clone(),
163
+ update: update.clone(),
164
+ body: new_body,
165
+ span: *span,
166
+ }
167
+ }
168
+ Statement::ForOf { name, iterable, body, span } => {
169
+ ctx.push_scope();
170
+ let new_body = Box::new(infer_statement(body, ctx));
171
+ ctx.pop_scope();
172
+ Statement::ForOf {
173
+ name: name.clone(),
174
+ iterable: iterable.clone(),
175
+ body: new_body,
176
+ span: *span,
177
+ }
178
+ }
179
+ Statement::While { cond, body, span } => {
180
+ ctx.push_scope();
181
+ let new_body = Box::new(infer_statement(body, ctx));
182
+ ctx.pop_scope();
183
+ Statement::While { cond: cond.clone(), body: new_body, span: *span }
184
+ }
185
+ Statement::DoWhile { body, cond, span } => {
186
+ ctx.push_scope();
187
+ let new_body = Box::new(infer_statement(body, ctx));
188
+ ctx.pop_scope();
189
+ Statement::DoWhile { body: new_body, cond: cond.clone(), span: *span }
190
+ }
191
+ Statement::If { cond, then_branch, else_branch, span } => {
192
+ let new_then = Box::new(infer_statement(then_branch, ctx));
193
+ let new_else = else_branch.as_ref().map(|e| Box::new(infer_statement(e, ctx)));
194
+ Statement::If {
195
+ cond: cond.clone(),
196
+ then_branch: new_then,
197
+ else_branch: new_else,
198
+ span: *span,
199
+ }
200
+ }
201
+ Statement::FunDecl { async_, name, params, rest_param, return_type, body, span } => {
202
+ ctx.push_scope();
203
+ for p in params {
204
+ if let FunParam::Simple(tp) = p {
205
+ if let Some(ann) = &tp.type_ann {
206
+ ctx.define(tp.name.as_ref(), ann.clone());
207
+ }
208
+ }
209
+ }
210
+ if let Some(rp) = rest_param {
211
+ if let Some(ann) = &rp.type_ann {
212
+ ctx.define(rp.name.as_ref(), ann.clone());
213
+ }
214
+ }
215
+ let new_body = Box::new(infer_statement(body, ctx));
216
+ ctx.pop_scope();
217
+ Statement::FunDecl {
218
+ async_: *async_,
219
+ name: name.clone(),
220
+ params: params.clone(),
221
+ rest_param: rest_param.clone(),
222
+ return_type: return_type.clone(),
223
+ body: new_body,
224
+ span: *span,
225
+ }
226
+ }
227
+ // For statements with no interesting sub-structure, clone as-is.
228
+ _ => stmt.clone(),
229
+ }
230
+ }
231
+
232
+ // Suppress unused import warning — CallArg is used indirectly via tishlang_ast.
233
+ #[allow(dead_code)]
234
+ fn _uses_call_arg(_: &CallArg) {}
235
+ #[allow(dead_code)]
236
+ fn _uses_arrow_body(_: &ArrowBody) {}
@@ -3,6 +3,7 @@
3
3
  //! Emits Rust source that links to tishlang_runtime.
4
4
 
5
5
  mod codegen;
6
+ mod infer;
6
7
  mod resolve;
7
8
  mod types;
8
9
 
@@ -25,6 +26,9 @@ mod tests {
25
26
 
26
27
  #[test]
27
28
  fn typed_assign_conversion() {
29
+ // With the inference pass and native emit, `total: number = 0` becomes f64.
30
+ // Assignment `total = total + n` (where n comes from ForOf over a Value::Array)
31
+ // emits a native f64 assignment that unboxes the Value result via from_value_expr.
28
32
  let src = r#"
29
33
  fn sum(...args: number[]): number {
30
34
  let total: number = 0
@@ -34,11 +38,16 @@ fn sum(...args: number[]): number {
34
38
  "#;
35
39
  let program = parse(src).unwrap();
36
40
  let rust = compile(&program).unwrap();
37
- assert!(rust.contains("match &_v { Value::Number(n) => *n"), "expected typed assign conversion");
41
+ // total should be declared as f64
42
+ assert!(rust.contains("let mut total: f64"), "expected total: f64");
43
+ // The return value of run() should convert total back to Value
44
+ assert!(rust.contains("Value::Number(total)"), "expected Value::Number(total) wrapping");
38
45
  }
39
46
 
40
47
  #[test]
41
48
  fn loop_var_decl_clone_outer_var() {
49
+ // With inference, outerVar = 42 gets inferred as f64. f64 is Copy, so no clone is
50
+ // needed — direct assignment is correct. The test verifies compilation succeeds.
42
51
  let src = r#"
43
52
  let outerVar = 42
44
53
  for (let i = 0; i < 5; i = i + 1) {
@@ -47,14 +56,51 @@ for (let i = 0; i < 5; i = i + 1) {
47
56
  "#;
48
57
  let program = parse(src).unwrap();
49
58
  let rust = compile(&program).unwrap();
59
+ // outerVar and x are f64 (inferred) — Copy assignment, no .clone() needed.
60
+ assert!(rust.contains("let mut outerVar: f64"), "expected outerVar: f64");
61
+ assert!(rust.contains("let mut x: f64"), "expected x: f64");
62
+ }
63
+
64
+ #[test]
65
+ fn new_expression_lowers_to_construct_on_native() {
66
+ let src = "fn f() { return new Uint8Array(4) }";
67
+ let program = parse(src).unwrap();
68
+ let rust = compile(&program).unwrap();
69
+ assert!(
70
+ rust.contains("tish_construct"),
71
+ "expected new to lower to tish_construct, got snippet missing it"
72
+ );
73
+ }
74
+
75
+ /// User-defined constructor name: `new ClassName(...)` must compile natively (host `construct`)
76
+ /// and is the same surface syntax as the JS target (`new` in emitted JavaScript).
77
+ #[test]
78
+ fn new_class_name_compiles_native_via_tish_construct() {
79
+ let src = r#"
80
+ fn ClassName(x) {
81
+ return x
82
+ }
83
+ fn factory() {
84
+ return new ClassName(42)
85
+ }
86
+ "#;
87
+ let program = parse(src).unwrap();
88
+ let rust = compile(&program).unwrap();
89
+ assert!(
90
+ rust.contains("tish_construct"),
91
+ "expected new ClassName to lower to tish_construct"
92
+ );
50
93
  assert!(
51
- rust.contains("(outerVar).clone()"),
52
- "expected outerVar to be cloned in loop body"
94
+ rust.contains("ClassName"),
95
+ "expected emitted Rust to reference ClassName callable"
53
96
  );
54
97
  }
55
98
 
56
99
  #[test]
57
100
  fn loop_var_decl_clone_via_project_full() {
101
+ // With the inference pass, `let outerVar = 42` is inferred as f64 (Copy) — no clone needed.
102
+ // This test verifies the full benchmark_granular project compiles and that outerVar
103
+ // is emitted as the inferred f64 type rather than requiring a Value clone.
58
104
  let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
59
105
  let bench = manifest.join("../../tests/core/benchmark_granular.tish").canonicalize().unwrap();
60
106
  // Use same default features as tish CLI (http, fs, process, regex)
@@ -63,9 +109,10 @@ for (let i = 0; i < 5; i = i + 1) {
63
109
  .map(String::from)
64
110
  .collect::<Vec<_>>();
65
111
  let (rust, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
112
+ // outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
66
113
  assert!(
67
- rust.contains("(outerVar).clone()"),
68
- "expected outerVar to be cloned in benchmark_granular loop"
114
+ rust.contains("let mut outerVar: f64"),
115
+ "expected outerVar to be inferred as f64 (Copy, no clone needed)"
69
116
  );
70
117
  }
71
118
  }
@@ -4,7 +4,7 @@
4
4
 
5
5
  use std::collections::HashMap;
6
6
  use std::sync::Arc;
7
- use tishlang_ast::TypeAnnotation;
7
+ use tishlang_ast::{BinOp, TypeAnnotation};
8
8
 
9
9
  /// Concrete Rust type representation for code generation.
10
10
  #[derive(Debug, Clone, PartialEq)]
@@ -89,6 +89,38 @@ impl RustType {
89
89
  !matches!(self, RustType::Value)
90
90
  }
91
91
 
92
+ /// Check if this type is numeric (f64).
93
+ pub fn is_numeric(&self) -> bool {
94
+ matches!(self, RustType::F64)
95
+ }
96
+
97
+ /// Infer the result type of a binary operation given the operand types.
98
+ /// Returns `None` if native code cannot be emitted (fall back to Value path).
99
+ pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
100
+ if lhs == &RustType::F64 && rhs == &RustType::F64 {
101
+ match op {
102
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
103
+ Some(RustType::F64)
104
+ }
105
+ BinOp::Lt
106
+ | BinOp::Le
107
+ | BinOp::Gt
108
+ | BinOp::Ge
109
+ | BinOp::StrictEq
110
+ | BinOp::StrictNe => Some(RustType::Bool),
111
+ _ => None,
112
+ }
113
+ } else if lhs == &RustType::Bool && rhs == &RustType::Bool {
114
+ match op {
115
+ BinOp::And | BinOp::Or => Some(RustType::Bool),
116
+ BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
117
+ _ => None,
118
+ }
119
+ } else {
120
+ None
121
+ }
122
+ }
123
+
92
124
  /// Get the Rust type string for code generation.
93
125
  pub fn to_rust_type_str(&self) -> String {
94
126
  match self {
@@ -169,14 +201,19 @@ impl RustType {
169
201
  match self {
170
202
  RustType::Value => native_expr.to_string(),
171
203
  RustType::F64 => format!("Value::Number({})", native_expr),
172
- RustType::String => format!("Value::String({}.into())", native_expr),
204
+ RustType::String => format!("Value::String({}.clone().into())", native_expr),
173
205
  RustType::Bool => format!("Value::Bool({})", native_expr),
174
206
  RustType::Unit => "Value::Null".to_string(),
175
207
  RustType::Vec(inner) => {
176
- let inner_to_value = inner.to_value_expr("v");
208
+ // Use iter()/copied()/cloned() to avoid moving the vector.
209
+ let (iter_suffix, val_expr) = match inner.as_ref() {
210
+ RustType::F64 => (".iter().copied()", "Value::Number(v)".to_string()),
211
+ RustType::Bool => (".iter().copied()", "Value::Bool(v)".to_string()),
212
+ _ => (".iter().cloned()", inner.to_value_expr("v")),
213
+ };
177
214
  format!(
178
- "Value::Array(Rc::new(RefCell::new({}.into_iter().map(|v| {}).collect())))",
179
- native_expr, inner_to_value
215
+ "Value::Array(Rc::new(RefCell::new({}{}.map(|v| {}).collect())))",
216
+ native_expr, iter_suffix, val_expr
180
217
  )
181
218
  }
182
219
  RustType::Option(inner) => {
@@ -14,3 +14,4 @@ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
14
14
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
15
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
16
16
  tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
17
+ tishlang_ui = { path = "../tish_ui", default-features = false, features = ["compiler"] }
@@ -2,18 +2,15 @@
2
2
 
3
3
  use tishlang_ast::{
4
4
  ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
5
- JsxAttrValue, JsxChild, JsxProp, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program,
6
- Statement, UnaryOp,
5
+ FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
7
6
  };
8
7
 
9
8
  use crate::error::CompileError;
10
- use crate::js_intrinsics::{JsIntrinsic, JsIntrinsics};
11
9
 
12
10
  struct Codegen {
13
11
  output: String,
14
12
  indent: usize,
15
13
  in_async: bool,
16
- intrinsics: JsIntrinsics,
17
14
  }
18
15
 
19
16
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
@@ -29,7 +26,6 @@ impl Codegen {
29
26
  output: String::new(),
30
27
  indent: 0,
31
28
  in_async: false,
32
- intrinsics: JsIntrinsics::new(),
33
29
  }
34
30
  }
35
31
 
@@ -61,9 +57,6 @@ impl Codegen {
61
57
  for stmt in &program.statements {
62
58
  self.emit_statement(stmt)?;
63
59
  }
64
- self.output = self
65
- .intrinsics
66
- .prepend_runtime_preamble(std::mem::take(&mut self.output));
67
60
  Ok(())
68
61
  }
69
62
 
@@ -304,20 +297,34 @@ impl Codegen {
304
297
 
305
298
  fn emit_params(
306
299
  &mut self,
307
- params: &[tishlang_ast::TypedParam],
300
+ params: &[FunParam],
308
301
  rest_param: Option<&tishlang_ast::TypedParam>,
309
302
  ) -> Result<String, CompileError> {
310
- let mut parts: Vec<String> = params
311
- .iter()
312
- .map(|p| {
313
- let n = Self::escape_ident(p.name.as_ref());
314
- if let Some(ref d) = p.default {
315
- format!("{} = {}", n, self.emit_expr(d).unwrap())
316
- } else {
317
- n
303
+ let mut parts: Vec<String> = Vec::new();
304
+ for p in params {
305
+ match p {
306
+ FunParam::Simple(tp) => {
307
+ let n = Self::escape_ident(tp.name.as_ref());
308
+ let s = if let Some(ref d) = tp.default {
309
+ format!("{} = {}", n, self.emit_expr(d)?)
310
+ } else {
311
+ n
312
+ };
313
+ parts.push(s);
314
+ }
315
+ FunParam::Destructure {
316
+ pattern,
317
+ type_ann: _,
318
+ default,
319
+ } => {
320
+ let mut s = self.emit_destruct_pattern(pattern)?;
321
+ if let Some(ref d) = default {
322
+ s = format!("{} = {}", s, self.emit_expr(d)?);
323
+ }
324
+ parts.push(s);
318
325
  }
319
- })
320
- .collect();
326
+ }
327
+ }
321
328
  if let Some(rest) = rest_param {
322
329
  parts.push(format!("...{}", Self::escape_ident(rest.name.as_ref())));
323
330
  }
@@ -421,16 +428,6 @@ impl Codegen {
421
428
  }
422
429
  }
423
430
  Expr::Call { callee, args, .. } => {
424
- if let Some(kind) =
425
- JsIntrinsics::classify_call(callee.as_ref(), args)?
426
- {
427
- self.intrinsics.mark(kind);
428
- if kind == JsIntrinsic::Uint8Array {
429
- let n = self.emit_call_arg(&args[0])?;
430
- return Ok(JsIntrinsics::emit_expr(kind, &n));
431
- }
432
- return Ok(JsIntrinsics::emit_expr(kind, ""));
433
- }
434
431
  let c = self.emit_expr(callee)?;
435
432
  let arg_strs: Result<Vec<_>, _> =
436
433
  args.iter().map(|a| self.emit_call_arg(a)).collect();
@@ -438,6 +435,13 @@ impl Codegen {
438
435
  // Tish uses null for undefined (e.g. empty array pop/shift)
439
436
  format!("({}({}) ?? null)", c, arg_strs)
440
437
  }
438
+ Expr::New { callee, args, .. } => {
439
+ let c = self.emit_expr(callee)?;
440
+ let arg_strs: Result<Vec<_>, _> =
441
+ args.iter().map(|a| self.emit_call_arg(a)).collect();
442
+ let arg_strs = arg_strs?.join(", ");
443
+ format!("(new {}({}) ?? null)", c, arg_strs)
444
+ }
441
445
  Expr::Member {
442
446
  object,
443
447
  prop,
@@ -625,23 +629,11 @@ impl Codegen {
625
629
  let o = self.emit_expr(operand)?;
626
630
  format!("(await {})", o)
627
631
  }
628
- Expr::JsxElement { tag, props, children, .. } => {
629
- let tag_str = if tag.chars().next().map(|c| c.is_uppercase()).unwrap_or(false) {
630
- tag.as_ref().to_string()
631
- } else {
632
- format!("{:?}", tag.as_ref())
633
- };
634
- let props_str = self.emit_jsx_props(props)?;
635
- let children_strs: Result<Vec<_>, _> =
636
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
637
- let children_str = children_strs?.join(", ");
638
- format!("h({}, {}, [{}])", tag_str, props_str, children_str)
639
- }
640
- Expr::JsxFragment { children, .. } => {
641
- let children_strs: Result<Vec<_>, _> =
642
- children.iter().map(|c| self.emit_jsx_child(c)).collect();
643
- let children_str = children_strs?.join(", ");
644
- format!("h(Fragment, null, [{}])", children_str)
632
+ Expr::JsxElement { .. } | Expr::JsxFragment { .. } => {
633
+ tishlang_ui::jsx::emit_jsx_js(expr, &mut |e| {
634
+ self.emit_expr(e).map_err(|ce| ce.message)
635
+ })
636
+ .map_err(|m| CompileError { message: m })?
645
637
  }
646
638
  Expr::NativeModuleLoad { spec, .. } => {
647
639
  return Err(CompileError {
@@ -661,54 +653,6 @@ impl Codegen {
661
653
  }
662
654
  }
663
655
 
664
- fn emit_jsx_props(&mut self, props: &[JsxProp]) -> Result<String, CompileError> {
665
- if props.is_empty() {
666
- return Ok("null".to_string());
667
- }
668
- let parts: Result<Vec<_>, _> = props
669
- .iter()
670
- .map(|p| match p {
671
- JsxProp::Attr { name, value } => {
672
- let val = match value {
673
- JsxAttrValue::String(s) => format!("{:?}", s.as_ref()),
674
- JsxAttrValue::Expr(e) => self.emit_expr(e)?,
675
- JsxAttrValue::ImplicitTrue => "true".to_string(),
676
- };
677
- let key = name.as_ref();
678
- Ok(if key.chars().all(|c| c.is_alphanumeric() || c == '_') {
679
- format!("{}: {}", key, val)
680
- } else {
681
- format!("{:?}: {}", key, val)
682
- })
683
- }
684
- JsxProp::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
685
- })
686
- .collect();
687
- Ok(format!("{{ {} }}", parts?.join(", ")))
688
- }
689
-
690
- fn emit_jsx_child(&mut self, child: &JsxChild) -> Result<String, CompileError> {
691
- match child {
692
- JsxChild::Text(s) => Ok(format!("{:?}", s.as_ref())),
693
- JsxChild::Expr(e) => {
694
- let inner = self.emit_expr(e)?;
695
- // Only wrap literals we know are primitives (number, bool, null). Never wrap:
696
- // string/template (already strings), JSX (elements), Call (components), Array/Ident (may hold elements).
697
- let needs_string = matches!(
698
- e,
699
- Expr::Literal {
700
- value: Literal::Number(_) | Literal::Bool(_) | Literal::Null,
701
- ..
702
- }
703
- );
704
- Ok(if needs_string {
705
- format!("String({})", inner)
706
- } else {
707
- inner
708
- })
709
- }
710
- }
711
- }
712
656
  }
713
657
 
714
658
  /// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
@@ -3,7 +3,6 @@
3
3
 
4
4
  mod codegen;
5
5
  mod error;
6
- mod js_intrinsics;
7
6
 
8
7
  #[cfg(test)]
9
8
  mod tests_jsx;