@tishlang/tish 1.5.0 → 1.7.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 (85) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/error.rs +2 -8
  4. package/crates/js_to_tish/src/transform/expr.rs +101 -130
  5. package/crates/js_to_tish/src/transform/stmt.rs +25 -22
  6. package/crates/tish/Cargo.toml +1 -1
  7. package/crates/tish/src/cli_help.rs +76 -29
  8. package/crates/tish/src/main.rs +85 -54
  9. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  12. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  13. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  14. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  15. package/crates/tish/tests/integration_test.rs +197 -47
  16. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  17. package/crates/tish/tests/shortcircuit.rs +19 -4
  18. package/crates/tish_ast/src/ast.rs +12 -14
  19. package/crates/tish_build_utils/src/lib.rs +64 -6
  20. package/crates/tish_builtins/src/array.rs +52 -21
  21. package/crates/tish_builtins/src/construct.rs +2 -8
  22. package/crates/tish_builtins/src/globals.rs +30 -15
  23. package/crates/tish_builtins/src/lib.rs +5 -5
  24. package/crates/tish_builtins/src/math.rs +5 -3
  25. package/crates/tish_builtins/src/string.rs +71 -19
  26. package/crates/tish_bytecode/src/chunk.rs +0 -1
  27. package/crates/tish_bytecode/src/compiler.rs +164 -60
  28. package/crates/tish_bytecode/src/opcode.rs +13 -4
  29. package/crates/tish_bytecode/src/peephole.rs +2 -2
  30. package/crates/tish_compile/Cargo.toml +1 -0
  31. package/crates/tish_compile/src/codegen.rs +989 -318
  32. package/crates/tish_compile/src/infer.rs +69 -19
  33. package/crates/tish_compile/src/lib.rs +21 -8
  34. package/crates/tish_compile/src/resolve.rs +515 -94
  35. package/crates/tish_compile/src/types.rs +10 -14
  36. package/crates/tish_compile_js/src/codegen.rs +34 -13
  37. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  38. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  39. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +40 -48
  40. package/crates/tish_core/src/json.rs +5 -3
  41. package/crates/tish_core/src/lib.rs +1 -1
  42. package/crates/tish_core/src/uri.rs +9 -6
  43. package/crates/tish_core/src/value.rs +92 -28
  44. package/crates/tish_cranelift/src/link.rs +6 -9
  45. package/crates/tish_cranelift/src/lower.rs +14 -8
  46. package/crates/tish_eval/src/eval.rs +398 -141
  47. package/crates/tish_eval/src/lib.rs +10 -6
  48. package/crates/tish_eval/src/natives.rs +95 -38
  49. package/crates/tish_eval/src/promise.rs +14 -8
  50. package/crates/tish_eval/src/timers.rs +28 -19
  51. package/crates/tish_eval/src/value.rs +10 -3
  52. package/crates/tish_fmt/src/lib.rs +29 -13
  53. package/crates/tish_lexer/src/lib.rs +217 -63
  54. package/crates/tish_lexer/src/token.rs +6 -6
  55. package/crates/tish_llvm/src/lib.rs +15 -8
  56. package/crates/tish_lsp/src/main.rs +41 -43
  57. package/crates/tish_native/src/build.rs +38 -15
  58. package/crates/tish_native/src/lib.rs +76 -32
  59. package/crates/tish_opt/src/lib.rs +67 -50
  60. package/crates/tish_parser/src/lib.rs +36 -11
  61. package/crates/tish_parser/src/parser.rs +172 -87
  62. package/crates/tish_runtime/src/http.rs +15 -6
  63. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  64. package/crates/tish_runtime/src/lib.rs +224 -168
  65. package/crates/tish_runtime/src/promise.rs +1 -5
  66. package/crates/tish_runtime/src/ws.rs +45 -20
  67. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  68. package/crates/tish_ui/src/jsx.rs +41 -22
  69. package/crates/tish_ui/src/lib.rs +2 -2
  70. package/crates/tish_vm/src/vm.rs +320 -116
  71. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  72. package/crates/tish_wasm/src/lib.rs +38 -28
  73. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  74. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  75. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  76. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  77. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  78. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  79. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  80. package/package.json +1 -1
  81. package/platform/darwin-arm64/tish +0 -0
  82. package/platform/darwin-x64/tish +0 -0
  83. package/platform/linux-arm64/tish +0 -0
  84. package/platform/linux-x64/tish +0 -0
  85. package/platform/win32-x64/tish.exe +0 -0
