@tishlang/tish-format 3.0.0 → 3.2.2

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 (32) hide show
  1. package/bin/tish-format +0 -0
  2. package/crates/tish/src/cli_help.rs +4 -0
  3. package/crates/tish/src/main.rs +13 -2
  4. package/crates/tish/tests/rust_lib_crate.rs +140 -0
  5. package/crates/tish/tests/scope_merge.rs +31 -0
  6. package/crates/tish_build_utils/src/lib.rs +131 -6
  7. package/crates/tish_compile/src/codegen.rs +517 -19
  8. package/crates/tish_compile/src/lib.rs +14 -1
  9. package/crates/tish_compile/src/resolve.rs +12 -2
  10. package/crates/tish_compile/tests/regr_andor_rhs_move.rs +85 -0
  11. package/crates/tish_compile/tests/regr_rust_lib_emit.rs +150 -0
  12. package/crates/tish_core/src/shape.rs +28 -3
  13. package/crates/tish_fmt/Cargo.toml +1 -1
  14. package/crates/tish_fmt/src/lib.rs +54 -1
  15. package/crates/tish_native/src/build.rs +131 -0
  16. package/crates/tish_native/src/config.rs +14 -0
  17. package/crates/tish_runtime/Cargo.toml +20 -1
  18. package/crates/tish_runtime/src/crypto.rs +127 -0
  19. package/crates/tish_runtime/src/encoding.rs +157 -0
  20. package/crates/tish_runtime/src/fs_ext.rs +27 -0
  21. package/crates/tish_runtime/src/lib.rs +32 -0
  22. package/crates/tish_runtime/src/net.rs +482 -0
  23. package/crates/tish_runtime/src/process_spawn.rs +508 -0
  24. package/crates/tish_runtime/src/ws.rs +206 -23
  25. package/crates/tish_runtime/src/zip.rs +196 -0
  26. package/crates/tish_runtime_gba/src/lib.rs +17 -0
  27. package/package.json +1 -1
  28. package/platform/darwin-arm64/tish-fmt +0 -0
  29. package/platform/darwin-x64/tish-fmt +0 -0
  30. package/platform/linux-arm64/tish-fmt +0 -0
  31. package/platform/linux-x64/tish-fmt +0 -0
  32. package/platform/win32-x64/tish-fmt.exe +0 -0
