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