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