package/bin/tish-format CHANGED
Binary file
@@ -404,6 +404,10 @@ pub fn build_after_help() -> String {
404
404
  Native executable (see --native-backend)
405
405
  {t}js{r}
406
406
  JavaScript bundle
407
+ {t}rust-lib{r}
408
+ Rust library source crate (crate-type = \"rlib\") at `-o <dir>` — one `pub fn` per
409
+ `export fn` of the entry module, over `tishlang_runtime::Value`. Cargo is NOT run; the
410
+ crate is the output, for the consumer to depend on. The Rust counterpart of `--target js`
407
411
  {t}wasm{r}
408
412
  WebAssembly (.tish project; .js source supported on some paths)
409
413
  {t}wasm-gpu{r}
@@ -914,9 +914,9 @@ fn build_file(
914
914
  .map_err(|e| e.to_string());
915
915
  }
916
916
 
917
- if target != "native" && target != "gba" {
917
+ if target != "native" && target != "gba" && target != "rust-lib" {
918
918
  return Err(format!(
919
- "Unknown target: {}. Use 'native', 'gba', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
919
+ "Unknown target: {}. Use 'native', 'gba', 'rust-lib', 'js', 'wasm', 'wasm-gpu', 'wasi', or 'bytecode'.",
920
920
  target
921
921
  ));
922
922
  }
@@ -935,6 +935,11 @@ fn build_file(
935
935
  return Err("tish build --target gba requires a .tish entry file.".to_string());
936
936
  }
937
937
  tishlang_native::NativeBuildConfig::gba()
938
+ } else if target == "rust-lib" {
939
+ if is_js {
940
+ return Err("tish build --target rust-lib requires a .tish entry file.".to_string());
941
+ }
942
+ tishlang_native::NativeBuildConfig::rust_lib()
938
943
  } else if let Some(triple) = ios_triple {
939
944
  tishlang_native::NativeBuildConfig::ios_staticlib(triple)
940
945
  } else if crate_type == "staticlib" {
@@ -982,6 +987,12 @@ fn build_file(
982
987
  .map_err(|e| e.to_string())?;
983
988
  }
984
989
 
990
+ // `rust-lib` writes a crate DIRECTORY at `-o`, and already reported it — the artifact-path
991
+ // rewriting below is all about locating a single built file, which does not apply.
992
+ if build_config.artifact == tishlang_native::NativeArtifact::RustLib {
993
+ return Ok(());
994
+ }
995
+
985
996
  let out_name = Path::new(output_path)
986
997
  .file_stem()
987
998
  .and_then(|s| s.to_str())
@@ -0,0 +1,140 @@
1
+ //! `tish build --target rust-lib` end to end: the emitted artifact is a Cargo **source crate**, and
2
+ //! cargo is deliberately not run — the consumer decides what to build it for.
3
+ //!
4
+ //! The fast test pins the crate layout. The behavioural test — module state surviving between
5
+ //! `pub fn` calls, a `clear*()` reassignment being visible to other exports, and the module body
6
+ //! running exactly once — actually builds the crate and a consumer, so it is gated behind
7
+ //! `TISH_SLOW_TESTS=1` to keep the default `cargo test` fast.
8
+ use std::path::{Path, PathBuf};
9
+
10
+ fn fixture() -> PathBuf {
11
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
12
+ .join("../../tests/regression/rust_lib_emit.tish")
13
+ .canonicalize()
14
+ .unwrap()
15
+ }
16
+
17
+ fn emit_crate(out: &Path) {
18
+ let entry = fixture();
19
+ tishlang_native::compile_to_native_with_config(
20
+ &entry,
21
+ entry.parent(),
22
+ out,
23
+ &[],
24
+ "rust",
25
+ true,
26
+ &tishlang_native::NativeBuildConfig::rust_lib(),
27
+ )
28
+ .expect("rust-lib emit");
29
+ }
30
+
31
+ #[test]
32
+ fn emits_an_rlib_source_crate_and_does_not_run_cargo() {
33
+ let out = std::env::temp_dir().join("tish_rustlib_layout");
34
+ let _ = std::fs::remove_dir_all(&out);
35
+ emit_crate(&out);
36
+
37
+ let manifest = std::fs::read_to_string(out.join("Cargo.toml")).expect("Cargo.toml");
38
+ assert!(
39
+ manifest.contains(r#"crate-type = ["rlib"]"#),
40
+ "expected an rlib library crate:\n{manifest}"
41
+ );
42
+ assert!(
43
+ manifest.contains("path = \"src/lib.rs\""),
44
+ "expected a lib entry point:\n{manifest}"
45
+ );
46
+ // Both `path` and `version` on the runtime dep: cargo prefers the path locally and falls back to
47
+ // the registry when the emitted crate is published, so it is publishable as-is.
48
+ assert!(
49
+ manifest.contains("tishlang_runtime = { path =") && manifest.contains("version = \""),
50
+ "the runtime dep must carry both path and version:\n{manifest}"
51
+ );
52
+
53
+ assert!(out.join("src/lib.rs").is_file(), "expected src/lib.rs");
54
+ assert!(
55
+ !out.join("src/main.rs").exists(),
56
+ "a library crate must not emit src/main.rs"
57
+ );
58
+ // Cargo was not invoked: no build output next to the emitted crate.
59
+ assert!(
60
+ !out.join("target").exists(),
61
+ "--target rust-lib must emit the crate and stop, not build it"
62
+ );
63
+ }
64
+
65
+ /// `TISH_SLOW_TESTS=1 cargo test -p tishlang --test rust_lib_crate` — compiles the emitted crate
66
+ /// plus a consumer that exercises the module-state guarantees.
67
+ #[test]
68
+ fn consumer_observes_persistent_module_state() {
69
+ if std::env::var("TISH_SLOW_TESTS").as_deref() != Ok("1") {
70
+ eprintln!("skipped (set TISH_SLOW_TESTS=1 to run the full cargo build)");
71
+ return;
72
+ }
73
+ let root = std::env::temp_dir().join("tish_rustlib_e2e");
74
+ let _ = std::fs::remove_dir_all(&root);
75
+ let lib = root.join("lib");
76
+ emit_crate(&lib);
77
+
78
+ let consumer = root.join("consumer");
79
+ std::fs::create_dir_all(consumer.join("src")).unwrap();
80
+ std::fs::write(
81
+ consumer.join("Cargo.toml"),
82
+ format!(
83
+ "[package]\nname = \"consumer\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
84
+ [workspace]\n\n[dependencies]\nlib = {{ package = \"lib\", path = {:?} }}\n",
85
+ lib.display().to_string()
86
+ ),
87
+ )
88
+ .unwrap();
89
+ std::fs::write(
90
+ consumer.join("src/main.rs"),
91
+ r#"
92
+ use lib::Value;
93
+ fn num(v: &Value) -> f64 { match v { Value::Number(x) => *x, _ => f64::NAN } }
94
+ fn text(v: &Value) -> String { match v { Value::String(x) => x.to_string(), _ => String::new() } }
95
+ fn main() {
96
+ // First call is not the first declared export: init must already have run.
97
+ assert_eq!(num(&lib::add(Value::Number(2.0), Value::Number(40.0))), 42.0, "add");
98
+ assert_eq!(num(&lib::initRuns()), 1.0, "module top level runs once");
99
+
100
+ lib::register(Value::String("kick".into()), Value::String("noiseBurst".into()));
101
+ assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "noiseBurst", "state persists");
102
+
103
+ // `clearAll` REASSIGNS the module-level binding; other exports must see that.
104
+ assert_eq!(num(&lib::clearAll()), 0.0, "clearAll empties");
105
+ assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "miss", "reassignment visible");
106
+
107
+ assert_eq!(num(&lib::initRuns()), 1.0, "still initialised once");
108
+
109
+ // A throw is parked for the next checkpoint, not unwound. Across the `pub fn` boundary there is
110
+ // no Tish frame to surface it in, so a parked throw used to sit latched and make every LATER
111
+ // call bail and return null. Reading a property off a null is all it takes.
112
+ let _ = lib::runtime::get_index(&Value::Null, &Value::String("nope".into()));
113
+ assert_eq!(
114
+ num(&lib::add(Value::Number(1.0), Value::Number(2.0))),
115
+ 3.0,
116
+ "a parked throw must not brick the module"
117
+ );
118
+ for _ in 0..20 {
119
+ let _ = lib::runtime::get_index(&Value::Null, &Value::String("nope".into()));
120
+ }
121
+ assert_eq!(text(&lib::lookup(Value::String("kick".into()))), "miss", "still working after 20");
122
+
123
+ println!("ALL_OK");
124
+ }
125
+ "#,
126
+ )
127
+ .unwrap();
128
+
129
+ let out = std::process::Command::new("cargo")
130
+ .args(["run", "-q"])
131
+ .current_dir(&consumer)
132
+ .output()
133
+ .expect("cargo run");
134
+ assert!(
135
+ String::from_utf8_lossy(&out.stdout).contains("ALL_OK"),
136
+ "consumer failed:\nstdout: {}\nstderr: {}",
137
+ String::from_utf8_lossy(&out.stdout),
138
+ String::from_utf8_lossy(&out.stderr)
139
+ );
140
+ }
@@ -77,3 +77,34 @@ fn scope_exports_tish_merges_as_source() {
77
77
  fn scope_plain_main_merges_as_source() {
78
78
  assert_merged_as_source(&setup(r#","main":"index.tish""#));
79
79
  }
80
+
81
+ /// The other half of the #38 split: a `@scope/pkg` that opts in via `tish.crate` (the tish-apple
82
+ /// hosts, published as `@tishlang/tish-macos` / `@tishlang/tish-ios`) routes to the NATIVE path —
83
+ /// lowered to `NativeModuleLoad`, not merged as source.
84
+ #[test]
85
+ fn scope_tish_crate_routes_native() {
86
+ let dir = setup(r#","tish":{"module":true,"crate":"test-greet","export":"greet_object"}"#);
87
+ let root = dir.path();
88
+ let modules = resolve_project(&root.join("main.tish"), Some(root)).expect("resolve");
89
+ let merged = merge_modules(modules).expect("merge");
90
+ let native_scope = merged.program.statements.iter().any(|s| {
91
+ matches!(
92
+ s,
93
+ Statement::VarDecl { init: Some(Expr::NativeModuleLoad { spec, .. }), .. }
94
+ if spec.as_ref() == "@test/greet"
95
+ )
96
+ });
97
+ assert!(
98
+ native_scope,
99
+ "@scope package with tish.crate must lower to NativeModuleLoad (native Rust-crate path)"
100
+ );
101
+ let has_greet = merged
102
+ .program
103
+ .statements
104
+ .iter()
105
+ .any(|s| matches!(s, Statement::FunDecl { name, .. } if name.as_ref() == "greet"));
106
+ assert!(
107
+ !has_greet,
108
+ "@scope package with tish.crate must NOT be merged as Tish source"
109
+ );
110
+ }
@@ -3,10 +3,11 @@
3
3
  //! Provides workspace discovery, path resolution, and Cargo build orchestration
4
4
  //! used by tishlang_wasm, tishlang_cranelift, tishlang_native, and the tish CLI.
5
5
 
6
+ use std::collections::BTreeSet;
6
7
  use std::fs;
7
8
  use std::path::{Path, PathBuf};
8
9
  use std::process::Command;
9
- use std::sync::Mutex;
10
+ use std::sync::{Mutex, OnceLock};
10
11
 
11
12
  /// Serialize nested `cargo build` calls that share the workspace `target/` dir (tests + `tish build`).
12
13
  static NESTED_CARGO_MUTEX: Mutex<()> = Mutex::new(());
@@ -19,6 +20,56 @@ fn mold_available() -> bool {
19
20
  .unwrap_or(false)
20
21
  }
21
22
 
23
+ /// The `target_feature="..."` values in `rustc --print cfg` output.
24
+ fn parse_target_features(print_cfg_stdout: &str) -> BTreeSet<String> {
25
+ print_cfg_stdout
26
+ .lines()
27
+ .filter_map(|l| l.trim().strip_prefix("target_feature=\""))
28
+ .filter_map(|l| l.strip_suffix('"'))
29
+ .map(str::to_owned)
30
+ .collect()
31
+ }
32
+
33
+ /// Host target features rustc reports, optionally under one extra `-C` flag.
34
+ fn rustc_target_features(codegen_flag: Option<&str>) -> Option<BTreeSet<String>> {
35
+ let rustc = std::env::var("RUSTC").unwrap_or_else(|_| "rustc".to_string());
36
+ let mut cmd = Command::new(rustc);
37
+ cmd.args(["--print", "cfg"]);
38
+ if let Some(flag) = codegen_flag {
39
+ cmd.args(["-C", flag]);
40
+ }
41
+ let out = cmd.output().ok()?;
42
+ if !out.status.success() {
43
+ return None;
44
+ }
45
+ Some(parse_target_features(&String::from_utf8_lossy(&out.stdout)))
46
+ }
47
+
48
+ /// True when `-C target-cpu=native` is worth passing on this host.
49
+ ///
50
+ /// `native` asks LLVM to identify the host core. On hosts it cannot identify — notably
51
+ /// the virtualized Apple Silicon behind GitHub Actions' macOS runners — it answers
52
+ /// `generic`, which is *narrower* than the target's own default CPU. On
53
+ /// aarch64-apple-darwin that silently drops `aes` and `sha2`, and crates that assert
54
+ /// those are statically present then fail to compile at all; `ring` 0.17 trips
55
+ /// `assertion failed: CAPS_STATIC == MIN_STATIC_FEATURES` in const-eval.
56
+ ///
57
+ /// So this compares feature sets rather than trusting `native` blindly: keep the flag
58
+ /// only when it is a strict win (every default feature survives). If rustc cannot be
59
+ /// queried, skip the flag — the target default is always correct, just less tuned.
60
+ fn native_target_cpu_is_safe() -> bool {
61
+ static SAFE: OnceLock<bool> = OnceLock::new();
62
+ *SAFE.get_or_init(|| {
63
+ match (
64
+ rustc_target_features(None),
65
+ rustc_target_features(Some("target-cpu=native")),
66
+ ) {
67
+ (Some(default), Some(native)) => default.is_subset(&native),
68
+ _ => false,
69
+ }
70
+ })
71
+ }
72
+
22
73
  /// True if `root` looks like the Tish language repo (has `crates/tish_runtime`).
23
74
  ///
24
75
  /// Used so we do not treat unrelated workspaces (e.g. a parent `zectre-platform` repo) as Tish
@@ -347,7 +398,13 @@ pub fn cargo_target_name(stem: &str) -> String {
347
398
  // A leading digit is prefixed with `_`; an empty result falls back to a fixed default.
348
399
  let mut name: String = stem
349
400
  .chars()
350
- .map(|c| if c.is_ascii_alphanumeric() || c == '_' { c } else { '_' })
401
+ .map(|c| {
402
+ if c.is_ascii_alphanumeric() || c == '_' {
403
+ c
404
+ } else {
405
+ '_'
406
+ }
407
+ })
351
408
  .collect();
352
409
  if name.is_empty() {
353
410
  return "tish_output".to_string();
@@ -371,6 +428,10 @@ pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String>
371
428
  }
372
429
 
373
430
  /// Run cargo build in the given directory.
431
+ ///
432
+ /// `-C target-cpu=native` is added only for host builds, only when the caller has not
433
+ /// already pinned a `target-cpu` in `RUSTFLAGS`, and only when `native` does not lose
434
+ /// target features against the target default (see `native_target_cpu_is_safe`).
374
435
  /// If `cross_target` is Some, passes `--target` and skips `-C target-cpu=native`.
375
436
  pub fn run_cargo_build(
376
437
  build_dir: &Path,
@@ -395,10 +456,14 @@ pub fn run_cargo_build(
395
456
  merged_rustflags = format!("{} -C link-arg=-fuse-ld=mold", merged_rustflags.trim());
396
457
  merged_rustflags = merged_rustflags.trim().to_string();
397
458
  }
398
- } else if merged_rustflags.is_empty() {
399
- merged_rustflags = "-C target-cpu=native".to_string();
400
- } else if !merged_rustflags.contains("target-cpu") {
401
- merged_rustflags = format!("{} -C target-cpu=native", merged_rustflags);
459
+ } else if !merged_rustflags.contains("target-cpu") && native_target_cpu_is_safe() {
460
+ // Caller did not pin a CPU and `native` resolves to something at least as
461
+ // capable as the target default — see `native_target_cpu_is_safe`.
462
+ merged_rustflags = if merged_rustflags.is_empty() {
463
+ "-C target-cpu=native".to_string()
464
+ } else {
465
+ format!("{} -C target-cpu=native", merged_rustflags)
466
+ };
402
467
  }
403
468
 
404
469
  let mut cmd = Command::new("cargo");
@@ -438,6 +503,66 @@ pub fn run_cargo_build(
438
503
  mod protoc_tests {
439
504
  use super::*;
440
505
 
506
+ /// Trimmed `rustc --print cfg --target aarch64-apple-darwin`.
507
+ const APPLE_DARWIN_DEFAULT_CFG: &str = r#"
508
+ debug_assertions
509
+ panic="unwind"
510
+ target_arch="aarch64"
511
+ target_feature="aes"
512
+ target_feature="neon"
513
+ target_feature="sha2"
514
+ target_feature="sha3"
515
+ target_os="macos"
516
+ "#;
517
+
518
+ #[test]
519
+ fn parses_target_features_from_print_cfg() {
520
+ let features = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
521
+ assert_eq!(
522
+ features.iter().map(String::as_str).collect::<Vec<_>>(),
523
+ ["aes", "neon", "sha2", "sha3"]
524
+ );
525
+ // Non-`target_feature` keys never leak in, and neither do the quotes.
526
+ assert!(!features.contains("aarch64"));
527
+ assert!(!features.contains("\"aes\""));
528
+ }
529
+
530
+ #[test]
531
+ fn native_cpu_rejected_when_it_drops_target_features() {
532
+ // What GitHub's virtualized Apple Silicon reports: `native` -> `generic`, which
533
+ // keeps only `neon`. Pinning it would break `ring`'s static feature assertions.
534
+ let default = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
535
+ let generic = parse_target_features("target_feature=\"neon\"\n");
536
+ assert!(!default.is_subset(&generic));
537
+ }
538
+
539
+ #[test]
540
+ fn native_cpu_accepted_when_it_only_adds() {
541
+ let default = parse_target_features(APPLE_DARWIN_DEFAULT_CFG);
542
+ let apple_m1 = parse_target_features(
543
+ "target_feature=\"aes\"\ntarget_feature=\"neon\"\n\
544
+ target_feature=\"sha2\"\ntarget_feature=\"sha3\"\ntarget_feature=\"lse\"\n",
545
+ );
546
+ assert!(default.is_subset(&apple_m1));
547
+ // Identical sets count as safe too — `native` matching the default is a no-op.
548
+ assert!(default.is_subset(&default));
549
+ }
550
+
551
+ #[test]
552
+ fn live_rustc_probe_still_parses() {
553
+ // Guards the parse against a `rustc --print cfg` format change: every target Tish
554
+ // builds on has at least one baseline target feature (x86_64 sse2, aarch64 neon).
555
+ // If this ever comes back empty the probe would silently disable target-cpu=native
556
+ // everywhere rather than only on hosts LLVM cannot identify.
557
+ let Some(features) = rustc_target_features(None) else {
558
+ return; // no rustc on PATH — nothing to assert
559
+ };
560
+ assert!(
561
+ !features.is_empty(),
562
+ "no target_feature in rustc --print cfg"
563
+ );
564
+ }
565
+
441
566
  #[test]
442
567
  fn cargo_target_name_replaces_hyphens() {
443
568
  assert_eq!(cargo_target_name("hello-ios"), "hello_ios");