@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
@@ -49,6 +49,7 @@ pub struct NativeBuildArtifacts {
49
49
  const BUILTIN_ALIASES: &[(&str, &str)] = &[
50
50
  ("fs", "tish:fs"),
51
51
  ("http", "tish:http"),
52
+ ("timers", "tish:timers"),
52
53
  ("process", "tish:process"),
53
54
  ("ws", "tish:ws"),
54
55
  ];
@@ -66,14 +67,17 @@ pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
66
67
 
67
68
  /// Built-in modules that come from tishlang_runtime, not from package.json.
68
69
  pub fn is_builtin_native_spec(spec: &str) -> bool {
69
- matches!(spec, "tish:fs" | "tish:http" | "tish:process" | "tish:ws")
70
- || matches!(spec, "fs" | "http" | "process" | "ws")
70
+ matches!(spec, "tish:fs" | "tish:http" | "tish:timers" | "tish:process" | "tish:ws")
71
+ || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
71
72
  }
72
73
 
73
74
  /// Resolve all native imports in a merged program via package.json lookup.
74
75
  /// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tishlang_runtime directly.
75
76
  /// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
76
- pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
77
+ pub fn resolve_native_modules(
78
+ program: &Program,
79
+ project_root: &Path,
80
+ ) -> Result<Vec<ResolvedNativeModule>, String> {
77
81
  let root_canon = project_root
78
82
  .canonicalize()
79
83
  .map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
@@ -131,12 +135,17 @@ pub fn cargo_export_fn_name(spec: &str) -> String {
131
135
  out
132
136
  }
133
137
 
134
- fn resolve_cargo_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
138
+ fn resolve_cargo_native_module(
139
+ spec: &str,
140
+ project_root: &Path,
141
+ ) -> Result<ResolvedNativeModule, String> {
135
142
  let tail = spec
136
143
  .strip_prefix("cargo:")
137
144
  .ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
138
145
  if tail.is_empty() {
139
- return Err("cargo: import needs a dependency name, e.g. import { x } from 'cargo:serde_json'".into());
146
+ return Err(
147
+ "cargo: import needs a dependency name, e.g. import { x } from 'cargo:my_crate'".into(),
148
+ );
140
149
  }
141
150
  let dep_key = tail.to_string();
142
151
  let tish = read_project_tish_config(project_root);
@@ -179,14 +188,26 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
179
188
  let pkg_json = pkg_dir.join("package.json");
180
189
  let content = std::fs::read_to_string(&pkg_json)
181
190
  .map_err(|e| format!("Cannot read {}: {}", pkg_json.display(), e))?;
182
- let json: serde_json::Value =
183
- serde_json::from_str(&content).map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
191
+ let json: serde_json::Value = serde_json::from_str(&content)
192
+ .map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
184
193
  let tish = json
185
194
  .get("tish")
186
195
  .and_then(|v| v.as_object())
187
- .ok_or_else(|| format!("Package {} has no \"tish\" config in package.json", package_name))?;
188
- if !tish.get("module").and_then(|v| v.as_bool()).unwrap_or(false) {
189
- return Err(format!("Package {} is not a Tish native module (tish.module must be true)", package_name));
196
+ .ok_or_else(|| {
197
+ format!(
198
+ "Package {} has no \"tish\" config in package.json",
199
+ package_name
200
+ )
201
+ })?;
202
+ if !tish
203
+ .get("module")
204
+ .and_then(|v| v.as_bool())
205
+ .unwrap_or(false)
206
+ {
207
+ return Err(format!(
208
+ "Package {} is not a Tish native module (tish.module must be true)",
209
+ package_name
210
+ ));
190
211
  }
191
212
  let raw_crate = tish
192
213
  .get("crate")
@@ -219,7 +240,9 @@ pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
219
240
  let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
220
241
  return serde_json::json!({});
221
242
  };
222
- json.get("tish").cloned().unwrap_or_else(|| serde_json::json!({}))
243
+ json.get("tish")
244
+ .cloned()
245
+ .unwrap_or_else(|| serde_json::json!({}))
223
246
  }
224
247
 
225
248
  fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
@@ -233,7 +256,10 @@ fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
233
256
  resolved.display().to_string().replace('\\', "/")
234
257
  }
235
258
 
236
- fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Result<String, String> {
259
+ fn json_to_cargo_inline_value(
260
+ v: &serde_json::Value,
261
+ project_root: &Path,
262
+ ) -> Result<String, String> {
237
263
  match v {
238
264
  serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
239
265
  serde_json::Value::Bool(b) => Ok(b.to_string()),
@@ -264,7 +290,10 @@ fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Res
264
290
 
265
291
  /// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
266
292
  /// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
267
- pub fn format_rust_dependencies_toml(tish: &serde_json::Value, project_root: &Path) -> Result<String, String> {
293
+ pub fn format_rust_dependencies_toml(
294
+ tish: &serde_json::Value,
295
+ project_root: &Path,
296
+ ) -> Result<String, String> {
268
297
  let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
269
298
  return Ok(String::new());
270
299
  };
@@ -313,7 +342,10 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
313
342
  for stmt in &program.statements {
314
343
  match stmt {
315
344
  Statement::VarDecl {
316
- init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
345
+ init:
346
+ Some(Expr::NativeModuleLoad {
347
+ spec, export_name, ..
348
+ }),
317
349
  ..
318
350
  } => {
319
351
  let s = spec.as_ref();
@@ -324,8 +356,11 @@ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet
324
356
  .or_default()
325
357
  .insert(export_name.to_string());
326
358
  }
327
- Statement::Import { specifiers, from, .. } if is_native_import(from.as_ref()) => {
328
- let spec = normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
359
+ Statement::Import {
360
+ specifiers, from, ..
361
+ } if is_native_import(from.as_ref()) => {
362
+ let spec =
363
+ normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
329
364
  if is_builtin_native_spec(&spec) {
330
365
  continue;
331
366
  }
@@ -354,11 +389,15 @@ pub fn generate_native_wrapper_rs(
354
389
  use std::cell::RefCell;\n\
355
390
  use std::rc::Rc;\n\
356
391
  use std::sync::Arc;\n\
357
- use tishlang_runtime::{ObjectMap, Value};\n\n",
392
+ use tishlang_runtime::{ObjectMap, Value, VmRef};\n\n",
358
393
  );
359
394
  let mut any = false;
360
395
  for m in modules {
361
- let Some(NativeModuleInit::Generated { shim_crate, export_fn }) = init_by_spec.get(&m.spec) else {
396
+ let Some(NativeModuleInit::Generated {
397
+ shim_crate,
398
+ export_fn,
399
+ }) = init_by_spec.get(&m.spec)
400
+ else {
362
401
  continue;
363
402
  };
364
403
  let Some(names) = inferred.get(&m.spec) else {
@@ -376,11 +415,11 @@ pub fn generate_native_wrapper_rs(
376
415
  let rust_fn = export_name_to_rust_ident(&export_name);
377
416
  let key_lit = format!("{:?}", export_name);
378
417
  file.push_str(&format!(
379
- " m.insert(Arc::from({}), Value::Function(Rc::new(|args: &[Value]| {{\n {}::{}(args)\n }})));\n",
418
+ " m.insert(Arc::from({}), Value::native(|args: &[Value]| {{\n {}::{}(args)\n }}));\n",
380
419
  key_lit, shim_crate, rust_fn
381
420
  ));
382
421
  }
383
- file.push_str(" Value::Object(Rc::new(RefCell::new(m)))\n}\n\n");
422
+ file.push_str(" Value::Object(VmRef::new(m))\n}\n\n");
384
423
  }
385
424
  if !any {
386
425
  return String::new();
@@ -405,9 +444,16 @@ pub fn compute_native_build_artifacts(
405
444
  let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
406
445
  for m in native_modules {
407
446
  let use_gen = if is_cargo_native_spec(&m.spec) {
408
- inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
447
+ inferred
448
+ .get(&m.spec)
449
+ .map(|s| !s.is_empty())
450
+ .unwrap_or(false)
409
451
  } else {
410
- gen_tish && inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
452
+ gen_tish
453
+ && inferred
454
+ .get(&m.spec)
455
+ .map(|s| !s.is_empty())
456
+ .unwrap_or(false)
411
457
  };
412
458
  let init = if use_gen {
413
459
  NativeModuleInit::Generated {
@@ -538,13 +584,18 @@ pub fn resolve_project(
538
584
  entry_path: &Path,
539
585
  project_root: Option<&Path>,
540
586
  ) -> Result<Vec<ResolvedModule>, String> {
541
- let project_root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
587
+ let project_root =
588
+ project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
542
589
  let entry_canon = entry_path
543
590
  .canonicalize()
544
591
  .map_err(|e| format!("Cannot canonicalize entry {}: {}", entry_path.display(), e))?;
545
- let root_canon = project_root
546
- .canonicalize()
547
- .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
592
+ let root_canon = project_root.canonicalize().map_err(|e| {
593
+ format!(
594
+ "Cannot canonicalize project root {}: {}",
595
+ project_root.display(),
596
+ e
597
+ )
598
+ })?;
548
599
 
549
600
  let mut visited = HashSet::new();
550
601
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
@@ -574,21 +625,23 @@ pub fn resolve_project_from_stdin(
574
625
  source: &str,
575
626
  project_root: &Path,
576
627
  ) -> Result<Vec<ResolvedModule>, String> {
577
- let root_canon = project_root
578
- .canonicalize()
579
- .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
628
+ let root_canon = project_root.canonicalize().map_err(|e| {
629
+ format!(
630
+ "Cannot canonicalize project root {}: {}",
631
+ project_root.display(),
632
+ e
633
+ )
634
+ })?;
580
635
 
581
636
  let stdin_path = root_canon.join("<stdin>");
582
- let program = tishlang_parser::parse(source)
583
- .map_err(|e| format!("Parse error (stdin): {}", e))?;
637
+ let program =
638
+ tishlang_parser::parse(source).map_err(|e| format!("Parse error (stdin): {}", e))?;
584
639
 
585
640
  let mut visited = HashSet::new();
586
641
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
587
642
  let mut load_order: Vec<PathBuf> = Vec::new();
588
643
 
589
- let from_dir = stdin_path
590
- .parent()
591
- .unwrap_or_else(|| Path::new("."));
644
+ let from_dir = stdin_path.parent().unwrap_or_else(|| Path::new("."));
592
645
 
593
646
  for stmt in &program.statements {
594
647
  if let Statement::Import { from, .. } = stmt {
@@ -667,7 +720,7 @@ fn load_module_recursive(
667
720
  }
668
721
 
669
722
  /// Returns true for native module imports that don't resolve to files.
670
- /// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
723
+ /// - fs, http, timers, process, ws (Node-compatible aliases for tish:*)
671
724
  /// - tish:egui, tish:polars, etc.
672
725
  /// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
673
726
  /// - @scope/package (npm-style)
@@ -675,7 +728,7 @@ pub fn is_native_import(spec: &str) -> bool {
675
728
  spec.starts_with("tish:")
676
729
  || spec.starts_with("cargo:")
677
730
  || spec.starts_with('@')
678
- || matches!(spec, "fs" | "http" | "process" | "ws")
731
+ || matches!(spec, "fs" | "http" | "timers" | "process" | "ws")
679
732
  }
680
733
 
681
734
  /// Map native spec to Cargo feature name for built-in tish:* modules.
@@ -684,30 +737,52 @@ pub fn native_spec_to_feature(spec: &str) -> Option<String> {
684
737
  canonical.strip_prefix("tish:").map(|s| s.to_string())
685
738
  }
686
739
 
687
- /// Resolve a bare specifier (e.g. "lattish") to a path via node_modules.
688
- fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
740
+ /// Resolve `package.json` at `pkg_root` to the package's main `.tish` entry, if `name` matches `spec`.
741
+ fn resolve_package_root_to_entry(pkg_root: &Path, spec: &str) -> Option<PathBuf> {
742
+ let pkg_json = pkg_root.join("package.json");
743
+ if !pkg_json.exists() {
744
+ return None;
745
+ }
746
+ if read_package_name(&pkg_json).as_deref() != Some(spec) {
747
+ return None;
748
+ }
749
+ let content = std::fs::read_to_string(&pkg_json).ok()?;
750
+ let json: serde_json::Value = serde_json::from_str(&content).ok()?;
751
+ let entry = json
752
+ .get("tish")
753
+ .and_then(|t| t.get("module"))
754
+ .and_then(|m| m.as_str())
755
+ .or_else(|| json.get("main").and_then(|m| m.as_str()))
756
+ .unwrap_or("index.tish");
757
+ let entry_clean = entry.trim_start_matches("./");
758
+ let resolved = pkg_root.join(entry_clean);
759
+ if !resolved.exists() {
760
+ return None;
761
+ }
762
+ match resolved.canonicalize() {
763
+ Ok(p) => Some(p),
764
+ Err(_) => Some(resolved),
765
+ }
766
+ }
767
+
768
+ /// Resolve a bare specifier (e.g. "lattish") to the package entry `.tish` file.
769
+ ///
770
+ /// Walks upward from `from_dir` and, at each level, checks (same order as native [`find_package_dir`]):
771
+ /// - `node_modules/<spec>/`
772
+ /// - `<spec>/` as a sibling directory (monorepo: `…/tish/tish-candle` next to `…/tish/tish-hub`)
773
+ /// - the search directory itself if its `package.json` name matches `spec`
774
+ pub fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
689
775
  let mut search = from_dir.to_path_buf();
690
776
  loop {
691
- let node_mod = search.join("node_modules").join(spec);
692
- let pkg_json = node_mod.join("package.json");
693
- if pkg_json.exists() {
694
- if let Some(name) = read_package_name(&pkg_json) {
695
- if name == spec {
696
- let content = std::fs::read_to_string(&pkg_json).ok()?;
697
- let json: serde_json::Value = serde_json::from_str(&content).ok()?;
698
- let entry = json
699
- .get("tish")
700
- .and_then(|t| t.get("module"))
701
- .and_then(|m| m.as_str())
702
- .or_else(|| json.get("main").and_then(|m| m.as_str()));
703
- let entry = entry.unwrap_or("index.tish");
704
- let entry_clean = entry.trim_start_matches("./");
705
- let resolved = node_mod.join(entry_clean);
706
- if resolved.exists() {
707
- return resolved.canonicalize().ok();
708
- }
709
- }
710
- }
777
+ if let Some(p) = resolve_package_root_to_entry(&search.join("node_modules").join(spec), spec)
778
+ {
779
+ return Some(p);
780
+ }
781
+ if let Some(p) = resolve_package_root_to_entry(&search.join(spec), spec) {
782
+ return Some(p);
783
+ }
784
+ if let Some(p) = resolve_package_root_to_entry(&search, spec) {
785
+ return Some(p);
711
786
  }
712
787
  if let Some(parent) = search.parent() {
713
788
  if parent == search {
@@ -738,7 +813,7 @@ fn resolve_import_path(
738
813
  return Ok(path);
739
814
  }
740
815
  return Err(format!(
741
- "Package '{}' not found in node_modules. Install it with: npm install {}",
816
+ "Package '{}' not found. Install with `npm install {}`, or place the package under node_modules/ or as a sibling directory (same layout as native `find_package_dir`).",
742
817
  spec, spec
743
818
  ));
744
819
  }
@@ -787,7 +862,10 @@ pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
787
862
  .iter()
788
863
  .map(|&i| modules[i].path.display().to_string())
789
864
  .collect();
790
- return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
865
+ return Err(format!(
866
+ "Circular import detected: {}",
867
+ path_names.join(" -> ")
868
+ ));
791
869
  }
792
870
  }
793
871
  Ok(())
@@ -817,14 +895,8 @@ fn has_cycle_from(
817
895
  stack.push(dep_idx);
818
896
  let dep = &modules[dep_idx];
819
897
  let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
820
- if has_cycle_from(
821
- dep_dir,
822
- &dep.program,
823
- path_to_idx,
824
- modules,
825
- stack,
826
- visiting,
827
- )? {
898
+ if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
899
+ {
828
900
  return Ok(true);
829
901
  }
830
902
  stack.pop();
@@ -836,10 +908,27 @@ fn has_cycle_from(
836
908
  Ok(false)
837
909
  }
838
910
 
911
+ /// Result of [`merge_modules`]: merged AST plus, per top-level statement, the originating `.tish` file.
912
+ #[derive(Debug)]
913
+ pub struct MergedProgram {
914
+ pub program: Program,
915
+ pub statement_sources: Vec<PathBuf>,
916
+ }
917
+
918
+ fn merge_push(
919
+ statements: &mut Vec<Statement>,
920
+ statement_sources: &mut Vec<PathBuf>,
921
+ stmt: Statement,
922
+ source: PathBuf,
923
+ ) {
924
+ statements.push(stmt);
925
+ statement_sources.push(source);
926
+ }
927
+
839
928
  /// Merge all resolved modules into a single program. Dependencies are emitted first.
840
929
  /// Import statements are rewritten as bindings from already-emitted dep exports.
841
930
  /// Export statements are unwrapped (the inner declaration is emitted).
842
- pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
931
+ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<MergedProgram, String> {
843
932
  let path_to_idx: HashMap<PathBuf, usize> = modules
844
933
  .iter()
845
934
  .enumerate()
@@ -870,45 +959,62 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
870
959
  }
871
960
 
872
961
  let mut statements = Vec::new();
962
+ let mut statement_sources = Vec::new();
873
963
  for (idx, module) in modules.iter().enumerate() {
964
+ let src_path = module.path.clone();
874
965
  let dir = module.path.parent().unwrap_or(Path::new("."));
875
966
  for stmt in &module.program.statements {
876
967
  match stmt {
877
- Statement::Import { specifiers, from, span } => {
968
+ Statement::Import {
969
+ specifiers,
970
+ from,
971
+ span,
972
+ } => {
878
973
  if is_native_import(from.as_ref()) {
879
974
  // Normalize fs/http/process -> tish:fs etc. for Node compatibility
880
- let canonical_spec =
881
- normalize_builtin_spec(from.as_ref())
882
- .unwrap_or_else(|| from.to_string());
975
+ let canonical_spec = normalize_builtin_spec(from.as_ref())
976
+ .unwrap_or_else(|| from.to_string());
883
977
  // Emit VarDecl with NativeModuleLoad for each specifier
884
978
  for spec in specifiers {
885
979
  match spec {
886
- ImportSpecifier::Named { name, alias } => {
980
+ ImportSpecifier::Named {
981
+ name,
982
+ name_span,
983
+ alias,
984
+ alias_span,
985
+ } => {
887
986
  let bind = alias.as_deref().unwrap_or(name.as_ref());
987
+ let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
888
988
  let init = Expr::NativeModuleLoad {
889
989
  spec: Arc::from(canonical_spec.clone()),
890
990
  export_name: name.clone(),
891
991
  span: *span,
892
992
  };
893
- statements.push(Statement::VarDecl {
894
- name: Arc::from(bind),
895
- mutable: false,
896
- type_ann: None,
897
- init: Some(init),
898
- span: *span,
899
- });
993
+ merge_push(
994
+ &mut statements,
995
+ &mut statement_sources,
996
+ Statement::VarDecl {
997
+ name: Arc::from(bind),
998
+ name_span: *decl_name_span,
999
+ mutable: false,
1000
+ type_ann: None,
1001
+ init: Some(init),
1002
+ span: *span,
1003
+ },
1004
+ src_path.clone(),
1005
+ );
900
1006
  }
901
- ImportSpecifier::Namespace(ns) => {
1007
+ ImportSpecifier::Namespace { name, .. } => {
902
1008
  return Err(format!(
903
1009
  "Namespace import (* as {}) not supported for native module '{}'",
904
- ns.as_ref(),
1010
+ name.as_ref(),
905
1011
  from.as_ref()
906
1012
  ));
907
1013
  }
908
- ImportSpecifier::Default(bind) => {
1014
+ ImportSpecifier::Default { name, .. } => {
909
1015
  return Err(format!(
910
1016
  "Default import '{}' not supported for native module '{}'. Use named import, e.g. import {{ egui }} from '{}'",
911
- bind.as_ref(),
1017
+ name.as_ref(),
912
1018
  from.as_ref(),
913
1019
  from.as_ref()
914
1020
  ));
@@ -918,35 +1024,45 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
918
1024
  continue;
919
1025
  }
920
1026
  let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
921
- let dep_path = dep_path
922
- .canonicalize()
923
- .unwrap_or(dep_path);
1027
+ let dep_path = dep_path.canonicalize().unwrap_or(dep_path);
924
1028
  let dep_idx = *path_to_idx
925
1029
  .get(&dep_path)
926
1030
  .ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
927
1031
  let dep_exports = &module_exports[dep_idx];
928
1032
  for spec in specifiers {
929
1033
  match spec {
930
- ImportSpecifier::Named { name, alias } => {
1034
+ ImportSpecifier::Named {
1035
+ name,
1036
+ name_span,
1037
+ alias,
1038
+ alias_span,
1039
+ } => {
931
1040
  let source = dep_exports
932
1041
  .get(name.as_ref())
933
1042
  .cloned()
934
1043
  .unwrap_or_else(|| name.to_string());
935
1044
  let bind = alias.as_deref().unwrap_or(name.as_ref());
936
1045
  if bind != source {
937
- statements.push(Statement::VarDecl {
938
- name: Arc::from(bind),
939
- mutable: false,
940
- type_ann: None,
941
- init: Some(Expr::Ident {
942
- name: Arc::from(source),
1046
+ let decl_name_span = alias_span.as_ref().unwrap_or(name_span);
1047
+ merge_push(
1048
+ &mut statements,
1049
+ &mut statement_sources,
1050
+ Statement::VarDecl {
1051
+ name: Arc::from(bind),
1052
+ name_span: *decl_name_span,
1053
+ mutable: false,
1054
+ type_ann: None,
1055
+ init: Some(Expr::Ident {
1056
+ name: Arc::from(source),
1057
+ span: *span,
1058
+ }),
943
1059
  span: *span,
944
- }),
945
- span: *span,
946
- });
1060
+ },
1061
+ src_path.clone(),
1062
+ );
947
1063
  }
948
1064
  }
949
- ImportSpecifier::Namespace(ns) => {
1065
+ ImportSpecifier::Namespace { name, name_span } => {
950
1066
  let mut props = Vec::new();
951
1067
  for (k, v) in dep_exports {
952
1068
  props.push(tishlang_ast::ObjectProp::KeyValue(
@@ -957,58 +1073,83 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
957
1073
  },
958
1074
  ));
959
1075
  }
960
- statements.push(Statement::VarDecl {
961
- name: ns.clone(),
962
- mutable: false,
963
- type_ann: None,
964
- init: Some(Expr::Object {
965
- props,
1076
+ merge_push(
1077
+ &mut statements,
1078
+ &mut statement_sources,
1079
+ Statement::VarDecl {
1080
+ name: name.clone(),
1081
+ name_span: *name_span,
1082
+ mutable: false,
1083
+ type_ann: None,
1084
+ init: Some(Expr::Object { props, span: *span }),
966
1085
  span: *span,
967
- }),
968
- span: *span,
969
- });
1086
+ },
1087
+ src_path.clone(),
1088
+ );
970
1089
  }
971
- ImportSpecifier::Default(bind) => {
972
- let source = dep_exports
973
- .get("default")
974
- .cloned()
975
- .ok_or_else(|| {
1090
+ ImportSpecifier::Default { name, name_span } => {
1091
+ let source =
1092
+ dep_exports.get("default").cloned().ok_or_else(|| {
976
1093
  format!("Module '{}' has no default export", from)
977
1094
  })?;
978
- statements.push(Statement::VarDecl {
979
- name: bind.clone(),
980
- mutable: false,
981
- type_ann: None,
982
- init: Some(Expr::Ident {
983
- name: Arc::from(source),
1095
+ merge_push(
1096
+ &mut statements,
1097
+ &mut statement_sources,
1098
+ Statement::VarDecl {
1099
+ name: name.clone(),
1100
+ name_span: *name_span,
1101
+ mutable: false,
1102
+ type_ann: None,
1103
+ init: Some(Expr::Ident {
1104
+ name: Arc::from(source),
1105
+ span: *span,
1106
+ }),
984
1107
  span: *span,
985
- }),
986
- span: *span,
987
- });
1108
+ },
1109
+ src_path.clone(),
1110
+ );
988
1111
  }
989
1112
  }
990
1113
  }
991
1114
  }
992
- Statement::Export { declaration, .. } => {
993
- match declaration.as_ref() {
994
- ExportDeclaration::Named(s) => statements.push(*s.clone()),
995
- ExportDeclaration::Default(e) => {
996
- let default_name = format!("__default_{}", idx);
997
- statements.push(Statement::VarDecl {
1115
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1116
+ ExportDeclaration::Named(s) => merge_push(
1117
+ &mut statements,
1118
+ &mut statement_sources,
1119
+ *s.clone(),
1120
+ src_path.clone(),
1121
+ ),
1122
+ ExportDeclaration::Default(e) => {
1123
+ let default_name = format!("__default_{}", idx);
1124
+ let espan = e.span();
1125
+ merge_push(
1126
+ &mut statements,
1127
+ &mut statement_sources,
1128
+ Statement::VarDecl {
998
1129
  name: Arc::from(default_name),
1130
+ name_span: espan,
999
1131
  mutable: false,
1000
1132
  type_ann: None,
1001
1133
  init: Some((*e).clone()),
1002
- span: e.span(),
1003
- });
1004
- }
1134
+ span: espan,
1135
+ },
1136
+ src_path.clone(),
1137
+ );
1005
1138
  }
1006
- }
1007
- _ => statements.push(stmt.clone()),
1139
+ },
1140
+ _ => merge_push(
1141
+ &mut statements,
1142
+ &mut statement_sources,
1143
+ stmt.clone(),
1144
+ src_path.clone(),
1145
+ ),
1008
1146
  }
1009
1147
  }
1010
1148
  }
1011
- Ok(Program { statements })
1149
+ Ok(MergedProgram {
1150
+ program: Program { statements },
1151
+ statement_sources,
1152
+ })
1012
1153
  }
1013
1154
 
1014
1155
  #[cfg(test)]
@@ -1045,14 +1186,14 @@ mod cargo_spec_tests {
1045
1186
  std::fs::write(&p, src).unwrap();
1046
1187
  let root = dir.path();
1047
1188
  let modules = resolve_project(&p, Some(root)).unwrap();
1048
- merge_modules(modules).unwrap();
1189
+ let _ = merge_modules(modules).unwrap();
1049
1190
  }
1050
1191
 
1051
1192
  #[test]
1052
1193
  fn cargo_export_fn_name_sanitizes() {
1053
1194
  assert_eq!(
1054
- cargo_export_fn_name("cargo:serde_json"),
1055
- "cargo_native_serde_json_object"
1195
+ cargo_export_fn_name("cargo:tish_serde_json"),
1196
+ "cargo_native_tish_serde_json_object"
1056
1197
  );
1057
1198
  assert_eq!(
1058
1199
  cargo_export_fn_name("cargo:my-crate"),