@tishlang/tish 1.7.0 → 1.9.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 (99) hide show
  1. package/Cargo.toml +2 -0
  2. package/README.md +2 -0
  3. package/bin/tish +0 -0
  4. package/crates/js_to_tish/src/transform/expr.rs +28 -8
  5. package/crates/js_to_tish/src/transform/stmt.rs +49 -22
  6. package/crates/tish/Cargo.toml +14 -5
  7. package/crates/tish/src/cargo_native_registry.rs +29 -0
  8. package/crates/tish/src/cli_help.rs +16 -10
  9. package/crates/tish/src/main.rs +87 -32
  10. package/crates/tish/src/repl_completion.rs +3 -3
  11. package/crates/tish/tests/cargo_example_compile.rs +1 -1
  12. package/crates/tish/tests/integration_test.rs +19 -7
  13. package/crates/tish/tests/shortcircuit.rs +1 -1
  14. package/crates/tish_ast/src/ast.rs +80 -9
  15. package/crates/tish_build_utils/Cargo.toml +4 -0
  16. package/crates/tish_build_utils/src/lib.rs +105 -2
  17. package/crates/tish_builtins/Cargo.toml +5 -1
  18. package/crates/tish_builtins/src/array.rs +13 -12
  19. package/crates/tish_builtins/src/construct.rs +34 -33
  20. package/crates/tish_builtins/src/globals.rs +12 -11
  21. package/crates/tish_builtins/src/helpers.rs +2 -1
  22. package/crates/tish_builtins/src/object.rs +3 -2
  23. package/crates/tish_builtins/src/string.rs +73 -3
  24. package/crates/tish_bytecode/src/compiler.rs +12 -14
  25. package/crates/tish_bytecode/src/opcode.rs +12 -3
  26. package/crates/tish_compile/Cargo.toml +1 -0
  27. package/crates/tish_compile/src/codegen.rs +745 -199
  28. package/crates/tish_compile/src/infer.rs +6 -0
  29. package/crates/tish_compile/src/lib.rs +4 -3
  30. package/crates/tish_compile/src/resolve.rs +180 -82
  31. package/crates/tish_compile/src/types.rs +175 -11
  32. package/crates/tish_compile_js/Cargo.toml +1 -0
  33. package/crates/tish_compile_js/src/codegen.rs +152 -29
  34. package/crates/tish_compile_js/src/lib.rs +3 -1
  35. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +31 -12
  36. package/crates/tish_core/Cargo.toml +8 -0
  37. package/crates/tish_core/src/json.rs +102 -53
  38. package/crates/tish_core/src/lib.rs +3 -1
  39. package/crates/tish_core/src/macros.rs +5 -5
  40. package/crates/tish_core/src/value.rs +53 -15
  41. package/crates/tish_core/src/vmref.rs +178 -0
  42. package/crates/tish_eval/Cargo.toml +17 -2
  43. package/crates/tish_eval/src/eval.rs +90 -28
  44. package/crates/tish_eval/src/http.rs +61 -0
  45. package/crates/tish_eval/src/lib.rs +3 -3
  46. package/crates/tish_eval/src/natives.rs +41 -0
  47. package/crates/tish_eval/src/value.rs +7 -3
  48. package/crates/tish_eval/src/value_convert.rs +13 -5
  49. package/crates/tish_fmt/src/lib.rs +120 -30
  50. package/crates/tish_lexer/src/lib.rs +20 -5
  51. package/crates/tish_lexer/src/token.rs +4 -0
  52. package/crates/tish_llvm/src/lib.rs +3 -1
  53. package/crates/tish_lsp/Cargo.toml +4 -1
  54. package/crates/tish_lsp/README.md +1 -1
  55. package/crates/tish_lsp/src/builtin_goto.rs +261 -0
  56. package/crates/tish_lsp/src/import_goto.rs +549 -0
  57. package/crates/tish_lsp/src/main.rs +502 -102
  58. package/crates/tish_native/src/build.rs +3 -2
  59. package/crates/tish_native/src/lib.rs +6 -2
  60. package/crates/tish_opt/src/lib.rs +17 -2
  61. package/crates/tish_parser/src/lib.rs +10 -3
  62. package/crates/tish_parser/src/parser.rs +346 -56
  63. package/crates/tish_pg/Cargo.toml +34 -0
  64. package/crates/tish_pg/README.md +38 -0
  65. package/crates/tish_pg/src/error.rs +52 -0
  66. package/crates/tish_pg/src/lib.rs +967 -0
  67. package/crates/tish_resolve/Cargo.toml +13 -0
  68. package/crates/tish_resolve/src/lib.rs +3436 -0
  69. package/crates/tish_resolve/src/pos.rs +133 -0
  70. package/crates/tish_runtime/Cargo.toml +68 -3
  71. package/crates/tish_runtime/src/http.rs +1123 -141
  72. package/crates/tish_runtime/src/http_fetch.rs +15 -14
  73. package/crates/tish_runtime/src/http_hyper.rs +418 -0
  74. package/crates/tish_runtime/src/http_prefork.rs +189 -0
  75. package/crates/tish_runtime/src/lib.rs +159 -29
  76. package/crates/tish_runtime/src/promise.rs +199 -36
  77. package/crates/tish_runtime/src/promise_io.rs +2 -1
  78. package/crates/tish_runtime/src/timers.rs +37 -1
  79. package/crates/tish_runtime/src/ws.rs +26 -28
  80. package/crates/tish_ui/src/jsx.rs +279 -8
  81. package/crates/tish_ui/src/lib.rs +5 -2
  82. package/crates/tish_ui/src/runtime/hooks.rs +406 -45
  83. package/crates/tish_ui/src/runtime/mod.rs +36 -9
  84. package/crates/tish_vm/Cargo.toml +15 -5
  85. package/crates/tish_vm/src/vm.rs +506 -259
  86. package/crates/tish_vm/tests/peephole_jump_chain_logical_or.rs +3 -1
  87. package/crates/tish_wasm/src/lib.rs +17 -14
  88. package/crates/tish_wasm_runtime/Cargo.toml +2 -1
  89. package/crates/tish_wasm_runtime/src/lib.rs +1 -1
  90. package/crates/tishlang_cargo_bindgen/Cargo.toml +1 -0
  91. package/crates/tishlang_cargo_bindgen/src/discover.rs +68 -0
  92. package/crates/tishlang_cargo_bindgen/src/lib.rs +5 -4
  93. package/justfile +8 -0
  94. package/package.json +1 -1
  95. package/platform/darwin-arm64/tish +0 -0
  96. package/platform/darwin-x64/tish +0 -0
  97. package/platform/linux-arm64/tish +0 -0
  98. package/platform/linux-x64/tish +0 -0
  99. package/platform/win32-x64/tish.exe +0 -0
