@tishlang/tish 1.9.2 → 1.12.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 (84) hide show
  1. package/bin/tish +0 -0
  2. package/crates/js_to_tish/src/transform/expr.rs +8 -6
  3. package/crates/js_to_tish/src/transform/stmt.rs +12 -13
  4. package/crates/tish/Cargo.toml +1 -1
  5. package/crates/tish/src/cargo_native_registry.rs +4 -1
  6. package/crates/tish/src/cli_help.rs +9 -1
  7. package/crates/tish/src/main.rs +66 -11
  8. package/crates/tish/tests/integration_test.rs +145 -7
  9. package/crates/tish_ast/src/ast.rs +3 -9
  10. package/crates/tish_build_utils/src/lib.rs +74 -23
  11. package/crates/tish_builtins/src/array.rs +2 -3
  12. package/crates/tish_builtins/src/construct.rs +15 -28
  13. package/crates/tish_builtins/src/globals.rs +18 -16
  14. package/crates/tish_builtins/src/helpers.rs +1 -4
  15. package/crates/tish_builtins/src/lib.rs +1 -0
  16. package/crates/tish_builtins/src/math.rs +7 -0
  17. package/crates/tish_builtins/src/object.rs +10 -10
  18. package/crates/tish_builtins/src/string.rs +27 -3
  19. package/crates/tish_builtins/src/symbol.rs +83 -0
  20. package/crates/tish_compile/src/codegen.rs +324 -158
  21. package/crates/tish_compile/src/lib.rs +39 -7
  22. package/crates/tish_compile/src/resolve.rs +191 -6
  23. package/crates/tish_compile/src/types.rs +6 -6
  24. package/crates/tish_compile_js/src/codegen.rs +8 -5
  25. package/crates/tish_core/src/console_style.rs +9 -0
  26. package/crates/tish_core/src/json.rs +17 -7
  27. package/crates/tish_core/src/macros.rs +2 -2
  28. package/crates/tish_core/src/value.rs +213 -4
  29. package/crates/tish_cranelift/src/link.rs +1 -1
  30. package/crates/tish_cranelift_runtime/Cargo.toml +4 -0
  31. package/crates/tish_eval/src/eval.rs +135 -73
  32. package/crates/tish_eval/src/http.rs +18 -12
  33. package/crates/tish_eval/src/lib.rs +29 -0
  34. package/crates/tish_eval/src/regex.rs +1 -1
  35. package/crates/tish_eval/src/value.rs +89 -4
  36. package/crates/tish_eval/src/value_convert.rs +30 -8
  37. package/crates/tish_fmt/src/lib.rs +4 -1
  38. package/crates/tish_lexer/src/lib.rs +7 -2
  39. package/crates/tish_llvm/src/lib.rs +2 -2
  40. package/crates/tish_lsp/src/builtin_goto.rs +111 -10
  41. package/crates/tish_lsp/src/import_goto.rs +35 -22
  42. package/crates/tish_lsp/src/main.rs +118 -85
  43. package/crates/tish_native/src/build.rs +270 -24
  44. package/crates/tish_native/src/config.rs +48 -0
  45. package/crates/tish_native/src/lib.rs +139 -12
  46. package/crates/tish_parser/src/lib.rs +5 -2
  47. package/crates/tish_parser/src/parser.rs +45 -75
  48. package/crates/tish_pg/src/error.rs +1 -1
  49. package/crates/tish_pg/src/lib.rs +61 -73
  50. package/crates/tish_resolve/src/lib.rs +283 -158
  51. package/crates/tish_resolve/src/pos.rs +10 -2
  52. package/crates/tish_runtime/Cargo.toml +3 -0
  53. package/crates/tish_runtime/src/http.rs +39 -39
  54. package/crates/tish_runtime/src/http_fetch.rs +12 -12
  55. package/crates/tish_runtime/src/lib.rs +35 -44
  56. package/crates/tish_runtime/src/native_promise.rs +0 -11
  57. package/crates/tish_runtime/src/promise.rs +14 -1
  58. package/crates/tish_runtime/src/promise_io.rs +1 -4
  59. package/crates/tish_runtime/src/timers.rs +12 -7
  60. package/crates/tish_runtime/src/ws.rs +40 -27
  61. package/crates/tish_runtime/tests/fetch_readable_stream.rs +10 -8
  62. package/crates/tish_ui/src/jsx.rs +6 -4
  63. package/crates/tish_ui/src/lib.rs +5 -4
  64. package/crates/tish_ui/src/runtime/hooks.rs +123 -37
  65. package/crates/tish_ui/src/runtime/mod.rs +21 -41
  66. package/crates/tish_vm/Cargo.toml +2 -0
  67. package/crates/tish_vm/src/vm.rs +258 -153
  68. package/crates/tish_wasm/src/lib.rs +60 -7
  69. package/crates/tish_wasm_runtime/Cargo.toml +10 -1
  70. package/crates/tish_wasm_runtime/src/gpu.rs +413 -0
  71. package/crates/tish_wasm_runtime/src/lib.rs +7 -1
  72. package/crates/tishlang_cargo_bindgen/src/classify.rs +1 -3
  73. package/crates/tishlang_cargo_bindgen/src/discover.rs +10 -5
  74. package/crates/tishlang_cargo_bindgen/src/infer.rs +18 -8
  75. package/crates/tishlang_cargo_bindgen/src/lib.rs +25 -26
  76. package/crates/tishlang_cargo_bindgen/src/main.rs +41 -38
  77. package/crates/tishlang_cargo_bindgen/src/metadata.rs +4 -1
  78. package/justfile +3 -3
  79. package/package.json +1 -1
  80. package/platform/darwin-arm64/tish +0 -0
  81. package/platform/darwin-x64/tish +0 -0
  82. package/platform/linux-arm64/tish +0 -0
  83. package/platform/linux-x64/tish +0 -0
  84. package/platform/win32-x64/tish.exe +0 -0
