@tishlang/tish 1.0.7 → 1.0.10

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 (127) hide show
  1. package/Cargo.toml +43 -0
  2. package/LICENSE +13 -0
  3. package/README.md +66 -0
  4. package/crates/js_to_tish/Cargo.toml +9 -0
  5. package/crates/js_to_tish/README.md +18 -0
  6. package/crates/js_to_tish/src/error.rs +61 -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 +608 -0
  10. package/crates/js_to_tish/src/transform/stmt.rs +474 -0
  11. package/crates/js_to_tish/src/transform.rs +60 -0
  12. package/crates/tish/Cargo.toml +44 -0
  13. package/crates/tish/src/main.rs +585 -0
  14. package/crates/tish/src/repl_completion.rs +200 -0
  15. package/crates/tish/tests/integration_test.rs +726 -0
  16. package/crates/tish_ast/Cargo.toml +7 -0
  17. package/crates/tish_ast/src/ast.rs +494 -0
  18. package/crates/tish_ast/src/lib.rs +5 -0
  19. package/crates/tish_build_utils/Cargo.toml +5 -0
  20. package/crates/tish_build_utils/src/lib.rs +175 -0
  21. package/crates/tish_builtins/Cargo.toml +12 -0
  22. package/crates/tish_builtins/src/array.rs +410 -0
  23. package/crates/tish_builtins/src/globals.rs +197 -0
  24. package/crates/tish_builtins/src/helpers.rs +38 -0
  25. package/crates/tish_builtins/src/lib.rs +14 -0
  26. package/crates/tish_builtins/src/math.rs +80 -0
  27. package/crates/tish_builtins/src/object.rs +36 -0
  28. package/crates/tish_builtins/src/string.rs +253 -0
  29. package/crates/tish_bytecode/Cargo.toml +15 -0
  30. package/crates/tish_bytecode/src/chunk.rs +97 -0
  31. package/crates/tish_bytecode/src/compiler.rs +1361 -0
  32. package/crates/tish_bytecode/src/encoding.rs +100 -0
  33. package/crates/tish_bytecode/src/lib.rs +19 -0
  34. package/crates/tish_bytecode/src/opcode.rs +110 -0
  35. package/crates/tish_bytecode/src/peephole.rs +159 -0
  36. package/crates/tish_bytecode/src/serialize.rs +163 -0
  37. package/crates/tish_bytecode/tests/constant_folding.rs +84 -0
  38. package/crates/tish_bytecode/tests/shortcircuit.rs +49 -0
  39. package/crates/tish_bytecode/tests/sort_optimization.rs +31 -0
  40. package/crates/tish_compile/Cargo.toml +21 -0
  41. package/crates/tish_compile/src/codegen.rs +3316 -0
  42. package/crates/tish_compile/src/lib.rs +71 -0
  43. package/crates/tish_compile/src/resolve.rs +631 -0
  44. package/crates/tish_compile/src/types.rs +304 -0
  45. package/crates/tish_compile_js/Cargo.toml +16 -0
  46. package/crates/tish_compile_js/examples/jsx_vdom_smoke.tish +8 -0
  47. package/crates/tish_compile_js/src/codegen.rs +794 -0
  48. package/crates/tish_compile_js/src/error.rs +20 -0
  49. package/crates/tish_compile_js/src/js_intrinsics.rs +82 -0
  50. package/crates/tish_compile_js/src/lib.rs +27 -0
  51. package/crates/tish_compile_js/src/tests_jsx.rs +32 -0
  52. package/crates/tish_compiler_wasm/Cargo.toml +19 -0
  53. package/crates/tish_compiler_wasm/src/lib.rs +55 -0
  54. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +462 -0
  55. package/crates/tish_core/Cargo.toml +11 -0
  56. package/crates/tish_core/src/console_style.rs +128 -0
  57. package/crates/tish_core/src/json.rs +327 -0
  58. package/crates/tish_core/src/lib.rs +15 -0
  59. package/crates/tish_core/src/macros.rs +37 -0
  60. package/crates/tish_core/src/uri.rs +115 -0
  61. package/crates/tish_core/src/value.rs +376 -0
  62. package/crates/tish_cranelift/Cargo.toml +17 -0
  63. package/crates/tish_cranelift/src/lib.rs +41 -0
  64. package/crates/tish_cranelift/src/link.rs +120 -0
  65. package/crates/tish_cranelift/src/lower.rs +77 -0
  66. package/crates/tish_cranelift_runtime/Cargo.toml +19 -0
  67. package/crates/tish_cranelift_runtime/src/lib.rs +43 -0
  68. package/crates/tish_eval/Cargo.toml +26 -0
  69. package/crates/tish_eval/src/eval.rs +3205 -0
  70. package/crates/tish_eval/src/http.rs +122 -0
  71. package/crates/tish_eval/src/lib.rs +59 -0
  72. package/crates/tish_eval/src/natives.rs +301 -0
  73. package/crates/tish_eval/src/promise.rs +173 -0
  74. package/crates/tish_eval/src/regex.rs +298 -0
  75. package/crates/tish_eval/src/timers.rs +111 -0
  76. package/crates/tish_eval/src/value.rs +224 -0
  77. package/crates/tish_eval/src/value_convert.rs +85 -0
  78. package/crates/tish_fmt/Cargo.toml +16 -0
  79. package/crates/tish_fmt/src/bin/tish-fmt.rs +41 -0
  80. package/crates/tish_fmt/src/lib.rs +884 -0
  81. package/crates/tish_jsx_web/Cargo.toml +7 -0
  82. package/crates/tish_jsx_web/README.md +18 -0
  83. package/crates/tish_jsx_web/src/lib.rs +157 -0
  84. package/crates/tish_jsx_web/vendor/Lattish.tish +347 -0
  85. package/crates/tish_lexer/Cargo.toml +7 -0
  86. package/crates/tish_lexer/src/lib.rs +430 -0
  87. package/crates/tish_lexer/src/token.rs +155 -0
  88. package/crates/tish_lint/Cargo.toml +17 -0
  89. package/crates/tish_lint/src/bin/tish-lint.rs +77 -0
  90. package/crates/tish_lint/src/lib.rs +278 -0
  91. package/crates/tish_llvm/Cargo.toml +11 -0
  92. package/crates/tish_llvm/src/lib.rs +106 -0
  93. package/crates/tish_lsp/Cargo.toml +22 -0
  94. package/crates/tish_lsp/README.md +26 -0
  95. package/crates/tish_lsp/src/main.rs +615 -0
  96. package/crates/tish_native/Cargo.toml +14 -0
  97. package/crates/tish_native/src/build.rs +102 -0
  98. package/crates/tish_native/src/lib.rs +237 -0
  99. package/crates/tish_opt/Cargo.toml +11 -0
  100. package/crates/tish_opt/src/lib.rs +896 -0
  101. package/crates/tish_parser/Cargo.toml +9 -0
  102. package/crates/tish_parser/src/lib.rs +123 -0
  103. package/crates/tish_parser/src/parser.rs +1714 -0
  104. package/crates/tish_runtime/Cargo.toml +26 -0
  105. package/crates/tish_runtime/src/http.rs +308 -0
  106. package/crates/tish_runtime/src/http_fetch.rs +453 -0
  107. package/crates/tish_runtime/src/lib.rs +1004 -0
  108. package/crates/tish_runtime/src/native_promise.rs +26 -0
  109. package/crates/tish_runtime/src/promise.rs +77 -0
  110. package/crates/tish_runtime/src/promise_io.rs +41 -0
  111. package/crates/tish_runtime/src/timers.rs +125 -0
  112. package/crates/tish_runtime/src/ws.rs +725 -0
  113. package/crates/tish_runtime/tests/fetch_readable_stream.rs +99 -0
  114. package/crates/tish_vm/Cargo.toml +31 -0
  115. package/crates/tish_vm/src/lib.rs +39 -0
  116. package/crates/tish_vm/src/vm.rs +1399 -0
  117. package/crates/tish_wasm/Cargo.toml +13 -0
  118. package/crates/tish_wasm/src/lib.rs +358 -0
  119. package/crates/tish_wasm_runtime/Cargo.toml +25 -0
  120. package/crates/tish_wasm_runtime/src/lib.rs +36 -0
  121. package/justfile +260 -0
  122. package/package.json +8 -3
  123. package/platform/darwin-arm64/tish +0 -0
  124. package/platform/darwin-x64/tish +0 -0
  125. package/platform/linux-arm64/tish +0 -0
  126. package/platform/linux-x64/tish +0 -0
  127. package/platform/win32-x64/tish.exe +0 -0
