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, ®istry),
21 Command::Repl {
22 initial_language,
23 detect_language,
24 } => {
25 let language = resolve_language(initial_language, detect_language, None, ®istry)?;
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(®istry),
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, ®istry, iterations),
38 Command::Watch { spec } => watch_run(spec, ®istry),
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 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 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 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 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}