@@ -5,9 +5,18 @@ use std::path::Path;
5
5
 
6
6
  use tishlang_compile::ResolvedNativeModule;
7
7
 
8
+ use crate::config::{NativeArtifact, NativeBuildConfig};
9
+
8
10
  /// `tishlang_runtime` Cargo feature names (subset of CLI / compile feature names).
9
- const RUNTIME_CARGO_FEATURES: &[&str] =
10
- &["http", "http-hyper", "http-io-uring", "fs", "process", "regex", "ws"];
11
+ const RUNTIME_CARGO_FEATURES: &[&str] = &[
12
+ "http",
13
+ "http-hyper",
14
+ "http-io-uring",
15
+ "fs",
16
+ "process",
17
+ "regex",
18
+ "ws",
19
+ ];
11
20
 
12
21
  /// Map CLI/compile features to flags passed to `tishlang_runtime` in the temp crate's Cargo.toml.
13
22
  /// `full` enables every optional runtime capability (matches `tish build --feature full` / LANGUAGE.md).
@@ -29,6 +38,29 @@ fn runtime_features_for_cargo(features: &[String]) -> Vec<String> {
29
38
  out
30
39
  }
31
40
 
41
+ /// `[profile.release]` for nested `cargo build` of generated crates.
42
+ fn nested_release_profile_toml() -> &'static str {
43
+ if std::env::var("TISH_FAST_NATIVE_BUILD").as_deref() == Ok("1") {
44
+ r#"[profile.release]
45
+ opt-level = 1
46
+ lto = false
47
+ codegen-units = 16
48
+ incremental = true
49
+ strip = false
50
+ debug = 0
51
+ panic = "abort"
52
+ "#
53
+ } else {
54
+ r#"[profile.release]
55
+ # Reduce binary size: strip symbols, abort on panic (no unwinding), single codegen unit
56
+ strip = true
57
+ panic = "abort"
58
+ codegen-units = 1
59
+ lto = "fat"
60
+ "#
61
+ }
62
+ }
63
+
32
64
  /// Inject `mod generated_native;` after the crate attribute so the binary crate can call `crate::generated_native::…`.
