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>>, 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}