1mod bash;
2mod c;
3mod cpp;
4mod crystal;
5mod csharp;
6mod dart;
7mod elixir;
8mod go;
9mod groovy;
10mod haskell;
11mod java;
12mod javascript;
13mod julia;
14mod kotlin;
15mod lua;
16mod nim;
17mod perl;
18mod php;
19mod python;
20mod r;
21mod ruby;
22mod rust;
23mod swift;
24mod typescript;
25mod zig;
26
27use std::borrow::Cow;
28use std::collections::HashMap;
29use std::path::{Path, PathBuf};
30use std::process::{Child, Command, Output, Stdio};
31use std::sync::atomic::{AtomicBool, Ordering};
32use std::sync::{LazyLock, Mutex, OnceLock};
33use std::time::{Duration, Instant};
34
35use anyhow::{Context, Result, bail};
36
37use crate::cli::InputSource;
38use crate::language::{LanguageSpec, canonical_language_id};
39
40static SCCACHE_INIT: OnceLock<()> = OnceLock::new();
45static SCCACHE_READY: AtomicBool = AtomicBool::new(false);
46static PERF_COUNTERS: LazyLock<Mutex<HashMap<String, u64>>> =
47 LazyLock::new(|| Mutex::new(HashMap::new()));
48
49pub fn hash_source(source: &str) -> u64 {
51 let digest = blake3::hash(source.as_bytes());
52 let mut bytes = [0u8; 8];
53 bytes.copy_from_slice(&digest.as_bytes()[..8]);
54 u64::from_le_bytes(bytes)
55}
56
57fn perf_file_path() -> PathBuf {
58 std::env::temp_dir().join("run-perf-counters.csv")
59}
60
61fn read_perf_file() -> HashMap<String, u64> {
62 let path = perf_file_path();
63 let Ok(text) = std::fs::read_to_string(path) else {
64 return HashMap::new();
65 };
66 let mut map = HashMap::new();
67 for line in text.lines() {
68 if let Some((key, value)) = line.split_once(',')
69 && let Ok(parsed) = value.parse::<u64>()
70 {
71 map.insert(key.to_string(), parsed);
72 }
73 }
74 map
75}
76
77fn write_perf_file(map: &HashMap<String, u64>) {
78 let mut rows = map.iter().collect::<Vec<_>>();
79 rows.sort_by(|a, b| a.0.cmp(b.0));
80 let mut buf = String::new();
81 for (key, value) in rows {
82 buf.push_str(key);
83 buf.push(',');
84 buf.push_str(&value.to_string());
85 buf.push('\n');
86 }
87 let _ = std::fs::write(perf_file_path(), buf);
88}
89
90pub fn cache_lookup(namespace: &str, source_hash: u64) -> Option<PathBuf> {
93 crate::cache::lookup(namespace, source_hash)
94}
95
96pub fn cache_store(namespace: &str, source_hash: u64, binary: &Path) -> Option<PathBuf> {
98 crate::cache::store(namespace, source_hash, binary)
99}
100
101pub fn try_cached_execution(namespace: &str, source_hash: u64) -> Option<std::process::Output> {
103 let cached = cache_lookup(namespace, source_hash)?;
104 let mut cmd = std::process::Command::new(&cached);
105 cmd.stdout(std::process::Stdio::piped())
106 .stderr(std::process::Stdio::piped())
107 .stdin(std::process::Stdio::inherit());
108 cmd.output().ok()
109}
110
111pub fn compiler_command(compiler: &Path) -> Command {
119 let mode = std::env::var("RUN_COMPILER_DAEMON")
120 .unwrap_or_else(|_| "adaptive".to_string())
121 .to_ascii_lowercase();
122 if mode == "off" {
123 perf_record("global", "compiler.raw");
124 return Command::new(compiler);
125 }
126
127 let want_sccache = mode == "sccache" || mode == "auto" || mode == "adaptive";
128 if want_sccache && let Ok(sccache) = which::which("sccache") {
129 if mode == "adaptive" && !SCCACHE_READY.load(Ordering::Relaxed) {
130 let _ = SCCACHE_INIT.get_or_init(|| {
131 let sccache_clone = sccache.clone();
132 std::thread::spawn(move || {
133 let ready = std::process::Command::new(&sccache_clone)
134 .arg("--start-server")
135 .stdout(Stdio::null())
136 .stderr(Stdio::null())
137 .status()
138 .is_ok_and(|s| s.success());
139 if ready {
140 SCCACHE_READY.store(true, Ordering::Relaxed);
141 }
142 });
143 });
144 perf_record("global", "compiler.raw.adaptive_warmup");
145 return Command::new(compiler);
146 }
147 let _ = SCCACHE_INIT.get_or_init(|| {
148 let ready = std::process::Command::new(&sccache)
149 .arg("--start-server")
150 .stdout(Stdio::null())
151 .stderr(Stdio::null())
152 .status()
153 .is_ok_and(|s| s.success());
154 if ready {
155 SCCACHE_READY.store(true, Ordering::Relaxed);
156 }
157 });
158 let mut cmd = Command::new(sccache);
159 cmd.arg(compiler);
160 perf_record("global", "compiler.sccache");
161 return cmd;
162 }
163
164 let want_ccache = mode == "ccache" || mode == "auto" || mode == "adaptive";
165 if want_ccache && let Ok(ccache) = which::which("ccache") {
166 let mut cmd = Command::new(ccache);
167 cmd.arg(compiler);
168 perf_record("global", "compiler.ccache");
169 return cmd;
170 }
171
172 perf_record("global", "compiler.raw.fallback");
173 Command::new(compiler)
174}
175
176pub fn perf_record(language: &str, event: &str) {
177 let key = format!("{language}.{event}");
178 if let Ok(mut counters) = PERF_COUNTERS.lock() {
179 let entry = counters.entry(key).or_insert(0);
180 *entry += 1;
181 let mut disk = read_perf_file();
182 let disk_entry = disk.entry(language.to_string() + "." + event).or_insert(0);
183 *disk_entry += 1;
184 write_perf_file(&disk);
185 }
186}
187
188pub fn perf_snapshot() -> Vec<(String, u64)> {
189 let disk = read_perf_file();
190 let mut rows = if !disk.is_empty() {
191 disk.into_iter().collect::<Vec<_>>()
192 } else if let Ok(counters) = PERF_COUNTERS.lock() {
193 counters
194 .iter()
195 .map(|(k, v)| (k.clone(), *v))
196 .collect::<Vec<_>>()
197 } else {
198 Vec::new()
199 };
200 rows.sort_by(|a, b| a.0.cmp(&b.0));
201 rows
202}
203
204pub fn perf_reset() {
205 if let Ok(mut counters) = PERF_COUNTERS.lock() {
206 counters.clear();
207 }
208 let _ = std::fs::remove_file(perf_file_path());
209}
210
211pub fn execution_timeout() -> Duration {
215 let secs = crate::runtime::timeout_secs();
216 Duration::from_secs(secs)
217}
218
219pub fn wait_with_timeout(mut child: Child, timeout: Duration) -> Result<std::process::Output> {
222 let start = Instant::now();
223 let poll_interval = Duration::from_millis(50);
224
225 loop {
226 match child.try_wait() {
227 Ok(Some(_status)) => {
228 return child.wait_with_output().map_err(Into::into);
229 }
230 Ok(None) => {
231 if timeout.as_secs() > 0 && start.elapsed() > timeout {
232 let _ = child.kill();
233 let _ = child.wait(); bail!("Execution timed out after {}s", timeout.as_secs());
235 }
236 std::thread::sleep(poll_interval);
237 }
238 Err(e) => {
239 return Err(e.into());
240 }
241 }
242 }
243}
244
245pub use bash::BashEngine;
246pub use c::CEngine;
247pub use cpp::CppEngine;
248pub use crystal::CrystalEngine;
249pub use csharp::CSharpEngine;
250pub use dart::DartEngine;
251pub use elixir::ElixirEngine;
252pub use go::GoEngine;
253pub use groovy::GroovyEngine;
254pub use haskell::HaskellEngine;
255pub use java::JavaEngine;
256pub use javascript::JavascriptEngine;
257pub use julia::JuliaEngine;
258pub use kotlin::KotlinEngine;
259pub use lua::LuaEngine;
260pub use nim::NimEngine;
261pub use perl::PerlEngine;
262pub use php::PhpEngine;
263pub use python::PythonEngine;
264pub use r::REngine;
265pub use ruby::RubyEngine;
266pub use rust::RustEngine;
267pub use swift::SwiftEngine;
268pub use typescript::TypeScriptEngine;
269pub use zig::ZigEngine;
270
271pub trait LanguageSession {
273 fn language_id(&self) -> &str;
274 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome>;
275 fn shutdown(&mut self) -> Result<()>;
276}
277
278pub trait LanguageEngine {
309 fn id(&self) -> &'static str;
310 fn display_name(&self) -> &'static str {
311 self.id()
312 }
313 fn aliases(&self) -> &[&'static str] {
314 &[]
315 }
316 fn supports_sessions(&self) -> bool {
317 false
318 }
319 fn validate(&self) -> Result<()> {
320 Ok(())
321 }
322 fn toolchain_version(&self) -> Result<Option<String>> {
323 Ok(None)
324 }
325 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome>;
326 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
327 bail!("{} does not support interactive sessions yet", self.id())
328 }
329}
330
331pub(crate) fn version_line_from_output(output: &Output) -> Option<String> {
332 let stdout = String::from_utf8_lossy(&output.stdout);
333 for line in stdout.lines() {
334 let trimmed = line.trim();
335 if !trimmed.is_empty() {
336 return Some(trimmed.to_string());
337 }
338 }
339 let stderr = String::from_utf8_lossy(&output.stderr);
340 for line in stderr.lines() {
341 let trimmed = line.trim();
342 if !trimmed.is_empty() {
343 return Some(trimmed.to_string());
344 }
345 }
346 None
347}
348
349pub(crate) fn run_version_command(mut cmd: Command, context: &str) -> Result<Option<String>> {
350 cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
351 let output = cmd
352 .output()
353 .with_context(|| format!("failed to invoke {context}"))?;
354 let version = version_line_from_output(&output);
355 if output.status.success() || version.is_some() {
356 Ok(version)
357 } else {
358 bail!("{context} exited with status {}", output.status);
359 }
360}
361
362#[derive(Debug, Clone, PartialEq, Eq)]
363pub enum ExecutionPayload {
364 Inline {
365 code: String,
366 args: Vec<String>,
367 },
368 File {
369 path: std::path::PathBuf,
370 args: Vec<String>,
371 },
372 Stdin {
373 code: String,
374 args: Vec<String>,
375 },
376}
377
378impl ExecutionPayload {
379 pub fn from_input_source(source: &InputSource, args: &[String]) -> Result<Self> {
380 let args = args.to_vec();
381 match source {
382 InputSource::Inline(code) => Ok(Self::Inline {
383 code: normalize_inline_code(code).into_owned(),
384 args,
385 }),
386 InputSource::File(path) => Ok(Self::File {
387 path: path.clone(),
388 args,
389 }),
390 InputSource::Stdin => {
391 use std::io::Read;
392 let mut buffer = String::new();
393 std::io::stdin().read_to_string(&mut buffer)?;
394 Ok(Self::Stdin { code: buffer, args })
395 }
396 }
397 }
398
399 pub fn as_inline(&self) -> Option<&str> {
400 match self {
401 ExecutionPayload::Inline { code, .. } => Some(code.as_str()),
402 ExecutionPayload::Stdin { code, .. } => Some(code.as_str()),
403 ExecutionPayload::File { .. } => None,
404 }
405 }
406
407 pub fn as_file_path(&self) -> Option<&Path> {
408 match self {
409 ExecutionPayload::File { path, .. } => Some(path.as_path()),
410 _ => None,
411 }
412 }
413
414 pub fn args(&self) -> &[String] {
415 match self {
416 ExecutionPayload::Inline { args, .. } => args.as_slice(),
417 ExecutionPayload::File { args, .. } => args.as_slice(),
418 ExecutionPayload::Stdin { args, .. } => args.as_slice(),
419 }
420 }
421}
422
423fn normalize_inline_code(code: &str) -> Cow<'_, str> {
424 if !code.contains('\\') {
425 return Cow::Borrowed(code);
426 }
427
428 let mut result = String::with_capacity(code.len());
429 let mut chars = code.chars().peekable();
430 let mut in_single = false;
431 let mut in_double = false;
432 let mut escape_in_quote = false;
433
434 while let Some(ch) = chars.next() {
435 if in_single {
436 result.push(ch);
437 if escape_in_quote {
438 escape_in_quote = false;
439 } else if ch == '\\' {
440 escape_in_quote = true;
441 } else if ch == '\'' {
442 in_single = false;
443 }
444 continue;
445 }
446
447 if in_double {
448 result.push(ch);
449 if escape_in_quote {
450 escape_in_quote = false;
451 } else if ch == '\\' {
452 escape_in_quote = true;
453 } else if ch == '"' {
454 in_double = false;
455 }
456 continue;
457 }
458
459 match ch {
460 '\'' => {
461 in_single = true;
462 result.push(ch);
463 }
464 '"' => {
465 in_double = true;
466 result.push(ch);
467 }
468 '\\' => match chars.next() {
469 Some('n') => result.push('\n'),
470 Some('r') => result.push('\r'),
471 Some('t') => result.push('\t'),
472 Some('\\') => result.push('\\'),
473 Some(other) => {
474 result.push('\\');
475 result.push(other);
476 }
477 None => result.push('\\'),
478 },
479 _ => result.push(ch),
480 }
481 }
482
483 Cow::Owned(result)
484}
485
486#[derive(Debug, Clone, PartialEq, Eq)]
487pub struct ExecutionOutcome {
488 pub language: String,
489 pub exit_code: Option<i32>,
490 pub stdout: String,
491 pub stderr: String,
492 pub duration: Duration,
493}
494
495impl ExecutionOutcome {
496 pub fn success(&self) -> bool {
497 match self.exit_code {
498 Some(code) => code == 0,
499 None => self.stderr.trim().is_empty(),
500 }
501 }
502}
503
504pub struct LanguageRegistry {
505 engines: HashMap<String, Box<dyn LanguageEngine + Send + Sync>>, alias_lookup: HashMap<String, String>,
507}
508
509impl LanguageRegistry {
510 pub fn bootstrap() -> Self {
511 let mut registry = Self {
512 engines: HashMap::new(),
513 alias_lookup: HashMap::new(),
514 };
515
516 registry.register_language(PythonEngine::new());
517 registry.register_language(BashEngine::new());
518 registry.register_language(JavascriptEngine::new());
519 registry.register_language(RubyEngine::new());
520 registry.register_language(RustEngine::new());
521 registry.register_language(GoEngine::new());
522 registry.register_language(CSharpEngine::new());
523 registry.register_language(TypeScriptEngine::new());
524 registry.register_language(LuaEngine::new());
525 registry.register_language(JavaEngine::new());
526 registry.register_language(GroovyEngine::new());
527 registry.register_language(PhpEngine::new());
528 registry.register_language(KotlinEngine::new());
529 registry.register_language(CEngine::new());
530 registry.register_language(CppEngine::new());
531 registry.register_language(REngine::new());
532 registry.register_language(DartEngine::new());
533 registry.register_language(SwiftEngine::new());
534 registry.register_language(PerlEngine::new());
535 registry.register_language(JuliaEngine::new());
536 registry.register_language(HaskellEngine::new());
537 registry.register_language(ElixirEngine::new());
538 registry.register_language(CrystalEngine::new());
539 registry.register_language(ZigEngine::new());
540 registry.register_language(NimEngine::new());
541
542 registry
543 }
544
545 pub fn register_language<E>(&mut self, engine: E)
546 where
547 E: LanguageEngine + Send + Sync + 'static,
548 {
549 let id = engine.id().to_string();
550 for alias in engine.aliases() {
551 self.alias_lookup
552 .insert(canonical_language_id(alias), id.clone());
553 }
554 self.alias_lookup
555 .insert(canonical_language_id(&id), id.clone());
556 self.engines.insert(id, Box::new(engine));
557 }
558
559 pub fn resolve(&self, spec: &LanguageSpec) -> Option<&(dyn LanguageEngine + Send + Sync)> {
560 let canonical = canonical_language_id(spec.canonical_id());
561 let target_id = self
562 .alias_lookup
563 .get(&canonical)
564 .cloned()
565 .unwrap_or(canonical);
566 self.engines
567 .get(&target_id)
568 .map(|engine| engine.as_ref() as _)
569 }
570
571 pub fn resolve_by_id(&self, id: &str) -> Option<&(dyn LanguageEngine + Send + Sync)> {
572 let canonical = canonical_language_id(id);
573 let target_id = self
574 .alias_lookup
575 .get(&canonical)
576 .cloned()
577 .unwrap_or(canonical);
578 self.engines
579 .get(&target_id)
580 .map(|engine| engine.as_ref() as _)
581 }
582
583 pub fn engines(&self) -> impl Iterator<Item = &(dyn LanguageEngine + Send + Sync)> {
584 self.engines.values().map(|engine| engine.as_ref() as _)
585 }
586
587 pub fn known_languages(&self) -> Vec<String> {
588 let mut ids: Vec<_> = self.engines.keys().cloned().collect();
589 ids.sort();
590 ids
591 }
592}
593
594pub fn package_install_command(
597 language_id: &str,
598) -> Option<(&'static str, &'static [&'static str])> {
599 match language_id {
600 "python" => Some(("pip", &["install"])),
601 "javascript" | "typescript" => Some(("npm", &["install"])),
602 "rust" => Some(("cargo", &["add"])),
603 "go" => Some(("go", &["get"])),
604 "ruby" => Some(("gem", &["install"])),
605 "php" => Some(("composer", &["require"])),
606 "lua" => Some(("luarocks", &["install"])),
607 "dart" => Some(("dart", &["pub", "add"])),
608 "perl" => Some(("cpanm", &[])),
609 "julia" => Some(("julia", &["-e"])), "haskell" => Some(("cabal", &["install"])),
611 "nim" => Some(("nimble", &["install"])),
612 "r" => Some(("Rscript", &["-e"])), "kotlin" => None, "java" => None, "c" | "cpp" => None, "bash" => None,
617 "swift" => None,
618 "crystal" => Some(("shards", &["install"])),
619 "elixir" => None, "groovy" => None,
621 "csharp" => Some(("dotnet", &["add", "package"])),
622 "zig" => None,
623 _ => None,
624 }
625}
626
627fn install_override_command(language_id: &str, package: &str) -> Option<std::process::Command> {
628 let key = format!("RUN_INSTALL_COMMAND_{}", language_id.to_ascii_uppercase());
629 let template = std::env::var(&key).ok()?;
630 let expanded = if template.contains("{package}") {
631 template.replace("{package}", package)
632 } else {
633 format!("{template} {package}")
634 };
635 let parts = shell_words::split(&expanded).ok()?;
636 if parts.is_empty() {
637 return None;
638 }
639 let mut cmd = std::process::Command::new(&parts[0]);
640 for arg in &parts[1..] {
641 cmd.arg(arg);
642 }
643 Some(cmd)
644}
645
646pub fn build_install_command(language_id: &str, package: &str) -> Option<std::process::Command> {
649 if let Some(cmd) = install_override_command(language_id, package) {
650 return Some(cmd);
651 }
652
653 if language_id == "python" {
654 let python = python::resolve_python_binary();
655 let mut cmd = std::process::Command::new(python);
656 cmd.arg("-m").arg("pip").arg("install").arg(package);
657 return Some(cmd);
658 }
659
660 let (binary, base_args) = package_install_command(language_id)?;
661
662 let mut cmd = std::process::Command::new(binary);
663
664 match language_id {
665 "julia" => {
666 cmd.arg("-e")
668 .arg(format!("using Pkg; Pkg.add(\"{package}\")"));
669 }
670 "r" => {
671 cmd.arg("-e").arg(format!(
673 "install.packages(\"{package}\", repos=\"https://cran.r-project.org\")"
674 ));
675 }
676 _ => {
677 for arg in base_args {
678 cmd.arg(arg);
679 }
680 cmd.arg(package);
681 }
682 }
683
684 Some(cmd)
685}
686
687pub fn default_language() -> &'static str {
688 "python"
689}
690
691pub fn ensure_known_language(spec: &LanguageSpec, registry: &LanguageRegistry) -> Result<()> {
692 if registry.resolve(spec).is_some() {
693 return Ok(());
694 }
695
696 let available = registry.known_languages();
697 bail!(
698 "Unknown language '{}'. Available languages: {}",
699 spec.canonical_id(),
700 available.join(", ")
701 )
702}
703
704pub fn detect_language_for_source(
705 source: &ExecutionPayload,
706 registry: &LanguageRegistry,
707) -> Option<LanguageSpec> {
708 if let Some(path) = source.as_file_path()
709 && let Some(ext) = path.extension().and_then(|e| e.to_str())
710 {
711 let ext_lower = ext.to_ascii_lowercase();
712 if let Some(lang) = extension_to_language(&ext_lower) {
713 let spec = LanguageSpec::new(lang);
714 if registry.resolve(&spec).is_some() {
715 return Some(spec);
716 }
717 }
718 }
719
720 if let Some(code) = source.as_inline()
721 && let Some(lang) = crate::detect::detect_language_from_snippet(code)
722 {
723 let spec = LanguageSpec::new(lang);
724 if registry.resolve(&spec).is_some() {
725 return Some(spec);
726 }
727 }
728
729 None
730}
731
732fn extension_to_language(ext: &str) -> Option<&'static str> {
733 match ext {
734 "py" | "pyw" => Some("python"),
735 "rs" => Some("rust"),
736 "go" => Some("go"),
737 "cs" => Some("csharp"),
738 "ts" | "tsx" => Some("typescript"),
739 "js" | "mjs" | "cjs" | "jsx" => Some("javascript"),
740 "rb" => Some("ruby"),
741 "lua" => Some("lua"),
742 "java" => Some("java"),
743 "groovy" => Some("groovy"),
744 "php" => Some("php"),
745 "kt" | "kts" => Some("kotlin"),
746 "c" => Some("c"),
747 "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some("cpp"),
748 "sh" | "bash" | "zsh" => Some("bash"),
749 "r" => Some("r"),
750 "dart" => Some("dart"),
751 "swift" => Some("swift"),
752 "perl" | "pl" | "pm" => Some("perl"),
753 "julia" | "jl" => Some("julia"),
754 "hs" => Some("haskell"),
755 "ex" | "exs" => Some("elixir"),
756 "cr" => Some("crystal"),
757 "zig" => Some("zig"),
758 "nim" => Some("nim"),
759 _ => None,
760 }
761}