@tishlang/tish-format 1.0.12 → 2.0.1

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 (189) hide show
  1. package/Cargo.toml +51 -0
  2. package/LICENSE +13 -0
  3. package/bin/tish-format +0 -0
  4. package/crates/js_to_tish/Cargo.toml +11 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +55 -0
  7. package/crates/js_to_tish/src/lib.rs +11 -0
  8. package/crates/js_to_tish/src/span_util.rs +35 -0
  9. package/crates/js_to_tish/src/transform/expr.rs +611 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +503 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +62 -0
  13. package/crates/tish/build.rs +21 -0
  14. package/crates/tish/src/cargo_native_registry.rs +32 -0
  15. package/crates/tish/src/cli_help.rs +576 -0
  16. package/crates/tish/src/main.rs +853 -0
  17. package/crates/tish/src/repl_completion.rs +199 -0
  18. package/crates/tish/tests/cargo_example_compile.rs +67 -0
  19. package/crates/tish/tests/error_source_location.rs +36 -0
  20. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  21. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  22. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  23. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  24. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  25. package/crates/tish/tests/fixtures/runtime_error_location.tish +5 -0
  26. package/crates/tish/tests/fixtures/trycatch_runtime_errors.tish +15 -0
  27. package/crates/tish/tests/fixtures/tty_capability.tish +9 -0
  28. package/crates/tish/tests/integration_test.rs +1406 -0
  29. package/crates/tish/tests/run_optimize_stdout_parity.rs +50 -0
  30. package/crates/tish/tests/shortcircuit.rs +65 -0
  31. package/crates/tish/tests/trycatch_runtime_errors.rs +45 -0
  32. package/crates/tish/tests/tty_capability.rs +43 -0
  33. package/crates/tish_ast/Cargo.toml +9 -0
  34. package/crates/tish_ast/src/ast.rs +649 -0
  35. package/crates/tish_ast/src/lib.rs +5 -0
  36. package/crates/tish_build_utils/Cargo.toml +11 -0
  37. package/crates/tish_build_utils/src/lib.rs +577 -0
  38. package/crates/tish_builtins/Cargo.toml +22 -0
  39. package/crates/tish_builtins/src/array.rs +803 -0
  40. package/crates/tish_builtins/src/collections.rs +481 -0
  41. package/crates/tish_builtins/src/construct.rs +199 -0
  42. package/crates/tish_builtins/src/date.rs +538 -0
  43. package/crates/tish_builtins/src/globals.rs +293 -0
  44. package/crates/tish_builtins/src/helpers.rs +35 -0
  45. package/crates/tish_builtins/src/iterator.rs +129 -0
  46. package/crates/tish_builtins/src/lib.rs +21 -0
  47. package/crates/tish_builtins/src/math.rs +89 -0
  48. package/crates/tish_builtins/src/number.rs +96 -0
  49. package/crates/tish_builtins/src/object.rs +36 -0
  50. package/crates/tish_builtins/src/string.rs +646 -0
  51. package/crates/tish_builtins/src/symbol.rs +83 -0
  52. package/crates/tish_builtins/src/typedarrays.rs +298 -0
  53. package/crates/tish_bytecode/Cargo.toml +17 -0
  54. package/crates/tish_bytecode/src/chunk.rs +164 -0
  55. package/crates/tish_bytecode/src/compiler.rs +2604 -0
  56. package/crates/tish_bytecode/src/encoding.rs +102 -0
  57. package/crates/tish_bytecode/src/lib.rs +20 -0
  58. package/crates/tish_bytecode/src/opcode.rs +185 -0
  59. package/crates/tish_bytecode/src/peephole.rs +189 -0
  60. package/crates/tish_bytecode/src/serialize.rs +193 -0
  61. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  62. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  63. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  64. package/crates/tish_compile/Cargo.toml +27 -0
  65. package/crates/tish_compile/src/check.rs +774 -0
  66. package/crates/tish_compile/src/codegen.rs +7317 -0
  67. package/crates/tish_compile/src/infer.rs +1681 -0
  68. package/crates/tish_compile/src/lib.rs +206 -0
  69. package/crates/tish_compile/src/resolve.rs +1951 -0
  70. package/crates/tish_compile/src/types.rs +605 -0
  71. package/crates/tish_compile_js/Cargo.toml +18 -0
  72. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  73. package/crates/tish_compile_js/src/codegen.rs +938 -0
  74. package/crates/tish_compile_js/src/error.rs +20 -0
  75. package/crates/tish_compile_js/src/lib.rs +26 -0
  76. package/crates/tish_compile_js/src/tests_jsx.rs +414 -0
  77. package/crates/tish_compiler_wasm/Cargo.toml +21 -0
  78. package/crates/tish_compiler_wasm/src/lib.rs +57 -0
  79. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +473 -0
  80. package/crates/tish_core/Cargo.toml +32 -0
  81. package/crates/tish_core/src/console_style.rs +170 -0
  82. package/crates/tish_core/src/json.rs +430 -0
  83. package/crates/tish_core/src/lib.rs +20 -0
  84. package/crates/tish_core/src/macros.rs +36 -0
  85. package/crates/tish_core/src/shape.rs +85 -0
  86. package/crates/tish_core/src/uri.rs +118 -0
  87. package/crates/tish_core/src/value.rs +1350 -0
  88. package/crates/tish_core/src/vmref.rs +183 -0
  89. package/crates/tish_cranelift/Cargo.toml +19 -0
  90. package/crates/tish_cranelift/src/lib.rs +43 -0
  91. package/crates/tish_cranelift/src/link.rs +130 -0
  92. package/crates/tish_cranelift/src/lower.rs +85 -0
  93. package/crates/tish_cranelift_runtime/Cargo.toml +26 -0
  94. package/crates/tish_cranelift_runtime/src/lib.rs +45 -0
  95. package/crates/tish_eval/Cargo.toml +51 -0
  96. package/crates/tish_eval/src/eval.rs +4265 -0
  97. package/crates/tish_eval/src/http.rs +191 -0
  98. package/crates/tish_eval/src/lib.rs +99 -0
  99. package/crates/tish_eval/src/natives.rs +551 -0
  100. package/crates/tish_eval/src/promise.rs +179 -0
  101. package/crates/tish_eval/src/regex.rs +299 -0
  102. package/crates/tish_eval/src/timers.rs +120 -0
  103. package/crates/tish_eval/src/value.rs +336 -0
  104. package/crates/tish_eval/src/value_convert.rs +117 -0
  105. package/crates/tish_ffi/Cargo.toml +26 -0
  106. package/crates/tish_ffi/src/lib.rs +518 -0
  107. package/crates/tish_ffi/tests/fixtures/testmod/Cargo.toml +18 -0
  108. package/crates/tish_ffi/tests/fixtures/testmod/src/lib.rs +46 -0
  109. package/crates/tish_ffi/tests/loader.rs +65 -0
  110. package/crates/tish_fmt/Cargo.toml +16 -0
  111. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  112. package/crates/tish_fmt/src/lib.rs +2157 -0
  113. package/crates/tish_jsx_web/Cargo.toml +9 -0
  114. package/crates/tish_jsx_web/README.md +5 -0
  115. package/crates/tish_jsx_web/src/lib.rs +2 -0
  116. package/crates/tish_lexer/Cargo.toml +9 -0
  117. package/crates/tish_lexer/src/lib.rs +1104 -0
  118. package/crates/tish_lexer/src/token.rs +170 -0
  119. package/crates/tish_lint/Cargo.toml +18 -0
  120. package/crates/tish_lint/src/bin/tish-lint.rs +195 -0
  121. package/crates/tish_lint/src/lib.rs +281 -0
  122. package/crates/tish_llvm/Cargo.toml +13 -0
  123. package/crates/tish_llvm/src/lib.rs +115 -0
  124. package/crates/tish_lsp/Cargo.toml +25 -0
  125. package/crates/tish_lsp/README.md +26 -0
  126. package/crates/tish_lsp/src/builtin_goto.rs +362 -0
  127. package/crates/tish_lsp/src/import_goto.rs +564 -0
  128. package/crates/tish_lsp/src/main.rs +1459 -0
  129. package/crates/tish_native/Cargo.toml +16 -0
  130. package/crates/tish_native/src/build.rs +481 -0
  131. package/crates/tish_native/src/config.rs +48 -0
  132. package/crates/tish_native/src/lib.rs +416 -0
  133. package/crates/tish_opt/Cargo.toml +13 -0
  134. package/crates/tish_opt/src/lib.rs +1046 -0
  135. package/crates/tish_parser/Cargo.toml +11 -0
  136. package/crates/tish_parser/src/lib.rs +386 -0
  137. package/crates/tish_parser/src/parser.rs +2726 -0
  138. package/crates/tish_pg/Cargo.toml +34 -0
  139. package/crates/tish_pg/README.md +38 -0
  140. package/crates/tish_pg/src/error.rs +52 -0
  141. package/crates/tish_pg/src/lib.rs +955 -0
  142. package/crates/tish_resolve/Cargo.toml +13 -0
  143. package/crates/tish_resolve/src/lib.rs +3601 -0
  144. package/crates/tish_resolve/src/pos.rs +141 -0
  145. package/crates/tish_runtime/Cargo.toml +100 -0
  146. package/crates/tish_runtime/src/http.rs +1347 -0
  147. package/crates/tish_runtime/src/http_fetch.rs +492 -0
  148. package/crates/tish_runtime/src/http_hyper.rs +441 -0
  149. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  150. package/crates/tish_runtime/src/lib.rs +1447 -0
  151. package/crates/tish_runtime/src/native_promise.rs +15 -0
  152. package/crates/tish_runtime/src/promise.rs +558 -0
  153. package/crates/tish_runtime/src/promise_io.rs +38 -0
  154. package/crates/tish_runtime/src/timers.rs +172 -0
  155. package/crates/tish_runtime/src/tty.rs +226 -0
  156. package/crates/tish_runtime/src/ws.rs +778 -0
  157. package/crates/tish_runtime/tests/fetch_readable_stream.rs +102 -0
  158. package/crates/tish_ui/Cargo.toml +17 -0
  159. package/crates/tish_ui/src/jsx.rs +692 -0
  160. package/crates/tish_ui/src/lib.rs +20 -0
  161. package/crates/tish_ui/src/runtime/hooks.rs +573 -0
  162. package/crates/tish_ui/src/runtime/mod.rs +183 -0
  163. package/crates/tish_vm/Cargo.toml +60 -0
  164. package/crates/tish_vm/src/jit.rs +1050 -0
  165. package/crates/tish_vm/src/lib.rs +41 -0
  166. package/crates/tish_vm/src/vm.rs +3536 -0
  167. package/crates/tish_vm/tests/concurrent_shared_state.rs +140 -0
  168. package/crates/tish_vm/tests/fixtures/or_string_cmd.tish +2 -0
  169. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  170. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +150 -0
  171. package/crates/tish_wasm/Cargo.toml +15 -0
  172. package/crates/tish_wasm/src/lib.rs +428 -0
  173. package/crates/tish_wasm_runtime/Cargo.toml +37 -0
  174. package/crates/tish_wasm_runtime/src/gpu.rs +429 -0
  175. package/crates/tish_wasm_runtime/src/lib.rs +42 -0
  176. package/crates/tishlang_cargo_bindgen/Cargo.toml +26 -0
  177. package/crates/tishlang_cargo_bindgen/src/classify.rs +261 -0
  178. package/crates/tishlang_cargo_bindgen/src/discover.rs +125 -0
  179. package/crates/tishlang_cargo_bindgen/src/infer.rs +382 -0
  180. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  181. package/crates/tishlang_cargo_bindgen/src/main.rs +167 -0
  182. package/crates/tishlang_cargo_bindgen/src/metadata.rs +117 -0
  183. package/justfile +276 -0
  184. package/package.json +2 -2
  185. package/platform/darwin-arm64/tish-fmt +0 -0
  186. package/platform/darwin-x64/tish-fmt +0 -0
  187. package/platform/linux-arm64/tish-fmt +0 -0
  188. package/platform/linux-x64/tish-fmt +0 -0
  189. package/platform/win32-x64/tish-fmt.exe +0 -0
