@tishlang/tish 1.6.0 → 1.8.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 (113) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/error.rs +2 -8
  5. package/crates/js_to_tish/src/transform/expr.rs +128 -137
  6. package/crates/js_to_tish/src/transform/stmt.rs +62 -32
  7. package/crates/tish/Cargo.toml +15 -5
  8. package/crates/tish/src/cargo_native_registry.rs +29 -0
  9. package/crates/tish/src/cli_help.rs +92 -39
  10. package/crates/tish/src/main.rs +172 -86
  11. package/crates/tish/src/repl_completion.rs +3 -3
  12. package/crates/tish/tests/cargo_example_compile.rs +4 -2
  13. package/crates/tish/tests/integration_test.rs +216 -54
  14. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  15. package/crates/tish/tests/shortcircuit.rs +20 -5
  16. package/crates/tish_ast/src/ast.rs +92 -23
  17. package/crates/tish_build_utils/Cargo.toml +4 -0
  18. package/crates/tish_build_utils/src/lib.rs +136 -8
  19. package/crates/tish_builtins/Cargo.toml +5 -1
  20. package/crates/tish_builtins/src/array.rs +65 -33
  21. package/crates/tish_builtins/src/construct.rs +34 -39
  22. package/crates/tish_builtins/src/globals.rs +42 -26
  23. package/crates/tish_builtins/src/helpers.rs +2 -1
  24. package/crates/tish_builtins/src/lib.rs +5 -5
  25. package/crates/tish_builtins/src/math.rs +5 -3
  26. package/crates/tish_builtins/src/object.rs +3 -2
  27. package/crates/tish_builtins/src/string.rs +144 -22
  28. package/crates/tish_bytecode/src/chunk.rs +0 -1
  29. package/crates/tish_bytecode/src/compiler.rs +173 -71
  30. package/crates/tish_bytecode/src/opcode.rs +24 -6
  31. package/crates/tish_bytecode/src/peephole.rs +2 -2
  32. package/crates/tish_compile/Cargo.toml +1 -0
  33. package/crates/tish_compile/src/codegen.rs +1621 -453
  34. package/crates/tish_compile/src/infer.rs +75 -19
  35. package/crates/tish_compile/src/lib.rs +19 -8
  36. package/crates/tish_compile/src/resolve.rs +278 -137
  37. package/crates/tish_compile/src/types.rs +184 -24
  38. package/crates/tish_compile_js/Cargo.toml +1 -0
  39. package/crates/tish_compile_js/src/codegen.rs +181 -37
  40. package/crates/tish_compile_js/src/lib.rs +3 -1
  41. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  42. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  43. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +69 -59
  44. package/crates/tish_core/Cargo.toml +8 -0
  45. package/crates/tish_core/src/json.rs +107 -56
  46. package/crates/tish_core/src/lib.rs +4 -2
  47. package/crates/tish_core/src/macros.rs +5 -5
  48. package/crates/tish_core/src/uri.rs +9 -6
  49. package/crates/tish_core/src/value.rs +145 -43
  50. package/crates/tish_core/src/vmref.rs +178 -0
  51. package/crates/tish_cranelift/src/link.rs +6 -9
  52. package/crates/tish_cranelift/src/lower.rs +14 -8
  53. package/crates/tish_eval/Cargo.toml +17 -2
  54. package/crates/tish_eval/src/eval.rs +474 -165
  55. package/crates/tish_eval/src/http.rs +61 -0
  56. package/crates/tish_eval/src/lib.rs +12 -8
  57. package/crates/tish_eval/src/natives.rs +136 -38
  58. package/crates/tish_eval/src/promise.rs +14 -8
  59. package/crates/tish_eval/src/timers.rs +28 -19
  60. package/crates/tish_eval/src/value.rs +17 -6
  61. package/crates/tish_eval/src/value_convert.rs +13 -5
  62. package/crates/tish_fmt/src/lib.rs +149 -43
  63. package/crates/tish_lexer/src/lib.rs +232 -63
  64. package/crates/tish_lexer/src/token.rs +10 -6
  65. package/crates/tish_llvm/src/lib.rs +17 -8
  66. package/crates/tish_lsp/Cargo.toml +4 -1
  67. package/crates/tish_lsp/README.md +1 -1
  68. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  69. package/crates/tish_lsp/src/import_goto.rs +549 -0
  70. package/crates/tish_lsp/src/main.rs +504 -106
  71. package/crates/tish_native/src/build.rs +4 -8
  72. package/crates/tish_native/src/lib.rs +54 -21
  73. package/crates/tish_opt/src/lib.rs +84 -52
  74. package/crates/tish_parser/src/lib.rs +45 -13
  75. package/crates/tish_parser/src/parser.rs +505 -130
  76. package/crates/tish_resolve/Cargo.toml +13 -0
  77. package/crates/tish_resolve/src/lib.rs +3436 -0
  78. package/crates/tish_resolve/src/pos.rs +133 -0
  79. package/crates/tish_runtime/Cargo.toml +68 -3
  80. package/crates/tish_runtime/src/http.rs +1136 -145
  81. package/crates/tish_runtime/src/http_fetch.rs +38 -27
  82. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  83. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  84. package/crates/tish_runtime/src/lib.rs +375 -189
  85. package/crates/tish_runtime/src/promise.rs +199 -40
  86. package/crates/tish_runtime/src/promise_io.rs +2 -1
  87. package/crates/tish_runtime/src/timers.rs +37 -1
  88. package/crates/tish_runtime/src/ws.rs +65 -42
  89. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  90. package/crates/tish_ui/src/jsx.rs +317 -27
  91. package/crates/tish_ui/src/lib.rs +5 -2
  92. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  93. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  94. package/crates/tish_vm/Cargo.toml +15 -5
  95. package/crates/tish_vm/src/vm.rs +725 -281
  96. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +11 -4
  97. package/crates/tish_wasm/src/lib.rs +55 -42
  98. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  99. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  100. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  101. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  102. package/crates/tishlang_cargo_bindgen/src/discover.rs +120 -0
  103. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  104. package/crates/tishlang_cargo_bindgen/src/lib.rs +350 -0
  105. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  106. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  107. package/justfile +8 -0
  108. package/package.json +1 -1
  109. package/platform/darwin-arm64/tish +0 -0
  110. package/platform/darwin-x64/tish +0 -0
  111. package/platform/linux-arm64/tish +0 -0
  112. package/platform/linux-x64/tish +0 -0
  113. package/platform/win32-x64/tish.exe +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  use std::collections::HashMap;
