1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5use std::time::{Duration, Instant};
6
7use anyhow::{Context, Result};
8use tempfile::{Builder, TempDir};
9
10use super::{ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession};
11
12pub struct SwiftEngine {
13 executable: Option<PathBuf>,
14}
15
16impl Default for SwiftEngine {
17 fn default() -> Self {
18 Self::new()
19 }
20}
21
22impl SwiftEngine {
23 pub fn new() -> Self {
24 Self {
25 executable: resolve_swift_binary(),
26 }
27 }
28
29 fn ensure_executable(&self) -> Result<&Path> {
30 self.executable.as_deref().ok_or_else(|| {
31 anyhow::anyhow!(
32 "Swift support requires the `swift` executable. Install Xcode command-line tools or the Swift toolchain from https://www.swift.org/download/ and ensure `swift` is on your PATH."
33 )
34 })
35 }
36
37 fn write_temp_source(&self, code: &str) -> Result<(TempDir, PathBuf)> {
38 let dir = Builder::new()
39 .prefix("run-swift")
40 .tempdir()
41 .context("failed to create temporary directory for Swift source")?;
42 let path = dir.path().join("snippet.swift");
43 let mut contents = code.to_string();
44 if !contents.ends_with('\n') {
45 contents.push('\n');
46 }
47 fs::write(&path, contents).with_context(|| {
48 format!(
49 "failed to write temporary Swift source to {}",
50 path.display()
51 )
52 })?;
53 Ok((dir, path))
54 }
55
56 fn execute_path(&self, path: &Path) -> Result<std::process::Output> {
57 let executable = self.ensure_executable()?;
58 let mut cmd = Command::new(executable);
59 cmd.arg(path).stdout(Stdio::piped()).stderr(Stdio::piped());
60 cmd.stdin(Stdio::inherit());
61 if let Some(parent) = path.parent() {
62 cmd.current_dir(parent);
63 }
64 cmd.output().with_context(|| {
65 format!(
66 "failed to execute {} with script {}",
67 executable.display(),
68 path.display()
69 )
70 })
71 }
72}
73
74impl LanguageEngine for SwiftEngine {
75 fn id(&self) -> &'static str {
76 "swift"
77 }
78
79 fn display_name(&self) -> &'static str {
80 "Swift"
81 }
82
83 fn aliases(&self) -> &[&'static str] {
84 &["swiftlang"]
85 }
86
87 fn supports_sessions(&self) -> bool {
88 self.executable.is_some()
89 }
90
91 fn validate(&self) -> Result<()> {
92 let executable = self.ensure_executable()?;
93 let mut cmd = Command::new(executable);
94 cmd.arg("--version")
95 .stdout(Stdio::null())
96 .stderr(Stdio::null());
97 cmd.status()
98 .with_context(|| format!("failed to invoke {}", executable.display()))?
99 .success()
100 .then_some(())
101 .ok_or_else(|| anyhow::anyhow!("{} is not executable", executable.display()))
102 }
103
104 fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
105 let start = Instant::now();
106 let (temp_dir, path) = match payload {
107 ExecutionPayload::Inline { code } | ExecutionPayload::Stdin { code } => {
108 let (dir, path) = self.write_temp_source(code)?;
109 (Some(dir), path)
110 }
111 ExecutionPayload::File { path } => (None, path.clone()),
112 };
113
114 let output = self.execute_path(&path)?;
115 drop(temp_dir);
116
117 Ok(ExecutionOutcome {
118 language: self.id().to_string(),
119 exit_code: output.status.code(),
120 stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
121 stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
122 duration: start.elapsed(),
123 })
124 }
125
126 fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
127 let executable = self.ensure_executable()?.to_path_buf();
128 Ok(Box::new(SwiftSession::new(executable)?))
129 }
130}
131
132fn resolve_swift_binary() -> Option<PathBuf> {
133 which::which("swift").ok()
134}
135
136#[derive(Default)]
137struct SwiftSessionState {
138 imports: BTreeSet<String>,
139 declarations: Vec<String>,
140 statements: Vec<String>,
141}
142
143struct SwiftSession {
144 executable: PathBuf,
145 workspace: TempDir,
146 state: SwiftSessionState,
147 previous_stdout: String,
148 previous_stderr: String,
149}
150
151impl SwiftSession {
152 fn new(executable: PathBuf) -> Result<Self> {
153 let workspace = Builder::new()
154 .prefix("run-swift-repl")
155 .tempdir()
156 .context("failed to create temporary directory for Swift repl")?;
157 let session = Self {
158 executable,
159 workspace,
160 state: SwiftSessionState::default(),
161 previous_stdout: String::new(),
162 previous_stderr: String::new(),
163 };
164 session.persist_source()?;
165 Ok(session)
166 }
167
168 fn source_path(&self) -> PathBuf {
169 self.workspace.path().join("session.swift")
170 }
171
172 fn persist_source(&self) -> Result<()> {
173 let source = self.render_source();
174 fs::write(self.source_path(), source)
175 .with_context(|| "failed to write Swift session source".to_string())
176 }
177
178 fn render_source(&self) -> String {
179 let mut source = String::from("import Foundation\n");
180
181 for import in &self.state.imports {
182 let trimmed = import.trim();
183 if trimmed.eq("import Foundation") {
184 continue;
185 }
186 source.push_str(trimmed);
187 if !trimmed.ends_with('\n') {
188 source.push('\n');
189 }
190 }
191 source.push('\n');
192
193 for decl in &self.state.declarations {
194 source.push_str(decl);
195 if !decl.ends_with('\n') {
196 source.push('\n');
197 }
198 source.push('\n');
199 }
200
201 if self.state.statements.is_empty() {
202 source.push_str("// session body\n");
203 } else {
204 for stmt in &self.state.statements {
205 source.push_str(stmt);
206 if !stmt.ends_with('\n') {
207 source.push('\n');
208 }
209 }
210 }
211
212 source
213 }
214
215 fn run_program(&self) -> Result<std::process::Output> {
216 let mut cmd = Command::new(&self.executable);
217 cmd.arg("session.swift")
218 .stdout(Stdio::piped())
219 .stderr(Stdio::piped())
220 .current_dir(self.workspace.path());
221 cmd.output().with_context(|| {
222 format!(
223 "failed to execute {} for Swift session",
224 self.executable.display()
225 )
226 })
227 }
228
229 fn run_current(&mut self, start: Instant) -> Result<(ExecutionOutcome, bool)> {
230 self.persist_source()?;
231 let output = self.run_program()?;
232 let stdout_full = normalize_output(&output.stdout);
233 let stderr_full = normalize_output(&output.stderr);
234
235 let stdout_delta = diff_output(&self.previous_stdout, &stdout_full);
236 let stderr_delta = diff_output(&self.previous_stderr, &stderr_full);
237
238 let success = output.status.success();
239 if success {
240 self.previous_stdout = stdout_full;
241 self.previous_stderr = stderr_full;
242 }
243
244 let outcome = ExecutionOutcome {
245 language: "swift".to_string(),
246 exit_code: output.status.code(),
247 stdout: stdout_delta,
248 stderr: stderr_delta,
249 duration: start.elapsed(),
250 };
251
252 Ok((outcome, success))
253 }
254
255 fn apply_import(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
256 let mut inserted = Vec::new();
257 for line in code.lines() {
258 let trimmed = line.trim();
259 if trimmed.is_empty() {
260 continue;
261 }
262 let stmt = if trimmed.ends_with(';') {
263 trimmed.trim_end_matches(';').to_string()
264 } else {
265 trimmed.to_string()
266 };
267 if self.state.imports.insert(stmt.clone()) {
268 inserted.push(stmt);
269 }
270 }
271
272 if inserted.is_empty() {
273 return Ok((
274 ExecutionOutcome {
275 language: "swift".to_string(),
276 exit_code: None,
277 stdout: String::new(),
278 stderr: String::new(),
279 duration: Duration::default(),
280 },
281 true,
282 ));
283 }
284
285 let start = Instant::now();
286 let (outcome, success) = self.run_current(start)?;
287 if !success {
288 for stmt in inserted {
289 self.state.imports.remove(&stmt);
290 }
291 self.persist_source()?;
292 }
293 Ok((outcome, success))
294 }
295
296 fn apply_declaration(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
297 let snippet = ensure_trailing_newline(code);
298 self.state.declarations.push(snippet);
299 let start = Instant::now();
300 let (outcome, success) = self.run_current(start)?;
301 if !success {
302 let _ = self.state.declarations.pop();
303 self.persist_source()?;
304 }
305 Ok((outcome, success))
306 }
307
308 fn apply_statement(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
309 self.state.statements.push(ensure_trailing_newline(code));
310 let start = Instant::now();
311 let (outcome, success) = self.run_current(start)?;
312 if !success {
313 let _ = self.state.statements.pop();
314 self.persist_source()?;
315 }
316 Ok((outcome, success))
317 }
318
319 fn apply_expression(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
320 self.state.statements.push(wrap_expression(code));
321 let start = Instant::now();
322 let (outcome, success) = self.run_current(start)?;
323 if !success {
324 let _ = self.state.statements.pop();
325 self.persist_source()?;
326 }
327 Ok((outcome, success))
328 }
329
330 fn reset(&mut self) -> Result<()> {
331 self.state.imports.clear();
332 self.state.declarations.clear();
333 self.state.statements.clear();
334 self.previous_stdout.clear();
335 self.previous_stderr.clear();
336 self.persist_source()
337 }
338}
339
340impl LanguageSession for SwiftSession {
341 fn language_id(&self) -> &str {
342 "swift"
343 }
344
345 fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
346 let trimmed = code.trim();
347 if trimmed.is_empty() {
348 return Ok(ExecutionOutcome {
349 language: "swift".to_string(),
350 exit_code: None,
351 stdout: String::new(),
352 stderr: String::new(),
353 duration: Duration::default(),
354 });
355 }
356
357 if trimmed.eq_ignore_ascii_case(":reset") {
358 self.reset()?;
359 return Ok(ExecutionOutcome {
360 language: "swift".to_string(),
361 exit_code: None,
362 stdout: String::new(),
363 stderr: String::new(),
364 duration: Duration::default(),
365 });
366 }
367
368 if trimmed.eq_ignore_ascii_case(":help") {
369 return Ok(ExecutionOutcome {
370 language: "swift".to_string(),
371 exit_code: None,
372 stdout:
373 "Swift commands:\n :reset - clear session state\n :help - show this message\n"
374 .to_string(),
375 stderr: String::new(),
376 duration: Duration::default(),
377 });
378 }
379
380 match classify_snippet(trimmed) {
381 SwiftSnippet::Import => {
382 let (outcome, _) = self.apply_import(code)?;
383 Ok(outcome)
384 }
385 SwiftSnippet::Declaration => {
386 let (outcome, _) = self.apply_declaration(code)?;
387 Ok(outcome)
388 }
389 SwiftSnippet::Expression => {
390 let (outcome, _) = self.apply_expression(trimmed)?;
391 Ok(outcome)
392 }
393 SwiftSnippet::Statement => {
394 let (outcome, _) = self.apply_statement(code)?;
395 Ok(outcome)
396 }
397 }
398 }
399
400 fn shutdown(&mut self) -> Result<()> {
401 Ok(())
402 }
403}
404
405enum SwiftSnippet {
406 Import,
407 Declaration,
408 Statement,
409 Expression,
410}
411
412fn classify_snippet(code: &str) -> SwiftSnippet {
413 if is_import(code) {
414 return SwiftSnippet::Import;
415 }
416
417 if is_declaration(code) {
418 return SwiftSnippet::Declaration;
419 }
420
421 if should_wrap_expression(code) {
422 return SwiftSnippet::Expression;
423 }
424
425 SwiftSnippet::Statement
426}
427
428fn is_import(code: &str) -> bool {
429 code.lines()
430 .all(|line| line.trim_start().starts_with("import "))
431}
432
433fn is_declaration(code: &str) -> bool {
434 let lowered = code.trim_start().to_ascii_lowercase();
435 const PREFIXES: [&str; 8] = [
436 "func ",
437 "class ",
438 "struct ",
439 "enum ",
440 "protocol ",
441 "extension ",
442 "actor ",
443 "typealias ",
444 ];
445 PREFIXES.iter().any(|prefix| lowered.starts_with(prefix))
446}
447
448fn should_wrap_expression(code: &str) -> bool {
449 if code.contains('\n') {
450 return false;
451 }
452
453 let trimmed = code.trim();
454 if trimmed.is_empty() {
455 return false;
456 }
457
458 if trimmed.ends_with(';') {
459 return false;
460 }
461
462 let lowered = trimmed.to_ascii_lowercase();
463 const STATEMENT_PREFIXES: [&str; 10] = [
464 "let ", "var ", "if ", "for ", "while ", "repeat ", "guard ", "switch ", "return ",
465 "throw ",
466 ];
467
468 if STATEMENT_PREFIXES
469 .iter()
470 .any(|prefix| lowered.starts_with(prefix))
471 {
472 return false;
473 }
474
475 if trimmed.contains('=') {
476 return false;
477 }
478
479 true
480}
481
482fn ensure_trailing_newline(code: &str) -> String {
483 let mut owned = code.to_string();
484 if !owned.ends_with('\n') {
485 owned.push('\n');
486 }
487 owned
488}
489
490fn wrap_expression(code: &str) -> String {
491 format!("print(({}))\n", code.trim())
492}
493
494fn diff_output(previous: &str, current: &str) -> String {
495 if let Some(stripped) = current.strip_prefix(previous) {
496 stripped.to_string()
497 } else {
498 current.to_string()
499 }
500}
501
502fn normalize_output(bytes: &[u8]) -> String {
503 String::from_utf8_lossy(bytes)
504 .replace("\r\n", "\n")
505 .replace('\r', "")
506}