@@ -0,0 +1,1406 @@
1
+ //! Full-stack integration tests: run .tish files with interpreter or each backend and compare
2
+ //! stdout to static expected files (e.g. `fn_any.tish.expected`).
3
+ //!
4
+ //! - Run: `cargo test -p tishlang` (or `cargo nextest run -p tishlang`).
5
+ //! - Generate/update expected files: `REGENERATE_EXPECTED=1 cargo test -p tishlangtest_mvp_programs_interpreter`
6
+ //! then commit the new/updated `tests/core/*.tish.expected` files.
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.
9
+
10
+ use std::collections::hash_map::DefaultHasher;
11
+ use std::ffi::OsString;
12
+ use std::hash::{Hash, Hasher};
13
+ use std::io::Read;
14
+ use std::path::{Path, PathBuf};
15
+ use std::process::Command;
16
+
17
+ use rayon::prelude::*;
18
+ use tishlang_native::compile_many_to_native;
19
+
20
+ fn workspace_root() -> PathBuf {
21
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
22
+ .join("..")
23
+ .join("..")
24
+ }
25
+
26
+ fn core_dir() -> PathBuf {
27
+ workspace_root().join("tests").join("core")
28
+ }
29
+
30
+ /// Path to the static expected stdout for a .tish file (e.g. fn_any.tish -> fn_any.tish.expected).
31
+ fn expected_path(path: &Path) -> PathBuf {
32
+ path.with_file_name(format!(
33
+ "{}.expected",
34
+ path.file_name().unwrap().to_string_lossy()
35
+ ))
36
+ }
37
+
38
+ /// Read static expected stdout for a test file. Returns None if the file does not exist.
39
+ fn get_expected(path: &Path) -> Option<String> {
40
+ let p = expected_path(path);
41
+ std::fs::read_to_string(&p).ok()
42
+ }
43
+
44
+ fn target_dir() -> PathBuf {
45
+ std::env::var("CARGO_TARGET_DIR")
46
+ .map(PathBuf::from)
47
+ .unwrap_or_else(|_| workspace_root().join("target"))
48
+ }
49
+
50
+ /// Cache dir for tish build outputs (under target/ so CI rust-cache restores it).
51
+ fn integration_compile_cache_dir() -> PathBuf {
52
+ target_dir().join("integration_compile_cache")
53
+ }
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
+ // Inference (struct/param/return typing — M1/M4/M5) also drives native emission, so the
90
+ // native batch cache must invalidate when it changes too.
91
+ let infer_rs = workspace_root().join("crates/tish_compile/src/infer.rs");
92
+ if infer_rs.is_file() {
93
+ file_content_hash(&infer_rs).hash(&mut h);
94
+ }
95
+ h.finish()
96
+ }
97
+
98
+ fn mvp_native_batch_cache_dir(combined: u64) -> PathBuf {
99
+ integration_compile_cache_dir()
100
+ .join("native_many")
101
+ .join(format!("{:016x}", combined))
102
+ }
103
+
104
+ /// Restores the previous process env when dropped (for `TISH_FAST_NATIVE_BUILD` in batch tests).
105
+ struct EnvVarGuard {
106
+ key: &'static str,
107
+ previous: Option<std::ffi::OsString>,
108
+ }
109
+
110
+ impl EnvVarGuard {
111
+ fn set(key: &'static str, value: &str) -> Self {
112
+ let previous = std::env::var_os(key);
113
+ std::env::set_var(key, value);
114
+ Self { key, previous }
115
+ }
116
+ }
117
+
118
+ impl Drop for EnvVarGuard {
119
+ fn drop(&mut self) {
120
+ match &self.previous {
121
+ None => std::env::remove_var(self.key),
122
+ Some(v) => std::env::set_var(self.key, v),
123
+ }
124
+ }
125
+ }
126
+
127
+ fn file_content_hash(path: &Path) -> u64 {
128
+ let mut f = std::fs::File::open(path).expect("open file for hash");
129
+ let mut content = Vec::new();
130
+ f.read_to_end(&mut content).expect("read file for hash");
131
+ let mut h = DefaultHasher::new();
132
+ path.to_string_lossy().hash(&mut h);
133
+ content.hash(&mut h);
134
+ h.finish()
135
+ }
136
+
137
+ /// Compile a .tish file with the given backend, using a persistent cache so we only run
138
+ /// `tish build` when the file or backend changed. Returns path to the compiled artifact
139
+ /// (binary, .js, or .wasm) in a temp dir; caller may run it and then delete it.
140
+ ///
141
+ /// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
142
+ /// compiles of the same file do not overwrite each other: .../cranelift/<stem>_<hash> vs .../wasi/<stem>_<hash>.wasm.
143
+ ///
144
+ /// The artifact **basename** must be unique per `(stem, hash, backend)`: nested `tish build`
145
+ /// uses it as the Cargo binary name under the workspace `target/release/`. If native and
146
+ /// cranelift both used `strict_equality_<hash>`, parallel `cargo nextest` could run those
147
+ /// tests concurrently and corrupt the same `target/release/...` output (Linux: ETXTBSY when
148
+ /// executing a binary still being written).
149
+ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
150
+ let stem = path.file_stem().unwrap().to_string_lossy();
151
+ // The cache must invalidate when the COMPILER changes, not only the `.tish` source: cranelift/wasi
152
+ // (and native) bake the VM/codegen/runtime into the artifact, so a stale cached binary built before
153
+ // a VM fix would silently use old behaviour. The `tish` binary's mtime is the precise signal — any
154
+ // VM/compiler/runtime/codegen source edit rebuilds it. Mix it into the key alongside the file hash.
155
+ let bin_stamp = std::fs::metadata(bin)
156
+ .and_then(|m| m.modified())
157
+ .ok()
158
+ .and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
159
+ .map(|d| d.as_nanos() as u64)
160
+ .unwrap_or(0);
161
+ let hash = {
162
+ let mut h = DefaultHasher::new();
163
+ file_content_hash(path).hash(&mut h);
164
+ bin_stamp.hash(&mut h);
165
+ h.finish()
166
+ };
167
+ let hash8 = &format!("{:016x}", hash)[..8];
168
+ let cache_base = integration_compile_cache_dir().join(backend);
169
+ let _ = std::fs::create_dir_all(&cache_base);
170
+ // Include `backend` in the leaf name so nested cargo bin names never collide across backends.
171
+ let leaf = format!("{}__{}__{}", stem, backend, hash8);
172
+
173
+ let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
174
+ "native" => {
175
+ let ext = if cfg!(target_os = "windows") {
176
+ ".exe"
177
+ } else {
178
+ ""
179
+ };
180
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
181
+ let args = vec![
182
+ OsString::from("build"),
183
+ OsString::from(path),
184
+ OsString::from("-o"),
185
+ OsString::from(&cached),
186
+ ];
187
+ (cached, args)
188
+ }
189
+ "cranelift" => {
190
+ let ext = if cfg!(target_os = "windows") {
191
+ ".exe"
192
+ } else {
193
+ ""
194
+ };
195
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
196
+ let args = vec![
197
+ OsString::from("build"),
198
+ OsString::from(path),
199
+ OsString::from("-o"),
200
+ OsString::from(&cached),
201
+ OsString::from("--native-backend"),
202
+ OsString::from("cranelift"),
203
+ ];
204
+ (cached, args)
205
+ }
206
+ "js" => {
207
+ let cached = cache_base.join(format!("{}.js", leaf));
208
+ let args = vec![
209
+ OsString::from("build"),
210
+ OsString::from(path),
211
+ OsString::from("--target"),
212
+ OsString::from("js"),
213
+ OsString::from("-o"),
214
+ OsString::from(&cached),
215
+ ];
216
+ (cached, args)
217
+ }
218
+ "wasi" => {
219
+ let out_base = cache_base.join(&leaf);
220
+ let artifact = out_base.with_extension("wasm");
221
+ let args = vec![
222
+ OsString::from("build"),
223
+ OsString::from(path),
224
+ OsString::from("-o"),
225
+ OsString::from(&out_base),
226
+ OsString::from("--target"),
227
+ OsString::from("wasi"),
228
+ ];
229
+ (artifact, args)
230
+ }
231
+ _ => panic!("unknown backend {}", backend),
232
+ };
233
+
234
+ if !artifact_path.exists() {
235
+ let out = Command::new(bin)
236
+ .args(compile_args)
237
+ .current_dir(workspace_root())
238
+ .output()
239
+ .expect("run tish build");
240
+ assert!(
241
+ out.status.success(),
242
+ "Compile failed for {} ({}): {}",
243
+ path.display(),
244
+ backend,
245
+ String::from_utf8_lossy(&out.stderr)
246
+ );
247
+ }
248
+
249
+ // Copy to temp so caller can run and delete without touching cache.
250
+ let ext = artifact_path
251
+ .extension()
252
+ .map(|e| e.to_string_lossy().to_string())
253
+ .unwrap_or_default();
254
+ let temp_dest =
255
+ std::env::temp_dir().join(format!("tish_cached_{}_{}_{}", backend, stem, hash8));
256
+ let temp_dest = if ext.is_empty() {
257
+ temp_dest
258
+ } else {
259
+ temp_dest.with_extension(ext)
260
+ };
261
+ std::fs::copy(&artifact_path, &temp_dest).expect("copy cached artifact to temp");
262
+ temp_dest
263
+ }
264
+
265
+ /// Path to the tish CLI binary. When running under cargo-llvm-cov, the build goes to
266
+ /// target/llvm-cov-target and CARGO_TARGET_DIR may not be set for the test process.
267
+ fn tish_bin() -> PathBuf {
268
+ let bin_name = if cfg!(target_os = "windows") {
269
+ "tish.exe"
270
+ } else {
271
+ "tish"
272
+ };
273
+ let default = target_dir().join("debug").join(bin_name);
274
+ if default.exists() {
275
+ return default;
276
+ }
277
+ let llvm_cov = workspace_root()
278
+ .join("target")
279
+ .join("llvm-cov-target")
280
+ .join("debug")
281
+ .join(bin_name);
282
+ if llvm_cov.exists() {
283
+ return llvm_cov;
284
+ }
285
+ default
286
+ }
287
+
288
+ /// tish -V and --version print the version.
289
+ #[test]
290
+ fn test_tish_version_flag() {
291
+ let bin = tish_bin();
292
+ assert!(
293
+ bin.exists(),
294
+ "tish binary not found. Run `cargo build -p tishlang` first."
295
+ );
296
+ let out = Command::new(&bin).arg("-V").output().expect("run tish -V");
297
+ assert!(
298
+ out.status.success(),
299
+ "tish -V failed: {}",
300
+ String::from_utf8_lossy(&out.stderr)
301
+ );
302
+ let stdout = String::from_utf8_lossy(&out.stdout);
303
+ assert!(
304
+ stdout.contains(env!("CARGO_PKG_VERSION")),
305
+ "tish -V should print version {}; got: {}",
306
+ env!("CARGO_PKG_VERSION"),
307
+ stdout
308
+ );
309
+ let out2 = Command::new(&bin)
310
+ .arg("--version")
311
+ .output()
312
+ .expect("run tish --version");
313
+ assert!(out2.status.success());
314
+ let stdout2 = String::from_utf8_lossy(&out2.stdout);
315
+ assert!(
316
+ stdout2.contains(env!("CARGO_PKG_VERSION")),
317
+ "tish --version should print version"
318
+ );
319
+ }
320
+
321
+ /// Parse async-await example (validates async fn parsing).
322
+ #[test]
323
+ fn test_async_await_parse() {
324
+ let path = workspace_root()
325
+ .join("examples")
326
+ .join("async-await")
327
+ .join("src")
328
+ .join("main.tish");
329
+ if path.exists() {
330
+ let source = std::fs::read_to_string(&path).unwrap();
331
+ let result = tishlang_parser::parse(&source);
332
+ assert!(
333
+ result.is_ok(),
334
+ "Parse failed for {}: {:?}",
335
+ path.display(),
336
+ result.err()
337
+ );
338
+ }
339
+ }
340
+
341
+ /// Invoke tish binary to compile async-await and run compiled output (validates non-blocking pipeline).
342
+ #[test]
343
+ #[cfg(feature = "http")]
344
+ fn test_async_await_compile_via_binary() {
345
+ let bin = tish_bin();
346
+ let path = workspace_root()
347
+ .join("examples")
348
+ .join("async-await")
349
+ .join("src")
350
+ .join("main.tish");
351
+ if path.exists() && bin.exists() {
352
+ let out = std::env::temp_dir().join("tish_async_test_out");
353
+ let compile_result = Command::new(&bin)
354
+ .args([
355
+ "build",
356
+ path.to_string_lossy().as_ref(),
357
+ "-o",
358
+ out.to_string_lossy().as_ref(),
359
+ ])
360
+ .current_dir(workspace_root())
361
+ .output();
362
+ let compile_out = compile_result.expect("run tish build");
363
+ assert!(
364
+ compile_out.status.success(),
365
+ "tish build failed: {}",
366
+ String::from_utf8_lossy(&compile_out.stderr)
367
+ );
368
+ // Run compiled binary to validate non-blocking fetchAll executes correctly
369
+ let run_result = Command::new(&out).current_dir(workspace_root()).output();
370
+ let run_out = run_result.expect("run compiled async binary");
371
+ assert!(
372
+ run_out.status.success(),
373
+ "compiled async binary failed: {}",
374
+ String::from_utf8_lossy(&run_out.stderr)
375
+ );
376
+ let stdout = String::from_utf8_lossy(&run_out.stdout);
377
+ assert!(
378
+ stdout.contains("Fetching"),
379
+ "expected output to mention fetching"
380
+ );
381
+ assert!(stdout.contains("Done"), "expected output to contain Done");
382
+ }
383
+ }
384
+
385
+ /// DEFINITIVE VALIDATION: Parallel fetches must be faster than sequential.
386
+ /// Uses httpbin.org/delay/1 (1s each). 3 parallel ≈ 1s, 3 sequential ≈ 3s.
387
+ #[test]
388
+ #[cfg(feature = "http")]
389
+ #[ignore = "timing and network sensitive; run manually: cargo test test_async_parallel_vs_sequential_timing -p tishlang--features http -- --ignored"]
390
+ fn test_async_parallel_vs_sequential_timing() {
391
+ let bin = tish_bin();
392
+ let parallel_src = workspace_root()
393
+ .join("examples")
394
+ .join("async-await")
395
+ .join("src")
396
+ .join("parallel.tish");
397
+ let sequential_src = workspace_root()
398
+ .join("examples")
399
+ .join("async-await")
400
+ .join("src")
401
+ .join("sequential.tish");
402
+ if !parallel_src.exists() || !sequential_src.exists() || !bin.exists() {
403
+ return;
404
+ }
405
+ let out_parallel = std::env::temp_dir().join("tish_parallel_timing");
406
+ let out_sequential = std::env::temp_dir().join("tish_sequential_timing");
407
+
408
+ // Compile both
409
+ let compile_par = Command::new(&bin)
410
+ .args([
411
+ "build",
412
+ parallel_src.to_string_lossy().as_ref(),
413
+ "-o",
414
+ out_parallel.to_string_lossy().as_ref(),
415
+ ])
416
+ .current_dir(workspace_root())
417
+ .output();
418
+ assert!(
419
+ compile_par.as_ref().unwrap().status.success(),
420
+ "compile parallel: {}",
421
+ String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr)
422
+ );
423
+
424
+ let compile_seq = Command::new(&bin)
425
+ .args([
426
+ "build",
427
+ sequential_src.to_string_lossy().as_ref(),
428
+ "-o",
429
+ out_sequential.to_string_lossy().as_ref(),
430
+ ])
431
+ .current_dir(workspace_root())
432
+ .output();
433
+ assert!(
434
+ compile_seq.as_ref().unwrap().status.success(),
435
+ "compile sequential: {}",
436
+ String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr)
437
+ );
438
+
439
+ // Run parallel and time
440
+ let t_parallel = std::time::Instant::now();
441
+ let run_par = Command::new(&out_parallel)
442
+ .current_dir(workspace_root())
443
+ .output();
444
+ let elapsed_parallel = t_parallel.elapsed();
445
+ assert!(
446
+ run_par.as_ref().unwrap().status.success(),
447
+ "run parallel: {}",
448
+ String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr)
449
+ );
450
+
451
+ // Run sequential and time
452
+ let t_sequential = std::time::Instant::now();
453
+ let run_seq = Command::new(&out_sequential)
454
+ .current_dir(workspace_root())
455
+ .output();
456
+ let elapsed_sequential = t_sequential.elapsed();
457
+ assert!(
458
+ run_seq.as_ref().unwrap().status.success(),
459
+ "run sequential: {}",
460
+ String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr)
461
+ );
462
+
463
+ // PARALLEL MUST BE FASTER: parallel < sequential * 0.6 (parallel ~1s, sequential ~3s)
464
+ let parallel_secs = elapsed_parallel.as_secs_f64();
465
+ let sequential_secs = elapsed_sequential.as_secs_f64();
466
+ assert!(
467
+ parallel_secs < sequential_secs * 0.6,
468
+ "Async NOT validated: parallel took {:.2}s but sequential took {:.2}s. Parallel must be < 60% of sequential to prove non-blocking.",
469
+ parallel_secs,
470
+ sequential_secs
471
+ );
472
+ }
473
+
474
+ /// Run async-await example via tishlang_eval (same path as `tish run`).
475
+ /// Ignored: tishlang_eval::run() is synchronous and does not run the event loop.
476
+ #[test]
477
+ #[cfg(feature = "http")]
478
+ #[ignore = "requires async runtime; use test_async_await_compile_via_binary for CI"]
479
+ fn test_async_await_run() {
480
+ let path = workspace_root()
481
+ .join("examples")
482
+ .join("async-await")
483
+ .join("src")
484
+ .join("main.tish");
485
+ if path.exists() {
486
+ let source = std::fs::read_to_string(&path).unwrap();
487
+ let result = tishlang_eval::run(&source);
488
+ assert!(
489
+ result.is_ok(),
490
+ "Run failed for {}: {:?}",
491
+ path.display(),
492
+ result.err()
493
+ );
494
+ }
495
+ }
496
+
497
+ /// `promise.tish` — full Promise API including `new Promise(executor)`, `.then`, `.catch`,
498
+ /// `all`, `race`, `any`, `allSettled`. Runs via the binary (vm + interp), asserts exact output.
499
+ /// This was previously `#[ignore]`'d and only checked "doesn't error" — the old fixture also
500
+ /// avoided `new Promise` entirely, hiding a bug where the executor never ran on the VM. Now
501
+ /// CI-gated and output-asserting. vm ≡ interp.
502
+ #[test]
503
+ fn test_promise_core() {
504
+ let bin = tish_bin();
505
+ if !bin.exists() {
506
+ return;
507
+ }
508
+ let path = workspace_root()
509
+ .join("tests")
510
+ .join("modules")
511
+ .join("promise.tish");
512
+ if !path.exists() {
513
+ return;
514
+ }
515
+ let expected = "\
516
+ new Promise: new-ctor
517
+ Promise sync resolve: 42
518
+ Promise.resolve: 100
519
+ Promise.reject caught: true
520
+ .then chain: 4
521
+ .catch: handled: fail
522
+ Promise.all: 1 2 3
523
+ Promise.race: fast
524
+ Promise.any: any-win
525
+ Promise.allSettled: fulfilled rejected reason
526
+ Promise tests completed
527
+ ";
528
+ for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
529
+ let mut args = backend_args.clone();
530
+ args.push(path.to_string_lossy().to_string().leak());
531
+ let out = Command::new(&bin)
532
+ .args(&args)
533
+ .current_dir(workspace_root())
534
+ .output()
535
+ .expect("run tish binary");
536
+ assert!(
537
+ out.status.success(),
538
+ "promise.tish ({:?}) failed: stderr={}",
539
+ backend_args,
540
+ String::from_utf8_lossy(&out.stderr)
541
+ );
542
+ assert_eq!(
543
+ String::from_utf8_lossy(&out.stdout),
544
+ expected,
545
+ "Promise output mismatch on backend {:?} — check new Promise/any/allSettled regressions",
546
+ backend_args
547
+ );
548
+ }
549
+ }
550
+
551
+ /// Import aliasing — `{ x as y }` rename and `* as M` namespace. `as` is a dedicated keyword
552
+ /// token (shared with type casts), so the import-specifier parser must accept it in that position.
553
+ /// Regression: previously `import { a as b }` failed with "Expected Comma, got As". vm ≡ interp.
554
+ #[test]
555
+ fn test_import_alias() {
556
+ let bin = tish_bin();
557
+ if !bin.exists() {
558
+ return;
559
+ }
560
+ let path = workspace_root()
561
+ .join("tests")
562
+ .join("modules")
563
+ .join("import_alias.tish");
564
+ if !path.exists() {
565
+ return;
566
+ }
567
+ let expected = "42\nhi there\n42\n1.0\n";
568
+ for backend_args in [vec!["run"], vec!["run", "--backend", "vm"]] {
569
+ let mut args = backend_args.clone();
570
+ args.push(path.to_string_lossy().to_string().leak());
571
+ let out = Command::new(&bin)
572
+ .args(&args)
573
+ .current_dir(workspace_root())
574
+ .output()
575
+ .expect("run tish binary");
576
+ assert!(
577
+ out.status.success(),
578
+ "import_alias.tish ({:?}) failed: stderr={}",
579
+ backend_args,
580
+ String::from_utf8_lossy(&out.stderr)
581
+ );
582
+ assert_eq!(
583
+ String::from_utf8_lossy(&out.stdout),
584
+ expected,
585
+ "import alias output mismatch on backend {:?}",
586
+ backend_args
587
+ );
588
+ }
589
+ }
590
+
591
+ /// Combined validation: async/await + Promise + setTimeout + multiple HTTP requests.
592
+ /// #97: a module's non-exported top-level bindings stay private — a same-named binding in
593
+ /// another module must not overwrite them (runtime), and the `--target js` bundle must not
594
+ /// emit duplicate `let` declarations (SyntaxError). Verified identical across interp / VM /
595
+ /// native / node, including parameter and inner-`let` shadowing.
596
+ #[test]
597
+ fn test_module_private_binding_isolation() {
598
+ let bin = tish_bin();
599
+ if !bin.exists() {
600
+ return;
601
+ }
602
+ let path = workspace_root()
603
+ .join("tests")
604
+ .join("modules")
605
+ .join("private_isolation.tish");
606
+ if !path.exists() {
607
+ return;
608
+ }
609
+ let expected = "from-a:helper-a from-b:helper-b\narg inner\nhelper-b\n";
610
+
611
+ // Interpreter + VM via `tish run`.
612
+ for backend in ["interp", "vm"] {
613
+ let out = Command::new(&bin)
614
+ .args(["run", "--backend", backend])
615
+ .arg(&path)
616
+ .current_dir(workspace_root())
617
+ .output()
618
+ .expect("run tish binary");
619
+ assert!(
620
+ out.status.success(),
621
+ "private_isolation.tish ({backend}) failed: stderr={}",
622
+ String::from_utf8_lossy(&out.stderr)
623
+ );
624
+ assert_eq!(
625
+ String::from_utf8_lossy(&out.stdout),
626
+ expected,
627
+ "module private-binding isolation mismatch on backend {backend}"
628
+ );
629
+ }
630
+
631
+ // Native: compile + run.
632
+ let native_bin = compile_cached(&bin, &path, "native");
633
+ let out = Command::new(&native_bin)
634
+ .current_dir(workspace_root())
635
+ .output()
636
+ .expect("run native binary");
637
+ let _ = std::fs::remove_file(&native_bin);
638
+ assert!(
639
+ out.status.success(),
640
+ "private_isolation native run failed: stderr={}",
641
+ String::from_utf8_lossy(&out.stderr)
642
+ );
643
+ assert_eq!(
644
+ String::from_utf8_lossy(&out.stdout),
645
+ expected,
646
+ "module private-binding isolation mismatch on native backend"
647
+ );
648
+
649
+ // JS target: compile + run through Node (also asserts no duplicate-`let` SyntaxError).
650
+ let node_available = Command::new("node")
651
+ .arg("--version")
652
+ .output()
653
+ .map(|o| o.status.success())
654
+ .unwrap_or(false);
655
+ if node_available {
656
+ let out_js = compile_cached(&bin, &path, "js");
657
+ let out = Command::new("node")
658
+ .arg(&out_js)
659
+ .current_dir(workspace_root())
660
+ .output()
661
+ .expect("run node");
662
+ let _ = std::fs::remove_file(&out_js);
663
+ assert!(
664
+ out.status.success(),
665
+ "private_isolation JS run failed: stderr={}",
666
+ String::from_utf8_lossy(&out.stderr)
667
+ );
668
+ assert_eq!(
669
+ String::from_utf8_lossy(&out.stdout),
670
+ expected,
671
+ "module private-binding isolation mismatch on JS target"
672
+ );
673
+ }
674
+ }
675
+
676
+ /// Ignored: tishlang_eval::run() does not run the event loop.
677
+ #[test]
678
+ #[cfg(feature = "http")]
679
+ #[ignore = "requires async runtime"]
680
+ fn test_async_promise_settimeout_combined() {
681
+ let path = workspace_root()
682
+ .join("tests")
683
+ .join("modules")
684
+ .join("async_promise_settimeout.tish");
685
+ if path.exists() {
686
+ let source = std::fs::read_to_string(&path).unwrap();
687
+ let result = tishlang_eval::run(&source);
688
+ assert!(
689
+ result.is_ok(),
690
+ "Failed to run async_promise_settimeout: {:?}",
691
+ result.err()
692
+ );
693
+ }
694
+ }
695
+
696
+ /// VM run with Date global (resolve+merge+bytecode+run pipeline).
697
+ #[test]
698
+ fn test_vm_date_now() {
699
+ let path = workspace_root()
700
+ .join("tests")
701
+ .join("core")
702
+ .join("date.tish");
703
+ if !path.exists() {
704
+ return;
705
+ }
706
+ // Library path
707
+ let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
708
+ tishlang_compile::detect_cycles(&modules).expect("cycles");
709
+ let program = tishlang_compile::merge_modules(modules)
710
+ .expect("merge")
711
+ .program;
712
+ let chunk = tishlang_bytecode::compile(&program).expect("compile");
713
+ let result = tishlang_vm::run(&chunk);
714
+ assert!(
715
+ result.is_ok(),
716
+ "VM run (library) failed: {:?}",
717
+ result.err()
718
+ );
719
+ // Binary path - same flow as `tish run <file>`
720
+ let bin = tish_bin();
721
+ if bin.exists() {
722
+ let out = Command::new(&bin)
723
+ .args(["run", path.to_string_lossy().as_ref()])
724
+ .current_dir(workspace_root())
725
+ .output()
726
+ .expect("run tish binary");
727
+ assert!(
728
+ out.status.success(),
729
+ "tish run failed: stdout={} stderr={}",
730
+ String::from_utf8_lossy(&out.stdout),
731
+ String::from_utf8_lossy(&out.stderr)
732
+ );
733
+ }
734
+ }
735
+
736
+ /// `Promise.any`, `Promise.allSettled`, fixed `Promise.race` — cross-backend, network-free, CI-gated.
737
+ /// vm ≡ interp exact match.
738
+ #[test]
739
+ fn test_promise_combinators() {
740
+ let bin = tish_bin();
741
+ if !bin.exists() {
742
+ return;
743
+ }
744
+ let path = workspace_root()
745
+ .join("tests")
746
+ .join("modules")
747
+ .join("promise_combinators.tish");
748
+ if !path.exists() {
749
+ return;
750
+ }
751
+ let expected = "\
752
+ any first-fulfilled: win
753
+ any all-rejected: [\"e1\",\"e2\"]
754
+ allSettled[0] ok: 10
755
+ allSettled[1] rejected: boom
756
+ allSettled[2] ok: 30
757
+ race winner: A
758
+ any passthrough: 42
759
+ ";
760
+ for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
761
+ let mut args = backend_args.clone();
762
+ args.push(path.to_string_lossy().to_string().leak());
763
+ let out = Command::new(&bin)
764
+ .args(&args)
765
+ .current_dir(workspace_root())
766
+ .output()
767
+ .expect("run tish binary");
768
+ assert!(
769
+ out.status.success(),
770
+ "promise_combinators ({:?}) failed: stderr={}",
771
+ backend_args,
772
+ String::from_utf8_lossy(&out.stderr)
773
+ );
774
+ assert_eq!(
775
+ String::from_utf8_lossy(&out.stdout),
776
+ expected,
777
+ "Promise.any/allSettled/race divergence on backend {:?}",
778
+ backend_args
779
+ );
780
+ }
781
+ }
782
+
783
+ /// Pins tish's DOCUMENTED async/Promise ordering (docs/concurrency-model.md) — network-free, so it
784
+ /// runs in CI (unlike the `#[ignore]`'d network async tests). Asserts Promise.all order + non-promise
785
+ /// passthrough, `.then` chaining, await-reject catch, Promise.all reject short-circuit, AND the
786
+ /// deliberate blocking signature: a 0ms `setTimeout` queued before an `await` fires LAST (a JS event
787
+ /// loop would interleave it earlier — tish's `await` blocks instead of yielding). NOT compared to node
788
+ /// on purpose; this is tish's own contract. vm ≡ interp guards cross-backend agreement.
789
+ #[test]
790
+ fn test_async_ordering_documented() {
791
+ let bin = tish_bin();
792
+ if !bin.exists() {
793
+ return;
794
+ }
795
+ let path = workspace_root()
796
+ .join("tests")
797
+ .join("modules")
798
+ .join("async_ordering.tish");
799
+ if !path.exists() {
800
+ return;
801
+ }
802
+ // tish's documented, deliberately-non-JS ordering (timer drains last because await blocks).
803
+ let expected = "\
804
+ 1: sync-start
805
+ 2: await = 42
806
+ 2b: new Promise = ctor-ran
807
+ 3: all = a b c
808
+ 4: chain = 13
809
+ 5: caught = boom
810
+ 6: all-reject = rej
811
+ 7: post-await = after-timer-was-queued
812
+ 9: sync-end
813
+ 8: timer-fires-LAST (await did not yield)
814
+ ";
815
+ // Both the bytecode VM and the tree-walking interpreter must agree on this contract.
816
+ for backend_args in [vec!["run"], vec!["run", "--backend", "interp"]] {
817
+ let mut args = backend_args.clone();
818
+ args.push(path.to_string_lossy().to_string().leak());
819
+ let out = Command::new(&bin)
820
+ .args(&args)
821
+ .current_dir(workspace_root())
822
+ .output()
823
+ .expect("run tish binary");
824
+ assert!(
825
+ out.status.success(),
826
+ "async_ordering ({:?}) failed: stderr={}",
827
+ backend_args,
828
+ String::from_utf8_lossy(&out.stderr)
829
+ );
830
+ assert_eq!(
831
+ String::from_utf8_lossy(&out.stdout),
832
+ expected,
833
+ "async ordering divergence on backend {:?} — the documented blocking-await/timer contract changed",
834
+ backend_args
835
+ );
836
+ }
837
+ }
838
+
839
+ /// VM run with parse+compile only (no resolve/merge) - isolates bytecode IndexAssign.
840
+ #[test]
841
+ fn test_vm_index_assign_direct() {
842
+ let source = r#"let arr = [1, 2, 3]; arr[1] = 99; console.log(arr[1]);"#;
843
+ let program = tishlang_parser::parse(source).expect("parse");
844
+ let chunk = tishlang_bytecode::compile(&program).expect("compile");
845
+ let result = tishlang_vm::run(&chunk);
846
+ assert!(result.is_ok(), "VM IndexAssign failed: {:?}", result.err());
847
+ }
848
+
849
+ /// VM run via resolve+merge (same as tish run) - must also pass.
850
+ #[test]
851
+ fn test_vm_index_assign_via_resolve() {
852
+ let path = workspace_root()
853
+ .join("tests")
854
+ .join("core")
855
+ .join("array_sort_minimal.tish");
856
+ let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
857
+ tishlang_compile::detect_cycles(&modules).expect("cycles");
858
+ let program = tishlang_compile::merge_modules(modules)
859
+ .expect("merge")
860
+ .program;
861
+ let chunk = tishlang_bytecode::compile(&program).expect("compile");
862
+ let result = tishlang_vm::run(&chunk);
863
+ assert!(
864
+ result.is_ok(),
865
+ "VM IndexAssign via resolve failed: {:?}",
866
+ result.err()
867
+ );
868
+ }
869
+
870
+ /// tish run binary must pass array_sort_minimal (ensures CLI works).
871
+ #[test]
872
+ fn test_tish_run_index_assign() {
873
+ let bin = tish_bin();
874
+ let path = workspace_root()
875
+ .join("tests")
876
+ .join("core")
877
+ .join("array_sort_minimal.tish");
878
+ if !bin.exists() {
879
+ eprintln!("Skipping: tish binary not built");
880
+ return;
881
+ }
882
+ let out = Command::new(&bin)
883
+ .args(["run", path.to_string_lossy().as_ref()])
884
+ .current_dir(workspace_root())
885
+ .output()
886
+ .expect("run tish");
887
+ assert!(
888
+ out.status.success(),
889
+ "tish run failed: stdout={} stderr={}",
890
+ String::from_utf8_lossy(&out.stdout),
891
+ String::from_utf8_lossy(&out.stderr)
892
+ );
893
+ assert!(
894
+ String::from_utf8_lossy(&out.stdout).contains("pass"),
895
+ "Expected 'pass' in output"
896
+ );
897
+ }
898
+
899
+ /// Full stack: lex + parse each .tish file and assert no parse error.
900
+ #[test]
901
+ fn test_full_stack_parse() {
902
+ let core_dir = core_dir();
903
+ for entry in std::fs::read_dir(&core_dir).unwrap() {
904
+ let path = entry.unwrap().path();
905
+ if path.extension().map(|e| e == "tish").unwrap_or(false) {
906
+ let source = std::fs::read_to_string(&path).unwrap();
907
+ let result = tishlang_parser::parse(&source);
908
+ assert!(
909
+ result.is_ok(),
910
+ "Parse failed for {}: {:?}",
911
+ path.display(),
912
+ result.err()
913
+ );
914
+ }
915
+ }
916
+ }
917
+
918
+ // (The hand-maintained `MVP_TEST_FILES` allowlist was removed in favor of `discover_core_tests()`
919
+ // below — every `tests/core/*.tish` with a `.expected` now gets cross-backend coverage automatically.)
920
+
921
+ /// Tests whose `.expected` embeds elapsed-ms timings (perf/stress/probe) — nondeterministic, so
922
+ /// excluded from exact-output comparison. Run them via `just perf-*` instead.
923
+ const TIMING_NONDETERMINISTIC: &[&str] = &[
924
+ "array_stress.tish",
925
+ "array_stress_01_large_array_creation.tish",
926
+ "array_stress_02_iteration.tish",
927
+ "array_stress_03_map_filter_reduce.tish",
928
+ "array_stress_04_chained.tish",
929
+ "array_stress_05_sorting.tish",
930
+ "array_stress_06_search.tish",
931
+ "array_stress_07_splice_slice.tish",
932
+ "array_stress_08_concat_spread.tish",
933
+ "array_stress_09_flat.tish",
934
+ "array_stress_10_objects.tish",
935
+ "basic_types.tish",
936
+ "benchmark_granular.tish",
937
+ "new_features_perf.tish",
938
+ "object_stress.tish",
939
+ "objects_perf.tish",
940
+ "string_methods_perf.tish",
941
+ "recursion_stress.tish",
942
+ "jit_probe.tish",
943
+ ];
944
+
945
+ /// Known cross-backend gaps skipped on the interp↔vm parity check, with reason + a tracking note.
946
+ /// Each is a real divergence to fix, not a permanent exclusion.
947
+ ///
948
+ /// Empty: the former `nested_complex.tish` gap (the VM's fixed 2-level `enclosing` lost captures
949
+ /// >2 levels deep — `level4` couldn't see `level1`'s `a`) is fixed. The VM now captures the full
950
+ /// lexical chain (`Vm.enclosing: Vec<ScopeMap>`), so closures nested arbitrarily deep resolve
951
+ /// every ancestor's locals. interp↔vm parity holds for all discovered tests.
952
+ const VM_PARITY_SKIP: &[&str] = &[];
953
+
954
+ /// Discover every `tests/core/*.tish` that has a `.expected` sibling, minus timing-nondeterministic
955
+ /// ones. Replaces the hand-maintained `MVP_TEST_FILES` allowlist so a new `*.tish` + `*.expected`
956
+ /// gets cross-backend coverage automatically (the allowlist silently left ~38 tests running nowhere).
957
+ fn discover_core_tests() -> Vec<String> {
958
+ let mut v: Vec<String> = std::fs::read_dir(core_dir())
959
+ .expect("read tests/core")
960
+ .filter_map(|e| {
961
+ let p = e.ok()?.path();
962
+ if p.extension().map(|x| x == "tish").unwrap_or(false) && expected_path(&p).exists() {
963
+ Some(p.file_name()?.to_string_lossy().into_owned())
964
+ } else {
965
+ None
966
+ }
967
+ })
968
+ .filter(|n| !TIMING_NONDETERMINISTIC.contains(&n.as_str()))
969
+ .collect();
970
+ v.sort();
971
+ v
972
+ }
973
+
974
+ /// True if `name` has a sibling `.js` (so it can run through the Node oracle).
975
+ fn has_js_sibling(name: &str) -> bool {
976
+ core_dir()
977
+ .join(name)
978
+ .with_extension("js")
979
+ .exists()
980
+ }
981
+
982
+ /// The gradual type checker must produce ZERO diagnostics on the (valid) corpus — any diagnostic is
983
+ /// a false positive. Lists every offender so they can be inspected/fixed.
984
+ #[test]
985
+ fn checker_no_false_positives_on_corpus() {
986
+ let mut flagged: Vec<String> = Vec::new();
987
+ for name in discover_core_tests() {
988
+ let src = std::fs::read_to_string(core_dir().join(&name)).unwrap();
989
+ if let Ok(prog) = tishlang_parser::parse(&src) {
990
+ let diags = tishlang_compile::check_program(&prog);
991
+ if !diags.is_empty() {
992
+ let msgs: Vec<String> = diags.iter().map(|d| d.message.clone()).collect();
993
+ flagged.push(format!("{name}: {}", msgs.join(" | ")));
994
+ }
995
+ }
996
+ }
997
+ assert!(
998
+ flagged.is_empty(),
999
+ "type checker flagged valid corpus programs (false positives):\n{}",
1000
+ flagged.join("\n")
1001
+ );
1002
+ }
1003
+
1004
+ /// Run each .tish file with interpreter and compare stdout to static expected.
1005
+ /// Set REGENERATE_EXPECTED=1 to write .expected files from interpreter output (run once, then commit).
1006
+ #[test]
1007
+ fn test_mvp_programs_interpreter() {
1008
+ let core_dir = core_dir();
1009
+ let bin = tish_bin();
1010
+ assert!(
1011
+ bin.exists(),
1012
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1013
+ bin.display()
1014
+ );
1015
+ let regenerate = std::env::var("REGENERATE_EXPECTED").as_deref() == Ok("1");
1016
+ for name in &discover_core_tests() {
1017
+ let path = core_dir.join(name);
1018
+ if !path.exists() {
1019
+ continue;
1020
+ }
1021
+ let path_str = path.to_string_lossy();
1022
+ let out = Command::new(&bin)
1023
+ .args(["run", path_str.as_ref(), "--backend", "interp"])
1024
+ .current_dir(workspace_root())
1025
+ .output()
1026
+ .expect("run tish interpreter");
1027
+ assert!(
1028
+ out.status.success(),
1029
+ "Interpreter failed for {}: {}",
1030
+ path.display(),
1031
+ String::from_utf8_lossy(&out.stderr)
1032
+ );
1033
+ let stdout = String::from_utf8_lossy(&out.stdout).to_string();
1034
+ if regenerate {
1035
+ std::fs::write(expected_path(&path), &stdout).expect("write expected");
1036
+ } else {
1037
+ let expected = get_expected(&path).unwrap_or_else(|| {
1038
+ panic!(
1039
+ "missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
1040
+ path.display()
1041
+ )
1042
+ });
1043
+ assert_eq!(
1044
+ stdout,
1045
+ expected,
1046
+ "Interpreter output mismatch for {}",
1047
+ path.display()
1048
+ );
1049
+ }
1050
+ }
1051
+ }
1052
+
1053
+ /// Default bytecode VM must match the tree-walking interpreter for every MVP program.
1054
+ #[test]
1055
+ fn test_mvp_programs_interp_vm_stdout_parity() {
1056
+ let core_dir = core_dir();
1057
+ let bin = tish_bin();
1058
+ assert!(
1059
+ bin.exists(),
1060
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1061
+ bin.display()
1062
+ );
1063
+ for name in &discover_core_tests() {
1064
+ if VM_PARITY_SKIP.contains(&name.as_str()) {
1065
+ continue;
1066
+ }
1067
+ let path = core_dir.join(name);
1068
+ if !path.exists() {
1069
+ continue;
1070
+ }
1071
+ let path_str = path.to_string_lossy();
1072
+ let out_interp = Command::new(&bin)
1073
+ .args(["run", path_str.as_ref(), "--backend", "interp"])
1074
+ .current_dir(workspace_root())
1075
+ .output()
1076
+ .expect("run tish interpreter");
1077
+ assert!(
1078
+ out_interp.status.success(),
1079
+ "Interpreter failed for {}: {}",
1080
+ path.display(),
1081
+ String::from_utf8_lossy(&out_interp.stderr)
1082
+ );
1083
+ let out_vm = Command::new(&bin)
1084
+ .args(["run", path_str.as_ref()])
1085
+ .current_dir(workspace_root())
1086
+ .output()
1087
+ .expect("run tish VM");
1088
+ assert!(
1089
+ out_vm.status.success(),
1090
+ "VM failed for {}: {}",
1091
+ path.display(),
1092
+ String::from_utf8_lossy(&out_vm.stderr)
1093
+ );
1094
+ let s_interp = String::from_utf8_lossy(&out_interp.stdout);
1095
+ let s_vm = String::from_utf8_lossy(&out_vm.stdout);
1096
+ assert_eq!(
1097
+ s_interp,
1098
+ s_vm,
1099
+ "interp vs VM stdout mismatch for {}",
1100
+ path.display()
1101
+ );
1102
+ }
1103
+ }
1104
+
1105
+ /// Compile each .tish file to native, run, and compare stdout to static expected (parallelized).
1106
+ #[test]
1107
+ fn test_mvp_programs_native() {
1108
+ let _fast_native = EnvVarGuard::set("TISH_FAST_NATIVE_BUILD", "1");
1109
+ let core_dir = core_dir();
1110
+ let bin = tish_bin();
1111
+ assert!(
1112
+ bin.exists(),
1113
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1114
+ bin.display()
1115
+ );
1116
+
1117
+ let mut paths: Vec<PathBuf> = discover_core_tests()
1118
+ .iter()
1119
+ .filter_map(|name| {
1120
+ let p = core_dir.join(name);
1121
+ if p.exists() {
1122
+ Some(p)
1123
+ } else {
1124
+ None
1125
+ }
1126
+ })
1127
+ .collect();
1128
+ paths.sort();
1129
+
1130
+ if paths.is_empty() {
1131
+ return;
1132
+ }
1133
+
1134
+ let combined = combined_mvp_native_inputs_hash(&paths);
1135
+ let cache_dir = mvp_native_batch_cache_dir(combined);
1136
+ let _ = std::fs::create_dir_all(&cache_dir);
1137
+
1138
+ let ext = if cfg!(target_os = "windows") {
1139
+ ".exe"
1140
+ } else {
1141
+ ""
1142
+ };
1143
+
1144
+ let entries_owned: Vec<(PathBuf, PathBuf)> = paths
1145
+ .iter()
1146
+ .map(|p| {
1147
+ let stem = p.file_stem().unwrap().to_string_lossy();
1148
+ let cached = cache_dir.join(format!("{}{}", stem, ext));
1149
+ (p.clone(), cached)
1150
+ })
1151
+ .collect();
1152
+
1153
+ let need_build = entries_owned.iter().any(|(_, o)| !o.exists());
1154
+ if need_build {
1155
+ let refs: Vec<(&Path, &Path)> = entries_owned
1156
+ .iter()
1157
+ .map(|(a, b)| (a.as_path(), b.as_path()))
1158
+ .collect();
1159
+ let feats = native_build_features_for_integration_test();
1160
+ compile_many_to_native(&refs, Some(workspace_root().as_path()), &feats, true)
1161
+ .unwrap_or_else(|e| panic!("compile_many_to_native: {}", e.message));
1162
+ }
1163
+
1164
+ // Run each binary sequentially. Parallel `fs::copy` + `exec` caused Linux ETXTBSY (errno 26)
1165
+ // in CI when several threads replaced/ran temp executables under load.
1166
+ let errors: Vec<String> = entries_owned
1167
+ .iter()
1168
+ .enumerate()
1169
+ .filter_map(|(run_index, (path, cached_bin))| {
1170
+ let expected = match get_expected(path) {
1171
+ Some(e) => e,
1172
+ None => return Some(format!("missing expected: {}", path.display())),
1173
+ };
1174
+ if !cached_bin.exists() {
1175
+ return Some(format!("missing cached binary: {}", cached_bin.display()));
1176
+ }
1177
+ let stem = path.file_stem().unwrap().to_string_lossy();
1178
+ let ext_bin = cached_bin
1179
+ .extension()
1180
+ .map(|e| e.to_string_lossy().to_string())
1181
+ .unwrap_or_default();
1182
+ let temp_dest = std::env::temp_dir().join(format!(
1183
+ "tish_mvp_native_{}_{:x}_{}_{}",
1184
+ stem,
1185
+ file_content_hash(path),
1186
+ std::process::id(),
1187
+ run_index
1188
+ ));
1189
+ let temp_dest = if ext_bin.is_empty() {
1190
+ temp_dest
1191
+ } else {
1192
+ temp_dest.with_extension(&ext_bin)
1193
+ };
1194
+ std::fs::copy(cached_bin, &temp_dest).expect("copy cached native bin to temp");
1195
+ let out_bin = temp_dest;
1196
+ let out = match Command::new(&out_bin)
1197
+ .current_dir(workspace_root())
1198
+ .output()
1199
+ {
1200
+ Ok(o) => o,
1201
+ Err(e) => {
1202
+ let _ = std::fs::remove_file(&out_bin);
1203
+ return Some(format!("{}: run failed: {}", path.display(), e));
1204
+ }
1205
+ };
1206
+ let _ = std::fs::remove_file(&out_bin);
1207
+ if !out.status.success() {
1208
+ return Some(format!(
1209
+ "{}: {}",
1210
+ path.display(),
1211
+ String::from_utf8_lossy(&out.stderr)
1212
+ ));
1213
+ }
1214
+ let stdout = String::from_utf8_lossy(&out.stdout);
1215
+ if stdout != expected {
1216
+ return Some(format!("{}: output mismatch", path.display()));
1217
+ }
1218
+ None
1219
+ })
1220
+ .collect();
1221
+ assert!(errors.is_empty(), "native failures:\n{}", errors.join("\n"));
1222
+ }
1223
+
1224
+ // cranelift + wasi now use `discover_core_tests()` (full file discovery), like interp/vm/native —
1225
+ // the former curated `CRANELIFT_TEST_FILES` allowlist is gone. They embed the bytecode VM, which
1226
+ // has full interp↔vm parity (`VM_PARITY_SKIP` empty), so they inherit it: a disk-safe sweep confirmed
1227
+ // cranelift 66/66 and wasi 66/66. The old blocker was build COST — each backend build used to emit a
1228
+ // per-program `target/` (~2-5 GB) that accumulated to ~130 GB; now `tish_cranelift`/`tish_wasm` build
1229
+ // into a SHARED target dir so the deps compile once (610 MB / 85 MB total for the whole sweep, and a
1230
+ // repeat build is ~1 s). If a construct ever regresses on a backend, add it to a documented skip set
1231
+ // rather than re-introducing an allowlist.
1232
+
1233
+ /// Compile each .tish file with Cranelift backend, run, and compare stdout to static expected (parallelized).
1234
+ #[test]
1235
+ fn test_mvp_programs_cranelift() {
1236
+ let core_dir = core_dir();
1237
+ let bin = tish_bin();
1238
+ assert!(
1239
+ bin.exists(),
1240
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1241
+ bin.display()
1242
+ );
1243
+ let errors: Vec<String> = discover_core_tests()
1244
+ .par_iter()
1245
+ .filter_map(|name| {
1246
+ let path = core_dir.join(name);
1247
+ if !path.exists() {
1248
+ return None;
1249
+ }
1250
+ let expected = match get_expected(&path) {
1251
+ Some(e) => e,
1252
+ None => return Some(format!("missing expected: {}", path.display())),
1253
+ };
1254
+ let out_bin = compile_cached(&bin, &path, "cranelift");
1255
+ let out = match Command::new(&out_bin)
1256
+ .current_dir(workspace_root())
1257
+ .output()
1258
+ {
1259
+ Ok(o) => o,
1260
+ Err(e) => {
1261
+ let _ = std::fs::remove_file(&out_bin);
1262
+ return Some(format!("{}: run failed: {}", path.display(), e));
1263
+ }
1264
+ };
1265
+ let _ = std::fs::remove_file(&out_bin);
1266
+ if !out.status.success() {
1267
+ return Some(format!(
1268
+ "{}: {}",
1269
+ path.display(),
1270
+ String::from_utf8_lossy(&out.stderr)
1271
+ ));
1272
+ }
1273
+ let stdout = String::from_utf8_lossy(&out.stdout);
1274
+ if stdout != expected {
1275
+ return Some(format!("{}: output mismatch", path.display()));
1276
+ }
1277
+ None
1278
+ })
1279
+ .collect();
1280
+ assert!(
1281
+ errors.is_empty(),
1282
+ "cranelift failures:\n{}",
1283
+ errors.join("\n")
1284
+ );
1285
+ }
1286
+
1287
+ /// Compile each .tish file to WASI, run with wasmtime, and compare stdout to static expected (parallelized).
1288
+ /// Skips if wasmtime is not available.
1289
+ #[test]
1290
+ fn test_mvp_programs_wasi() {
1291
+ let wasmtime_available = Command::new("wasmtime")
1292
+ .arg("--version")
1293
+ .output()
1294
+ .map(|o| o.status.success())
1295
+ .unwrap_or(false);
1296
+ if !wasmtime_available {
1297
+ eprintln!("Skipping test_mvp_programs_wasi: wasmtime not found");
1298
+ return;
1299
+ }
1300
+ let core_dir = core_dir();
1301
+ let bin = tish_bin();
1302
+ assert!(
1303
+ bin.exists(),
1304
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1305
+ bin.display()
1306
+ );
1307
+ let errors: Vec<String> = discover_core_tests()
1308
+ .par_iter()
1309
+ .filter_map(|name| {
1310
+ let path = core_dir.join(name);
1311
+ if !path.exists() {
1312
+ return None;
1313
+ }
1314
+ let expected = match get_expected(&path) {
1315
+ Some(e) => e,
1316
+ None => return Some(format!("missing expected: {}", path.display())),
1317
+ };
1318
+ let out_wasm = compile_cached(&bin, &path, "wasi");
1319
+ let out = match Command::new("wasmtime")
1320
+ .arg(out_wasm.as_os_str())
1321
+ .current_dir(workspace_root())
1322
+ .output()
1323
+ {
1324
+ Ok(o) => o,
1325
+ Err(e) => {
1326
+ let _ = std::fs::remove_file(&out_wasm);
1327
+ return Some(format!("{}: wasmtime failed: {}", path.display(), e));
1328
+ }
1329
+ };
1330
+ let _ = std::fs::remove_file(&out_wasm);
1331
+ if !out.status.success() {
1332
+ return Some(format!(
1333
+ "{}: {}",
1334
+ path.display(),
1335
+ String::from_utf8_lossy(&out.stderr)
1336
+ ));
1337
+ }
1338
+ let stdout = String::from_utf8_lossy(&out.stdout);
1339
+ if stdout != expected {
1340
+ return Some(format!("{}: output mismatch", path.display()));
1341
+ }
1342
+ None
1343
+ })
1344
+ .collect();
1345
+ assert!(errors.is_empty(), "wasi failures:\n{}", errors.join("\n"));
1346
+ }
1347
+
1348
+ /// Files where Tish intentionally differs from JavaScript (typeof, void); skip in JS test since we compare to Tish expected.
1349
+ const JS_SKIP_FILES: &[&str] = &["typeof.tish", "void.tish"];
1350
+
1351
+ /// Compile each .tish file to JS, run with Node, and compare stdout to static expected.
1352
+ #[test]
1353
+ fn test_mvp_programs_js() {
1354
+ let node_available = Command::new("node")
1355
+ .args(["--version"])
1356
+ .output()
1357
+ .map(|o| o.status.success())
1358
+ .unwrap_or(false);
1359
+ if !node_available {
1360
+ eprintln!("Skipping test_mvp_programs_js: Node.js not found");
1361
+ return;
1362
+ }
1363
+ let core_dir = core_dir();
1364
+ let bin = tish_bin();
1365
+ assert!(
1366
+ bin.exists(),
1367
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
1368
+ bin.display()
1369
+ );
1370
+ for name in &discover_core_tests() {
1371
+ // intentional JS divergences + tests without a `.js` sibling can't use the Node oracle
1372
+ if JS_SKIP_FILES.contains(&name.as_str()) || !has_js_sibling(name) {
1373
+ continue;
1374
+ }
1375
+ let path = core_dir.join(name);
1376
+ if !path.exists() {
1377
+ continue;
1378
+ }
1379
+ let expected = get_expected(&path).unwrap_or_else(|| {
1380
+ panic!(
1381
+ "missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
1382
+ path.display()
1383
+ )
1384
+ });
1385
+ let out_js = compile_cached(&bin, &path, "js");
1386
+ let out = Command::new("node")
1387
+ .arg(&out_js)
1388
+ .current_dir(workspace_root())
1389
+ .output()
1390
+ .expect("run node");
1391
+ let _ = std::fs::remove_file(&out_js);
1392
+ assert!(
1393
+ out.status.success(),
1394
+ "Node failed for {}: {}",
1395
+ path.display(),
1396
+ String::from_utf8_lossy(&out.stderr)
1397
+ );
1398
+ let stdout = String::from_utf8_lossy(&out.stdout);
1399
+ assert_eq!(
1400
+ stdout,
1401
+ expected,
1402
+ "JS output mismatch for {}",
1403
+ path.display()
1404
+ );
1405
+ }
1406
+ }