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