@tishlang/tish 2.10.1 → 2.16.13

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 (75) hide show
  1. package/Cargo.toml +3 -0
  2. package/bin/tish +0 -0
  3. package/crates/tish/Cargo.toml +1 -1
  4. package/crates/tish/src/cli_help.rs +16 -0
  5. package/crates/tish/src/main.rs +24 -4
  6. package/crates/tish/tests/integration_test.rs +149 -0
  7. package/crates/tish_ast/src/ast.rs +10 -0
  8. package/crates/tish_builtins/src/array.rs +529 -56
  9. package/crates/tish_builtins/src/collections.rs +114 -40
  10. package/crates/tish_builtins/src/string.rs +95 -8
  11. package/crates/tish_bytecode/src/chunk.rs +7 -0
  12. package/crates/tish_bytecode/src/compiler.rs +560 -64
  13. package/crates/tish_bytecode/src/lib.rs +1 -1
  14. package/crates/tish_bytecode/src/opcode.rs +154 -2
  15. package/crates/tish_bytecode/src/serialize.rs +2 -0
  16. package/crates/tish_bytecode/tests/append_local_string_builder.rs +63 -0
  17. package/crates/tish_bytecode/tests/math_unary_intrinsic.rs +47 -0
  18. package/crates/tish_compile/src/codegen.rs +17736 -5269
  19. package/crates/tish_compile/src/infer.rs +1707 -190
  20. package/crates/tish_compile/src/lib.rs +29 -4
  21. package/crates/tish_compile/src/resolve.rs +66 -5
  22. package/crates/tish_compile/src/types.rs +224 -28
  23. package/crates/tish_compile/tests/dump_codegen.rs +21 -0
  24. package/crates/tish_compile/tests/perf_codegen_169_173.rs +8 -17
  25. package/crates/tish_compile/tests/perf_codegen_173_part3.rs +61 -0
  26. package/crates/tish_compile/tests/perf_codegen_174.rs +160 -0
  27. package/crates/tish_compile/tests/perf_codegen_175.rs +190 -0
  28. package/crates/tish_compile/tests/perf_codegen_176.rs +60 -0
  29. package/crates/tish_compile/tests/perf_codegen_177.rs +167 -0
  30. package/crates/tish_compile/tests/perf_codegen_178.rs +114 -0
  31. package/crates/tish_compile/tests/perf_codegen_178_rec.rs +124 -0
  32. package/crates/tish_compile/tests/perf_codegen_181.rs +36 -0
  33. package/crates/tish_compile/tests/perf_codegen_320.rs +211 -0
  34. package/crates/tish_compile/tests/perf_codegen_module_const_forof.rs +40 -0
  35. package/crates/tish_compile_js/src/codegen.rs +91 -4
  36. package/crates/tish_compile_js/src/lib.rs +5 -2
  37. package/crates/tish_compile_js/src/tests_jsx.rs +175 -7
  38. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +100 -2
  39. package/crates/tish_core/Cargo.toml +4 -0
  40. package/crates/tish_core/src/json.rs +104 -5
  41. package/crates/tish_core/src/lib.rs +174 -0
  42. package/crates/tish_core/src/shape.rs +4 -2
  43. package/crates/tish_core/src/value.rs +565 -35
  44. package/crates/tish_core/src/vmref.rs +14 -0
  45. package/crates/tish_eval/src/eval.rs +675 -76
  46. package/crates/tish_eval/src/natives.rs +19 -0
  47. package/crates/tish_eval/src/value.rs +76 -21
  48. package/crates/tish_ffi/src/lib.rs +11 -1
  49. package/crates/tish_ffi/tests/double_free.rs +35 -0
  50. package/crates/tish_fmt/src/lib.rs +75 -1
  51. package/crates/tish_lexer/src/lib.rs +76 -0
  52. package/crates/tish_lexer/src/token.rs +4 -0
  53. package/crates/tish_lint/src/lib.rs +126 -0
  54. package/crates/tish_lsp/README.md +2 -1
  55. package/crates/tish_lsp/src/main.rs +378 -28
  56. package/crates/tish_native/src/build.rs +41 -0
  57. package/crates/tish_parser/Cargo.toml +4 -0
  58. package/crates/tish_parser/src/lib.rs +27 -0
  59. package/crates/tish_parser/src/parser.rs +302 -20
  60. package/crates/tish_resolve/src/lib.rs +9 -0
  61. package/crates/tish_runtime/Cargo.toml +5 -0
  62. package/crates/tish_runtime/src/http.rs +28 -10
  63. package/crates/tish_runtime/src/http_fetch.rs +150 -1
  64. package/crates/tish_runtime/src/http_prefork.rs +72 -11
  65. package/crates/tish_runtime/src/lib.rs +523 -42
  66. package/crates/tish_runtime/src/timers.rs +49 -2
  67. package/crates/tish_ui/src/jsx.rs +253 -0
  68. package/crates/tish_vm/src/jit.rs +2514 -117
  69. package/crates/tish_vm/src/vm.rs +1227 -182
  70. package/package.json +1 -1
  71. package/platform/darwin-arm64/tish +0 -0
  72. package/platform/darwin-x64/tish +0 -0
  73. package/platform/linux-arm64/tish +0 -0
  74. package/platform/linux-x64/tish +0 -0
  75. package/platform/win32-x64/tish.exe +0 -0
