@tishlang/tish 1.0.7 → 1.0.11

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 (127) hide show
  1. package/Cargo.toml +43 -0
  2. package/LICENSE +13 -0
  3. package/README.md +66 -0
  4. package/crates/js_to_tish/Cargo.toml +9 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +61 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +608 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +474 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +44 -0
  13. package/crates/tish/src/main.rs +585 -0
  14. package/crates/tish/src/repl_completion.rs +200 -0
  15. package/crates/tish/tests/integration_test.rs +726 -0
  16. package/crates/tish_ast/Cargo.toml +7 -0
  17. package/crates/tish_ast/src/ast.rs +494 -0
  18. package/crates/tish_ast/src/lib.rs +5 -0
  19. package/crates/tish_build_utils/Cargo.toml +5 -0
  20. package/crates/tish_build_utils/src/lib.rs +175 -0
  21. package/crates/tish_builtins/Cargo.toml +12 -0
  22. package/crates/tish_builtins/src/array.rs +410 -0
  23. package/crates/tish_builtins/src/globals.rs +197 -0
  24. package/crates/tish_builtins/src/helpers.rs +38 -0
  25. package/crates/tish_builtins/src/lib.rs +14 -0
  26. package/crates/tish_builtins/src/math.rs +80 -0
  27. package/crates/tish_builtins/src/object.rs +36 -0
  28. package/crates/tish_builtins/src/string.rs +253 -0
  29. package/crates/tish_bytecode/Cargo.toml +15 -0
  30. package/crates/tish_bytecode/src/chunk.rs +97 -0
  31. package/crates/tish_bytecode/src/compiler.rs +1361 -0
  32. package/crates/tish_bytecode/src/encoding.rs +100 -0
  33. package/crates/tish_bytecode/src/lib.rs +19 -0
  34. package/crates/tish_bytecode/src/opcode.rs +110 -0
  35. package/crates/tish_bytecode/src/peephole.rs +159 -0
  36. package/crates/tish_bytecode/src/serialize.rs +163 -0
  37. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  38. package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
  39. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  40. package/crates/tish_compile/Cargo.toml +21 -0
  41. package/crates/tish_compile/src/codegen.rs +3316 -0
  42. package/crates/tish_compile/src/lib.rs +71 -0
  43. package/crates/tish_compile/src/resolve.rs +631 -0
  44. package/crates/tish_compile/src/types.rs +304 -0
  45. package/crates/tish_compile_js/Cargo.toml +16 -0
  46. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  47. package/crates/tish_compile_js/src/codegen.rs +794 -0
  48. package/crates/tish_compile_js/src/error.rs +20 -0
  49. package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
  50. package/crates/tish_compile_js/src/lib.rs +27 -0
  51. package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +19 -0
  53. package/crates/tish_compiler_wasm/src/lib.rs +55 -0
  54. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
  55. package/crates/tish_core/Cargo.toml +11 -0
  56. package/crates/tish_core/src/console_style.rs +128 -0
  57. package/crates/tish_core/src/json.rs +327 -0
  58. package/crates/tish_core/src/lib.rs +15 -0
  59. package/crates/tish_core/src/macros.rs +37 -0
  60. package/crates/tish_core/src/uri.rs +115 -0
  61. package/crates/tish_core/src/value.rs +376 -0
  62. package/crates/tish_cranelift/Cargo.toml +17 -0
  63. package/crates/tish_cranelift/src/lib.rs +41 -0
  64. package/crates/tish_cranelift/src/link.rs +120 -0
  65. package/crates/tish_cranelift/src/lower.rs +77 -0
  66. package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
  67. package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
  68. package/crates/tish_eval/Cargo.toml +26 -0
  69. package/crates/tish_eval/src/eval.rs +3205 -0
  70. package/crates/tish_eval/src/http.rs +122 -0
  71. package/crates/tish_eval/src/lib.rs +59 -0
  72. package/crates/tish_eval/src/natives.rs +301 -0
  73. package/crates/tish_eval/src/promise.rs +173 -0
  74. package/crates/tish_eval/src/regex.rs +298 -0
  75. package/crates/tish_eval/src/timers.rs +111 -0
  76. package/crates/tish_eval/src/value.rs +224 -0
  77. package/crates/tish_eval/src/value_convert.rs +85 -0
  78. package/crates/tish_fmt/Cargo.toml +16 -0
  79. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  80. package/crates/tish_fmt/src/lib.rs +884 -0
  81. package/crates/tish_jsx_web/Cargo.toml +7 -0
  82. package/crates/tish_jsx_web/README.md +18 -0
  83. package/crates/tish_jsx_web/src/lib.rs +157 -0
  84. package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
  85. package/crates/tish_lexer/Cargo.toml +7 -0
  86. package/crates/tish_lexer/src/lib.rs +430 -0
  87. package/crates/tish_lexer/src/token.rs +155 -0
  88. package/crates/tish_lint/Cargo.toml +17 -0
  89. package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
  90. package/crates/tish_lint/src/lib.rs +278 -0
  91. package/crates/tish_llvm/Cargo.toml +11 -0
  92. package/crates/tish_llvm/src/lib.rs +106 -0
  93. package/crates/tish_lsp/Cargo.toml +22 -0
  94. package/crates/tish_lsp/README.md +26 -0
  95. package/crates/tish_lsp/src/main.rs +615 -0
  96. package/crates/tish_native/Cargo.toml +14 -0
  97. package/crates/tish_native/src/build.rs +102 -0
  98. package/crates/tish_native/src/lib.rs +237 -0
  99. package/crates/tish_opt/Cargo.toml +11 -0
  100. package/crates/tish_opt/src/lib.rs +896 -0
  101. package/crates/tish_parser/Cargo.toml +9 -0
  102. package/crates/tish_parser/src/lib.rs +123 -0
  103. package/crates/tish_parser/src/parser.rs +1714 -0
  104. package/crates/tish_runtime/Cargo.toml +26 -0
  105. package/crates/tish_runtime/src/http.rs +308 -0
  106. package/crates/tish_runtime/src/http_fetch.rs +453 -0
  107. package/crates/tish_runtime/src/lib.rs +1004 -0
  108. package/crates/tish_runtime/src/native_promise.rs +26 -0
  109. package/crates/tish_runtime/src/promise.rs +77 -0
  110. package/crates/tish_runtime/src/promise_io.rs +41 -0
  111. package/crates/tish_runtime/src/timers.rs +125 -0
  112. package/crates/tish_runtime/src/ws.rs +725 -0
  113. package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
  114. package/crates/tish_vm/Cargo.toml +31 -0
  115. package/crates/tish_vm/src/lib.rs +39 -0
  116. package/crates/tish_vm/src/vm.rs +1399 -0
  117. package/crates/tish_wasm/Cargo.toml +13 -0
  118. package/crates/tish_wasm/src/lib.rs +358 -0
  119. package/crates/tish_wasm_runtime/Cargo.toml +25 -0
  120. package/crates/tish_wasm_runtime/src/lib.rs +36 -0
  121. package/justfile +260 -0
  122. package/package.json +8 -3
  123. package/platform/darwin-arm64/tish +0 -0
  124. package/platform/darwin-x64/tish +0 -0
  125. package/platform/linux-arm64/tish +0 -0
  126. package/platform/linux-x64/tish +0 -0
  127. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,71 @@
