@tishlang/tish 1.7.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 (95) hide show
  1. package/Cargo.toml +1 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +15 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_resolve/Cargo.toml +13 -0
  64. package/crates/tish_resolve/src/lib.rs +3436 -0
  65. package/crates/tish_resolve/src/pos.rs +133 -0
  66. package/crates/tish_runtime/Cargo.toml +68 -3
  67. package/crates/tish_runtime/src/http.rs +1123 -141
  68. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  69. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  70. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  71. package/crates/tish_runtime/src/lib.rs +159 -29
  72. package/crates/tish_runtime/src/promise.rs +199 -36
  73. package/crates/tish_runtime/src/promise_io.rs +2 -1
  74. package/crates/tish_runtime/src/timers.rs +37 -1
  75. package/crates/tish_runtime/src/ws.rs +26 -28
  76. package/crates/tish_ui/src/jsx.rs +279 -8
  77. package/crates/tish_ui/src/lib.rs +5 -2
  78. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  79. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  80. package/crates/tish_vm/Cargo.toml +15 -5
  81. package/crates/tish_vm/src/vm.rs +506 -259
  82. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  83. package/crates/tish_wasm/src/lib.rs +17 -14
  84. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  85. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  86. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  87. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  88. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  89. package/justfile +8 -0
  90. package/package.json +1 -1
  91. package/platform/darwin-arm64/tish +0 -0
  92. package/platform/darwin-x64/tish +0 -0
  93. package/platform/linux-arm64/tish +0 -0
  94. package/platform/linux-x64/tish +0 -0
  95. 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,19 +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) => RustType::Vec(Box::new(Self::from_annotation(elem))),
86
+ TypeAnnotation::Array(elem) => RustType::Vec(Box::new(
87
+ Self::from_annotation_with_aliases(elem, aliases),
88
+ )),
49
89
  TypeAnnotation::Object(fields) => {
50
90
  let typed_fields: Vec<_> = fields
51
91
  .iter()
52
- .map(|(k, v)| (k.clone(), Self::from_annotation(v)))
92
+ .map(|(k, v)| (k.clone(), Self::from_annotation_with_aliases(v, aliases)))
53
93
  .collect();
54
94
  RustType::Object(typed_fields)
55
95
  }
56
96
  TypeAnnotation::Function { params, returns } => {
57
- let typed_params: Vec<_> = params.iter().map(Self::from_annotation).collect();
58
- 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));
59
102
  RustType::Function {
60
103
  params: typed_params,
61
104
  returns: typed_returns,
@@ -72,7 +115,9 @@ impl RustType {
72
115
  |t| !matches!(t, TypeAnnotation::Simple(s) if s.as_ref() == "null"),
73
116
  );
74
117
  if let Some(inner) = non_null {
75
- return RustType::Option(Box::new(Self::from_annotation(inner)));
118
+ return RustType::Option(Box::new(
119
+ Self::from_annotation_with_aliases(inner, aliases),
120
+ ));
76
121
  }
77
122
  }
78
123
  }
@@ -130,9 +175,11 @@ impl RustType {
130
175
  RustType::Vec(inner) => format!("Vec<{}>", inner.to_rust_type_str()),
131
176
  RustType::Option(inner) => format!("Option<{}>", inner.to_rust_type_str()),
132
177
  RustType::Object(_) => {
133
- // 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.
134
180
  "Value".to_string()
135
181
  }
182
+ RustType::Named { name, .. } => named_struct_ident(name),
136
183
  RustType::Function { params, returns } => {
137
184
  let params_str: Vec<_> = params.iter().map(|p| p.to_rust_type_str()).collect();
138
185
  format!(
@@ -155,6 +202,23 @@ impl RustType {
155
202
  RustType::Vec(_) => "Vec::new()".to_string(),
156
203
  RustType::Option(_) => "None".to_string(),
157
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
+ }
158
222
  RustType::Function { .. } => "Value::Null".to_string(),
159
223
  }
160
224
  }
@@ -190,6 +254,22 @@ impl RustType {
190
254
  value_expr, inner_conversion
191
255
  )
192
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
+ }
193
273
  _ => value_expr.to_string(), // Fallback
