@tishlang/tish 1.3.8 → 1.5.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.
- package/bin/tish +0 -0
- package/crates/tish/Cargo.toml +2 -2
- package/crates/tish/src/cli_help.rs +504 -0
- package/crates/tish/src/main.rs +76 -90
- package/crates/tish/src/repl_completion.rs +1 -1
- package/crates/tish/tests/integration_test.rs +48 -0
- package/crates/tish_build_utils/src/lib.rs +171 -1
- package/crates/tish_builtins/src/string.rs +248 -0
- package/crates/tish_bytecode/Cargo.toml +1 -0
- package/crates/tish_bytecode/src/compiler.rs +289 -66
- package/crates/tish_bytecode/src/opcode.rs +13 -3
- package/crates/tish_bytecode/src/peephole.rs +21 -16
- package/crates/tish_bytecode/tests/break_continue_bytecode.rs +44 -0
- package/crates/tish_compile/src/codegen.rs +214 -79
- package/crates/tish_compile/src/lib.rs +1 -1
- package/crates/tish_core/src/value.rs +1 -0
- package/crates/tish_eval/src/eval.rs +39 -1
- package/crates/tish_eval/src/lib.rs +1 -1
- package/crates/tish_lint/Cargo.toml +1 -0
- package/crates/tish_lint/src/bin/tish-lint.rs +141 -23
- package/crates/tish_lint/src/lib.rs +3 -1
- package/crates/tish_lsp/README.md +1 -1
- package/crates/tish_native/src/build.rs +48 -7
- package/crates/tish_native/src/lib.rs +8 -3
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_vm/src/lib.rs +1 -1
- package/crates/tish_vm/src/vm.rs +155 -16
- package/crates/tish_vm/tests/lexical_scope_declare.rs +34 -0
- package/package.json +1 -8
- package/platform/darwin-arm64/tish +0 -0
- package/platform/darwin-x64/tish +0 -0
- package/platform/linux-arm64/tish +0 -0
- package/platform/linux-x64/tish +0 -0
- package/platform/win32-x64/tish.exe +0 -0
package/crates/tish/src/main.rs
CHANGED
|
@@ -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::
|
|
13
|
+
use clap::FromArgMatches;
|
|
12
14
|
use rustyline::{Behavior, ColorMode, CompletionType, Config, Editor};
|
|
13
15
|
|
|
14
|
-
|
|
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|
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
|
-
|
|
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(
|
|
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
|
|
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::
|
|
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().
|
|
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().
|
|
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> =
|
|
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
|
|
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().
|
|
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
|
|
@@ -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() {
|
|
@@ -15,6 +15,123 @@ fn is_tish_workspace_root(root: &Path) -> bool {
|
|
|
15
15
|
root.join("crates").join("tish_runtime").is_dir()
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
/// True if `line` (trimmed) opens a Cargo.toml table whose body may contain path dependencies.
|
|
19
|
+
fn cargo_section_may_contain_path_deps(header: &str) -> bool {
|
|
20
|
+
let h = header.trim();
|
|
21
|
+
if h == "dependencies"
|
|
22
|
+
|| h == "dev-dependencies"
|
|
23
|
+
|| h == "build-dependencies"
|
|
24
|
+
|| h == "workspace.dependencies"
|
|
25
|
+
{
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
h.starts_with("dependencies.")
|
|
29
|
+
|| h.starts_with("dev-dependencies.")
|
|
30
|
+
|| h.starts_with("build-dependencies.")
|
|
31
|
+
|| h.starts_with("workspace.dependencies.")
|
|
32
|
+
|| h.starts_with("patch.")
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/// Collect `path = "..."` / `path = '...'` strings from lines in dependency-related sections.
|
|
36
|
+
fn path_values_from_cargo_toml(content: &str) -> Vec<String> {
|
|
37
|
+
let mut out = Vec::new();
|
|
38
|
+
let mut in_section = false;
|
|
39
|
+
for line in content.lines() {
|
|
40
|
+
let trimmed = line.trim();
|
|
41
|
+
if let Some(rest) = trimmed.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
|
|
42
|
+
in_section = cargo_section_may_contain_path_deps(rest);
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if !in_section {
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
extract_path_assignments_from_line(trimmed, &mut out);
|
|
49
|
+
}
|
|
50
|
+
out
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
fn extract_path_assignments_from_line(line: &str, out: &mut Vec<String>) {
|
|
54
|
+
let mut rest = line;
|
|
55
|
+
while let Some(idx) = rest.find("path") {
|
|
56
|
+
let after = rest[idx + 4..].trim_start();
|
|
57
|
+
let after = match after.strip_prefix('=') {
|
|
58
|
+
Some(a) => a.trim_start(),
|
|
59
|
+
None => {
|
|
60
|
+
rest = &rest[idx + 4..];
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
let quote = match after.chars().next() {
|
|
65
|
+
Some('"') => '"',
|
|
66
|
+
Some('\'') => '\'',
|
|
67
|
+
_ => {
|
|
68
|
+
rest = &rest[idx + 4..];
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
let after = &after[quote.len_utf8()..];
|
|
73
|
+
let end = after.find(quote);
|
|
74
|
+
let Some(end) = end else {
|
|
75
|
+
rest = &rest[idx + 4..];
|
|
76
|
+
continue;
|
|
77
|
+
};
|
|
78
|
+
out.push(after[..end].to_string());
|
|
79
|
+
rest = &after[end + quote.len_utf8()..];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/// Starting from a filesystem path (crate dir or file), walk ancestors for `crates/tish_runtime`.
|
|
84
|
+
fn tish_root_from_path_hint(start: &Path) -> Option<PathBuf> {
|
|
85
|
+
let mut dir = if start.is_file() {
|
|
86
|
+
start.parent()?.to_path_buf()
|
|
87
|
+
} else {
|
|
88
|
+
start.to_path_buf()
|
|
89
|
+
};
|
|
90
|
+
dir = fs::canonicalize(&dir).unwrap_or(dir);
|
|
91
|
+
let mut cur = dir.as_path();
|
|
92
|
+
for _ in 0..32 {
|
|
93
|
+
if is_tish_workspace_root(cur) {
|
|
94
|
+
return Some(cur.to_path_buf());
|
|
95
|
+
}
|
|
96
|
+
cur = cur.parent()?;
|
|
97
|
+
}
|
|
98
|
+
None
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/// Scan `dir/Cargo.toml` for path dependencies; if any resolves inside a Tish workspace, return that root.
|
|
102
|
+
fn tish_root_from_cargo_manifest_dir(dir: &Path) -> Option<PathBuf> {
|
|
103
|
+
let cargo_toml = dir.join("Cargo.toml");
|
|
104
|
+
if !cargo_toml.is_file() {
|
|
105
|
+
return None;
|
|
106
|
+
}
|
|
107
|
+
let content = fs::read_to_string(&cargo_toml).ok()?;
|
|
108
|
+
let base = dir;
|
|
109
|
+
for rel in path_values_from_cargo_toml(&content) {
|
|
110
|
+
let joined = base.join(&rel);
|
|
111
|
+
let resolved = match joined.canonicalize() {
|
|
112
|
+
Ok(p) => p,
|
|
113
|
+
Err(_) => continue,
|
|
114
|
+
};
|
|
115
|
+
if let Some(root) = tish_root_from_path_hint(&resolved) {
|
|
116
|
+
return Some(root);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
None
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/// Walk from `start` upward; at each directory try [`tish_root_from_cargo_manifest_dir`].
|
|
123
|
+
fn tish_root_from_project_cargo_files(mut start: PathBuf) -> Option<PathBuf> {
|
|
124
|
+
for _ in 0..32 {
|
|
125
|
+
if let Some(root) = tish_root_from_cargo_manifest_dir(&start) {
|
|
126
|
+
return Some(root);
|
|
127
|
+
}
|
|
128
|
+
if !start.pop() {
|
|
129
|
+
break;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
None
|
|
133
|
+
}
|
|
134
|
+
|
|
18
135
|
/// Find the Tish workspace root using multiple strategies.
|
|
19
136
|
///
|
|
20
137
|
/// Returns the directory containing the workspace Cargo.toml (with [workspace]).
|
|
@@ -30,6 +147,10 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
|
|
|
30
147
|
return Ok(root_buf);
|
|
31
148
|
}
|
|
32
149
|
}
|
|
150
|
+
// Consumer workspace: manifest is the app crate; path deps point at Tish checkout.
|
|
151
|
+
if let Some(root) = tish_root_from_project_cargo_files(path.clone()) {
|
|
152
|
+
return Ok(root);
|
|
153
|
+
}
|
|
33
154
|
}
|
|
34
155
|
|
|
35
156
|
// Strategy 2: Walk from current executable (e.g. target/debug/tish)
|
|
@@ -49,7 +170,14 @@ pub fn find_workspace_root() -> Result<PathBuf, String> {
|
|
|
49
170
|
}
|
|
50
171
|
}
|
|
51
172
|
|
|
52
|
-
// Strategy 3: Walk from current working directory
|
|
173
|
+
// Strategy 3: Walk from current working directory (path deps on a consumer crate)
|
|
174
|
+
if let Ok(cwd) = std::env::current_dir() {
|
|
175
|
+
if let Some(root) = tish_root_from_project_cargo_files(cwd.clone()) {
|
|
176
|
+
return Ok(root);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Strategy 4: Walk from current working directory
|
|
53
181
|
if let Ok(mut current) = std::env::current_dir() {
|
|
54
182
|
for _ in 0..15 {
|
|
55
183
|
let cargo_toml = current.join("Cargo.toml");
|
|
@@ -193,3 +321,45 @@ pub fn copy_binary_to_output(binary: &Path, output_path: &Path) -> Result<(), St
|
|
|
193
321
|
fs::copy(binary, output_path).map_err(|e| format!("Cannot copy to {}: {}", output_path.display(), e))?;
|
|
194
322
|
Ok(())
|
|
195
323
|
}
|
|
324
|
+
|
|
325
|
+
#[cfg(test)]
|
|
326
|
+
mod tests {
|
|
327
|
+
use super::*;
|
|
328
|
+
|
|
329
|
+
#[test]
|
|
330
|
+
fn path_values_dependencies_section_only() {
|
|
331
|
+
let toml = r#"
|
|
332
|
+
[package]
|
|
333
|
+
name = "app"
|
|
334
|
+
path = "ignored-outside-deps"
|
|
335
|
+
|
|
336
|
+
[dependencies]
|
|
337
|
+
tishlang_runtime = { path = "../tish/crates/tish_runtime" }
|
|
338
|
+
|
|
339
|
+
[metadata]
|
|
340
|
+
path = "also-ignored"
|
|
341
|
+
"#;
|
|
342
|
+
let paths = path_values_from_cargo_toml(toml);
|
|
343
|
+
assert_eq!(paths, vec!["../tish/crates/tish_runtime"]);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
#[test]
|
|
347
|
+
fn path_values_workspace_dependencies() {
|
|
348
|
+
let toml = r#"
|
|
349
|
+
[workspace.dependencies]
|
|
350
|
+
tishlang_runtime = { path = "../../tish/tish/crates/tish_runtime" }
|
|
351
|
+
"#;
|
|
352
|
+
let paths = path_values_from_cargo_toml(toml);
|
|
353
|
+
assert_eq!(paths, vec!["../../tish/tish/crates/tish_runtime"]);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
#[test]
|
|
357
|
+
fn path_values_patch_section() {
|
|
358
|
+
let toml = r#"
|
|
359
|
+
[patch.crates-io]
|
|
360
|
+
tishlang_runtime = { path = "../vendor/tish_runtime" }
|
|
361
|
+
"#;
|
|
362
|
+
let paths = path_values_from_cargo_toml(toml);
|
|
363
|
+
assert_eq!(paths, vec!["../vendor/tish_runtime"]);
|
|
364
|
+
}
|
|
365
|
+
}
|