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
39use 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
76pub fn hash_source(source: &str) -> u64 {
78 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
87pub fn cache_lookup(source_hash: u64) -> Option<PathBuf> {
90 let cache = COMPILE_CACHE.lock().ok()?;
91 cache.get(source_hash).cloned()
92}
93
94pub 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 #[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
114pub 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
124pub 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
134pub 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 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(); 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 execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome>;
213 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
214 bail!("{} does not support interactive sessions yet", self.id())
215 }
216}
217
218#[derive(Debug, Clone, PartialEq, Eq)]
219pub enum ExecutionPayload {
220 Inline { code: String },
221 File { path: std::path::PathBuf },
222 Stdin { code: String },
223}
224
225impl ExecutionPayload {
226 pub fn from_input_source(source: &InputSource) -> Result<Self> {
227 match source {
228 InputSource::Inline(code) => Ok(Self::Inline {
229 code: normalize_inline_code(code).into_owned(),
230 }),
231 InputSource::File(path) => Ok(Self::File { path: path.clone() }),
232 InputSource::Stdin => {
233 use std::io::Read;
234 let mut buffer = String::new();
235 std::io::stdin().read_to_string(&mut buffer)?;
236 Ok(Self::Stdin { code: buffer })
237 }
238 }
239 }
240
241 pub fn as_inline(&self) -> Option<&str> {
242 match self {
243 ExecutionPayload::Inline { code } => Some(code.as_str()),
244 ExecutionPayload::Stdin { code } => Some(code.as_str()),
245 ExecutionPayload::File { .. } => None,
246 }
247 }
248
249 pub fn as_file_path(&self) -> Option<&Path> {
250 match self {
251 ExecutionPayload::File { path } => Some(path.as_path()),
252 _ => None,
253 }
254 }
255}
256
257fn normalize_inline_code(code: &str) -> Cow<'_, str> {
258 if !code.contains('\\') {
259 return Cow::Borrowed(code);
260 }
261
262 let mut result = String::with_capacity(code.len());
263 let mut chars = code.chars().peekable();
264 let mut in_single = false;
265 let mut in_double = false;
266 let mut escape_in_quote = false;
267
268 while let Some(ch) = chars.next() {
269 if in_single {
270 result.push(ch);
271 if escape_in_quote {
272 escape_in_quote = false;
273 } else if ch == '\\' {
274 escape_in_quote = true;
275 } else if ch == '\'' {
276 in_single = false;
277 }
278 continue;
279 }
280
281 if in_double {
282 result.push(ch);
283 if escape_in_quote {
284 escape_in_quote = false;
285 } else if ch == '\\' {
286 escape_in_quote = true;
287 } else if ch == '"' {
288 in_double = false;
289 }
290 continue;
291 }
292
293 match ch {
294 '\'' => {
295 in_single = true;
296 result.push(ch);
297 }
298 '"' => {
299 in_double = true;
300 result.push(ch);
301 }
302 '\\' => match chars.next() {
303 Some('n') => result.push('\n'),
304 Some('r') => result.push('\r'),
305 Some('t') => result.push('\t'),
306 Some('\\') => result.push('\\'),
307 Some(other) => {
308 result.push('\\');
309 result.push(other);
310 }
311 None => result.push('\\'),
312 },
313 _ => result.push(ch),
314 }
315 }
316
317 Cow::Owned(result)
318}
319
320#[derive(Debug, Clone, PartialEq, Eq)]
321pub struct ExecutionOutcome {
322 pub language: String,
323 pub exit_code: Option<i32>,
324 pub stdout: String,
325 pub stderr: String,
326 pub duration: Duration,
327}
328
329impl ExecutionOutcome {
330 pub fn success(&self) -> bool {
331 match self.exit_code {
332 Some(code) => code == 0,
333 None => self.stderr.trim().is_empty(),
334 }
335 }
336}
337
338pub struct LanguageRegistry {
339 engines: HashMap<String, Box<dyn LanguageEngine + Send + Sync>>, alias_lookup: HashMap<String, String>,
341}
342
343impl LanguageRegistry {
344 pub fn bootstrap() -> Self {
345 let mut registry = Self {
346 engines: HashMap::new(),
347 alias_lookup: HashMap::new(),
348 };
349
350 registry.register_language(PythonEngine::new());
351 registry.register_language(BashEngine::new());
352 registry.register_language(JavascriptEngine::new());
353 registry.register_language(RubyEngine::new());
354 registry.register_language(RustEngine::new());
355 registry.register_language(GoEngine::new());
356 registry.register_language(CSharpEngine::new());
357 registry.register_language(TypeScriptEngine::new());
358 registry.register_language(LuaEngine::new());
359 registry.register_language(JavaEngine::new());
360 registry.register_language(GroovyEngine::new());
361 registry.register_language(PhpEngine::new());
362 registry.register_language(KotlinEngine::new());
363 registry.register_language(CEngine::new());
364 registry.register_language(CppEngine::new());
365 registry.register_language(REngine::new());
366 registry.register_language(DartEngine::new());
367 registry.register_language(SwiftEngine::new());
368 registry.register_language(PerlEngine::new());
369 registry.register_language(JuliaEngine::new());
370 registry.register_language(HaskellEngine::new());
371 registry.register_language(ElixirEngine::new());
372 registry.register_language(CrystalEngine::new());
373 registry.register_language(ZigEngine::new());
374 registry.register_language(NimEngine::new());
375
376 registry
377 }
378
379 pub fn register_language<E>(&mut self, engine: E)
380 where
381 E: LanguageEngine + Send + Sync + 'static,
382 {
383 let id = engine.id().to_string();
384 for alias in engine.aliases() {
385 self.alias_lookup
386 .insert(canonical_language_id(alias), id.clone());
387 }
388 self.alias_lookup
389 .insert(canonical_language_id(&id), id.clone());
390 self.engines.insert(id, Box::new(engine));
391 }
392
393 pub fn resolve(&self, spec: &LanguageSpec) -> Option<&(dyn LanguageEngine + Send + Sync)> {
394 let canonical = canonical_language_id(spec.canonical_id());
395 let target_id = self
396 .alias_lookup
397 .get(&canonical)
398 .cloned()
399 .unwrap_or(canonical);
400 self.engines
401 .get(&target_id)
402 .map(|engine| engine.as_ref() as _)
403 }
404
405 pub fn resolve_by_id(&self, id: &str) -> Option<&(dyn LanguageEngine + Send + Sync)> {
406 let canonical = canonical_language_id(id);
407 let target_id = self
408 .alias_lookup
409 .get(&canonical)
410 .cloned()
411 .unwrap_or(canonical);
412 self.engines
413 .get(&target_id)
414 .map(|engine| engine.as_ref() as _)
415 }
416
417 pub fn engines(&self) -> impl Iterator<Item = &(dyn LanguageEngine + Send + Sync)> {
418 self.engines.values().map(|engine| engine.as_ref() as _)
419 }
420
421 pub fn known_languages(&self) -> Vec<String> {
422 let mut ids: Vec<_> = self.engines.keys().cloned().collect();
423 ids.sort();
424 ids
425 }
426}
427
428pub fn package_install_command(
431 language_id: &str,
432) -> Option<(&'static str, &'static [&'static str])> {
433 match language_id {
434 "python" => Some(("pip", &["install"])),
435 "javascript" | "typescript" => Some(("npm", &["install"])),
436 "rust" => Some(("cargo", &["add"])),
437 "go" => Some(("go", &["get"])),
438 "ruby" => Some(("gem", &["install"])),
439 "php" => Some(("composer", &["require"])),
440 "lua" => Some(("luarocks", &["install"])),
441 "dart" => Some(("dart", &["pub", "add"])),
442 "perl" => Some(("cpanm", &[])),
443 "julia" => Some(("julia", &["-e"])), "haskell" => Some(("cabal", &["install"])),
445 "nim" => Some(("nimble", &["install"])),
446 "r" => Some(("Rscript", &["-e"])), "kotlin" => None, "java" => None, "c" | "cpp" => None, "bash" => None,
451 "swift" => None,
452 "crystal" => Some(("shards", &["install"])),
453 "elixir" => None, "groovy" => None,
455 "csharp" => Some(("dotnet", &["add", "package"])),
456 "zig" => None,
457 _ => None,
458 }
459}
460
461pub fn build_install_command(language_id: &str, package: &str) -> Option<std::process::Command> {
464 let (binary, base_args) = package_install_command(language_id)?;
465
466 let mut cmd = std::process::Command::new(binary);
467
468 match language_id {
469 "julia" => {
470 cmd.arg("-e")
472 .arg(format!("using Pkg; Pkg.add(\"{package}\")"));
473 }
474 "r" => {
475 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 && let Some(ext) = path.extension().and_then(|e| e.to_str())
514 {
515 let ext_lower = ext.to_ascii_lowercase();
516 if let Some(lang) = extension_to_language(&ext_lower) {
517 let spec = LanguageSpec::new(lang);
518 if registry.resolve(&spec).is_some() {
519 return Some(spec);
520 }
521 }
522 }
523
524 if let Some(code) = source.as_inline()
525 && let Some(lang) = crate::detect::detect_language_from_snippet(code)
526 {
527 let spec = LanguageSpec::new(lang);
528 if registry.resolve(&spec).is_some() {
529 return Some(spec);
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}