Skip to main content

run/
app.rs

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, &registry),
22        Command::Repl {
23            initial_language,
24            detect_language,
25        } => {
26            let language = resolve_language(initial_language, detect_language, None, &registry)?;
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(&registry),
34        Command::ShowVersions { language } => show_versions(&registry, 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, &registry, iterations),
40        Command::Watch { spec } => watch_run(spec, &registry),
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    // Show timing on stderr if RUN_TIMING=1 or if execution was slow (>1s)
209    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    // Warmup run (not counted)
306    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    // Standard deviation
340    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    // Initial run
402    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}