@tishlang/tish 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/Cargo.toml +1 -0
  2. package/bin/tish +0 -0
  3. package/crates/js_to_tish/src/error.rs +2 -8
  4. package/crates/js_to_tish/src/transform/expr.rs +101 -130
  5. package/crates/js_to_tish/src/transform/stmt.rs +25 -22
  6. package/crates/tish/Cargo.toml +1 -1
  7. package/crates/tish/src/cli_help.rs +76 -29
  8. package/crates/tish/src/main.rs +85 -54
  9. package/crates/tish/tests/cargo_example_compile.rs +3 -1
  10. package/crates/tish/tests/integration_test.rs +197 -47
  11. package/crates/tish/tests/run_optimize_stdout_parity.rs +3 -7
  12. package/crates/tish/tests/shortcircuit.rs +19 -4
  13. package/crates/tish_ast/src/ast.rs +12 -14
  14. package/crates/tish_build_utils/src/lib.rs +31 -6
  15. package/crates/tish_builtins/src/array.rs +52 -21
  16. package/crates/tish_builtins/src/construct.rs +2 -8
  17. package/crates/tish_builtins/src/globals.rs +30 -15
  18. package/crates/tish_builtins/src/lib.rs +5 -5
  19. package/crates/tish_builtins/src/math.rs +5 -3
  20. package/crates/tish_builtins/src/string.rs +71 -19
  21. package/crates/tish_bytecode/src/chunk.rs +0 -1
  22. package/crates/tish_bytecode/src/compiler.rs +164 -60
  23. package/crates/tish_bytecode/src/opcode.rs +13 -4
  24. package/crates/tish_bytecode/src/peephole.rs +2 -2
  25. package/crates/tish_compile/src/codegen.rs +921 -299
  26. package/crates/tish_compile/src/infer.rs +69 -19
  27. package/crates/tish_compile/src/lib.rs +15 -5
  28. package/crates/tish_compile/src/resolve.rs +112 -69
  29. package/crates/tish_compile/src/types.rs +10 -14
  30. package/crates/tish_compile_js/src/codegen.rs +34 -13
  31. package/crates/tish_compile_js/src/tests_jsx.rs +30 -6
  32. package/crates/tish_compiler_wasm/src/lib.rs +16 -13
  33. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +39 -48
  34. package/crates/tish_core/src/json.rs +5 -3
  35. package/crates/tish_core/src/lib.rs +1 -1
  36. package/crates/tish_core/src/uri.rs +9 -6
  37. package/crates/tish_core/src/value.rs +92 -28
  38. package/crates/tish_cranelift/src/link.rs +6 -9
  39. package/crates/tish_cranelift/src/lower.rs +14 -8
  40. package/crates/tish_eval/src/eval.rs +389 -142
  41. package/crates/tish_eval/src/lib.rs +10 -6
  42. package/crates/tish_eval/src/natives.rs +95 -38
  43. package/crates/tish_eval/src/promise.rs +14 -8
  44. package/crates/tish_eval/src/timers.rs +28 -19
  45. package/crates/tish_eval/src/value.rs +10 -3
  46. package/crates/tish_fmt/src/lib.rs +29 -13
  47. package/crates/tish_lexer/src/lib.rs +217 -63
  48. package/crates/tish_lexer/src/token.rs +6 -6
  49. package/crates/tish_llvm/src/lib.rs +15 -8
  50. package/crates/tish_lsp/src/main.rs +41 -43
  51. package/crates/tish_native/src/build.rs +1 -6
  52. package/crates/tish_native/src/lib.rs +48 -19
  53. package/crates/tish_opt/src/lib.rs +67 -50
  54. package/crates/tish_parser/src/lib.rs +36 -11
  55. package/crates/tish_parser/src/parser.rs +172 -87
  56. package/crates/tish_runtime/src/http.rs +15 -6
  57. package/crates/tish_runtime/src/http_fetch.rs +24 -14
  58. package/crates/tish_runtime/src/lib.rs +224 -168
  59. package/crates/tish_runtime/src/promise.rs +1 -5
  60. package/crates/tish_runtime/src/ws.rs +45 -20
  61. package/crates/tish_runtime/tests/fetch_readable_stream.rs +5 -4
  62. package/crates/tish_ui/src/jsx.rs +41 -22
  63. package/crates/tish_ui/src/lib.rs +2 -2
  64. package/crates/tish_vm/src/vm.rs +309 -112
  65. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +8 -3
  66. package/crates/tish_wasm/src/lib.rs +38 -28
  67. package/crates/tishlang_cargo_bindgen/Cargo.toml +25 -0
  68. package/crates/tishlang_cargo_bindgen/src/classify.rs +265 -0
  69. package/crates/tishlang_cargo_bindgen/src/discover.rs +52 -0
  70. package/crates/tishlang_cargo_bindgen/src/infer.rs +372 -0
  71. package/crates/tishlang_cargo_bindgen/src/lib.rs +349 -0
  72. package/crates/tishlang_cargo_bindgen/src/main.rs +164 -0
  73. package/crates/tishlang_cargo_bindgen/src/metadata.rs +114 -0
  74. package/package.json +1 -1
  75. package/platform/darwin-arm64/tish +0 -0
  76. package/platform/darwin-x64/tish +0 -0
  77. package/platform/linux-arm64/tish +0 -0
  78. package/platform/linux-x64/tish +0 -0
  79. package/platform/win32-x64/tish.exe +0 -0