@@ -1,5 +1,5 @@
1
1
  //! Module resolver: resolves relative imports, builds dependency graph, detects cycles.
2
- //! Supports native imports: tish:egui, tish:polars, @scope/pkg (via package.json).
2
+ //! Supports native imports: `tish:…`, `cargo:…`, `@scope/pkg` (via package.json).
3
3
 
4
4
  use std::collections::{HashMap, HashSet};
5
5
  use std::path::{Path, PathBuf};
@@ -16,6 +16,33 @@ pub struct ResolvedNativeModule {
16
16
  pub crate_name: String,
17
17
  pub crate_path: PathBuf,
18
18
  pub export_fn: String,
19
+ /// When false, omit `path = …` in the generated Cargo.toml (crate comes from `tish.rustDependencies` only).
20
+ pub use_path_dependency: bool,
21
+ }
22
+
23
+ /// How codegen links a native import to Rust (`generateNativeWrapper` for `tish:*`; `cargo:*` always generated).
24
+ #[derive(Debug, Clone)]
25
+ pub enum NativeModuleInit {
26
+ /// Call `external_crate::export_fn()` and read named exports from the returned object.
27
+ Legacy {
28
+ crate_name: String,
29
+ export_fn: String,
30
+ },
31
+ /// Call `crate::generated_native::export_fn()` — object built from per-export fns on `shim_crate`.
32
+ Generated {
33
+ shim_crate: String,
34
+ export_fn: String,
35
+ },
36
+ }
37
+
38
+ /// Extra native build inputs produced alongside Rust source (Cargo merge + optional wrapper).
39
+ #[derive(Debug, Clone)]
40
+ pub struct NativeBuildArtifacts {
41
+ /// Extra `[dependencies]` lines from `tish.rustDependencies`.
42
+ pub rust_dependencies_toml: String,
43
+ /// Generated `generated_native.rs` when using [`NativeModuleInit::Generated`].
44
+ pub generated_native_rs: Option<String>,
45
+ pub native_init: std::collections::HashMap<String, NativeModuleInit>,
19
46
  }
20
47
 
21
48
  /// Node-compatible aliases for built-in modules (fs -> tish:fs, etc.).
@@ -45,32 +72,109 @@ pub fn is_builtin_native_spec(spec: &str) -> bool {
45
72
 
46
73
  /// Resolve all native imports in a merged program via package.json lookup.
47
74
  /// Built-in modules (tish:fs, tish:http, tish:process) are skipped - they use tishlang_runtime directly.
48
- pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
75
+ /// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
76
+ pub fn resolve_native_modules(
77
+ program: &Program,
78
+ project_root: &Path,
79
+ ) -> Result<Vec<ResolvedNativeModule>, String> {
49
80
  let root_canon = project_root
50
81
  .canonicalize()
51
82
  .map_err(|e| format!("Cannot canonicalize project root: {}", e))?;
52
83
  let mut seen = HashSet::new();
53
84
  let mut modules = Vec::new();
54
85
  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 tishlang_runtime, no package.json lookup
86
+ let specs: Vec<String> = match stmt {
87
+ Statement::VarDecl {
88
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
89
+ ..
90
+ } => vec![spec.as_ref().to_string()],
91
+ Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
92
+ vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
93
+ }
94
+ _ => continue,
95
+ };
96
+ for s in specs {
97
+ if is_builtin_native_spec(&s) {
98
+ continue;
63
99
  }
64
- if !seen.insert(s.to_string()) {
100
+ if !seen.insert(s.clone()) {
65
101
  continue;
66
102
  }
67
- let m = resolve_native_module(s, &root_canon)?;
103
+ let m = if s.starts_with("cargo:") {
104
+ resolve_cargo_native_module(&s, &root_canon)?
105
+ } else {
106
+ resolve_native_module(&s, &root_canon)?
107
+ };
68
108
  modules.push(m);
69
109
  }
70
110
  }
71
111
  Ok(modules)
72
112
  }
73
113
 
