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