33
65
  fn inject_generated_native_mod(rust_code: &str) -> String {
34
66
  if let Some(pos) = rust_code.find("\n\n") {
@@ -39,6 +71,10 @@ fn inject_generated_native_mod(rust_code: &str) -> String {
39
71
  }
40
72
  }
41
73
 
74
+ pub(crate) fn rust_code_needs_tokio(rust_code: &str) -> bool {
75
+ rust_code.contains("#[tokio::main]") || rust_code.contains("tokio::runtime::Runtime")
76
+ }
77
+
42
78
  pub fn build_via_cargo(
43
79
  rust_code: &str,
44
80
  native_modules: Vec<ResolvedNativeModule>,
@@ -48,11 +84,34 @@ pub fn build_via_cargo(
48
84
  generated_native_rs: Option<&str>,
49
85
  project_root: Option<&Path>,
50
86
  ) -> Result<(), String> {
51
- let out_name = output_path
87
+ build_via_cargo_with_config(
88
+ rust_code,
89
+ native_modules,
90
+ output_path,
91
+ features,
92
+ extra_dependencies_toml,
93
+ generated_native_rs,
94
+ project_root,
95
+ &NativeBuildConfig::desktop(),
96
+ )
97
+ }
98
+
99
+ pub fn build_via_cargo_with_config(
100
+ rust_code: &str,
101
+ native_modules: Vec<ResolvedNativeModule>,
102
+ output_path: &Path,
103
+ features: &[String],
104
+ extra_dependencies_toml: &str,
105
+ generated_native_rs: Option<&str>,
106
+ project_root: Option<&Path>,
107
+ build_config: &NativeBuildConfig,
108
+ ) -> Result<(), String> {
109
+ let out_stem = output_path
52
110
  .file_stem()
53
111
  .and_then(|s| s.to_str())
54
112
  .unwrap_or("tish_out");
55
- let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_name)?;
113
+ let cargo_name = tishlang_build_utils::cargo_target_name(out_stem);
114
+ let build_dir = tishlang_build_utils::create_build_dir("tish_build", out_stem)?;
56
115
 
57
116
  let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
58
117
 
@@ -64,7 +123,7 @@ pub fn build_via_cargo(
64
123
  format!(", features = {:?}", runtime_refs)
65
124
  };
66
125
 
67
- let needs_tokio = rust_code.contains("#[tokio::main]");
126
+ let needs_tokio = rust_code_needs_tokio(rust_code);
68
127
  let tokio_dep = if needs_tokio {
69
128
  "\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\"] }\n"
70
129
  } else {
@@ -108,27 +167,43 @@ pub fn build_via_cargo(
108
167
  String::new()
109
168
  };
110
169
 
170
+ let profile = nested_release_profile_toml();
171
+ let src_file = if build_config.artifact == NativeArtifact::StaticLib {
172
+ "lib.rs"
173
+ } else {
174
+ "main.rs"
175
+ };
176
+ let crate_section = if build_config.artifact == NativeArtifact::StaticLib {
177
+ format!(
178
+ r#"[lib]
179
+ name = "{}"
180
+ crate-type = ["staticlib"]
181
+ path = "src/lib.rs"
182
+
183
+ "#,
184
+ cargo_name
185
+ )
186
+ } else {
187
+ format!(
188
+ r#"[[bin]]
189
+ name = "{}"
190
+ path = "src/main.rs"
191
+
192
+ "#,
193
+ cargo_name
194
+ )
195
+ };
111
196
  let cargo_toml = format!(
112
197
  r#"[package]
113
198
  name = "tish_output"
114
199
  version = "0.1.0"
115
200
  edition = "2021"
116
201
 
117
- [[bin]]
118
- name = "{}"
119
- path = "src/main.rs"
120
-
121
- [profile.release]
122
- # Reduce binary size: strip symbols, abort on panic (no unwinding), single codegen unit
123
- strip = true
124
- panic = "abort"
125
- codegen-units = 1
126
- lto = "fat"
127
-
202
+ {}{}
128
203
  [dependencies]
129
204
  tishlang_runtime = {{ path = {:?}{} }}
130
205
  {}{}"#,
131
- out_name, runtime_path, features_str, more_deps, ui_dep
206
+ crate_section, profile, runtime_path, features_str, more_deps, ui_dep
132
207
  );
133
208
 
134
209
  fs::write(build_dir.join("Cargo.toml"), cargo_toml)
@@ -137,8 +212,173 @@ tishlang_runtime = {{ path = {:?}{} }}
137
212
  fs::write(build_dir.join("src/generated_native.rs"), gen)
138
213
  .map_err(|e| format!("Cannot write generated_native.rs: {}", e))?;
139
214
  }
140
- fs::write(build_dir.join("src/main.rs"), rust_main)
141
- .map_err(|e| format!("Cannot write main.rs: {}", e))?;
215
+ fs::write(build_dir.join("src").join(src_file), rust_main)
216
+ .map_err(|e| format!("Cannot write {}: {}", src_file, e))?;
217
+
218
+ let workspace_target = Path::new(&runtime_path)
219
+ .parent()
220
+ .and_then(|p| p.parent())
221
+ .map(|ws| ws.join("target"));
222
+ let target_dir = workspace_target.filter(|p| p.exists());
223
+ let cross = build_config.cargo_target.as_deref();
224
+ let release_sub = if let Some(triple) = cross {
225
+ format!("{triple}/release")
226
+ } else {
227
+ "release".to_string()
228
+ };
229
+ let binary_dir = target_dir
230
+ .as_ref()
231
+ .map(|t| t.join(&release_sub))
232
+ .unwrap_or_else(|| build_dir.join("target").join(&release_sub));
233
+
234
+ tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref(), cross)?;
235
+
236
+ let artifact = if build_config.artifact == NativeArtifact::StaticLib {
237
+ tishlang_build_utils::find_release_staticlib(&binary_dir, &cargo_name)?
238
+ } else {
239
+ tishlang_build_utils::find_release_binary(&binary_dir, &cargo_name)?
240
+ };
241
+ let target = if build_config.artifact == NativeArtifact::StaticLib {
242
+ if output_path.extension().is_some_and(|e| e == "a") {
243
+ output_path.to_path_buf()
244
+ } else if output_path.to_string_lossy().ends_with('/') || output_path.is_dir() {
245
+ output_path.join(format!("lib{out_stem}.a"))
246
+ } else {
247
+ output_path.with_extension("a")
248
+ }
249
+ } else {
250
+ tishlang_build_utils::resolve_output_path(output_path, out_stem)
251
+ };
252
+ tishlang_build_utils::copy_binary_to_output(&artifact, &target)?;
253
+
254
+ Ok(())
255
+ }
256
+
257
+ /// Build several native binaries in **one** nested Cargo project (shared `tishlang_runtime` compile).
258
+ ///
259
+ /// `bins` order must match `outputs`: each `(stem, rust_code, generated_native_rs)` pairs with
260
+ /// `outputs[i].0` (entry path — used only for validation) and `outputs[i].1` (final binary path).
261
+ pub(crate) fn build_many_via_cargo(
262
+ bins: Vec<(String, String, Option<String>)>,
263
+ native_modules: Vec<ResolvedNativeModule>,
264
+ features: &[String],
265
+ extra_dependencies_toml: &str,
266
+ needs_tokio: bool,
267
+ needs_ui: bool,
268
+ outputs: &[(&Path, &Path)],
269
+ project_root: Option<&Path>,
270
+ ) -> Result<(), String> {
271
+ if bins.len() != outputs.len() {
272
+ return Err(format!(
273
+ "build_many_via_cargo: bins ({}) != outputs ({})",
274
+ bins.len(),
275
+ outputs.len()
276
+ ));
277
+ }
278
+ for (i, (stem, _, _)) in bins.iter().enumerate() {
279
+ let entry = outputs[i].0;
280
+ let expect = entry.file_stem().and_then(|s| s.to_str()).unwrap_or("");
281
+ if expect != stem {
282
+ return Err(format!(
283
+ "build_many_via_cargo: stem mismatch at {}: {} vs {}",
284
+ i, stem, expect
285
+ ));
286
+ }
287
+ }
288
+
289
+ let batch_id = format!("many_{}", std::process::id());
290
+ let build_dir = tishlang_build_utils::create_build_dir("tish_build_many", &batch_id)?;
291
+
292
+ let runtime_path = tishlang_build_utils::find_runtime_path_for_project(project_root)?;
293
+
294
+ let runtime_features = runtime_features_for_cargo(features);
295
+ let runtime_refs: Vec<&str> = runtime_features.iter().map(String::as_str).collect();
296
+ let features_str = if runtime_refs.is_empty() {
297
+ String::new()
298
+ } else {
299
+ format!(", features = {:?}", runtime_refs)
300
+ };
301
+
302
+ let tokio_dep = if needs_tokio {
303
+ "\ntokio = { version = \"1\", features = [\"rt-multi-thread\", \"macros\"] }\n"
304
+ } else {
305
+ ""
306
+ };
307
+
308
+ let native_deps: String = native_modules
309
+ .iter()
310
+ .filter(|m| m.use_path_dependency)
311
+ .map(|m| {
312
+ let path = m.crate_path.display().to_string().replace('\\', "/");
313
+ format!("{} = {{ path = {:?} }}\n", m.package_name, path)
314
+ })
315
+ .collect();
316
+
317
+ let mut more_deps = String::new();
318
+ more_deps.push_str(tokio_dep);
319
+ if !native_deps.is_empty() {
320
+ more_deps.push_str(&format!("\n{}", native_deps));
321
+ }
322
+ if !extra_dependencies_toml.trim().is_empty() {
323
+ more_deps.push_str(&format!("\n{}", extra_dependencies_toml));
324
+ }
325
+
326
+ let tish_ui_path = std::path::Path::new(&runtime_path)
327
+ .parent()
328
+ .ok_or_else(|| "invalid tishlang_runtime path (no parent)".to_string())?
329
+ .join("tish_ui");
330
+ let ui_dep = if needs_ui {
331
+ format!(
332
+ "\ntishlang_ui = {{ path = {:?}, default-features = false, features = [\"runtime\"] }}\n",
333
+ tish_ui_path.display().to_string().replace('\\', "/")
334
+ )
335
+ } else {
336
+ String::new()
337
+ };
338
+
339
+ let mut bin_tables = String::new();
340
+ for (stem, rust_code, generated_native_rs) in &bins {
341
+ let bin_dir = build_dir.join("src/bin").join(stem);
342
+ fs::create_dir_all(&bin_dir).map_err(|e| format!("create bin dir: {}", e))?;
343
+
344
+ let rust_main = if generated_native_rs.is_some() {
345
+ inject_generated_native_mod(rust_code)
346
+ } else {
347
+ rust_code.clone()
348
+ };
349
+
350
+ fs::write(bin_dir.join("main.rs"), rust_main)
351
+ .map_err(|e| format!("write main.rs for {}: {}", stem, e))?;
352
+ if let Some(gen) = generated_native_rs {
353
+ fs::write(bin_dir.join("generated_native.rs"), gen)
354
+ .map_err(|e| format!("write generated_native.rs for {}: {}", stem, e))?;
355
+ }
356
+
357
+ bin_tables.push_str(&format!(
358
+ r#"[[bin]]
359
+ name = "{stem}"
360
+ path = "src/bin/{stem}/main.rs"
361
+
362
+ "#
363
+ ));
364
+ }
365
+
366
+ let profile = nested_release_profile_toml();
367
+ let cargo_toml = format!(
368
+ r#"[package]
369
+ name = "tish_output_many"
370
+ version = "0.1.0"
371
+ edition = "2021"
372
+
373
+ {}{}
374
+ [dependencies]
375
+ tishlang_runtime = {{ path = {:?}{} }}
376
+ {}{}"#,
377
+ bin_tables, profile, runtime_path, features_str, more_deps, ui_dep
378
+ );
379
+
380
+ fs::write(build_dir.join("Cargo.toml"), cargo_toml)
381
+ .map_err(|e| format!("Cannot write Cargo.toml: {}", e))?;
142
382
 
143
383
  let workspace_target = Path::new(&runtime_path)
144
384
  .parent()
@@ -150,11 +390,15 @@ tishlang_runtime = {{ path = {:?}{} }}
150
390
  .map(|t| t.join("release"))
151
391
  .unwrap_or_else(|| build_dir.join("target").join("release"));
152
392
 
153
- tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref())?;
393
+ tishlang_build_utils::run_cargo_build(&build_dir, target_dir.as_deref(), None)?;
154
394
 