@@ -0,0 +1,726 @@
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 tish` (or `cargo nextest run -p tish`).
5
+ //! - Generate/update expected files: `REGENERATE_EXPECTED=1 cargo test -p tish test_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
+
9
+ use std::collections::hash_map::DefaultHasher;
10
+ use std::hash::{Hash, Hasher};
11
+ use std::io::Read;
12
+ use std::ffi::OsString;
13
+ use std::path::{Path, PathBuf};
14
+ use std::process::Command;
15
+
16
+ use rayon::prelude::*;
17
+
18
+ fn workspace_root() -> PathBuf {
19
+ PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
20
+ }
21
+
22
+ fn core_dir() -> PathBuf {
23
+ workspace_root().join("tests").join("core")
24
+ }
25
+
26
+ /// Path to the static expected stdout for a .tish file (e.g. fn_any.tish -> fn_any.tish.expected).
27
+ fn expected_path(path: &Path) -> PathBuf {
28
+ path.with_file_name(format!("{}.expected", path.file_name().unwrap().to_string_lossy()))
29
+ }
30
+
31
+ /// Read static expected stdout for a test file. Returns None if the file does not exist.
32
+ fn get_expected(path: &Path) -> Option<String> {
33
+ let p = expected_path(path);
34
+ std::fs::read_to_string(&p).ok()
35
+ }
36
+
37
+ fn target_dir() -> PathBuf {
38
+ std::env::var("CARGO_TARGET_DIR")
39
+ .map(PathBuf::from)
40
+ .unwrap_or_else(|_| workspace_root().join("target"))
41
+ }
42
+
43
+ /// Cache dir for tish compile outputs (under target/ so CI rust-cache restores it).
44
+ fn integration_compile_cache_dir() -> PathBuf {
45
+ target_dir().join("integration_compile_cache")
46
+ }
47
+
48
+ fn file_content_hash(path: &Path) -> u64 {
49
+ let mut f = std::fs::File::open(path).expect("open file for hash");
50
+ let mut content = Vec::new();
51
+ f.read_to_end(&mut content).expect("read file for hash");
52
+ let mut h = DefaultHasher::new();
53
+ path.to_string_lossy().hash(&mut h);
54
+ content.hash(&mut h);
55
+ h.finish()
56
+ }
57
+
58
+ /// Compile a .tish file with the given backend, using a persistent cache so we only run
59
+ /// `tish compile` when the file or backend changed. Returns path to the compiled artifact
60
+ /// (binary, .js, or .wasm) in a temp dir; caller may run it and then delete it.
61
+ ///
62
+ /// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
63
+ /// compiles of the same file do not overwrite each other: .../cranelift/<stem>_<hash> vs .../wasi/<stem>_<hash>.wasm.
64
+ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
65
+ let stem = path.file_stem().unwrap().to_string_lossy();
66
+ let hash = file_content_hash(path);
67
+ let hash8 = &format!("{:016x}", hash)[..8];
68
+ let cache_base = integration_compile_cache_dir().join(backend);
69
+ let _ = std::fs::create_dir_all(&cache_base);
70
+
71
+ let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
72
+ "native" => {
73
+ let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
74
+ let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
75
+ let args = vec![
76
+ OsString::from("compile"),
77
+ OsString::from(path),
78
+ OsString::from("-o"),
79
+ OsString::from(&cached),
80
+ ];
81
+ (cached, args)
82
+ }
83
+ "cranelift" => {
84
+ let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
85
+ let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
86
+ let args = vec![
87
+ OsString::from("compile"),
88
+ OsString::from(path),
89
+ OsString::from("-o"),
90
+ OsString::from(&cached),
91
+ OsString::from("--native-backend"),
92
+ OsString::from("cranelift"),
93
+ ];
94
+ (cached, args)
95
+ }
96
+ "js" => {
97
+ let cached = cache_base.join(format!("{}_{}.js", stem, hash8));
98
+ let args = vec![
99
+ OsString::from("compile"),
100
+ OsString::from(path),
101
+ OsString::from("--target"),
102
+ OsString::from("js"),
103
+ OsString::from("-o"),
104
+ OsString::from(&cached),
105
+ ];
106
+ (cached, args)
107
+ }
108
+ "wasi" => {
109
+ let out_base = cache_base.join(format!("{}_{}", stem, hash8));
110
+ let artifact = out_base.with_extension("wasm");
111
+ let args = vec![
112
+ OsString::from("compile"),
113
+ OsString::from(path),
114
+ OsString::from("-o"),
115
+ OsString::from(&out_base),
116
+ OsString::from("--target"),
117
+ OsString::from("wasi"),
118
+ ];
119
+ (artifact, args)
120
+ }
121
+ _ => panic!("unknown backend {}", backend),
122
+ };
123
+
124
+ if !artifact_path.exists() {
125
+ let out = Command::new(bin)
126
+ .args(compile_args)
127
+ .current_dir(workspace_root())
128
+ .output()
129
+ .expect("run tish compile");
130
+ assert!(
131
+ out.status.success(),
132
+ "Compile failed for {} ({}): {}",
133
+ path.display(),
134
+ backend,
135
+ String::from_utf8_lossy(&out.stderr)
136
+ );
137
+ }
138
+
139
+ // Copy to temp so caller can run and delete without touching cache.
140
+ let ext = artifact_path.extension().map(|e| e.to_string_lossy().to_string()).unwrap_or_default();
141
+ let temp_dest = std::env::temp_dir().join(format!("tish_cached_{}_{}_{}", backend, stem, hash8));
142
+ let temp_dest = if ext.is_empty() {
143
+ temp_dest
144
+ } else {
145
+ temp_dest.with_extension(ext)
146
+ };
147
+ std::fs::copy(&artifact_path, &temp_dest).expect("copy cached artifact to temp");
148
+ temp_dest
149
+ }
150
+
151
+ /// Path to the tish CLI binary. When running under cargo-llvm-cov, the build goes to
152
+ /// target/llvm-cov-target and CARGO_TARGET_DIR may not be set for the test process.
153
+ fn tish_bin() -> PathBuf {
154
+ let bin_name = if cfg!(target_os = "windows") { "tish.exe" } else { "tish" };
155
+ let default = target_dir().join("debug").join(bin_name);
156
+ if default.exists() {
157
+ return default;
158
+ }
159
+ let llvm_cov = workspace_root().join("target").join("llvm-cov-target").join("debug").join(bin_name);
160
+ if llvm_cov.exists() {
161
+ return llvm_cov;
162
+ }
163
+ default
164
+ }
165
+
166
+ /// Parse async-await example (validates async fn parsing).
167
+ #[test]
168
+ fn test_async_await_parse() {
169
+ let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
170
+ if path.exists() {
171
+ let source = std::fs::read_to_string(&path).unwrap();
172
+ let result = tish_parser::parse(&source);
173
+ assert!(result.is_ok(), "Parse failed for {}: {:?}", path.display(), result.err());
174
+ }
175
+ }
176
+
177
+ /// Invoke tish binary to compile async-await and run compiled output (validates non-blocking pipeline).
178
+ #[test]
179
+ #[cfg(feature = "http")]
180
+ fn test_async_await_compile_via_binary() {
181
+ let bin = tish_bin();
182
+ let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
183
+ if path.exists() && bin.exists() {
184
+ let out = std::env::temp_dir().join("tish_async_test_out");
185
+ let compile_result = Command::new(&bin)
186
+ .args(["compile", path.to_string_lossy().as_ref(), "-o", out.to_string_lossy().as_ref()])
187
+ .current_dir(workspace_root())
188
+ .output();
189
+ let compile_out = compile_result.expect("run tish compile");
190
+ assert!(
191
+ compile_out.status.success(),
192
+ "tish compile failed: {}",
193
+ String::from_utf8_lossy(&compile_out.stderr)
194
+ );
195
+ // Run compiled binary to validate non-blocking fetchAll executes correctly
196
+ let run_result = Command::new(&out)
197
+ .current_dir(workspace_root())
198
+ .output();
199
+ let run_out = run_result.expect("run compiled async binary");
200
+ assert!(
201
+ run_out.status.success(),
202
+ "compiled async binary failed: {}",
203
+ String::from_utf8_lossy(&run_out.stderr)
204
+ );
205
+ let stdout = String::from_utf8_lossy(&run_out.stdout);
206
+ assert!(stdout.contains("Fetching"), "expected output to mention fetching");
207
+ assert!(stdout.contains("Done"), "expected output to contain Done");
208
+ }
209
+ }
210
+
211
+ /// DEFINITIVE VALIDATION: Parallel fetches must be faster than sequential.
212
+ /// Uses httpbin.org/delay/1 (1s each). 3 parallel ≈ 1s, 3 sequential ≈ 3s.
213
+ #[test]
214
+ #[cfg(feature = "http")]
215
+ #[ignore = "timing and network sensitive; run manually: cargo test test_async_parallel_vs_sequential_timing -p tish --features http -- --ignored"]
216
+ fn test_async_parallel_vs_sequential_timing() {
217
+ let bin = tish_bin();
218
+ let parallel_src = workspace_root().join("examples").join("async-await").join("src").join("parallel.tish");
219
+ let sequential_src = workspace_root().join("examples").join("async-await").join("src").join("sequential.tish");
220
+ if !parallel_src.exists() || !sequential_src.exists() || !bin.exists() {
221
+ return;
222
+ }
223
+ let out_parallel = std::env::temp_dir().join("tish_parallel_timing");
224
+ let out_sequential = std::env::temp_dir().join("tish_sequential_timing");
225
+
226
+ // Compile both
227
+ let compile_par = Command::new(&bin)
228
+ .args(["compile", parallel_src.to_string_lossy().as_ref(), "-o", out_parallel.to_string_lossy().as_ref()])
229
+ .current_dir(workspace_root())
230
+ .output();
231
+ assert!(compile_par.as_ref().unwrap().status.success(), "compile parallel: {}", String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr));
232
+
233
+ let compile_seq = Command::new(&bin)
234
+ .args(["compile", sequential_src.to_string_lossy().as_ref(), "-o", out_sequential.to_string_lossy().as_ref()])
235
+ .current_dir(workspace_root())
236
+ .output();
237
+ assert!(compile_seq.as_ref().unwrap().status.success(), "compile sequential: {}", String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr));
238
+
239
+ // Run parallel and time
240
+ let t_parallel = std::time::Instant::now();
241
+ let run_par = Command::new(&out_parallel).current_dir(workspace_root()).output();
242
+ let elapsed_parallel = t_parallel.elapsed();
243
+ assert!(run_par.as_ref().unwrap().status.success(), "run parallel: {}", String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr));
244
+
245
+ // Run sequential and time
246
+ let t_sequential = std::time::Instant::now();
247
+ let run_seq = Command::new(&out_sequential).current_dir(workspace_root()).output();
248
+ let elapsed_sequential = t_sequential.elapsed();
249
+ assert!(run_seq.as_ref().unwrap().status.success(), "run sequential: {}", String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr));
250
+
251
+ // PARALLEL MUST BE FASTER: parallel < sequential * 0.6 (parallel ~1s, sequential ~3s)
252
+ let parallel_secs = elapsed_parallel.as_secs_f64();
253
+ let sequential_secs = elapsed_sequential.as_secs_f64();
254
+ assert!(
255
+ parallel_secs < sequential_secs * 0.6,
256
+ "Async NOT validated: parallel took {:.2}s but sequential took {:.2}s. Parallel must be < 60% of sequential to prove non-blocking.",
257
+ parallel_secs,
258
+ sequential_secs
259
+ );
260
+ }
261
+
262
+ /// Run async-await example via tish_eval (same path as `tish run`).
263
+ /// Ignored: tish_eval::run() is synchronous and does not run the event loop.
264
+ #[test]
265
+ #[cfg(feature = "http")]
266
+ #[ignore = "requires async runtime; use test_async_await_compile_via_binary for CI"]
267
+ fn test_async_await_run() {
268
+ let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
269
+ if path.exists() {
270
+ let source = std::fs::read_to_string(&path).unwrap();
271
+ let result = tish_eval::run(&source);
272
+ assert!(result.is_ok(), "Run failed for {}: {:?}", path.display(), result.err());
273
+ }
274
+ }
275
+
276
+ /// Run Promise and setTimeout module tests (require http feature).
277
+ /// Ignored: tish_eval::run() does not run the event loop.
278
+ #[test]
279
+ #[cfg(feature = "http")]
280
+ #[ignore = "requires async runtime"]
281
+ fn test_promise_and_settimeout() {
282
+ for name in ["promise", "settimeout"] {
283
+ let path = workspace_root().join("tests").join("modules").join(format!("{}.tish", name));
284
+ if path.exists() {
285
+ let source = std::fs::read_to_string(&path).unwrap();
286
+ let result = tish_eval::run(&source);
287
+ assert!(
288
+ result.is_ok(),
289
+ "Failed to run {}: {:?}",
290
+ path.display(),
291
+ result.err()
292
+ );
293
+ }
294
+ }
295
+ }
296
+
297
+ /// Combined validation: async/await + Promise + setTimeout + multiple HTTP requests.
298
+ /// Ignored: tish_eval::run() does not run the event loop.
299
+ #[test]
300
+ #[cfg(feature = "http")]
301
+ #[ignore = "requires async runtime"]
302
+ fn test_async_promise_settimeout_combined() {
303
+ let path = workspace_root()
304
+ .join("tests")
305
+ .join("modules")
306
+ .join("async_promise_settimeout.tish");
307
+ if path.exists() {
308
+ let source = std::fs::read_to_string(&path).unwrap();
309
+ let result = tish_eval::run(&source);
310
+ assert!(
311
+ result.is_ok(),
312
+ "Failed to run async_promise_settimeout: {:?}",
313
+ result.err()
314
+ );
315
+ }
316
+ }
317
+
318
+ /// VM run with Date global (resolve+merge+bytecode+run pipeline).
319
+ #[test]
320
+ fn test_vm_date_now() {
321
+ let path = workspace_root().join("tests").join("core").join("date.tish");
322
+ if !path.exists() {
323
+ return;
324
+ }
325
+ // Library path
326
+ let modules = tish_compile::resolve_project(&path, path.parent()).expect("resolve");
327
+ tish_compile::detect_cycles(&modules).expect("cycles");
328
+ let program = tish_compile::merge_modules(modules).expect("merge");
329
+ let chunk = tish_bytecode::compile(&program).expect("compile");
330
+ let result = tish_vm::run(&chunk);
331
+ assert!(result.is_ok(), "VM run (library) failed: {:?}", result.err());
332
+ // Binary path - same flow as `tish run <file>`
333
+ let bin = tish_bin();
334
+ if bin.exists() {
335
+ let out = Command::new(&bin)
336
+ .args(["run", path.to_string_lossy().as_ref()])
337
+ .current_dir(workspace_root())
338
+ .output()
339
+ .expect("run tish binary");
340
+ assert!(
341
+ out.status.success(),
342
+ "tish run failed: stdout={} stderr={}",
343
+ String::from_utf8_lossy(&out.stdout),
344
+ String::from_utf8_lossy(&out.stderr)
345
+ );
346
+ }
347
+ }
348
+
349
+ /// VM run with parse+compile only (no resolve/merge) - isolates bytecode IndexAssign.
350
+ #[test]
351
+ fn test_vm_index_assign_direct() {
352
+ let source = r#"let arr = [1, 2, 3]; arr[1] = 99; console.log(arr[1]);"#;
353
+ let program = tish_parser::parse(source).expect("parse");
354
+ let chunk = tish_bytecode::compile(&program).expect("compile");
355
+ let result = tish_vm::run(&chunk);
356
+ assert!(result.is_ok(), "VM IndexAssign failed: {:?}", result.err());
357
+ }
358
+
359
+ /// VM run via resolve+merge (same as tish run) - must also pass.
360
+ #[test]
361
+ fn test_vm_index_assign_via_resolve() {
362
+ let path = workspace_root().join("tests").join("core").join("array_sort_minimal.tish");
363
+ let modules = tish_compile::resolve_project(&path, path.parent()).expect("resolve");
364
+ tish_compile::detect_cycles(&modules).expect("cycles");
365
+ let program = tish_compile::merge_modules(modules).expect("merge");
366
+ let chunk = tish_bytecode::compile(&program).expect("compile");
367
+ let result = tish_vm::run(&chunk);
368
+ assert!(result.is_ok(), "VM IndexAssign via resolve failed: {:?}", result.err());
369
+ }
370
+
371
+ /// tish run binary must pass array_sort_minimal (ensures CLI works).
372
+ #[test]
373
+ fn test_tish_run_index_assign() {
374
+ let bin = tish_bin();
375
+ let path = workspace_root().join("tests").join("core").join("array_sort_minimal.tish");
376
+ if !bin.exists() {
377
+ eprintln!("Skipping: tish binary not built");
378
+ return;
379
+ }
380
+ let out = Command::new(&bin)
381
+ .args(["run", path.to_string_lossy().as_ref()])
382
+ .current_dir(workspace_root())
383
+ .output()
384
+ .expect("run tish");
385
+ assert!(
386
+ out.status.success(),
387
+ "tish run failed: stdout={} stderr={}",
388
+ String::from_utf8_lossy(&out.stdout),
389
+ String::from_utf8_lossy(&out.stderr)
390
+ );
391
+ assert!(
392
+ String::from_utf8_lossy(&out.stdout).contains("pass"),
393
+ "Expected 'pass' in output"
394
+ );
395
+ }
396
+
397
+ /// Full stack: lex + parse each .tish file and assert no parse error.
398
+ #[test]
399
+ fn test_full_stack_parse() {
400
+ let core_dir = core_dir();
401
+ for entry in std::fs::read_dir(&core_dir).unwrap() {
402
+ let path = entry.unwrap().path();
403
+ if path.extension().map(|e| e == "tish").unwrap_or(false) {
404
+ let source = std::fs::read_to_string(&path).unwrap();
405
+ let result = tish_parser::parse(&source);
406
+ assert!(
407
+ result.is_ok(),
408
+ "Parse failed for {}: {:?}",
409
+ path.display(),
410
+ result.err()
411
+ );
412
+ }
413
+ }
414
+ }
415
+
416
+ /// Shared list of MVP test files used for static comparison (interpreter and native).
417
+ const MVP_TEST_FILES: &[&str] = &[
418
+ "nested_loops.tish",
419
+ "scopes.tish",
420
+ "optional_braces.tish",
421
+ "optional_braces_braced.tish",
422
+ "tab_indent.tish",
423
+ "space_indent.tish",
424
+ "fn_any.tish",
425
+ "strict_equality.tish",
426
+ "arrays.tish",
427
+ "break_continue.tish",
428
+ "length.tish",
429
+ "objects.tish",
430
+ "conditional.tish",
431
+ "switch.tish",
432
+ "do_while.tish",
433
+ "typeof.tish",
434
+ "inc_dec.tish",
435
+ "try_catch.tish",
436
+ "builtins.tish",
437
+ "exponentiation.tish",
438
+ "for_of.tish",
439
+ "bitwise.tish",
440
+ "math.tish",
441
+ "optional_chaining.tish",
442
+ "void.tish",
443
+ "rest_params.tish",
444
+ "json.tish",
445
+ "uri.tish",
446
+ "in_op.tish",
447
+ "arrow_functions.tish",
448
+ "template_literals.tish",
449
+ "compound_assign.tish",
450
+ "mutation.tish",
451
+ "string_methods.tish",
452
+ "array_methods.tish",
453
+ "object_methods.tish",
454
+ "types.tish",
455
+ "logical_assign.tish",
456
+ "spread.tish",
457
+ ];
458
+
459
+ /// Run each .tish file with interpreter and compare stdout to static expected.
460
+ /// Set REGENERATE_EXPECTED=1 to write .expected files from interpreter output (run once, then commit).
461
+ #[test]
462
+ fn test_mvp_programs_interpreter() {
463
+ let core_dir = core_dir();
464
+ let bin = tish_bin();
465
+ assert!(
466
+ bin.exists(),
467
+ "tish binary not found at {}. Run `cargo build -p tish` first.",
468
+ bin.display()
469
+ );
470
+ let regenerate = std::env::var("REGENERATE_EXPECTED").as_deref() == Ok("1");
471
+ for name in MVP_TEST_FILES {
472
+ let path = core_dir.join(name);
473
+ if !path.exists() {
474
+ continue;
475
+ }
476
+ let path_str = path.to_string_lossy();
477
+ let out = Command::new(&bin)
478
+ .args(["run", path_str.as_ref(), "--backend", "interp"])
479
+ .current_dir(workspace_root())
480
+ .output()
481
+ .expect("run tish interpreter");
482
+ assert!(
483
+ out.status.success(),
484
+ "Interpreter failed for {}: {}",
485
+ path.display(),
486
+ String::from_utf8_lossy(&out.stderr)
487
+ );
488
+ let stdout = String::from_utf8_lossy(&out.stdout).to_string();
489
+ if regenerate {
490
+ std::fs::write(expected_path(&path), &stdout).expect("write expected");
491
+ } else {
492
+ let expected = get_expected(&path).unwrap_or_else(|| {
493
+ panic!(
494
+ "missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
495
+ path.display()
496
+ )
497
+ });
498
+ assert_eq!(stdout, expected, "Interpreter output mismatch for {}", path.display());
499
+ }
500
+ }
501
+ }
502
+
503
+ /// Compile each .tish file to native, run, and compare stdout to static expected (parallelized).
504
+ #[test]
505
+ fn test_mvp_programs_native() {
506
+ let core_dir = core_dir();
507
+ let bin = tish_bin();
508
+ assert!(
509
+ bin.exists(),
510
+ "tish binary not found at {}. Run `cargo build -p tish` first.",
511
+ bin.display()
512
+ );
513
+ let errors: Vec<String> = MVP_TEST_FILES
514
+ .par_iter()
515
+ .filter_map(|name| {
516
+ let path = core_dir.join(name);
517
+ if !path.exists() {
518
+ return None;
519
+ }
520
+ let expected = match get_expected(&path) {
521
+ Some(e) => e,
522
+ None => return Some(format!("missing expected: {}", path.display())),
523
+ };
524
+ let out_bin = compile_cached(&bin, &path, "native");
525
+ let out = match Command::new(&out_bin)
526
+ .current_dir(workspace_root())
527
+ .output()
528
+ {
529
+ Ok(o) => o,
530
+ Err(e) => {
531
+ let _ = std::fs::remove_file(&out_bin);
532
+ return Some(format!("{}: run failed: {}", path.display(), e));
533
+ }
534
+ };
535
+ let _ = std::fs::remove_file(&out_bin);
536
+ if !out.status.success() {
537
+ return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
538
+ }
539
+ let stdout = String::from_utf8_lossy(&out.stdout);
540
+ if stdout != expected {
541
+ return Some(format!("{}: output mismatch", path.display()));
542
+ }
543
+ None
544
+ })
545
+ .collect();
546
+ assert!(errors.is_empty(), "native failures:\n{}", errors.join("\n"));
547
+ }
548
+
549
+ /// Curated list: files that pass with Cranelift (some constructs cause stack-underflow; see docs/builtins-gap-analysis.md).
550
+ const CRANELIFT_TEST_FILES: &[&str] = &[
551
+ "fn_any.tish",
552
+ "strict_equality.tish",
553
+ "switch.tish",
554
+ "do_while.tish",
555
+ "typeof.tish",
556
+ "try_catch.tish",
557
+ "json.tish",
558
+ "math.tish",
559
+ "builtins.tish",
560
+ "uri.tish",
561
+ "inc_dec.tish",
562
+ "exponentiation.tish",
563
+ "void.tish",
564
+ "rest_params.tish",
565
+ "arrow_functions.tish",
566
+ "array_methods.tish",
567
+ "types.tish",
568
+ ];
569
+
570
+ /// Compile each .tish file with Cranelift backend, run, and compare stdout to static expected (parallelized).
571
+ #[test]
572
+ fn test_mvp_programs_cranelift() {
573
+ let core_dir = core_dir();
574
+ let bin = tish_bin();
575
+ assert!(
576
+ bin.exists(),
577
+ "tish binary not found at {}. Run `cargo build -p tish` first.",
578
+ bin.display()
579
+ );
580
+ let errors: Vec<String> = CRANELIFT_TEST_FILES
581
+ .par_iter()
582
+ .filter_map(|name| {
583
+ let path = core_dir.join(name);
584
+ if !path.exists() {
585
+ return None;
586
+ }
587
+ let expected = match get_expected(&path) {
588
+ Some(e) => e,
589
+ None => return Some(format!("missing expected: {}", path.display())),
590
+ };
591
+ let out_bin = compile_cached(&bin, &path, "cranelift");
592
+ let out = match Command::new(&out_bin)
593
+ .current_dir(workspace_root())
594
+ .output()
595
+ {
596
+ Ok(o) => o,
597
+ Err(e) => {
598
+ let _ = std::fs::remove_file(&out_bin);
599
+ return Some(format!("{}: run failed: {}", path.display(), e));
600
+ }
601
+ };
602
+ let _ = std::fs::remove_file(&out_bin);
603
+ if !out.status.success() {
604
+ return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
605
+ }
606
+ let stdout = String::from_utf8_lossy(&out.stdout);
607
+ if stdout != expected {
608
+ return Some(format!("{}: output mismatch", path.display()));
609
+ }
610
+ None
611
+ })
612
+ .collect();
613
+ assert!(errors.is_empty(), "cranelift failures:\n{}", errors.join("\n"));
614
+ }
615
+
616
+ /// Compile each .tish file to WASI, run with wasmtime, and compare stdout to static expected (parallelized).
617
+ /// Skips if wasmtime is not available.
618
+ #[test]
619
+ fn test_mvp_programs_wasi() {
620
+ let wasmtime_available = Command::new("wasmtime")
621
+ .arg("--version")
622
+ .output()
623
+ .map(|o| o.status.success())
624
+ .unwrap_or(false);
625
+ if !wasmtime_available {
626
+ eprintln!("Skipping test_mvp_programs_wasi: wasmtime not found");
627
+ return;
628
+ }
629
+ let core_dir = core_dir();
630
+ let bin = tish_bin();
631
+ assert!(
632
+ bin.exists(),
633
+ "tish binary not found at {}. Run `cargo build -p tish` first.",
634
+ bin.display()
635
+ );
636
+ let errors: Vec<String> = CRANELIFT_TEST_FILES
637
+ .par_iter()
638
+ .filter_map(|name| {
639
+ let path = core_dir.join(name);
640
+ if !path.exists() {
641
+ return None;
642
+ }
643
+ let expected = match get_expected(&path) {
644
+ Some(e) => e,
645
+ None => return Some(format!("missing expected: {}", path.display())),
646
+ };
647
+ let out_wasm = compile_cached(&bin, &path, "wasi");
648
+ let out = match Command::new("wasmtime")
649
+ .arg(out_wasm.as_os_str())
650
+ .current_dir(workspace_root())
651
+ .output()
652
+ {
653
+ Ok(o) => o,
654
+ Err(e) => {
655
+ let _ = std::fs::remove_file(&out_wasm);
656
+ return Some(format!("{}: wasmtime failed: {}", path.display(), e));
657
+ }
658
+ };
659
+ let _ = std::fs::remove_file(&out_wasm);
660
+ if !out.status.success() {
661
+ return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
662
+ }
663
+ let stdout = String::from_utf8_lossy(&out.stdout);
664
+ if stdout != expected {
665
+ return Some(format!("{}: output mismatch", path.display()));
666
+ }
667
+ None
668
+ })
669
+ .collect();
670
+ assert!(errors.is_empty(), "wasi failures:\n{}", errors.join("\n"));
671
+ }
672
+
673
+ /// Files where Tish intentionally differs from JavaScript (typeof, void); skip in JS test since we compare to Tish expected.
674
+ const JS_SKIP_FILES: &[&str] = &["typeof.tish", "void.tish"];
675
+
676
+ /// Compile each .tish file to JS, run with Node, and compare stdout to static expected.
677
+ #[test]
678
+ fn test_mvp_programs_js() {
679
+ let node_available = Command::new("node")
680
+ .args(["--version"])
681
+ .output()
682
+ .map(|o| o.status.success())
683
+ .unwrap_or(false);
684
+ if !node_available {
685
+ eprintln!("Skipping test_mvp_programs_js: Node.js not found");
686
+ return;
687
+ }
688
+ let core_dir = core_dir();
689
+ let bin = tish_bin();
690
+ assert!(
691
+ bin.exists(),
692
+ "tish binary not found at {}. Run `cargo build -p tish` first.",
693
+ bin.display()
694
+ );
695
+ for name in MVP_TEST_FILES {
696
+ if JS_SKIP_FILES.contains(name) {
697
+ continue;
698
+ }
699
+ let path = core_dir.join(name);
700
+ if !path.exists() {
701
+ continue;
702
+ }
703
+ let expected = get_expected(&path).unwrap_or_else(|| {
704
+ panic!(
705
+ "missing expected file for {}; run with REGENERATE_EXPECTED=1 to generate",
706
+ path.display()
707
+ )
708
+ });
709
+ let out_js = compile_cached(&bin, &path, "js");
710
+ let out = Command::new("node")
711
+ .arg(&out_js)
712
+ .current_dir(workspace_root())
713
+ .output()
714
+ .expect("run node");
715
+ let _ = std::fs::remove_file(&out_js);
716
+ assert!(
717
+ out.status.success(),
718
+ "Node failed for {}: {}",
719
+ path.display(),
720
+ String::from_utf8_lossy(&out.stderr)
721
+ );
722
+ let stdout = String::from_utf8_lossy(&out.stdout);
723
+ assert_eq!(stdout, expected, "JS output mismatch for {}", path.display());
724
+ }
725
+ }
726
+