@@ -11,6 +11,10 @@ use tishlang_ast::{
11
11
 
12
12
  use crate::error::CompileError;
13
13
 
14
+ /// Default module specifier the JSX runtime (`h` / `Fragment`) is auto-imported from (issue #291).
15
+ /// Matches the repo convention (`import { h } from "lattish"`); overridable via `--jsx-import-source`.
16
+ pub const DEFAULT_JSX_IMPORT_SOURCE: &str = "lattish";
17
+
14
18
  /// JS output mode. `Bundle` flattens every module into one file (imports/exports are resolved away
15
19
  /// by `merge_modules`). `Esm` emits one file per module with real ES `import`/`export` statements so
16
20
  /// a bundler (Vite/Rollup) can tree-shake and code-split. See issue #177's sibling, #282.
@@ -42,6 +46,9 @@ struct Codegen {
42
46
  module_path: PathBuf,
43
47
  /// ESM only: the project root, so import targets can be located relative to the output tree.
44
48
  project_root: PathBuf,
49
+ /// ESM only: the module specifier the JSX runtime (`h` / `Fragment`) is auto-imported from when a
50
+ /// module uses JSX but doesn't import them itself (issue #291). Defaults to `lattish`.
51
+ jsx_import_source: String,
45
52
  }
46
53
 
47
54
  fn stmt_terminates_switch(stmt: Option<&Statement>) -> bool {
@@ -63,10 +70,16 @@ impl Codegen {
63
70
  import_rewrite: ImportRewrite::Disk,
64
71
  module_path: PathBuf::new(),
65
72
  project_root: PathBuf::new(),
73
+ jsx_import_source: DEFAULT_JSX_IMPORT_SOURCE.to_string(),
66
74
  }
67
75
  }
68
76
 
69
- fn new_esm(module_path: PathBuf, project_root: PathBuf, import_rewrite: ImportRewrite) -> Self {
77
+ fn new_esm(
78
+ module_path: PathBuf,
79
+ project_root: PathBuf,
80
+ import_rewrite: ImportRewrite,
81
+ jsx_import_source: String,
82
+ ) -> Self {
70
83
  Self {
71
84
  output: String::new(),
72
85
  indent: 0,
@@ -75,6 +88,7 @@ impl Codegen {
75
88
  import_rewrite,
76
89
  module_path,
77
90
  project_root,
91
+ jsx_import_source,
78
92
  }
79
93
  }
80
94
 
@@ -139,6 +153,7 @@ impl Codegen {
139
153
  map_builder: Option<&mut SourceMapBuilder>,
140
154
  ) -> Result<(), CompileError> {
141
155
  self.write("// Generated by tishlang_compile_js\n");
156
+ self.emit_jsx_runtime_auto_import(program)?;
142
157
  match (map_sources, map_builder) {
143
158
  (Some((srcs, root)), Some(sm)) => {
144
159
  for (i, stmt) in program.statements.iter().enumerate() {
@@ -176,6 +191,34 @@ impl Codegen {
176
191
  Ok(())
177
192
  }
178
193
 
194
+ /// Auto-import the JSX runtime (`h` / `Fragment`) for ESM modules that use JSX but don't bring
195
+ /// those bindings into scope themselves (issue #291). In `--format bundle` the merged scope makes
196
+ /// a single `lattish` import visible everywhere, but each ES module is its own scope, so a JSX
197
+ /// module that didn't `import { h }` throws `ReferenceError` at load. We inject only the missing
198
+ /// names (so an explicit `import { h }` isn't duplicated) from `jsx_import_source`, rewritten the
199
+ /// same way as user imports (disk `.js` path vs. Vite-dev bare specifier). No-op outside ESM.
200
+ fn emit_jsx_runtime_auto_import(&mut self, program: &Program) -> Result<(), CompileError> {
201
+ if self.emit_mode != EmitMode::Esm {
202
+ return Ok(());
203
+ }
204
+ let needed = tishlang_ui::jsx::jsx_runtime_imports_needed(program);
205
+ if needed.is_empty() {
206
+ return Ok(());
207
+ }
208
+ let spec = rewrite_import_to_js(
209
+ &self.jsx_import_source,
210
+ &self.module_path,
211
+ &self.project_root,
212
+ self.import_rewrite,
213
+ )?;
214
+ self.writeln(&format!(
215
+ "import {{ {} }} from \"{}\";",
216
+ needed.join(", "),
217
+ spec
218
+ ));
219
+ Ok(())
220
+ }
221
+
179
222
  fn emit_statement(&mut self, stmt: &Statement) -> Result<(), CompileError> {
180
223
  match stmt {
181
224
  Statement::Block { statements, .. } => {
@@ -993,6 +1036,39 @@ impl Codegen {
993
1036
  let v = self.emit_expr(e)?;
994
1037
  self.writeln(&format!("export default {};", v));
995
1038
  }
1039
+ // #305: re-export in ESM mode — emit the native `export { … } from` / `export * from`,
1040
+ // rewriting the `.tish` specifier to the emitted `.js` module path.
1041
+ ExportDeclaration::ReExport {
1042
+ specifiers,
1043
+ all,
1044
+ from,
1045
+ ..
1046
+ } => {
1047
+ let spec = rewrite_import_to_js(
1048
+ from.as_ref(),
1049
+ &self.module_path,
1050
+ &self.project_root,
1051
+ self.import_rewrite,
1052
+ )?;
1053
+ if *all {
1054
+ self.writeln(&format!("export * from {:?};", spec));
1055
+ } else {
1056
+ let mut parts: Vec<String> = Vec::new();
1057
+ for s in specifiers {
1058
+ if let ImportSpecifier::Named { name, alias, .. } = s {
1059
+ match alias {
1060
+ Some(a) => parts.push(format!(
1061
+ "{} as {}",
1062
+ Self::escape_ident(name.as_ref()),
1063
+ Self::escape_ident(a.as_ref())
1064
+ )),
1065
+ None => parts.push(Self::escape_ident(name.as_ref()).to_string()),
1066
+ }
1067
+ }
1068
+ }
1069
+ self.writeln(&format!("export {{ {} }} from {:?};", parts.join(", "), spec));
1070
+ }
1071
+ }
996
1072
  }
997
1073
  Ok(())
998
1074
  }
@@ -1235,6 +1311,7 @@ pub fn compile_project_esm(
1235
1311
  entry_path: &Path,
1236
1312
  project_root: Option<&Path>,
1237
1313
  optimize: bool,
1314
+ jsx_import_source: &str,
1238
1315
  ) -> Result<Vec<EmittedJsModule>, CompileError> {
1239
1316
  let modules = tishlang_compile::resolve_project(entry_path, project_root)
1240
1317
  .map_err(|e| CompileError { message: e })?;
@@ -1274,8 +1351,12 @@ pub fn compile_project_esm(
1274
1351
  } else {
1275
1352
  module.program.clone()
1276
1353
  };
1277
- let mut gen =
1278
- Codegen::new_esm(mod_canon.clone(), res_root_canon.clone(), ImportRewrite::Disk);
1354
+ let mut gen = Codegen::new_esm(
1355
+ mod_canon.clone(),
1356
+ res_root_canon.clone(),
1357
+ ImportRewrite::Disk,
1358
+ jsx_import_source.to_string(),
1359
+ );
1279
1360
  gen.emit_program(&program, None, None)?;
1280
1361
  out.push(EmittedJsModule {
1281
1362
  relative_path: rel_js,
@@ -1297,6 +1378,7 @@ pub fn compile_module_esm(
1297
1378
  optimize: bool,
1298
1379
  import_rewrite: ImportRewrite,
1299
1380
  source_map: bool,
1381
+ jsx_import_source: &str,
1300
1382
  ) -> Result<JsBundle, CompileError> {
1301
1383
  if source_map && optimize {
1302
1384
  return Err(CompileError {
@@ -1324,7 +1406,12 @@ pub fn compile_module_esm(
1324
1406
  .unwrap_or_else(|| PathBuf::from("."));
1325
1407
  let root_canon = root.canonicalize().unwrap_or(root);
1326
1408
 
1327
- let mut gen = Codegen::new_esm(module_canon.clone(), root_canon.clone(), import_rewrite);
1409
+ let mut gen = Codegen::new_esm(
1410
+ module_canon.clone(),
1411
+ root_canon.clone(),
1412
+ import_rewrite,
1413
+ jsx_import_source.to_string(),
1414
+ );
1328
1415
  if source_map {
1329
1416
  let stmt_sources = vec![module_canon.clone(); program.statements.len()];
1330
1417
  let mut builder = SourceMapBuilder::new(Some("module.js"));
@@ -10,11 +10,14 @@ mod tests_jsx;
10
10
  pub use codegen::{
11
11
  compile_module_esm, compile_project_esm, compile_project_with_jsx,
12
12
  compile_project_with_jsx_and_source_map, compile_with_jsx, EmittedJsModule, ImportRewrite,
13
- JsBundle,
13
+ JsBundle, DEFAULT_JSX_IMPORT_SOURCE,
14
14
  };
15
15
  pub use error::CompileError;
16
16
 
17
- /// JSX lowers to `h` / `Fragment`; merge the `lattish` runtime for hooks and DOM.
17
+ /// Compile a single program to a bundle-style JS string. JSX lowers to `h` / `Fragment`; in bundle
18
+ /// mode those resolve against the merged scope (merge the `lattish` runtime for hooks and DOM). The
19
+ /// per-module ESM paths ([`compile_module_esm`] / [`compile_project_esm`]) auto-import the runtime
20
+ /// for JSX modules that don't import it themselves (issue #291).
18
21
  pub fn compile(program: &tishlang_ast::Program, optimize: bool) -> Result<String, CompileError> {
19
22
  compile_with_jsx(program, optimize)
20
23
  }
@@ -475,7 +475,8 @@ fn factory() {
475
475
  f.write_all(src.as_bytes()).unwrap();
476
476
  f.sync_all().unwrap();
477
477
  }
478
- compile_project_esm(&dir.join(entry), Some(dir), false).expect("compile_project_esm failed")
478
+ compile_project_esm(&dir.join(entry), Some(dir), false, "lattish")
479
+ .expect("compile_project_esm failed")
479
480
  }
480
481
 
481
482
  fn module_js<'a>(mods: &'a [EmittedJsModule], rel: &str) -> &'a str {
@@ -557,6 +558,7 @@ fn factory() {
557
558
  &base.join("app/src/main.tish"),
558
559
  Some(&base.join("app")),
559
560
  false,
561
+ "lattish",
560
562
  )
561
563
  .expect("compile_project_esm failed for sibling dep");
562
564
  let rels: Vec<String> = mods
@@ -607,7 +609,7 @@ fn factory() {
607
609
  "import { ping } from \"pkg\"\nconsole.log(ping())\n",
608
610
  )
609
611
  .unwrap();
610
- let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false)
612
+ let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false, "lattish")
611
613
  .expect("compile_project_esm failed for node_modules dep");
612
614
  let rels: Vec<String> = mods
613
615
  .iter()
@@ -635,7 +637,7 @@ fn factory() {
635
637
  let dir = tmp.path();
636
638
  let p = dir.join("main.tish");
637
639
  std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
638
- let err = compile_project_esm(&p, Some(dir), false).unwrap_err();
640
+ let err = compile_project_esm(&p, Some(dir), false, "lattish").unwrap_err();
639
641
  assert!(
640
642
  err.message.contains("Native module import") && err.message.contains("esm"),
641
643
  "expected a native-import rejection for ESM, got: {}",
@@ -665,6 +667,7 @@ fn factory() {
665
667
  false,
666
668
  ImportRewrite::ViteDev,
667
669
  false,
670
+ "lattish",
668
671
  )
669
672
  .expect("compile_module_esm failed")
670
673
  .js
@@ -741,7 +744,7 @@ fn factory() {
741
744
  let dir = tmp.path();
742
745
  let p = dir.join("main.tish");
743
746
  std::fs::write(&p, "import { readFile } from \"fs\"\nconsole.log(1)\n").unwrap();
744
- let err = compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, false)
747
+ let err = compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, false, "lattish")
745
748
  .unwrap_err();
746
749
  assert!(
747
750
  err.message.contains("Native module import"),
@@ -758,7 +761,8 @@ fn factory() {
758
761
  std::fs::write(&p, "export fn greet(n) { return \"hi \" + n }\nconsole.log(greet(\"x\"))\n")
759
762
  .unwrap();
760
763
  let bundle =
761
- compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true).unwrap();
764
+ compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
765
+ .unwrap();
762
766
  let map = bundle
763
767
  .source_map_json
764
768
  .expect("source map requested → must be present");
@@ -782,7 +786,8 @@ fn factory() {
782
786
  let src = "export fn greet(n) { return \"hi \" + n }\n";
783
787
  std::fs::write(&p, src).unwrap();
784
788
  let bundle =
785
- compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true).unwrap();
789
+ compile_module_esm(&p, Some(dir), false, ImportRewrite::ViteDev, true, "lattish")
790
+ .unwrap();
786
791
  let map = bundle.source_map_json.expect("source map requested");
787
792
  assert!(
788
793
  map.contains("\"sourcesContent\""),
@@ -800,7 +805,7 @@ fn factory() {
800
805
  let dir = tmp.path();
801
806
  let p = dir.join("main.tish");
802
807
  std::fs::write(&p, "console.log(1)\n").unwrap();
803
- let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true)
808
+ let err = compile_module_esm(&p, Some(dir), true, ImportRewrite::ViteDev, true, "lattish")
804
809
  .unwrap_err();
805
810
  assert!(
806
811
  err.message.to_lowercase().contains("optimiz"),
@@ -808,4 +813,167 @@ fn factory() {
808
813
  err.message
809
814
  );
810
815
  }
816
+
817
+ // ── #291: JSX runtime auto-import (h / Fragment) for per-module ESM ────────────────────────
818
+
819
+ #[test]
820
+ fn compile_module_esm_jsx_auto_imports_h() {
821
+ // A module that uses a JSX element but never imports `h` must get the runtime auto-imported,
822
+ // or it throws `ReferenceError: h` at load in per-module ESM (issue #291).
823
+ let js = build_module_vite("main.tish", &[("main.tish", "fn X() { return <div/> }\n")]);
824
+ assert!(
825
+ js.contains("import { h } from \"lattish\";"),
826
+ "JSX element module must auto-import h:\n{js}"
827
+ );
828
+ assert!(
829
+ !js.contains("Fragment"),
830
+ "no fragment used → Fragment must not be imported:\n{js}"
831
+ );
832
+ assert!(js.contains("h(\"div\""), "JSX still lowers to h(...):\n{js}");
833
+ }
834
+
835
+ #[test]
836
+ fn compile_module_esm_jsx_auto_imports_fragment() {
837
+ // A fragment lowers to `h(Fragment, …)`, so both `h` and `Fragment` must be auto-imported.
838
+ let js = build_module_vite(
839
+ "main.tish",
840
+ &[("main.tish", "fn X() { return <>{\"a\"}</> }\n")],
841
+ );
842
+ assert!(
843
+ js.contains("import { h, Fragment } from \"lattish\";"),
844
+ "fragment module must auto-import both h and Fragment:\n{js}"
845
+ );
846
+ }
847
+
848
+ #[test]
849
+ fn compile_module_esm_jsx_skips_import_when_present() {
850
+ // An explicit `import { h, Fragment } from "lattish"` must not be duplicated.
851
+ let js = build_module_vite(
852
+ "main.tish",
853
+ &[(
854
+ "main.tish",
855
+ "import { h, Fragment } from \"lattish\"\nfn X() { return <></> }\n",
856
+ )],
857
+ );
858
+ assert_eq!(
859
+ js.matches("from \"lattish\"").count(),
860
+ 1,
861
+ "exactly one lattish import (the author's), no auto-import duplicate:\n{js}"
862
+ );
863
+ assert!(
864
+ js.contains("import { h, Fragment } from \"lattish\";"),
865
+ "the author's import is preserved:\n{js}"
866
+ );
867
+ }
868
+
869
+ #[test]
870
+ fn compile_module_esm_jsx_partial_import_extends() {
871
+ // The author imported only `h`; a fragment also needs `Fragment`, so just that one missing
872
+ // binding is auto-imported (no re-import of `h`).
873
+ let js = build_module_vite(
874
+ "main.tish",
875
+ &[(
876
+ "main.tish",
877
+ "import { h } from \"lattish\"\nfn X() { return <></> }\n",
878
+ )],
879
+ );
880
+ assert!(
881
+ js.contains("import { Fragment } from \"lattish\";"),
882
+ "only the missing Fragment binding is auto-imported:\n{js}"
883
+ );
884
+ assert!(
885
+ js.contains("import { h } from \"lattish\";"),
886
+ "the author's h import is preserved:\n{js}"
887
+ );
888
+ assert!(
889
+ !js.contains("import { h, Fragment }"),
890
+ "h must not be re-imported alongside Fragment:\n{js}"
891
+ );
892
+ }
893
+
894
+ #[test]
895
+ fn compile_module_esm_vite_dev_jsx_keeps_bare_runtime_import() {
896
+ // In Vite-dev mode the auto-import specifier stays the bare package so Node/Vite resolves it
897
+ // (it is NOT rewritten to a relative `.js` path the way disk ESM does).
898
+ let js = build_module_vite("main.tish", &[("main.tish", "fn X() { return <div/> }\n")]);
899
+ assert!(
900
+ js.contains("from \"lattish\";"),
901
+ "Vite dev keeps the bare runtime specifier:\n{js}"
902
+ );
903
+ assert!(
904
+ !js.contains("lattish.js") && !js.contains("./lattish"),
905
+ "Vite dev must not rewrite the runtime specifier to a path:\n{js}"
906
+ );
907
+ }
908
+
909
+ #[test]
910
+ fn compile_module_esm_jsx_import_source_is_configurable() {
911
+ // The runtime package is overridable (e.g. the scoped `@tishlang/lattish`, or a custom runtime).
912
+ let tmp = tempfile::tempdir().expect("tempdir");
913
+ let dir = tmp.path();
914
+ let p = dir.join("main.tish");
915
+ std::fs::write(&p, "fn X() { return <div/> }\n").unwrap();
916
+ let js = compile_module_esm(
917
+ &p,
918
+ Some(dir),
919
+ false,
920
+ ImportRewrite::ViteDev,
921
+ false,
922
+ "@tishlang/lattish",
923
+ )
924
+ .expect("compile_module_esm failed")
925
+ .js;
926
+ assert!(
927
+ js.contains("import { h } from \"@tishlang/lattish\";"),
928
+ "custom jsx import source used:\n{js}"
929
+ );
930
+ }
931
+
932
+ #[test]
933
+ fn compile_project_esm_jsx_auto_import_per_module() {
934
+ // Only the module that actually uses JSX gets the runtime import; plain modules are untouched.
935
+ let tmp = tempfile::tempdir().expect("tempdir");
936
+ let base = tmp.path();
937
+ std::fs::create_dir_all(base.join("src")).unwrap();
938
+ std::fs::create_dir_all(base.join("node_modules/lattish")).unwrap();
939
+ std::fs::write(
940
+ base.join("node_modules/lattish/package.json"),
941
+ "{\"name\":\"lattish\",\"main\":\"index.tish\"}\n",
942
+ )
943
+ .unwrap();
944
+ std::fs::write(
945
+ base.join("node_modules/lattish/index.tish"),
946
+ "export fn h(tag, props, children) { return tag }\nexport let Fragment = \"frag\"\n",
947
+ )
948
+ .unwrap();
949
+ std::fs::write(
950
+ base.join("src/view.tish"),
951
+ "export fn V() { return <div/> }\n",
952
+ )
953
+ .unwrap();
954
+ std::fs::write(
955
+ base.join("src/plain.tish"),
956
+ "export fn add(a, b) { return a + b }\n",
957
+ )
958
+ .unwrap();
959
+ std::fs::write(
960
+ base.join("src/main.tish"),
961
+ "import { V } from \"./view.tish\"\nimport { add } from \"./plain.tish\"\nconsole.log(add(1, 2))\n",
962
+ )
963
+ .unwrap();
964
+ let mods = compile_project_esm(&base.join("src/main.tish"), Some(base), false, "lattish")
965
+ .expect("compile_project_esm failed");
966
+ // The runtime isn't imported by any module, so it's not in the graph; the common-ancestor
967
+ // output base is `src/`, making the emitted paths flat (`view.js`, `plain.js`, `main.js`).
968
+ let view = module_js(&mods, "view.js");
969
+ let plain = module_js(&mods, "plain.js");
970
+ assert!(
971
+ view.contains("import { h } from") && view.contains("lattish"),
972
+ "JSX module auto-imports the runtime:\n{view}"
973
+ );
974
+ assert!(
975
+ !plain.contains("import { h }") && !plain.contains("Fragment"),
976
+ "non-JSX module must not get a runtime import:\n{plain}"
977
+ );
978
+ }
811
979
  }
@@ -98,6 +98,21 @@ fn resolve_import_to_key(
98
98
  ))
99
99
  }
100
100
 
101
+ /// The module specifier a statement imports/re-exports from, if any — covers both
102
+ /// `import … from "x"` and `export … from "x"` (#305 re-export). Dependency discovery and cycle
103
+ /// detection use this so they follow re-export edges, not just plain imports (otherwise a module
104
+ /// reached only via a re-export would never be loaded).
105
+ fn stmt_module_source(stmt: &Statement) -> Option<&str> {
106
+ match stmt {
107
+ Statement::Import { from, .. } => Some(&**from),
108
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
109
+ ExportDeclaration::ReExport { from, .. } => Some(&**from),
110
+ _ => None,
111
+ },
112
+ _ => None,
113
+ }
114
+ }
115
+
101
116
  /// Resolve all modules starting from the entry file. Returns modules in dependency order.
102
117
  pub fn resolve_virtual(
103
118
  entry_path: &str,
@@ -163,7 +178,7 @@ fn load_module_recursive(
163
178
 
164
179
  let from_dir = parent_dir(module_path);
165
180
  for stmt in &program.statements {
166
- if let Statement::Import { from, .. } = stmt {
181
+ if let Some(from) = stmt_module_source(stmt) {
167
182
  if is_native_import(from) {
168
183
  continue;
169
184
  }
@@ -217,7 +232,7 @@ fn has_cycle_from(
217
232
  visiting: &mut HashSet<usize>,
218
233
  ) -> Result<bool, String> {
219
234
  for stmt in &program.statements {
220
- if let Statement::Import { from, .. } = stmt {
235
+ if let Some(from) = stmt_module_source(stmt) {
221
236
  if is_native_import(from) {
222
237
  continue;
223
238
  }
@@ -292,6 +307,40 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
292
307
  let default_name = format!("__default_{}", idx);
293
308
  module_exports[idx].insert("default".to_string(), default_name);
294
309
  }
310
+ // #305: re-export — map each re-exported name to the DEP's binding (the dep is
311
+ // earlier in load order, so its export table is already built). `export *` copies
312
+ // every dep export without overriding an explicit one; `export { a as b }` maps
313
+ // b -> dep's binding for a. Mirrors `merge_modules` in tish_compile/src/resolve.rs.
314
+ ExportDeclaration::ReExport {
315
+ specifiers,
316
+ all,
317
+ from,
318
+ ..
319
+ } => {
320
+ let dir = parent_dir(&module.path);
321
+ let dep = resolve_import_to_key_for_cycle(from, dir, &path_to_idx)
322
+ .ok()
323
+ .and_then(|key| path_to_idx.get(&key).copied())
324
+ .map(|dep_idx| module_exports[dep_idx].clone());
325
+ if let Some(dep) = dep {
326
+ if *all {
327
+ for (k, v) in &dep {
328
+ module_exports[idx]
329
+ .entry(k.clone())
330
+ .or_insert_with(|| v.clone());
331
+ }
332
+ }
333
+ for spec in specifiers {
334
+ if let ImportSpecifier::Named { name, alias, .. } = spec {
335
+ if let Some(binding) = dep.get(name.as_ref()) {
336
+ let export_name =
337
+ alias.as_deref().unwrap_or(name.as_ref()).to_string();
338
+ module_exports[idx].insert(export_name, binding.clone());
339
+ }
340
+ }
341
+ }
342
+ }
343
+ }
295
344
  }
296
345
  }
297
346
  }
@@ -440,6 +489,10 @@ pub fn merge_modules_virtual(modules: Vec<VirtualModule>) -> Result<Program, Str
440
489
  span: espan,
441
490
  });
442
491
  }
492
+ // #305: re-export emits no code — `module_exports` already maps the re-exported
493
+ // names to the dep's bindings (in scope in the flattened bundle), so downstream
494
+ // imports resolve directly. Nothing to push here.
495
+ ExportDeclaration::ReExport { .. } => {}
443
496
  },
444
497
  _ => statements.push(stmt.clone()),
445
498
  }
@@ -471,4 +524,49 @@ mod tests {
471
524
  let program = merge_modules_virtual(modules).unwrap();
472
525
  assert!(!program.statements.is_empty());
473
526
  }
527
+
528
+ #[test]
529
+ fn test_resolve_virtual_reexport_alias() {
530
+ // #305: `export { add as plus } from "./dep"` re-exports dep's `add` as `plus`; a downstream
531
+ // `import { plus }` must resolve to dep's binding. Regression guard: the merge previously did
532
+ // not handle `ExportDeclaration::ReExport` (E0004 build failure), and discovery did not follow
533
+ // the re-export edge (so a dep reached only via re-export was never loaded).
534
+ let mut files = HashMap::new();
535
+ files.insert(
536
+ "dep.tish".to_string(),
537
+ "export fn add(a, b) { return a + b }".to_string(),
538
+ );
539
+ files.insert(
540
+ "mid.tish".to_string(),
541
+ "export { add as plus } from \"./dep.tish\"".to_string(),
542
+ );
543
+ files.insert(
544
+ "main.tish".to_string(),
545
+ "import { plus } from \"./mid.tish\"\nconsole.log(plus(1, 2))".to_string(),
546
+ );
547
+ let modules = resolve_virtual("main.tish", &files).unwrap();
548
+ // dep.tish is reached ONLY via mid's re-export → discovery must have loaded it.
549
+ assert!(
550
+ modules.iter().any(|m| m.path == "dep.tish"),
551
+ "re-exported dep should be discovered and loaded"
552
+ );
553
+ detect_cycles_virtual(&modules).unwrap();
554
+ let program = merge_modules_virtual(modules).unwrap();
555
+ let has_add_fn = program
556
+ .statements
557
+ .iter()
558
+ .any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "add"));
559
+ let binds_plus_to_add = program.statements.iter().any(|s| {
560
+ matches!(
561
+ s,
562
+ Statement::VarDecl { name, init: Some(Expr::Ident { name: src, .. }), .. }
563
+ if name.as_ref() == "plus" && src.as_ref() == "add"
564
+ )
565
+ });
566
+ assert!(has_add_fn, "dep's `add` fn should be in the merged bundle");
567
+ assert!(
568
+ binds_plus_to_add,
569
+ "import of re-exported `plus` should bind to dep's `add`"
570
+ );
571
+ }
474
572
  }
@@ -25,6 +25,10 @@ smallvec = "1"
25
25
  indexmap = "2"
26
26
  # Fast integer→decimal for the JSON.stringify number fast-path (already in the workspace lock).
27
27
  itoa = "1"
28
+ # Fast shortest-float→decimal for the number→string general path (JSON/template/log). ryu writes the
29
+ # shortest round-trip digits straight to a buffer with none of `core::fmt`'s `{:e}` Formatter overhead
30
+ # (profiled as ~40% of JSON.stringify self-time on numeric payloads). Already in the workspace lock.
31
+ ryu = "1"
28
32
  fancy-regex = { version = "0.17.0", optional = true }
29
33
  # Only under `send-values` (the Arc<Mutex> path): parking_lot's uncontended lock is a
30
34
  # single atomic with no pthread syscall — profiled as a top cost on object/array access.