155
- let binary = tishlang_build_utils::find_release_binary(&binary_dir, out_name)?;
156
- let target = tishlang_build_utils::resolve_output_path(output_path, out_name);
157
- tishlang_build_utils::copy_binary_to_output(&binary, &target)?;
395
+ for i in 0..bins.len() {
396
+ let stem = bins[i].0.as_str();
397
+ let output_path = outputs[i].1;
398
+ let binary = tishlang_build_utils::find_release_binary(binary_dir.as_path(), stem)?;
399
+ let target = tishlang_build_utils::resolve_output_path(output_path, stem);
400
+ tishlang_build_utils::copy_binary_to_output(&binary, &target)?;
401
+ }
158
402
 
159
403
  Ok(())
160
404
  }
@@ -176,6 +420,8 @@ mod tests {
176
420
  #[test]
177
421
  fn runtime_features_merges_full_and_specific() {
178
422
  let f = runtime_features_for_cargo(&["full".to_string(), "http".to_string()]);
179
- assert_eq!(f.len(), 5);
423
+ // `full` expands to every RUNTIME_CARGO_FEATURES entry; redundant `http` must not duplicate.
424
+ assert_eq!(f.len(), super::RUNTIME_CARGO_FEATURES.len());
425
+ assert_eq!(f.iter().filter(|x| *x == "http").count(), 1);
180
426
  }
181
427
  }
