1use std::io::{self, Write};
2use std::path::Path;
3use std::time::SystemTime;
4
5use anyhow::{Context, Result};
6
7use crate::cli::{Command, ExecutionSpec};
8use crate::engine::{
9 ExecutionPayload, LanguageRegistry, build_install_command, default_language,
10 detect_language_for_source, ensure_known_language, perf_reset, perf_snapshot,
11};
12use crate::language::LanguageSpec;
13use crate::output;
14use crate::repl;
15use crate::version;
16
17pub fn run(command: Command) -> Result<i32> {
18 let registry = LanguageRegistry::bootstrap();
19
20 match command {
21 Command::Execute(spec) => execute_once(spec, ®istry),
22 Command::Repl {
23 initial_language,
24 detect_language,
25 } => {
26 let language = resolve_language(initial_language, detect_language, None, ®istry)?;
27 repl::run_repl(language, registry, detect_language)
28 }
29 Command::ShowVersion => {
30 println!("{}", version::describe());
31 Ok(0)
32 }
33 Command::CheckToolchains => check_toolchains(®istry),
34 Command::ShowVersions { language } => show_versions(®istry, language),
35 Command::Install { language, package } => {
36 let lang = language.unwrap_or_else(|| LanguageSpec::new(default_language()));
37 install_package(&lang, &package)
38 }
39 Command::Bench { spec, iterations } => bench_run(spec, ®istry, iterations),
40 Command::Watch { spec } => watch_run(spec, ®istry),
41 Command::PerfReport => {
42 print_perf_report();
43 Ok(0)
44 }
45 Command::PerfReset => {
46 perf_reset();
47 eprintln!("\x1b[2m[perf] counters reset\x1b[0m");
48 Ok(0)
49 }
50 }
51}
52
53fn print_perf_report() {
54 let rows = perf_snapshot();
55 if rows.is_empty() {
56 println!("perf_counter,count");
57 return;
58 }
59 println!("perf_counter,count");
60 for (key, value) in rows {
61 println!("{key},{value}");
62 }
63}
64
65fn check_toolchains(registry: &LanguageRegistry) -> Result<i32> {
66 println!("Checking language toolchains...\n");
67
68 let mut available = 0u32;
69 let mut missing = 0u32;
70
71 let mut languages: Vec<_> = registry.known_languages();
72 languages.sort();
73
74 for lang_id in &languages {
75 let spec = LanguageSpec::new(lang_id.to_string());
76 if let Some(engine) = registry.resolve(&spec) {
77 let status = match engine.validate() {
78 Ok(()) => {
79 available += 1;
80 "\x1b[32m OK \x1b[0m"
81 }
82 Err(_) => {
83 missing += 1;
84 "\x1b[31mMISS\x1b[0m"
85 }
86 };
87 println!(" [{status}] {:<14} {}", engine.display_name(), lang_id);
88 }
89 }
90
91 println!();
92 println!(
93 " {} available, {} missing, {} total",
94 available,
95 missing,
96 available + missing
97 );
98
99 if missing > 0 {
100 println!("\n Tip: Install missing toolchains to enable those languages.");
101 }
102
103 Ok(0)
104}
105
106fn show_versions(registry: &LanguageRegistry, language: Option<LanguageSpec>) -> Result<i32> {
107 println!("Language toolchain versions...\n");
108
109 let mut available = 0u32;
110 let mut missing = 0u32;
111
112 let mut languages: Vec<String> = if let Some(lang) = language {
113 vec![lang.canonical_id().to_string()]
114 } else {
115 registry
116 .known_languages()
117 .into_iter()
118 .map(|value| value.to_string())
119 .collect()
120 };
121 languages.sort();
122
123 for lang_id in &languages {
124 let spec = LanguageSpec::new(lang_id.to_string());
125 if let Some(engine) = registry.resolve(&spec) {
126 match engine.toolchain_version() {
127 Ok(Some(version)) => {
128 available += 1;
129 println!(
130 " [\x1b[32m OK \x1b[0m] {:<14} {} - {}",
131 engine.display_name(),
132 lang_id,
133 version
134 );
135 }
136 Ok(None) => {
137 available += 1;
138 println!(
139 " [\x1b[33m ?? \x1b[0m] {:<14} {} - unknown",
140 engine.display_name(),
141 lang_id
142 );
143 }
144 Err(_) => {
145 missing += 1;
146 println!(
147 " [\x1b[31mMISS\x1b[0m] {:<14} {}",
148 engine.display_name(),
149 lang_id
150 );
151 }
152 }
153 }
154 }
155
156 println!();
157 println!(
158 " {} available, {} missing, {} total",
159 available,
160 missing,
161 available + missing
162 );
163
164 if missing > 0 {
165 println!("\n Tip: Install missing toolchains to enable those languages.");
166 }
167
168 Ok(0)
169}
170
171fn execute_once(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
172 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
173 .context("failed to materialize execution payload")?;
174 let language = resolve_language(
175 spec.language,
176 spec.detect_language,
177 Some(&payload),
178 registry,
179 )?;
180
181 let engine = registry
182 .resolve(&language)
183 .context("failed to resolve language engine")?;
184
185 if let Err(e) = engine.validate() {
186 let display = engine.display_name();
187 let id = engine.id();
188 eprintln!(
189 "Warning: {display} ({id}) toolchain not found: {e:#}\n\
190 Install the required toolchain and ensure it is on your PATH."
191 );
192 return Err(e.context(format!("{display} is not available")));
193 }
194
195 let outcome = engine.execute(&payload)?;
196
197 if !outcome.stdout.is_empty() {
198 print!("{}", outcome.stdout);
199 io::stdout().flush().ok();
200 }
201 if !outcome.stderr.is_empty() {
202 let formatted =
203 output::format_stderr(engine.display_name(), &outcome.stderr, outcome.success());
204 eprint!("{formatted}");
205 io::stderr().flush().ok();
206 }
207
208 let show_timing = std::env::var("RUN_TIMING").is_ok_and(|v| v == "1" || v == "true");
210 if show_timing || outcome.duration.as_millis() > 1000 {
211 eprintln!(
212 "\x1b[2m[{} {}ms]\x1b[0m",
213 engine.display_name(),
214 outcome.duration.as_millis()
215 );
216 }
217
218 if std::env::var("RUN_PERF_REPORT").is_ok_and(|v| v == "1" || v == "true") {
219 eprintln!("\x1b[2m[perf]\x1b[0m");
220 for (key, value) in perf_snapshot() {
221 eprintln!("\x1b[2m {key}={value}\x1b[0m");
222 }
223 }
224
225 Ok(outcome
226 .exit_code
227 .unwrap_or(if outcome.success() { 0 } else { 1 }))
228}
229
230fn install_package(language: &LanguageSpec, package: &str) -> Result<i32> {
231 let lang_id = language.canonical_id();
232 let override_key = format!("RUN_INSTALL_COMMAND_{}", lang_id.to_ascii_uppercase());
233 let override_value = std::env::var(&override_key).ok();
234
235 let Some(mut cmd) = build_install_command(lang_id, package) else {
236 if override_value.is_some() {
237 eprintln!(
238 "\x1b[31mError:\x1b[0m {override_key} is set but could not be parsed.\n\
239 Provide a valid command, e.g. {override_key}=\"uv pip install {{package}}\""
240 );
241 return Ok(1);
242 }
243 eprintln!(
244 "\x1b[31mError:\x1b[0m No package manager available for '{lang_id}'.\n\
245 This language doesn't have a standard CLI package manager.\n\
246 Tip: You can override with {override_key}=\"<cmd> {{package}}\"",
247 );
248 return Ok(1);
249 };
250
251 eprintln!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
252
253 let result = cmd
254 .stdin(std::process::Stdio::inherit())
255 .stdout(std::process::Stdio::inherit())
256 .stderr(std::process::Stdio::inherit())
257 .status();
258
259 match result {
260 Ok(status) if status.success() => {
261 eprintln!("\x1b[32m[run]\x1b[0m Successfully installed '{package}' for {lang_id}");
262 Ok(0)
263 }
264 Ok(status) => {
265 eprintln!("\x1b[31m[run]\x1b[0m Failed to install '{package}' for {lang_id}");
266 Ok(status.code().unwrap_or(1))
267 }
268 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
269 let program = cmd.get_program().to_string_lossy();
270 eprintln!("\x1b[31m[run]\x1b[0m Package manager not found: {program}");
271 eprintln!("Tip: install it or set {override_key}=\"<cmd> {{package}}\"");
272 Ok(1)
273 }
274 Err(err) => {
275 Err(err).with_context(|| format!("failed to run package manager for {lang_id}"))
276 }
277 }
278}
279
280fn bench_run(spec: ExecutionSpec, registry: &LanguageRegistry, iterations: u32) -> Result<i32> {
281 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
282 .context("failed to materialize execution payload")?;
283 let language = resolve_language(
284 spec.language,
285 spec.detect_language,
286 Some(&payload),
287 registry,
288 )?;
289
290 let engine = registry
291 .resolve(&language)
292 .context("failed to resolve language engine")?;
293
294 engine
295 .validate()
296 .with_context(|| format!("{} is not available", engine.display_name()))?;
297
298 eprintln!(
299 "\x1b[1mBenchmark:\x1b[0m {} — {} iteration{}",
300 engine.display_name(),
301 iterations,
302 if iterations == 1 { "" } else { "s" }
303 );
304
305 let warmup = engine.execute(&payload)?;
307 if !warmup.success() {
308 eprintln!("\x1b[31mError:\x1b[0m Code failed during warmup run");
309 if !warmup.stderr.is_empty() {
310 eprint!("{}", warmup.stderr);
311 }
312 return Ok(1);
313 }
314 eprintln!("\x1b[2m warmup: {}ms\x1b[0m", warmup.duration.as_millis());
315
316 let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
317
318 for i in 0..iterations {
319 let outcome = engine.execute(&payload)?;
320 let ms = outcome.duration.as_secs_f64() * 1000.0;
321 times.push(ms);
322
323 if i < 3 || i == iterations - 1 || (i + 1) % 10 == 0 {
324 eprintln!("\x1b[2m run {}: {:.2}ms\x1b[0m", i + 1, ms);
325 }
326 }
327
328 times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
329 let total: f64 = times.iter().sum();
330 let avg = total / times.len() as f64;
331 let min = times.first().copied().unwrap_or(0.0);
332 let max = times.last().copied().unwrap_or(0.0);
333 let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
334 (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
335 } else {
336 times[times.len() / 2]
337 };
338
339 let variance: f64 = times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
341 let stddev = variance.sqrt();
342
343 eprintln!();
344 eprintln!("\x1b[1mResults ({} runs):\x1b[0m", iterations);
345 eprintln!(" min: \x1b[32m{:.2}ms\x1b[0m", min);
346 eprintln!(" max: \x1b[33m{:.2}ms\x1b[0m", max);
347 eprintln!(" avg: \x1b[36m{:.2}ms\x1b[0m", avg);
348 eprintln!(" median: \x1b[36m{:.2}ms\x1b[0m", median);
349 eprintln!(" stddev: {:.2}ms", stddev);
350
351 if !warmup.stdout.is_empty() {
352 print!("{}", warmup.stdout);
353 io::stdout().flush().ok();
354 }
355
356 Ok(0)
357}
358
359fn watch_run(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
360 use crate::cli::InputSource;
361
362 let file_path = match &spec.source {
363 InputSource::File(p) => p.clone(),
364 _ => anyhow::bail!("--watch requires a file path (use -f or pass a file as argument)"),
365 };
366
367 if !file_path.exists() {
368 anyhow::bail!("File not found: {}", file_path.display());
369 }
370
371 let payload = ExecutionPayload::from_input_source(&spec.source, &spec.args)
372 .context("failed to materialize execution payload")?;
373 let language = resolve_language(
374 spec.language.clone(),
375 spec.detect_language,
376 Some(&payload),
377 registry,
378 )?;
379
380 let engine = registry
381 .resolve(&language)
382 .context("failed to resolve language engine")?;
383
384 engine
385 .validate()
386 .with_context(|| format!("{} is not available", engine.display_name()))?;
387
388 eprintln!(
389 "\x1b[1m[watch]\x1b[0m Watching \x1b[36m{}\x1b[0m ({}). Press Ctrl+C to stop.",
390 file_path.display(),
391 engine.display_name()
392 );
393
394 fn get_mtime(path: &Path) -> Option<SystemTime> {
395 std::fs::metadata(path).ok()?.modified().ok()
396 }
397
398 let mut last_mtime = get_mtime(&file_path);
399 let mut run_count = 0u32;
400
401 run_count += 1;
403 eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
404 run_file_once(&file_path, engine, &spec.args);
405
406 loop {
407 std::thread::sleep(std::time::Duration::from_millis(300));
408
409 let current_mtime = get_mtime(&file_path);
410 if current_mtime != last_mtime {
411 last_mtime = current_mtime;
412 run_count += 1;
413
414 eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
415
416 run_file_once(&file_path, engine, &spec.args);
417 }
418 }
419}
420
421fn run_file_once(file_path: &Path, engine: &dyn crate::engine::LanguageEngine, args: &[String]) {
422 let payload = ExecutionPayload::File {
423 path: file_path.to_path_buf(),
424 args: args.to_vec(),
425 };
426 match engine.execute(&payload) {
427 Ok(outcome) => {
428 if !outcome.stdout.is_empty() {
429 print!("{}", outcome.stdout);
430 io::stdout().flush().ok();
431 }
432 if !outcome.stderr.is_empty() {
433 eprint!("\x1b[31m{}\x1b[0m", outcome.stderr);
434 io::stderr().flush().ok();
435 }
436 let ms = outcome.duration.as_millis();
437 let status = if outcome.success() {
438 "\x1b[32mOK\x1b[0m"
439 } else {
440 "\x1b[31mFAIL\x1b[0m"
441 };
442 eprintln!("\x1b[2m[{status} {ms}ms]\x1b[0m");
443 }
444 Err(e) => {
445 eprintln!("\x1b[31mError:\x1b[0m {e:#}");
446 }
447 }
448}
449
450fn resolve_language(
451 explicit: Option<LanguageSpec>,
452 allow_detect: bool,
453 payload: Option<&ExecutionPayload>,
454 registry: &LanguageRegistry,
455) -> Result<LanguageSpec> {
456 if let Some(spec) = explicit {
457 ensure_known_language(&spec, registry)?;
458 return Ok(spec);
459 }
460
461 if allow_detect
462 && let Some(payload) = payload
463 && let Some(detected) = detect_language_for_source(payload, registry)
464 {
465 return Ok(detected);
466 }
467
468 let default = LanguageSpec::new(default_language());
469 ensure_known_language(&default, registry)?;
470 Ok(default)
471}