114
+ /// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
115
+ pub fn is_cargo_native_spec(spec: &str) -> bool {
116
+ spec.starts_with("cargo:")
117
+ }
118
+
119
+ /// Stable Rust symbol for the generated namespace function, e.g. `cargo:my-crate` → `cargo_native_my_crate_object`.
120
+ pub fn cargo_export_fn_name(spec: &str) -> String {
121
+ let tail = spec.strip_prefix("cargo:").unwrap_or(spec);
122
+ let mut out = String::from("cargo_native_");
123
+ for c in tail.chars() {
124
+ if c.is_ascii_alphanumeric() {
125
+ out.push(c);
126
+ } else {
127
+ out.push('_');
128
+ }
129
+ }
130
+ if out == "cargo_native_" {
131
+ out.push_str("unnamed");
132
+ }
133
+ out.push_str("_object");
134
+ out
135
+ }
136
+
137
+ fn resolve_cargo_native_module(
138
+ spec: &str,
139
+ project_root: &Path,
140
+ ) -> Result<ResolvedNativeModule, String> {
141
+ let tail = spec
142
+ .strip_prefix("cargo:")
143
+ .ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
144
+ if tail.is_empty() {
145
+ return Err(
146
+ "cargo: import needs a dependency name, e.g. import { x } from 'cargo:my_crate'".into(),
147
+ );
148
+ }
149
+ let dep_key = tail.to_string();
150
+ let tish = read_project_tish_config(project_root);
151
+ let rust_deps = tish.get("rustDependencies").and_then(|v| v.as_object()).ok_or_else(|| {
152
+ format!(
153
+ "cargo:{} requires package.json \"tish\": {{ \"rustDependencies\": {{ \"{}\": \"…\" }} }}",
154
+ tail, dep_key
155
+ )
156
+ })?;
157
+ if !rust_deps.contains_key(&dep_key) {
158
+ return Err(format!(
159
+ "cargo:{}: add \"{}\" to tish.rustDependencies in package.json (version string or inline table)",
160
+ tail, dep_key
161
+ ));
162
+ }
163
+ let crate_name = dep_key.replace('-', "_");
164
+ let export_fn = cargo_export_fn_name(spec);
165
+ let crate_path = project_root
166
+ .canonicalize()
167
+ .unwrap_or_else(|_| project_root.to_path_buf());
168
+ Ok(ResolvedNativeModule {
169
+ spec: spec.to_string(),
170
+ package_name: dep_key.clone(),
171
+ crate_name,
172
+ crate_path,
173
+ export_fn,
174
+ use_path_dependency: false,
175
+ })
176
+ }
177
+
74
178
  fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
75
179
  let package_name = if spec.starts_with("tish:") {
76
180
  format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
@@ -83,14 +187,26 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
83
187
  let pkg_json = pkg_dir.join("package.json");
84
188
  let content = std::fs::read_to_string(&pkg_json)
85
189
  .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))?;
190
+ let json: serde_json::Value = serde_json::from_str(&content)
191
+ .map_err(|e| format!("Invalid JSON in {}: {}", pkg_json.display(), e))?;
88
192
  let tish = json
89
193
  .get("tish")
90
194
  .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));
195
+ .ok_or_else(|| {
196
+ format!(
197
+ "Package {} has no \"tish\" config in package.json",
198
+ package_name
199
+ )
200
+ })?;
201
+ if !tish
202
+ .get("module")
203
+ .and_then(|v| v.as_bool())
204
+ .unwrap_or(false)
205
+ {
206
+ return Err(format!(
207
+ "Package {} is not a Tish native module (tish.module must be true)",
208
+ package_name
209
+ ));
94
210
  }
95
211
  let raw_crate = tish
96
212
  .get("crate")
@@ -110,6 +226,261 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
110
226
  crate_name: raw_crate.replace('-', "_"),
111
227
  crate_path,
112
228
  export_fn,
