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::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
40// ---------------------------------------------------------------------------
41// Compilation cache: hash source code -> reuse compiled binaries
42// ---------------------------------------------------------------------------
43
44static 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
49/// Hash source code for cache lookup.
50pub 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
90/// Look up a cached binary for the given language namespace + source hash.
91/// Returns Some(path) if a valid cached binary exists.
92pub fn cache_lookup(namespace: &str, source_hash: u64) -> Option<PathBuf> {
93    crate::cache::lookup(namespace, source_hash)
94}
95
96/// Store a compiled binary in the cache. Copies the binary to the cache directory.
97pub fn cache_store(namespace: &str, source_hash: u64, binary: &Path) -> Option<PathBuf> {
98    crate::cache::store(namespace, source_hash, binary)
99}
100
101/// Execute a cached binary, returning the Output. Returns None if no cache entry.
102pub 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
111/// Build a compiler command with optional daemon/cache wrappers.
112///
113/// Behavior controlled by RUN_COMPILER_DAEMON:
114/// - off: use raw compiler
115/// - ccache: force ccache wrapper
116/// - sccache: force sccache wrapper
117/// - auto (default): prefer sccache, then ccache, else raw compiler
118pub 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
211/// Return the configured execution timeout.
212///
213/// Zero means no timeout. `RUN_TIMEOUT_SECS` overrides config and CLI settings.
214pub fn execution_timeout() -> Duration {
215    let secs = crate::runtime::timeout_secs();
216    Duration::from_secs(secs)
217}
218
219/// Wait for a child process with a timeout. Kills the process if it exceeds the limit.
220/// Returns the Output on success, or an error on timeout.
221pub 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(); // reap
234                    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
271/// Stateful interactive execution context for one language engine.
272pub 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
278/// Runtime adapter for a supported programming language.
279///
280/// Implementors validate a real local toolchain, execute one-shot payloads,
281/// and optionally create a persistent [`LanguageSession`] for REPL use.
282///
283/// # Example
284///
285/// ```ignore
286/// use anyhow::Result;
287/// use run::engine::{ExecutionOutcome, ExecutionPayload, LanguageEngine};
288///
289/// struct ToyEngine;
290///
291/// impl LanguageEngine for ToyEngine {
292///     fn id(&self) -> &'static str { "toy" }
293///     fn display_name(&self) -> &'static str { "Toy" }
294///
295///     fn validate(&self) -> Result<()> {
296///         // Check that the interpreter/compiler exists and is runnable.
297///         Ok(())
298///     }
299///
300///     fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
301///         // Materialize payload input, spawn the toolchain, capture stdout and stderr,
302///         // and return the process exit code and elapsed time.
303///         # let _ = payload;
304///         # unimplemented!()
305///     }
306/// }
307/// ```
308pub 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>>, // keyed by canonical id
506    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
594/// Returns the package install command for a language, if one exists.
595/// Returns (binary, args_before_package) so the caller can append the package name.
596pub 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"])), // special: wraps in Pkg.add()
610        "haskell" => Some(("cabal", &["install"])),
611        "nim" => Some(("nimble", &["install"])),
612        "r" => Some(("Rscript", &["-e"])), // special: wraps in install.packages()
613        "kotlin" => None,                  // no standard CLI package manager
614        "java" => None,                    // maven/gradle are project-based
615        "c" | "cpp" => None,               // system packages
616        "bash" => None,
617        "swift" => None,
618        "crystal" => Some(("shards", &["install"])),
619        "elixir" => None, // mix deps.get is project-based
620        "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
646/// Build a full install command for a package in the given language.
647/// Returns None if the language has no package manager.
648pub 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            // julia -e 'using Pkg; Pkg.add("package")'
667            cmd.arg("-e")
668                .arg(format!("using Pkg; Pkg.add(\"{package}\")"));
669        }
670        "r" => {
671            // Rscript -e 'install.packages("package", repos="https://cran.r-project.org")'
672            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}