6
6
  use std::sync::Arc;
7
- use tishlang_ast::{BinOp, TypeAnnotation};
7
+ use tishlang_ast::{BinOp, FunParam, TypeAnnotation, TypedParam};
8
8
 
9
9
  /// Concrete Rust type representation for code generation.
10
10
  #[derive(Debug, Clone, PartialEq)]
@@ -23,8 +23,19 @@ pub enum RustType {
23
23
  Vec(Box<RustType>),
24
24
  /// Option<T> for nullable types (T | null)
25
25
  Option(Box<RustType>),
26
- /// Rc<RefCell<HashMap<Arc<str>, T>>> for typed objects
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")`.
27
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
+ },
28
39
  /// Fn trait for typed functions
29
40
  Function {
30
41
  params: Vec<RustType>,
@@ -33,8 +44,21 @@ pub enum RustType {
33
44
  }
34
45
 
35
46
  impl RustType {
36
- /// Convert a TypeAnnotation to a RustType.
47
+ /// Convert a TypeAnnotation to a RustType (no alias resolution).
48
+ /// Use [`Self::from_annotation_with_aliases`] when a registry is
49
+ /// available so user-defined `type X = { ... }` aliases land as
50
+ /// `RustType::Named` and can drive struct emission.
37
51
  pub fn from_annotation(ann: &TypeAnnotation) -> Self {
52
+ Self::from_annotation_with_aliases(ann, &HashMap::new())
53
+ }
54
+
55
+ /// Like [`from_annotation`], but consults `aliases` so a `Simple(name)`
56
+ /// reference to a user-declared `type X = { ... }` resolves to a
57
+ /// `RustType::Named { name, fields }` carrying the struct shape.
58
+ pub fn from_annotation_with_aliases(
59
+ ann: &TypeAnnotation,
60
+ aliases: &HashMap<String, RustType>,
61
+ ) -> Self {
38
62
  match ann {
39
63
  TypeAnnotation::Simple(name) => match name.as_ref() {
40
64
  "number" => RustType::F64,
@@ -43,21 +67,38 @@ impl RustType {
43
67
  "void" | "undefined" => RustType::Unit,
44
68
  "null" => RustType::Unit,
45
69
  "any" => RustType::Value,
46
- _ => RustType::Value, // Unknown types fall back to Value
70
+ other => {
71
+ // User-declared `type X = { ... }`: lift the inline
72
+ // object shape into a `Named` so the codegen can emit
73
+ // a Rust struct and direct field access for it.
74
+ if let Some(t) = aliases.get(other) {
75
+ if let RustType::Object(fields) = t {
76
+ return RustType::Named {
77
+ name: Arc::from(other),
78
+ fields: fields.clone(),
79
+ };
80
+ }
81
+ return t.clone();
82
+ }
83
+ RustType::Value
84
+ }
47
85
  },
48
- TypeAnnotation::Array(elem) => {
49
- RustType::Vec(Box::new(Self::from_annotation(elem)))
50
- }
86
+ TypeAnnotation::Array(elem) => RustType::Vec(Box::new(
87
+ Self::from_annotation_with_aliases(elem, aliases),
88
+ )),
51
89
  TypeAnnotation::Object(fields) => {
52
90
  let typed_fields: Vec<_> = fields
53
91
  .iter()
54
- .map(|(k, v)| (k.clone(), Self::from_annotation(v)))
92
+ .map(|(k, v)| (k.clone(), Self::from_annotation_with_aliases(v, aliases)))
55
93
  .collect();
56
94
  RustType::Object(typed_fields)
57
95
  }
58
96
  TypeAnnotation::Function { params, returns } => {
59
- let typed_params: Vec<_> = params.iter().map(Self::from_annotation).collect();
60
- let typed_returns = Box::new(Self::from_annotation(returns));
97
+ let typed_params: Vec<_> = params
98
+ .iter()
99
+ .map(|p| Self::from_annotation_with_aliases(p, aliases))
100
+ .collect();
101
+ let typed_returns = Box::new(Self::from_annotation_with_aliases(returns, aliases));
61
102
  RustType::Function {
62
103
  params: typed_params,
63
104
  returns: typed_returns,
@@ -66,15 +107,17 @@ impl RustType {
66
107
  TypeAnnotation::Union(types) => {
67
108
  // Check for T | null pattern -> Option<T>
68
109
  if types.len() == 2 {
69
- let has_null = types.iter().any(|t| {
70
- matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null")
71
- });
110
+ let has_null = types
111
+ .iter()
112
+ .any(|t| matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"));
72
113
  if has_null {
73
- let non_null = types.iter().find(|t| {
74
- !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null")
75
- });
114
+ let non_null = types.iter().find(
115
+ |t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
116
+ );
76
117
  if let Some(inner) = non_null {
77
- return RustType::Option(Box::new(Self::from_annotation(inner)));
118
+ return RustType::Option(Box::new(
119
+ Self::from_annotation_with_aliases(inner, aliases),
120
+ ));
78
121
  }
79
122
  }
80
123
  }
@@ -132,9 +175,11 @@ impl RustType {
132
175
  RustType::Vec(inner) => format!("Vec<{}>", inner.to_rust_type_str()),
133
176
  RustType::Option(inner) => format!("Option<{}>", inner.to_rust_type_str()),
134
177
  RustType::Object(_) => {
135
- // For now, typed objects still use Value
178
+ // Anonymous inline shapes don't have a Rust struct; fall
179
+ // back to the dynamic Value path.
136
180
  "Value".to_string()
137
181
  }
182
+ RustType::Named { name, .. } => named_struct_ident(name),
138
183
  RustType::Function { params, returns } => {
139
184
  let params_str: Vec<_> = params.iter().map(|p| p.to_rust_type_str()).collect();
140
185
  format!(
@@ -157,6 +202,23 @@ impl RustType {
157
202
  RustType::Vec(_) => "Vec::new()".to_string(),
158
203
  RustType::Option(_) => "None".to_string(),
159
204
  RustType::Object(_) => "Value::Null".to_string(),
205
+ RustType::Named { fields, .. } => {
206
+ // Build a literal struct with each field at its own default,
207
+ // so unannotated decls of a typed struct still compile.
208
+ let init = fields
209
+ .iter()
210
+ .map(|(k, t)| format!("{}: {}", field_ident(k), t.default_value()))
211
+ .collect::<Vec<_>>()
212
+ .join(", ");
213
+ format!(
214
+ "{} {{ {} }}",
215
+ named_struct_ident(match self {
216
+ RustType::Named { name, .. } => name,
217
+ _ => unreachable!(),
218
+ }),
219
+ init
220
+ )
221
+ }
160
222
  RustType::Function { .. } => "Value::Null".to_string(),
161
223
  }
162
224
  }
@@ -192,6 +254,22 @@ impl RustType {
192
254
  value_expr, inner_conversion
193
255
  )
194
256
  }
257
+ RustType::Named { name, fields } => {
258
+ // Each field is fetched out of the Value::Object via
259
+ // `get_prop` and converted to its native type. Falls back
260
+ // to the field's `default_value()` if the field is absent
261
+ // (rare — usually these come from JSON or PG).
262
+ let field_assigns = fields
263
+ .iter()
264
+ .map(|(k, ty)| {
265
+ let fetch =
266
+ format!("tishlang_runtime::get_prop(&{}, {:?})", value_expr, k.as_ref());
267
+ format!("{}: {}", field_ident(k), ty.from_value_expr(&fetch))
268
+ })
269
+ .collect::<Vec<_>>()
270
+ .join(", ");
271
+ format!("{} {{ {} }}", named_struct_ident(name), field_assigns)
272
+ }
195
273
  _ => value_expr.to_string(), // Fallback
196
274
  }
197
275
  }
@@ -212,7 +290,7 @@ impl RustType {
212
290
  _ => (".iter().cloned()", inner.to_value_expr("v")),
213
291
  };
214
292
  format!(
215
- "Value::Array(Rc::new(RefCell::new({}{}.map(|v| {}).collect())))",
293
+ "Value::Array(VmRef::new({}{}.map(|v| {}).collect()))",
216
294
  native_expr, iter_suffix, val_expr
217
295
  )
218
296
  }
@@ -223,11 +301,57 @@ impl RustType {
223
301
  native_expr, inner_to_value
224
302
  )
225
303
  }
304
+ RustType::Named { fields, .. } => {
305
+ // Walk fields, build an ObjectMap, wrap in Value::Object.
306
+ // The boundary is paid only when crossing into untyped
307
+ // Tish (JSON.stringify, calling a Value::Function, etc.);
308
+ // direct Rust-to-Rust paths between two Named values stay
309
+ // as plain struct moves.
310
+ let inserts = fields
311
+ .iter()
312
+ .map(|(k, ty)| {
313
+ let access = format!("{}.{}", native_expr, field_ident(k));
314
+ let v_expr = ty.to_value_expr(&access);
315
+ format!(
316
+ "_om.insert(::std::sync::Arc::from({:?}), {});",
317
+ k.as_ref(),
318
+ v_expr
319
+ )
320
+ })
321
+ .collect::<Vec<_>>()
322
+ .join(" ");
323
+ format!(
324
+ "{{ let mut _om = ObjectMap::default(); {} Value::Object(VmRef::new(_om)) }}",
325
+ inserts
326
+ )
327
+ }
226
328
  _ => native_expr.to_string(), // Fallback
227
329
  }
228
330
  }
229
331
  }
230
332
 
333
+ /// Map a Tish type-alias name to the Rust struct identifier we emit.
334
+ /// Prefixed so user names can never collide with runtime types like `Value`.
335
+ pub fn named_struct_ident(tish_name: &str) -> String {
336
+ format!("TishStruct_{}", tish_name)
337
+ }
338
+
339
+ /// Map a Tish field name (`randomNumber`) to a valid Rust identifier
340
+ /// (kept identical here — non-snake-case is allowed via
341
+ /// `#[allow(non_snake_case)]` on the struct, so JS-style camelCase keys
342
+ /// stay readable in the generated source).
343
+ pub fn field_ident(tish_name: &str) -> String {
344
+ // Reserve Rust keywords that would otherwise conflict.
345
+ match tish_name {
346
+ "type" | "ref" | "fn" | "match" | "move" | "mod" | "self" | "Self" | "super" | "use"
347
+ | "where" | "loop" | "yield" | "async" | "await" | "dyn" | "impl" | "trait" | "in"
348
+ | "as" | "box" | "crate" | "const" | "extern" | "let" | "mut" | "pub" | "static"
349
+ | "unsafe" | "abstract" | "become" | "do" | "final" | "macro" | "override" | "priv"
350
+ | "typeof" | "unsized" | "virtual" => format!("r#{}", tish_name),
351
+ _ => tish_name.to_string(),
352
+ }
353
+ }
354
+
231
355
  /// Type context for tracking variable types during code generation.
232
356
  #[derive(Debug, Clone, Default)]
233
357
  pub struct TypeContext {
@@ -252,6 +376,23 @@ impl TypeContext {
252
376
  self.scopes.pop();
253
377
  }
254
378
 
379
+ /// Push a scope for a function or arrow body and record formals as [`RustType::Value`].
380
+ ///
381
+ /// Native codegen always binds parameters from `args.get(i)` as `Value`; this prevents
382
+ /// outer locals (e.g. a loop counter inferred as [`RustType::F64`]) from shadowing the
383
+ /// wrong type for the same identifier.
384
+ pub fn push_fun_param_scope(&mut self, params: &[FunParam], rest_param: Option<&TypedParam>) {
385
+ self.push_scope();
386
+ for p in params {
387
+ for name in p.bound_names() {
388
+ self.define(name.as_ref(), RustType::Value);
389
+ }
390
+ }
391
+ if let Some(rp) = rest_param {
392
+ self.define(rp.name.as_ref(), RustType::Value);
393
+ }
394
+ }
395
+
255
396
  /// Define a variable in the current scope.
256
397
  pub fn define(&mut self, name: &str, ty: RustType) {
257
398
  if let Some(scope) = self.scopes.last_mut() {
@@ -271,9 +412,7 @@ impl TypeContext {
271
412
 
272
413
  /// Check if a variable is typed (has a non-Value type).
273
414
  pub fn is_typed(&self, name: &str) -> bool {
274
- self.lookup(name)
275
- .map(|ty| ty.is_native())
276
- .unwrap_or(false)
415
+ self.lookup(name).map(|ty| ty.is_native()).unwrap_or(false)
277
416
  }
278
417
 
279
418
  /// Get the type of a variable, defaulting to Value if not found.
@@ -329,13 +468,34 @@ mod tests {
329
468
  ctx.define("x", RustType::F64);
330
469
  assert_eq!(ctx.get_type("x"), RustType::F64);
331
470
  assert!(ctx.is_typed("x"));
332
-
471
+
333
472
  ctx.push_scope();
334
473
  ctx.define("y", RustType::String);
335
474
  assert_eq!(ctx.get_type("y"), RustType::String);
336
475
  assert_eq!(ctx.get_type("x"), RustType::F64); // Can still see outer scope
337
-
476
+
338
477
  ctx.pop_scope();
339
478
  assert_eq!(ctx.get_type("y"), RustType::Value); // y no longer visible
340
479
  }
480
+
481
+ #[test]
482
+ fn push_fun_param_scope_shadows_outer() {
483
+ use tishlang_ast::{FunParam, Span, TypedParam};
484
+
485
+ let mut ctx = TypeContext::new();
486
+ ctx.define("n", RustType::F64);
487
+ let params = vec![FunParam::Simple(TypedParam {
488
+ name: "n".into(),
489
+ name_span: Span {
490
+ start: (0, 0),
491
+ end: (0, 0),
492
+ },
493
+ type_ann: None,
494
+ default: None,
495
+ })];
496
+ ctx.push_fun_param_scope(&params, None);
497
+ assert_eq!(ctx.get_type("n"), RustType::Value);
498
+ ctx.pop_scope();
499
+ assert_eq!(ctx.get_type("n"), RustType::F64);
500
+ }
341
501
  }
@@ -10,6 +10,7 @@ repository = { workspace = true }
10
10
  default = []
11
11
 
12
12
  [dependencies]
13
+ sourcemap = "9.3"
13
14
  tishlang_ast = { path = "../tish_ast", version = ">=0.1" }
14
15
  tishlang_compile = { path = "../tish_compile", version = ">=0.1" }
15
16
  tishlang_opt = { path = "../tish_opt", version = ">=0.1" }
@@ -1,5 +1,8 @@
1
1
  //! Code generation: AST -> JavaScript source.
2
2
 
3
+ use std::path::{Path, PathBuf};
4
+
5
+ use sourcemap::SourceMapBuilder;
3
6
  use tishlang_ast::{
4
7
  ArrayElement, ArrowBody, BinOp, CallArg, CompoundOp, DestructElement, DestructPattern, Expr,
5
8
  FunParam, Literal, LogicalAssignOp, MemberProp, ObjectProp, Program, Statement, UnaryOp,
@@ -16,7 +19,9 @@ struct Codegen {
16
19
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
17
20
  matches!(
18
21
  stmt,
19
- Some(Statement::Break { .. }) | Some(Statement::Return { .. }) | Some(Statement::Throw { .. })
22
+ Some(Statement::Break { .. })
23
+ | Some(Statement::Return { .. })
24
+ | Some(Statement::Throw { .. })
20
25
  )
21
26
  }
22
27
 
@@ -43,6 +48,10 @@ impl Codegen {
43
48
  self.output.push('\n');
44
49
  }
45
50
 
51
+ fn output_line(&self) -> u32 {
52
+ self.output.as_bytes().iter().filter(|&&b| b == b'\n').count() as u32
53
+ }
54
+
46
55
  fn escape_ident(s: &str) -> String {
47
56
  let s = s.to_string();
48
57
  if s == "await" || s == "default" {
@@ -52,10 +61,46 @@ impl Codegen {
52
61
  }
53
62
  }
54
63
 
55
- fn emit_program(&mut self, program: &Program) -> Result<(), CompileError> {
64
+ fn emit_program(
65
+ &mut self,
66
+ program: &Program,
67
+ map_sources: Option<(&[PathBuf], &Path)>,
68
+ map_builder: Option<&mut SourceMapBuilder>,
69
+ ) -> Result<(), CompileError> {
56
70
  self.write("// Generated by tishlang_compile_js\n");
57
- for stmt in &program.statements {
58
- self.emit_statement(stmt)?;
71
+ match (map_sources, map_builder) {
72
+ (Some((srcs, root)), Some(sm)) => {
73
+ for (i, stmt) in program.statements.iter().enumerate() {
74
+ if i < srcs.len() {
75
+ let dst_line = self.output_line();
76
+ let sp = stmt.span();
77
+ let src_line = sp.start.0.saturating_sub(1) as u32;
78
+ let src_col = sp.start.1.saturating_sub(1) as u32;
79
+ let abs = srcs[i].as_path();
80
+ let root_canon = root.canonicalize().unwrap_or_else(|_| root.to_path_buf());
81
+ let abs_canon = abs.canonicalize().unwrap_or_else(|_| abs.to_path_buf());
82
+ let rel = abs_canon
83
+ .strip_prefix(&root_canon)
84
+ .unwrap_or(abs_canon.as_path());
85
+ let rel_str = rel.to_string_lossy();
86
+ sm.add(
87
+ dst_line,
88
+ 0,
89
+ src_line,
90
+ src_col,
91
+ Some(rel_str.as_ref()),
92
+ None,
93
+ false,
94
+ );
95
+ }
96
+ self.emit_statement(stmt)?;
97
+ }
98
+ }
99
+ _ => {
100
+ for stmt in &program.statements {
101
+ self.emit_statement(stmt)?;
102
+ }
103
+ }
59
104
  }
60
105
  Ok(())
61
106
  }
@@ -174,7 +219,12 @@ impl Codegen {
174
219
  self.emit_statement(body)?;
175
220
  self.indent -= 1;
176
221
  }
177
- Statement::ForOf { name, iterable, body, .. } => {
222
+ Statement::ForOf {
223
+ name,
224
+ iterable,
225
+ body,
226
+ ..
227
+ } => {
178
228
  let escaped = Self::escape_ident(name.as_ref());
179
229
  let it = self.emit_expr(iterable)?;
180
230
  self.writeln(&format!("for (const {} of {})", escaped, it));
@@ -204,7 +254,10 @@ impl Codegen {
204
254
  let async_prefix = if *async_ { "async " } else { "" };
205
255
  let escaped = Self::escape_ident(name.as_ref());
206
256
  let params_str = self.emit_params(params, rest_param.as_ref())?;
207
- self.writeln(&format!("{}function {} ({}) {{", async_prefix, escaped, params_str));
257
+ self.writeln(&format!(
258
+ "{}function {} ({}) {{",
259
+ async_prefix, escaped, params_str
260
+ ));
208
261
  self.indent += 1;
209
262
  if *async_ {
210
263
  self.in_async = true;
@@ -291,6 +344,9 @@ impl Codegen {
291
344
  Statement::Import { .. } | Statement::Export { .. } => {
292
345
  // Resolved away by merge_modules
293
346
  }
347
+ Statement::TypeAlias { .. }
348
+ | Statement::DeclareVar { .. }
349
+ | Statement::DeclareFun { .. } => {}
294
350
  }
295
351
  Ok(())
296
352
  }
@@ -337,11 +393,9 @@ impl Codegen {
337
393
  let parts: Vec<String> = elements
338
394
  .iter()
339
395
  .map(|el| match el {
340
- Some(DestructElement::Ident(n)) => {
341
- Ok(Self::escape_ident(n.as_ref()))
342
- }
396
+ Some(DestructElement::Ident(n, _)) => Ok(Self::escape_ident(n.as_ref())),
343
397
  Some(DestructElement::Pattern(p)) => self.emit_destruct_pattern(p),
344
- Some(DestructElement::Rest(n)) => {
398
+ Some(DestructElement::Rest(n, _)) => {
345
399
  Ok(format!("...{}", Self::escape_ident(n.as_ref())))
346
400
  }
347
401
  None => Ok("".to_string()),
@@ -355,7 +409,7 @@ impl Codegen {
355
409
  .map(|p| {
356
410
  let k = p.key.as_ref();
357
411
  match &p.value {
358
- DestructElement::Ident(n) => {
412
+ DestructElement::Ident(n, _) => {
359
413
  if k == n.as_ref() {
360
414
  Ok(k.to_string())
361
415
  } else {
@@ -365,7 +419,7 @@ impl Codegen {
365
419
  DestructElement::Pattern(pat) => {
366
420
  Ok(format!("{}: {}", k, self.emit_destruct_pattern(pat)?))
367
421
  }
368
- DestructElement::Rest(n) => {
422
+ DestructElement::Rest(n, _) => {
369
423
  Ok(format!("...{}", Self::escape_ident(n.as_ref())))
370
424
  }
371
425
  }
@@ -385,7 +439,9 @@ impl Codegen {
385
439
  Literal::Null => "null".to_string(),
386
440
  },
387
441
  Expr::Ident { name, .. } => Self::escape_ident(name.as_ref()),
388
- Expr::Binary { left, op, right, .. } => {
442
+ Expr::Binary {
443
+ left, op, right, ..
444
+ } => {
389
445
  let l = self.emit_expr(left)?;
390
446
  let r = self.emit_expr(right)?;
391
447
  let op_str = match op {
@@ -450,12 +506,14 @@ impl Codegen {
450
506
  } => {
451
507
  let obj = self.emit_expr(object)?;
452
508
  let expr = match prop {
453
- MemberProp::Name(p) => {
454
- if p.parse::<u32>().is_ok() || !p.chars().all(|c| c.is_alphanumeric() || c == '_') {
455
- format!("{}[{:?}]", obj, p.as_ref())
509
+ MemberProp::Name { name, .. } => {
510
+ if name.parse::<u32>().is_ok()
511
+ || !name.chars().all(|c| c.is_alphanumeric() || c == '_')
512
+ {
513
+ format!("{}[{:?}]", obj, name.as_ref())
456
514
  } else {
457
515
  let sep = if *optional { "?." } else { "." };
458
- format!("{}{}{}", obj, sep, p.as_ref())
516
+ format!("{}{}{}", obj, sep, name.as_ref())
459
517
  }
460
518
  }
461
519
  MemberProp::Expr(e) => {
@@ -548,7 +606,9 @@ impl Codegen {
548
606
  Expr::PrefixDec { name, .. } => {
549
607
  format!("--{}", Self::escape_ident(name.as_ref()))
550
608
  }
551
- Expr::CompoundAssign { name, op, value, .. } => {
609
+ Expr::CompoundAssign {
610
+ name, op, value, ..
611
+ } => {
552
612
  let n = Self::escape_ident(name.as_ref());
553
613
  let v = self.emit_expr(value)?;
554
614
  let op_str = match op {
@@ -560,7 +620,9 @@ impl Codegen {
560
620
  };
561
621
  format!("({} {} {})", n, op_str, v)
562
622
  }
563
- Expr::LogicalAssign { name, op, value, .. } => {
623
+ Expr::LogicalAssign {
624
+ name, op, value, ..
625
+ } => {
564
626
  let n = Self::escape_ident(name.as_ref());
565
627
  let v = self.emit_expr(value)?;
566
628
  let op_str = match op {
@@ -570,7 +632,12 @@ impl Codegen {
570
632
  };
571
633
  format!("({} {} {})", n, op_str, v)
572
634
  }
573
- Expr::MemberAssign { object, prop, value, .. } => {
635
+ Expr::MemberAssign {
636
+ object,
637
+ prop,
638
+ value,
639
+ ..
640
+ } => {
574
641
  let obj = self.emit_expr(object)?;
575
642
  let val = self.emit_expr(value)?;
576
643
  format!("({}.{} = {})", obj, prop.as_ref(), val)
@@ -652,7 +719,6 @@ impl Codegen {
652
719
  CallArg::Spread(e) => Ok(format!("...{}", self.emit_expr(e)?)),
653
720
  }
654
721
  }
655
-
656
722
  }
657
723
 
658
724
  /// Compile a single program (no imports) to JavaScript. JSX lowers to `h` / `Fragment` (Lattish).
@@ -663,29 +729,45 @@ pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, Com
663
729
  program.clone()
664
730
  };
665
731
  let mut g = Codegen::new();
666
- g.emit_program(&program)?;
732
+ g.emit_program(&program, None, None)?;
667
733
  Ok(g.output)
668
734
  }
669
735
 
670
- /// Compile a project from entry path, resolving and merging modules.
671
- /// Uses shared resolve from tishlang_compile (same pipeline as native/WASM).
672
- pub fn compile_project_with_jsx(
673
- entry_path: &std::path::Path,
674
- project_root: Option<&std::path::Path>,
736
+ /// JavaScript plus optional v3 source map JSON (for publishing Tish libraries consumed from JS/TS).
737
+ #[derive(Debug, Clone)]
738
+ pub struct JsBundle {
739
+ pub js: String,
740
+ pub source_map_json: Option<String>,
741
+ }
742
+
743
+ /// Same as [`compile_project_with_jsx`] plus a v3 source map pointing at merged statements’ original `.tish` files.
744
+ /// **Does not run AST optimization** (required so statement ↔ file alignment stays valid).
745
+ pub fn compile_project_with_jsx_and_source_map(
746
+ entry_path: &Path,
747
+ project_root: Option<&Path>,
748
+ output_js_file_name: &str,
749
+ ) -> Result<JsBundle, CompileError> {
750
+ compile_project_js_inner(entry_path, project_root, false, true, output_js_file_name)
751
+ }
752
+
753
+ fn compile_project_js_inner(
754
+ entry_path: &Path,
755
+ project_root: Option<&Path>,
675
756
  optimize: bool,
676
- ) -> Result<String, CompileError> {
757
+ emit_source_map: bool,
758
+ output_js_file_name: &str,
759
+ ) -> Result<JsBundle, CompileError> {
677
760
  use tishlang_ast::Statement;
678
761
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
679
762
  .map_err(|e| CompileError { message: e })?;
680
763
  tishlang_compile::detect_cycles(&modules).map_err(|e| CompileError { message: e })?;
681
- let program = {
682
- let prog = tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
683
- if optimize {
684
- tishlang_opt::optimize(&prog)
685
- } else {
686
- prog
687
- }
764
+ let merged = tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
765
+ let program = if optimize {
766
+ tishlang_opt::optimize(&merged.program)
767
+ } else {
768
+ merged.program.clone()
688
769
  };
770
+ let stmt_sources = merged.statement_sources;
689
771
  let default_export = program.statements.iter().find_map(|s| {
690
772
  if let Statement::VarDecl { name, .. } = s {
691
773
  let n = name.as_ref();
@@ -698,9 +780,71 @@ pub fn compile_project_with_jsx(
698
780
  None
699
781
  }
700
782
  });
701
- let mut js = compile_with_jsx(&program, optimize)?;
783
+ if emit_source_map && optimize {
784
+ return Err(CompileError {
785
+ message: "internal: source map requested with optimize".into(),
786
+ });
787
+ }
788
+ let root = project_root
789
+ .map(Path::to_path_buf)
790
+ .or_else(|| entry_path.parent().map(Path::to_path_buf))
791
+ .unwrap_or_else(|| PathBuf::from("."));
792
+ let mut gen = Codegen::new();
793
+ let mut map_builder = if emit_source_map {
794
+ let mut b = SourceMapBuilder::new(Some(output_js_file_name));
795
+ b.set_source_root(Some(""));
796
+ Some(b)
797
+ } else {
798
+ None
799
+ };
800
+ if let Some(ref mut b) = map_builder {
801
+ gen.emit_program(
802
+ &program,
803
+ Some((stmt_sources.as_slice(), root.as_path())),
804
+ Some(b),
805
+ )?;
806
+ } else {
807
+ gen.emit_program(&program, None, None)?;
808
+ }
809
+ let mut js = gen.output;
702
810
  if let Some(name) = default_export {
703
811
  js.push_str(&format!("\nexport default {};\n", name));
704
812
  }
705
- Ok(js)
813
+ let map_json = if let Some(b) = map_builder {
814
+ let sm = b.into_sourcemap();
815
+ let mut v = Vec::new();
816
+ sm.to_writer(&mut v).map_err(|e| CompileError {
817
+ message: e.to_string(),
818
+ })?;
819
+ Some(String::from_utf8(v).map_err(|e| CompileError {
820
+ message: e.to_string(),
821
+ })?)
822
+ } else {
823
+ None
824
+ };
825
+ Ok(JsBundle {
826
+ js,
827
+ source_map_json: map_json,
828
+ })
829
+ }
830
+
831
+ /// Compile a project from entry path, resolving and merging modules.
832
+ /// Uses shared resolve from tishlang_compile (same pipeline as native/WASM).
833
+ pub fn compile_project_with_jsx(
834
+ entry_path: &std::path::Path,
835
+ project_root: Option<&std::path::Path>,
836
+ optimize: bool,
837
+ ) -> Result<String, CompileError> {
838
+ let stem = entry_path
839
+ .file_name()
840
+ .and_then(|s| s.to_str())
841
+ .unwrap_or("out.js");
842
+ let out_name = if stem.ends_with(".tish") {
843
+ format!("{}.js", stem.trim_end_matches(".tish"))
844
+ } else {
845
+ format!("{stem}.js")
846
+ };
847
+ Ok(
848
+ compile_project_js_inner(entry_path, project_root, optimize, false, &out_name)?.js,
849
+ )
706
850
  }