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;
31use std::sync::Mutex;
32use std::time::{Duration, Instant};
33
34use anyhow::{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(
137    mut child: Child,
138    timeout: Duration,
139) -> Result<std::process::Output> {
140    let start = Instant::now();
141    let poll_interval = Duration::from_millis(50);
142
143    loop {
144        match child.try_wait() {
145            Ok(Some(_status)) => {
146                // Process finished — collect output
147                return child.wait_with_output().map_err(Into::into);
148            }
149            Ok(None) => {
150                if start.elapsed() > timeout {
151                    let _ = child.kill();
152                    let _ = child.wait(); // reap
153                    bail!(
154                        "Execution timed out after {:.1}s (limit: {}s). \
155                         Set RUN_TIMEOUT_SECS to increase.",
156                        start.elapsed().as_secs_f64(),
157                        timeout.as_secs()
158                    );
159                }
160                std::thread::sleep(poll_interval);
161            }
162            Err(e) => {
163                return Err(e.into());
164            }
165        }
166    }
167}
168
169pub use bash::BashEngine;
170pub use c::CEngine;
171pub use cpp::CppEngine;
172pub use crystal::CrystalEngine;
173pub use csharp::CSharpEngine;
174pub use dart::DartEngine;
175pub use elixir::ElixirEngine;
176pub use go::GoEngine;
177pub use groovy::GroovyEngine;
178pub use haskell::HaskellEngine;
179pub use java::JavaEngine;
180pub use javascript::JavascriptEngine;
181pub use julia::JuliaEngine;
182pub use kotlin::KotlinEngine;
183pub use lua::LuaEngine;
184pub use nim::NimEngine;
185pub use perl::PerlEngine;
186pub use php::PhpEngine;
187pub use python::PythonEngine;
188pub use r::REngine;
189pub use ruby::RubyEngine;
190pub use rust::RustEngine;
191pub use swift::SwiftEngine;
192pub use typescript::TypeScriptEngine;
193pub use zig::ZigEngine;
194
195pub trait LanguageSession {
196    fn language_id(&self) -> &str;
197    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome>;
198    fn shutdown(&mut self) -> Result<()>;
199}
200
201pub trait LanguageEngine {
202    fn id(&self) -> &'static str;
203    fn display_name(&self) -> &'static str {
204        self.id()
205    }
206    fn aliases(&self) -> &[&'static str] {
207        &[]
208    }
209    fn supports_sessions(&self) -> bool {
210        false
211    }
212    fn validate(&self) -> Result<()> {
213        Ok(())
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
221#[derive(Debug, Clone, PartialEq, Eq)]
222pub enum ExecutionPayload {
223    Inline { code: String },
224    File { path: std::path::PathBuf },
225    Stdin { code: String },
226}
227
228impl ExecutionPayload {
229    pub fn from_input_source(source: &InputSource) -> Result<Self> {
230        match source {
231            InputSource::Inline(code) => Ok(Self::Inline {
232                code: normalize_inline_code(code).into_owned(),
233            }),
234            InputSource::File(path) => Ok(Self::File { path: path.clone() }),
235            InputSource::Stdin => {
236                use std::io::Read;
237                let mut buffer = String::new();
238                std::io::stdin().read_to_string(&mut buffer)?;
239                Ok(Self::Stdin { code: buffer })
240            }
241        }
242    }
243
244    pub fn as_inline(&self) -> Option<&str> {
245        match self {
246            ExecutionPayload::Inline { code } => Some(code.as_str()),
247            ExecutionPayload::Stdin { code } => Some(code.as_str()),
248            ExecutionPayload::File { .. } => None,
249        }
250    }
251
252    pub fn as_file_path(&self) -> Option<&Path> {
253        match self {
254            ExecutionPayload::File { path } => Some(path.as_path()),
255            _ => None,
256        }
257    }
258}
259
260fn normalize_inline_code(code: &str) -> Cow<'_, str> {
261    if !code.contains('\\') {
262        return Cow::Borrowed(code);
263    }
264
265    let mut result = String::with_capacity(code.len());
266    let mut chars = code.chars().peekable();
267    let mut in_single = false;
268    let mut in_double = false;
269    let mut escape_in_quote = false;
270
271    while let Some(ch) = chars.next() {
272        if in_single {
273            result.push(ch);
274            if escape_in_quote {
275                escape_in_quote = false;
276            } else if ch == '\\' {
277                escape_in_quote = true;
278            } else if ch == '\'' {
279                in_single = false;
280            }
281            continue;
282        }
283
284        if in_double {
285            result.push(ch);
286            if escape_in_quote {
287                escape_in_quote = false;
288            } else if ch == '\\' {
289                escape_in_quote = true;
290            } else if ch == '"' {
291                in_double = false;
292            }
293            continue;
294        }
295
296        match ch {
297            '\'' => {
298                in_single = true;
299                result.push(ch);
300            }
301            '"' => {
302                in_double = true;
303                result.push(ch);
304            }
305            '\\' => match chars.next() {
306                Some('n') => result.push('\n'),
307                Some('r') => result.push('\r'),
308                Some('t') => result.push('\t'),
309                Some('\\') => result.push('\\'),
310                Some(other) => {
311                    result.push('\\');
312                    result.push(other);
313                }
314                None => result.push('\\'),
315            },
316            _ => result.push(ch),
317        }
318    }
319
320    Cow::Owned(result)
321}
322
323#[derive(Debug, Clone, PartialEq, Eq)]
324pub struct ExecutionOutcome {
325    pub language: String,
326    pub exit_code: Option<i32>,
327    pub stdout: String,
328    pub stderr: String,
329    pub duration: Duration,
330}
331
332impl ExecutionOutcome {
333    pub fn success(&self) -> bool {
334        match self.exit_code {
335            Some(code) => code == 0,
336            None => self.stderr.trim().is_empty(),
337        }
338    }
339}
340
341pub struct LanguageRegistry {
342    engines: HashMap<String, Box<dyn LanguageEngine + Send + Sync>>, // keyed by canonical id
343    alias_lookup: HashMap<String, String>,
344}
345
346impl LanguageRegistry {
347    pub fn bootstrap() -> Self {
348        let mut registry = Self {
349            engines: HashMap::new(),
350            alias_lookup: HashMap::new(),
351        };
352
353        registry.register_language(PythonEngine::new());
354        registry.register_language(BashEngine::new());
355        registry.register_language(JavascriptEngine::new());
356        registry.register_language(RubyEngine::new());
357        registry.register_language(RustEngine::new());
358        registry.register_language(GoEngine::new());
359        registry.register_language(CSharpEngine::new());
360        registry.register_language(TypeScriptEngine::new());
361        registry.register_language(LuaEngine::new());
362        registry.register_language(JavaEngine::new());
363        registry.register_language(GroovyEngine::new());
364        registry.register_language(PhpEngine::new());
365        registry.register_language(KotlinEngine::new());
366        registry.register_language(CEngine::new());
367        registry.register_language(CppEngine::new());
368        registry.register_language(REngine::new());
369        registry.register_language(DartEngine::new());
370        registry.register_language(SwiftEngine::new());
371        registry.register_language(PerlEngine::new());
372        registry.register_language(JuliaEngine::new());
373        registry.register_language(HaskellEngine::new());
374        registry.register_language(ElixirEngine::new());
375        registry.register_language(CrystalEngine::new());
376        registry.register_language(ZigEngine::new());
377        registry.register_language(NimEngine::new());
378
379        registry
380    }
381
382    pub fn register_language<E>(&mut self, engine: E)
383    where
384        E: LanguageEngine + Send + Sync + 'static,
385    {
386        let id = engine.id().to_string();
387        for alias in engine.aliases() {
388            self.alias_lookup
389                .insert(canonical_language_id(alias), id.clone());
390        }
391        self.alias_lookup
392            .insert(canonical_language_id(&id), id.clone());
393        self.engines.insert(id, Box::new(engine));
394    }
395
396    pub fn resolve(&self, spec: &LanguageSpec) -> Option<&(dyn LanguageEngine + Send + Sync)> {
397        let canonical = canonical_language_id(spec.canonical_id());
398        let target_id = self
399            .alias_lookup
400            .get(&canonical)
401            .cloned()
402            .unwrap_or_else(|| canonical);
403        self.engines
404            .get(&target_id)
405            .map(|engine| engine.as_ref() as _)
406    }
407
408    pub fn resolve_by_id(&self, id: &str) -> Option<&(dyn LanguageEngine + Send + Sync)> {
409        let canonical = canonical_language_id(id);
410        let target_id = self
411            .alias_lookup
412            .get(&canonical)
413            .cloned()
414            .unwrap_or_else(|| canonical);
415        self.engines
416            .get(&target_id)
417            .map(|engine| engine.as_ref() as _)
418    }
419
420    pub fn engines(&self) -> impl Iterator<Item = &(dyn LanguageEngine + Send + Sync)> {
421        self.engines.values().map(|engine| engine.as_ref() as _)
422    }
423
424    pub fn known_languages(&self) -> Vec<String> {
425        let mut ids: Vec<_> = self.engines.keys().cloned().collect();
426        ids.sort();
427        ids
428    }
429}
430
431/// Returns the package install command for a language, if one exists.
432/// Returns (binary, args_before_package) so the caller can append the package name.
433pub fn package_install_command(language_id: &str) -> Option<(&'static str, &'static [&'static str])> {
434    match language_id {
435        "python" => Some(("pip", &["install"])),
436        "javascript" | "typescript" => Some(("npm", &["install"])),
437        "rust" => Some(("cargo", &["add"])),
438        "go" => Some(("go", &["get"])),
439        "ruby" => Some(("gem", &["install"])),
440        "php" => Some(("composer", &["require"])),
441        "lua" => Some(("luarocks", &["install"])),
442        "dart" => Some(("dart", &["pub", "add"])),
443        "perl" => Some(("cpanm", &[])),
444        "julia" => Some(("julia", &["-e"])),  // special: wraps in Pkg.add()
445        "haskell" => Some(("cabal", &["install"])),
446        "nim" => Some(("nimble", &["install"])),
447        "r" => Some(("Rscript", &["-e"])),  // special: wraps in install.packages()
448        "kotlin" => None, // no standard CLI package manager
449        "java" => None,   // maven/gradle are project-based
450        "c" | "cpp" => None, // system packages
451        "bash" => None,
452        "swift" => None,
453        "crystal" => Some(("shards", &["install"])),
454        "elixir" => None, // mix deps.get is project-based
455        "groovy" => None,
456        "csharp" => Some(("dotnet", &["add", "package"])),
457        "zig" => None,
458        _ => None,
459    }
460}
461
462/// Build a full install command for a package in the given language.
463/// Returns None if the language has no package manager.
464pub fn build_install_command(language_id: &str, package: &str) -> Option<std::process::Command> {
465    let (binary, base_args) = package_install_command(language_id)?;
466
467    let mut cmd = std::process::Command::new(binary);
468
469    match language_id {
470        "julia" => {
471            // julia -e 'using Pkg; Pkg.add("package")'
472            cmd.arg("-e").arg(format!("using Pkg; Pkg.add(\"{package}\")"));
473        }
474        "r" => {
475            // Rscript -e 'install.packages("package", repos="https://cran.r-project.org")'
476            cmd.arg("-e").arg(format!(
477                "install.packages(\"{package}\", repos=\"https://cran.r-project.org\")"
478            ));
479        }
480        _ => {
481            for arg in base_args {
482                cmd.arg(arg);
483            }
484            cmd.arg(package);
485        }
486    }
487
488    Some(cmd)
489}
490
491pub fn default_language() -> &'static str {
492    "python"
493}
494
495pub fn ensure_known_language(spec: &LanguageSpec, registry: &LanguageRegistry) -> Result<()> {
496    if registry.resolve(spec).is_some() {
497        return Ok(());
498    }
499
500    let available = registry.known_languages();
501    bail!(
502        "Unknown language '{}'. Available languages: {}",
503        spec.canonical_id(),
504        available.join(", ")
505    )
506}
507
508pub fn detect_language_for_source(
509    source: &ExecutionPayload,
510    registry: &LanguageRegistry,
511) -> Option<LanguageSpec> {
512    if let Some(path) = source.as_file_path() {
513        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
514            let ext_lower = ext.to_ascii_lowercase();
515            if let Some(lang) = extension_to_language(&ext_lower) {
516                let spec = LanguageSpec::new(lang);
517                if registry.resolve(&spec).is_some() {
518                    return Some(spec);
519                }
520            }
521        }
522    }
523
524    if let Some(code) = source.as_inline() {
525        if let Some(lang) = crate::detect::detect_language_from_snippet(code) {
526            let spec = LanguageSpec::new(lang);
527            if registry.resolve(&spec).is_some() {
528                return Some(spec);
529            }
530        }
531    }
532
533    None
534}
535
536fn extension_to_language(ext: &str) -> Option<&'static str> {
537    match ext {
538        "py" | "pyw" => Some("python"),
539        "rs" => Some("rust"),
540        "go" => Some("go"),
541        "cs" => Some("csharp"),
542        "ts" | "tsx" => Some("typescript"),
543        "js" | "mjs" | "cjs" | "jsx" => Some("javascript"),
544        "rb" => Some("ruby"),
545        "lua" => Some("lua"),
546        "java" => Some("java"),
547        "groovy" => Some("groovy"),
548        "php" => Some("php"),
549        "kt" | "kts" => Some("kotlin"),
550        "c" => Some("c"),
551        "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some("cpp"),
552        "sh" | "bash" | "zsh" => Some("bash"),
553        "r" => Some("r"),
554        "dart" => Some("dart"),
555        "swift" => Some("swift"),
556        "perl" | "pl" | "pm" => Some("perl"),
557        "julia" | "jl" => Some("julia"),
558        "hs" => Some("haskell"),
559        "ex" | "exs" => Some("elixir"),
560        "cr" => Some("crystal"),
561        "zig" => Some("zig"),
562        "nim" => Some("nim"),
563        _ => None,
564    }
565}