1use std::fs;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Stdio};
4use std::time::{Duration, Instant};
5
6use anyhow::{Context, Result};
7use tempfile::{Builder, TempDir};
8
9use super::{ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, hash_source};
10
11pub struct KotlinEngine {
12 compiler: Option<PathBuf>,
13 java: Option<PathBuf>,
14}
15
16impl KotlinEngine {
17 pub fn new() -> Self {
18 Self {
19 compiler: resolve_kotlinc_binary(),
20 java: resolve_java_binary(),
21 }
22 }
23
24 fn ensure_compiler(&self) -> Result<&Path> {
25 self.compiler.as_deref().ok_or_else(|| {
26 anyhow::anyhow!(
27 "Kotlin support requires the `kotlinc` compiler. Install it from https://kotlinlang.org/docs/command-line.html and ensure it is on your PATH."
28 )
29 })
30 }
31
32 fn ensure_java(&self) -> Result<&Path> {
33 self.java.as_deref().ok_or_else(|| {
34 anyhow::anyhow!(
35 "Kotlin execution requires a `java` runtime. Install a JDK and ensure `java` is on your PATH."
36 )
37 })
38 }
39
40 fn write_inline_source(&self, code: &str, dir: &Path) -> Result<PathBuf> {
41 let source_path = dir.join("Main.kt");
42 let wrapped = wrap_inline_kotlin(code);
43 std::fs::write(&source_path, wrapped).with_context(|| {
44 format!(
45 "failed to write temporary Kotlin source to {}",
46 source_path.display()
47 )
48 })?;
49 Ok(source_path)
50 }
51
52 fn copy_source(&self, original: &Path, dir: &Path) -> Result<PathBuf> {
53 let file_name = original
54 .file_name()
55 .map(|f| f.to_owned())
56 .ok_or_else(|| anyhow::anyhow!("invalid Kotlin source path"))?;
57 let target = dir.join(&file_name);
58 std::fs::copy(original, &target).with_context(|| {
59 format!(
60 "failed to copy Kotlin source from {} to {}",
61 original.display(),
62 target.display()
63 )
64 })?;
65 Ok(target)
66 }
67
68 fn compile(&self, source: &Path, jar: &Path) -> Result<std::process::Output> {
69 let compiler = self.ensure_compiler()?;
70 invoke_kotlin_compiler(compiler, source, jar)
71 }
72
73 fn run(&self, jar: &Path) -> Result<std::process::Output> {
74 let java = self.ensure_java()?;
75 run_kotlin_jar(java, jar)
76 }
77}
78
79impl LanguageEngine for KotlinEngine {
80 fn id(&self) -> &'static str {
81 "kotlin"
82 }
83
84 fn display_name(&self) -> &'static str {
85 "Kotlin"
86 }
87
88 fn aliases(&self) -> &[&'static str] {
89 &["kt"]
90 }
91
92 fn supports_sessions(&self) -> bool {
93 self.compiler.is_some() && self.java.is_some()
94 }
95
96 fn validate(&self) -> Result<()> {
97 let compiler = self.ensure_compiler()?;
98 let mut compile_check = Command::new(compiler);
99 compile_check
100 .arg("-version")
101 .stdout(Stdio::null())
102 .stderr(Stdio::null());
103 compile_check
104 .status()
105 .with_context(|| format!("failed to invoke {}", compiler.display()))?
106 .success()
107 .then_some(())
108 .ok_or_else(|| anyhow::anyhow!("{} is not executable", compiler.display()))?;
109
110 let java = self.ensure_java()?;
111 let mut java_check = Command::new(java);
112 java_check
113 .arg("-version")
114 .stdout(Stdio::null())
115 .stderr(Stdio::null());
116 java_check
117 .status()
118 .with_context(|| format!("failed to invoke {}", java.display()))?
119 .success()
120 .then_some(())
121 .ok_or_else(|| anyhow::anyhow!("{} is not executable", java.display()))?;
122
123 Ok(())
124 }
125
126 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
127 if let Some(code) = match payload {
129 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
130 _ => None,
131 } {
132 let wrapped = wrap_inline_kotlin(code);
133 let src_hash = hash_source(&wrapped);
134 let cached_jar = std::env::temp_dir()
135 .join("run-compile-cache")
136 .join(format!("kotlin-{:016x}.jar", src_hash));
137 if cached_jar.exists() {
138 let start = Instant::now();
139 if let Ok(output) = self.run(&cached_jar) {
140 return Ok(ExecutionOutcome {
141 language: self.id().to_string(),
142 exit_code: output.status.code(),
143 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
144 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
145 duration: start.elapsed(),
146 });
147 }
148 }
149 }
150
151 let temp_dir = Builder::new()
152 .prefix("run-kotlin")
153 .tempdir()
154 .context("failed to create temporary directory for kotlin build")?;
155 let dir_path = temp_dir.path();
156
157 let source_path = match payload {
158 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => {
159 self.write_inline_source(code, dir_path)?
160 }
161 ExecutionPayload::File { path } => self.copy_source(path, dir_path)?,
162 };
163
164 let jar_path = dir_path.join("snippet.jar");
165 let start = Instant::now();
166
167 let compile_output = self.compile(&source_path, &jar_path)?;
168 if !compile_output.status.success() {
169 return Ok(ExecutionOutcome {
170 language: self.id().to_string(),
171 exit_code: compile_output.status.code(),
172 stdout: String::from_utf8_lossy(&compile_output.stdout).into_owned(),
173 stderr: String::from_utf8_lossy(&compile_output.stderr).into_owned(),
174 duration: start.elapsed(),
175 });
176 }
177
178 if let Some(code) = match payload {
180 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => Some(code.as_str()),
181 _ => None,
182 } {
183 let wrapped = wrap_inline_kotlin(code);
184 let src_hash = hash_source(&wrapped);
185 let cache_dir = std::env::temp_dir().join("run-compile-cache");
186 let _ = std::fs::create_dir_all(&cache_dir);
187 let cached_jar = cache_dir.join(format!("kotlin-{:016x}.jar", src_hash));
188 let _ = std::fs::copy(&jar_path, &cached_jar);
189 }
190
191 let run_output = self.run(&jar_path)?;
192 Ok(ExecutionOutcome {
193 language: self.id().to_string(),
194 exit_code: run_output.status.code(),
195 stdout: String::from_utf8_lossy(&run_output.stdout).into_owned(),
196 stderr: String::from_utf8_lossy(&run_output.stderr).into_owned(),
197 duration: start.elapsed(),
198 })
199 }
200
201 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
202 let compiler = self.ensure_compiler()?.to_path_buf();
203 let java = self.ensure_java()?.to_path_buf();
204
205 let dir = Builder::new()
206 .prefix("run-kotlin-repl")
207 .tempdir()
208 .context("failed to create temporary directory for kotlin repl")?;
209 let dir_path = dir.path();
210
211 let source_path = dir_path.join("Session.kt");
212 let jar_path = dir_path.join("session.jar");
213 fs::write(&source_path, "// Kotlin REPL session\n").with_context(|| {
214 format!(
215 "failed to initialize Kotlin session source at {}",
216 source_path.display()
217 )
218 })?;
219
220 Ok(Box::new(KotlinSession {
221 compiler,
222 java,
223 _dir: dir,
224 source_path,
225 jar_path,
226 definitions: Vec::new(),
227 statements: Vec::new(),
228 previous_stdout: String::new(),
229 previous_stderr: String::new(),
230 }))
231 }
232}
233
234fn resolve_kotlinc_binary() -> Option<PathBuf> {
235 which::which("kotlinc").ok()
236}
237
238fn resolve_java_binary() -> Option<PathBuf> {
239 which::which("java").ok()
240}
241
242fn wrap_inline_kotlin(body: &str) -> String {
243 if body.contains("fun main") {
244 return body.to_string();
245 }
246
247 let mut header_lines = Vec::new();
248 let mut rest_lines = Vec::new();
249 let mut in_header = true;
250
251 for line in body.lines() {
252 let trimmed = line.trim_start();
253 if in_header && (trimmed.starts_with("import ") || trimmed.starts_with("package ")) {
254 header_lines.push(line);
255 continue;
256 }
257 in_header = false;
258 rest_lines.push(line);
259 }
260
261 let mut result = String::new();
262 if !header_lines.is_empty() {
263 for hl in header_lines {
264 result.push_str(hl);
265 if !hl.ends_with('\n') {
266 result.push('\n');
267 }
268 }
269 result.push('\n');
270 }
271
272 result.push_str("fun main() {\n");
273 for line in rest_lines {
274 if line.trim().is_empty() {
275 result.push_str(" \n");
276 } else {
277 result.push_str(" ");
278 result.push_str(line);
279 result.push('\n');
280 }
281 }
282 result.push_str("}\n");
283 result
284}
285
286fn contains_main_function(code: &str) -> bool {
287 code.lines()
288 .any(|line| line.trim_start().starts_with("fun main"))
289}
290
291#[derive(Debug, Clone, Copy, PartialEq, Eq)]
292enum SnippetKind {
293 Definition,
294 Statement,
295 Expression,
296}
297
298fn classify_snippet(code: &str) -> SnippetKind {
299 let trimmed = code.trim();
300 if trimmed.is_empty() {
301 return SnippetKind::Statement;
302 }
303
304 const DEF_PREFIXES: [&str; 13] = [
305 "fun ",
306 "class ",
307 "object ",
308 "interface ",
309 "enum ",
310 "sealed ",
311 "data class ",
312 "annotation ",
313 "typealias ",
314 "package ",
315 "import ",
316 "val ",
317 "var ",
318 ];
319 if DEF_PREFIXES
320 .iter()
321 .any(|prefix| trimmed.starts_with(prefix))
322 {
323 return SnippetKind::Definition;
324 }
325
326 if trimmed.starts_with('@') {
327 return SnippetKind::Definition;
328 }
329
330 if is_kotlin_expression(trimmed) {
331 return SnippetKind::Expression;
332 }
333
334 SnippetKind::Statement
335}
336
337fn is_kotlin_expression(code: &str) -> bool {
338 if code.contains('\n') {
339 return false;
340 }
341 if code.ends_with(';') {
342 return false;
343 }
344
345 let lowered = code.trim_start().to_ascii_lowercase();
346 const DISALLOWED_PREFIXES: [&str; 14] = [
347 "while ", "for ", "do ", "try ", "catch", "finally", "return ", "throw ", "break",
348 "continue", "val ", "var ", "fun ", "class ",
349 ];
350 if DISALLOWED_PREFIXES
351 .iter()
352 .any(|prefix| lowered.starts_with(prefix))
353 {
354 return false;
355 }
356
357 if code.starts_with("print") {
358 return false;
359 }
360
361 if code == "true" || code == "false" {
362 return true;
363 }
364 if code.parse::<f64>().is_ok() {
365 return true;
366 }
367 if code.starts_with('"') && code.ends_with('"') && code.len() >= 2 {
368 return true;
369 }
370 if code.contains("==")
371 || code.contains("!=")
372 || code.contains("<=")
373 || code.contains(">=")
374 || code.contains("&&")
375 || code.contains("||")
376 {
377 return true;
378 }
379 const ASSIGN_OPS: [&str; 7] = ["=", "+=", "-=", "*=", "/=", "%=", "= "];
380 if ASSIGN_OPS.iter().any(|op| code.contains(op))
381 && !code.contains("==")
382 && !code.contains("!=")
383 && !code.contains(">=")
384 && !code.contains("<=")
385 && !code.contains("=>")
386 {
387 return false;
388 }
389
390 if code.chars().any(|c| "+-*/%<>^|&".contains(c)) {
391 return true;
392 }
393
394 if code
395 .chars()
396 .all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '.' || c == '$')
397 {
398 return true;
399 }
400
401 code.contains('(') && code.contains(')')
402}
403
404fn wrap_kotlin_expression(code: &str, index: usize) -> String {
405 format!("val __repl_val_{index} = ({code})\nprintln(__repl_val_{index})\n")
406}
407
408fn ensure_trailing_newline(code: &str) -> String {
409 let mut owned = code.to_string();
410 if !owned.ends_with('\n') {
411 owned.push('\n');
412 }
413 owned
414}
415
416fn diff_output(previous: &str, current: &str) -> String {
417 if let Some(stripped) = current.strip_prefix(previous) {
418 stripped.to_string()
419 } else {
420 current.to_string()
421 }
422}
423
424fn normalize_output(bytes: &[u8]) -> String {
425 String::from_utf8_lossy(bytes)
426 .replace("\r\n", "\n")
427 .replace('\r', "")
428}
429
430fn invoke_kotlin_compiler(
431 compiler: &Path,
432 source: &Path,
433 jar: &Path,
434) -> Result<std::process::Output> {
435 let mut cmd = Command::new(compiler);
436 cmd.arg(source)
437 .arg("-include-runtime")
438 .arg("-d")
439 .arg(jar)
440 .stdout(Stdio::piped())
441 .stderr(Stdio::piped());
442 cmd.output().with_context(|| {
443 format!(
444 "failed to invoke {} to compile {}",
445 compiler.display(),
446 source.display()
447 )
448 })
449}
450
451fn run_kotlin_jar(java: &Path, jar: &Path) -> Result<std::process::Output> {
452 let mut cmd = Command::new(java);
453 cmd.arg("-jar")
454 .arg(jar)
455 .stdout(Stdio::piped())
456 .stderr(Stdio::piped());
457 cmd.stdin(Stdio::inherit());
458 cmd.output().with_context(|| {
459 format!(
460 "failed to execute {} -jar {}",
461 java.display(),
462 jar.display()
463 )
464 })
465}
466struct KotlinSession {
467 compiler: PathBuf,
468 java: PathBuf,
469 _dir: TempDir,
470 source_path: PathBuf,
471 jar_path: PathBuf,
472 definitions: Vec<String>,
473 statements: Vec<String>,
474 previous_stdout: String,
475 previous_stderr: String,
476}
477
478impl KotlinSession {
479 fn render_prelude(&self) -> String {
480 let mut source = String::from("import kotlin.math.*\n\n");
481 for def in &self.definitions {
482 source.push_str(def);
483 if !def.ends_with('\n') {
484 source.push('\n');
485 }
486 source.push('\n');
487 }
488 source
489 }
490
491 fn render_source(&self) -> String {
492 let mut source = self.render_prelude();
493 source.push_str("fun main() {\n");
494 for stmt in &self.statements {
495 for line in stmt.lines() {
496 source.push_str(" ");
497 source.push_str(line);
498 source.push('\n');
499 }
500 if !stmt.ends_with('\n') {
501 source.push('\n');
502 }
503 }
504 source.push_str("}\n");
505 source
506 }
507
508 fn write_source(&self, contents: &str) -> Result<()> {
509 fs::write(&self.source_path, contents).with_context(|| {
510 format!(
511 "failed to write generated Kotlin REPL source to {}",
512 self.source_path.display()
513 )
514 })
515 }
516
517 fn compile_and_run(&mut self) -> Result<(std::process::Output, Duration)> {
518 let start = Instant::now();
519 let source = self.render_source();
520 self.write_source(&source)?;
521 let compile_output =
522 invoke_kotlin_compiler(&self.compiler, &self.source_path, &self.jar_path)?;
523 if !compile_output.status.success() {
524 return Ok((compile_output, start.elapsed()));
525 }
526 let run_output = run_kotlin_jar(&self.java, &self.jar_path)?;
527 Ok((run_output, start.elapsed()))
528 }
529
530 fn diff_outputs(
531 &mut self,
532 output: &std::process::Output,
533 duration: Duration,
534 ) -> ExecutionOutcome {
535 let stdout_full = normalize_output(&output.stdout);
536 let stderr_full = normalize_output(&output.stderr);
537
538 let stdout_delta = diff_output(&self.previous_stdout, &stdout_full);
539 let stderr_delta = diff_output(&self.previous_stderr, &stderr_full);
540
541 if output.status.success() {
542 self.previous_stdout = stdout_full;
543 self.previous_stderr = stderr_full;
544 }
545
546 ExecutionOutcome {
547 language: "kotlin".to_string(),
548 exit_code: output.status.code(),
549 stdout: stdout_delta,
550 stderr: stderr_delta,
551 duration,
552 }
553 }
554
555 fn add_definition(&mut self, snippet: String) {
556 self.definitions.push(snippet);
557 }
558
559 fn add_statement(&mut self, snippet: String) {
560 self.statements.push(snippet);
561 }
562
563 fn remove_last_definition(&mut self) {
564 let _ = self.definitions.pop();
565 }
566
567 fn remove_last_statement(&mut self) {
568 let _ = self.statements.pop();
569 }
570
571 fn reset_state(&mut self) -> Result<()> {
572 self.definitions.clear();
573 self.statements.clear();
574 self.previous_stdout.clear();
575 self.previous_stderr.clear();
576 let source = self.render_source();
577 self.write_source(&source)
578 }
579
580 fn run_standalone_program(&mut self, code: &str) -> Result<ExecutionOutcome> {
581 let start = Instant::now();
582 let mut source = self.render_prelude();
583 if !source.ends_with('\n') {
584 source.push('\n');
585 }
586 source.push_str(code);
587 if !code.ends_with('\n') {
588 source.push('\n');
589 }
590
591 let standalone_path = self
592 .source_path
593 .parent()
594 .unwrap_or_else(|| Path::new("."))
595 .join("standalone.kt");
596 fs::write(&standalone_path, &source).with_context(|| {
597 format!(
598 "failed to write standalone Kotlin source to {}",
599 standalone_path.display()
600 )
601 })?;
602
603 let compile_output =
604 invoke_kotlin_compiler(&self.compiler, &standalone_path, &self.jar_path)?;
605 if !compile_output.status.success() {
606 return Ok(ExecutionOutcome {
607 language: "kotlin".to_string(),
608 exit_code: compile_output.status.code(),
609 stdout: normalize_output(&compile_output.stdout),
610 stderr: normalize_output(&compile_output.stderr),
611 duration: start.elapsed(),
612 });
613 }
614
615 let run_output = run_kotlin_jar(&self.java, &self.jar_path)?;
616 Ok(ExecutionOutcome {
617 language: "kotlin".to_string(),
618 exit_code: run_output.status.code(),
619 stdout: normalize_output(&run_output.stdout),
620 stderr: normalize_output(&run_output.stderr),
621 duration: start.elapsed(),
622 })
623 }
624}
625
626impl LanguageSession for KotlinSession {
627 fn language_id(&self) -> &str {
628 "kotlin"
629 }
630
631 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
632 let trimmed = code.trim();
633 if trimmed.is_empty() {
634 return Ok(ExecutionOutcome {
635 language: self.language_id().to_string(),
636 exit_code: None,
637 stdout: String::new(),
638 stderr: String::new(),
639 duration: Duration::default(),
640 });
641 }
642
643 if trimmed.eq_ignore_ascii_case(":reset") {
644 self.reset_state()?;
645 return Ok(ExecutionOutcome {
646 language: self.language_id().to_string(),
647 exit_code: None,
648 stdout: String::new(),
649 stderr: String::new(),
650 duration: Duration::default(),
651 });
652 }
653
654 if trimmed.eq_ignore_ascii_case(":help") {
655 return Ok(ExecutionOutcome {
656 language: self.language_id().to_string(),
657 exit_code: None,
658 stdout:
659 "Kotlin commands:\n :reset - clear session state\n :help - show this message\n"
660 .to_string(),
661 stderr: String::new(),
662 duration: Duration::default(),
663 });
664 }
665
666 if contains_main_function(code) {
667 return self.run_standalone_program(code);
668 }
669
670 let classification = classify_snippet(trimmed);
671 match classification {
672 SnippetKind::Definition => {
673 self.add_definition(code.to_string());
674 let (output, duration) = self.compile_and_run()?;
675 if !output.status.success() {
676 self.remove_last_definition();
677 }
678 Ok(self.diff_outputs(&output, duration))
679 }
680 SnippetKind::Expression => {
681 let wrapped = wrap_kotlin_expression(trimmed, self.statements.len());
682 self.add_statement(wrapped);
683 let (output, duration) = self.compile_and_run()?;
684 if !output.status.success() {
685 self.remove_last_statement();
686 }
687 Ok(self.diff_outputs(&output, duration))
688 }
689 SnippetKind::Statement => {
690 let stmt = ensure_trailing_newline(code);
691 self.add_statement(stmt);
692 let (output, duration) = self.compile_and_run()?;
693 if !output.status.success() {
694 self.remove_last_statement();
695 }
696 Ok(self.diff_outputs(&output, duration))
697 }
698 }
699 }
700
701 fn shutdown(&mut self) -> Result<()> {
702 Ok(())
703 }
704}