229
+ use_path_dependency: true,
230
+ })
231
+ }
232
+
233
+ /// Read the `tish` object from the project root `package.json` (empty JSON object if missing).
234
+ pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
235
+ let path = project_root.join("package.json");
236
+ let Ok(content) = std::fs::read_to_string(&path) else {
237
+ return serde_json::json!({});
238
+ };
239
+ let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
240
+ return serde_json::json!({});
241
+ };
242
+ json.get("tish")
243
+ .cloned()
244
+ .unwrap_or_else(|| serde_json::json!({}))
245
+ }
246
+
247
+ fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
248
+ let p = Path::new(raw);
249
+ let resolved = if p.is_absolute() {
250
+ p.to_path_buf()
251
+ } else {
252
+ project_root.join(p)
253
+ };
254
+ let resolved = resolved.canonicalize().unwrap_or(resolved);
255
+ resolved.display().to_string().replace('\\', "/")
256
+ }
257
+
258
+ fn json_to_cargo_inline_value(
259
+ v: &serde_json::Value,
260
+ project_root: &Path,
261
+ ) -> Result<String, String> {
262
+ match v {
263
+ serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
264
+ serde_json::Value::Bool(b) => Ok(b.to_string()),
265
+ serde_json::Value::Number(n) => Ok(n.to_string()),
266
+ serde_json::Value::Array(arr) => {
267
+ let mut inner = Vec::new();
268
+ for item in arr {
269
+ inner.push(json_to_cargo_inline_value(item, project_root)?);
270
+ }
271
+ Ok(format!("[{}]", inner.join(", ")))
272
+ }
273
+ serde_json::Value::Object(map) => {
274
+ let mut parts = Vec::new();
275
+ for (k, v) in map {
276
+ let rhs = if k == "path" && v.as_str().is_some() {
277
+ let s = v.as_str().unwrap();
278
+ format!("{:?}", resolve_cargo_path_for_toml(project_root, s))
279
+ } else {
280
+ json_to_cargo_inline_value(v, project_root)?
281
+ };
282
+ parts.push(format!("{} = {}", k, rhs));
283
+ }
284
+ Ok(format!("{{ {} }}", parts.join(", ")))
285
+ }
286
+ serde_json::Value::Null => Err("null is not valid in a Cargo dependency value".to_string()),
287
+ }
288
+ }
289
+
290
+ /// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
291
+ /// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
292
+ pub fn format_rust_dependencies_toml(
293
+ tish: &serde_json::Value,
294
+ project_root: &Path,
295
+ ) -> Result<String, String> {
296
+ let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
297
+ return Ok(String::new());
298
+ };
299
+ let mut out = String::new();
300
+ for (name, val) in obj {
301
+ match val {
302
+ serde_json::Value::String(_) | serde_json::Value::Object(_) => {
303
+ out.push_str(&format!(
304
+ "{} = {}\n",
305
+ name,
306
+ json_to_cargo_inline_value(val, project_root)?
307
+ ));
308
+ }
309
+ _ => {
310
+ return Err(format!(
311
+ "tish.rustDependencies.{} must be a string (version) or object (inline table)",
312
+ name
313
+ ));
314
+ }
315
+ }
316
+ }
317
+ Ok(out)
318
+ }
319
+
320
+ /// Map a Tish export name to a Rust identifier (e.g. `readFile` → `read_file`) for shim crate symbols.
321
+ pub fn export_name_to_rust_ident(export_name: &str) -> String {
322
+ let mut out = String::new();
323
+ for (i, c) in export_name.chars().enumerate() {
324
+ if c.is_uppercase() && i > 0 {
325
+ out.push('_');
326
+ }
327
+ for lower in c.to_lowercase() {
328
+ out.push(lower);
329
+ }
330
+ }
331
+ if out.is_empty() {
332
+ "native_export".to_string()
333
+ } else {
334
+ out
335
+ }
336
+ }
337
+
338
+ /// Collect `(spec, export_name)` for every non-builtin native import in the program.
339
+ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet<String>> {
340
+ let mut map: HashMap<String, HashSet<String>> = HashMap::new();
341
+ for stmt in &program.statements {
342
+ match stmt {
343
+ Statement::VarDecl {
344
+ init:
345
+ Some(Expr::NativeModuleLoad {
346
+ spec, export_name, ..
347
+ }),
348
+ ..
349
+ } => {
350
+ let s = spec.as_ref();
351
+ if is_builtin_native_spec(s) {
352
+ continue;
353
+ }
354
+ map.entry(s.to_string())
355
+ .or_default()
356
+ .insert(export_name.to_string());
357
+ }
358
+ Statement::Import {
359
+ specifiers, from, ..
360
+ } if is_native_import(from.as_ref()) => {
361
+ let spec =
362
+ normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
363
+ if is_builtin_native_spec(&spec) {
364
+ continue;
365
+ }
366
+ for sp in specifiers {
367
+ if let ImportSpecifier::Named { name, .. } = sp {
368
+ map.entry(spec.clone())
369
+ .or_default()
370
+ .insert(name.to_string());
371
+ }
372
+ }
373
+ }
374
+ _ => {}
375
+ }
376
+ }
377
+ map
378
+ }
379
+
380
+ /// Emit `generated_native.rs` for [`NativeModuleInit::Generated`] modules.
381
+ pub fn generate_native_wrapper_rs(
382
+ modules: &[ResolvedNativeModule],
383
+ inferred: &HashMap<String, HashSet<String>>,
384
+ init_by_spec: &HashMap<String, NativeModuleInit>,
385
+ ) -> String {
386
+ let mut file = String::from(
387
+ "//! Generated by `tish build` — do not edit.\n\
388
+ use std::cell::RefCell;\n\
389
+ use std::rc::Rc;\n\
390
+ use std::sync::Arc;\n\
391
+ use tishlang_runtime::{ObjectMap, Value};\n\n",
392
+ );
393
+ let mut any = false;
394
+ for m in modules {
395
+ let Some(NativeModuleInit::Generated {
396
+ shim_crate,
397
+ export_fn,
398
+ }) = init_by_spec.get(&m.spec)
399
+ else {
400
+ continue;
401
+ };
402
+ let Some(names) = inferred.get(&m.spec) else {
403
+ continue;
404
+ };
405
+ if names.is_empty() {
406
+ continue;
407
+ }
408
+ any = true;
409
+ let mut keys: Vec<_> = names.iter().cloned().collect();
410
+ keys.sort();
411
+ file.push_str(&format!("pub fn {}() -> Value {{\n", export_fn));
412
+ file.push_str(" let mut m = ObjectMap::default();\n");
413
+ for export_name in keys {
414
+ let rust_fn = export_name_to_rust_ident(&export_name);
415
+ let key_lit = format!("{:?}", export_name);
416
+ file.push_str(&format!(
417
+ " m.insert(Arc::from({}), Value::Function(Rc::new(|args: &[Value]| {{\n {}::{}(args)\n }})));\n",
418
+ key_lit, shim_crate, rust_fn
419
+ ));
420
+ }
421
+ file.push_str(" Value::Object(Rc::new(RefCell::new(m)))\n}\n\n");
422
+ }
423
+ if !any {
424
+ return String::new();
425
+ }
426
+ file
427
+ }
428
+
429
+ /// Combine project `package.json`, inferred exports, and resolved native modules into build artifacts.
430
+ pub fn compute_native_build_artifacts(
431
+ program: &Program,
432
+ project_root: &Path,
433
+ native_modules: &[ResolvedNativeModule],
434
+ ) -> Result<NativeBuildArtifacts, String> {
435
+ let tish = read_project_tish_config(project_root);
436
+ let rust_dependencies_toml = format_rust_dependencies_toml(&tish, project_root)?;
437
+ let inferred = infer_native_module_exports(program);
438
+ let gen_tish = tish
439
+ .get("generateNativeWrapper")
440
+ .and_then(|v| v.as_bool())
441
+ .unwrap_or(false);
442
+
443
+ let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
444
+ for m in native_modules {
445
+ let use_gen = if is_cargo_native_spec(&m.spec) {
446
+ inferred
447
+ .get(&m.spec)
448
+ .map(|s| !s.is_empty())
449
+ .unwrap_or(false)
450
+ } else {
451
+ gen_tish
452
+ && inferred
453
+ .get(&m.spec)
454
+ .map(|s| !s.is_empty())
455
+ .unwrap_or(false)
456
+ };
457
+ let init = if use_gen {
458
+ NativeModuleInit::Generated {
459
+ shim_crate: m.crate_name.clone(),
460
+ export_fn: m.export_fn.clone(),
461
+ }
462
+ } else {
463
+ NativeModuleInit::Legacy {
464
+ crate_name: m.crate_name.clone(),
465
+ export_fn: m.export_fn.clone(),
466
+ }
467
+ };
468
+ native_init.insert(m.spec.clone(), init);
469
+ }
470
+
471
+ let generated_native_rs = {
472
+ let s = generate_native_wrapper_rs(native_modules, &inferred, &native_init);
473
+ if s.trim().is_empty() {
474
+ None
475
+ } else {
476
+ Some(s)
477
+ }
478
+ };
479
+
480
+ Ok(NativeBuildArtifacts {
481
+ rust_dependencies_toml,
482
+ generated_native_rs,
483
+ native_init,
113
484
  })
114
485
  }
