@tishlang/tish 1.4.2 → 1.6.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 (39) hide show
  1. package/bin/tish +0 -0
  2. package/crates/tish/Cargo.toml +2 -2
  3. package/crates/tish/src/cli_help.rs +504 -0
  4. package/crates/tish/src/main.rs +76 -90
  5. package/crates/tish/src/repl_completion.rs +1 -1
  6. package/crates/tish/tests/cargo_example_compile.rs +65 -0
  7. package/crates/tish/tests/fixtures/cargo_example_project/Cargo.toml +3 -0
  8. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/Cargo.toml +11 -0
  9. package/crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim/src/lib.rs +12 -0
  10. package/crates/tish/tests/fixtures/cargo_example_project/package.json +10 -0
  11. package/crates/tish/tests/fixtures/cargo_example_project/src/main.tish +3 -0
  12. package/crates/tish/tests/integration_test.rs +48 -0
  13. package/crates/tish_build_utils/src/lib.rs +204 -1
  14. package/crates/tish_builtins/src/string.rs +248 -0
  15. package/crates/tish_bytecode/Cargo.toml +1 -0
  16. package/crates/tish_bytecode/src/compiler.rs +289 -66
  17. package/crates/tish_bytecode/src/opcode.rs +13 -3
  18. package/crates/tish_bytecode/src/peephole.rs +21 -16
  19. package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
  20. package/crates/tish_compile/Cargo.toml +1 -0
  21. package/crates/tish_compile/src/codegen.rs +277 -93
  22. package/crates/tish_compile/src/lib.rs +7 -4
  23. package/crates/tish_compile/src/resolve.rs +418 -40
  24. package/crates/tish_compiler_wasm/src/resolve_virtual.rs +1 -0
  25. package/crates/tish_core/src/value.rs +1 -0
  26. package/crates/tish_eval/src/eval.rs +49 -1
  27. package/crates/tish_eval/src/lib.rs +1 -1
  28. package/crates/tish_native/src/build.rs +86 -17
  29. package/crates/tish_native/src/lib.rs +36 -16
  30. package/crates/tish_runtime/src/lib.rs +4 -0
  31. package/crates/tish_vm/src/lib.rs +1 -1
  32. package/crates/tish_vm/src/vm.rs +165 -19
  33. package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
  34. package/package.json +1 -1
  35. package/platform/darwin-arm64/tish +0 -0
  36. package/platform/darwin-x64/tish +0 -0
  37. package/platform/linux-arm64/tish +0 -0
  38. package/platform/linux-x64/tish +0 -0
  39. package/platform/win32-x64/tish.exe +0 -0
@@ -1,80 +1,59 @@
1
1
  //! Tish CLI - run, REPL, build to native or other targets.
2
2
 
3
+ mod cli_help;
3
4
  mod repl_completion;
4
5
 
5
6
  use std::cell::RefCell;
7
+ use std::collections::HashSet;
6
8
  use std::fs;
7
9
  use std::io::{self, IsTerminal, Read, Write};
8
10
  use std::path::{Path, PathBuf};
9
11
  use std::rc::Rc;
10
12
 
11
- use clap::{Parser, Subcommand};
13
+ use clap::FromArgMatches;
12
14
  use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
13
15
 
14
- #[derive(Parser)]
15
- #[command(name = "tish")]
16
- #[command(about = "Tish - minimal TS/JS-compatible language")]
17
- #[command(version = env!("CARGO_PKG_VERSION"))]
18
- #[command(after_help = "To disable optimizations: TISH_NO_OPTIMIZE=1")]
19
- pub(crate) struct Cli {
20
- #[command(subcommand)]
21
- command: Option<Commands>,
22
- }
23
-
24
- #[derive(Parser)]
25
- struct RunArgs {
26
- /// Path to a `.tish` file, or `-` to read the program from stdin (like `node -`).
27
- #[arg(required = true, allow_hyphen_values = true)]
28
- file: String,
29
- #[arg(long, default_value = "vm")]
30
- backend: String,
31
- /// Enable capabilities (http, fs, process, regex, ws). Must match how tish was built.
32
- /// E.g. cargo run -p tishlang--features http,fs -- run script.tish --feature http,fs
33
- #[arg(long = "feature", action = clap::ArgAction::Append)]
34
- features: Vec<String>,
35
- /// Disable AST and bytecode optimizations (for debugging)
36
- #[arg(long)]
37
- no_optimize: bool,
38
- }
16
+ use cli_help::{Cli, Commands};
39
17
 
