Skip to main content

run/engine/
go.rs

1use std::collections::BTreeSet;
2use std::fs;
3use std::path::{Path, PathBuf};
4use std::process::{Command, Stdio};
5use std::time::Instant;
6
7use anyhow::{Context, Result};
8use tempfile::{Builder, TempDir};
9
10use super::{
11    ExecutionOutcome, ExecutionPayload, LanguageEngine, LanguageSession, cache_store,
12    execution_timeout, hash_source, perf_record, run_version_command, try_cached_execution,
13    wait_with_timeout,
14};
15
16pub struct GoEngine {
17    executable: Option<PathBuf>,
18}
19
20impl Default for GoEngine {
21    fn default() -> Self {
22        Self::new()
23    }
24}
25
26impl GoEngine {
27    pub fn new() -> Self {
28        Self {
29            executable: resolve_go_binary(),
30        }
31    }
32
33    fn ensure_executable(&self) -> Result<&Path> {
34        self.executable.as_deref().ok_or_else(|| {
35            anyhow::anyhow!(
36                "Go support requires the `go` executable. Install it from https://go.dev/dl/ and ensure it is on your PATH."
37            )
38        })
39    }
40
41    fn write_temp_source(&self, code: &str) -> Result<(tempfile::TempDir, PathBuf)> {
42        let dir = Builder::new()
43            .prefix("run-go")
44            .tempdir()
45            .context("failed to create temporary directory for go source")?;
46        let path = dir.path().join("main.go");
47        let mut contents = code.to_string();
48        if !contents.ends_with('\n') {
49            contents.push('\n');
50        }
51        std::fs::write(&path, contents).with_context(|| {
52            format!("failed to write temporary Go source to {}", path.display())
53        })?;
54        Ok((dir, path))
55    }
56
57    fn execute_with_path(
58        &self,
59        binary: &Path,
60        source: &Path,
61        args: &[String],
62    ) -> Result<std::process::Output> {
63        let mut cmd = Command::new(binary);
64        cmd.arg("run")
65            .stdout(Stdio::piped())
66            .stderr(Stdio::piped())
67            .env("GO111MODULE", "off");
68        cmd.stdin(Stdio::inherit());
69
70        if let Some(parent) = source.parent() {
71            cmd.current_dir(parent);
72            if let Some(file_name) = source.file_name() {
73                cmd.arg(file_name);
74            } else {
75                cmd.arg(source);
76            }
77            cmd.args(args);
78        } else {
79            cmd.arg(source).args(args);
80        }
81        let child = cmd.spawn().with_context(|| {
82            format!(
83                "failed to invoke {} to run {}",
84                binary.display(),
85                source.display()
86            )
87        })?;
88        wait_with_timeout(child, execution_timeout())
89    }
90}
91
92impl LanguageEngine for GoEngine {
93    fn id(&self) -> &'static str {
94        "go"
95    }
96
97    fn display_name(&self) -> &'static str {
98        "Go"
99    }
100
101    fn aliases(&self) -> &[&'static str] {
102        &["golang"]
103    }
104
105    fn supports_sessions(&self) -> bool {
106        true
107    }
108
109    fn validate(&self) -> Result<()> {
110        let binary = self.ensure_executable()?;
111        let mut cmd = Command::new(binary);
112        cmd.arg("version")
113            .stdout(Stdio::null())
114            .stderr(Stdio::null());
115        cmd.status()
116            .with_context(|| format!("failed to invoke {}", binary.display()))?
117            .success()
118            .then_some(())
119            .ok_or_else(|| anyhow::anyhow!("{} is not executable", binary.display()))
120    }
121
122    fn toolchain_version(&self) -> Result<Option<String>> {
123        let binary = self.ensure_executable()?;
124        let mut cmd = Command::new(binary);
125        cmd.arg("version");
126        let context = format!("{}", binary.display());
127        run_version_command(cmd, &context)
128    }
129
130    fn execute(&self, payload: &ExecutionPayload) -> Result<ExecutionOutcome> {
131        // Try cache for inline/stdin payloads
132        let args = payload.args();
133        if let ExecutionPayload::File { path, .. } = payload {
134            let start = Instant::now();
135            let source_text = fs::read_to_string(path).unwrap_or_default();
136            let src_hash = hash_source(&source_text);
137            if let Some(output) = try_cached_execution("go-file", src_hash) {
138                perf_record("go", "file.cache_hit");
139                return Ok(ExecutionOutcome {
140                    language: self.id().to_string(),
141                    exit_code: output.status.code(),
142                    stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
143                    stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
144                    duration: start.elapsed(),
145                });
146            }
147            perf_record("go", "file.cache_miss");
148
149            let binary = self.ensure_executable()?;
150            let temp_dir = Builder::new()
151                .prefix("run-go-file")
152                .tempdir()
153                .context("failed to create temporary directory for go file build")?;
154            let bin_path = temp_dir.path().join("run_go_file_binary");
155            let mut build_cmd = Command::new(binary);
156            perf_record("go", "file.build");
157            build_cmd
158                .arg("build")
159                .arg("-o")
160                .arg(&bin_path)
161                .arg(path)
162                .env("GO111MODULE", "off")
163                .stdout(Stdio::piped())
164                .stderr(Stdio::piped());
165            let build_output = build_cmd.output().with_context(|| {
166                format!("failed to invoke {} to build Go source", binary.display())
167            })?;
168            if !build_output.status.success() {
169                perf_record("go", "file.build_fail");
170                return Ok(ExecutionOutcome {
171                    language: self.id().to_string(),
172                    exit_code: build_output.status.code(),
173                    stdout: String::from_utf8_lossy(&build_output.stdout).into_owned(),
174                    stderr: String::from_utf8_lossy(&build_output.stderr).into_owned(),
175                    duration: start.elapsed(),
176                });
177            }
178
179            let cached_bin =
180                cache_store("go-file", src_hash, &bin_path).unwrap_or(bin_path.clone());
181            let mut run_cmd = Command::new(&cached_bin);
182            run_cmd
183                .args(args)
184                .stdout(Stdio::piped())
185                .stderr(Stdio::piped())
186                .stdin(Stdio::inherit());
187            let child = run_cmd.spawn().with_context(|| {
188                format!(
189                    "failed to execute compiled Go binary {}",
190                    cached_bin.display()
191                )
192            })?;
193            let output = wait_with_timeout(child, execution_timeout())?;
194            return Ok(ExecutionOutcome {
195                language: self.id().to_string(),
196                exit_code: output.status.code(),
197                stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
198                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
199                duration: start.elapsed(),
200            });
201        }
202
203        if let Some(code) = match payload {
204            ExecutionPayload::Inline { code, .. } | ExecutionPayload::Stdin { code, .. } => {
205                Some(code.as_str())
206            }
207            _ => None,
208        } {
209            let src_hash = hash_source(code);
210            if let Some(output) = try_cached_execution("go", src_hash) {
211                perf_record("go", "inline.cache_hit");
212                let start = Instant::now();
213                return Ok(ExecutionOutcome {
214                    language: self.id().to_string(),
215                    exit_code: output.status.code(),
216                    stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
217                    stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
218                    duration: start.elapsed(),
219                });
220            }
221            perf_record("go", "inline.cache_miss");
222        }
223
224        let binary = self.ensure_executable()?;
225        let start = Instant::now();
226
227        let (temp_dir, source_path, cache_key) = match payload {
228            ExecutionPayload::Inline { code, .. } => {
229                let h = hash_source(code);
230                let (dir, path) = self.write_temp_source(code)?;
231                (Some(dir), path, Some(h))
232            }
233            ExecutionPayload::Stdin { code, .. } => {
234                let h = hash_source(code);
235                let (dir, path) = self.write_temp_source(code)?;
236                (Some(dir), path, Some(h))
237            }
238            ExecutionPayload::File { path, .. } => (None, path.clone(), None),
239        };
240
241        // For cacheable code, use go build + run instead of go run
242        if let Some(h) = cache_key {
243            let dir = source_path.parent().unwrap_or(std::path::Path::new("."));
244            let bin_path = dir.join("run_go_binary");
245            let mut build_cmd = Command::new(binary);
246            build_cmd
247                .arg("build")
248                .arg("-o")
249                .arg(&bin_path)
250                .env("GO111MODULE", "off")
251                .stdout(Stdio::piped())
252                .stderr(Stdio::piped());
253            if let Some(file_name) = source_path.file_name() {
254                build_cmd.current_dir(dir).arg(file_name);
255            } else {
256                build_cmd.arg(&source_path);
257            }
258
259            let build_output = build_cmd.output().with_context(|| {
260                format!("failed to invoke {} to build Go source", binary.display())
261            })?;
262
263            if !build_output.status.success() {
264                return Ok(ExecutionOutcome {
265                    language: self.id().to_string(),
266                    exit_code: build_output.status.code(),
267                    stdout: String::from_utf8_lossy(&build_output.stdout).into_owned(),
268                    stderr: String::from_utf8_lossy(&build_output.stderr).into_owned(),
269                    duration: start.elapsed(),
270                });
271            }
272
273            cache_store("go", h, &bin_path);
274
275            let mut run_cmd = Command::new(&bin_path);
276            run_cmd
277                .args(args)
278                .stdout(Stdio::piped())
279                .stderr(Stdio::piped())
280                .stdin(Stdio::inherit());
281            let child = run_cmd.spawn().with_context(|| {
282                format!(
283                    "failed to execute compiled Go binary {}",
284                    bin_path.display()
285                )
286            })?;
287            let output = wait_with_timeout(child, execution_timeout())?;
288
289            drop(temp_dir);
290            return Ok(ExecutionOutcome {
291                language: self.id().to_string(),
292                exit_code: output.status.code(),
293                stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
294                stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
295                duration: start.elapsed(),
296            });
297        }
298
299        let output = self.execute_with_path(binary, &source_path, args)?;
300        drop(temp_dir);
301
302        Ok(ExecutionOutcome {
303            language: self.id().to_string(),
304            exit_code: output.status.code(),
305            stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
306            stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
307            duration: start.elapsed(),
308        })
309    }
310
311    fn start_session(&self) -> Result<Box<dyn LanguageSession>> {
312        let binary = self.ensure_executable()?.to_path_buf();
313        let session = GoSession::new(binary)?;
314        Ok(Box::new(session))
315    }
316}
317
318fn resolve_go_binary() -> Option<PathBuf> {
319    which::which("go").ok()
320}
321
322fn import_is_used_in_code(import: &str, code: &str) -> bool {
323    let import_trimmed = import.trim().trim_matches('"');
324    let package_name = import_trimmed.rsplit('/').next().unwrap_or(import_trimmed);
325    let pattern = format!("{}.", package_name);
326    code.contains(&pattern)
327}
328
329const SESSION_MAIN_FILE: &str = "main.go";
330
331struct GoSession {
332    go_binary: PathBuf,
333    workspace: TempDir,
334    imports: BTreeSet<String>,
335    items: Vec<String>,
336    statements: Vec<String>,
337    last_stdout: String,
338    last_stderr: String,
339}
340
341enum GoSnippetKind {
342    Import(Option<String>),
343    Item,
344    Statement,
345}
346
347impl GoSession {
348    fn new(go_binary: PathBuf) -> Result<Self> {
349        let workspace = TempDir::new().context("failed to create Go session workspace")?;
350        let mut imports = BTreeSet::new();
351        imports.insert("\"fmt\"".to_string());
352        let session = Self {
353            go_binary,
354            workspace,
355            imports,
356            items: Vec::new(),
357            statements: Vec::new(),
358            last_stdout: String::new(),
359            last_stderr: String::new(),
360        };
361        session.persist_source()?;
362        Ok(session)
363    }
364
365    fn language_id(&self) -> &str {
366        "go"
367    }
368
369    fn source_path(&self) -> PathBuf {
370        self.workspace.path().join(SESSION_MAIN_FILE)
371    }
372
373    fn persist_source(&self) -> Result<()> {
374        let source = self.render_source();
375        fs::write(self.source_path(), source)
376            .with_context(|| "failed to write Go session source".to_string())
377    }
378
379    fn render_source(&self) -> String {
380        let mut source = String::from("package main\n\n");
381
382        if !self.imports.is_empty() {
383            source.push_str("import (\n");
384            for import in &self.imports {
385                source.push('\t');
386                source.push_str(import);
387                source.push('\n');
388            }
389            source.push_str(")\n\n");
390        }
391
392        source.push_str(concat!(
393            "func __print(value interface{}) {\n",
394            "\tif s, ok := value.(string); ok {\n",
395            "\t\tfmt.Println(s)\n",
396            "\t\treturn\n",
397            "\t}\n",
398            "\tfmt.Printf(\"%#v\\n\", value)\n",
399            "}\n\n",
400        ));
401
402        for item in &self.items {
403            source.push_str(item);
404            if !item.ends_with('\n') {
405                source.push('\n');
406            }
407            source.push('\n');
408        }
409
410        source.push_str("func main() {\n");
411        if self.statements.is_empty() {
412            source.push_str("\t// session body\n");
413        } else {
414            for snippet in &self.statements {
415                for line in snippet.lines() {
416                    source.push('\t');
417                    source.push_str(line);
418                    source.push('\n');
419                }
420            }
421        }
422        source.push_str("}\n");
423
424        source
425    }
426
427    fn run_program(&self) -> Result<std::process::Output> {
428        let mut cmd = Command::new(&self.go_binary);
429        cmd.arg("run")
430            .arg(SESSION_MAIN_FILE)
431            .env("GO111MODULE", "off")
432            .stdout(Stdio::piped())
433            .stderr(Stdio::piped())
434            .current_dir(self.workspace.path());
435        cmd.output().with_context(|| {
436            format!(
437                "failed to execute {} for Go session",
438                self.go_binary.display()
439            )
440        })
441    }
442
443    fn run_standalone_program(&self, code: &str) -> Result<ExecutionOutcome> {
444        let start = Instant::now();
445        let standalone_path = self.workspace.path().join("standalone.go");
446
447        let source = if has_package_declaration(code) {
448            let mut snippet = code.to_string();
449            if !snippet.ends_with('\n') {
450                snippet.push('\n');
451            }
452            snippet
453        } else {
454            let mut source = String::from("package main\n\n");
455
456            let used_imports: Vec<_> = self
457                .imports
458                .iter()
459                .filter(|import| import_is_used_in_code(import, code))
460                .cloned()
461                .collect();
462
463            if !used_imports.is_empty() {
464                source.push_str("import (\n");
465                for import in &used_imports {
466                    source.push('\t');
467                    source.push_str(import);
468                    source.push('\n');
469                }
470                source.push_str(")\n\n");
471            }
472
473            source.push_str(code);
474            if !code.ends_with('\n') {
475                source.push('\n');
476            }
477            source
478        };
479
480        fs::write(&standalone_path, source)
481            .with_context(|| "failed to write Go standalone source".to_string())?;
482
483        let mut cmd = Command::new(&self.go_binary);
484        cmd.arg("run")
485            .arg("standalone.go")
486            .env("GO111MODULE", "off")
487            .stdout(Stdio::piped())
488            .stderr(Stdio::piped())
489            .current_dir(self.workspace.path());
490
491        let output = cmd.output().with_context(|| {
492            format!(
493                "failed to execute {} for Go standalone program",
494                self.go_binary.display()
495            )
496        })?;
497
498        let outcome = ExecutionOutcome {
499            language: self.language_id().to_string(),
500            exit_code: output.status.code(),
501            stdout: Self::normalize_output(&output.stdout),
502            stderr: Self::normalize_output(&output.stderr),
503            duration: start.elapsed(),
504        };
505
506        let _ = fs::remove_file(&standalone_path);
507
508        Ok(outcome)
509    }
510
511    fn add_import(&mut self, spec: &str) -> GoSnippetKind {
512        let added = self.imports.insert(spec.to_string());
513        if added {
514            GoSnippetKind::Import(Some(spec.to_string()))
515        } else {
516            GoSnippetKind::Import(None)
517        }
518    }
519
520    fn add_item(&mut self, code: &str) -> GoSnippetKind {
521        let mut snippet = code.to_string();
522        if !snippet.ends_with('\n') {
523            snippet.push('\n');
524        }
525        self.items.push(snippet);
526        GoSnippetKind::Item
527    }
528
529    fn add_statement(&mut self, code: &str) -> GoSnippetKind {
530        let snippet = sanitize_statement(code);
531        self.statements.push(snippet);
532        GoSnippetKind::Statement
533    }
534
535    fn add_expression(&mut self, code: &str) -> GoSnippetKind {
536        let wrapped = wrap_expression(code);
537        self.statements.push(wrapped);
538        GoSnippetKind::Statement
539    }
540
541    fn rollback(&mut self, kind: GoSnippetKind) -> Result<()> {
542        match kind {
543            GoSnippetKind::Import(Some(spec)) => {
544                self.imports.remove(&spec);
545            }
546            GoSnippetKind::Import(None) => {}
547            GoSnippetKind::Item => {
548                self.items.pop();
549            }
550            GoSnippetKind::Statement => {
551                self.statements.pop();
552            }
553        }
554        self.persist_source()
555    }
556
557    fn normalize_output(bytes: &[u8]) -> String {
558        String::from_utf8_lossy(bytes)
559            .replace("\r\n", "\n")
560            .replace('\r', "")
561    }
562
563    fn diff_outputs(previous: &str, current: &str) -> String {
564        if let Some(suffix) = current.strip_prefix(previous) {
565            suffix.to_string()
566        } else {
567            current.to_string()
568        }
569    }
570
571    fn run_insertion(&mut self, kind: GoSnippetKind) -> Result<(ExecutionOutcome, bool)> {
572        match kind {
573            GoSnippetKind::Import(None) => Ok((
574                ExecutionOutcome {
575                    language: self.language_id().to_string(),
576                    exit_code: None,
577                    stdout: String::new(),
578                    stderr: String::new(),
579                    duration: Default::default(),
580                },
581                true,
582            )),
583            other_kind => {
584                self.persist_source()?;
585                let start = Instant::now();
586                let output = self.run_program()?;
587
588                let stdout_full = Self::normalize_output(&output.stdout);
589                let stderr_full = Self::normalize_output(&output.stderr);
590
591                let stdout = Self::diff_outputs(&self.last_stdout, &stdout_full);
592                let stderr = Self::diff_outputs(&self.last_stderr, &stderr_full);
593                let duration = start.elapsed();
594
595                if output.status.success() {
596                    self.last_stdout = stdout_full;
597                    self.last_stderr = stderr_full;
598                    let outcome = ExecutionOutcome {
599                        language: self.language_id().to_string(),
600                        exit_code: output.status.code(),
601                        stdout,
602                        stderr,
603                        duration,
604                    };
605                    return Ok((outcome, true));
606                }
607
608                if matches!(&other_kind, GoSnippetKind::Import(Some(_)))
609                    && stderr_full.contains("imported and not used")
610                {
611                    return Ok((
612                        ExecutionOutcome {
613                            language: self.language_id().to_string(),
614                            exit_code: None,
615                            stdout: String::new(),
616                            stderr: String::new(),
617                            duration,
618                        },
619                        true,
620                    ));
621                }
622
623                self.rollback(other_kind)?;
624                let outcome = ExecutionOutcome {
625                    language: self.language_id().to_string(),
626                    exit_code: output.status.code(),
627                    stdout,
628                    stderr,
629                    duration,
630                };
631                Ok((outcome, false))
632            }
633        }
634    }
635
636    fn run_import(&mut self, spec: &str) -> Result<(ExecutionOutcome, bool)> {
637        let kind = self.add_import(spec);
638        self.run_insertion(kind)
639    }
640
641    fn run_item(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
642        let kind = self.add_item(code);
643        self.run_insertion(kind)
644    }
645
646    fn run_statement(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
647        let kind = self.add_statement(code);
648        self.run_insertion(kind)
649    }
650
651    fn run_expression(&mut self, code: &str) -> Result<(ExecutionOutcome, bool)> {
652        let kind = self.add_expression(code);
653        self.run_insertion(kind)
654    }
655}
656
657impl LanguageSession for GoSession {
658    fn language_id(&self) -> &str {
659        GoSession::language_id(self)
660    }
661
662    fn eval(&mut self, code: &str) -> Result<ExecutionOutcome> {
663        let trimmed = code.trim();
664        if trimmed.is_empty() {
665            return Ok(ExecutionOutcome {
666                language: self.language_id().to_string(),
667                exit_code: None,
668                stdout: String::new(),
669                stderr: String::new(),
670                duration: Instant::now().elapsed(),
671            });
672        }
673
674        if trimmed.starts_with("package ") && !trimmed.contains('\n') {
675            return Ok(ExecutionOutcome {
676                language: self.language_id().to_string(),
677                exit_code: None,
678                stdout: String::new(),
679                stderr: String::new(),
680                duration: Instant::now().elapsed(),
681            });
682        }
683
684        if contains_main_definition(trimmed) {
685            let outcome = self.run_standalone_program(code)?;
686            return Ok(outcome);
687        }
688
689        if let Some(import) = parse_import_spec(trimmed) {
690            let (outcome, _) = self.run_import(&import)?;
691            return Ok(outcome);
692        }
693
694        if is_item_snippet(trimmed) {
695            let (outcome, _) = self.run_item(code)?;
696            return Ok(outcome);
697        }
698
699        if should_treat_as_expression(trimmed) {
700            let (outcome, success) = self.run_expression(trimmed)?;
701            if success {
702                return Ok(outcome);
703            }
704        }
705
706        let (outcome, _) = self.run_statement(code)?;
707        Ok(outcome)
708    }
709
710    fn shutdown(&mut self) -> Result<()> {
711        Ok(())
712    }
713}
714
715fn parse_import_spec(code: &str) -> Option<String> {
716    let trimmed = code.trim_start();
717    if !trimmed.starts_with("import ") {
718        return None;
719    }
720    let rest = trimmed.trim_start_matches("import").trim();
721    if rest.is_empty() || rest.starts_with('(') {
722        return None;
723    }
724    Some(rest.to_string())
725}
726
727fn is_item_snippet(code: &str) -> bool {
728    let trimmed = code.trim_start();
729    if trimmed.is_empty() {
730        return false;
731    }
732    const KEYWORDS: [&str; 6] = ["type", "const", "var", "func", "package", "import"];
733    KEYWORDS.iter().any(|kw| {
734        trimmed.starts_with(kw)
735            && trimmed
736                .chars()
737                .nth(kw.len())
738                .map(|ch| ch.is_whitespace() || ch == '(')
739                .unwrap_or(true)
740    })
741}
742
743fn should_treat_as_expression(code: &str) -> bool {
744    let trimmed = code.trim();
745    if trimmed.is_empty() {
746        return false;
747    }
748    if trimmed.contains('\n') {
749        return false;
750    }
751    if trimmed.ends_with(';') {
752        return false;
753    }
754    if trimmed.contains(":=") {
755        return false;
756    }
757    if trimmed.contains('=') && !trimmed.contains("==") {
758        return false;
759    }
760    const RESERVED: [&str; 8] = [
761        "if ", "for ", "switch ", "select ", "return ", "go ", "defer ", "var ",
762    ];
763    if RESERVED.iter().any(|kw| trimmed.starts_with(kw)) {
764        return false;
765    }
766    true
767}
768
769fn wrap_expression(code: &str) -> String {
770    format!("__print({});\n", code)
771}
772
773fn sanitize_statement(code: &str) -> String {
774    let mut snippet = code.to_string();
775    if !snippet.ends_with('\n') {
776        snippet.push('\n');
777    }
778
779    let trimmed = code.trim();
780    if trimmed.is_empty() || trimmed.contains('\n') {
781        return snippet;
782    }
783
784    let mut identifiers: Vec<String> = Vec::new();
785
786    if let Some(idx) = trimmed.find(" :=") {
787        let lhs = &trimmed[..idx];
788        identifiers = lhs
789            .split(',')
790            .map(|part| part.trim())
791            .filter(|name| !name.is_empty() && *name != "_")
792            .map(|name| name.to_string())
793            .collect();
794    } else if let Some(idx) = trimmed.find(':') {
795        if trimmed[idx..].starts_with(":=") {
796            let lhs = &trimmed[..idx];
797            identifiers = lhs
798                .split(',')
799                .map(|part| part.trim())
800                .filter(|name| !name.is_empty() && *name != "_")
801                .map(|name| name.to_string())
802                .collect();
803        }
804    } else if let Some(stripped) = trimmed.strip_prefix("var ") {
805        let rest = stripped.trim();
806        if !rest.starts_with('(') {
807            let names_part = rest.split('=').next().unwrap_or(rest).trim();
808            identifiers = names_part
809                .split(',')
810                .filter_map(|segment| {
811                    let token = segment.split_whitespace().next().unwrap_or("");
812                    if token.is_empty() || token == "_" {
813                        None
814                    } else {
815                        Some(token.to_string())
816                    }
817                })
818                .collect();
819        }
820    } else if let Some(stripped) = trimmed.strip_prefix("const ") {
821        let rest = stripped.trim();
822        if !rest.starts_with('(') {
823            let names_part = rest.split('=').next().unwrap_or(rest).trim();
824            identifiers = names_part
825                .split(',')
826                .filter_map(|segment| {
827                    let token = segment.split_whitespace().next().unwrap_or("");
828                    if token.is_empty() || token == "_" {
829                        None
830                    } else {
831                        Some(token.to_string())
832                    }
833                })
834                .collect();
835        }
836    }
837
838    if identifiers.is_empty() {
839        return snippet;
840    }
841
842    for name in identifiers {
843        snippet.push_str("_ = ");
844        snippet.push_str(&name);
845        snippet.push('\n');
846    }
847
848    snippet
849}
850
851fn has_package_declaration(code: &str) -> bool {
852    code.lines()
853        .any(|line| line.trim_start().starts_with("package "))
854}
855
856fn contains_main_definition(code: &str) -> bool {
857    let bytes = code.as_bytes();
858    let len = bytes.len();
859    let mut i = 0;
860    let mut in_line_comment = false;
861    let mut in_block_comment = false;
862    let mut in_string = false;
863    let mut string_delim = b'"';
864    let mut in_char = false;
865
866    while i < len {
867        let b = bytes[i];
868
869        if in_line_comment {
870            if b == b'\n' {
871                in_line_comment = false;
872            }
873            i += 1;
874            continue;
875        }
876
877        if in_block_comment {
878            if b == b'*' && i + 1 < len && bytes[i + 1] == b'/' {
879                in_block_comment = false;
880                i += 2;
881                continue;
882            }
883            i += 1;
884            continue;
885        }
886
887        if in_string {
888            if b == b'\\' {
889                i = (i + 2).min(len);
890                continue;
891            }
892            if b == string_delim {
893                in_string = false;
894            }
895            i += 1;
896            continue;
897        }
898
899        if in_char {
900            if b == b'\\' {
901                i = (i + 2).min(len);
902                continue;
903            }
904            if b == b'\'' {
905                in_char = false;
906            }
907            i += 1;
908            continue;
909        }
910
911        match b {
912            b'/' if i + 1 < len && bytes[i + 1] == b'/' => {
913                in_line_comment = true;
914                i += 2;
915                continue;
916            }
917            b'/' if i + 1 < len && bytes[i + 1] == b'*' => {
918                in_block_comment = true;
919                i += 2;
920                continue;
921            }
922            b'"' | b'`' => {
923                in_string = true;
924                string_delim = b;
925                i += 1;
926                continue;
927            }
928            b'\'' => {
929                in_char = true;
930                i += 1;
931                continue;
932            }
933            b'f' if i + 4 <= len && &bytes[i..i + 4] == b"func" => {
934                if i > 0 {
935                    let prev = bytes[i - 1];
936                    if prev.is_ascii_alphanumeric() || prev == b'_' {
937                        i += 1;
938                        continue;
939                    }
940                }
941
942                let mut j = i + 4;
943                while j < len && bytes[j].is_ascii_whitespace() {
944                    j += 1;
945                }
946
947                if j + 4 > len || &bytes[j..j + 4] != b"main" {
948                    i += 1;
949                    continue;
950                }
951
952                let after = j + 4;
953                if after < len {
954                    let ch = bytes[after];
955                    if ch.is_ascii_alphanumeric() || ch == b'_' {
956                        i += 1;
957                        continue;
958                    }
959                }
960
961                let mut k = after;
962                while k < len && bytes[k].is_ascii_whitespace() {
963                    k += 1;
964                }
965                if k < len && bytes[k] == b'(' {
966                    return true;
967                }
968            }
969            _ => {}
970        }
971
972        i += 1;
973    }
974
975    false
976}