115
486
 
@@ -151,16 +522,25 @@ fn read_package_name(pkg_path: &Path) -> Option<String> {
151
522
  json.get("name").and_then(|v| v.as_str()).map(String::from)
152
523
  }
153
524
 
525
+ fn stmt_native_specs(stmt: &Statement) -> Vec<String> {
526
+ match stmt {
527
+ Statement::VarDecl {
528
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
529
+ ..
530
+ } => vec![spec.to_string()],
531
+ Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
532
+ vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
533
+ }
534
+ _ => vec![],
535
+ }
536
+ }
537
+
154
538
  /// Extract Cargo feature names from native imports in a merged program.
155
539
  /// Used to enable tishlang_runtime features based on `import { x } from 'tish:egui'` etc.
156
540
  pub fn extract_native_import_features(program: &Program) -> Vec<String> {
157
541
  let mut features = std::collections::HashSet::new();
158
542
  for stmt in &program.statements {
159
- if let Statement::VarDecl {
160
- init: Some(Expr::NativeModuleLoad { spec, .. }),
161
- ..
162
- } = stmt
163
- {
543
+ for spec in stmt_native_specs(stmt) {
164
544
  if let Some(f) = native_spec_to_feature(spec.as_ref()) {
165
545
  features.insert(f);
166
546
  }
@@ -171,27 +551,17 @@ pub fn extract_native_import_features(program: &Program) -> Vec<String> {
171
551
 
172
552
  /// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
173
553
  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
554
+ program
555
+ .statements
556
+ .iter()
557
+ .any(|stmt| !stmt_native_specs(stmt).is_empty())
184
558
  }
185
559
 
186
560
  /// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
187
561
  /// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
188
562
  pub fn has_external_native_imports(program: &Program) -> bool {
189
563
  for stmt in &program.statements {
190
- if let Statement::VarDecl {
191
- init: Some(Expr::NativeModuleLoad { spec, .. }),
192
- ..
193
- } = stmt
194
- {
564
+ for spec in stmt_native_specs(stmt) {
195
565
  if !is_builtin_native_spec(spec.as_ref()) {
196
566
  return true;
197
567
  }
@@ -213,13 +583,18 @@ pub fn resolve_project(
213
583
  entry_path: &Path,
214
584
  project_root: Option<&Path>,
215
585
  ) -> Result<Vec<ResolvedModule>, String> {
216
- let project_root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
586
+ let project_root =
587
+ project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
217
588
  let entry_canon = entry_path
218
589
  .canonicalize()
219
590
  .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))?;
591
+ let root_canon = project_root.canonicalize().map_err(|e| {
592
+ format!(
593
+ "Cannot canonicalize project root {}: {}",
594
+ project_root.display(),
595
+ e
596
+ )
597
+ })?;
223
598
 
224
599
  let mut visited = HashSet::new();
225
600
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
@@ -249,28 +624,30 @@ pub fn resolve_project_from_stdin(
249
624
  source: &str,
250
625
  project_root: &Path,
251
626
  ) -> Result<Vec<ResolvedModule>, String> {
252
- let root_canon = project_root
253
- .canonicalize()
254
- .map_err(|e| format!("Cannot canonicalize project root {}: {}", project_root.display(), e))?;
627
+ let root_canon = project_root.canonicalize().map_err(|e| {
628
+ format!(
629
+ "Cannot canonicalize project root {}: {}",
630
+ project_root.display(),
631
+ e
632
+ )
633
+ })?;
255
634
 
256
635
  let stdin_path = root_canon.join("<stdin>");
257
- let program = tishlang_parser::parse(source)
258
- .map_err(|e| format!("Parse error (stdin): {}", e))?;
636
+ let program =
637
+ tishlang_parser::parse(source).map_err(|e| format!("Parse error (stdin): {}", e))?;
259
638
 
260
639
  let mut visited = HashSet::new();
261
640
  let mut path_to_module: HashMap<PathBuf, Program> = HashMap::new();
262
641
  let mut load_order: Vec<PathBuf> = Vec::new();
263
642
 
264
- let from_dir = stdin_path
265
- .parent()
266
- .unwrap_or_else(|| Path::new("."));
643
+ let from_dir = stdin_path.parent().unwrap_or_else(|| Path::new("."));
267
644
 
268
645
  for stmt in &program.statements {
269
646
  if let Statement::Import { from, .. } = stmt {
270
- if is_native_import(from) {
647
+ if is_native_import(from.as_ref()) {
271
648
  continue;
272
649
  }
273
- let dep_path = resolve_import_path(from, from_dir, &root_canon)?;
650
+ let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
274
651
  if !path_to_module.contains_key(&dep_path) {
275
652
  load_module_recursive(
276
653
  &dep_path,
@@ -320,10 +697,10 @@ fn load_module_recursive(
320
697
  let dir = canonical.parent().unwrap_or(Path::new("."));
321
698
  for stmt in &program.statements {
322
699
  if let Statement::Import { from, .. } = stmt {
323
- if is_native_import(from) {
700
+ if is_native_import(from.as_ref()) {
324
701
  continue; // Native imports don't load files
325
702
  }
326
- let dep_path = resolve_import_path(from, dir, project_root)?;
703
+ let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
327
704
  if !path_to_module.contains_key(&dep_path) {
328
705
  load_module_recursive(
329
706
  &dep_path,
@@ -344,9 +721,11 @@ fn load_module_recursive(
344
721
  /// Returns true for native module imports that don't resolve to files.
345
722
  /// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
346
723
  /// - tish:egui, tish:polars, etc.
724
+ /// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
347
725
  /// - @scope/package (npm-style)
348
726
  pub fn is_native_import(spec: &str) -> bool {
349
727
  spec.starts_with("tish:")
728
+ || spec.starts_with("cargo:")
350
729
  || spec.starts_with('@')
351
730
  || matches!(spec, "fs" | "http" | "process" | "ws")
352
731
  }
@@ -460,7 +839,10 @@ pub fn detect_cycles(modules: &[ResolvedModule]) -> Result<(), String> {
460
839
  .iter()
461
840
  .map(|&i| modules[i].path.display().to_string())
462
841
  .collect();
463
- return Err(format!("Circular import detected: {}", path_names.join(" -> ")));
842
+ return Err(format!(
843
+ "Circular import detected: {}",
844
+ path_names.join(" -> ")
845
+ ));
464
846
  }
465
847
  }
466
848
  Ok(())
@@ -476,10 +858,10 @@ fn has_cycle_from(
476
858
  ) -> Result<bool, String> {
477
859
  for stmt in &program.statements {
478
860
  if let Statement::Import { from, .. } = stmt {
479
- if is_native_import(from) {
861
+ if is_native_import(from.as_ref()) {
480
862
  continue;
481
863
  }
482
- let dep_path = resolve_import_path(from, from_dir, Path::new("."))?;
864
+ let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
483
865
  if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
484
866
  if stack.contains(&dep_idx) {
485
867
  stack.push(dep_idx);
@@ -490,14 +872,8 @@ fn has_cycle_from(
490
872
  stack.push(dep_idx);
491
873
  let dep = &modules[dep_idx];
492
874
  let dep_dir = dep.path.parent().unwrap_or(Path::new("."));
493
- if has_cycle_from(
494
- dep_dir,
495
- &dep.program,
496
- path_to_idx,
497
- modules,
498
- stack,
499
- visiting,
500
- )? {
875
+ if has_cycle_from(dep_dir, &dep.program, path_to_idx, modules, stack, visiting)?
876
+ {
501
877
  return Ok(true);
502
878
  }
503
879
  stack.pop();
@@ -547,11 +923,15 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
547
923
  let dir = module.path.parent().unwrap_or(Path::new("."));
548
924
  for stmt in &module.program.statements {
549
925
  match stmt {
550
- Statement::Import { specifiers, from, span } => {
551
- if is_native_import(from) {
926
+ Statement::Import {
927
+ specifiers,
928
+ from,
929
+ span,
930
+ } => {
931
+ if is_native_import(from.as_ref()) {
552
932
  // Normalize fs/http/process -> tish:fs etc. for Node compatibility
553
- let canonical_spec =
554
- normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
933
+ let canonical_spec = normalize_builtin_spec(from.as_ref())
934
+ .unwrap_or_else(|| from.to_string());
555
935
  // Emit VarDecl with NativeModuleLoad for each specifier
556
936
  for spec in specifiers {
557
937
  match spec {
@@ -589,10 +969,8 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
589
969
  }
590
970
  continue;
591
971
  }
592
- let dep_path = resolve_import_path(from, dir, Path::new("."))?;
593
- let dep_path = dep_path
594
- .canonicalize()
595
- .unwrap_or(dep_path);
972
+ let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
973
+ let dep_path = dep_path.canonicalize().unwrap_or(dep_path);
596
974
  let dep_idx = *path_to_idx
597
975
  .get(&dep_path)
598
976
  .ok_or_else(|| format!("Resolved import '{}' not in module list", from))?;
@@ -633,18 +1011,13 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
633
1011
  name: ns.clone(),
634
1012
  mutable: false,
635
1013
  type_ann: None,
636
- init: Some(Expr::Object {
637
- props,
638
- span: *span,
639
- }),
1014
+ init: Some(Expr::Object { props, span: *span }),
640
1015
  span: *span,
641
1016
  });
642
1017
  }
643
1018
  ImportSpecifier::Default(bind) => {
644
- let source = dep_exports
645
- .get("default")
646
- .cloned()
647
- .ok_or_else(|| {
1019
+ let source =
1020
+ dep_exports.get("default").cloned().ok_or_else(|| {
648
1021
  format!("Module '{}' has no default export", from)
649
1022
  })?;
650
1023
  statements.push(Statement::VarDecl {
@@ -661,24 +1034,72 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
661
1034
  }
662
1035
  }
663
1036
  }
664
- Statement::Export { declaration, .. } => {
665
- match declaration.as_ref() {
666
- ExportDeclaration::Named(s) => statements.push(*s.clone()),
667
- ExportDeclaration::Default(e) => {
668
- let default_name = format!("__default_{}", idx);
669
- statements.push(Statement::VarDecl {
670
- name: Arc::from(default_name),
671
- mutable: false,
672
- type_ann: None,
673
- init: Some((*e).clone()),
674
- span: e.span(),
675
- });
676
- }
1037
+ Statement::Export { declaration, .. } => match declaration.as_ref() {
1038
+ ExportDeclaration::Named(s) => statements.push(*s.clone()),
1039
+ ExportDeclaration::Default(e) => {
1040
+ let default_name = format!("__default_{}", idx);
1041
+ statements.push(Statement::VarDecl {
1042
+ name: Arc::from(default_name),
1043
+ mutable: false,
1044
+ type_ann: None,
1045
+ init: Some((*e).clone()),
1046
+ span: e.span(),
1047
+ });
677
1048
  }
678
- }
1049
+ },
679
1050
  _ => statements.push(stmt.clone()),
680
1051
  }
681
1052
  }
682
1053
  }
683
1054
  Ok(Program { statements })
684
1055
  }
1056
+
1057
+ #[cfg(test)]
1058
+ mod cargo_spec_tests {
1059
+ use std::sync::Arc;
1060
+
1061
+ use super::cargo_export_fn_name;
1062
+ use super::is_native_import;
1063
+
1064
+ #[test]
1065
+ fn is_native_import_accepts_arc_str_ref() {
1066
+ let from: &Arc<str> = &Arc::from("cargo:demo_shim");
1067
+ assert!(is_native_import(from));
1068
+ }
1069
+
1070
+ #[test]
1071
+ fn detect_cycles_skips_cargo_import() {
1072
+ use super::{detect_cycles, resolve_project};
1073
+ let dir = tempfile::tempdir().expect("tempdir");
1074
+ let p = dir.path().join("main.tish");
1075
+ let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
1076
+ std::fs::write(&p, src).unwrap();
1077
+ let root = dir.path();
1078
+ let modules = resolve_project(&p, Some(root)).unwrap();
1079
+ detect_cycles(&modules).unwrap();
1080
+ }
1081
+
1082
+ #[test]
1083
+ fn merge_modules_skips_cargo_import() {
1084
+ use super::{merge_modules, resolve_project};
1085
+ let dir = tempfile::tempdir().expect("tempdir");
1086
+ let p = dir.path().join("main.tish");
1087
+ let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
1088
+ std::fs::write(&p, src).unwrap();
1089
+ let root = dir.path();
1090
+ let modules = resolve_project(&p, Some(root)).unwrap();
1091
+ merge_modules(modules).unwrap();
1092
+ }
1093
+
1094
+ #[test]
1095
+ fn cargo_export_fn_name_sanitizes() {
1096
+ assert_eq!(
1097
+ cargo_export_fn_name("cargo:tish_serde_json"),
1098
+ "cargo_native_tish_serde_json_object"
1099
+ );
1100
+ assert_eq!(
1101
+ cargo_export_fn_name("cargo:my-crate"),
1102
+ "cargo_native_my_crate_object"
1103
+ );
1104
+ }
1105
+ }