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;
30use std::time::Duration;
31
32use anyhow::{Result, bail};
33
34use crate::cli::InputSource;
35use crate::language::{LanguageSpec, canonical_language_id};
36
37pub use bash::BashEngine;
38pub use c::CEngine;
39pub use cpp::CppEngine;
40pub use crystal::CrystalEngine;
41pub use csharp::CSharpEngine;
42pub use dart::DartEngine;
43pub use elixir::ElixirEngine;
44pub use go::GoEngine;
45pub use groovy::GroovyEngine;
46pub use haskell::HaskellEngine;
47pub use java::JavaEngine;
48pub use javascript::JavascriptEngine;
49pub use julia::JuliaEngine;
50pub use kotlin::KotlinEngine;
51pub use lua::LuaEngine;
52pub use nim::NimEngine;
53pub use perl::PerlEngine;
54pub use php::PhpEngine;
55pub use python::PythonEngine;
56pub use r::REngine;
57pub use ruby::RubyEngine;
58pub use rust::RustEngine;
59pub use swift::SwiftEngine;
60pub use typescript::TypeScriptEngine;
61pub use zig::ZigEngine;
62
63pub trait LanguageSession {
64    fn language_id(&self) -> &str;
65    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome>;
66    fn shutdown(&mut self) -> Result<()>;
67}
68
69pub trait LanguageEngine {
70    fn id(&self) -> &'static str;
71    fn display_name(&self) -> &'static str {
72        self.id()
73    }
74    fn aliases(&self) -> &[&'static str] {
75        &[]
76    }
77    fn supports_sessions(&self) -> bool {
78        false
79    }
80    fn validate(&self) -> Result<()> {
81        Ok(())
82    }
83    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome>;
84    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
85        bail!("{} does not support interactive sessions yet", self.id())
86    }
87}
88
89#[derive(Debug, Clone, PartialEq, Eq)]
90pub enum ExecutionPayload {
91    Inline { code: String },
92    File { path: std::path::PathBuf },
93    Stdin { code: String },
94}
95
96impl ExecutionPayload {
97    pub fn from_input_source(source: &InputSource) -> Result<Self> {
98        match source {
99            InputSource::Inline(code) => Ok(Self::Inline {
100                code: normalize_inline_code(code).into_owned(),
101            }),
102            InputSource::File(path) => Ok(Self::File { path: path.clone() }),
103            InputSource::Stdin => {
104                use std::io::Read;
105                let mut buffer = String::new();
106                std::io::stdin().read_to_string(&mut buffer)?;
107                Ok(Self::Stdin { code: buffer })
108            }
109        }
110    }
111
112    pub fn as_inline(&self) -> Option<&str> {
113        match self {
114            ExecutionPayload::Inline { code } => Some(code.as_str()),
115            ExecutionPayload::Stdin { code } => Some(code.as_str()),
116            ExecutionPayload::File { .. } => None,
117        }
118    }
119
120    pub fn as_file_path(&self) -> Option<&Path> {
121        match self {
122            ExecutionPayload::File { path } => Some(path.as_path()),
123            _ => None,
124        }
125    }
126}
127
128fn normalize_inline_code(code: &str) -> Cow<'_, str> {
129    if !code.contains('\\') {
130        return Cow::Borrowed(code);
131    }
132
133    let mut result = String::with_capacity(code.len());
134    let mut chars = code.chars().peekable();
135    let mut in_single = false;
136    let mut in_double = false;
137    let mut escape_in_quote = false;
138
139    while let Some(ch) = chars.next() {
140        if in_single {
141            result.push(ch);
142            if escape_in_quote {
143                escape_in_quote = false;
144            } else if ch == '\\' {
145                escape_in_quote = true;
146            } else if ch == '\'' {
147                in_single = false;
148            }
149            continue;
150        }
151
152        if in_double {
153            result.push(ch);
154            if escape_in_quote {
155                escape_in_quote = false;
156            } else if ch == '\\' {
157                escape_in_quote = true;
158            } else if ch == '"' {
159                in_double = false;
160            }
161            continue;
162        }
163
164        match ch {
165            '\'' => {
166                in_single = true;
167                result.push(ch);
168            }
169            '"' => {
170                in_double = true;
171                result.push(ch);
172            }
173            '\\' => match chars.next() {
174                Some('n') => result.push('\n'),
175                Some('r') => result.push('\r'),
176                Some('t') => result.push('\t'),
177                Some('\\') => result.push('\\'),
178                Some(other) => {
179                    result.push('\\');
180                    result.push(other);
181                }
182                None => result.push('\\'),
183            },
184            _ => result.push(ch),
185        }
186    }
187
188    Cow::Owned(result)
189}
190
191#[derive(Debug, Clone, PartialEq, Eq)]
192pub struct ExecutionOutcome {
193    pub language: String,
194    pub exit_code: Option<i32>,
195    pub stdout: String,
196    pub stderr: String,
197    pub duration: Duration,
198}
199
200impl ExecutionOutcome {
201    pub fn success(&self) -> bool {
202        match self.exit_code {
203            Some(code) => code == 0,
204            None => self.stderr.trim().is_empty(),
205        }
206    }
207}
208
209pub struct LanguageRegistry {
210    engines: HashMap<String, Box<dyn LanguageEngine + Send + Sync>>, // keyed by canonical id
211    alias_lookup: HashMap<String, String>,
212}
213
214impl LanguageRegistry {
215    pub fn bootstrap() -> Self {
216        let mut registry = Self {
217            engines: HashMap::new(),
218            alias_lookup: HashMap::new(),
219        };
220
221        registry.register_language(PythonEngine::new());
222        registry.register_language(BashEngine::new());
223        registry.register_language(JavascriptEngine::new());
224        registry.register_language(RubyEngine::new());
225        registry.register_language(RustEngine::new());
226        registry.register_language(GoEngine::new());
227        registry.register_language(CSharpEngine::new());
228        registry.register_language(TypeScriptEngine::new());
229        registry.register_language(LuaEngine::new());
230        registry.register_language(JavaEngine::new());
231        registry.register_language(GroovyEngine::new());
232        registry.register_language(PhpEngine::new());
233        registry.register_language(KotlinEngine::new());
234        registry.register_language(CEngine::new());
235        registry.register_language(CppEngine::new());
236        registry.register_language(REngine::new());
237        registry.register_language(DartEngine::new());
238        registry.register_language(SwiftEngine::new());
239        registry.register_language(PerlEngine::new());
240        registry.register_language(JuliaEngine::new());
241        registry.register_language(HaskellEngine::new());
242        registry.register_language(ElixirEngine::new());
243        registry.register_language(CrystalEngine::new());
244        registry.register_language(ZigEngine::new());
245        registry.register_language(NimEngine::new());
246
247        registry
248    }
249
250    pub fn register_language<E>(&mut self, engine: E)
251    where
252        E: LanguageEngine + Send + Sync + 'static,
253    {
254        let id = engine.id().to_string();
255        for alias in engine.aliases() {
256            self.alias_lookup
257                .insert(canonical_language_id(alias), id.clone());
258        }
259        self.alias_lookup
260            .insert(canonical_language_id(&id), id.clone());
261        self.engines.insert(id, Box::new(engine));
262    }
263
264    pub fn resolve(&self, spec: &LanguageSpec) -> Option<&(dyn LanguageEngine + Send + Sync)> {
265        let canonical = canonical_language_id(spec.canonical_id());
266        let target_id = self
267            .alias_lookup
268            .get(&canonical)
269            .cloned()
270            .unwrap_or_else(|| canonical);
271        self.engines
272            .get(&target_id)
273            .map(|engine| engine.as_ref() as _)
274    }
275
276    pub fn resolve_by_id(&self, id: &str) -> Option<&(dyn LanguageEngine + Send + Sync)> {
277        let canonical = canonical_language_id(id);
278        let target_id = self
279            .alias_lookup
280            .get(&canonical)
281            .cloned()
282            .unwrap_or_else(|| canonical);
283        self.engines
284            .get(&target_id)
285            .map(|engine| engine.as_ref() as _)
286    }
287
288    pub fn engines(&self) -> impl Iterator<Item = &(dyn LanguageEngine + Send + Sync)> {
289        self.engines.values().map(|engine| engine.as_ref() as _)
290    }
291
292    pub fn known_languages(&self) -> Vec<String> {
293        let mut ids: Vec<_> = self.engines.keys().cloned().collect();
294        ids.sort();
295        ids
296    }
297}
298
299pub fn default_language() -> &'static str {
300    "python"
301}
302
303pub fn ensure_known_language(spec: &LanguageSpec, registry: &LanguageRegistry) -> Result<()> {
304    if registry.resolve(spec).is_some() {
305        return Ok(());
306    }
307
308    let available = registry.known_languages();
309    bail!(
310        "Unknown language '{}'. Available languages: {}",
311        spec.canonical_id(),
312        available.join(", ")
313    )
314}
315
316pub fn detect_language_for_source(
317    source: &ExecutionPayload,
318    registry: &LanguageRegistry,
319) -> Option<LanguageSpec> {
320    if let Some(path) = source.as_file_path() {
321        if let Some(ext) = path.extension().and_then(|e| e.to_str()) {
322            let ext_lower = ext.to_ascii_lowercase();
323            if let Some(lang) = extension_to_language(&ext_lower) {
324                let spec = LanguageSpec::new(lang);
325                if registry.resolve(&spec).is_some() {
326                    return Some(spec);
327                }
328            }
329        }
330    }
331
332    if let Some(code) = source.as_inline() {
333        if let Some(lang) = crate::detect::detect_language_from_snippet(code) {
334            let spec = LanguageSpec::new(lang);
335            if registry.resolve(&spec).is_some() {
336                return Some(spec);
337            }
338        }
339    }
340
341    None
342}
343
344fn extension_to_language(ext: &str) -> Option<&'static str> {
345    match ext {
346        "py" | "pyw" => Some("python"),
347        "rs" => Some("rust"),
348        "go" => Some("go"),
349        "cs" => Some("csharp"),
350        "ts" | "tsx" => Some("typescript"),
351        "js" | "mjs" | "cjs" | "jsx" => Some("javascript"),
352        "rb" => Some("ruby"),
353        "lua" => Some("lua"),
354        "java" => Some("java"),
355        "groovy" => Some("groovy"),
356        "php" => Some("php"),
357        "kt" | "kts" => Some("kotlin"),
358        "c" => Some("c"),
359        "cpp" | "cc" | "cxx" | "hpp" | "hxx" => Some("cpp"),
360        "sh" | "bash" | "zsh" => Some("bash"),
361        "r" => Some("r"),
362        "dart" => Some("dart"),
363        "swift" => Some("swift"),
364        "perl" | "pl" | "pm" => Some("perl"),
365        "julia" | "jl" => Some("julia"),
366        "hs" => Some("haskell"),
367        "ex" | "exs" => Some("elixir"),
368        "cr" => Some("crystal"),
369        "zig" => Some("zig"),
370        "nim" => Some("nim"),
371        _ => None,
372    }
373}