1
+ //! Native compiler backend for Tish.
2
+ //!
3
+ //! Emits Rust source that links to tish_runtime.
4
+
5
+ mod codegen;
6
+ mod resolve;
7
+ mod types;
8
+
9
+ pub use codegen::{
10
+ compile, compile_project, compile_project_full, compile_with_features,
11
+ compile_with_native_modules, compile_with_project_root,
12
+ };
13
+ pub use codegen::CompileError;
14
+ pub use resolve::{
15
+ detect_cycles, extract_native_import_features, has_external_native_imports, has_native_imports,
16
+ is_builtin_native_spec, merge_modules, resolve_native_modules, resolve_project,
17
+ ResolvedNativeModule,
18
+ };
19
+ pub use types::{RustType, TypeContext};
20
+
21
+ #[cfg(test)]
22
+ mod tests {
23
+ use super::*;
24
+ use tish_parser::parse;
25
+
26
+ #[test]
27
+ fn typed_assign_conversion() {
28
+ let src = r#"
29
+ fn sum(...args: number[]): number {
30
+ let total: number = 0
31
+ for (let n of args) { total = total + n }
32
+ return total
33
+ }
34
+ "#;
35
+ let program = parse(src).unwrap();
36
+ let rust = compile(&program).unwrap();
37
+ assert!(rust.contains("match &_v { Value::Number(n) => *n"), "expected typed assign conversion");
38
+ }
39
+
40
+ #[test]
41
+ fn loop_var_decl_clone_outer_var() {
42
+ let src = r#"
43
+ let outerVar = 42
44
+ for (let i = 0; i < 5; i = i + 1) {
45
+ let x = outerVar
46
+ }
47
+ "#;
48
+ let program = parse(src).unwrap();
49
+ let rust = compile(&program).unwrap();
50
+ assert!(
51
+ rust.contains("(outerVar).clone()"),
52
+ "expected outerVar to be cloned in loop body"
53
+ );
54
+ }
55
+
56
+ #[test]
57
+ fn loop_var_decl_clone_via_project_full() {
58
+ let manifest = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
59
+ let bench = manifest.join("../../tests/core/benchmark_granular.tish").canonicalize().unwrap();
60
+ // Use same default features as tish CLI (http, fs, process, regex)
61
+ let features = ["http", "fs", "process", "regex"]
62
+ .into_iter()
63
+ .map(String::from)
64
+ .collect::<Vec<_>>();
65
+ let (rust, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
66
+ assert!(
67
+ rust.contains("(outerVar).clone()"),
68
+ "expected outerVar to be cloned in benchmark_granular loop"
69
+ );
70
+ }
71
+ }
@@ -0,0 +1,631 @@
1
+ //! Module resolver: resolves relative imports, builds dependency graph, detects cycles.
2
+ //! Supports native imports: tish:egui, tish:polars, @scope/pkg (via package.json).
3
+
4
+ use std::collections::{HashMap, HashSet};
5
+ use std::path::{Path, PathBuf};
6
+ use std::sync::Arc;
7
+ use tish_ast::{ExportDeclaration, Expr, ImportSpecifier, Program, Statement};
8
+
9
+ /// Resolved native module: crate path and init expression.
10
+ #[derive(Debug, Clone)]
11
+ pub struct ResolvedNativeModule {
12
+ pub spec: String,
13
+ /// Cargo package name (e.g. tish-egui) for [dependencies]
14
+ pub package_name: String,
15
+ /// Rust crate name with underscores (e.g. tish_egui) for use in generated code
16
+ pub crate_name: String,
17
+ pub crate_path: PathBuf,
18
+ pub export_fn: String,
19
+ }
20
+
21
+ /// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
22
+ const BUILTIN_ALIASES: &[(&str, &str)] = &[
23
+ ("fs", "tish:fs"),
24
+ ("http", "tish:http"),
25
+ ("process", "tish:process"),
26
+ ("ws", "tish:ws"),
27
+ ];
28
+
29
+ /// Normalize built-in spec to canonical form. E.g. "fs" -> "tish:fs".
30
+ pub fn normalize_builtin_spec(spec: &str) -> Option<String> {
31
+ if spec.starts_with("tish:") {
32
+ return Some(spec.to_string());
33
+ }
34
+ BUILTIN_ALIASES
35
+ .iter()
36
+ .find(|(alias, _)| *alias == spec)
37
+ .map(|(_, canonical)| (*canonical).to_string())
38
+ }
39
+
40
+ /// Built-in modules that come from tish_runtime, not from package.json.
41
+ pub fn is_builtin_native_spec(spec: &str) -> bool {
42
+ matches!(spec, "tish:fs" | "tish:http" | "tish:process" | "tish:ws")
43
+ || matches!(spec, "fs" | "http" | "process" | "ws")
44
+ }
45
+
46
+ /// Resolve all native imports in a merged program via package.json lookup.
47
+ /// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tish_runtime directly.
48
+ pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
49
+ let root_canon = project_root
50
+ .canonicalize()
51
+ .map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
52
+ let mut seen = HashSet::new();
53
+ let mut modules = Vec::new();
54
+ for stmt in &program.statements {
55
+ if let Statement::VarDecl {
56
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
57
+ ..
58
+ } = stmt
59
+ {
60
+ let s = spec.as_ref();
61
+ if is_builtin_native_spec(s) {
62
+ continue; // Built-ins use tish_runtime, no package.json lookup
63
+ }
64
+ if !seen.insert(s.to_string()) {
65
+ continue;
66
+ }
67
+ let m = resolve_native_module(s, &root_canon)?;
68
+ modules.push(m);
69
+ }
70
+ }
71
+ Ok(modules)
72
+ }
73
+
74
+ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
75
+ let package_name = if spec.starts_with("tish:") {
76
+ format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
77
+ } else if spec.starts_with('@') {
78
+ spec.to_string()
79
+ } else {
80
+ return Err(format!("Unsupported native import spec: {}", spec));
81
+ };
82
+ let pkg_dir = find_package_dir(&package_name, project_root)?;
83
+ let pkg_json = pkg_dir.join("package.json");
84
+ let content = std::fs::read_to_string(&pkg_json)
85
+ .map_err(|e| format!("Cannot read {}: {}", pkg_json.display(), e))?;
86
+ let json: serde_json::Value =
87
+ serde_json::from_str(&content).map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
88
+ let tish = json
89
+ .get("tish")
90
+ .and_then(|v| v.as_object())
91
+ .ok_or_else(|| format!("Package {} has no \"tish\" config in package.json", package_name))?;
92
+ if !tish.get("module").and_then(|v| v.as_bool()).unwrap_or(false) {
93
+ return Err(format!("Package {} is not a Tish native module (tish.module must be true)", package_name));
94
+ }
95
+ let raw_crate = tish
96
+ .get("crate")
97
+ .and_then(|v| v.as_str())
98
+ .unwrap_or(&package_name)
99
+ .to_string();
100
+ let module_part = spec.strip_prefix("tish:").unwrap_or(spec);
101
+ let export_fn = tish
102
+ .get("export")
103
+ .and_then(|v| v.as_str())
104
+ .map(String::from)
105
+ .unwrap_or_else(|| format!("{}_object", str::replace(module_part, "-", "_")));
106
+ let crate_path = pkg_dir.canonicalize().unwrap_or(pkg_dir);
107
+ Ok(ResolvedNativeModule {
108
+ spec: spec.to_string(),
109
+ package_name: raw_crate.clone(),
110
+ crate_name: raw_crate.replace('-', "_"),
111
+ crate_path,
112
+ export_fn,
113
+ })
114
+ }
115
+
116
+ fn find_package_dir(package_name: &str, project_root: &Path) -> Result<PathBuf, String> {
117
+ let mut search = project_root.to_path_buf();
118
+ loop {
119
+ let node_mod = search.join("node_modules").join(package_name);
120
+ if node_mod.join("package.json").exists()
121
+ && read_package_name(&node_mod.join("package.json")) == Some(package_name.to_string())
122
+ {
123
+ return Ok(node_mod);
124
+ }
125
+ let sibling = search.join(package_name);
126
+ if sibling.join("package.json").exists()
127
+ && read_package_name(&sibling.join("package.json")) == Some(package_name.to_string())
128
+ {
129
+ return Ok(sibling);
130
+ }
131
+ if search.join("package.json").exists()
132
+ && read_package_name(&search.join("package.json")) == Some(package_name.to_string())
133
+ {
134
+ return Ok(search);
135
+ }
136
+ if let Some(parent) = search.parent() {
137
+ search = parent.to_path_buf();
138
+ } else {
139
+ break;
140
+ }
141
+ }
142
+ Err(format!(
143
+ "Native module {} not found. Add it as a dependency or place it in node_modules/ or as a sibling directory.",
144
+ package_name
145
+ ))
146
+ }
147
+
148
+ fn read_package_name(pkg_path: &Path) -> Option<String> {
149
+ let content = std::fs::read_to_string(pkg_path).ok()?;
150
+ let json: serde_json::Value = serde_json::from_str(&content).ok()?;
151
+ json.get("name").and_then(|v| v.as_str()).map(String::from)
152
+ }
153
+
154
+ /// Extract Cargo feature names from native imports in a merged program.
155
+ /// Used to enable tish_runtime features based on `import { x } from 'tish:egui'` etc.
156
+ pub fn extract_native_import_features(program: &Program) -> Vec<String> {
157
+ let mut features = std::collections::HashSet::new();
158
+ for stmt in &program.statements {
159
+ if let Statement::VarDecl {
160
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
161
+ ..
162
+ } = stmt
163
+ {
164
+ if let Some(f) = native_spec_to_feature(spec.as_ref()) {
165
+ features.insert(f);
166
+ }
167
+ }
168
+ }
169
+ features.into_iter().collect()
170
+ }
171
+
172
+ /// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
173
+ pub fn has_native_imports(program: &Program) -> bool {
174
+ for stmt in &program.statements {
175
+ if let Statement::VarDecl {
176
+ init: Some(Expr::NativeModuleLoad { .. }),
177
+ ..
178
+ } = stmt
179
+ {
180
+ return true;
181
+ }
182
+ }
183
+ false
184
+ }
185
+
186
+ /// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
187
+ /// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
188
+ pub fn has_external_native_imports(program: &Program) -> bool {
189
+ for stmt in &program.statements {
190
+ if let Statement::VarDecl {
191
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
192
+ ..
193
+ } = stmt
194
+ {
195
+ if !is_builtin_native_spec(spec.as_ref()) {
196
+ return true;
197
+ }
198
+ }
199
+ }
200
+ false
201
+ }
202
+
203
+ /// A resolved module: path and its parsed program.
204
+ #[derive(Debug, Clone)]
205
+ pub struct ResolvedModule {
206
+ pub path: PathBuf,
207
+ pub program: Program,
208
+ }
209
+
210
+ /// Resolve all modules starting from the entry file. Returns modules in dependency order
211
+ /// (dependencies first, then dependents). Entry module is last.
212
+ pub fn resolve_project(
213
+ entry_path: &Path,
214
+ project_root: Option<&Path>,
215
+ ) -> Result<Vec<ResolvedModule>, String> {
216
+ let project_root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
217
+ let entry_canon = entry_path
218
+ .canonicalize()
219
+ .map_err(|e| format!("Cannot canonicalize entry {}: {}", entry_path.display(), e))?;
220
+ let root_canon = project_root
221
+ .canonicalize()
222
+ .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
223
+
224
+ let mut visited = HashSet::new();
225
+ let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
226
+ let mut load_order: Vec<PathBuf> = Vec::new();
227
+
228
+ load_module_recursive(
229
+ &entry_canon,
230
+ &root_canon,
231
+ &mut visited,
232
+ &mut path_to_module,
233
+ &mut load_order,
234
+ )?;
235
+
236
+ Ok(load_order
237
+ .into_iter()
238
+ .map(|p| {
239
+ let program = path_to_module.remove(&p).unwrap();
240
+ ResolvedModule { path: p, program }
241
+ })
242
+ .collect())
243
+ }
244
+
245
+ fn load_module_recursive(
246
+ module_path: &Path,
247
+ project_root: &Path,
248
+ visited: &mut HashSet<PathBuf>,
249
+ path_to_module: &mut HashMap<PathBuf, Program>,
250
+ load_order: &mut Vec<PathBuf>,
251
+ ) -> Result<(), String> {
252
+ let canonical = module_path
253
+ .canonicalize()
254
+ .map_err(|e| format!("Cannot read {}: {}", module_path.display(), e))?;
255
+
256
+ if visited.contains(&canonical) {
257
+ return Ok(());
258
+ }
259
+ visited.insert(canonical.clone());
260
+
261
+ let source = std::fs::read_to_string(&canonical)
262
+ .map_err(|e| format!("Cannot read {}: {}", canonical.display(), e))?;
263
+ let program = tish_parser::parse(&source)
264
+ .map_err(|e| format!("Parse error in {}: {}", canonical.display(), e))?;
265
+
266
+ // Collect imports and load dependencies first (skip native imports)
267
+ let dir = canonical.parent().unwrap_or(Path::new("."));
268
+ for stmt in &program.statements {
269
+ if let Statement::Import { from, .. } = stmt {
270
+ if is_native_import(from) {
271
+ continue; // Native imports don't load files
272
+ }
273
+ let dep_path = resolve_import_path(from, dir, project_root)?;
274
+ if !path_to_module.contains_key(&dep_path) {
275
+ load_module_recursive(
276
+ &dep_path,
277
+ project_root,
278
+ visited,
279
+ path_to_module,
280
+ load_order,
281
+ )?;
282
+ }
283
+ }
284
+ }
285
+
286
+ path_to_module.insert(canonical.clone(), program);
287
+ load_order.push(canonical);
288
+ Ok(())
289
+ }
290
+
291
+ /// Returns true for native module imports that don't resolve to files.
292
+ /// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
293
+ /// - tish:egui, tish:polars, etc.
294
+ /// - @scope/package (npm-style)
295
+ pub fn is_native_import(spec: &str) -> bool {
296
+ spec.starts_with("tish:")
297
+ || spec.starts_with('@')
298
+ || matches!(spec, "fs" | "http" | "process" | "ws")
299
+ }
300
+
301
+ /// Map native spec to Cargo feature name for built-in tish:* modules.
302
+ pub fn native_spec_to_feature(spec: &str) -> Option<String> {
303
+ let canonical = normalize_builtin_spec(spec)?;
304
+ canonical.strip_prefix("tish:").map(|s| s.to_string())
305
+ }
306
+
307
+ /// Resolve a bare specifier (e.g. "lattish") to a path via node_modules.
308
+ fn resolve_bare_spec(spec: &str, from_dir: &Path, _project_root: &Path) -> Option<PathBuf> {
309
+ let mut search = from_dir.to_path_buf();
310
+ loop {
311
+ let node_mod = search.join("node_modules").join(spec);
312
+ let pkg_json = node_mod.join("package.json");
313
+ if pkg_json.exists() {
314
+ if let Some(name) = read_package_name(&pkg_json) {
315
+ if name == spec {
316
+ let content = std::fs::read_to_string(&pkg_json).ok()?;
317
+ let json: serde_json::Value = serde_json::from_str(&content).ok()?;
318
+ let entry = json
319
+ .get("tish")
320
+ .and_then(|t| t.get("module"))
321
+ .and_then(|m| m.as_str())
322
+ .or_else(|| json.get("main").and_then(|m| m.as_str()));
323
+ let entry = entry.unwrap_or("index.tish");
324
+ let entry_clean = entry.trim_start_matches("./");
325
+ let resolved = node_mod.join(entry_clean);
326
+ if resolved.exists() {
327
+ return resolved.canonicalize().ok();
328
+ }
329
+ }
330
+ }
331
+ }
332
+ if let Some(parent) = search.parent() {
333
+ if parent == search {
334
+ break;
335
+ }
336
+ search = parent.to_path_buf();
337
+ } else {
338
+ break;
339
+ }
340
+ }
341
+ None
342
+ }
343
+
344
+ /// Resolve an import specifier (e.g. "./foo.tish", "../lib/utils", "lattish") to an absolute path.
345
+ fn resolve_import_path(
346
+ spec: &str,
347
+ from_dir: &Path,
348
+ project_root: &Path,
349
+ ) -> Result<PathBuf, String> {
350
+ if is_native_import(spec) {
351
+ return Err(format!(
352
+ "resolve_import_path called for native import (use merge_modules native branch): {}",
353
+ spec
354
+ ));
355
+ }
356
+ if !spec.starts_with("./") && !spec.starts_with("../") {
357
+ if let Some(path) = resolve_bare_spec(spec, from_dir, project_root) {
358
+ return Ok(path);
359
+ }
360
+ return Err(format!(
361
+ "Package '{}' not found in node_modules. Install it with: npm install {}",
362
+ spec, spec
363
+ ));
364
+ }
365
+ let base = from_dir.join(spec);
366
+ // Try with .tish extension if the path has no extension
367
+ let path = if base.extension().is_none() {
368
+ let with_ext = base.with_extension("tish");
369
+ if with_ext.exists() {
370
+ with_ext
371
+ } else {
372
+ base
373
+ }
374
+ } else {
375
+ base
376
+ };
377
+ path.canonicalize().map_err(|e| {
378
+ format!(
379
+ "Cannot resolve import '{}' from {}: {}",
380
+ spec,
381
+ from_dir.display(),
382
+ e
383
+ )
384
+ })
385
+ }
386
+
387
+ /// Check for cyclic imports. Returns Err if a cycle is detected.
388
+ pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
389
+ let path_to_idx: HashMap<_, _> = modules
390
+ .iter()
391
+ .enumerate()
392
+ .map(|(i, m)| (m.path.clone(), i))
393
+ .collect();
394
+
395
+ for (idx, module) in modules.iter().enumerate() {
396
+ let dir = module.path.parent().unwrap_or(Path::new("."));
397
+ let mut stack = vec![idx];
398
+ if has_cycle_from(
399
+ dir,
400
+ &module.program,
401
+ &path_to_idx,
402
+ modules,
403
+ &mut stack,
404
+ &mut HashSet::new(),
405
+ )? {
406
+ let path_names: Vec<_> = stack
407
+ .iter()
408
+ .map(|&i| modules[i].path.display().to_string())
409
+ .collect();
410
+ return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
411
+ }
412
+ }
413
+ Ok(())
414
+ }
415
+
416
+ fn has_cycle_from(
417
+ from_dir: &Path,
418
+ program: &Program,
419
+ path_to_idx: &HashMap<PathBuf, usize>,
420
+ modules: &[ResolvedModule],
421
+ stack: &mut Vec<usize>,
422
+ visiting: &mut HashSet<usize>,
423
+ ) -> Result<bool, String> {
424
+ for stmt in &program.statements {
425
+ if let Statement::Import { from, .. } = stmt {
426
+ if is_native_import(from) {
427
+ continue;
428
+ }
429
+ let dep_path = resolve_import_path(from, from_dir, Path::new("."))?;
430
+ if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
431
+ if stack.contains(&dep_idx) {
432
+ stack.push(dep_idx);
433
+ return Ok(true);
434
+ }
435
+ if !visiting.contains(&dep_idx) {
436
+ visiting.insert(dep_idx);
437
+ stack.push(dep_idx);
438
+ let dep = &modules[dep_idx];
439
+ let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
440
+ if has_cycle_from(
441
+ dep_dir,
442
+ &dep.program,
443
+ path_to_idx,
444
+ modules,
445
+ stack,
446
+ visiting,
447
+ )? {
448
+ return Ok(true);
449
+ }
450
+ stack.pop();
451
+ visiting.remove(&dep_idx);
452
+ }
453
+ }
454
+ }
455
+ }
456
+ Ok(false)
457
+ }
458
+
459
+ /// Merge all resolved modules into a single program. Dependencies are emitted first.
460
+ /// Import statements are rewritten as bindings from already-emitted dep exports.
461
+ /// Export statements are unwrapped (the inner declaration is emitted).
462
+ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
463
+ let path_to_idx: HashMap<PathBuf, usize> = modules
464
+ .iter()
465
+ .enumerate()
466
+ .map(|(i, m)| (m.path.canonicalize().unwrap_or(m.path.clone()), i))
467
+ .collect();
468
+
469
+ let mut module_exports: Vec<HashMap<String, String>> = vec![HashMap::new(); modules.len()];
470
+ for (idx, module) in modules.iter().enumerate() {
471
+ for stmt in &module.program.statements {
472
+ if let Statement::Export { declaration, .. } = stmt {
473
+ match declaration.as_ref() {
474
+ ExportDeclaration::Named(s) => {
475
+ let name = match s.as_ref() {
476
+ Statement::VarDecl { name, .. } | Statement::FunDecl { name, .. } => {
477
+ name.to_string()
478
+ }
479
+ _ => continue,
480
+ };
481
+ module_exports[idx].insert(name.clone(), name);
482
+ }
483
+ ExportDeclaration::Default(_) => {
484
+ let default_name = format!("__default_{}", idx);
485
+ module_exports[idx].insert("default".to_string(), default_name);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ let mut statements = Vec::new();
493
+ for (idx, module) in modules.iter().enumerate() {
494
+ let dir = module.path.parent().unwrap_or(Path::new("."));
495
+ for stmt in &module.program.statements {
496
+ match stmt {
497
+ Statement::Import { specifiers, from, span } => {
498
+ if is_native_import(from) {
499
+ // Normalize fs/http/process -> tish:fs etc. for Node compatibility
500
+ let canonical_spec =
501
+ normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
502
+ // Emit VarDecl with NativeModuleLoad for each specifier
503
+ for spec in specifiers {
504
+ match spec {
505
+ ImportSpecifier::Named { name, alias } => {
506
+ let bind = alias.as_deref().unwrap_or(name.as_ref());
507
+ let init = Expr::NativeModuleLoad {
508
+ spec: Arc::from(canonical_spec.clone()),
509
+ export_name: name.clone(),
510
+ span: *span,
511
+ };
512
+ statements.push(Statement::VarDecl {
513
+ name: Arc::from(bind),
514
+ mutable: false,
515
+ type_ann: None,
516
+ init: Some(init),
517
+ span: *span,
518
+ });
519
+ }
520
+ ImportSpecifier::Namespace(ns) => {
521
+ return Err(format!(
522
+ "Namespace import (* as {}) not supported for native module '{}'",
523
+ ns.as_ref(),
524
+ from.as_ref()
525
+ ));
526
+ }
527
+ ImportSpecifier::Default(bind) => {
528
+ return Err(format!(
529
+ "Default import '{}' not supported for native module '{}'. Use named import, e.g. import {{ egui }} from '{}'",
530
+ bind.as_ref(),
531
+ from.as_ref(),
532
+ from.as_ref()
533
+ ));
534
+ }
535
+ }
536
+ }
537
+ continue;
538
+ }
539
+ let dep_path = resolve_import_path(from, dir, Path::new("."))?;
540
+ let dep_path = dep_path
541
+ .canonicalize()
542
+ .unwrap_or(dep_path);
543
+ let dep_idx = *path_to_idx
544
+ .get(&dep_path)
545
+ .ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
546
+ let dep_exports = &module_exports[dep_idx];
547
+ for spec in specifiers {
548
+ match spec {
549
+ ImportSpecifier::Named { name, alias } => {
550
+ let source = dep_exports
551
+ .get(name.as_ref())
552
+ .cloned()
553
+ .unwrap_or_else(|| name.to_string());
554
+ let bind = alias.as_deref().unwrap_or(name.as_ref());
555
+ if bind != source {
556
+ statements.push(Statement::VarDecl {
557
+ name: Arc::from(bind),
558
+ mutable: false,
559
+ type_ann: None,
560
+ init: Some(Expr::Ident {
561
+ name: Arc::from(source),
562
+ span: *span,
563
+ }),
564
+ span: *span,
565
+ });
566
+ }
567
+ }
568
+ ImportSpecifier::Namespace(ns) => {
569
+ let mut props = Vec::new();
570
+ for (k, v) in dep_exports {
571
+ props.push(tish_ast::ObjectProp::KeyValue(
572
+ Arc::from(k.clone()),
573
+ Expr::Ident {
574
+ name: Arc::from(v.clone()),
575
+ span: *span,
576
+ },
577
+ ));
578
+ }
579
+ statements.push(Statement::VarDecl {
580
+ name: ns.clone(),
581
+ mutable: false,
582
+ type_ann: None,
583
+ init: Some(Expr::Object {
584
+ props,
585
+ span: *span,
586
+ }),
587
+ span: *span,
588
+ });
589
+ }
590
+ ImportSpecifier::Default(bind) => {
591
+ let source = dep_exports
592
+ .get("default")
593
+ .cloned()
594
+ .ok_or_else(|| {
595
+ format!("Module '{}' has no default export", from)
596
+ })?;
597
+ statements.push(Statement::VarDecl {
598
+ name: bind.clone(),
599
+ mutable: false,
600
+ type_ann: None,
601
+ init: Some(Expr::Ident {
602
+ name: Arc::from(source),
603
+ span: *span,
604
+ }),
605
+ span: *span,
606
+ });
607
+ }
608
+ }
609
+ }
610
+ }
611
+ Statement::Export { declaration, .. } => {
612
+ match declaration.as_ref() {
613
+ ExportDeclaration::Named(s) => statements.push(*s.clone()),
614
+ ExportDeclaration::Default(e) => {
615
+ let default_name = format!("__default_{}", idx);
616
+ statements.push(Statement::VarDecl {
617
+ name: Arc::from(default_name),
618
+ mutable: false,
619
+ type_ann: None,
620
+ init: Some((*e).clone()),
621
+ span: e.span(),
622
+ });
623
+ }
624
+ }
625
+ }
626
+ _ => statements.push(stmt.clone()),
627
+ }
628
+ }
629
+ }
630
+ Ok(Program { statements })
631
+ }