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