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, package_install_command,
11};
12use crate::language::LanguageSpec;
13use crate::repl;
14use crate::version;
15
16pub fn run(command: Command) -> Result<i32> {
17    let registry = LanguageRegistry::bootstrap();
18
19    match command {
20        Command::Execute(spec) => execute_once(spec, &registry),
21        Command::Repl {
22            initial_language,
23            detect_language,
24        } => {
25            let language = resolve_language(initial_language, detect_language, None, &registry)?;
26            repl::run_repl(language, registry, detect_language)
27        }
28        Command::ShowVersion => {
29            println!("{}", version::describe());
30            Ok(0)
31        }
32        Command::CheckToolchains => check_toolchains(&registry),
33        Command::Install { language, package } => {
34            let lang = language.unwrap_or_else(|| LanguageSpec::new(default_language()));
35            install_package(&lang, &package)
36        }
37        Command::Bench { spec, iterations } => bench_run(spec, &registry, iterations),
38        Command::Watch { spec } => watch_run(spec, &registry),
39    }
40}
41
42fn check_toolchains(registry: &LanguageRegistry) -> Result<i32> {
43    println!("Checking language toolchains...\n");
44
45    let mut available = 0u32;
46    let mut missing = 0u32;
47
48    let mut languages: Vec<_> = registry.known_languages();
49    languages.sort();
50
51    for lang_id in &languages {
52        let spec = LanguageSpec::new(lang_id.to_string());
53        if let Some(engine) = registry.resolve(&spec) {
54            let status = match engine.validate() {
55                Ok(()) => {
56                    available += 1;
57                    "\x1b[32m OK \x1b[0m"
58                }
59                Err(_) => {
60                    missing += 1;
61                    "\x1b[31mMISS\x1b[0m"
62                }
63            };
64            println!("  [{status}] {:<14} {}", engine.display_name(), lang_id);
65        }
66    }
67
68    println!();
69    println!(
70        "  {} available, {} missing, {} total",
71        available,
72        missing,
73        available + missing
74    );
75
76    if missing > 0 {
77        println!("\n  Tip: Install missing toolchains to enable those languages.");
78    }
79
80    Ok(0)
81}
82
83fn execute_once(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
84    let payload = ExecutionPayload::from_input_source(&spec.source)
85        .context("failed to materialize execution payload")?;
86    let language = resolve_language(
87        spec.language,
88        spec.detect_language,
89        Some(&payload),
90        registry,
91    )?;
92
93    let engine = registry
94        .resolve(&language)
95        .context("failed to resolve language engine")?;
96
97    if let Err(e) = engine.validate() {
98        let display = engine.display_name();
99        let id = engine.id();
100        eprintln!(
101            "Warning: {display} ({id}) toolchain not found: {e:#}\n\
102             Install the required toolchain and ensure it is on your PATH."
103        );
104        return Err(e.context(format!("{display} is not available")));
105    }
106
107    let outcome = engine.execute(&payload)?;
108
109    if !outcome.stdout.is_empty() {
110        print!("{}", outcome.stdout);
111        io::stdout().flush().ok();
112    }
113    if !outcome.stderr.is_empty() {
114        eprint!("{}", outcome.stderr);
115        io::stderr().flush().ok();
116    }
117
118    // Show timing on stderr if RUN_TIMING=1 or if execution was slow (>1s)
119    let show_timing = std::env::var("RUN_TIMING").is_ok_and(|v| v == "1" || v == "true");
120    if show_timing || outcome.duration.as_millis() > 1000 {
121        eprintln!(
122            "\x1b[2m[{} {}ms]\x1b[0m",
123            engine.display_name(),
124            outcome.duration.as_millis()
125        );
126    }
127
128    Ok(outcome
129        .exit_code
130        .unwrap_or(if outcome.success() { 0 } else { 1 }))
131}
132
133fn install_package(language: &LanguageSpec, package: &str) -> Result<i32> {
134    let lang_id = language.canonical_id();
135
136    if package_install_command(lang_id).is_none() {
137        eprintln!(
138            "\x1b[31mError:\x1b[0m No package manager available for '{lang_id}'.\n\
139             This language doesn't have a standard CLI package manager."
140        );
141        return Ok(1);
142    }
143
144    let mut cmd =
145        build_install_command(lang_id, package).context("failed to build install command")?;
146
147    eprintln!("\x1b[36m[run]\x1b[0m Installing '{package}' for {lang_id}...");
148
149    let status = cmd
150        .stdin(std::process::Stdio::inherit())
151        .stdout(std::process::Stdio::inherit())
152        .stderr(std::process::Stdio::inherit())
153        .status()
154        .with_context(|| format!("failed to run package manager for {lang_id}"))?;
155
156    if status.success() {
157        eprintln!("\x1b[32m[run]\x1b[0m Successfully installed '{package}' for {lang_id}");
158        Ok(0)
159    } else {
160        eprintln!("\x1b[31m[run]\x1b[0m Failed to install '{package}' for {lang_id}");
161        Ok(status.code().unwrap_or(1))
162    }
163}
164
165fn bench_run(spec: ExecutionSpec, registry: &LanguageRegistry, iterations: u32) -> Result<i32> {
166    let payload = ExecutionPayload::from_input_source(&spec.source)
167        .context("failed to materialize execution payload")?;
168    let language = resolve_language(
169        spec.language,
170        spec.detect_language,
171        Some(&payload),
172        registry,
173    )?;
174
175    let engine = registry
176        .resolve(&language)
177        .context("failed to resolve language engine")?;
178
179    engine
180        .validate()
181        .with_context(|| format!("{} is not available", engine.display_name()))?;
182
183    eprintln!(
184        "\x1b[1mBenchmark:\x1b[0m {} — {} iteration{}",
185        engine.display_name(),
186        iterations,
187        if iterations == 1 { "" } else { "s" }
188    );
189
190    // Warmup run (not counted)
191    let warmup = engine.execute(&payload)?;
192    if !warmup.success() {
193        eprintln!("\x1b[31mError:\x1b[0m Code failed during warmup run");
194        if !warmup.stderr.is_empty() {
195            eprint!("{}", warmup.stderr);
196        }
197        return Ok(1);
198    }
199    eprintln!("\x1b[2m  warmup: {}ms\x1b[0m", warmup.duration.as_millis());
200
201    let mut times: Vec<f64> = Vec::with_capacity(iterations as usize);
202
203    for i in 0..iterations {
204        let outcome = engine.execute(&payload)?;
205        let ms = outcome.duration.as_secs_f64() * 1000.0;
206        times.push(ms);
207
208        if i < 3 || i == iterations - 1 || (i + 1) % 10 == 0 {
209            eprintln!("\x1b[2m  run {}: {:.2}ms\x1b[0m", i + 1, ms);
210        }
211    }
212
213    times.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
214    let total: f64 = times.iter().sum();
215    let avg = total / times.len() as f64;
216    let min = times.first().copied().unwrap_or(0.0);
217    let max = times.last().copied().unwrap_or(0.0);
218    let median = if times.len().is_multiple_of(2) && times.len() >= 2 {
219        (times[times.len() / 2 - 1] + times[times.len() / 2]) / 2.0
220    } else {
221        times[times.len() / 2]
222    };
223
224    // Standard deviation
225    let variance: f64 = times.iter().map(|t| (t - avg).powi(2)).sum::<f64>() / times.len() as f64;
226    let stddev = variance.sqrt();
227
228    eprintln!();
229    eprintln!("\x1b[1mResults ({} runs):\x1b[0m", iterations);
230    eprintln!("  min:    \x1b[32m{:.2}ms\x1b[0m", min);
231    eprintln!("  max:    \x1b[33m{:.2}ms\x1b[0m", max);
232    eprintln!("  avg:    \x1b[36m{:.2}ms\x1b[0m", avg);
233    eprintln!("  median: \x1b[36m{:.2}ms\x1b[0m", median);
234    eprintln!("  stddev: {:.2}ms", stddev);
235
236    if !warmup.stdout.is_empty() {
237        print!("{}", warmup.stdout);
238        io::stdout().flush().ok();
239    }
240
241    Ok(0)
242}
243
244fn watch_run(spec: ExecutionSpec, registry: &LanguageRegistry) -> Result<i32> {
245    use crate::cli::InputSource;
246
247    let file_path = match &spec.source {
248        InputSource::File(p) => p.clone(),
249        _ => anyhow::bail!("--watch requires a file path (use -f or pass a file as argument)"),
250    };
251
252    if !file_path.exists() {
253        anyhow::bail!("File not found: {}", file_path.display());
254    }
255
256    let payload = ExecutionPayload::from_input_source(&spec.source)
257        .context("failed to materialize execution payload")?;
258    let language = resolve_language(
259        spec.language.clone(),
260        spec.detect_language,
261        Some(&payload),
262        registry,
263    )?;
264
265    let engine = registry
266        .resolve(&language)
267        .context("failed to resolve language engine")?;
268
269    engine
270        .validate()
271        .with_context(|| format!("{} is not available", engine.display_name()))?;
272
273    eprintln!(
274        "\x1b[1m[watch]\x1b[0m Watching \x1b[36m{}\x1b[0m ({}). Press Ctrl+C to stop.",
275        file_path.display(),
276        engine.display_name()
277    );
278
279    fn get_mtime(path: &Path) -> Option<SystemTime> {
280        std::fs::metadata(path).ok()?.modified().ok()
281    }
282
283    let mut last_mtime = get_mtime(&file_path);
284    let mut run_count = 0u32;
285
286    // Initial run
287    run_count += 1;
288    eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
289    run_file_once(&file_path, engine);
290
291    loop {
292        std::thread::sleep(std::time::Duration::from_millis(300));
293
294        let current_mtime = get_mtime(&file_path);
295        if current_mtime != last_mtime {
296            last_mtime = current_mtime;
297            run_count += 1;
298
299            eprintln!("\n\x1b[2m--- run #{run_count} ---\x1b[0m");
300
301            run_file_once(&file_path, engine);
302        }
303    }
304}
305
306fn run_file_once(file_path: &Path, engine: &dyn crate::engine::LanguageEngine) {
307    let payload = ExecutionPayload::File {
308        path: file_path.to_path_buf(),
309    };
310    match engine.execute(&payload) {
311        Ok(outcome) => {
312            if !outcome.stdout.is_empty() {
313                print!("{}", outcome.stdout);
314                io::stdout().flush().ok();
315            }
316            if !outcome.stderr.is_empty() {
317                eprint!("\x1b[31m{}\x1b[0m", outcome.stderr);
318                io::stderr().flush().ok();
319            }
320            let ms = outcome.duration.as_millis();
321            let status = if outcome.success() {
322                "\x1b[32mOK\x1b[0m"
323            } else {
324                "\x1b[31mFAIL\x1b[0m"
325            };
326            eprintln!("\x1b[2m[{status} {ms}ms]\x1b[0m");
327        }
328        Err(e) => {
329            eprintln!("\x1b[31mError:\x1b[0m {e:#}");
330        }
331    }
332}
333
334fn resolve_language(
335    explicit: Option<LanguageSpec>,
336    allow_detect: bool,
337    payload: Option<&ExecutionPayload>,
338    registry: &LanguageRegistry,
339) -> Result<LanguageSpec> {
340    if let Some(spec) = explicit {
341        ensure_known_language(&spec, registry)?;
342        return Ok(spec);
343    }
344
345    if allow_detect
346        && let Some(payload) = payload
347        && let Some(detected) = detect_language_for_source(payload, registry)
348    {
349        return Ok(detected);
350    }
351
352    let default = LanguageSpec::new(default_language());
353    ensure_known_language(&default, registry)?;
354    Ok(default)
355}