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