@@ -1,9 +1,11 @@
1
1
  //! Tish CLI - run, REPL, build to native or other targets.
2
2
 
3
+ mod cargo_native_registry;
3
4
  mod cli_help;
4
5
  mod repl_completion;
5
6
 
6
7
  use std::cell::RefCell;
8
+ use tishlang_core::VmRef;
7
9
  use std::collections::HashSet;
8
10
  use std::fs;
9
11
  use std::io::{self, IsTerminal, Read, Write};
@@ -15,13 +17,13 @@ use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
15
17
 
16
18
  use cli_help::{Cli, Commands};
17
19
 
18
- /// Normalize `--feature` / `--feature http,fs` / `--feature full` for VM runs and native builds.
20
+ /// Normalize `--feature` / `--feature http,timers,fs` / `--feature full` for VM runs and native builds.
19
21
  fn normalize_capability_flags(features: &[String]) -> HashSet<String> {
20
22
  let mut out = HashSet::new();
21
23
  for s in features {
22
24
  for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
23
25
  if part == "full" {
24
- for name in ["http", "fs", "process", "regex", "ws"] {
26
+ for name in ["http", "timers", "fs", "process", "regex", "ws"] {
25
27
  out.insert(name.to_string());
26
28
  }
27
29
  } else {
@@ -110,6 +112,7 @@ fn main() {
110
112
  &a.native_backend,
111
113
  &a.features,
112
114
  a.no_optimize || no_opt_env,
115
+ a.source_map,
113
116
  ),
114
117
  Some(Commands::DumpAst { file }) => dump_ast(&file),
115
118
  None => {
@@ -160,7 +163,7 @@ fn run_stdin_source(
160
163
  let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
161
164
  let modules = tishlang_compile::resolve_project_from_stdin(source, &cwd)?;
162
165
  tishlang_compile::detect_cycles(&modules)?;
163
- let prog = tishlang_compile::merge_modules(modules)?;
166
+ let prog = tishlang_compile::merge_modules(modules)?.program;
164
167
  let program = if no_optimize {
165
168
  prog
166
169
  } else {
@@ -202,7 +205,7 @@ fn run_file(
202
205
  } else {
203
206
  let modules = tishlang_compile::resolve_project(&path, project_root)?;
204
207
  tishlang_compile::detect_cycles(&modules)?;
205
- let prog = tishlang_compile::merge_modules(modules)?;
208
+ let prog = tishlang_compile::merge_modules(modules)?.program;
206
209
  if no_optimize {
207
210
  prog
208
211
  } else {
@@ -223,6 +226,10 @@ fn run_program(
223
226
  if backend == "interp" {
224
227
  let mut eval = tishlang_eval::Evaluator::new();
225
228
  let value = eval.eval_program(program)?;
229
+ #[cfg(feature = "timers")]
230
+ {
231
+ let _ = eval.run_timer_phase();
232
+ }
226
233
  if !matches!(value, tishlang_eval::Value::Null) {
227
234
  println!(
228
235
  "{}",
@@ -241,13 +248,9 @@ fn run_program(
241
248
  tishlang_bytecode::compile(program).map_err(|e| e.to_string())?
242
249
  };
243
250
  let caps = vm_capabilities_for_cli_run(features);
244
- let value = tishlang_vm::run_with_options(
245
- &chunk,
246
- tishlang_vm::VmRunOptions {
247
- repl_mode: false,
248
- capabilities: caps,
249
- },
250
- )?;
251
+ let mut vm = tishlang_vm::Vm::with_capabilities(caps);
252
+ cargo_native_registry::register_bytecode_native_modules(&mut vm);
253
+ let value = vm.run_with_options(&chunk, false)?;
251
254
  if !matches!(value, tishlang_core::Value::Null) {
252
255
  println!(
253
256
  "{}",
@@ -294,6 +297,10 @@ fn run_repl(backend: &str, no_optimize: bool, features: &[String]) -> Result<(),
294
297
  Ok(program) => {
295
298
  match eval.eval_program(&program) {
296
299
  Ok(v) => {
300
+ #[cfg(feature = "timers")]
301
+ {
302
+ let _ = eval.run_timer_phase();
303
+ }
297
304
  if !matches!(v, tishlang_eval::Value::Null) {
298
305
  println!(
299
306
  "{}",
@@ -325,11 +332,11 @@ fn run_repl(backend: &str, no_optimize: bool, features: &[String]) -> Result<(),
325
332
  if !std::io::stdin().is_terminal() {
326
333
  eprintln!("Note: Tab completion and grey preview require an interactive terminal (TTY).");
327
334
  }
328
- let vm = Rc::new(RefCell::new(tishlang_vm::Vm::with_capabilities(
329
- vm_capabilities_for_cli_run(features),
330
- )));
335
+ let mut vm0 = tishlang_vm::Vm::with_capabilities(vm_capabilities_for_cli_run(features));
336
+ cargo_native_registry::register_bytecode_native_modules(&mut vm0);
337
+ let vm = VmRef::new(vm0);
331
338
  let completer = repl_completion::ReplCompleter {
332
- vm: Rc::clone(&vm),
339
+ vm: vm.clone(),
333
340
  no_optimize,
334
341
  };
335
342
  let config = Config::builder()
@@ -455,7 +462,27 @@ fn tish_history_path() -> Option<PathBuf> {
455
462
  home.map(|h| PathBuf::from(h).join(".tish_history"))
456
463
  }
457
464
 
458
- fn compile_to_js(input_path: &Path, output_path: &str, optimize: bool) -> Result<(), String> {
465
+ fn compile_to_js(
466
+ input_path: &Path,
467
+ output_path: &str,
468
+ optimize: bool,
469
+ source_map: bool,
470
+ ) -> Result<(), String> {
471
+ if source_map && optimize {
472
+ return Err(
473
+ "tish build --target js --source-map requires --no-optimize (mappings follow unmerged statement order)."
474
+ .into(),
475
+ );
476
+ }
477
+ if source_map
478
+ && (input_path.extension().map(|e| e == "jsx") == Some(true)
479
+ || input_path.extension().map(|e| e == "js") == Some(true))
480
+ {
481
+ return Err(
482
+ "tish build --target js --source-map is only supported for .tish project builds (not single-file .jsx / .js inputs)."
483
+ .into(),
484
+ );
485
+ }
459
486
  let project_root = input_path.parent().and_then(|p| {
460
487
  if p.file_name().and_then(|n| n.to_str()) == Some("src") {
461
488
  p.parent()
@@ -463,7 +490,20 @@ fn compile_to_js(input_path: &Path, output_path: &str, optimize: bool) -> Result
463
490
  Some(p)
464
491
  }
465
492
  });
466
- let js = if input_path.extension().map(|e| e == "jsx") == Some(true) {
493
+ let out_path = Path::new(output_path);
494
+ let out_path = if out_path.extension().is_none()
495
+ || out_path.extension() == Some(std::ffi::OsStr::new(""))
496
+ {
497
+ out_path.with_extension("js")
498
+ } else {
499
+ out_path.to_path_buf()
500
+ };
501
+ let out_js_name = out_path
502
+ .file_name()
503
+ .and_then(|s| s.to_str())
504
+ .unwrap_or("out.js");
505
+
506
+ let (js, map_json) = if input_path.extension().map(|e| e == "jsx") == Some(true) {
467
507
  let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
468
508
  let wrapped = format!(
469
509
  "export fn __TishJsxRoot() {{\n return (\n{}\n )\n}}",
@@ -476,30 +516,44 @@ fn compile_to_js(input_path: &Path, output_path: &str, optimize: bool) -> Result
476
516
  } else {
477
517
  program
478
518
  };
479
- tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?
519
+ let js = tishlang_compile_js::compile_with_jsx(&p, optimize).map_err(|e| format!("{}", e))?;
520
+ (js, None)
480
521
  } else if input_path.extension().map(|e| e == "js") == Some(true) {
481
522
  let source = fs::read_to_string(input_path).map_err(|e| format!("{}", e))?;
482
523
  let program = tishlang_js_to_tish::convert(&source).map_err(|e| format!("{}", e))?;
483
- tishlang_compile_js::compile_with_jsx(&program, optimize).map_err(|e| format!("{}", e))?
484
- } else {
485
- tishlang_compile_js::compile_project_with_jsx(input_path, project_root, optimize)
486
- .map_err(|e| format!("{}", e))?
487
- };
488
-
489
- let out_path = Path::new(output_path);
490
- let out_path = if out_path.extension().is_none()
491
- || out_path.extension() == Some(std::ffi::OsStr::new(""))
492
- {
493
- out_path.with_extension("js")
524
+ let js =
525
+ tishlang_compile_js::compile_with_jsx(&program, optimize).map_err(|e| format!("{}", e))?;
526
+ (js, None)
527
+ } else if source_map {
528
+ let bundle = tishlang_compile_js::compile_project_with_jsx_and_source_map(
529
+ input_path,
530
+ project_root,
531
+ out_js_name,
532
+ )
533
+ .map_err(|e| format!("{}", e))?;
534
+ (bundle.js, bundle.source_map_json)
494
535
  } else {
495
- out_path.to_path_buf()
536
+ let js = tishlang_compile_js::compile_project_with_jsx(input_path, project_root, optimize)
537
+ .map_err(|e| format!("{}", e))?;
538
+ (js, None)
496
539
  };
497
540
 
498
541
  if let Some(parent) = out_path.parent() {
499
542
  fs::create_dir_all(parent)
500
543
  .map_err(|e| format!("Cannot create output directory {}: {}", parent.display(), e))?;
501
544
  }
502
- fs::write(&out_path, js).map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
545
+ let mut js_out = js;
546
+ if let Some(map) = &map_json {
547
+ let map_path = out_path.with_extension("js.map");
548
+ fs::write(&map_path, map).map_err(|e| format!("Cannot write {}: {}", map_path.display(), e))?;
549
+ let map_url = map_path
550
+ .file_name()
551
+ .and_then(|s| s.to_str())
552
+ .unwrap_or("out.js.map");
553
+ js_out.push_str(&format!("\n//# sourceMappingURL={map_url}\n"));
554
+ println!("Built: {}", map_path.display());
555
+ }
556
+ fs::write(&out_path, js_out).map_err(|e| format!("Cannot write {}: {}", out_path.display(), e))?;
503
557
  println!("Built: {}", out_path.display());
504
558
  Ok(())
505
559
  }
@@ -512,6 +566,7 @@ fn build_file(
512
566
  native_backend: &str,
513
567
  cli_features: &[String],
514
568
  no_optimize: bool,
569
+ source_map: bool,
515
570
  ) -> Result<(), String> {
516
571
  let optimize = !no_optimize;
517
572
  let input_path = Path::new(input_path)
@@ -521,7 +576,7 @@ fn build_file(
521
576
  let is_js = input_path.extension().map(|e| e == "js") == Some(true);
522
577
 
523
578
  if target == "js" {
524
- return compile_to_js(&input_path, output_path, optimize);
579
+ return compile_to_js(&input_path, output_path, optimize, source_map);
525
580
  }
526
581
 
527
582
  if target == "wasm" && is_js {
@@ -2,8 +2,8 @@
2
2
  //! Grey preview hint below the line (like Node) and Tab for full list.
3
3
 
4
4
  use std::borrow::Cow;
5
- use std::cell::RefCell;
6
- use std::rc::Rc;
5
+
6
+ use tishlang_core::VmRef;
7
7
 
8
8
  use rustyline::completion::{Completer, Pair};
9
9
  use rustyline::highlight::Highlighter;
@@ -29,7 +29,7 @@ const ANSI_RESET: &str = "\x1b[0m";
29
29
 
30
30
  /// Tab completer that evaluates the expression before the last `.` and suggests property/method names.
31
31
  pub struct ReplCompleter {
32
- pub vm: Rc<RefCell<Vm>>,
32
+ pub vm: VmRef<Vm>,
33
33
  pub no_optimize: bool,
34
34
  }
35
35
 
@@ -39,7 +39,7 @@ fn resolve_and_merge_cargo_example_fixture() {
39
39
  panic!("expected import, got {:?}", first);
40
40
  };
41
41
  assert_eq!(from.as_ref(), "cargo:demo_shim");
42
- merge_modules(modules).unwrap();
42
+ let _ = merge_modules(modules).unwrap();
43
43
  }
44
44
 
45
45
  #[test]
@@ -66,12 +66,20 @@ fn file_content_hash(path: &Path) -> u64 {
66
66
  ///
67
67
  /// Cache is keyed by backend (native, cranelift, js, wasi) so e.g. cranelift and wasi
68
68
  /// compiles of the same file do not overwrite each other: .../cranelift/<stem>_<hash> vs .../wasi/<stem>_<hash>.wasm.
69
+ ///
70
+ /// The artifact **basename** must be unique per `(stem, hash, backend)`: nested `tish build`
71
+ /// uses it as the Cargo binary name under the workspace `target/release/`. If native and
72
+ /// cranelift both used `strict_equality_<hash>`, parallel `cargo nextest` could run those
73
+ /// tests concurrently and corrupt the same `target/release/...` output (Linux: ETXTBSY when
74
+ /// executing a binary still being written).
69
75
  fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
70
76
  let stem = path.file_stem().unwrap().to_string_lossy();
71
77
  let hash = file_content_hash(path);
72
78
  let hash8 = &format!("{:016x}", hash)[..8];
73
79
  let cache_base = integration_compile_cache_dir().join(backend);
74
80
  let _ = std::fs::create_dir_all(&cache_base);
81
+ // Include `backend` in the leaf name so nested cargo bin names never collide across backends.
82
+ let leaf = format!("{}__{}__{}", stem, backend, hash8);
75
83
 
76
84
  let (artifact_path, compile_args): (PathBuf, Vec<OsString>) = match backend {
77
85
  "native" => {
@@ -80,7 +88,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
80
88
  } else {
81
89
  ""
82
90
  };
83
- let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
91
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
84
92
  let args = vec![
85
93
  OsString::from("build"),
86
94
  OsString::from(path),
@@ -95,7 +103,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
95
103
  } else {
96
104
  ""
97
105
  };
98
- let cached = cache_base.join(format!("{}_{}{}", stem, hash8, ext));
106
+ let cached = cache_base.join(format!("{}{}", leaf, ext));
99
107
  let args = vec![
100
108
  OsString::from("build"),
101
109
  OsString::from(path),
@@ -107,7 +115,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
107
115
  (cached, args)
108
116
  }
109
117
  "js" => {
110
- let cached = cache_base.join(format!("{}_{}.js", stem, hash8));
118
+ let cached = cache_base.join(format!("{}.js", leaf));
111
119
  let args = vec![
112
120
  OsString::from("build"),
113
121
  OsString::from(path),
@@ -119,7 +127,7 @@ fn compile_cached(bin: &Path, path: &Path, backend: &str) -> PathBuf {
119
127
  (cached, args)
120
128
  }
121
129
  "wasi" => {
122
- let out_base = cache_base.join(format!("{}_{}", stem, hash8));
130
+ let out_base = cache_base.join(&leaf);
123
131
  let artifact = out_base.with_extension("wasm");
124
132
  let args = vec![
125
133
  OsString::from("build"),
@@ -397,7 +405,7 @@ fn test_async_await_run() {
397
405
  }
398
406
  }
399
407
 
400
- /// Run Promise and setTimeout module tests (require http feature).
408
+ /// Run Promise and setTimeout module tests (`promise` needs `http`; `settimeout` needs `timers`, which `http` enables).
401
409
  /// Ignored: tishlang_eval::run() does not run the event loop.
402
410
  #[test]
403
411
  #[cfg(feature = "http")]
@@ -455,7 +463,9 @@ fn test_vm_date_now() {
455
463
  // Library path
456
464
  let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
457
465
  tishlang_compile::detect_cycles(&modules).expect("cycles");
458
- let program = tishlang_compile::merge_modules(modules).expect("merge");
466
+ let program = tishlang_compile::merge_modules(modules)
467
+ .expect("merge")
468
+ .program;
459
469
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
460
470
  let result = tishlang_vm::run(&chunk);
461
471
  assert!(
@@ -499,7 +509,9 @@ fn test_vm_index_assign_via_resolve() {
499
509
  .join("array_sort_minimal.tish");
500
510
  let modules = tishlang_compile::resolve_project(&path, path.parent()).expect("resolve");
501
511
  tishlang_compile::detect_cycles(&modules).expect("cycles");
502
- let program = tishlang_compile::merge_modules(modules).expect("merge");
512
+ let program = tishlang_compile::merge_modules(modules)
513
+ .expect("merge")
514
+ .program;
503
515
  let chunk = tishlang_bytecode::compile(&program).expect("compile");
504
516
  let result = tishlang_vm::run(&chunk);
505
517
  assert!(
@@ -53,7 +53,7 @@ fn test_and_shortcircuit_via_resolve_project() {
53
53
  let path = path.canonicalize().expect("path");
54
54
  let project_root = path.parent().unwrap();
55
55
  let modules = resolve_project(&path, Some(project_root)).expect("resolve");
56
- let program = merge_modules(modules).expect("merge");
56
+ let program = merge_modules(modules).expect("merge").program;
57
57
  let program = tishlang_opt::optimize(&program); // Mirror CLI
58
58
  let chunk = compile(&program).expect("compile");
59
59
  let result = tishlang_vm::run(&chunk);
@@ -30,6 +30,7 @@ pub enum TypeAnnotation {
30
30
  #[derive(Debug, Clone, PartialEq)]
31
31
  pub struct TypedParam {
32
32
  pub name: Arc<str>,
33
+ pub name_span: Span,
33
34
  pub type_ann: Option<TypeAnnotation>,
34
35
  pub default: Option<Expr>,
35
36
  }
@@ -64,11 +65,11 @@ impl FunParam {
64
65
  for el in elements {
65
66
  if let Some(el) = el {
66
67
  match el {
67
- DestructElement::Ident(n) => out.push(Arc::clone(n)),
68
+ DestructElement::Ident(n, _) => out.push(Arc::clone(n)),
68
69
  DestructElement::Pattern(p) => {
69
70
  Self::collect_pattern_binding_names(p, out);
70
71
  }
71
- DestructElement::Rest(n) => out.push(Arc::clone(n)),
72
+ DestructElement::Rest(n, _) => out.push(Arc::clone(n)),
72
73
  }
73
74
  }
74
75
  }
@@ -76,11 +77,11 @@ impl FunParam {
76
77
  DestructPattern::Object(props) => {
77
78
  for prop in props {
78
79
  match &prop.value {
79
- DestructElement::Ident(n) => out.push(Arc::clone(n)),
80
+ DestructElement::Ident(n, _) => out.push(Arc::clone(n)),
80
81
  DestructElement::Pattern(p) => {
81
82
  Self::collect_pattern_binding_names(p, out);
82
83
  }
83
- DestructElement::Rest(n) => out.push(Arc::clone(n)),
84
+ DestructElement::Rest(n, _) => out.push(Arc::clone(n)),
84
85
  }
85
86
  }
86
87
  }
@@ -101,11 +102,11 @@ pub enum DestructPattern {
101
102
  #[derive(Debug, Clone, PartialEq)]
102
103
  pub enum DestructElement {
103
104
  /// Simple binding: a
104
- Ident(Arc<str>),
105
+ Ident(Arc<str>, Span),
105
106
  /// Nested pattern: [a, b] or { x, y }
106
107
  Pattern(Box<DestructPattern>),
107
108
  /// Rest element: ...rest
108
- Rest(Arc<str>),
109
+ Rest(Arc<str>, Span),
109
110
  }
110
111
 
111
112
  /// Property in object destructuring pattern
@@ -123,12 +124,20 @@ pub enum ImportSpecifier {
123
124
  /// Named: { foo } or { foo as bar }
124
125
  Named {
125
126
  name: Arc<str>,
127
+ name_span: Span,
126
128
  alias: Option<Arc<str>>,
129
+ alias_span: Option<Span>,
127
130
  },
128
131
  /// Namespace: * as M
129
- Namespace(Arc<str>),
132
+ Namespace {
133
+ name: Arc<str>,
134
+ name_span: Span,
135
+ },
130
136
  /// Default: import X from "..."
131
- Default(Arc<str>),
137
+ Default {
138
+ name: Arc<str>,
139
+ name_span: Span,
140
+ },
132
141
  }
133
142
 
134
143
  /// Export declaration: named (const/let/fn) or default
@@ -153,6 +162,7 @@ pub enum Statement {
153
162
  },
154
163
  VarDecl {
155
164
  name: Arc<str>,
165
+ name_span: Span,
156
166
  mutable: bool, // true for `let`, false for `const`
157
167
  type_ann: Option<TypeAnnotation>,
158
168
  init: Option<Expr>,
@@ -189,6 +199,7 @@ pub enum Statement {
189
199
  },
190
200
  ForOf {
191
201
  name: Arc<str>,
202
+ name_span: Span,
192
203
  iterable: Expr,
193
204
  body: Box<Statement>,
194
205
  span: Span,
@@ -206,6 +217,7 @@ pub enum Statement {
206
217
  FunDecl {
207
218
  async_: bool,
208
219
  name: Arc<str>,
220
+ name_span: Span,
209
221
  params: Vec<FunParam>,
210
222
  rest_param: Option<TypedParam>,
211
223
  return_type: Option<TypeAnnotation>,
@@ -230,6 +242,7 @@ pub enum Statement {
230
242
  Try {
231
243
  body: Box<Statement>,
232
244
  catch_param: Option<Arc<str>>,
245
+ catch_param_span: Option<Span>,
233
246
  catch_body: Option<Box<Statement>>,
234
247
  finally_body: Option<Box<Statement>>,
235
248
  span: Span,
@@ -243,6 +256,31 @@ pub enum Statement {
243
256
  declaration: Box<ExportDeclaration>,
244
257
  span: Span,
245
258
  },
259
+ /// `type Name = Type` (erased at runtime; for checker / declaration files).
260
+ TypeAlias {
261
+ name: Arc<str>,
262
+ name_span: Span,
263
+ ty: TypeAnnotation,
264
+ span: Span,
265
+ },
266
+ /// `declare let name: T` or `declare const name: T`
267
+ DeclareVar {
268
+ name: Arc<str>,
269
+ name_span: Span,
270
+ type_ann: Option<TypeAnnotation>,
271
+ const_: bool,
272
+ span: Span,
273
+ },
274
+ /// `declare [async] function name(...): R` (no body).
275
+ DeclareFun {
276
+ async_: bool,
277
+ name: Arc<str>,
278
+ name_span: Span,
279
+ params: Vec<FunParam>,
280
+ rest_param: Option<TypedParam>,
281
+ return_type: Option<TypeAnnotation>,
282
+ span: Span,
283
+ },
246
284
  }
247
285
 
248
286
  #[derive(Debug, Clone, PartialEq)]
@@ -550,6 +588,39 @@ pub enum UnaryOp {
550
588
 
551
589
  #[derive(Debug, Clone, PartialEq)]
552
590
  pub enum MemberProp {
553
- Name(Arc<str>),
591
+ /// Property name in `obj.prop` / `obj?.prop` (span covers **prop** only).
592
+ Name {
593
+ name: Arc<str>,
594
+ span: Span,
595
+ },
554
596
  Expr(Box<Expr>), // for computed property
555
597
  }
598
+
599
+ impl Statement {
600
+ /// Source span covering this statement (including nested bodies where applicable).
601
+ pub fn span(&self) -> Span {
602
+ match self {
603
+ Statement::Block { span, .. }
604
+ | Statement::VarDecl { span, .. }
605
+ | Statement::VarDeclDestructure { span, .. }
606
+ | Statement::ExprStmt { span, .. }
607
+ | Statement::If { span, .. }
608
+ | Statement::While { span, .. }
609
+ | Statement::For { span, .. }
610
+ | Statement::ForOf { span, .. }
611
+ | Statement::Return { span, .. }
612
+ | Statement::Break { span, .. }
613
+ | Statement::Continue { span, .. }
614
+ | Statement::FunDecl { span, .. }
615
+ | Statement::Switch { span, .. }
616
+ | Statement::DoWhile { span, .. }
617
+ | Statement::Throw { span, .. }
618
+ | Statement::Try { span, .. }
619
+ | Statement::Import { span, .. }
620
+ | Statement::Export { span, .. }
621
+ | Statement::TypeAlias { span, .. }
622
+ | Statement::DeclareVar { span, .. }
623
+ | Statement::DeclareFun { span, .. } => *span,
624
+ }
625
+ }
626
+ }
@@ -5,3 +5,7 @@ edition = "2021"
5
5
  description = "Shared build utilities for Tish (workspace discovery, path resolution)"
6
6
  license-file = { workspace = true }
7
7
  repository = { workspace = true }
8
+
9
+ [dependencies]
10
+ # Bundled `protoc` for nested `cargo build` (Lance / prost-build) when none is on PATH.
11
+ protoc-bin-vendored = "3"
@@ -137,6 +137,39 @@ fn tish_root_from_project_cargo_files(mut start: PathBuf) -> Option<PathBuf> {
137
137
  /// Returns the directory containing the workspace Cargo.toml (with [workspace]).
138
138
  /// Used when building native binaries, WASM, or locating runtime crates.
139
139
  pub fn find_workspace_root() -> Result<PathBuf, String> {
140
+ // Strategy 0: explicit checkout (e.g. tish-hub `npm run dev` / native build from a JS-only tree)
141
+ if let Ok(root) = std::env::var("TISHLANG_WORKSPACE") {
142
+ let trimmed = root.trim();
143
+ if !trimmed.is_empty() {
144
+ let p = PathBuf::from(trimmed);
145
+ if p.is_dir() && is_tish_workspace_root(&p) {
146
+ return p.canonicalize().map_err(|e| {
147
+ format!(
148
+ "TISHLANG_WORKSPACE is set but not a usable directory: {}",
149
+ e
150
+ )
151
+ });
152
+ }
153
+ // Non-empty but invalid: fall through (sibling `tish/` discovery may still apply).
154
+ }
155
+ }
156
+
157
+ // Strategy 0b: monorepo layout `…/<parent>/tish-hub` (cwd) next to `…/<parent>/tish` (language repo).
158
+ // Works without `TISHLANG_WORKSPACE` so older `tish` binaries still find the compiler tree.
159
+ if let Ok(mut dir) = std::env::current_dir() {
160
+ for _ in 0..24 {
161
+ let candidate = dir.join("tish");
162
+ if is_tish_workspace_root(&candidate) {
163
+ return candidate.canonicalize().map_err(|e| {
164
+ format!("Cannot canonicalize Tish workspace {}: {}", candidate.display(), e)
165
+ });
166
+ }
167
+ if !dir.pop() {
168
+ break;
169
+ }
170
+ }
171
+ }
172
+
140
173
  // Strategy 1: CARGO_MANIFEST_DIR (works during cargo build/run from workspace)
141
174
  if let Ok(manifest_dir) = std::env::var("CARGO_MANIFEST_DIR") {
142
175
  let path = PathBuf::from(&manifest_dir);
@@ -301,18 +334,70 @@ pub fn create_build_dir(prefix: &str, out_name: &str) -> Result<PathBuf, String>
301
334
  Ok(build_dir)
302
335
  }
303
336
 
337
+ /// `PROTOC` for prost-build in transitive crates (e.g. lance-encoding) during nested `cargo build`.
338
+ /// Respects an existing `PROTOC`, then `protoc` on `PATH`, then `protoc-bin-vendored`.
339
+ fn protoc_for_nested_cargo() -> Option<PathBuf> {
340
+ // Empty or non-file PROTOC must not short-circuit: prost-build treats "" like a missing binary.
341
+ if let Some(v) = std::env::var_os("PROTOC") {
342
+ if !v.is_empty() {
343
+ let p = PathBuf::from(&v);
344
+ if p.is_file() {
345
+ return None;
346
+ }
347
+ }
348
+ }
349
+ // Prefer vendored protoc over PATH: a broken or non-executable `protoc` on PATH still passes `is_file()`.
350
+ if let Ok(p) = protoc_bin_vendored::protoc_bin_path() {
351
+ return Some(p);
352
+ }
353
+ let ext = if cfg!(windows) { ".exe" } else { "" };
354
+ let name = format!("protoc{}", ext);
355
+ if let Some(paths) = std::env::var_os("PATH") {
356
+ for dir in std::env::split_paths(&paths) {
357
+ let candidate = dir.join(&name);
358
+ if candidate.is_file() {
359
+ return Some(candidate);
360
+ }
361
+ }
362
+ }
363
+ None
364
+ }
365
+
304
366
  /// Run cargo build in the given directory.
305
367
  /// If target_dir is Some, use that for --target-dir (e.g. workspace target for caching).
306
368
  pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<(), String> {
307
369
  let target_dir = target_dir
308
370
  .map(|p| p.to_path_buf())
309
371
  .unwrap_or_else(|| build_dir.join("target"));
310
- let output = Command::new("cargo")
311
- .args(["build", "--release", "--target-dir"])
372
+ // Default to target-cpu=native so the emitted binary uses every SIMD / ISA
373
+ // extension the build host supports. Callers can override by pre-setting
374
+ // RUSTFLAGS in the environment.
375
+ let existing_rustflags = std::env::var("RUSTFLAGS").unwrap_or_default();
376
+ let merged_rustflags = if existing_rustflags.is_empty() {
377
+ "-C target-cpu=native".to_string()
378
+ } else if existing_rustflags.contains("target-cpu") {
379
+ existing_rustflags
380
+ } else {
381
+ format!("{} -C target-cpu=native", existing_rustflags)
382
+ };
383
+ // Nested `cargo build` (e.g. `tish build --native-backend rust`) inherits the parent
384
+ // environment. CI often sets `RUSTC_WRAPPER=sccache`; wrapping this inner compile too can
385
+ // cause flaky or failed builds (LTO / temp-crate paths). Use plain rustc here; the main
386
+ // workspace build still benefits from the wrapper.
387
+ let mut cmd = Command::new("cargo");
388
+ cmd.args(["build", "--release", "--target-dir"])
312
389
  .arg(&target_dir)
313
390
  .current_dir(build_dir)
314
391
  .env_remove("CARGO_TARGET_DIR")
392
+ .env_remove("RUSTC_WRAPPER")
393
+ .env_remove("RUSTC_WORKSPACE_WRAPPER")
394
+ .env_remove("CARGO_BUILD_RUSTC_WRAPPER")
315
395
  .env("CARGO_TERM_PROGRESS", "always")
396
+ .env("RUSTFLAGS", &merged_rustflags);
397
+ if let Some(protoc) = protoc_for_nested_cargo() {
398
+ cmd.env("PROTOC", protoc);
399
+ }
400
+ let output = cmd
316
401
  .output()
317
402
  .map_err(|e| format!("Failed to run cargo: {}", e))?;
318
403
 
@@ -327,6 +412,24 @@ pub fn run_cargo_build(build_dir: &Path, target_dir: Option<&Path>) -> Result<()
327
412
  Ok(())
328
413
  }
329
414
 
415
+ #[cfg(test)]
416
+ mod protoc_tests {
417
+ use super::*;
418
+
419
+ #[test]
420
+ fn protoc_for_nested_cargo_without_env_uses_vendored_or_path() {
421
+ let _lock = std::sync::Mutex::new(());
422
+ let _guard = _lock.lock().unwrap();
423
+ std::env::remove_var("PROTOC");
424
+ let p = protoc_for_nested_cargo().expect("expected vendored or PATH protoc");
425
+ assert!(
426
+ p.exists(),
427
+ "resolved protoc should exist: {}",
428
+ p.display()
429
+ );
430
+ }
431
+ }
432
+
330
433
  /// Find the built binary in target/release.
331
434
  pub fn find_release_binary(binary_dir: &Path, bin_name: &str) -> Result<PathBuf, String> {
332
435
  let binary_no_ext = binary_dir.join(bin_name);