@tishlang/tish 1.5.0 → 1.6.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.
package/bin/tish CHANGED
Binary file
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.5.0"
3
+ version = "1.6.0"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -0,0 +1,65 @@
1
+ //! `cargo:` + `tish.rustDependencies` using the in-repo fixture at `tests/fixtures/cargo_example_project/`
2
+ //! (same layout as the standalone `tish-cargo-example` template).
3
+
4
+ use std::path::PathBuf;
5
+
6
+ use tishlang_ast::Statement;
7
+ use tishlang_compile::{compile_project_full, merge_modules, resolve_project};
8
+
9
+ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
10
+ if cli_features.is_empty() {
11
+ let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities().into_iter().collect();
12
+ v.sort();
13
+ v
14
+ } else {
15
+ cli_features.to_vec()
16
+ }
17
+ }
18
+
19
+ #[test]
20
+ fn resolve_and_merge_cargo_example_fixture() {
21
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
22
+ let input_path = manifest_dir
23
+ .join("tests/fixtures/cargo_example_project/src/main.tish")
24
+ .canonicalize()
25
+ .expect("cargo_example_project test fixture");
26
+ let project_root = input_path.parent().map(|p| {
27
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
28
+ p.parent().unwrap_or(p)
29
+ } else {
30
+ p
31
+ }
32
+ });
33
+ let modules = resolve_project(&input_path, project_root).unwrap();
34
+ assert_eq!(modules.len(), 1, "expected single entry module");
35
+ let first = &modules[0].program.statements[0];
36
+ let Statement::Import { from, .. } = first else {
37
+ panic!("expected import, got {:?}", first);
38
+ };
39
+ assert_eq!(from.as_ref(), "cargo:demo_shim");
40
+ merge_modules(modules).unwrap();
41
+ }
42
+
43
+ #[test]
44
+ fn compile_project_full_cargo_example_fixture() {
45
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
46
+ let example_main = manifest_dir
47
+ .join("tests/fixtures/cargo_example_project/src/main.tish")
48
+ .canonicalize()
49
+ .expect("cargo_example_project test fixture");
50
+ let input_path = example_main;
51
+ let project_root = input_path.parent().map(|p| {
52
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
53
+ p.parent().unwrap_or(p)
54
+ } else {
55
+ p
56
+ }
57
+ });
58
+ let features = native_build_features_from_cli(&[]);
59
+ let r = compile_project_full(&input_path, project_root, &features, true);
60
+ assert!(
61
+ r.is_ok(),
62
+ "compile_project_full failed: {:?}",
63
+ r.map_err(|e| e.message)
64
+ );
65
+ }
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = ["crates/demo-shim"]
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "demo_shim"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ crate-type = ["rlib"]
8
+
9
+ [dependencies]
10
+ # Fixture lives at crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim → repo crates/tish_core
11
+ tishlang_core = { path = "../../../../../../../crates/tish_core" }
@@ -0,0 +1,12 @@
1
+ //! Rust exports for `import { … } from 'cargo:demo_shim'` (test fixture).
2
+
3
+ use std::sync::Arc;
4
+ use tishlang_core::Value;
5
+
6
+ pub fn greet(args: &[Value]) -> Value {
7
+ let name = match args.first() {
8
+ Some(Value::String(s)) => s.as_ref(),
9
+ _ => "world",
10
+ };
11
+ Value::String(Arc::from(format!("Hello, {}!", name)))
12
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "tish-cargo-example-fixture",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "tish": {
6
+ "rustDependencies": {
7
+ "demo_shim": { "path": "./crates/demo-shim" }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ import { greet } from 'cargo:demo_shim'
2
+
3
+ console.log(greet('from cargo'))
@@ -202,6 +202,21 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
202
202
  Err("Cannot find Tish workspace root. Run from workspace root or use cargo run.".to_string())
203
203
  }
204
204
 
205
+ /// Path to `crates/tish_runtime` inside a locally installed `@tishlang/tish` npm package.
206
+ pub fn npm_package_runtime_path(project_root: &Path) -> Option<PathBuf> {
207
+ let p = project_root
208
+ .join("node_modules")
209
+ .join("@tishlang")
210
+ .join("tish")
211
+ .join("crates")
212
+ .join("tish_runtime");
213
+ if p.is_dir() {
214
+ Some(p)
215
+ } else {
216
+ None
217
+ }
218
+ }
219
+
205
220
  /// Find the path to the tishlang_runtime crate.
206
221
  ///
207
222
  /// Returns a canonical path string suitable for Cargo.toml path dependencies.
@@ -217,6 +232,24 @@ pub fn find_runtime_path() -> Result<String, String> {
217
232
  .map(|p| p.display().to_string().replace('\\', "/"))
218
233
  }
219
234
 
235
+ /// Resolve `tishlang_runtime` for a Cargo build, preferring the npm install under `project_root`.
236
+ ///
237
+ /// When a Tish app lives next to a checkout of the language repo (e.g. `…/tish/tish-cargo-example`),
238
+ /// [`find_workspace_root`] can return the checkout while `rustDependencies` point at
239
+ /// `node_modules/@tishlang/tish/crates/tish_core`. Using the npm tree for **both** runtime and shim
240
+ /// avoids Cargo lockfile "package collision" for the same crate name/version at two paths.
241
+ pub fn find_runtime_path_for_project(project_root: Option<&Path>) -> Result<String, String> {
242
+ if let Some(root) = project_root {
243
+ if let Some(rt) = npm_package_runtime_path(root) {
244
+ return rt
245
+ .canonicalize()
246
+ .map_err(|e| format!("Cannot canonicalize tishlang_runtime (npm): {}", e))
247
+ .map(|p| p.display().to_string().replace('\\', "/"));
248
+ }
249
+ }
250
+ find_runtime_path()
251
+ }
252
+
220
253
  /// Crate package name -> directory name (directories kept as tish_* for historical reasons).
221
254
  const CRATE_NAME_TO_DIR: &[(&str, &str)] = &[
222
255
  ("tishlang_runtime", "tish_runtime"),
@@ -22,3 +22,4 @@ tishlang_ui = { path = "../tish_ui", version = ">=0.1", default-features = false
22
22
  serde_json = "1.0"
23
23
 
24
24
  [dev-dependencies]
25
+ tempfile = "3"
@@ -393,19 +393,27 @@ pub fn compile_project(
393
393
  project_root: Option<&Path>,
394
394
  features: &[String],
395
395
  ) -> Result<String, CompileError> {
396
- let (rust, _, _) = compile_project_full(entry_path, project_root, features, true)?;
396
+ let (rust, _, _, _) = compile_project_full(entry_path, project_root, features, true)?;
397
397
  Ok(rust)
398
398
  }
399
399
 
400
- /// Compile a project and return Rust code, resolved native modules, and the **effective** feature list
401
- /// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports). Pass this list to
402
- /// `tishlang_runtime` when linking (e.g. `build_via_cargo`) so Cargo `features` match codegen.
400
+ /// Compile a project and return Rust code, resolved native modules, the **effective** feature list
401
+ /// (CLI features plus any inferred from `tish:fs` / `tish:http` / … imports), and native build
402
+ /// artifacts (Cargo dep lines, optional `generated_native.rs` source, init strategy per spec).
403
403
  pub fn compile_project_full(
404
404
  entry_path: &Path,
405
405
  project_root: Option<&Path>,
406
406
  features: &[String],
407
407
  optimize: bool,
408
- ) -> Result<(String, Vec<crate::resolve::ResolvedNativeModule>, Vec<String>), CompileError> {
408
+ ) -> Result<
409
+ (
410
+ String,
411
+ Vec<crate::resolve::ResolvedNativeModule>,
412
+ Vec<String>,
413
+ crate::resolve::NativeBuildArtifacts,
414
+ ),
415
+ CompileError,
416
+ > {
409
417
  use crate::resolve;
410
418
  let root = project_root.unwrap_or_else(|| entry_path.parent().unwrap_or(Path::new(".")));
411
419
  let modules = resolve::resolve_project(entry_path, project_root)
@@ -416,14 +424,23 @@ pub fn compile_project_full(
416
424
  .map_err(|e| CompileError { message: e, span: None })?;
417
425
  let native_modules = resolve::resolve_native_modules(&merged, root)
418
426
  .map_err(|e| CompileError { message: e, span: None })?;
427
+ let native_build = resolve::compute_native_build_artifacts(&merged, root, &native_modules)
428
+ .map_err(|e| CompileError { message: e, span: None })?;
419
429
  let mut all_features: Vec<String> = features.to_vec();
420
430
  for f in resolve::extract_native_import_features(&merged) {
421
431
  if !all_features.contains(&f) {
422
432
  all_features.push(f);
423
433
  }
424
434
  }
425
- let rust = compile_with_native_modules(&merged, project_root, &all_features, &native_modules, optimize)?;
426
- Ok((rust, native_modules, all_features))
435
+ let rust = compile_with_native_modules(
436
+ &merged,
437
+ project_root,
438
+ &all_features,
439
+ &native_modules,
440
+ &native_build.native_init,
441
+ optimize,
442
+ )?;
443
+ Ok((rust, native_modules, all_features, native_build))
427
444
  }
428
445
 
429
446
  /// Compile with explicit feature flags. When features are provided, codegen uses them
@@ -433,7 +450,8 @@ pub fn compile_with_features(
433
450
  project_root: Option<&Path>,
434
451
  features: &[String],
435
452
  ) -> Result<String, CompileError> {
436
- compile_with_native_modules(program, project_root, features, &[], true)
453
+ let empty = std::collections::HashMap::new();
454
+ compile_with_native_modules(program, project_root, features, &[], &empty, true)
437
455
  }
438
456
 
439
457
  /// Compile with resolved native modules. Native imports emit calls to the module crates directly.
@@ -442,16 +460,30 @@ pub fn compile_with_native_modules(
442
460
  project_root: Option<&Path>,
443
461
  features: &[String],
444
462
  native_modules: &[crate::resolve::ResolvedNativeModule],
463
+ native_init: &std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
445
464
  optimize: bool,
446
465
  ) -> Result<String, CompileError> {
447
466
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
448
467
  // Type-inference pass: fills in `type_ann` on unannotated VarDecl nodes where
449
468
  // the type is unambiguous (literals, arithmetic of typed vars, etc.).
450
469
  let program = crate::infer::infer_program(&program);
451
- let map: std::collections::HashMap<String, (String, String)> = native_modules
452
- .iter()
453
- .map(|m| (m.spec.clone(), (m.crate_name.clone(), m.export_fn.clone())))
454
- .collect();
470
+ let map: std::collections::HashMap<String, crate::resolve::NativeModuleInit> =
471
+ if native_init.is_empty() {
472
+ native_modules
473
+ .iter()
474
+ .map(|m| {
475
+ (
476
+ m.spec.clone(),
477
+ crate::resolve::NativeModuleInit::Legacy {
478
+ crate_name: m.crate_name.clone(),
479
+ export_fn: m.export_fn.clone(),
480
+ },
481
+ )
482
+ })
483
+ .collect()
484
+ } else {
485
+ native_init.clone()
486
+ };
455
487
  let mut g = Codegen::new_with_native_modules(project_root, features, map);
456
488
  g.emit_program(&program)?;
457
489
  Ok(g.output)
@@ -465,8 +497,8 @@ struct Codegen {
465
497
  project_root: Option<std::path::PathBuf>,
466
498
  /// Requested features (http, process, fs, regex, polars). When non-empty, used instead of #[cfg].
467
499
  features: std::collections::HashSet<String>,
468
- /// spec -> (crate_name, export_fn) for native modules resolved via package.json
469
- native_module_map: std::collections::HashMap<String, (String, String)>,
500
+ /// spec -> native init strategy (legacy adapter object vs generated `generated_native` wrapper)
501
+ native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
470
502
  /// Stack: true = async Rust context (run body), false = sync closure (Tish fn body)
471
503
  async_context_stack: Vec<bool>,
472
504
  loop_stack: Vec<(String, Option<String>)>, // (break_label, continue_update) for innermost loop
@@ -497,7 +529,7 @@ impl Codegen {
497
529
  fn new_with_native_modules(
498
530
  project_root: Option<&Path>,
499
531
  features: &[String],
500
- native_module_map: std::collections::HashMap<String, (String, String)>,
532
+ native_module_init: std::collections::HashMap<String, crate::resolve::NativeModuleInit>,
501
533
  ) -> Self {
502
534
  let features: std::collections::HashSet<String> = features.iter().cloned().collect();
503
535
  Self {
@@ -507,7 +539,7 @@ impl Codegen {
507
539
  is_async: false,
508
540
  project_root: project_root.map(|p| p.to_path_buf()),
509
541
  features,
510
- native_module_map,
542
+ native_module_init,
511
543
  async_context_stack: Vec::new(),
512
544
  loop_stack: Vec::new(),
513
545
  function_scope_stack: vec![Vec::new()], // Start with global scope
@@ -540,12 +572,21 @@ impl Codegen {
540
572
  if is_builtin_native_spec(spec) {
541
573
  return self.builtin_native_module_rust_init(spec, export_name);
542
574
  }
543
- self.native_module_map.get(spec).map(|(crate_name, export_fn)| {
575
+ self.native_module_init.get(spec).map(|init| {
544
576
  // Native modules return a namespace object (like an ES module).
545
577
  // Named imports extract the field from that namespace: `import { foo } from "pkg"` → `ns.foo`.
578
+ let init_expr = match init {
579
+ crate::resolve::NativeModuleInit::Legacy {
580
+ crate_name,
581
+ export_fn,
582
+ } => format!("{}::{}()", crate_name, export_fn),
583
+ crate::resolve::NativeModuleInit::Generated { export_fn, .. } => {
584
+ format!("crate::generated_native::{}()", export_fn)
585
+ }
586
+ };
546
587
  format!(
547
- "{{ let _ns = {}::{}(); match _ns {{ Value::Object(ref _o) => _o.borrow().get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
548
- crate_name, export_fn, export_name
588
+ "{{ let _ns = {}; match _ns {{ Value::Object(ref _o) => _o.borrow().get({:?}).cloned().unwrap_or(Value::Null), _ => Value::Null }} }}",
589
+ init_expr, export_name
549
590
  )
550
591
  })
551
592
  }
@@ -1779,7 +1820,15 @@ impl Codegen {
1779
1820
  }
1780
1821
  Expr::Call { callee, args, .. } => {
1781
1822
  // Compile-time embed: Polars.read_csv("<literal path>") when file exists
1782
- if let Some((crate_name, _)) = self.native_module_map.get("tish:polars") {
1823
+ if let Some(init) = self.native_module_init.get("tish:polars") {
1824
+ let crate_name = match init {
1825
+ crate::resolve::NativeModuleInit::Legacy { crate_name, .. } => {
1826
+ crate_name.as_str()
1827
+ }
1828
+ crate::resolve::NativeModuleInit::Generated { shim_crate, .. } => {
1829
+ shim_crate.as_str()
1830
+ }
1831
+ };
1783
1832
  if let (Some(root), Some(CallArg::Expr(first_arg))) =
1784
1833
  (self.project_root.as_ref(), args.first())
1785
1834
  {
@@ -13,9 +13,12 @@ pub use codegen::{
13
13
  };
14
14
  pub use codegen::CompileError;
15
15
  pub use resolve::{
16
- detect_cycles, extract_native_import_features, has_external_native_imports, has_native_imports,
17
- is_builtin_native_spec, merge_modules, resolve_native_modules, resolve_project,
18
- resolve_project_from_stdin, ResolvedNativeModule,
16
+ cargo_export_fn_name, compute_native_build_artifacts, detect_cycles, export_name_to_rust_ident,
17
+ extract_native_import_features, format_rust_dependencies_toml, generate_native_wrapper_rs,
18
+ has_external_native_imports, has_native_imports, infer_native_module_exports,
19
+ is_builtin_native_spec, is_cargo_native_spec, merge_modules, read_project_tish_config,
20
+ resolve_native_modules, resolve_project, resolve_project_from_stdin, NativeBuildArtifacts,
21
+ NativeModuleInit, ResolvedNativeModule,
19
22
  };
20
23
  pub use types::{RustType, TypeContext};
21
24
 
@@ -108,7 +111,7 @@ fn factory() {
108
111
  .into_iter()
109
112
  .map(String::from)
110
113
  .collect::<Vec<_>>();
111
- let (rust, _, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
114
+ let (rust, _, _, _) = compile_project_full(&bench, bench.parent(), &features, true).unwrap();
112
115
  // outerVar = 42 is inferred as f64; f64 is Copy so no .clone() is emitted.
113
116
  assert!(
114
117
  rust.contains("let mut outerVar: f64"),
@@ -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,6 +72,7 @@ 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.
75
+ /// Handles both lowered `NativeModuleLoad` (merged modules) and raw `import { … } from 'tish:…'`.
48
76
  pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<Vec<ResolvedNativeModule>, String> {
49
77
  let root_canon = project_root
50
78
  .canonicalize()
@@ -52,25 +80,93 @@ pub fn resolve_native_modules(program: &Program, project_root: &Path) -> Result<
52
80
  let mut seen = HashSet::new();
53
81
  let mut modules = Vec::new();
54
82
  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
83
+ let specs: Vec<String> = match stmt {
84
+ Statement::VarDecl {
85
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
86
+ ..
87
+ } => vec![spec.as_ref().to_string()],
88
+ Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
89
+ vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
90
+ }
91
+ _ => continue,
92
+ };
93
+ for s in specs {
94
+ if is_builtin_native_spec(&s) {
95
+ continue;
63
96
  }
64
- if !seen.insert(s.to_string()) {
97
+ if !seen.insert(s.clone()) {
65
98
  continue;
66
99
  }
67
- let m = resolve_native_module(s, &root_canon)?;
100
+ let m = if s.starts_with("cargo:") {
101
+ resolve_cargo_native_module(&s, &root_canon)?
102
+ } else {
103
+ resolve_native_module(&s, &root_canon)?
104
+ };
68
105
  modules.push(m);
69
106
  }
70
107
  }
71
108
  Ok(modules)
72
109
  }
73
110
 
111
+ /// True for `cargo:…` specs (Cargo-backed imports; Rust native backend only).
112
+ pub fn is_cargo_native_spec(spec: &str) -> bool {
113
+ spec.starts_with("cargo:")
114
+ }
115
+
116
+ /// Stable Rust symbol for the generated namespace function, e.g. `cargo:my-crate` → `cargo_native_my_crate_object`.
117
+ pub fn cargo_export_fn_name(spec: &str) -> String {
118
+ let tail = spec.strip_prefix("cargo:").unwrap_or(spec);
119
+ let mut out = String::from("cargo_native_");
120
+ for c in tail.chars() {
121
+ if c.is_ascii_alphanumeric() {
122
+ out.push(c);
123
+ } else {
124
+ out.push('_');
125
+ }
126
+ }
127
+ if out == "cargo_native_" {
128
+ out.push_str("unnamed");
129
+ }
130
+ out.push_str("_object");
131
+ out
132
+ }
133
+
134
+ fn resolve_cargo_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
135
+ let tail = spec
136
+ .strip_prefix("cargo:")
137
+ .ok_or_else(|| format!("Invalid cargo native spec: {}", spec))?;
138
+ if tail.is_empty() {
139
+ return Err("cargo: import needs a dependency name, e.g. import { x } from 'cargo:serde_json'".into());
140
+ }
141
+ let dep_key = tail.to_string();
142
+ let tish = read_project_tish_config(project_root);
143
+ let rust_deps = tish.get("rustDependencies").and_then(|v| v.as_object()).ok_or_else(|| {
144
+ format!(
145
+ "cargo:{} requires package.json \"tish\": {{ \"rustDependencies\": {{ \"{}\": \"…\" }} }}",
146
+ tail, dep_key
147
+ )
148
+ })?;
149
+ if !rust_deps.contains_key(&dep_key) {
150
+ return Err(format!(
151
+ "cargo:{}: add \"{}\" to tish.rustDependencies in package.json (version string or inline table)",
152
+ tail, dep_key
153
+ ));
154
+ }
155
+ let crate_name = dep_key.replace('-', "_");
156
+ let export_fn = cargo_export_fn_name(spec);
157
+ let crate_path = project_root
158
+ .canonicalize()
159
+ .unwrap_or_else(|_| project_root.to_path_buf());
160
+ Ok(ResolvedNativeModule {
161
+ spec: spec.to_string(),
162
+ package_name: dep_key.clone(),
163
+ crate_name,
164
+ crate_path,
165
+ export_fn,
166
+ use_path_dependency: false,
167
+ })
168
+ }
169
+
74
170
  fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNativeModule, String> {
75
171
  let package_name = if spec.starts_with("tish:") {
76
172
  format!("tish-{}", spec.strip_prefix("tish:").unwrap_or(spec))
@@ -110,6 +206,236 @@ fn resolve_native_module(spec: &str, project_root: &Path) -> Result<ResolvedNati
110
206
  crate_name: raw_crate.replace('-', "_"),
111
207
  crate_path,
112
208
  export_fn,
209
+ use_path_dependency: true,
210
+ })
211
+ }
212
+
213
+ /// Read the `tish` object from the project root `package.json` (empty JSON object if missing).
214
+ pub fn read_project_tish_config(project_root: &Path) -> serde_json::Value {
215
+ let path = project_root.join("package.json");
216
+ let Ok(content) = std::fs::read_to_string(&path) else {
217
+ return serde_json::json!({});
218
+ };
219
+ let Ok(json) = serde_json::from_str::<serde_json::Value>(&content) else {
220
+ return serde_json::json!({});
221
+ };
222
+ json.get("tish").cloned().unwrap_or_else(|| serde_json::json!({}))
223
+ }
224
+
225
+ fn resolve_cargo_path_for_toml(project_root: &Path, raw: &str) -> String {
226
+ let p = Path::new(raw);
227
+ let resolved = if p.is_absolute() {
228
+ p.to_path_buf()
229
+ } else {
230
+ project_root.join(p)
231
+ };
232
+ let resolved = resolved.canonicalize().unwrap_or(resolved);
233
+ resolved.display().to_string().replace('\\', "/")
234
+ }
235
+
236
+ fn json_to_cargo_inline_value(v: &serde_json::Value, project_root: &Path) -> Result<String, String> {
237
+ match v {
238
+ serde_json::Value::String(s) => Ok(format!("{:?}", s.as_str())),
239
+ serde_json::Value::Bool(b) => Ok(b.to_string()),
240
+ serde_json::Value::Number(n) => Ok(n.to_string()),
241
+ serde_json::Value::Array(arr) => {
242
+ let mut inner = Vec::new();
243
+ for item in arr {
244
+ inner.push(json_to_cargo_inline_value(item, project_root)?);
245
+ }
246
+ Ok(format!("[{}]", inner.join(", ")))
247
+ }
248
+ serde_json::Value::Object(map) => {
249
+ let mut parts = Vec::new();
250
+ for (k, v) in map {
251
+ let rhs = if k == "path" && v.as_str().is_some() {
252
+ let s = v.as_str().unwrap();
253
+ format!("{:?}", resolve_cargo_path_for_toml(project_root, s))
254
+ } else {
255
+ json_to_cargo_inline_value(v, project_root)?
256
+ };
257
+ parts.push(format!("{} = {}", k, rhs));
258
+ }
259
+ Ok(format!("{{ {} }}", parts.join(", ")))
260
+ }
261
+ serde_json::Value::Null => Err("null is not valid in a Cargo dependency value".to_string()),
262
+ }
263
+ }
264
+
265
+ /// Serialize `tish.rustDependencies` from project `package.json` into Cargo.toml `[dependencies]` lines.
266
+ /// Relative `path = "…"` entries in inline tables are resolved against `project_root` so the temp build crate can find them.
267
+ pub fn format_rust_dependencies_toml(tish: &serde_json::Value, project_root: &Path) -> Result<String, String> {
268
+ let Some(obj) = tish.get("rustDependencies").and_then(|v| v.as_object()) else {
269
+ return Ok(String::new());
270
+ };
271
+ let mut out = String::new();
272
+ for (name, val) in obj {
273
+ match val {
274
+ serde_json::Value::String(_) | serde_json::Value::Object(_) => {
275
+ out.push_str(&format!(
276
+ "{} = {}\n",
277
+ name,
278
+ json_to_cargo_inline_value(val, project_root)?
279
+ ));
280
+ }
281
+ _ => {
282
+ return Err(format!(
283
+ "tish.rustDependencies.{} must be a string (version) or object (inline table)",
284
+ name
285
+ ));
286
+ }
287
+ }
288
+ }
289
+ Ok(out)
290
+ }
291
+
292
+ /// Map a Tish export name to a Rust identifier (e.g. `readFile` → `read_file`) for shim crate symbols.
293
+ pub fn export_name_to_rust_ident(export_name: &str) -> String {
294
+ let mut out = String::new();
295
+ for (i, c) in export_name.chars().enumerate() {
296
+ if c.is_uppercase() && i > 0 {
297
+ out.push('_');
298
+ }
299
+ for lower in c.to_lowercase() {
300
+ out.push(lower);
301
+ }
302
+ }
303
+ if out.is_empty() {
304
+ "native_export".to_string()
305
+ } else {
306
+ out
307
+ }
308
+ }
309
+
310
+ /// Collect `(spec, export_name)` for every non-builtin native import in the program.
311
+ pub fn infer_native_module_exports(program: &Program) -> HashMap<String, HashSet<String>> {
312
+ let mut map: HashMap<String, HashSet<String>> = HashMap::new();
313
+ for stmt in &program.statements {
314
+ match stmt {
315
+ Statement::VarDecl {
316
+ init: Some(Expr::NativeModuleLoad { spec, export_name, .. }),
317
+ ..
318
+ } => {
319
+ let s = spec.as_ref();
320
+ if is_builtin_native_spec(s) {
321
+ continue;
322
+ }
323
+ map.entry(s.to_string())
324
+ .or_default()
325
+ .insert(export_name.to_string());
326
+ }
327
+ Statement::Import { specifiers, from, .. } if is_native_import(from.as_ref()) => {
328
+ let spec = normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string());
329
+ if is_builtin_native_spec(&spec) {
330
+ continue;
331
+ }
332
+ for sp in specifiers {
333
+ if let ImportSpecifier::Named { name, .. } = sp {
334
+ map.entry(spec.clone())
335
+ .or_default()
336
+ .insert(name.to_string());
337
+ }
338
+ }
339
+ }
340
+ _ => {}
341
+ }
342
+ }
343
+ map
344
+ }
345
+
346
+ /// Emit `generated_native.rs` for [`NativeModuleInit::Generated`] modules.
347
+ pub fn generate_native_wrapper_rs(
348
+ modules: &[ResolvedNativeModule],
349
+ inferred: &HashMap<String, HashSet<String>>,
350
+ init_by_spec: &HashMap<String, NativeModuleInit>,
351
+ ) -> String {
352
+ let mut file = String::from(
353
+ "//! Generated by `tish build` — do not edit.\n\
354
+ use std::cell::RefCell;\n\
355
+ use std::rc::Rc;\n\
356
+ use std::sync::Arc;\n\
357
+ use tishlang_runtime::{ObjectMap, Value};\n\n",
358
+ );
359
+ let mut any = false;
360
+ for m in modules {
361
+ let Some(NativeModuleInit::Generated { shim_crate, export_fn }) = init_by_spec.get(&m.spec) else {
362
+ continue;
363
+ };
364
+ let Some(names) = inferred.get(&m.spec) else {
365
+ continue;
366
+ };
367
+ if names.is_empty() {
368
+ continue;
369
+ }
370
+ any = true;
371
+ let mut keys: Vec<_> = names.iter().cloned().collect();
372
+ keys.sort();
373
+ file.push_str(&format!("pub fn {}() -> Value {{\n", export_fn));
374
+ file.push_str(" let mut m = ObjectMap::default();\n");
375
+ for export_name in keys {
376
+ let rust_fn = export_name_to_rust_ident(&export_name);
377
+ let key_lit = format!("{:?}", export_name);
378
+ file.push_str(&format!(
379
+ " m.insert(Arc::from({}), Value::Function(Rc::new(|args: &[Value]| {{\n {}::{}(args)\n }})));\n",
380
+ key_lit, shim_crate, rust_fn
381
+ ));
382
+ }
383
+ file.push_str(" Value::Object(Rc::new(RefCell::new(m)))\n}\n\n");
384
+ }
385
+ if !any {
386
+ return String::new();
387
+ }
388
+ file
389
+ }
390
+
391
+ /// Combine project `package.json`, inferred exports, and resolved native modules into build artifacts.
392
+ pub fn compute_native_build_artifacts(
393
+ program: &Program,
394
+ project_root: &Path,
395
+ native_modules: &[ResolvedNativeModule],
396
+ ) -> Result<NativeBuildArtifacts, String> {
397
+ let tish = read_project_tish_config(project_root);
398
+ let rust_dependencies_toml = format_rust_dependencies_toml(&tish, project_root)?;
399
+ let inferred = infer_native_module_exports(program);
400
+ let gen_tish = tish
401
+ .get("generateNativeWrapper")
402
+ .and_then(|v| v.as_bool())
403
+ .unwrap_or(false);
404
+
405
+ let mut native_init: HashMap<String, NativeModuleInit> = HashMap::new();
406
+ for m in native_modules {
407
+ let use_gen = if is_cargo_native_spec(&m.spec) {
408
+ inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
409
+ } else {
410
+ gen_tish && inferred.get(&m.spec).map(|s| !s.is_empty()).unwrap_or(false)
411
+ };
412
+ let init = if use_gen {
413
+ NativeModuleInit::Generated {
414
+ shim_crate: m.crate_name.clone(),
415
+ export_fn: m.export_fn.clone(),
416
+ }
417
+ } else {
418
+ NativeModuleInit::Legacy {
419
+ crate_name: m.crate_name.clone(),
420
+ export_fn: m.export_fn.clone(),
421
+ }
422
+ };
423
+ native_init.insert(m.spec.clone(), init);
424
+ }
425
+
426
+ let generated_native_rs = {
427
+ let s = generate_native_wrapper_rs(native_modules, &inferred, &native_init);
428
+ if s.trim().is_empty() {
429
+ None
430
+ } else {
431
+ Some(s)
432
+ }
433
+ };
434
+
435
+ Ok(NativeBuildArtifacts {
436
+ rust_dependencies_toml,
437
+ generated_native_rs,
438
+ native_init,
113
439
  })
114
440
  }
115
441
 
@@ -151,16 +477,25 @@ fn read_package_name(pkg_path: &Path) -> Option<String> {
151
477
  json.get("name").and_then(|v| v.as_str()).map(String::from)
152
478
  }
153
479
 
480
+ fn stmt_native_specs(stmt: &Statement) -> Vec<String> {
481
+ match stmt {
482
+ Statement::VarDecl {
483
+ init: Some(Expr::NativeModuleLoad { spec, .. }),
484
+ ..
485
+ } => vec![spec.to_string()],
486
+ Statement::Import { from, .. } if is_native_import(from.as_ref()) => {
487
+ vec![normalize_builtin_spec(from.as_ref()).unwrap_or_else(|| from.to_string())]
488
+ }
489
+ _ => vec![],
490
+ }
491
+ }
492
+
154
493
  /// Extract Cargo feature names from native imports in a merged program.
155
494
  /// Used to enable tishlang_runtime features based on `import { x } from 'tish:egui'` etc.
156
495
  pub fn extract_native_import_features(program: &Program) -> Vec<String> {
157
496
  let mut features = std::collections::HashSet::new();
158
497
  for stmt in &program.statements {
159
- if let Statement::VarDecl {
160
- init: Some(Expr::NativeModuleLoad { spec, .. }),
161
- ..
162
- } = stmt
163
- {
498
+ for spec in stmt_native_specs(stmt) {
164
499
  if let Some(f) = native_spec_to_feature(spec.as_ref()) {
165
500
  features.insert(f);
166
501
  }
@@ -171,27 +506,17 @@ pub fn extract_native_import_features(program: &Program) -> Vec<String> {
171
506
 
172
507
  /// Returns true if the merged program contains native imports (tish:*, @scope/pkg).
173
508
  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
509
+ program
510
+ .statements
511
+ .iter()
512
+ .any(|stmt| !stmt_native_specs(stmt).is_empty())
184
513
  }
185
514
 
186
515
  /// Returns true if the merged program contains external native imports (not built-in tish:fs/http/process).
187
516
  /// Cranelift/LLVM reject these; bytecode VM supports built-ins only.
188
517
  pub fn has_external_native_imports(program: &Program) -> bool {
189
518
  for stmt in &program.statements {
190
- if let Statement::VarDecl {
191
- init: Some(Expr::NativeModuleLoad { spec, .. }),
192
- ..
193
- } = stmt
194
- {
519
+ for spec in stmt_native_specs(stmt) {
195
520
  if !is_builtin_native_spec(spec.as_ref()) {
196
521
  return true;
197
522
  }
@@ -267,10 +592,10 @@ pub fn resolve_project_from_stdin(
267
592
 
268
593
  for stmt in &program.statements {
269
594
  if let Statement::Import { from, .. } = stmt {
270
- if is_native_import(from) {
595
+ if is_native_import(from.as_ref()) {
271
596
  continue;
272
597
  }
273
- let dep_path = resolve_import_path(from, from_dir, &root_canon)?;
598
+ let dep_path = resolve_import_path(from.as_ref(), from_dir, &root_canon)?;
274
599
  if !path_to_module.contains_key(&dep_path) {
275
600
  load_module_recursive(
276
601
  &dep_path,
@@ -320,10 +645,10 @@ fn load_module_recursive(
320
645
  let dir = canonical.parent().unwrap_or(Path::new("."));
321
646
  for stmt in &program.statements {
322
647
  if let Statement::Import { from, .. } = stmt {
323
- if is_native_import(from) {
648
+ if is_native_import(from.as_ref()) {
324
649
  continue; // Native imports don't load files
325
650
  }
326
- let dep_path = resolve_import_path(from, dir, project_root)?;
651
+ let dep_path = resolve_import_path(from.as_ref(), dir, project_root)?;
327
652
  if !path_to_module.contains_key(&dep_path) {
328
653
  load_module_recursive(
329
654
  &dep_path,
@@ -344,9 +669,11 @@ fn load_module_recursive(
344
669
  /// Returns true for native module imports that don't resolve to files.
345
670
  /// - fs, http, process, ws (Node-compatible aliases for tish:fs, tish:http, tish:process, tish:ws)
346
671
  /// - tish:egui, tish:polars, etc.
672
+ /// - cargo:… (Cargo `rustDependencies` + generated wrapper; Rust native backend)
347
673
  /// - @scope/package (npm-style)
348
674
  pub fn is_native_import(spec: &str) -> bool {
349
675
  spec.starts_with("tish:")
676
+ || spec.starts_with("cargo:")
350
677
  || spec.starts_with('@')
351
678
  || matches!(spec, "fs" | "http" | "process" | "ws")
352
679
  }
@@ -476,10 +803,10 @@ fn has_cycle_from(
476
803
  ) -> Result<bool, String> {
477
804
  for stmt in &program.statements {
478
805
  if let Statement::Import { from, .. } = stmt {
479
- if is_native_import(from) {
806
+ if is_native_import(from.as_ref()) {
480
807
  continue;
481
808
  }
482
- let dep_path = resolve_import_path(from, from_dir, Path::new("."))?;
809
+ let dep_path = resolve_import_path(from.as_ref(), from_dir, Path::new("."))?;
483
810
  if let Some(&dep_idx) = path_to_idx.get(&dep_path) {
484
811
  if stack.contains(&dep_idx) {
485
812
  stack.push(dep_idx);
@@ -548,10 +875,11 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
548
875
  for stmt in &module.program.statements {
549
876
  match stmt {
550
877
  Statement::Import { specifiers, from, span } => {
551
- if is_native_import(from) {
878
+ if is_native_import(from.as_ref()) {
552
879
  // Normalize fs/http/process -> tish:fs etc. for Node compatibility
553
880
  let canonical_spec =
554
- normalize_builtin_spec(from).unwrap_or_else(|| from.to_string());
881
+ normalize_builtin_spec(from.as_ref())
882
+ .unwrap_or_else(|| from.to_string());
555
883
  // Emit VarDecl with NativeModuleLoad for each specifier
556
884
  for spec in specifiers {
557
885
  match spec {
@@ -589,7 +917,7 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
589
917
  }
590
918
  continue;
591
919
  }
592
- let dep_path = resolve_import_path(from, dir, Path::new("."))?;
920
+ let dep_path = resolve_import_path(from.as_ref(), dir, Path::new("."))?;
593
921
  let dep_path = dep_path
594
922
  .canonicalize()
595
923
  .unwrap_or(dep_path);
@@ -682,3 +1010,53 @@ pub fn merge_modules(modules: Vec<ResolvedModule>) -> Result<Program, String> {
682
1010
  }
683
1011
  Ok(Program { statements })
684
1012
  }
1013
+
1014
+ #[cfg(test)]
1015
+ mod cargo_spec_tests {
1016
+ use std::sync::Arc;
1017
+
1018
+ use super::cargo_export_fn_name;
1019
+ use super::is_native_import;
1020
+
1021
+ #[test]
1022
+ fn is_native_import_accepts_arc_str_ref() {
1023
+ let from: &Arc<str> = &Arc::from("cargo:demo_shim");
1024
+ assert!(is_native_import(from));
1025
+ }
1026
+
1027
+ #[test]
1028
+ fn detect_cycles_skips_cargo_import() {
1029
+ use super::{detect_cycles, resolve_project};
1030
+ let dir = tempfile::tempdir().expect("tempdir");
1031
+ let p = dir.path().join("main.tish");
1032
+ let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
1033
+ std::fs::write(&p, src).unwrap();
1034
+ let root = dir.path();
1035
+ let modules = resolve_project(&p, Some(root)).unwrap();
1036
+ detect_cycles(&modules).unwrap();
1037
+ }
1038
+
1039
+ #[test]
1040
+ fn merge_modules_skips_cargo_import() {
1041
+ use super::{merge_modules, resolve_project};
1042
+ let dir = tempfile::tempdir().expect("tempdir");
1043
+ let p = dir.path().join("main.tish");
1044
+ let src = "import { greet } from 'cargo:demo_shim'\nconsole.log(1)\n";
1045
+ std::fs::write(&p, src).unwrap();
1046
+ let root = dir.path();
1047
+ let modules = resolve_project(&p, Some(root)).unwrap();
1048
+ merge_modules(modules).unwrap();
1049
+ }
1050
+
1051
+ #[test]
1052
+ fn cargo_export_fn_name_sanitizes() {
1053
+ assert_eq!(
1054
+ cargo_export_fn_name("cargo:serde_json"),
1055
+ "cargo_native_serde_json_object"
1056
+ );
1057
+ assert_eq!(
1058
+ cargo_export_fn_name("cargo:my-crate"),
1059
+ "cargo_native_my_crate_object"
1060
+ );
1061
+ }
1062
+ }
@@ -33,6 +33,7 @@ fn normalize_builtin_spec(spec: &str) -> Option<String> {
33
33
 
34
34
  fn is_native_import(spec: &str) -> bool {
35
35
  spec.starts_with("tish:")
36
+ || spec.starts_with("cargo:")
36
37
  || spec.starts_with('@')
37
38
  || matches!(spec, "fs" | "http" | "process" | "ws")
38
39
  }
@@ -512,6 +512,11 @@ impl Evaluator {
512
512
 
513
513
  /// Load and evaluate a module, returning its exports object. Uses cache.
514
514
  fn load_module(&mut self, from: &str) -> Result<Value, EvalError> {
515
+ if from.starts_with("cargo:") {
516
+ return Err(EvalError::Error(
517
+ "cargo:… imports are only supported by `tish build` with the Rust native backend.".into(),
518
+ ));
519
+ }
515
520
  if from.starts_with("tish:") {
516
521
  return self.load_builtin_module(from);
517
522
  }
@@ -599,6 +604,11 @@ impl Evaluator {
599
604
 
600
605
  /// Load built-in module (tish:fs, tish:http, tish:process, …) or a virtual module from native crates.
601
606
  fn load_builtin_module(&self, spec: &str) -> Result<Value, EvalError> {
607
+ if spec.starts_with("cargo:") {
608
+ return Err(EvalError::Error(
609
+ "cargo:… imports are only supported when compiling with `tish build` and the Rust native backend. They link Cargo crates via package.json tish.rustDependencies and a generated native wrapper — not the interpreter or VM.".into(),
610
+ ));
611
+ }
602
612
  if let Some(v) = self.virtual_builtins.borrow().get(spec) {
603
613
  return Ok(v.clone());
604
614
  }
@@ -28,11 +28,24 @@ fn runtime_features_for_cargo(features: &[String]) -> Vec<String> {
28
28
  out
29
29
  }
30
30
 
31
+ /// Inject `mod generated_native;` after the crate attribute so the binary crate can call `crate::generated_native::…`.
32
+ fn inject_generated_native_mod(rust_code: &str) -> String {
33
+ if let Some(pos) = rust_code.find("\n\n") {
34
+ let (a, b) = rust_code.split_at(pos + 2);
35
+ format!("{}mod generated_native;\n{}", a, b)
36
+ } else {
37
+ format!("{}\n\nmod generated_native;\n", rust_code)
38
+ }
39
+ }
40
+
31
41
  pub fn build_via_cargo(
32
42
  rust_code: &str,
33
43
  native_modules: Vec<ResolvedNativeModule>,
34
44
  output_path: &Path,
35
45
  features: &[String],
46
+ extra_dependencies_toml: &str,
47
+ generated_native_rs: Option<&str>,
48
+ project_root: Option<&Path>,
36
49
  ) -> Result<(), String> {
37
50
  let out_name = output_path
38
51
  .file_stem()
@@ -40,7 +53,7 @@ pub fn build_via_cargo(
40
53
  .unwrap_or("tish_out");
41
54
  let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_name)?;
42
55
 
43
- let runtime_path = tishlang_build_utils::find_runtime_path()?;
56
+ let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
44
57
 
45
58
  let runtime_features = runtime_features_for_cargo(features);
46
59
  let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
@@ -59,12 +72,28 @@ pub fn build_via_cargo(
59
72
 
60
73
  let native_deps: String = native_modules
61
74
  .iter()
75
+ .filter(|m| m.use_path_dependency)
62
76
  .map(|m| {
63
77
  let path = m.crate_path.display().to_string().replace('\\', "/");
64
78
  format!("{} = {{ path = {:?} }}\n", m.package_name, path)
65
79
  })
66
80
  .collect();
67
81
 
82
+ let mut more_deps = String::new();
83
+ more_deps.push_str(&tokio_dep);
84
+ if !native_deps.is_empty() {
85
+ more_deps.push_str(&format!("\n{}", native_deps));
86
+ }
87
+ if !extra_dependencies_toml.trim().is_empty() {
88
+ more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
89
+ }
90
+
91
+ let rust_main = if generated_native_rs.is_some() {
92
+ inject_generated_native_mod(rust_code)
93
+ } else {
94
+ rust_code.to_string()
95
+ };
96
+
68
97
  let tish_ui_path = std::path::Path::new(&runtime_path)
69
98
  .parent()
70
99
  .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
@@ -96,23 +125,22 @@ codegen-units = 1
96
125
  lto = "thin"
97
126
 
98
127
  [dependencies]
99
- tishlang_runtime = {{ path = {:?}{} }}{}{}{}
100
- "#,
128
+ tishlang_runtime = {{ path = {:?}{} }}
129
+ {}{}"#,
101
130
  out_name,
102
131
  runtime_path,
103
132
  features_str,
104
- tokio_dep,
105
- if native_deps.is_empty() {
106
- String::new()
107
- } else {
108
- format!("\n{}", native_deps)
109
- },
133
+ more_deps,
110
134
  ui_dep
111
135
  );
112
136
 
113
137
  fs::write(build_dir.join("Cargo.toml"), cargo_toml)
114
138
  .map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
115
- fs::write(build_dir.join("src/main.rs"), rust_code)
139
+ if let Some(gen) = generated_native_rs {
140
+ fs::write(build_dir.join("src/generated_native.rs"), gen)
141
+ .map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
142
+ }
143
+ fs::write(build_dir.join("src/main.rs"), rust_main)
116
144
  .map_err(|e| format!("Cannot write main.rs: {}", e))?;
117
145
 
118
146
  let workspace_target = Path::new(&runtime_path)
@@ -57,21 +57,25 @@ pub fn compile_to_native(
57
57
 
58
58
  match backend {
59
59
  Backend::Rust => {
60
- let (rust_code, native_modules, effective_features) = tishlang_compile::compile_project_full(
61
- entry_path,
62
- project_root,
63
- features,
64
- optimize,
65
- )
66
- .map_err(|e| NativeError {
67
- message: e.to_string(),
68
- })?;
60
+ let (rust_code, native_modules, effective_features, native_build) =
61
+ tishlang_compile::compile_project_full(
62
+ entry_path,
63
+ project_root,
64
+ features,
65
+ optimize,
66
+ )
67
+ .map_err(|e| NativeError {
68
+ message: e.to_string(),
69
+ })?;
69
70
 
70
71
  crate::build::build_via_cargo(
71
72
  &rust_code,
72
73
  native_modules,
73
74
  output_path,
74
75
  &effective_features,
76
+ &native_build.rust_dependencies_toml,
77
+ native_build.generated_native_rs.as_deref(),
78
+ project_root,
75
79
  )
76
80
  .map_err(|e| NativeError { message: e })
77
81
  }
@@ -96,7 +100,7 @@ pub fn compile_to_native(
96
100
 
97
101
  if tishlang_compile::has_external_native_imports(&program) {
98
102
  return Err(NativeError {
99
- message: "Cranelift backend does not support external native imports (tish:egui, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.".to_string(),
103
+ message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported. Use --native-backend rust for external modules.".to_string(),
100
104
  });
101
105
  }
102
106
 
@@ -132,7 +136,7 @@ pub fn compile_to_native(
132
136
  };
133
137
  if tishlang_compile::has_external_native_imports(&program) {
134
138
  return Err(NativeError {
135
- message: "LLVM backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
139
+ message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
136
140
  });
137
141
  }
138
142
  let chunk = if optimize {
@@ -180,6 +184,8 @@ pub fn compile_program_to_native(
180
184
  let root = project_root.unwrap_or_else(|| Path::new("."));
181
185
  let native_modules = tishlang_compile::resolve_native_modules(&program, root)
182
186
  .map_err(|e| NativeError { message: e })?;
187
+ let native_build = tishlang_compile::compute_native_build_artifacts(&program, root, &native_modules)
188
+ .map_err(|e| NativeError { message: e })?;
183
189
  let mut all_features = features.to_vec();
184
190
  for f in tishlang_compile::extract_native_import_features(&program) {
185
191
  if !all_features.contains(&f) {
@@ -191,18 +197,27 @@ pub fn compile_program_to_native(
191
197
  project_root,
192
198
  &all_features,
193
199
  &native_modules,
200
+ &native_build.native_init,
194
201
  optimize,
195
202
  )
196
203
  .map_err(|e| NativeError {
197
204
  message: e.message,
198
205
  })?;
199
- crate::build::build_via_cargo(&rust_code, native_modules, output_path, &all_features)
200
- .map_err(|e| NativeError { message: e })
206
+ crate::build::build_via_cargo(
207
+ &rust_code,
208
+ native_modules,
209
+ output_path,
210
+ &all_features,
211
+ &native_build.rust_dependencies_toml,
212
+ native_build.generated_native_rs.as_deref(),
213
+ Some(root),
214
+ )
215
+ .map_err(|e| NativeError { message: e })
201
216
  }
202
217
  Backend::Cranelift => {
203
218
  if tishlang_compile::has_external_native_imports(program) {
204
219
  return Err(NativeError {
205
- message: "Cranelift backend does not support external native imports. Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
220
+ message: "Cranelift backend does not support external native imports (tish:…, cargo:…, @scope/pkg). Built-in tish:fs, tish:http, tish:process are supported.".to_string(),
206
221
  });
207
222
  }
208
223
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
@@ -218,7 +233,7 @@ pub fn compile_program_to_native(
218
233
  Backend::Llvm => {
219
234
  if tishlang_compile::has_external_native_imports(program) {
220
235
  return Err(NativeError {
221
- message: "LLVM backend does not support external native imports.".to_string(),
236
+ message: "LLVM backend does not support external native imports (tish:…, cargo:…, @scope/pkg).".to_string(),
222
237
  });
223
238
  }
224
239
  let program = if optimize { tishlang_opt::optimize(program) } else { program.clone() };
@@ -1260,10 +1260,17 @@ impl Vm {
1260
1260
  }
1261
1261
  };
1262
1262
  let v = get_builtin_export(self.capabilities.as_ref(), spec, export_name).ok_or_else(|| {
1263
- format!(
1264
- "Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
1265
- spec, export_name
1266
- )
1263
+ if spec.starts_with("cargo:") {
1264
+ format!(
1265
+ "cargo:… imports are only supported by `tish build` with the Rust native backend (not the bytecode VM). Spec: {}",
1266
+ spec
1267
+ )
1268
+ } else {
1269
+ format!(
1270
+ "Built-in module '{}' does not export '{}' or capability not enabled for this run. Use e.g. tish run --feature fs (or full). The tish binary must also be built with that capability linked in.",
1271
+ spec, export_name
1272
+ )
1273
+ }
1267
1274
  })?;
1268
1275
  self.stack.push(v);
1269
1276
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tishlang/tish",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Tish - minimal TS/JS-compatible language. Run, REPL, build to native or other targets.",
5
5
  "license": "PIF",
6
6
  "repository": {
Binary file
Binary file
Binary file
Binary file
Binary file