@@ -0,0 +1,48 @@
1
+ //! Native build configuration (desktop binary vs iOS staticlib, cross-target).
2
+
3
+ use tishlang_compile::NativeEmitMode;
4
+
5
+ /// Output artifact kind for `tish build --target native`.
6
+ #[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7
+ pub enum NativeArtifact {
8
+ #[default]
9
+ Bin,
10
+ StaticLib,
11
+ }
12
+
13
+ /// Options passed from the CLI into nested `cargo build`.
14
+ #[derive(Debug, Clone, Default)]
15
+ pub struct NativeBuildConfig {
16
+ pub artifact: NativeArtifact,
17
+ /// When set, run `cargo build --target <triple>` and skip `-C target-cpu=native`.
18
+ pub cargo_target: Option<String>,
19
+ pub emit_mode: NativeEmitMode,
20
+ }
21
+
22
+ impl NativeBuildConfig {
23
+ pub fn desktop() -> Self {
24
+ Self::default()
25
+ }
26
+
27
+ pub fn ios_staticlib(triple: &str) -> Self {
28
+ Self {
29
+ artifact: NativeArtifact::StaticLib,
30
+ cargo_target: Some(triple.to_string()),
31
+ emit_mode: NativeEmitMode::EmbeddedLib,
32
+ }
33
+ }
34
+
35
+ pub fn is_cross_compile(&self) -> bool {
36
+ self.cargo_target.is_some()
37
+ }
38
+ }
39
+
40
+ /// Filter runtime features for iOS sandbox builds.
41
+ pub fn ios_runtime_features(features: &[String]) -> Vec<String> {
42
+ const ALLOW: &[&str] = &["http", "http-hyper", "regex", "timers"];
43
+ features
44
+ .iter()
45
+ .filter(|f| ALLOW.contains(&f.as_str()))
46
+ .cloned()
47
+ .collect()
48
+ }
@@ -10,10 +10,29 @@
10
10
  //! emit Rust using `Vec<f64>` / fixed primitives instead of `Value` on hot paths.
