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!(
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 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 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 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 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}