@tishlang/tish-format 1.0.12 → 2.0.1

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 (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,605 @@
1
+ //! Type system for Tish static typing.
2
+ //!
3
+ //! Maps TypeAnnotation from the AST to concrete Rust types for code generation.
4
+
5
+ use std::collections::HashMap;
6
+ use std::sync::Arc;
7
+ use tishlang_ast::{BinOp, FunParam, TypeAnnotation, TypedParam};
8
+
9
+ /// Concrete Rust type representation for code generation.
10
+ #[derive(Debug, Clone, PartialEq)]
11
+ pub enum RustType {
12
+ /// Dynamic Value type (untyped or complex types)
13
+ Value,
14
+ /// f64 (for number)
15
+ F64,
16
+ /// String (for string)
17
+ String,
18
+ /// bool (for boolean)
19
+ Bool,
20
+ /// () for void/null
21
+ Unit,
22
+ /// Vec<T> for arrays
23
+ Vec(Box<RustType>),
24
+ /// Option<T> for nullable types (T | null)
25
+ Option(Box<RustType>),
26
+ /// Inline object shape — used during inference / annotation lowering
27
+ /// before a `Named` alias has been registered. Once a corresponding
28
+ /// `type Foo = { ... }` declaration is found in the program, occurrences
29
+ /// of this shape can be canonicalised into `RustType::Named("Foo")`.
30
+ Object(Vec<(Arc<str>, RustType)>),
31
+ /// User-defined named type (a struct emitted by the compiler).
32
+ /// The field list is duplicated here so the codegen can emit struct
33
+ /// literals, member access, and Value-conversion glue without going
34
+ /// back to a global registry on every call site.
35
+ Named {
36
+ name: Arc<str>,
37
+ fields: Vec<(Arc<str>, RustType)>,
38
+ },
39
+ /// Fn trait for typed functions
40
+ Function {
41
+ params: Vec<RustType>,
42
+ returns: Box<RustType>,
43
+ },
44
+ /// Tuple `(T0, T1, …)` for `[T0, T1]` tuple types — a native Rust tuple.
45
+ Tuple(Vec<RustType>),
46
+ }
47
+
48
+ impl RustType {
49
+ /// Convert a TypeAnnotation to a RustType (no alias resolution).
50
+ /// Use [`Self::from_annotation_with_aliases`] when a registry is
51
+ /// available so user-defined `type X = { ... }` aliases land as
52
+ /// `RustType::Named` and can drive struct emission.
53
+ pub fn from_annotation(ann: &TypeAnnotation) -> Self {
54
+ Self::from_annotation_with_aliases(ann, &HashMap::new())
55
+ }
56
+
57
+ /// Like [`from_annotation`], but consults `aliases` so a `Simple(name)`
58
+ /// reference to a user-declared `type X = { ... }` resolves to a
59
+ /// `RustType::Named { name, fields }` carrying the struct shape.
60
+ pub fn from_annotation_with_aliases(
61
+ ann: &TypeAnnotation,
62
+ aliases: &HashMap<String, RustType>,
63
+ ) -> Self {
64
+ match ann {
65
+ TypeAnnotation::Simple(name) => match name.as_ref() {
66
+ "number" => RustType::F64,
67
+ "string" => RustType::String,
68
+ "boolean" | "bool" => RustType::Bool,
69
+ "void" | "undefined" => RustType::Unit,
70
+ "null" => RustType::Unit,
71
+ "any" => RustType::Value,
72
+ other => {
73
+ // User-declared `type X = { ... }`: lift the inline
74
+ // object shape into a `Named` so the codegen can emit
75
+ // a Rust struct and direct field access for it.
76
+ if let Some(t) = aliases.get(other) {
77
+ if let RustType::Object(fields) = t {
78
+ return RustType::Named {
79
+ name: Arc::from(other),
80
+ fields: fields.clone(),
81
+ };
82
+ }
83
+ return t.clone();
84
+ }
85
+ RustType::Value
86
+ }
87
+ },
88
+ TypeAnnotation::Array(elem) => {
89
+ RustType::Vec(Box::new(Self::from_annotation_with_aliases(elem, aliases)))
90
+ }
91
+ TypeAnnotation::Object(fields) => {
92
+ let typed_fields: Vec<_> = fields
93
+ .iter()
94
+ .map(|(k, v)| (k.clone(), Self::from_annotation_with_aliases(v, aliases)))
95
+ .collect();
96
+ RustType::Object(typed_fields)
97
+ }
98
+ TypeAnnotation::Function { params, returns } => {
99
+ let typed_params: Vec<_> = params
100
+ .iter()
101
+ .map(|p| Self::from_annotation_with_aliases(p, aliases))
102
+ .collect();
103
+ let typed_returns = Box::new(Self::from_annotation_with_aliases(returns, aliases));
104
+ RustType::Function {
105
+ params: typed_params,
106
+ returns: typed_returns,
107
+ }
108
+ }
109
+ TypeAnnotation::Union(types) => {
110
+ // Check for T | null pattern -> Option<T>
111
+ if types.len() == 2 {
112
+ let has_null = types
113
+ .iter()
114
+ .any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
115
+ if has_null {
116
+ let non_null = types.iter().find(
117
+ |t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
118
+ );
119
+ if let Some(inner) = non_null {
120
+ return RustType::Option(Box::new(Self::from_annotation_with_aliases(
121
+ inner, aliases,
122
+ )));
123
+ }
124
+ }
125
+ }
126
+ // Other unions fall back to Value
127
+ RustType::Value
128
+ }
129
+ // `[T0, T1]` -> a native Rust tuple `(T0, T1)`.
130
+ TypeAnnotation::Tuple(elems) => RustType::Tuple(
131
+ elems
132
+ .iter()
133
+ .map(|e| Self::from_annotation_with_aliases(e, aliases))
134
+ .collect(),
135
+ ),
136
+ // A literal type lowers to its base primitive.
137
+ TypeAnnotation::Literal(lit) => match lit {
138
+ tishlang_ast::TypeLiteral::Str(_) => RustType::String,
139
+ tishlang_ast::TypeLiteral::Num(_) => RustType::F64,
140
+ tishlang_ast::TypeLiteral::Bool(_) => RustType::Bool,
141
+ },
142
+ // Intersection of object shapes (e.g. `interface X extends Y { … }` → `Y & { … }`):
143
+ // merge the fields into one shape. Registered as a `type` alias, this becomes a native
144
+ // struct. Any non-object member → can't merge → fall back to boxed `Value`.
145
+ TypeAnnotation::Intersection(parts) => {
146
+ let mut fields: Vec<(Arc<str>, RustType)> = Vec::new();
147
+ for p in parts {
148
+ match Self::from_annotation_with_aliases(p, aliases) {
149
+ RustType::Object(fs) | RustType::Named { fields: fs, .. } => {
150
+ for (k, v) in fs {
151
+ if !fields.iter().any(|(ek, _)| *ek == k) {
152
+ fields.push((k, v));
153
+ }
154
+ }
155
+ }
156
+ _ => return RustType::Value,
157
+ }
158
+ }
159
+ RustType::Object(fields)
160
+ }
161
+ }
162
+ }
163
+
164
+ /// Check if this type is a native Rust type (not Value).
165
+ pub fn is_native(&self) -> bool {
166
+ !matches!(self, RustType::Value)
167
+ }
168
+
169
+ /// Check if this type is numeric (f64).
170
+ pub fn is_numeric(&self) -> bool {
171
+ matches!(self, RustType::F64)
172
+ }
173
+
174
+ /// Infer the result type of a binary operation given the operand types.
175
+ /// Returns `None` if native code cannot be emitted (fall back to Value path).
176
+ pub fn result_type_of_binop(op: BinOp, lhs: &RustType, rhs: &RustType) -> Option<RustType> {
177
+ if lhs == &RustType::F64 && rhs == &RustType::F64 {
178
+ match op {
179
+ BinOp::Add | BinOp::Sub | BinOp::Mul | BinOp::Div | BinOp::Mod | BinOp::Pow => {
180
+ Some(RustType::F64)
181
+ }
182
+ // Bitwise / shift ops: JS coerces both sides to int32, computes, and
183
+ // returns a Number — so the native result is still F64. Big win for
184
+ // crypto/hashing loops that would otherwise box every `^`/`>>>`.
185
+ BinOp::BitAnd
186
+ | BinOp::BitOr
187
+ | BinOp::BitXor
188
+ | BinOp::Shl
189
+ | BinOp::Shr
190
+ | BinOp::UShr => Some(RustType::F64),
191
+ BinOp::Lt
192
+ | BinOp::Le
193
+ | BinOp::Gt
194
+ | BinOp::Ge
195
+ | BinOp::StrictEq
196
+ | BinOp::StrictNe => Some(RustType::Bool),
197
+ _ => None,
198
+ }
199
+ } else if lhs == &RustType::Bool && rhs == &RustType::Bool {
200
+ match op {
201
+ BinOp::And | BinOp::Or => Some(RustType::Bool),
202
+ BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
203
+ _ => None,
204
+ }
205
+ } else if lhs == &RustType::String && rhs == &RustType::String {
206
+ // M2: native string concat + value equality. `+` concatenates; `===`/`!==` compare by
207
+ // value (byte-identical to JS and to the boxed `Value::String` path). Relational
208
+ // `< <= > >=` deliberately stay on the boxed path: JS orders strings by UTF-16 code
209
+ // units while Rust `String` orders by UTF-8 bytes — they diverge outside the BMP.
210
+ match op {
211
+ BinOp::Add => Some(RustType::String),
212
+ BinOp::StrictEq | BinOp::StrictNe => Some(RustType::Bool),
213
+ _ => None,
214
+ }
215
+ } else {
216
+ None
217
+ }
218
+ }
219
+
220
+ /// Get the Rust type string for code generation.
221
+ pub fn to_rust_type_str(&self) -> String {
222
+ match self {
223
+ RustType::Value => "Value".to_string(),
224
+ RustType::F64 => "f64".to_string(),
225
+ RustType::String => "String".to_string(),
226
+ RustType::Bool => "bool".to_string(),
227
+ RustType::Unit => "()".to_string(),
228
+ RustType::Vec(inner) => format!("Vec<{}>", inner.to_rust_type_str()),
229
+ RustType::Option(inner) => format!("Option<{}>", inner.to_rust_type_str()),
230
+ RustType::Object(_) => {
231
+ // Anonymous inline shapes don't have a Rust struct; fall
232
+ // back to the dynamic Value path.
233
+ "Value".to_string()
234
+ }
235
+ RustType::Named { name, .. } => named_struct_ident(name),
236
+ RustType::Function { params, returns } => {
237
+ let params_str: Vec<_> = params.iter().map(|p| p.to_rust_type_str()).collect();
238
+ format!(
239
+ "Rc<dyn Fn({}) -> {}>",
240
+ params_str.join(", "),
241
+ returns.to_rust_type_str()
242
+ )
243
+ }
244
+ RustType::Tuple(elems) => tuple_text(&elems.iter().map(|e| e.to_rust_type_str()).collect::<Vec<_>>()),
245
+ }
246
+ }
247
+
248
+ /// Get the default value for this type.
249
+ pub fn default_value(&self) -> String {
250
+ match self {
251
+ RustType::Value => "Value::Null".to_string(),
252
+ RustType::F64 => "0.0".to_string(),
253
+ RustType::String => "String::new()".to_string(),
254
+ RustType::Bool => "false".to_string(),
255
+ RustType::Unit => "()".to_string(),
256
+ RustType::Vec(_) => "Vec::new()".to_string(),
257
+ RustType::Option(_) => "None".to_string(),
258
+ RustType::Object(_) => "Value::Null".to_string(),
259
+ RustType::Named { fields, .. } => {
260
+ // Build a literal struct with each field at its own default,
261
+ // so unannotated decls of a typed struct still compile.
262
+ let init = fields
263
+ .iter()
264
+ .map(|(k, t)| format!("{}: {}", field_ident(k), t.default_value()))
265
+ .collect::<Vec<_>>()
266
+ .join(", ");
267
+ format!(
268
+ "{} {{ {} }}",
269
+ named_struct_ident(match self {
270
+ RustType::Named { name, .. } => name,
271
+ _ => unreachable!(),
272
+ }),
273
+ init
274
+ )
275
+ }
276
+ RustType::Function { .. } => "Value::Null".to_string(),
277
+ RustType::Tuple(elems) => {
278
+ tuple_text(&elems.iter().map(|e| e.default_value()).collect::<Vec<_>>())
279
+ }
280
+ }
281
+ }
282
+
283
+ /// Generate code to convert from Value to this native type.
284
+ pub fn from_value_expr(&self, value_expr: &str) -> String {
285
+ match self {
286
+ RustType::Tuple(elems) => {
287
+ // `Value::Array([..])` -> `(e0, e1, …)`, converting each slot from its `Value`.
288
+ let parts: Vec<String> = elems
289
+ .iter()
290
+ .enumerate()
291
+ .map(|(i, e)| {
292
+ e.from_value_expr(&format!(
293
+ "_t.get({}).cloned().unwrap_or(Value::Null)",
294
+ i
295
+ ))
296
+ })
297
+ .collect();
298
+ format!(
299
+ "match &{} {{ Value::Array(_a) => {{ let _t = _a.borrow(); {} }}, _ => panic!(\"expected tuple\") }}",
300
+ value_expr,
301
+ tuple_text(&parts)
302
+ )
303
+ }
304
+ RustType::Value => value_expr.to_string(),
305
+ RustType::F64 => format!(
306
+ "match &{} {{ Value::Number(n) => *n, _ => panic!(\"expected number\") }}",
307
+ value_expr
308
+ ),
309
+ RustType::String => format!(
310
+ "match &{} {{ Value::String(s) => s.to_string(), _ => panic!(\"expected string\") }}",
311
+ value_expr
312
+ ),
313
+ RustType::Bool => format!(
314
+ "match &{} {{ Value::Bool(b) => *b, _ => panic!(\"expected boolean\") }}",
315
+ value_expr
316
+ ),
317
+ RustType::Unit => "()".to_string(),
318
+ RustType::Vec(inner) => {
319
+ let inner_conversion = inner.from_value_expr("v");
320
+ format!(
321
+ "match &{} {{ Value::Array(arr) => arr.borrow().iter().map(|v| {}).collect(), _ => panic!(\"expected array\") }}",
322
+ value_expr, inner_conversion
323
+ )
324
+ }
325
+ RustType::Option(inner) => {
326
+ let inner_conversion = inner.from_value_expr(value_expr);
327
+ format!(
328
+ "match &{} {{ Value::Null => None, _ => Some({}) }}",
329
+ value_expr, inner_conversion
330
+ )
331
+ }
332
+ RustType::Named { name, fields } => {
333
+ // Each field is fetched out of the Value::Object via
334
+ // `get_prop` and converted to its native type. Falls back
335
+ // to the field's `default_value()` if the field is absent
336
+ // (rare — usually these come from JSON or PG).
337
+ let field_assigns = fields
338
+ .iter()
339
+ .map(|(k, ty)| {
340
+ let fetch =
341
+ format!("tishlang_runtime::get_prop(&{}, {:?})", value_expr, k.as_ref());
342
+ format!("{}: {}", field_ident(k), ty.from_value_expr(&fetch))
343
+ })
344
+ .collect::<Vec<_>>()
345
+ .join(", ");
346
+ format!("{} {{ {} }}", named_struct_ident(name), field_assigns)
347
+ }
348
+ _ => value_expr.to_string(), // Fallback
349
+ }
350
+ }
351
+
352
+ /// Generate code to convert from this native type to Value.
353
+ pub fn to_value_expr(&self, native_expr: &str) -> String {
354
+ match self {
355
+ RustType::Tuple(elems) => {
356
+ // `(e0, e1, …)` -> `Value::Array([e0.into_value(), …])`.
357
+ let parts: Vec<String> = elems
358
+ .iter()
359
+ .enumerate()
360
+ .map(|(i, e)| e.to_value_expr(&format!("{}.{}", native_expr, i)))
361
+ .collect();
362
+ format!("Value::Array(VmRef::new(vec![{}]))", parts.join(", "))
363
+ }
364
+ RustType::Value => native_expr.to_string(),
365
+ RustType::F64 => format!("Value::Number({})", native_expr),
366
+ RustType::String => format!("Value::String({}.clone().into())", native_expr),
367
+ RustType::Bool => format!("Value::Bool({})", native_expr),
368
+ RustType::Unit => "Value::Null".to_string(),
369
+ RustType::Vec(inner) => {
370
+ // Use iter()/copied()/cloned() to avoid moving the vector.
371
+ let (iter_suffix, val_expr) = match inner.as_ref() {
372
+ RustType::F64 => (".iter().copied()", "Value::Number(v)".to_string()),
373
+ RustType::Bool => (".iter().copied()", "Value::Bool(v)".to_string()),
374
+ _ => (".iter().cloned()", inner.to_value_expr("v")),
375
+ };
376
+ format!(
377
+ "Value::Array(VmRef::new({}{}.map(|v| {}).collect()))",
378
+ native_expr, iter_suffix, val_expr
379
+ )
380
+ }
381
+ RustType::Option(inner) => {
382
+ let inner_to_value = inner.to_value_expr("v");
383
+ format!(
384
+ "match {} {{ Some(v) => {}, None => Value::Null }}",
385
+ native_expr, inner_to_value
386
+ )
387
+ }
388
+ RustType::Named { fields, .. } => {
389
+ // Walk fields, build an ObjectMap, wrap in Value::Object.
390
+ // The boundary is paid only when crossing into untyped
391
+ // Tish (JSON.stringify, calling a Value::Function, etc.);
392
+ // direct Rust-to-Rust paths between two Named values stay
393
+ // as plain struct moves.
394
+ let inserts = fields
395
+ .iter()
396
+ .map(|(k, ty)| {
397
+ let access = format!("{}.{}", native_expr, field_ident(k));
398
+ // A `Value`-typed field (e.g. from a generic struct `Box<T>`) accessed
399
+ // behind `&self` must be cloned — it isn't `Copy` and `to_value_expr(Value)`
400
+ // is identity. Native field types clone/copy inside their own `to_value_expr`.
401
+ let v_expr = if matches!(ty, RustType::Value) {
402
+ format!("{}.clone()", access)
403
+ } else {
404
+ ty.to_value_expr(&access)
405
+ };
406
+ format!(
407
+ "_om.insert(::std::sync::Arc::from({:?}), {});",
408
+ k.as_ref(),
409
+ v_expr
410
+ )
411
+ })
412
+ .collect::<Vec<_>>()
413
+ .join(" ");
414
+ // `Value::object` wraps the `ObjectMap` in an `ObjectData` (what `Value::Object`
415
+ // actually holds) — emitting `Value::Object(VmRef::new(_om))` directly is a type
416
+ // error (`ObjectMap` != `ObjectData`); only surfaced once a whole struct is boxed
417
+ // into a `Value`, e.g. passing it to a function.
418
+ format!(
419
+ "{{ let mut _om = ObjectMap::default(); {} Value::object(_om) }}",
420
+ inserts
421
+ )
422
+ }
423
+ _ => native_expr.to_string(), // Fallback
424
+ }
425
+ }
426
+ }
427
+
428
+ /// Map a Tish type-alias name to the Rust struct identifier we emit.
429
+ /// Prefixed so user names can never collide with runtime types like `Value`.
430
+ /// Render a Rust tuple type/value from its parts, using the `(T,)` form for 1-tuples.
431
+ fn tuple_text(parts: &[String]) -> String {
432
+ if parts.len() == 1 {
433
+ format!("({},)", parts[0])
434
+ } else {
435
+ format!("({})", parts.join(", "))
436
+ }
437
+ }
438
+
439
+ pub fn named_struct_ident(tish_name: &str) -> String {
440
+ format!("TishStruct_{}", tish_name)
441
+ }
442
+
443
+ /// Map a Tish field name (`randomNumber`) to a valid Rust identifier
444
+ /// (kept identical here — non-snake-case is allowed via
445
+ /// `#[allow(non_snake_case)]` on the struct, so JS-style camelCase keys
446
+ /// stay readable in the generated source).
447
+ pub fn field_ident(tish_name: &str) -> String {
448
+ // Reserve Rust keywords that would otherwise conflict.
449
+ match tish_name {
450
+ "type" | "ref" | "fn" | "match" | "move" | "mod" | "self" | "Self" | "super" | "use"
451
+ | "where" | "loop" | "yield" | "async" | "await" | "dyn" | "impl" | "trait" | "in"
452
+ | "as" | "box" | "crate" | "const" | "extern" | "let" | "mut" | "pub" | "static"
453
+ | "unsafe" | "abstract" | "become" | "do" | "final" | "macro" | "override" | "priv"
454
+ | "typeof" | "unsized" | "virtual" => format!("r#{}", tish_name),
455
+ _ => tish_name.to_string(),
456
+ }
457
+ }
458
+
459
+ /// Type context for tracking variable types during code generation.
460
+ #[derive(Debug, Clone, Default)]
461
+ pub struct TypeContext {
462
+ /// Stack of scopes, each mapping variable names to their types
463
+ scopes: Vec<HashMap<String, RustType>>,
464
+ }
465
+
466
+ impl TypeContext {
467
+ pub fn new() -> Self {
468
+ Self {
469
+ scopes: vec![HashMap::new()], // Start with global scope
470
+ }
471
+ }
472
+
473
+ /// Enter a new scope (e.g., function body, block).
474
+ pub fn push_scope(&mut self) {
475
+ self.scopes.push(HashMap::new());
476
+ }
477
+
478
+ /// Exit the current scope.
479
+ pub fn pop_scope(&mut self) {
480
+ self.scopes.pop();
481
+ }
482
+
483
+ /// Push a scope for a function or arrow body and record formals as [`RustType::Value`].
484
+ ///
485
+ /// Native codegen always binds parameters from `args.get(i)` as `Value`; this prevents
486
+ /// outer locals (e.g. a loop counter inferred as [`RustType::F64`]) from shadowing the
487
+ /// wrong type for the same identifier.
488
+ pub fn push_fun_param_scope(&mut self, params: &[FunParam], rest_param: Option<&TypedParam>) {
489
+ self.push_scope();
490
+ for p in params {
491
+ for name in p.bound_names() {
492
+ self.define(name.as_ref(), RustType::Value);
493
+ }
494
+ }
495
+ if let Some(rp) = rest_param {
496
+ self.define(rp.name.as_ref(), RustType::Value);
497
+ }
498
+ }
499
+
500
+ /// Define a variable in the current scope.
501
+ pub fn define(&mut self, name: &str, ty: RustType) {
502
+ if let Some(scope) = self.scopes.last_mut() {
503
+ scope.insert(name.to_string(), ty);
504
+ }
505
+ }
506
+
507
+ /// Look up a variable's type (searches from innermost to outermost scope).
508
+ pub fn lookup(&self, name: &str) -> Option<&RustType> {
509
+ for scope in self.scopes.iter().rev() {
510
+ if let Some(ty) = scope.get(name) {
511
+ return Some(ty);
512
+ }
513
+ }
514
+ None
515
+ }
516
+
517
+ /// Check if a variable is typed (has a non-Value type).
518
+ pub fn is_typed(&self, name: &str) -> bool {
519
+ self.lookup(name).map(|ty| ty.is_native()).unwrap_or(false)
520
+ }
521
+
522
+ /// Get the type of a variable, defaulting to Value if not found.
523
+ pub fn get_type(&self, name: &str) -> RustType {
524
+ self.lookup(name).cloned().unwrap_or(RustType::Value)
525
+ }
526
+ }
527
+
528
+ #[cfg(test)]
529
+ mod tests {
530
+ use super::*;
531
+
532
+ #[test]
533
+ fn test_simple_types() {
534
+ assert_eq!(
535
+ RustType::from_annotation(&TypeAnnotation::Simple("number".into())),
536
+ RustType::F64
537
+ );
538
+ assert_eq!(
539
+ RustType::from_annotation(&TypeAnnotation::Simple("string".into())),
540
+ RustType::String
541
+ );
542
+ assert_eq!(
543
+ RustType::from_annotation(&TypeAnnotation::Simple("boolean".into())),
544
+ RustType::Bool
545
+ );
546
+ }
547
+
548
+ #[test]
549
+ fn test_array_type() {
550
+ let arr_type = TypeAnnotation::Array(Box::new(TypeAnnotation::Simple("number".into())));
551
+ assert_eq!(
552
+ RustType::from_annotation(&arr_type),
553
+ RustType::Vec(Box::new(RustType::F64))
554
+ );
555
+ }
556
+
557
+ #[test]
558
+ fn test_nullable_type() {
559
+ let nullable = TypeAnnotation::Union(vec![
560
+ TypeAnnotation::Simple("string".into()),
561
+ TypeAnnotation::Simple("null".into()),
562
+ ]);
563
+ assert_eq!(
564
+ RustType::from_annotation(&nullable),
565
+ RustType::Option(Box::new(RustType::String))
566
+ );
567
+ }
568
+
569
+ #[test]
570
+ fn test_type_context() {
571
+ let mut ctx = TypeContext::new();
572
+ ctx.define("x", RustType::F64);
573
+ assert_eq!(ctx.get_type("x"), RustType::F64);
574
+ assert!(ctx.is_typed("x"));
575
+
576
+ ctx.push_scope();
577
+ ctx.define("y", RustType::String);
578
+ assert_eq!(ctx.get_type("y"), RustType::String);
579
+ assert_eq!(ctx.get_type("x"), RustType::F64); // Can still see outer scope
580
+
581
+ ctx.pop_scope();
582
+ assert_eq!(ctx.get_type("y"), RustType::Value); // y no longer visible
583
+ }
584
+
585
+ #[test]
586
+ fn push_fun_param_scope_shadows_outer() {
587
+ use tishlang_ast::{FunParam, Span, TypedParam};
588
+
589
+ let mut ctx = TypeContext::new();
590
+ ctx.define("n", RustType::F64);
591
+ let params = vec![FunParam::Simple(TypedParam {
592
+ name: "n".into(),
593
+ name_span: Span {
594
+ start: (0, 0),
595
+ end: (0, 0),
596
+ },
597
+ type_ann: None,
598
+ default: None,
599
+ })];
600
+ ctx.push_fun_param_scope(&params, None);
601
+ assert_eq!(ctx.get_type("n"), RustType::Value);
602
+ ctx.pop_scope();
603
+ assert_eq!(ctx.get_type("n"), RustType::F64);
604
+ }
605
+ }
@@ -0,0 +1,18 @@
1
+ [package]
2
+ name = "tishlang_compile_js"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+ description = "Tish to JavaScript transpiler backend"
6
+
7
+ license-file = { workspace = true }
8
+ repository = { workspace = true }
9
+ [features]
10
+ default = []
11
+
12
+ [dependencies]
13
+ sourcemap = "9.3"
14
+ tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
15
+ tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
16
+ tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
17
+ tishlang_parser = { path = "../tish_parser", version = ">=0.1" }
18
+ tishlang_ui = { path = "../tish_ui", version = ">=0.1", default-features = false, features = ["compiler"] }
@@ -0,0 +1,8 @@
1
+ // Smoke: JSX + vdom compile (used by CI / manual verify)
2
+ fn Root() {
3
+ return <p class="smoke">{"ok"}</p>
4
+ }
5
+
6
+ export fn main() {
7
+ return 0
8
+ }