11
11
 
12
12
  mod build;
13
+ mod config;
14
+
15
+ pub use config::{ios_runtime_features, NativeArtifact, NativeBuildConfig};
13
16
 
14
17
  use std::path::Path;
15
18
  use tishlang_ast::Program;
16
19
 
20
+ /// Features for the embeddable VM (`cranelift` / `llvm` backends): merge CLI capability flags
21
+ /// (same set as `tish run` / `tish build --target native`) with features inferred from
22
+ /// `import … from 'tish:…'`. Programs that use globals only (`Promise`, `fetch`, `setTimeout`)
23
+ /// still need the former so [`tishlang_cranelift_runtime`] links `tishlang_vm` with matching gates.
24
+ fn merged_embed_runtime_features(cli: &[String], program: &Program) -> Vec<String> {
25
+ let mut out = cli.to_vec();
26
+ for f in tishlang_compile::extract_native_import_features(program) {
27
+ if !out.contains(&f) {
28
+ out.push(f);
29
+ }
30
+ }
31
+ out.sort();
32
+ out.dedup();
33
+ out
34
+ }
35
+
17
36
  /// Error from native compilation.
18
37
  #[derive(Debug)]
19
38
  pub struct NativeError {
@@ -40,6 +59,27 @@ pub fn compile_to_native(
40
59
  features: &[String],
41
60
  native_backend: &str,
42
61
  optimize: bool,
62
+ ) -> Result<(), NativeError> {
63
+ compile_to_native_with_config(
64
+ entry_path,
65
+ project_root,
66
+ output_path,
67
+ features,
68
+ native_backend,
69
+ optimize,
70
+ &NativeBuildConfig::desktop(),
71
+ )
72
+ }
73
+
74
+ /// Compile a Tish project to a native binary or static library.
75
+ pub fn compile_to_native_with_config(
76
+ entry_path: &Path,
77
+ project_root: Option<&Path>,
78
+ output_path: &Path,
79
+ features: &[String],
80
+ native_backend: &str,
81
+ optimize: bool,
82
+ build_config: &NativeBuildConfig,
43
83
  ) -> Result<(), NativeError> {
44
84
  let backend = match native_backend {
45
85
  "rust" => Backend::Rust,
@@ -57,25 +97,44 @@ pub fn compile_to_native(
57
97
 
58
98
  match backend {
59
99
  Backend::Rust => {
100
+ let ios_cap = build_config.cargo_target.as_ref().map(|_| {
101
+ ios_runtime_features(features)
102
+ .into_iter()
103
+ .collect::<std::collections::HashSet<_>>()
104
+ });
105
+ let compile_features = if ios_cap.is_some() {
106
+ ios_runtime_features(features)
107
+ } else {
108
+ features.to_vec()
109
+ };
60
110
  let (rust_code, native_modules, effective_features, native_build) =
61
- tishlang_compile::compile_project_full(
111
+ tishlang_compile::compile_project_full_emit(
62
112
  entry_path,
63
113
  project_root,
64
- features,
114
+ &compile_features,
65
115
  optimize,
116
+ build_config.emit_mode,
117
+ ios_cap.as_ref(),
66
118
  )
67
119
  .map_err(|e| NativeError {
68
120
  message: e.to_string(),
69
121
  })?;
70
122
 
71
- crate::build::build_via_cargo(
123
+ let features_for_cargo = if build_config.cargo_target.is_some() {
124
+ ios_runtime_features(&effective_features)
125
+ } else {
126
+ effective_features
127
+ };
128
+
129
+ crate::build::build_via_cargo_with_config(
72
130
  &rust_code,
73
131
  native_modules,
74
132
  output_path,
75
- &effective_features,
133
+ &features_for_cargo,
76
134
  &native_build.rust_dependencies_toml,
77
135
  native_build.generated_native_rs.as_deref(),
78
136
  project_root,
137
+ build_config,
79
138
  )
80
139
  .map_err(|e| NativeError { message: e })
81
140
  }
@@ -93,8 +152,8 @@ pub fn compile_to_native(
93
152
  let prog = tishlang_compile::merge_modules(modules)
94
153
  .map(|m| m.program)
95
154
  .map_err(|e| NativeError {
96
- message: e.to_string(),
97
- })?;
155
+ message: e.to_string(),
156
+ })?;
98
157
  if optimize {
99
158
  tishlang_opt::optimize(&prog)
100
159
  } else {
@@ -118,7 +177,7 @@ pub fn compile_to_native(
118
177
  })?
119
178
  };
120
179
 
121
- let cranelift_features = tishlang_compile::extract_native_import_features(&program);
180
+ let cranelift_features = merged_embed_runtime_features(features, &program);
122
181
  tishlang_cranelift::compile_chunk_to_native(&chunk, output_path, &cranelift_features)
123
182
  .map_err(|e| NativeError {
124
183
  message: e.to_string(),
@@ -138,8 +197,8 @@ pub fn compile_to_native(
138
197
  let prog = tishlang_compile::merge_modules(modules)
139
198
  .map(|m| m.program)
140
199
  .map_err(|e| NativeError {
141
- message: e.to_string(),
142
- })?;
200
+ message: e.to_string(),
201
+ })?;
143
202
  if optimize {
144
203
  tishlang_opt::optimize(&prog)
145
204
  } else {
@@ -160,7 +219,7 @@ pub fn compile_to_native(
160
219
  message: e.to_string(),
161
220
  })?
162
221
  };
163
- let llvm_features = tishlang_compile::extract_native_import_features(&program);
222
+ let llvm_features = merged_embed_runtime_features(features, &program);
164
223
  tishlang_llvm::compile_chunk_to_native(&chunk, output_path, &llvm_features)
165
224
  .map_err(|e| NativeError { message: e.message })
166
225
  }
@@ -249,7 +308,7 @@ pub fn compile_program_to_native(
249
308
  message: e.to_string(),
250
309
  })?
251
310
  };
252
- let cranelift_features = tishlang_compile::extract_native_import_features(&program);
311
+ let cranelift_features = merged_embed_runtime_features(features, &program);
253
312
  tishlang_cranelift::compile_chunk_to_native(&chunk, output_path, &cranelift_features)
254
313
  .map_err(|e| NativeError {
255
314
  message: e.to_string(),
@@ -275,13 +334,81 @@ pub fn compile_program_to_native(
275
334
  message: e.to_string(),
276
335
  })?
277
336
  };
278
- let llvm_features = tishlang_compile::extract_native_import_features(&program);
337
+ let llvm_features = merged_embed_runtime_features(features, &program);
279
338
  tishlang_llvm::compile_chunk_to_native(&chunk, output_path, &llvm_features)
280
339
  .map_err(|e| NativeError { message: e.message })
281
340
  }
282
341
  }
