@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
package/bin/tish CHANGED
Binary file
@@ -539,9 +539,10 @@ pub fn convert_params(
539
539
  let fp = p;
540
540
  {
541
541
  let (name, name_span) = match &fp.pattern {
542
- oxc::ast::ast::BindingPattern::BindingIdentifier(b) => {
543
- (b.name.as_str(), crate::span_util::oxc_span_to_tish(ctx.1, b.as_ref()))
544
- }
542
+ oxc::ast::ast::BindingPattern::BindingIdentifier(b) => (
543
+ b.name.as_str(),
544
+ crate::span_util::oxc_span_to_tish(ctx.1, b.as_ref()),
545
+ ),
545
546
  _ => {
546
547
  return Err(ConvertError::new(ConvertErrorKind::Unsupported {
547
548
  what: "destructuring in params".into(),
@@ -565,9 +566,10 @@ pub fn convert_params(
565
566
  if rest_param.is_none() {
566
567
  if let Some(rest) = &params.rest {
567
568
  let (rest_name, rest_name_span) = match &rest.rest.argument {
568
- oxc::ast::ast::BindingPattern::BindingIdentifier(b) => {
569
- (b.name.as_str(), crate::span_util::oxc_span_to_tish(ctx.1, b.as_ref()))
570
- }
569
+ oxc::ast::ast::BindingPattern::BindingIdentifier(b) => (
570
+ b.name.as_str(),
571
+ crate::span_util::oxc_span_to_tish(ctx.1, b.as_ref()),
572
+ ),
571
573
  _ => {
572
574
  return Err(ConvertError::new(ConvertErrorKind::Unsupported {
573
575
  what: "rest param with non-identifier".into(),
@@ -247,9 +247,10 @@ fn convert_for_of_statement(
247
247
  if v.declarations.len() == 1 {
248
248
  let d = &v.declarations[0];
249
249
  match &d.id {
250
- oxc::ast::ast::BindingPattern::BindingIdentifier(b) => {
251
- (b.name.as_str(), span_util::oxc_span_to_tish(ctx.1, b.as_ref()))
252
- }
250
+ oxc::ast::ast::BindingPattern::BindingIdentifier(b) => (
251
+ b.name.as_str(),
252
+ span_util::oxc_span_to_tish(ctx.1, b.as_ref()),
253
+ ),
253
254
  _ => {
254
255
  return Err(ConvertError::new(ConvertErrorKind::Incompatible {
255
256
  what: "for-of with destructuring".into(),
@@ -368,16 +369,14 @@ fn convert_function_decl(
368
369
  span: Span,
369
370
  ) -> Result<Statement, ConvertError> {
370
371
  let async_ = f.r#async;
371
- let name: Arc<str> = f
372
- .id
373
- .as_ref()
374
- .map(|id| Arc::from(id.name.as_str()))
375
- .unwrap_or_else(|| Arc::from(""));
376
- let name_span = f
377
- .id
378
- .as_ref()
379
- .map(|id| span_util::oxc_span_to_tish(ctx.1, id))
380
- .unwrap_or_else(span_util::stub_span);
372
+ let name: Arc<str> =
373
+ f.id.as_ref()
374
+ .map(|id| Arc::from(id.name.as_str()))
375
+ .unwrap_or_else(|| Arc::from(""));
376
+ let name_span =
377
+ f.id.as_ref()
378
+ .map(|id| span_util::oxc_span_to_tish(ctx.1, id))
379
+ .unwrap_or_else(span_util::stub_span);
381
380
  let (params, rest_param) = expr::convert_params(&f.params, ctx)?;
382
381
  let body = match &f.body {
383
382
  Some(fb) => {
@@ -1,6 +1,6 @@
1
1
  [package]
2
2
  name = "tishlang"
3
- version = "1.9.2"
3
+ version = "1.12.0"
4
4
  edition = "2021"
5
5
  description = "Tish CLI - run, REPL, compile to native"
6
6
  license-file = { workspace = true }
@@ -19,7 +19,10 @@ pub(crate) fn register_bytecode_native_modules(vm: &mut tishlang_vm::Vm) {
19
19
  Arc::from("query_prepared"),
20
20
  Value::native(tishlang_pg::query_prepared),
21
21
  );
22
- om.insert(Arc::from("query_all"), Value::native(tishlang_pg::query_all));
22
+ om.insert(
23
+ Arc::from("query_all"),
24
+ Value::native(tishlang_pg::query_all),
25
+ );
23
26
  om.insert(Arc::from("migrate"), Value::native(tishlang_pg::migrate));
24
27
  om.insert(Arc::from("close"), Value::native(tishlang_pg::close));
25
28
  vm.register_native_module("cargo:tish_pg", om);
@@ -405,6 +405,8 @@ pub fn build_after_help() -> String {
405
405
  WebAssembly (.tish project; .js source supported on some paths)
406
406
  {t}wasi{r}
407
407
  WASI WebAssembly
408
+ {t}bytecode{r}
409
+ Raw serialized bytecode chunk (no VM binary/JS/HTML); for hosts that already ship the runtime
408
410
 
409
411
  {oh}Native backends{r} (--native-backend, only with --target native, default: rust):
410
412
  {t}rust{r}
@@ -506,7 +508,7 @@ pub(crate) struct BuildArgs {
506
508
  help_heading = "Options"
507
509
  )]
508
510
  pub output: String,
509
- /// `native`, `js`, `wasm`, or `wasi` (see long help below).
511
+ /// `native`, `js`, `wasm`, `wasi`, or `bytecode` (see long help below).
510
512
  #[arg(
511
513
  long,
512
514
  default_value = "native",
@@ -530,6 +532,12 @@ pub(crate) struct BuildArgs {
530
532
  help_heading = "Options"
531
533
  )]
532
534
  pub features: Vec<String>,
535
+ /// Cross-compile to an Apple iOS triple (e.g. `aarch64-apple-ios-sim`). Implies `--crate-type staticlib`.
536
+ #[arg(long, value_name = "TRIPLE", help_heading = "Options")]
537
+ pub ios_triple: Option<String>,
538
+ /// Output artifact for `--target native` (default: `bin`; use `staticlib` for embedded iOS).
539
+ #[arg(long, value_name = "TYPE", default_value = "bin", help_heading = "Options")]
540
+ pub crate_type: String,
533
541
  #[arg(long, help_heading = "Options")]
534
542
  pub no_optimize: bool,
535
543
  /// For `--target js` project builds: emit `OUTPUT.js.map` and `//# sourceMappingURL=…` so JS/TS tools can jump to original `.tish` (implies `--no-optimize` for that build).
@@ -4,13 +4,11 @@ mod cargo_native_registry;
4
4
  mod cli_help;
5
5
  mod repl_completion;
6
6
 
7
- use std::cell::RefCell;
8
- use tishlang_core::VmRef;
9
7
  use std::collections::HashSet;
10
8
  use std::fs;
11
9
  use std::io::{self, IsTerminal, Read, Write};
12
10
  use std::path::{Path, PathBuf};
13
- use std::rc::Rc;
11
+ use tishlang_core::VmRef;
14
12
 
15
13
  use clap::FromArgMatches;
16
14
  use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
@@ -113,6 +111,8 @@ fn main() {
113
111
  &a.features,
114
112
  a.no_optimize || no_opt_env,
115
113
  a.source_map,
114
+ a.ios_triple.as_deref(),
115
+ &a.crate_type,
116
116
  ),
117
117
  Some(Commands::DumpAst { file }) => dump_ast(&file),
118
118
  None => {
@@ -516,13 +516,14 @@ fn compile_to_js(
516
516
  } else {
517
517
  program
518
518
  };
519
- let js = tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?;
519
+ let js =
520
+ tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?;
520
521
  (js, None)
521
522
  } else if input_path.extension().map(|e| e == "js") == Some(true) {
522
523
  let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
523
524
  let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
524
- let js =
525
- tishlang_compile_js::compile_with_jsx(&program, optimize).map_err(|e| format!("{}", e))?;
525
+ let js = tishlang_compile_js::compile_with_jsx(&program, optimize)
526
+ .map_err(|e| format!("{}", e))?;
526
527
  (js, None)
527
528
  } else if source_map {
528
529
  let bundle = tishlang_compile_js::compile_project_with_jsx_and_source_map(
@@ -545,7 +546,8 @@ fn compile_to_js(
545
546
  let mut js_out = js;
546
547
  if let Some(map) = &map_json {
547
548
  let map_path = out_path.with_extension("js.map");
548
- fs::write(&map_path, map).map_err(|e| format!("Cannot write {}: {}", map_path.display(), e))?;
549
+ fs::write(&map_path, map)
550
+ .map_err(|e| format!("Cannot write {}: {}", map_path.display(), e))?;
549
551
  let map_url = map_path
550
552
  .file_name()
551
553
  .and_then(|s| s.to_str())
@@ -553,7 +555,8 @@ fn compile_to_js(
553
555
  js_out.push_str(&format!("\n//# sourceMappingURL={map_url}\n"));
554
556
  println!("Built: {}", map_path.display());
555
557
  }
556
- fs::write(&out_path, js_out).map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
558
+ fs::write(&out_path, js_out)
559
+ .map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
557
560
  println!("Built: {}", out_path.display());
558
561
  Ok(())
559
562
  }
@@ -567,6 +570,8 @@ fn build_file(
567
570
  cli_features: &[String],
568
571
  no_optimize: bool,
569
572
  source_map: bool,
573
+ ios_triple: Option<&str>,
574
+ crate_type: &str,
570
575
  ) -> Result<(), String> {
571
576
  let optimize = !no_optimize;
572
577
  let input_path = Path::new(input_path)
@@ -611,18 +616,37 @@ fn build_file(
611
616
  Some(p)
612
617
  }
613
618
  });
619
+ let features = native_build_features_from_cli(cli_features);
614
620
  return tishlang_wasm::compile_to_wasi(
615
621
  &input_path,
616
622
  project_root,
617
623
  Path::new(output_path),
618
624
  optimize,
625
+ &features,
626
+ )
627
+ .map_err(|e| e.to_string());
628
+ }
629
+
630
+ if target == "bytecode" {
631
+ let project_root = input_path.parent().and_then(|p| {
632
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
633
+ p.parent()
634
+ } else {
635
+ Some(p)
636
+ }
637
+ });
638
+ return tishlang_wasm::compile_to_bytecode(
639
+ &input_path,
640
+ project_root,
641
+ Path::new(output_path),
642
+ optimize,
619
643
  )
620
644
  .map_err(|e| e.to_string());
621
645
  }
622
646
 
623
647
  if target != "native" {
624
648
  return Err(format!(
625
- "Unknown target: {}. Use 'native', 'js', 'wasm', or 'wasi'.",
649
+ "Unknown target: {}. Use 'native', 'js', 'wasm', 'wasi', or 'bytecode'.",
626
650
  target
627
651
  ));
628
652
  }
@@ -636,9 +660,31 @@ fn build_file(
636
660
  });
637
661
  let features: Vec<String> = native_build_features_from_cli(cli_features);
638
662
 
663
+ let build_config = if let Some(triple) = ios_triple {
664
+ tishlang_native::NativeBuildConfig::ios_staticlib(triple)
665
+ } else if crate_type == "staticlib" {
666
+ tishlang_native::NativeBuildConfig {
667
+ artifact: tishlang_native::NativeArtifact::StaticLib,
668
+ cargo_target: None,
669
+ emit_mode: tishlang_compile::NativeEmitMode::EmbeddedLib,
670
+ }
671
+ } else if crate_type != "bin" {
672
+ return Err(format!(
673
+ "Unknown --crate-type: {}. Use 'bin' or 'staticlib'.",
674
+ crate_type
675
+ ));
676
+ } else {
677
+ tishlang_native::NativeBuildConfig::desktop()
678
+ };
679
+
639
680
  if is_js {
640
681
  let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
641
682
  let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
683
+ if build_config.artifact != tishlang_native::NativeArtifact::Bin {
684
+ return Err(
685
+ "--crate-type staticlib / --ios-triple require a .tish entry file.".to_string(),
686
+ );
687
+ }
642
688
  tishlang_native::compile_program_to_native(
643
689
  &program,
644
690
  project_root,
@@ -649,13 +695,14 @@ fn build_file(
649
695
  )
650
696
  .map_err(|e| e.to_string())?;
651
697
  } else {
652
- tishlang_native::compile_to_native(
698
+ tishlang_native::compile_to_native_with_config(
653
699
  &input_path,
654
700
  project_root,
655
701
  Path::new(output_path),
656
702
  &features,
657
703
  native_backend,
658
704
  optimize,
705
+ &build_config,
659
706
  )
660
707
  .map_err(|e| e.to_string())?;
661
708
  }
@@ -664,7 +711,15 @@ fn build_file(
664
711
  .file_stem()
665
712
  .and_then(|s| s.to_str())
666
713
  .unwrap_or("tish_out");
667
- let built_path = if output_path.ends_with('/') || Path::new(output_path).is_dir() {
714
+ let built_path = if build_config.artifact == tishlang_native::NativeArtifact::StaticLib {
715
+ if output_path.ends_with('/') || Path::new(output_path).is_dir() {
716
+ Path::new(output_path).join(format!("lib{out_name}.a"))
717
+ } else if output_path.ends_with(".a") {
718
+ Path::new(output_path).to_path_buf()
719
+ } else {
720
+ Path::new(output_path).with_extension("a")
721
+ }
722
+ } else if output_path.ends_with('/') || Path::new(output_path).is_dir() {
668
723
  Path::new(output_path).join(out_name)
669
724
  } else {
670
725
  Path::new(output_path).to_path_buf()
@@ -5,6 +5,7 @@
5
5
  //! - Generate/update expected files: `REGENERATE_EXPECTED=1 cargo test -p tishlangtest_mvp_programs_interpreter`
6
6
  //! then commit the new/updated `tests/core/*.tish.expected` files.
7
7
  //! - Compiled outputs are cached under `target/integration_compile_cache/` per backend.
8
+ //! MVP native tests use `native_many/<hash>/` plus one batched nested Cargo build.
8
9
 
9
10
  use std::collections::hash_map::DefaultHasher;
10
11
  use std::ffi::OsString;
@@ -14,6 +15,7 @@ use std::path::{Path, PathBuf};
14
15
  use std::process::Command;
15
16
 
16
17
  use rayon::prelude::*;
18
+ use tishlang_native::compile_many_to_native;
17
19
 
18
20
  fn workspace_root() -> PathBuf {
19
21
  PathBuf::from(env!("CARGO_MANIFEST_DIR"))
@@ -50,6 +52,72 @@ fn integration_compile_cache_dir() -> PathBuf {
50
52
  target_dir().join("integration_compile_cache")
51
53
  }
52
54
 
55
+ /// Match `tish build` with no `--feature`: link every capability compiled into this `tish` binary.
56
+ fn native_build_features_for_integration_test() -> Vec<String> {
57
+ let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities()
58
+ .into_iter()
59
+ .collect();
60
+ v.sort();
61
+ v
62
+ }
63
+
64
+ fn combined_mvp_native_inputs_hash(paths: &[PathBuf]) -> u64 {
65
+ let mut h = DefaultHasher::new();
66
+ let feats = native_build_features_for_integration_test();
67
+ feats.len().hash(&mut h);
68
+ for f in &feats {
69
+ f.hash(&mut h);
70
+ }
71
+ paths.len().hash(&mut h);
72
+ for p in paths {
73
+ p.file_name()
74
+ .unwrap_or_default()
75
+ .to_string_lossy()
76
+ .hash(&mut h);
77
+ file_content_hash(p).hash(&mut h);
78
+ }
79
+ // Native batch cache must invalidate when the emitter or `value_call` changes — not only
80
+ // when `.tish` sources change; otherwise CI/rust-cache can keep stale nested binaries.
81
+ let codegen_rs = workspace_root().join("crates/tish_compile/src/codegen.rs");
82
+ if codegen_rs.is_file() {
83
+ file_content_hash(&codegen_rs).hash(&mut h);
84
+ }
85
+ let value_rs = workspace_root().join("crates/tish_core/src/value.rs");
86
+ if value_rs.is_file() {
87
+ file_content_hash(&value_rs).hash(&mut h);
88
+ }
89
+ h.finish()
90
+ }
91
+
92
+ fn mvp_native_batch_cache_dir(combined: u64) -> PathBuf {
93
+ integration_compile_cache_dir()
94
+ .join("native_many")
95
+ .join(format!("{:016x}", combined))
96
+ }
97
+
98
+ /// Restores the previous process env when dropped (for `TISH_FAST_NATIVE_BUILD` in batch tests).
99
+ struct EnvVarGuard {
100
+ key: &'static str,
101
+ previous: Option<std::ffi::OsString>,
102
+ }
103
+
104
+ impl EnvVarGuard {
105
+ fn set(key: &'static str, value: &str) -> Self {
106
+ let previous = std::env::var_os(key);
107
+ std::env::set_var(key, value);
108
+ Self { key, previous }
109
+ }
110
+ }
111
+
112
+ impl Drop for EnvVarGuard {
113
+ fn drop(&mut self) {
114
+ match &self.previous {
115
+ None => std::env::remove_var(self.key),
116
+ Some(v) => std::env::set_var(self.key, v),
117
+ }
118
+ }
119
+ }
120
+
53
121
  fn file_content_hash(path: &Path) -> u64 {
54
122
  let mut f = std::fs::File::open(path).expect("open file for hash");
55
123
  let mut content = Vec::new();
@@ -583,6 +651,7 @@ const MVP_TEST_FILES: &[&str] = &[
583
651
  "break_continue.tish",
584
652
  "length.tish",
585
653
  "objects.tish",
654
+ "symbol.tish",
586
655
  "conditional.tish",
587
656
  "switch.tish",
588
657
  "do_while.tish",
@@ -714,6 +783,7 @@ fn test_mvp_programs_interp_vm_stdout_parity() {
714
783
  /// Compile each .tish file to native, run, and compare stdout to static expected (parallelized).
715
784
  #[test]
716
785
  fn test_mvp_programs_native() {
786
+ let _fast_native = EnvVarGuard::set("TISH_FAST_NATIVE_BUILD", "1");
717
787
  let core_dir = core_dir();
718
788
  let bin = tish_bin();
719
789
  assert!(
@@ -721,18 +791,86 @@ fn test_mvp_programs_native() {
721
791
  "tish binary not found at {}. Run `cargo build -p tishlang` first.",
722
792
  bin.display()
723
793
  );
724
- let errors: Vec<String> = MVP_TEST_FILES
725
- .par_iter()
794
+
795
+ let mut paths: Vec<PathBuf> = MVP_TEST_FILES
796
+ .iter()
726
797
  .filter_map(|name| {
727
- let path = core_dir.join(name);
728
- if !path.exists() {
729
- return None;
798
+ let p = core_dir.join(name);
799
+ if p.exists() {
800
+ Some(p)
801
+ } else {
802
+ None
730
803
  }
731
- let expected = match get_expected(&path) {
804
+ })
805
+ .collect();
806
+ paths.sort();
807
+
808
+ if paths.is_empty() {
809
+ return;
810
+ }
811
+
812
+ let combined = combined_mvp_native_inputs_hash(&paths);
813
+ let cache_dir = mvp_native_batch_cache_dir(combined);
814
+ let _ = std::fs::create_dir_all(&cache_dir);
815
+
816
+ let ext = if cfg!(target_os = "windows") {
817
+ ".exe"
818
+ } else {
819
+ ""
820
+ };
821
+
822
+ let entries_owned: Vec<(PathBuf, PathBuf)> = paths
823
+ .iter()
824
+ .map(|p| {
825
+ let stem = p.file_stem().unwrap().to_string_lossy();
826
+ let cached = cache_dir.join(format!("{}{}", stem, ext));
827
+ (p.clone(), cached)
828
+ })
829
+ .collect();
830
+
831
+ let need_build = entries_owned.iter().any(|(_, o)| !o.exists());
832
+ if need_build {
833
+ let refs: Vec<(&Path, &Path)> = entries_owned
834
+ .iter()
835
+ .map(|(a, b)| (a.as_path(), b.as_path()))
836
+ .collect();
837
+ let feats = native_build_features_for_integration_test();
838
+ compile_many_to_native(&refs, Some(workspace_root().as_path()), &feats, true)
839
+ .unwrap_or_else(|e| panic!("compile_many_to_native: {}", e.message));
840
+ }
841
+
842
+ // Run each binary sequentially. Parallel `fs::copy` + `exec` caused Linux ETXTBSY (errno 26)
843
+ // in CI when several threads replaced/ran temp executables under load.
844
+ let errors: Vec<String> = entries_owned
845
+ .iter()
846
+ .enumerate()
847
+ .filter_map(|(run_index, (path, cached_bin))| {
848
+ let expected = match get_expected(path) {
732
849
  Some(e) => e,
733
850
  None => return Some(format!("missing expected: {}", path.display())),
734
851
  };
735
- let out_bin = compile_cached(&bin, &path, "native");
852
+ if !cached_bin.exists() {
853
+ return Some(format!("missing cached binary: {}", cached_bin.display()));
854
+ }
855
+ let stem = path.file_stem().unwrap().to_string_lossy();
856
+ let ext_bin = cached_bin
857
+ .extension()
858
+ .map(|e| e.to_string_lossy().to_string())
859
+ .unwrap_or_default();
860
+ let temp_dest = std::env::temp_dir().join(format!(
861
+ "tish_mvp_native_{}_{:x}_{}_{}",
862
+ stem,
863
+ file_content_hash(path),
864
+ std::process::id(),
865
+ run_index
866
+ ));
867
+ let temp_dest = if ext_bin.is_empty() {
868
+ temp_dest
869
+ } else {
870
+ temp_dest.with_extension(&ext_bin)
871
+ };
872
+ std::fs::copy(cached_bin, &temp_dest).expect("copy cached native bin to temp");
873
+ let out_bin = temp_dest;
736
874
  let out = match Command::new(&out_bin)
737
875
  .current_dir(workspace_root())
738
876
  .output()
@@ -129,15 +129,9 @@ pub enum ImportSpecifier {
129
129
  alias_span: Option<Span>,
130
130
  },
131
131
  /// Namespace: * as M
132
- Namespace {
133
- name: Arc<str>,
134
- name_span: Span,
135
- },
132
+ Namespace { name: Arc<str>, name_span: Span },
136
133
  /// Default: import X from "..."
137
- Default {
138
- name: Arc<str>,
139
- name_span: Span,
140
- },
134
+ Default { name: Arc<str>, name_span: Span },
141
135
  }
142
136
 
143
137
  /// Export declaration: named (const/let/fn) or default
@@ -616,7 +610,7 @@ impl Statement {
616
610
  | Statement::DoWhile { span, .. }
617
611
  | Statement::Throw { span, .. }
618
612
  | Statement::Try { span, .. }
619
- | Statement::Import { span, .. }
613
+ | Statement::Import { span, .. }
620
614
  | Statement::Export { span, .. }
621
615
  | Statement::TypeAlias { span, .. }
622
616
  | Statement::DeclareVar { span, .. }
@@ -6,6 +6,18 @@
6
6
  use std::fs;
7
7
  use std::path::{Path, PathBuf};
8
8
  use std::process::Command;
9
+ use std::sync::Mutex;
10
+
11
+ /// Serialize nested `cargo build` calls that share the workspace `target/` dir (tests + `tish build`).
12
+ static NESTED_CARGO_MUTEX: Mutex<()> = Mutex::new(());
13
+
14
+ fn mold_available() -> bool {
15
+ Command::new("mold")
16
+ .arg("--version")
17
+ .output()
18
+ .map(|o| o.status.success())
19
+ .unwrap_or(false)
20
+ }
9
21
 
10
22
  /// True if `root` looks like the Tish language repo (has `crates/tish_runtime`).
11
23
  ///
@@ -161,7 +173,11 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
161
173
  let candidate = dir.join("tish");
162
174
  if is_tish_workspace_root(&candidate) {
163
175
  return candidate.canonicalize().map_err(|e| {
164
- format!("Cannot canonicalize Tish workspace {}: {}", candidate.display(), e)
176
+ format!(
177
+ "Cannot canonicalize Tish workspace {}: {}",
178
+ candidate.display(),
179
+ e
180
+ )
165
181
  });
166
182
  }
167
183
  if !dir.pop() {
@@ -322,6 +338,11 @@ pub fn find_crate_path(crate_name: &str) -> Result<PathBuf, String> {
322
338
  Ok(crate_path)
323
339
  }
324
340
 
341
+ /// Sanitize a user-chosen output stem for Cargo `[lib]` / `[[bin]]` target names.
342
+ pub fn cargo_target_name(stem: &str) -> String {
343
+ stem.replace('-', "_")
344
+ }
345
+
325
346
  /// Create a temp build directory with src subdir.
326
347
  pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String> {
327
348
  let build_dir =
@@ -364,26 +385,36 @@ fn protoc_for_nested_cargo() -> Option<PathBuf> {
364
385
  }
365
386
 
366
387
  /// Run cargo build in the given directory.
367
- /// If target_dir is Some, use that for --target-dir (e.g. workspace target for caching).
368
- pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<(), String> {
388
+ /// If `cross_target` is Some, passes `--target` and skips `-C target-cpu=native`.
389
+ pub fn run_cargo_build(
390
+ build_dir: &Path,
391
+ target_dir: Option<&Path>,
392
+ cross_target: Option<&str>,
393
+ ) -> Result<(), String> {
394
+ let _nested_guard = NESTED_CARGO_MUTEX.lock().unwrap_or_else(|e| e.into_inner());
395
+
369
396
  let target_dir = target_dir
370
397
  .map(|p| p.to_path_buf())
371
398
  .unwrap_or_else(|| build_dir.join("target"));
372
- // Default to target-cpu=native so the emitted binary uses every SIMD / ISA
373
- // extension the build host supports. Callers can override by pre-setting
374
- // RUSTFLAGS in the environment.
375
- let existing_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
376
- let merged_rustflags = if existing_rustflags.is_empty() {
377
- "-C target-cpu=native".to_string()
378
- } else if existing_rustflags.contains("target-cpu") {
379
- existing_rustflags
380
- } else {
381
- format!("{} -C target-cpu=native", existing_rustflags)
382
- };
383
- // Nested `cargo build` (e.g. `tish build --native-backend rust`) inherits the parent
384
- // environment. CI often sets `RUSTC_WRAPPER=sccache`; wrapping this inner compile too can
385
- // cause flaky or failed builds (LTO / temp-crate paths). Use plain rustc here; the main
386
- // workspace build still benefits from the wrapper.
399
+ let fast_native = std::env::var("TISH_FAST_NATIVE_BUILD").as_deref() == Ok("1");
400
+
401
+ let mut merged_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
402
+ if cross_target.is_some() {
403
+ // Cross-compiling (e.g. iOS): do not use host target-cpu.
404
+ } else if fast_native {
405
+ if cfg!(target_os = "linux")
406
+ && mold_available()
407
+ && !merged_rustflags.contains("fuse-ld=mold")
408
+ {
409
+ merged_rustflags = format!("{} -C link-arg=-fuse-ld=mold", merged_rustflags.trim());
410
+ merged_rustflags = merged_rustflags.trim().to_string();
411
+ }
412
+ } else if merged_rustflags.is_empty() {
413
+ merged_rustflags = "-C target-cpu=native".to_string();
414
+ } else if !merged_rustflags.contains("target-cpu") {
415
+ merged_rustflags = format!("{} -C target-cpu=native", merged_rustflags);
416
+ }
417
+
387
418
  let mut cmd = Command::new("cargo");
388
419
  cmd.args(["build", "--release", "--target-dir"])
389
420
  .arg(&target_dir)
@@ -394,6 +425,14 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
394
425
  .env_remove("CARGO_BUILD_RUSTC_WRAPPER")
395
426
  .env("CARGO_TERM_PROGRESS", "always")
396
427
  .env("RUSTFLAGS", &merged_rustflags);
428
+ if let Some(triple) = cross_target {
429
+ cmd.arg("--target").arg(triple);
430
+ }
431
+ if fast_native {
432
+ cmd.env("CARGO_INCREMENTAL", "1");
433
+ } else {
434
+ cmd.env_remove("CARGO_INCREMENTAL");
435
+ }
397
436
  if let Some(protoc) = protoc_for_nested_cargo() {
398
437
  cmd.env("PROTOC", protoc);
399
438
  }
@@ -416,17 +455,29 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
416
455
  mod protoc_tests {
417
456
  use super::*;
418
457
 
458
+ #[test]
459
+ fn cargo_target_name_replaces_hyphens() {
460
+ assert_eq!(cargo_target_name("hello-ios"), "hello_ios");
461
+ assert_eq!(cargo_target_name("tish_out"), "tish_out");
462
+ }
463
+
419
464
  #[test]
420
465
  fn protoc_for_nested_cargo_without_env_uses_vendored_or_path() {
421
466
  let _lock = std::sync::Mutex::new(());
422
467
  let _guard = _lock.lock().unwrap();
423
468
  std::env::remove_var("PROTOC");
424
469
  let p = protoc_for_nested_cargo().expect("expected vendored or PATH protoc");
425
- assert!(
426
- p.exists(),
427
- "resolved protoc should exist: {}",
428
- p.display()
429
- );
470
+ assert!(p.exists(), "resolved protoc should exist: {}", p.display());
471
+ }
472
+ }
473
+
474
+ /// Find the built static library in target/release (or target/$TRIPLE/release).
475
+ pub fn find_release_staticlib(binary_dir: &Path, lib_name: &str) -> Result<PathBuf, String> {
476
+ let path = binary_dir.join(format!("lib{lib_name}.a"));
477
+ if path.exists() {
478
+ Ok(path)
479
+ } else {
480
+ Err(format!("Static library not found at {}", path.display()))
430
481
  }
431
482
  }
432
483