Skip to main content

run/engine/
mod.rs

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::Mutex;
32use std::time::{Duration, Instant};
33
34use anyhow::{Context, Result, bail};
35
36use crate::cli::InputSource;
37use crate::language::{LanguageSpec, canonical_language_id};
38
39// ---------------------------------------------------------------------------
40// Compilation cache: hash source code -> reuse compiled binaries
41// ---------------------------------------------------------------------------
42
43use std::sync::LazyLock;
44
45static COMPILE_CACHE: LazyLock<Mutex<CompileCache>> =
46    LazyLock::new(|| Mutex::new(CompileCache::new()));
47
48struct CompileCache {
49    dir: PathBuf,
50    entries: HashMap<u64, PathBuf>,
51}
52
53impl CompileCache {
54    fn new() -> Self {
55        let dir = std::env::temp_dir().join("run-compile-cache");
56        let _ = std::fs::create_dir_all(&dir);
57        Self {
58            dir,
59            entries: HashMap::new(),
60        }
61    }
62
63    fn get(&self, hash: u64) -> Option<&PathBuf> {
64        self.entries.get(&hash).filter(|p| p.exists())
65    }
66
67    fn insert(&mut self, hash: u64, path: PathBuf) {
68        self.entries.insert(hash, path);
69    }
70
71    fn cache_dir(&self) -> &Path {
72        &self.dir
73    }
74}
75
76/// Hash source code for cache lookup.
77pub fn hash_source(source: &str) -> u64 {
78    // Simple FNV-1a hash — fast and good enough for cache keys.
79    let mut hash: u64 = 0xcbf29ce484222325;
80    for byte in source.as_bytes() {
81        hash ^= *byte as u64;
82        hash = hash.wrapping_mul(0x100000001b3);
83    }
84    hash
85}
86
87/// Look up a cached binary for the given source hash.
88/// Returns Some(path) if a valid cached binary exists.
89pub fn cache_lookup(source_hash: u64) -> Option<PathBuf> {
90    let cache = COMPILE_CACHE.lock().ok()?;
91    cache.get(source_hash).cloned()
92}
93
94/// Store a compiled binary in the cache. Copies the binary to the cache directory.
95pub fn cache_store(source_hash: u64, binary: &Path) -> Option<PathBuf> {
96    let mut cache = COMPILE_CACHE.lock().ok()?;
97    let suffix = std::env::consts::EXE_SUFFIX;
98    let cached_name = format!("{:016x}{}", source_hash, suffix);
99    let cached_path = cache.cache_dir().join(cached_name);
100    if std::fs::copy(binary, &cached_path).is_ok() {
101        // Ensure executable permission on Unix
102        #[cfg(unix)]
103        {
104            use std::os::unix::fs::PermissionsExt;
105            let _ = std::fs::set_permissions(&cached_path, std::fs::Permissions::from_mode(0o755));
106        }
107        cache.insert(source_hash, cached_path.clone());
108        Some(cached_path)
109    } else {
110        None
111    }
112}
113
114/// Execute a cached binary, returning the Output. Returns None if no cache entry.
115pub fn try_cached_execution(source_hash: u64) -> Option<std::process::Output> {
116    let cached = cache_lookup(source_hash)?;
117    let mut cmd = std::process::Command::new(&cached);
118    cmd.stdout(std::process::Stdio::piped())
119        .stderr(std::process::Stdio::piped())
120        .stdin(std::process::Stdio::inherit());
121    cmd.output().ok()
122}
123
124/// Default execution timeout: 60 seconds.
125/// Override with RUN_TIMEOUT_SECS env var.
126pub fn execution_timeout() -> Duration {
127    let secs = std::env::var("RUN_TIMEOUT_SECS")
128        .ok()
129        .and_then(|v| v.parse::<u64>().ok())
130        .unwrap_or(60);
131    Duration::from_secs(secs)
132}
133
134/// Wait for a child process with a timeout. Kills the process if it exceeds the limit.
135/// Returns the Output on success, or an error on timeout.
136pub fn wait_with_timeout(mut child: Child, timeout: Duration) -> Result<std::process::Output> {
137    let start = Instant::now();
138    let poll_interval = Duration::from_millis(50);
139
140    loop {
141        match child.try_wait() {
142            Ok(Some(_status)) => {
143                // Process finished — collect output
144                return child.wait_with_output().map_err(Into::into);
145            }
146            Ok(None) => {
147                if start.elapsed() > timeout {
148                    let _ = child.kill();
149                    let _ = child.wait(); // reap
150                    bail!(
151                        "Execution timed out after {:.1}s (limit: {}s). \
152                         Set RUN_TIMEOUT_SECS to increase.",
153                        start.elapsed().as_secs_f64(),
154                        timeout.as_secs()
155                    );
156                }
157                std::thread::sleep(poll_interval);
158            }
159            Err(e) => {
160                return Err(e.into());
161            }
162        }
163    }
164}
165
166pub use bash::BashEngine;
167pub use c::CEngine;
168pub use cpp::CppEngine;
169pub use crystal::CrystalEngine;
170pub use csharp::CSharpEngine;
171pub use dart::DartEngine;
172pub use elixir::ElixirEngine;
173pub use go::GoEngine;
174pub use groovy::GroovyEngine;
175pub use haskell::HaskellEngine;
176pub use java::JavaEngine;
177pub use javascript::JavascriptEngine;
178pub use julia::JuliaEngine;
179pub use kotlin::KotlinEngine;
180pub use lua::LuaEngine;
181pub use nim::NimEngine;
182pub use perl::PerlEngine;
183pub use php::PhpEngine;
184pub use python::PythonEngine;
185pub use r::REngine;
186pub use ruby::RubyEngine;
187pub use rust::RustEngine;
188pub use swift::SwiftEngine;
189pub use typescript::TypeScriptEngine;
190pub use zig::ZigEngine;
191
192pub trait LanguageSession {
193    fn language_id(&self) -> &str;
194    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome>;
195    fn shutdown(&mut self) -> Result<()>;
196}
197
198pub trait LanguageEngine {
199    fn id(&self) -> &'static str;
200    fn display_name(&self) -> &'static str {
201        self.id()
202    }
203    fn aliases(&self) -> &[&'static str] {
204        &[]
205    }
206    fn supports_sessions(&self) -> bool {
207        false
208    }
209    fn validate(&self) -> Result<()> {
210        Ok(())
211    }
212    fn toolchain_version(&self) -> Result<Option<String>> {
213        Ok(None)
214    }
215    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome>;
216    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
217        bail!("{} does not support interactive sessions yet", self.id())
218    }
219}
220
221pub(crate) fn version_line_from_output(output: &Output) -> Option<String> {
222    let stdout = String::from_utf8_lossy(&output.stdout);
223    for line in stdout.lines() {
224        let trimmed = line.trim();
225        if !trimmed.is_empty() {
226            return Some(trimmed.to_string());
227        }
228    }
229    let stderr = String::from_utf8_lossy(&output.stderr);
230    for line in stderr.lines() {
231        let trimmed = line.trim();
232        if !trimmed.is_empty() {
233            return Some(trimmed.to_string());
234        }
235    }
236    None
237}
238
239pub(crate) fn run_version_command(mut cmd: Command, context: &str) -> Result<Option<String>> {
240    cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
241    let output = cmd
242        .output()
243        .with_context(|| format!("failed to invoke {context}"))?;
244    let version = version_line_from_output(&output);
245    if output.status.success() || version.is_some() {
246        Ok(version)
247    } else {
248        bail!("{context} exited with status {}", output.status);
249    }
250}
251
252#[derive(Debug, Clone, PartialEq, Eq)]
253pub enum ExecutionPayload {
254    Inline {
255        code: String,
256        args: Vec<String>,
257    },
258    File {
259        path: std::path::PathBuf,
260        args: Vec<String>,
261    },
262    Stdin {
263        code: String,
264        args: Vec<String>,
265    },
266}
267
268impl ExecutionPayload {
269    pub fn from_input_source(source: &InputSource, args: &[String]) -> Result<Self> {
270        let args = args.to_vec();
271        match source {
272            InputSource::Inline(code) => Ok(Self::Inline {
273                code: normalize_inline_code(code).into_owned(),
274                args,
275            }),
276            InputSource::File(path) => Ok(Self::File {
277                path: path.clone(),
278                args,
279            }),
280            InputSource::Stdin => {
281                use std::io::Read;
282                let mut buffer = String::new();
283                std::io::stdin().read_to_string(&mut buffer)?;
284                Ok(Self::Stdin { code: buffer, args })
285            }
286        }
287    }
288
289    pub fn as_inline(&self) -> Option<&str> {
290        match self {
291            ExecutionPayload::Inline { code, .. } => Some(code.as_str()),
292            ExecutionPayload::Stdin { code, .. } => Some(code.as_str()),
293            ExecutionPayload::File { .. } => None,
294        }
295    }
296
297    pub fn as_file_path(&self) -> Option<&Path> {
298        match self {
299            ExecutionPayload::File { path, .. } => Some(path.as_path()),
300            _ => None,
301        }
302    }
303
304    pub fn args(&self) -> &[String] {
305        match self {
306            ExecutionPayload::Inline { args, .. } => args.as_slice(),
307            ExecutionPayload::File { args, .. } => args.as_slice(),
308            ExecutionPayload::Stdin { args, .. } => args.as_slice(),
309        }
310    }
311}
312
313fn normalize_inline_code(code: &str) -> Cow<'_, str> {
314    if !code.contains('\\') {
315        return Cow::Borrowed(code);
316    }
317
318    let mut result = String::with_capacity(code.len());
319    let mut chars = code.chars().peekable();
320    let mut in_single = false;
321    let mut in_double = false;
322    let mut escape_in_quote = false;
323
324    while let Some(ch) = chars.next() {
325        if in_single {
326            result.push(ch);
327            if escape_in_quote {
328                escape_in_quote = false;
329            } else if ch == '\\' {
330                escape_in_quote = true;
331            } else if ch == '\'' {
332                in_single = false;
333            }
334            continue;
335        }
336
337        if in_double {
338            result.push(ch);
339            if escape_in_quote {
340                escape_in_quote = false;
341            } else if ch == '\\' {
342                escape_in_quote = true;
343            } else if ch == '"' {
344                in_double = false;
345            }
346            continue;
347        }
348
349        match ch {
350            '\'' => {
351                in_single = true;
352                result.push(ch);
353            }
354            '"' => {
355                in_double = true;
356                result.push(ch);
357            }
358            '\\' => match chars.next() {
359                Some('n') => result.push('\n'),
360                Some('r') => result.push('\r'),
361                Some('t') => result.push('\t'),
362                Some('\\') => result.push('\\'),
363                Some(other) => {
364                    result.push('\\');
365                    result.push(other);
366                }
367                None => result.push('\\'),
368            },
369            _ => result.push(ch),
370        }
371    }
372
373    Cow::Owned(result)
374}
375
376#[derive(Debug, Clone, PartialEq, Eq)]
377pub struct ExecutionOutcome {
378    pub language: String,
379    pub exit_code: Option<i32>,
380    pub stdout: String,
381    pub stderr: String,
382    pub duration: Duration,
383}
384
385impl ExecutionOutcome {
386    pub fn success(&self) -> bool {
387        match self.exit_code {
388            Some(code) => code == 0,
389            None => self.stderr.trim().is_empty(),
390        }
391    }
392}
393
394pub struct LanguageRegistry {
395    engines: HashMap<String, Box<dyn LanguageEngine + Send + Sync>>, // keyed by canonical id
396    alias_lookup: HashMap<String, String>,
397}
398
399impl LanguageRegistry {
400    pub fn bootstrap() -> Self {
401        let mut registry = Self {
402            engines: HashMap::new(),
403            alias_lookup: HashMap::new(),
404        };
405
406        registry.register_language(PythonEngine::new());
407        registry.register_language(BashEngine::new());
408        registry.register_language(JavascriptEngine::new());
409        registry.register_language(RubyEngine::new());
410        registry.register_language(RustEngine::new());
411        registry.register_language(GoEngine::new());
412        registry.register_language(CSharpEngine::new());
413        registry.register_language(TypeScriptEngine::new());
414        registry.register_language(LuaEngine::new());
415        registry.register_language(JavaEngine::new());
416        registry.register_language(GroovyEngine::new());
417        registry.register_language(PhpEngine::new());
418        registry.register_language(KotlinEngine::new());
419        registry.register_language(CEngine::new());
420        registry.register_language(CppEngine::new());
421        registry.register_language(REngine::new());
422        registry.register_language(DartEngine::new());
423        registry.register_language(SwiftEngine::new());
424        registry.register_language(PerlEngine::new());
425        registry.register_language(JuliaEngine::new());
426        registry.register_language(HaskellEngine::new());
427        registry.register_language(ElixirEngine::new());
428        registry.register_language(CrystalEngine::new());
429        registry.register_language(ZigEngine::new());
430        registry.register_language(NimEngine::new());
431
432        registry
433    }
434
435    pub fn register_language<E>(&mut self, engine: E)
436    where
437        E: LanguageEngine + Send + Sync + 'static,
438    {
439        let id = engine.id().to_string();
440        for alias in engine.aliases() {
441            self.alias_lookup
442                .insert(canonical_language_id(alias), id.clone());
443        }
444        self.alias_lookup
445            .insert(canonical_language_id(&id), id.clone());
446        self.engines.insert(id, Box::new(engine));
447    }
448
449    pub fn resolve(&self, spec: &LanguageSpec) -> Option<&(dyn LanguageEngine + Send + Sync)> {
450        let canonical = canonical_language_id(spec.canonical_id());
451        let target_id = self
452            .alias_lookup
453            .get(&canonical)
454            .cloned()
455            .unwrap_or(canonical);
456        self.engines
457            .get(&target_id)
458            .map(|engine| engine.as_ref() as _)
459    }
460
461    pub fn resolve_by_id(&self, id: &str) -> Option<&(dyn LanguageEngine + Send + Sync)> {
462        let canonical = canonical_language_id(id);
463        let target_id = self
464            .alias_lookup
465            .get(&canonical)
466            .cloned()
467            .unwrap_or(canonical);
468        self.engines
469            .get(&target_id)
470            .map(|engine| engine.as_ref() as _)
471    }
472
473    pub fn engines(&self) -> impl Iterator<Item = &(dyn LanguageEngine + Send + Sync)> {
474        self.engines.values().map(|engine| engine.as_ref() as _)
475    }
476
477    pub fn known_languages(&self) -> Vec<String> {
478        let mut ids: Vec<_> = self.engines.keys().cloned().collect();
479        ids.sort();
480        ids
481    }
482}
483
484/// Returns the package install command for a language, if one exists.
485/// Returns (binary, args_before_package) so the caller can append the package name.
486pub fn package_install_command(
487    language_id: &str,
488) -> Option<(&'static str, &'static [&'static str])> {
489    match language_id {
490        "python" => Some(("pip", &["install"])),
491        "javascript" | "typescript" => Some(("npm", &["install"])),
492        "rust" => Some(("cargo", &["add"])),
493        "go" => Some(("go", &["get"])),
494        "ruby" => Some(("gem", &["install"])),
495        "php" => Some(("composer", &["require"])),
496        "lua" => Some(("luarocks", &["install"])),
497        "dart" => Some(("dart", &["pub", "add"])),
498        "perl" => Some(("cpanm", &[])),
499        "julia" => Some(("julia", &["-e"])), // special: wraps in Pkg.add()
500        "haskell" => Some(("cabal", &["install"])),
501        "nim" => Some(("nimble", &["install"])),
502        "r" => Some(("Rscript", &["-e"])), // special: wraps in install.packages()
503        "kotlin" => None,                  // no standard CLI package manager
504        "java" => None,                    // maven/gradle are project-based
505        "c" | "cpp" => None,               // system packages
506        "bash" => None,
507        "swift" => None,
508        "crystal" => Some(("shards", &["install"])),
509        "elixir" => None, // mix deps.get is project-based
510        "groovy" => None,
511        "csharp" => Some(("dotnet", &["add", "package"])),
512        "zig" => None,
513        _ => None,
514    }
515}
516
517fn install_override_command(language_id: &str, package: &str) -> Option<std::process::Command> {
518    let key = format!("RUN_INSTALL_COMMAND_{}", language_id.to_ascii_uppercase());
519    let template = std::env::var(&key).ok()?;
520    let expanded = if template.contains("{package}") {
521        template.replace("{package}", package)
522    } else {
523        format!("{template} {package}")
524    };
525    let parts = shell_words::split(&expanded).ok()?;
526    if parts.is_empty() {
527        return None;
528    }
529    let mut cmd = std::process::Command::new(&parts[0]);
530    for arg in &parts[1..] {
531        cmd.arg(arg);
532    }
533    Some(cmd)
534}
535
536/// Build a full install command for a package in the given language.
537/// Returns None if the language has no package manager.
538pub fn build_install_command(language_id: &str, package: &str) -> Option<std::process::Command> {
539    if let Some(cmd) = install_override_command(language_id, package) {
540        return Some(cmd);
541    }
542
543    if language_id == "python" {
544        let python = python::resolve_python_binary();
545        let mut cmd = std::process::Command::new(python);
546        cmd.arg("-m").arg("pip").arg("install").arg(package);
547        return Some(cmd);
548    }
549
550    let (binary, base_args) = package_install_command(language_id)?;
551
552    let mut cmd = std::process::Command::new(binary);
553
554    match language_id {
555        "julia" => {
556            // julia -e 'using Pkg; Pkg.add("package")'
557            cmd.arg("-e")
558                .arg(format!("using Pkg; Pkg.add(\"{package}\")"));
559        }
560        "r" => {
561            // Rscript -e 'install.packages("package", repos="https://cran.r-project.org")'
562            cmd.arg("-e").arg(format!(
563                "install.packages(\"{package}\", repos=\"https://cran.r-project.org\")"
564            ));
565        }
566        _ => {
567            for arg in base_args {
568                cmd.arg(arg);
569            }
570            cmd.arg(package);
571        }
572    }
573
574    Some(cmd)
575}
576
577pub fn default_language() -> &'static str {
578    "python"
579}
580
581pub fn ensure_known_language(spec: &LanguageSpec, registry: &LanguageRegistry) -> Result<()> {
582    if registry.resolve(spec).is_some() {
583        return Ok(());
584    }
585
586    let available = registry.known_languages();
587    bail!(
588        "Unknown language '{}'. Available languages: {}",
589        spec.canonical_id(),
590        available.join(", ")
591    )
592}
593
594pub fn detect_language_for_source(
595    source: &ExecutionPayload,
596    registry: &LanguageRegistry,
597) -> Option<LanguageSpec> {
598    if let Some(path) = source.as_file_path()
599        && let Some(ext) = path.extension().and_then(|e| e.to_str())
600    {
601        let ext_lower = ext.to_ascii_lowercase();
602        if let Some(lang) = extension_to_language(&ext_lower) {
603            let spec = LanguageSpec::new(lang);
604            if registry.resolve(&spec).is_some() {
605                return Some(spec);
606            }
607        }
608    }
609
610    if let Some(code) = source.as_inline()
611        && let Some(lang) = crate::detect::detect_language_from_snippet(code)
612    {
613        let spec = LanguageSpec::new(lang);
614        if registry.resolve(&spec).is_some() {
615            return Some(spec);
616        }
617    }
618
619    None
620}
621
622fn extension_to_language(ext: &str) -> Option<&'static str> {
623    match ext {
624        "py" | "pyw" => Some("python"),
625        "rs" => Some("rust"),
626        "go" => Some("go"),
627        "cs" => Some("csharp"),
628        "ts" | "tsx" => Some("typescript"),
629        "js" | "mjs" | "cjs" | "jsx" => Some("javascript"),
630        "rb" => Some("ruby"),
631        "lua" => Some("lua"),
632        "java" => Some("java"),
633        "groovy" => Some("groovy"),
634        "php" => Some("php"),
635        "kt" | "kts" => Some("kotlin"),
636        "c" => Some("c"),
637        "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some("cpp"),
638        "sh" | "bash" | "zsh" => Some("bash"),
639        "r" => Some("r"),
640        "dart" => Some("dart"),
641        "swift" => Some("swift"),
642        "perl" | "pl" | "pm" => Some("perl"),
643        "julia" | "jl" => Some("julia"),
644        "hs" => Some("haskell"),
645        "ex" | "exs" => Some("elixir"),
646        "cr" => Some("crystal"),
647        "zig" => Some("zig"),
648        "nim" => Some("nim"),
649        _ => None,
650    }
651}