283
342
  }
284
343
 
344
+ /// Compile multiple entry `.tish` files to native binaries in **one** nested Cargo build.
345
+ ///
346
+ /// Intended for integration tests and batch tooling; keeps production [`compile_to_native`] behavior
347
+ /// unchanged when `TISH_FAST_NATIVE_BUILD` is unset.
348
+ pub fn compile_many_to_native(
349
+ entries: &[(&Path, &Path)],
350
+ project_root: Option<&Path>,
351
+ features: &[String],
352
+ optimize: bool,
353
+ ) -> Result<(), NativeError> {
354
+ let mut bins: Vec<(String, String, Option<String>)> = Vec::with_capacity(entries.len());
355
+ let mut merged_native_modules: Vec<tishlang_compile::ResolvedNativeModule> = Vec::new();
356
+ let mut merged_features: Vec<String> = features.to_vec();
357
+ let mut merged_extra_deps = String::new();
358
+ let mut needs_tokio = false;
359
+ let mut needs_ui = false;
360
+
361
+ for (entry_path, _) in entries {
362
+ let (rust_code, native_modules, effective_features, native_build) =
363
+ tishlang_compile::compile_project_full(entry_path, project_root, features, optimize)
364
+ .map_err(|e| NativeError {
365
+ message: e.to_string(),
366
+ })?;
367
+ let stem = entry_path
368
+ .file_stem()
369
+ .and_then(|s| s.to_str())
370
+ .ok_or_else(|| NativeError {
371
+ message: format!("invalid entry path: {}", entry_path.display()),
372
+ })?
373
+ .to_string();
374
+
375
+ for f in &effective_features {
376
+ if !merged_features.contains(f) {
377
+ merged_features.push(f.clone());
378
+ }
379
+ }
380
+ for m in native_modules {
381
+ let dup = merged_native_modules
382
+ .iter()
383
+ .any(|x| x.package_name == m.package_name && x.crate_path == m.crate_path);
384
+ if !dup {
385
+ merged_native_modules.push(m);
386
+ }
387
+ }
388
+ let extra = native_build.rust_dependencies_toml.trim();
389
+ if !extra.is_empty() {
390
+ merged_extra_deps.push_str(extra);
391
+ merged_extra_deps.push('\n');
392
+ }
393
+ needs_tokio |= crate::build::rust_code_needs_tokio(&rust_code);
394
+ needs_ui |= rust_code.contains("tishlang_ui");
395
+ bins.push((stem, rust_code, native_build.generated_native_rs));
396
+ }
397
+
398
+ let merged_extra = merged_extra_deps.trim();
399
+ crate::build::build_many_via_cargo(
400
+ bins,
401
+ merged_native_modules,
402
+ &merged_features,
403
+ merged_extra,
404
+ needs_tokio,
405
+ needs_ui,
406
+ entries,
407
+ project_root,
408
+ )
409
+ .map_err(|e| NativeError { message: e })
410
+ }
411
+
285
412
  enum Backend {
286
413
  Rust,
287
414
  Cranelift,
@@ -320,10 +320,13 @@ mod tests {
320
320
  Statement::Block { statements, .. } => statements,
321
321
  _ => panic!("expected block body"),
322
322
  };
323
- assert_eq!(stmts.len(), 3, "expected expr; const; if as siblings — got {stmts:?}");
323
+ assert_eq!(
324
+ stmts.len(),
325
+ 3,
326
+ "expected expr; const; if as siblings — got {stmts:?}"
327
+ );
324
328
  assert!(matches!(stmts[0], Statement::ExprStmt { .. }));
325
329
  assert!(matches!(stmts[1], Statement::VarDecl { .. }));
326
330
  assert!(matches!(stmts[2], Statement::If { .. }));
327
331
  }
328
-
329
332
  }