194
274
  }
195
275
  }
@@ -210,7 +290,7 @@ impl RustType {
210
290
  _ => (".iter().cloned()", inner.to_value_expr("v")),
211
291
  };
212
292
  format!(
213
- "Value::Array(Rc::new(RefCell::new({}{}.map(|v| {}).collect())))",
293
+ "Value::Array(VmRef::new({}{}.map(|v| {}).collect()))",
214
294
  native_expr, iter_suffix, val_expr
215
295
  )
216
296
  }
@@ -221,11 +301,57 @@ impl RustType {
221
301
  native_expr, inner_to_value
222
302
  )
223
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
+ }
224
328
  _ => native_expr.to_string(), // Fallback
225
329
  }
226
330
  }
227
331
  }
228
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
+
229
355
  /// Type context for tracking variable types during code generation.
230
356
  #[derive(Debug, Clone, Default)]
231
357
  pub struct TypeContext {
@@ -250,6 +376,23 @@ impl TypeContext {
250
376
  self.scopes.pop();
251
377
  }
252
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
+
253
396
  /// Define a variable in the current scope.
254
397
  pub fn define(&mut self, name: &str, ty: RustType) {
255
398
  if let Some(scope) = self.scopes.last_mut() {
@@ -334,4 +477,25 @@ mod tests {
334
477
  ctx.pop_scope();
335
478
  assert_eq!(ctx.get_type("y"), RustType::Value); // y no longer visible
336
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
+ }
337
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,
@@ -45,6 +48,10 @@ impl Codegen {
45
48
  self.output.push('\n');
46
49
  }
47
50
 
51
+ fn output_line(&self) -> u32 {
52
+ self.output.as_bytes().iter().filter(|&&b| b == b'\n').count() as u32
53
+ }
54
+
48
55
  fn escape_ident(s: &str) -> String {
49
56
  let s = s.to_string();
50
57
  if s == "await" || s == "default" {
@@ -54,10 +61,46 @@ impl Codegen {
54
61
  }
55
62
  }
56
63
 
57
- 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> {
58
70
  self.write("// Generated by tishlang_compile_js\n");
59
- for stmt in &program.statements {
60
- 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
+ }
61
104
  }
62
105
  Ok(())
63
106
  }
@@ -301,6 +344,9 @@ impl Codegen {
301
344
  Statement::Import { .. } | Statement::Export { .. } => {
302
345
  // Resolved away by merge_modules
303
346
  }
347
+ Statement::TypeAlias { .. }
348
+ | Statement::DeclareVar { .. }
349
+ | Statement::DeclareFun { .. } => {}
304
350
  }
305
351
  Ok(())
306
352
  }
@@ -347,9 +393,9 @@ impl Codegen {
347
393
  let parts: Vec<String> = elements
348
394
  .iter()
349
395
  .map(|el| match el {
350
- Some(DestructElement::Ident(n)) => Ok(Self::escape_ident(n.as_ref())),
396
+ Some(DestructElement::Ident(n, _)) => Ok(Self::escape_ident(n.as_ref())),
351
397
  Some(DestructElement::Pattern(p)) => self.emit_destruct_pattern(p),
352
- Some(DestructElement::Rest(n)) => {
398
+ Some(DestructElement::Rest(n, _)) => {
353
399
  Ok(format!("...{}", Self::escape_ident(n.as_ref())))
354
400
  }
355
401
  None => Ok("".to_string()),
@@ -363,7 +409,7 @@ impl Codegen {
363
409
  .map(|p| {
364
410
  let k = p.key.as_ref();
365
411
  match &p.value {
366
- DestructElement::Ident(n) => {
412
+ DestructElement::Ident(n, _) => {
367
413
  if k == n.as_ref() {
368
414
  Ok(k.to_string())
369
415
  } else {
@@ -373,7 +419,7 @@ impl Codegen {
373
419
  DestructElement::Pattern(pat) => {
374
420
  Ok(format!("{}: {}", k, self.emit_destruct_pattern(pat)?))
375
421
  }
376
- DestructElement::Rest(n) => {
422
+ DestructElement::Rest(n, _) => {
377
423
  Ok(format!("...{}", Self::escape_ident(n.as_ref())))
378
424
  }
379
425
  }
@@ -460,14 +506,14 @@ impl Codegen {
460
506
  } => {
461
507
  let obj = self.emit_expr(object)?;
462
508
  let expr = match prop {
463
- MemberProp::Name(p) => {
464
- if p.parse::<u32>().is_ok()
465
- || !p.chars().all(|c| c.is_alphanumeric() || c == '_')
509
+ MemberProp::Name { name, .. } => {
510
+ if name.parse::<u32>().is_ok()
511
+ || !name.chars().all(|c| c.is_alphanumeric() || c == '_')
466
512
  {
467
- format!("{}[{:?}]", obj, p.as_ref())
513
+ format!("{}[{:?}]", obj, name.as_ref())
468
514
  } else {
469
515
  let sep = if *optional { "?." } else { "." };
470
- format!("{}{}{}", obj, sep, p.as_ref())
516
+ format!("{}{}{}", obj, sep, name.as_ref())
471
517
  }
472
518
  }
473
519
  MemberProp::Expr(e) => {
@@ -683,30 +729,45 @@ pub fn compile_with_jsx(program: &Program, optimize: bool) -> Result<String, Com
683
729
  program.clone()
684
730
  };
685
731
  let mut g = Codegen::new();
686
- g.emit_program(&program)?;
732
+ g.emit_program(&program, None, None)?;
687
733
  Ok(g.output)
688
734
  }
689
735
 
690
- /// Compile a project from entry path, resolving and merging modules.
691
- /// Uses shared resolve from tishlang_compile (same pipeline as native/WASM).
692
- pub fn compile_project_with_jsx(
693
- entry_path: &std::path::Path,
694
- 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>,
695
756
  optimize: bool,
696
- ) -> Result<String, CompileError> {
757
+ emit_source_map: bool,
758
+ output_js_file_name: &str,
759
+ ) -> Result<JsBundle, CompileError> {
697
760
  use tishlang_ast::Statement;
698
761
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
699
762
  .map_err(|e| CompileError { message: e })?;
700
763
  tishlang_compile::detect_cycles(&modules).map_err(|e| CompileError { message: e })?;
701
- let program = {
702
- let prog =
703
- tishlang_compile::merge_modules(modules).map_err(|e| CompileError { message: e })?;
704
- if optimize {
705
- tishlang_opt::optimize(&prog)
706
- } else {
707
- prog
708
- }
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()
709
769
  };
770
+ let stmt_sources = merged.statement_sources;
710
771
  let default_export = program.statements.iter().find_map(|s| {
711
772
  if let Statement::VarDecl { name, .. } = s {
712
773
  let n = name.as_ref();
@@ -719,9 +780,71 @@ pub fn compile_project_with_jsx(
719
780
  None
720
781
  }
721
782
  });
722
- 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;
723
810
  if let Some(name) = default_export {
724
811
  js.push_str(&format!("\nexport default {};\n", name));
725
812
  }
726
- 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
+ )
727
850
  }
@@ -7,7 +7,9 @@ mod error;
7
7
  #[cfg(test)]
8
8
  mod tests_jsx;
9
9
 
10
- pub use codegen::{compile_project_with_jsx, compile_with_jsx};
10
+ pub use codegen::{
11
+ compile_project_with_jsx, compile_project_with_jsx_and_source_map, compile_with_jsx, JsBundle,
12
+ };
11
13
  pub use error::CompileError;
12
14
 
13
15
  /// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.