@@ -7,16 +7,18 @@
7
7
  //! - Compiled outputs are cached under `target/integration_compile_cache/` per backend.
8
8
 
9
9
  use std::collections::hash_map::DefaultHasher;
10
+ use std::ffi::OsString;
10
11
  use std::hash::{Hash, Hasher};
11
12
  use std::io::Read;
12
- use std::ffi::OsString;
13
13
  use std::path::{Path, PathBuf};
14
14
  use std::process::Command;
15
15
 
16
16
  use rayon::prelude::*;
17
17
 
18
18
  fn workspace_root() -> PathBuf {
19
- PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("..").join("..")
19
+ PathBuf::from(env!("CARGO_MANIFEST_DIR"))
20
+ .join("..")
21
+ .join("..")
20
22
  }
21
23
 
22
24
  fn core_dir() -> PathBuf {
@@ -25,7 +27,10 @@ fn core_dir() -> PathBuf {
25
27
 
26
28
  /// Path to the static expected stdout for a .tish file (e.g. fn_any.tish -> fn_any.tish.expected).
27
29
  fn expected_path(path: &Path) -> PathBuf {
28
- path.with_file_name(format!("{}.expected", path.file_name().unwrap().to_string_lossy()))
30
+ path.with_file_name(format!(
31
+ "{}.expected",
32
+ path.file_name().unwrap().to_string_lossy()
33
+ ))
29
34
  }
30
35
 
31
36
  /// Read static expected stdout for a test file. Returns None if the file does not exist.
@@ -70,7 +75,11 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
70
75
 
71
76
  let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
72
77
  "native" => {
73
- let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
78
+ let ext = if cfg!(target_os = "windows") {
79
+ ".exe"
80
+ } else {
81
+ ""
82
+ };
74
83
  let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
75
84
  let args = vec![
76
85
  OsString::from("build"),
@@ -81,7 +90,11 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
81
90
  (cached, args)
82
91
  }
83
92
  "cranelift" => {
84
- let ext = if cfg!(target_os = "windows") { ".exe" } else { "" };
93
+ let ext = if cfg!(target_os = "windows") {
94
+ ".exe"
95
+ } else {
96
+ ""
97
+ };
85
98
  let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
86
99
  let args = vec![
87
100
  OsString::from("build"),
@@ -137,8 +150,12 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
137
150
  }
138
151
 
139
152
  // 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));
153
+ let ext = artifact_path
154
+ .extension()
155
+ .map(|e| e.to_string_lossy().to_string())
156
+ .unwrap_or_default();
157
+ let temp_dest =
158
+ std::env::temp_dir().join(format!("tish_cached_{}_{}_{}", backend, stem, hash8));
142
159
  let temp_dest = if ext.is_empty() {
143
160
  temp_dest
144
161
  } else {
@@ -151,12 +168,20 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
151
168
  /// Path to the tish CLI binary. When running under cargo-llvm-cov, the build goes to
152
169
  /// target/llvm-cov-target and CARGO_TARGET_DIR may not be set for the test process.
153
170
  fn tish_bin() -> PathBuf {
154
- let bin_name = if cfg!(target_os = "windows") { "tish.exe" } else { "tish" };
171
+ let bin_name = if cfg!(target_os = "windows") {
172
+ "tish.exe"
173
+ } else {
174
+ "tish"
175
+ };
155
176
  let default = target_dir().join("debug").join(bin_name);
156
177
  if default.exists() {
157
178
  return default;
158
179
  }
159
- let llvm_cov = workspace_root().join("target").join("llvm-cov-target").join("debug").join(bin_name);
180
+ let llvm_cov = workspace_root()
181
+ .join("target")
182
+ .join("llvm-cov-target")
183
+ .join("debug")
184
+ .join(bin_name);
160
185
  if llvm_cov.exists() {
161
186
  return llvm_cov;
162
187
  }
@@ -167,9 +192,16 @@ fn tish_bin() -> PathBuf {
167
192
  #[test]
168
193
  fn test_tish_version_flag() {
169
194
  let bin = tish_bin();
170
- assert!(bin.exists(), "tish binary not found. Run `cargo build -p tishlang` first.");
195
+ assert!(
196
+ bin.exists(),
197
+ "tish binary not found. Run `cargo build -p tishlang` first."
198
+ );
171
199
  let out = Command::new(&bin).arg("-V").output().expect("run tish -V");
172
- assert!(out.status.success(), "tish -V failed: {}", String::from_utf8_lossy(&out.stderr));
200
+ assert!(
201
+ out.status.success(),
202
+ "tish -V failed: {}",
203
+ String::from_utf8_lossy(&out.stderr)
204
+ );
173
205
  let stdout = String::from_utf8_lossy(&out.stdout);
174
206
  assert!(
175
207
  stdout.contains(env!("CARGO_PKG_VERSION")),
@@ -177,20 +209,35 @@ fn test_tish_version_flag() {
177
209
  env!("CARGO_PKG_VERSION"),
178
210
  stdout
179
211
  );
180
- let out2 = Command::new(&bin).arg("--version").output().expect("run tish --version");
212
+ let out2 = Command::new(&bin)
213
+ .arg("--version")
214
+ .output()
215
+ .expect("run tish --version");
181
216
  assert!(out2.status.success());
182
217
  let stdout2 = String::from_utf8_lossy(&out2.stdout);
183
- assert!(stdout2.contains(env!("CARGO_PKG_VERSION")), "tish --version should print version");
218
+ assert!(
219
+ stdout2.contains(env!("CARGO_PKG_VERSION")),
220
+ "tish --version should print version"
221
+ );
184
222
  }
185
223
 
186
224
  /// Parse async-await example (validates async fn parsing).
187
225
  #[test]
188
226
  fn test_async_await_parse() {
189
- let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
227
+ let path = workspace_root()
228
+ .join("examples")
229
+ .join("async-await")
230
+ .join("src")
231
+ .join("main.tish");
190
232
  if path.exists() {
191
233
  let source = std::fs::read_to_string(&path).unwrap();
192
234
  let result = tishlang_parser::parse(&source);
193
- assert!(result.is_ok(), "Parse failed for {}: {:?}", path.display(), result.err());
235
+ assert!(
236
+ result.is_ok(),
237
+ "Parse failed for {}: {:?}",
238
+ path.display(),
239
+ result.err()
240
+ );
194
241
  }
195
242
  }
196
243
 
@@ -199,11 +246,20 @@ fn test_async_await_parse() {
199
246
  #[cfg(feature = "http")]
200
247
  fn test_async_await_compile_via_binary() {
201
248
  let bin = tish_bin();
202
- let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
249
+ let path = workspace_root()
250
+ .join("examples")
251
+ .join("async-await")
252
+ .join("src")
253
+ .join("main.tish");
203
254
  if path.exists() && bin.exists() {
204
255
  let out = std::env::temp_dir().join("tish_async_test_out");
205
256
  let compile_result = Command::new(&bin)
206
- .args(["build", path.to_string_lossy().as_ref(), "-o", out.to_string_lossy().as_ref()])
257
+ .args([
258
+ "build",
259
+ path.to_string_lossy().as_ref(),
260
+ "-o",
261
+ out.to_string_lossy().as_ref(),
262
+ ])
207
263
  .current_dir(workspace_root())
208
264
  .output();
209
265
  let compile_out = compile_result.expect("run tish build");
@@ -213,9 +269,7 @@ fn test_async_await_compile_via_binary() {
213
269
  String::from_utf8_lossy(&compile_out.stderr)
214
270
  );
215
271
  // Run compiled binary to validate non-blocking fetchAll executes correctly
216
- let run_result = Command::new(&out)
217
- .current_dir(workspace_root())
218
- .output();
272
+ let run_result = Command::new(&out).current_dir(workspace_root()).output();
219
273
  let run_out = run_result.expect("run compiled async binary");
220
274
  assert!(
221
275
  run_out.status.success(),
@@ -223,7 +277,10 @@ fn test_async_await_compile_via_binary() {
223
277
  String::from_utf8_lossy(&run_out.stderr)
224
278
  );
225
279
  let stdout = String::from_utf8_lossy(&run_out.stdout);
226
- assert!(stdout.contains("Fetching"), "expected output to mention fetching");
280
+ assert!(
281
+ stdout.contains("Fetching"),
282
+ "expected output to mention fetching"
283
+ );
227
284
  assert!(stdout.contains("Done"), "expected output to contain Done");
228
285
  }
229
286
  }
@@ -235,8 +292,16 @@ fn test_async_await_compile_via_binary() {
235
292
  #[ignore = "timing and network sensitive; run manually: cargo test test_async_parallel_vs_sequential_timing -p tishlang--features http -- --ignored"]
236
293
  fn test_async_parallel_vs_sequential_timing() {
237
294
  let bin = tish_bin();
238
- let parallel_src = workspace_root().join("examples").join("async-await").join("src").join("parallel.tish");
239
- let sequential_src = workspace_root().join("examples").join("async-await").join("src").join("sequential.tish");
295
+ let parallel_src = workspace_root()
296
+ .join("examples")
297
+ .join("async-await")
298
+ .join("src")
299
+ .join("parallel.tish");
300
+ let sequential_src = workspace_root()
301
+ .join("examples")
302
+ .join("async-await")
303
+ .join("src")
304
+ .join("sequential.tish");
240
305
  if !parallel_src.exists() || !sequential_src.exists() || !bin.exists() {
241
306
  return;
242
307
  }
@@ -245,28 +310,58 @@ fn test_async_parallel_vs_sequential_timing() {
245
310
 
246
311
  // Compile both
247
312
  let compile_par = Command::new(&bin)
248
- .args(["build", parallel_src.to_string_lossy().as_ref(), "-o", out_parallel.to_string_lossy().as_ref()])
313
+ .args([
314
+ "build",
315
+ parallel_src.to_string_lossy().as_ref(),
316
+ "-o",
317
+ out_parallel.to_string_lossy().as_ref(),
318
+ ])
249
319
  .current_dir(workspace_root())
250
320
  .output();
251
- assert!(compile_par.as_ref().unwrap().status.success(), "compile parallel: {}", String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr));
321
+ assert!(
322
+ compile_par.as_ref().unwrap().status.success(),
323
+ "compile parallel: {}",
324
+ String::from_utf8_lossy(&compile_par.as_ref().unwrap().stderr)
325
+ );
252
326
 
253
327
  let compile_seq = Command::new(&bin)
254
- .args(["build", sequential_src.to_string_lossy().as_ref(), "-o", out_sequential.to_string_lossy().as_ref()])
328
+ .args([
329
+ "build",
330
+ sequential_src.to_string_lossy().as_ref(),
331
+ "-o",
332
+ out_sequential.to_string_lossy().as_ref(),
333
+ ])
255
334
  .current_dir(workspace_root())
256
335
  .output();
257
- assert!(compile_seq.as_ref().unwrap().status.success(), "compile sequential: {}", String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr));
336
+ assert!(
337
+ compile_seq.as_ref().unwrap().status.success(),
338
+ "compile sequential: {}",
339
+ String::from_utf8_lossy(&compile_seq.as_ref().unwrap().stderr)
340
+ );
258
341
 
259
342
  // Run parallel and time
260
343
  let t_parallel = std::time::Instant::now();
261
- let run_par = Command::new(&out_parallel).current_dir(workspace_root()).output();
344
+ let run_par = Command::new(&out_parallel)
345
+ .current_dir(workspace_root())
346
+ .output();
262
347
  let elapsed_parallel = t_parallel.elapsed();
263
- assert!(run_par.as_ref().unwrap().status.success(), "run parallel: {}", String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr));
348
+ assert!(
349
+ run_par.as_ref().unwrap().status.success(),
350
+ "run parallel: {}",
351
+ String::from_utf8_lossy(&run_par.as_ref().unwrap().stderr)
352
+ );
264
353
 
265
354
  // Run sequential and time
266
355
  let t_sequential = std::time::Instant::now();
267
- let run_seq = Command::new(&out_sequential).current_dir(workspace_root()).output();
356
+ let run_seq = Command::new(&out_sequential)
357
+ .current_dir(workspace_root())
358
+ .output();
268
359
  let elapsed_sequential = t_sequential.elapsed();
269
- assert!(run_seq.as_ref().unwrap().status.success(), "run sequential: {}", String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr));
360
+ assert!(
361
+ run_seq.as_ref().unwrap().status.success(),
362
+ "run sequential: {}",
363
+ String::from_utf8_lossy(&run_seq.as_ref().unwrap().stderr)
364
+ );
270
365
 
271
366
  // PARALLEL MUST BE FASTER: parallel < sequential * 0.6 (parallel ~1s, sequential ~3s)
272
367
  let parallel_secs = elapsed_parallel.as_secs_f64();
@@ -285,11 +380,20 @@ fn test_async_parallel_vs_sequential_timing() {
285
380
  #[cfg(feature = "http")]
286
381
  #[ignore = "requires async runtime; use test_async_await_compile_via_binary for CI"]
287
382
  fn test_async_await_run() {
288
- let path = workspace_root().join("examples").join("async-await").join("src").join("main.tish");
383
+ let path = workspace_root()
384
+ .join("examples")
385
+ .join("async-await")
386
+ .join("src")
387
+ .join("main.tish");
289
388
  if path.exists() {
290
389
  let source = std::fs::read_to_string(&path).unwrap();
291
390
  let result = tishlang_eval::run(&source);
292
- assert!(result.is_ok(), "Run failed for {}: {:?}", path.display(), result.err());
391
+ assert!(
392
+ result.is_ok(),
393
+ "Run failed for {}: {:?}",
394
+ path.display(),
395
+ result.err()
396
+ );
293
397
  }
294
398
  }
295
399
 
@@ -300,7 +404,10 @@ fn test_async_await_run() {
300
404
  #[ignore = "requires async runtime"]
301
405
  fn test_promise_and_settimeout() {
302
406
  for name in ["promise", "settimeout"] {
303
- let path = workspace_root().join("tests").join("modules").join(format!("{}.tish", name));
407
+ let path = workspace_root()
408
+ .join("tests")
409
+ .join("modules")
410
+ .join(format!("{}.tish", name));
304
411
  if path.exists() {
305
412
  let source = std::fs::read_to_string(&path).unwrap();
306
413
  let result = tishlang_eval::run(&source);
@@ -338,7 +445,10 @@ fn test_async_promise_settimeout_combined() {
338
445
  /// VM run with Date global (resolve+merge+bytecode+run pipeline).
339
446
  #[test]
340
447
  fn test_vm_date_now() {
341
- let path = workspace_root().join("tests").join("core").join("date.tish");
448
+ let path = workspace_root()
449
+ .join("tests")
450
+ .join("core")
451
+ .join("date.tish");
342
452
  if !path.exists() {
343
453
  return;
344
454
  }
@@ -348,7 +458,11 @@ fn test_vm_date_now() {
348
458
  let program = tishlang_compile::merge_modules(modules).expect("merge");
349
459
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
350
460
  let result = tishlang_vm::run(&chunk);
351
- assert!(result.is_ok(), "VM run (library) failed: {:?}", result.err());
461
+ assert!(
462
+ result.is_ok(),
463
+ "VM run (library) failed: {:?}",
464
+ result.err()
465
+ );
352
466
  // Binary path - same flow as `tish run <file>`
353
467
  let bin = tish_bin();
354
468
  if bin.exists() {
@@ -379,20 +493,30 @@ fn test_vm_index_assign_direct() {
379
493
  /// VM run via resolve+merge (same as tish run) - must also pass.
380
494
  #[test]
381
495
  fn test_vm_index_assign_via_resolve() {
382
- let path = workspace_root().join("tests").join("core").join("array_sort_minimal.tish");
496
+ let path = workspace_root()
497
+ .join("tests")
498
+ .join("core")
499
+ .join("array_sort_minimal.tish");
383
500
  let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
384
501
  tishlang_compile::detect_cycles(&modules).expect("cycles");
385
502
  let program = tishlang_compile::merge_modules(modules).expect("merge");
386
503
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
387
504
  let result = tishlang_vm::run(&chunk);
388
- assert!(result.is_ok(), "VM IndexAssign via resolve failed: {:?}", result.err());
505
+ assert!(
506
+ result.is_ok(),
507
+ "VM IndexAssign via resolve failed: {:?}",
508
+ result.err()
509
+ );
389
510
  }
390
511
 
391
512
  /// tish run binary must pass array_sort_minimal (ensures CLI works).
392
513
  #[test]
393
514
  fn test_tish_run_index_assign() {
394
515
  let bin = tish_bin();
395
- let path = workspace_root().join("tests").join("core").join("array_sort_minimal.tish");
516
+ let path = workspace_root()
517
+ .join("tests")
518
+ .join("core")
519
+ .join("array_sort_minimal.tish");
396
520
  if !bin.exists() {
397
521
  eprintln!("Skipping: tish binary not built");
398
522
  return;
@@ -516,7 +640,12 @@ fn test_mvp_programs_interpreter() {
516
640
  path.display()
517
641
  )
518
642
  });
519
- assert_eq!(stdout, expected, "Interpreter output mismatch for {}", path.display());
643
+ assert_eq!(
644
+ stdout,
645
+ expected,
646
+ "Interpreter output mismatch for {}",
647
+ path.display()
648
+ );
520
649
  }
521
650
  }
522
651
  }
@@ -562,7 +691,8 @@ fn test_mvp_programs_interp_vm_stdout_parity() {
562
691
  let s_interp = String::from_utf8_lossy(&out_interp.stdout);
563
692
  let s_vm = String::from_utf8_lossy(&out_vm.stdout);
564
693
  assert_eq!(
565
- s_interp, s_vm,
694
+ s_interp,
695
+ s_vm,
566
696
  "interp vs VM stdout mismatch for {}",
567
697
  path.display()
568
698
  );
@@ -603,7 +733,11 @@ fn test_mvp_programs_native() {
603
733
  };
604
734
  let _ = std::fs::remove_file(&out_bin);
605
735
  if !out.status.success() {
606
- return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
736
+ return Some(format!(
737
+ "{}: {}",
738
+ path.display(),
739
+ String::from_utf8_lossy(&out.stderr)
740
+ ));
607
741
  }
608
742
  let stdout = String::from_utf8_lossy(&out.stdout);
609
743
  if stdout != expected {
@@ -670,7 +804,11 @@ fn test_mvp_programs_cranelift() {
670
804
  };
671
805
  let _ = std::fs::remove_file(&out_bin);
672
806
  if !out.status.success() {
673
- return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
807
+ return Some(format!(
808
+ "{}: {}",
809
+ path.display(),
810
+ String::from_utf8_lossy(&out.stderr)
811
+ ));
674
812
  }
675
813
  let stdout = String::from_utf8_lossy(&out.stdout);
676
814
  if stdout != expected {
@@ -679,7 +817,11 @@ fn test_mvp_programs_cranelift() {
679
817
  None
680
818
  })
681
819
  .collect();
682
- assert!(errors.is_empty(), "cranelift failures:\n{}", errors.join("\n"));
820
+ assert!(
821
+ errors.is_empty(),
822
+ "cranelift failures:\n{}",
823
+ errors.join("\n")
824
+ );
683
825
  }
684
826
 
685
827
  /// Compile each .tish file to WASI, run with wasmtime, and compare stdout to static expected (parallelized).
@@ -727,7 +869,11 @@ fn test_mvp_programs_wasi() {
727
869
  };
728
870
  let _ = std::fs::remove_file(&out_wasm);
729
871
  if !out.status.success() {
730
- return Some(format!("{}: {}", path.display(), String::from_utf8_lossy(&out.stderr)));
872
+ return Some(format!(
873
+ "{}: {}",
874
+ path.display(),
875
+ String::from_utf8_lossy(&out.stderr)
876
+ ));
731
877
  }
732
878
  let stdout = String::from_utf8_lossy(&out.stdout);
733
879
  if stdout != expected {
@@ -789,7 +935,11 @@ fn test_mvp_programs_js() {
789
935
  String::from_utf8_lossy(&out.stderr)
790
936
  );
791
937
  let stdout = String::from_utf8_lossy(&out.stdout);
792
- assert_eq!(stdout, expected, "JS output mismatch for {}", path.display());
938
+ assert_eq!(
939
+ stdout,
940
+ expected,
941
+ "JS output mismatch for {}",
942
+ path.display()
943
+ );
793
944
  }
794
945
  }
795
-
@@ -15,12 +15,7 @@ fn string_or_fixture_stdout_matches_with_and_without_optimize() {
15
15
  assert!(fixture.is_file(), "missing fixture {}", fixture.display());
16
16
 
17
17
  let out_default = Command::new(&tish)
18
- .args([
19
- "run",
20
- "--feature",
21
- "process",
22
- fixture.to_str().unwrap(),
23
- ])
18
+ .args(["run", "--feature", "process", fixture.to_str().unwrap()])
24
19
  .output()
25
20
  .expect("spawn tish run");
26
21
  assert!(
@@ -46,7 +41,8 @@ fn string_or_fixture_stdout_matches_with_and_without_optimize() {
46
41
  );
47
42
 
48
43
  assert_eq!(
49
- out_default.stdout, out_noopt.stdout,
44
+ out_default.stdout,
45
+ out_noopt.stdout,
50
46
  "stdout differs:\n default: {:?}\n noopt: {:?}",
51
47
  String::from_utf8_lossy(&out_default.stdout),
52
48
  String::from_utf8_lossy(&out_noopt.stdout)
@@ -14,7 +14,10 @@ fn test_and_shortcircuit_emits_jump() {
14
14
  let chunk = compile_unoptimized(&program).expect("compile");
15
15
  let code = &chunk.code;
16
16
  let has_jump_if_false = code.windows(1).any(|w| w[0] == Opcode::JumpIfFalse as u8);
17
- assert!(has_jump_if_false, "And should emit JumpIfFalse for short-circuit");
17
+ assert!(
18
+ has_jump_if_false,
19
+ "And should emit JumpIfFalse for short-circuit"
20
+ );
18
21
  }
19
22
 
20
23
  #[test]
@@ -23,7 +26,11 @@ fn test_and_shortcircuit_runs_unoptimized() {
23
26
  let program = parse(source).expect("parse");
24
27
  let chunk = compile_unoptimized(&program).expect("compile");
25
28
  let result = tishlang_vm::run(&chunk);
26
- assert!(result.is_ok(), "Should not throw (short-circuit avoids x.foo): {:?}", result.err());
29
+ assert!(
30
+ result.is_ok(),
31
+ "Should not throw (short-circuit avoids x.foo): {:?}",
32
+ result.err()
33
+ );
27
34
  }
28
35
 
29
36
  #[test]
@@ -33,7 +40,11 @@ fn test_and_shortcircuit_runs_optimized() {
33
40
  let program = tishlang_opt::optimize(&program);
34
41
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
35
42
  let result = tishlang_vm::run(&chunk);
36
- assert!(result.is_ok(), "Should not throw with peephole (short-circuit): {:?}", result.err());
43
+ assert!(
44
+ result.is_ok(),
45
+ "Should not throw with peephole (short-circuit): {:?}",
46
+ result.err()
47
+ );
37
48
  }
38
49
 
39
50
  #[test]
@@ -46,5 +57,9 @@ fn test_and_shortcircuit_via_resolve_project() {
46
57
  let program = tishlang_opt::optimize(&program); // Mirror CLI
47
58
  let chunk = compile(&program).expect("compile");
48
59
  let result = tishlang_vm::run(&chunk);
49
- assert!(result.is_ok(), "Should not throw via resolve+merge+opt (CLI path): {:?}", result.err());
60
+ assert!(
61
+ result.is_ok(),
62
+ "Should not throw via resolve+merge+opt (CLI path): {:?}",
63
+ result.err()
64
+ );
50
65
  }
@@ -34,7 +34,6 @@ pub struct TypedParam {
34
34
  pub default: Option<Expr>,
35
35
  }
36
36
 
37
-
38
37
  /// Single formal parameter: simple identifier or destructuring pattern.
39
38
  #[derive(Debug, Clone, PartialEq)]
40
39
  pub enum FunParam {
@@ -46,7 +45,6 @@ pub enum FunParam {
46
45
  },
47
46
  }
48
47
 
49
-
50
48
  impl FunParam {
51
49
  /// Variable names introduced by this formal parameter.
52
50
  pub fn bound_names(&self) -> Vec<Arc<str>> {
@@ -123,7 +121,10 @@ pub struct DestructProp {
123
121
  #[derive(Debug, Clone, PartialEq)]
124
122
  pub enum ImportSpecifier {
125
123
  /// Named: { foo } or { foo as bar }
126
- Named { name: Arc<str>, alias: Option<Arc<str>> },
124
+ Named {
125
+ name: Arc<str>,
126
+ alias: Option<Arc<str>>,
127
+ },
127
128
  /// Namespace: * as M
128
129
  Namespace(Arc<str>),
129
130
  /// Default: import X from "..."
@@ -366,8 +367,8 @@ pub enum Expr {
366
367
  },
367
368
  /// Template literal: `text ${expr} text`
368
369
  TemplateLiteral {
369
- quasis: Vec<Arc<str>>, // Static string parts (n+1 for n expressions)
370
- exprs: Vec<Expr>, // Interpolated expressions (n)
370
+ quasis: Vec<Arc<str>>, // Static string parts (n+1 for n expressions)
371
+ exprs: Vec<Expr>, // Interpolated expressions (n)
371
372
  span: Span,
372
373
  },
373
374
  /// Await expression: await operand
@@ -399,10 +400,7 @@ pub enum Expr {
399
400
  #[derive(Debug, Clone, PartialEq)]
400
401
  pub enum JsxProp {
401
402
  /// name="value" or name={expr} or name (boolean shorthand)
402
- Attr {
403
- name: Arc<str>,
404
- value: JsxAttrValue,
405
- },
403
+ Attr { name: Arc<str>, value: JsxAttrValue },
406
404
  /// {...expr}
407
405
  Spread(Expr),
408
406
  }
@@ -493,11 +491,11 @@ pub enum CallArg {
493
491
 
494
492
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
495
493
  pub enum CompoundOp {
496
- Add, // +=
497
- Sub, // -=
498
- Mul, // *=
499
- Div, // /=
500
- Mod, // %=
494
+ Add, // +=
495
+ Sub, // -=
496
+ Mul, // *=
497
+ Div, // /=
498
+ Mod, // %=
501
499
  }
502
500
 
503
501
  #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -158,7 +158,9 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
158
158
  if let Some(mut current) = exe.parent() {
159
159
  for _ in 0..15 {
160
160
  let crates_dir = current.join("crates");
161
- if crates_dir.join("tish_runtime").exists() || crates_dir.join("tish_cranelift_runtime").exists() {
161
+ if crates_dir.join("tish_runtime").exists()
162
+ || crates_dir.join("tish_cranelift_runtime").exists()
163
+ {
162
164
  return Ok(current.to_path_buf());
163
165
  }
164
166
  if let Some(p) = current.parent() {
@@ -177,6 +179,19 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
177
179
  }
178
180
  }
179
181
 
182
+ // Strategy 3b: `node_modules/@tishlang/tish` from cwd or any ancestor (package.json-only apps)
183
+ if let Ok(mut dir) = std::env::current_dir() {
184
+ for _ in 0..32 {
185
+ let npm_pkg = dir.join("node_modules").join("@tishlang").join("tish");
186
+ if is_tish_workspace_root(&npm_pkg) {
187
+ return Ok(npm_pkg);
188
+ }
189
+ if !dir.pop() {
190
+ break;
191
+ }
192
+ }
193
+ }
194
+
180
195
  // Strategy 4: Walk from current working directory
181
196
  if let Ok(mut current) = std::env::current_dir() {
182
197
  for _ in 0..15 {
@@ -276,16 +291,22 @@ pub fn find_crate_path(crate_name: &str) -> Result<PathBuf, String> {
276
291
 
277
292
  /// Create a temp build directory with src subdir.
278
293
  pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String> {
279
- let build_dir = std::env::temp_dir().join(prefix).join(format!("{}_{}", out_name, std::process::id()));
294
+ let build_dir =
295
+ std::env::temp_dir()
296
+ .join(prefix)
297
+ .join(format!("{}_{}", out_name, std::process::id()));
280
298
  fs::create_dir_all(&build_dir).map_err(|e| format!("Cannot create build dir: {}", e))?;
281
- fs::create_dir_all(build_dir.join("src")).map_err(|e| format!("Cannot create src dir: {}", e))?;
299
+ fs::create_dir_all(build_dir.join("src"))
300
+ .map_err(|e| format!("Cannot create src dir: {}", e))?;
282
301
  Ok(build_dir)
283
302
  }
284
303
 
285
304
  /// Run cargo build in the given directory.
286
305
  /// If target_dir is Some, use that for --target-dir (e.g. workspace target for caching).
287
306
  pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<(), String> {
288
- let target_dir = target_dir.map(|p| p.to_path_buf()).unwrap_or_else(|| build_dir.join("target"));
307
+ let target_dir = target_dir
308
+ .map(|p| p.to_path_buf())
309
+ .unwrap_or_else(|| build_dir.join("target"));
289
310
  let output = Command::new("cargo")
290
311
  .args(["build", "--release", "--target-dir"])
291
312
  .arg(&target_dir)
@@ -298,7 +319,10 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
298
319
  if !output.status.success() {
299
320
  let stderr = String::from_utf8_lossy(&output.stderr);
300
321
  let stdout = String::from_utf8_lossy(&output.stdout);
301
- return Err(format!("Compilation failed.\nstdout:\n{}\nstderr:\n{}", stdout, stderr));
322
+ return Err(format!(
323
+ "Compilation failed.\nstdout:\n{}\nstderr:\n{}",
324
+ stdout, stderr
325
+ ));
302
326
  }
303
327
  Ok(())
304
328
  }
@@ -351,7 +375,8 @@ pub fn copy_binary_to_output(binary: &Path, output_path: &Path) -> Result<(), St
351
375
  if let Some(parent) = output_path.parent() {
352
376
  fs::create_dir_all(parent).map_err(|e| format!("Cannot create output dir: {}", e))?;
353
377
  }
354
- fs::copy(binary, output_path).map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
378
+ fs::copy(binary, output_path)
379
+ .map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
355
380
  Ok(())
356
381
  }
357
382