40
- #[derive(Parser)]
41
- struct ReplArgs {
42
- #[arg(long, default_value = "vm")]
43
- backend: String,
44
- #[arg(long)]
45
- no_optimize: bool,
18
+ /// Normalize `--feature` / `--feature http,fs` / `--feature full` for VM runs and native builds.
19
+ fn normalize_capability_flags(features: &[String]) -> HashSet<String> {
20
+ let mut out = HashSet::new();
21
+ for s in features {
22
+ for part in s.split(',').map(str::trim).filter(|p| !p.is_empty()) {
23
+ if part == "full" {
24
+ for name in ["http", "fs", "process", "regex", "ws"] {
25
+ out.insert(name.to_string());
26
+ }
27
+ } else {
28
+ out.insert(part.to_string());
29
+ }
30
+ }
31
+ }
32
+ out
46
33
  }
47
34
 
48
- #[derive(Parser)]
49
- struct BuildArgs {
50
- #[arg(short, long, default_value = "tish_out")]
51
- output: String,
52
- #[arg(long, default_value = "native")]
53
- target: String,
54
- #[arg(long, default_value = "rust")]
55
- native_backend: String,
56
- #[arg(long = "feature", action = clap::ArgAction::Append)]
57
- features: Vec<String>,
58
- #[arg(long)]
59
- no_optimize: bool,
60
- #[arg(required = true)]
61
- file: String,
35
+ /// VM capabilities for `run` / `repl` / stdin with the bytecode VM.
36
+ ///
37
+ /// If the user passes no `--feature`, enable **everything linked into this `tish` binary**
38
+ /// (so `cargo run --bin tish --features full -- script.tish` does not need `--feature full`).
39
+ /// If they pass `--feature …`, use **only** that set (e.g. restrict a full build to `http` only).
40
+ fn vm_capabilities_for_cli_run(cli_features: &[String]) -> HashSet<String> {
41
+ if cli_features.is_empty() {
42
+ tishlang_vm::all_compiled_capabilities()
43
+ } else {
44
+ normalize_capability_flags(cli_features)
45
+ }
62
46
  }
63
47
 
64
- #[derive(Subcommand)]
65
- pub(crate) enum Commands {
66
- /// Run a Tish file (interpret)
67
- Run(RunArgs),
68
- /// Interactive REPL
69
- Repl(ReplArgs),
70
- /// Build native binary, wasm, wasi, or JavaScript output
71
- Build(BuildArgs),
72
- /// Parse and dump AST
73
- #[command(name = "dump-ast")]
74
- DumpAst {
75
- #[arg(required = true)]
76
- file: String,
77
- },
48
+ /// `--feature` list for `tish build --target native`: same default as `tish run` (all linked-in caps).
49
+ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
50
+ if cli_features.is_empty() {
51
+ let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities().into_iter().collect();
52
+ v.sort();
53
+ v
54
+ } else {
55
+ cli_features.to_vec()
56
+ }
78
57
  }
79
58
 
80
59
  /// `tish script.tish` → insert `run` so it matches `tish run script.tish` (npx / npm UX).
@@ -107,11 +86,17 @@ fn main() {
107
86
  return;
108
87
  }
109
88
 
89
+ if cli_help::argv_requests_help(&argv) {
90
+ cli_help::print_banner_with_help(&argv);
91
+ std::process::exit(0);
92
+ }
93
+
110
94
  let argv = argv_with_implicit_run(argv);
111
- let cli = Cli::parse_from(argv);
95
+ let matches = cli_help::build_command().get_matches_from(&argv);
96
+ let cli = Cli::from_arg_matches(&matches).unwrap_or_else(|e| e.exit());
112
97
  let result = match cli.command {
113
98
  Some(Commands::Run(a)) => run_file(&a.file, &a.backend, &a.features, a.no_optimize || no_opt_env),
114
- Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env),
99
+ Some(Commands::Repl(a)) => run_repl(&a.backend, a.no_optimize || no_opt_env, &a.features),
115
100
  Some(Commands::Build(a)) => build_file(
116
101
  &a.file,
117
102
  &a.output,
@@ -123,7 +108,7 @@ fn main() {
123
108
  Some(Commands::DumpAst { file }) => dump_ast(&file),
124
109
  None => {
125
110
  if io::stdin().is_terminal() {
126
- run_repl("vm", no_opt_env)
111
+ run_repl("vm", no_opt_env, &[])
127
112
  } else {
128
113
  // `echo '...' | tish` — run script from stdin (Bun-style)
129
114
  run_stdin_pipe("vm", &[], no_opt_env, false)
@@ -162,7 +147,7 @@ fn run_stdin_pipe(
162
147
  fn run_stdin_source(
163
148
  source: &str,
164
149
  backend: &str,
165
- _features: &[String],
150
+ features: &[String],
166
151
  no_optimize: bool,
167
152
  ) -> Result<(), String> {
168
153
  let cwd = std::env::current_dir().map_err(|e| e.to_string())?;
@@ -174,7 +159,7 @@ fn run_stdin_source(
174
159
  } else {
175
160
  tishlang_opt::optimize(&prog)
176
161
  };
177
- run_program(&program, backend, no_optimize)
162
+ run_program(&program, backend, no_optimize, features)
178
163
  }
179
164
 
180
165
  fn run_file(path: &str, backend: &str, features: &[String], no_optimize: bool) -> Result<(), String> {
@@ -211,10 +196,15 @@ fn run_file(path: &str, backend: &str, features: &[String], no_optimize: bool) -
211
196
  }
212
197
  };
213
198
 
214
- run_program(&program, backend, no_optimize)
199
+ run_program(&program, backend, no_optimize, features)
215
200
  }
216
201
 
217
- fn run_program(program: &tishlang_ast::Program, backend: &str, no_optimize: bool) -> Result<(), String> {
202
+ fn run_program(
203
+ program: &tishlang_ast::Program,
204
+ backend: &str,
205
+ no_optimize: bool,
206
+ features: &[String],
207
+ ) -> Result<(), String> {
218
208
  if backend == "interp" {
219
209
  let mut eval = tishlang_eval::Evaluator::new();
220
210
  let value = eval.eval_program(program)?;
@@ -229,14 +219,22 @@ fn run_program(program: &tishlang_ast::Program, backend: &str, no_optimize: bool
229
219
  } else {
230
220
  tishlang_bytecode::compile(program).map_err(|e| e.to_string())?
231
221
  };
232
- let value = tishlang_vm::run(&chunk)?;
222
+ let caps = vm_capabilities_for_cli_run(features);
223
+ let value = tishlang_vm::run_with_options(
224
+ &chunk,
225
+ tishlang_vm::VmRunOptions {
226
+ repl_mode: false,
227
+ capabilities: caps,
228
+ },
229
+ )?;
233
230
  if !matches!(value, tishlang_core::Value::Null) {
234
231
  println!("{}", tishlang_core::format_value_styled(&value, tishlang_core::use_console_colors()));
235
232
  }
236
233
  Ok(())
237
234
  }
238
235
 
239
- fn run_repl(backend: &str, no_optimize: bool) -> Result<(), String> {
236
+ fn run_repl(backend: &str, no_optimize: bool, features: &[String]) -> Result<(), String> {
237
+ cli_help::print_tish_banner();
240
238
  println!("Tish REPL (Ctrl-D to exit)");
241
239
  let mut buffer = String::new();
242
240
 
@@ -293,7 +291,9 @@ fn run_repl(backend: &str, no_optimize: bool) -> Result<(), String> {
293
291
  if !std::io::stdin().is_terminal() {
294
292
  eprintln!("Note: Tab completion and grey preview require an interactive terminal (TTY).");
295
293
  }
296
- let vm = Rc::new(RefCell::new(tishlang_vm::Vm::new()));
294
+ let vm = Rc::new(RefCell::new(tishlang_vm::Vm::with_capabilities(
295
+ vm_capabilities_for_cli_run(features),
296
+ )));
297
297
  let completer = repl_completion::ReplCompleter {
298
298
  vm: Rc::clone(&vm),
299
299
  no_optimize,
@@ -333,7 +333,7 @@ fn run_repl(backend: &str, no_optimize: bool) -> Result<(), String> {
333
333
  tishlang_bytecode::compile_for_repl
334
334
  };
335
335
  if let Ok(chunk) = compile_fn(&program) {
336
- let _ = vm.borrow_mut().run(&chunk);
336
+ let _ = vm.borrow_mut().run_with_options(&chunk, true);
337
337
  }
338
338
  }
339
339
  Err(e) => eprintln!("Parse error: {}", e),
@@ -365,7 +365,7 @@ fn run_repl(backend: &str, no_optimize: bool) -> Result<(), String> {
365
365
  };
366
366
  match compile_fn(&program) {
367
367
  Ok(chunk) => {
368
- match vm.borrow_mut().run(&chunk) {
368
+ match vm.borrow_mut().run_with_options(&chunk, true) {
369
369
  Ok(v) => {
370
370
  if !matches!(v, tishlang_core::Value::Null) {
371
371
  println!("{}", tishlang_core::format_value_styled(&v, tishlang_core::use_console_colors()));
@@ -531,23 +531,7 @@ fn build_file(
531
531
  p
532
532
  }
533
533
  });
534
- let features: Vec<String> = if cli_features.is_empty() {
535
- #[allow(unused_mut)]
536
- let mut f = Vec::new();
537
- #[cfg(feature = "http")]
538
- f.push("http".to_string());
539
- #[cfg(feature = "fs")]
540
- f.push("fs".to_string());
541
- #[cfg(feature = "process")]
542
- f.push("process".to_string());
543
- #[cfg(feature = "regex")]
544
- f.push("regex".to_string());
545
- #[cfg(feature = "ws")]
546
- f.push("ws".to_string());
547
- f
548
- } else {
549
- cli_features.to_vec()
550
- };
534
+ let features: Vec<String> = native_build_features_from_cli(cli_features);
551
535
 
552
536
  if is_js {
553
537
  let source = fs::read_to_string(&input_path).map_err(|e| format!("{}", e))?;
@@ -592,7 +576,9 @@ fn build_file(
592
576
  mod cli_tests {
593
577
  use clap::Parser;
594
578
 
595
- use super::{argv_with_implicit_run, Cli, Commands};
579
+ use crate::cli_help::{Cli, Commands};
580
+
581
+ use super::argv_with_implicit_run;
596
582
 
597
583
  #[test]
598
584
  fn implicit_run_inserts_run_before_file() {
@@ -86,7 +86,7 @@ impl ReplCompleter {
86
86
  compile_for_repl
87
87
  };
88
88
  let chunk = compile_fn(&program).ok()?;
89
- let value = self.vm.borrow_mut().run(&chunk).ok()?;
89
+ let value = self.vm.borrow_mut().run_with_options(&chunk, true).ok()?;
90
90
 
91
91
  let keys = value.completion_keys();
92
92
  let filtered: Vec<String> = keys
@@ -0,0 +1,65 @@
1
+ //! `cargo:` + `tish.rustDependencies` using the in-repo fixture at `tests/fixtures/cargo_example_project/`
2
+ //! (same layout as the standalone `tish-cargo-example` template).
3
+
4
+ use std::path::PathBuf;
5
+
6
+ use tishlang_ast::Statement;
7
+ use tishlang_compile::{compile_project_full, merge_modules, resolve_project};
8
+
9
+ fn native_build_features_from_cli(cli_features: &[String]) -> Vec<String> {
10
+ if cli_features.is_empty() {
11
+ let mut v: Vec<String> = tishlang_vm::all_compiled_capabilities().into_iter().collect();
12
+ v.sort();
13
+ v
14
+ } else {
15
+ cli_features.to_vec()
16
+ }
17
+ }
18
+
19
+ #[test]
20
+ fn resolve_and_merge_cargo_example_fixture() {
21
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
22
+ let input_path = manifest_dir
23
+ .join("tests/fixtures/cargo_example_project/src/main.tish")
24
+ .canonicalize()
25
+ .expect("cargo_example_project test fixture");
26
+ let project_root = input_path.parent().map(|p| {
27
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
28
+ p.parent().unwrap_or(p)
29
+ } else {
30
+ p
31
+ }
32
+ });
33
+ let modules = resolve_project(&input_path, project_root).unwrap();
34
+ assert_eq!(modules.len(), 1, "expected single entry module");
35
+ let first = &modules[0].program.statements[0];
36
+ let Statement::Import { from, .. } = first else {
37
+ panic!("expected import, got {:?}", first);
38
+ };
39
+ assert_eq!(from.as_ref(), "cargo:demo_shim");
40
+ merge_modules(modules).unwrap();
41
+ }
42
+
43
+ #[test]
44
+ fn compile_project_full_cargo_example_fixture() {
45
+ let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
46
+ let example_main = manifest_dir
47
+ .join("tests/fixtures/cargo_example_project/src/main.tish")
48
+ .canonicalize()
49
+ .expect("cargo_example_project test fixture");
50
+ let input_path = example_main;
51
+ let project_root = input_path.parent().map(|p| {
52
+ if p.file_name().and_then(|n| n.to_str()) == Some("src") {
53
+ p.parent().unwrap_or(p)
54
+ } else {
55
+ p
56
+ }
57
+ });
58
+ let features = native_build_features_from_cli(&[]);
59
+ let r = compile_project_full(&input_path, project_root, &features, true);
60
+ assert!(
61
+ r.is_ok(),
62
+ "compile_project_full failed: {:?}",
63
+ r.map_err(|e| e.message)
64
+ );
65
+ }
@@ -0,0 +1,3 @@
1
+ [workspace]
2
+ resolver = "2"
3
+ members = ["crates/demo-shim"]
@@ -0,0 +1,11 @@
1
+ [package]
2
+ name = "demo_shim"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [lib]
7
+ crate-type = ["rlib"]
8
+
9
+ [dependencies]
10
+ # Fixture lives at crates/tish/tests/fixtures/cargo_example_project/crates/demo-shim → repo crates/tish_core
11
+ tishlang_core = { path = "../../../../../../../crates/tish_core" }
@@ -0,0 +1,12 @@
1
+ //! Rust exports for `import { … } from 'cargo:demo_shim'` (test fixture).
2
+
3
+ use std::sync::Arc;
4
+ use tishlang_core::Value;
5
+
6
+ pub fn greet(args: &[Value]) -> Value {
7
+ let name = match args.first() {
8
+ Some(Value::String(s)) => s.as_ref(),
9
+ _ => "world",
10
+ };
11
+ Value::String(Arc::from(format!("Hello, {}!", name)))
12
+ }
@@ -0,0 +1,10 @@
1
+ {
2
+ "name": "tish-cargo-example-fixture",
3
+ "version": "0.0.0",
4
+ "private": true,
5
+ "tish": {
6
+ "rustDependencies": {
7
+ "demo_shim": { "path": "./crates/demo-shim" }
8
+ }
9
+ }
10
+ }
@@ -0,0 +1,3 @@
1
+ import { greet } from 'cargo:demo_shim'
2
+
3
+ console.log(greet('from cargo'))
@@ -521,6 +521,54 @@ fn test_mvp_programs_interpreter() {
521
521
  }
522
522
  }
523
523
 
524
+ /// Default bytecode VM must match the tree-walking interpreter for every MVP program.
525
+ #[test]
526
+ fn test_mvp_programs_interp_vm_stdout_parity() {
527
+ let core_dir = core_dir();
528
+ let bin = tish_bin();
529
+ assert!(
530
+ bin.exists(),
531
+ "tish binary not found at {}. Run `cargo build -p tishlang` first.",
532
+ bin.display()
533
+ );
534
+ for name in MVP_TEST_FILES {
535
+ let path = core_dir.join(name);
536
+ if !path.exists() {
537
+ continue;
538
+ }
539
+ let path_str = path.to_string_lossy();
540
+ let out_interp = Command::new(&bin)
541
+ .args(["run", path_str.as_ref(), "--backend", "interp"])
542
+ .current_dir(workspace_root())
543
+ .output()
544
+ .expect("run tish interpreter");
545
+ assert!(
546
+ out_interp.status.success(),
547
+ "Interpreter failed for {}: {}",
548
+ path.display(),
549
+ String::from_utf8_lossy(&out_interp.stderr)
550
+ );
551
+ let out_vm = Command::new(&bin)
552
+ .args(["run", path_str.as_ref()])
553
+ .current_dir(workspace_root())
554
+ .output()
555
+ .expect("run tish VM");
556
+ assert!(
557
+ out_vm.status.success(),
558
+ "VM failed for {}: {}",
559
+ path.display(),
560
+ String::from_utf8_lossy(&out_vm.stderr)
561
+ );
562
+ let s_interp = String::from_utf8_lossy(&out_interp.stdout);
563
+ let s_vm = String::from_utf8_lossy(&out_vm.stdout);
564
+ assert_eq!(
565
+ s_interp, s_vm,
566
+ "interp vs VM stdout mismatch for {}",
567
+ path.display()
568
+ );
569
+ }
570
+ }
571
+
524
572
  /// Compile each .tish file to native, run, and compare stdout to static expected (parallelized).
525
573
  #[test]
526
574
  fn test_mvp_programs_native() {