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