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 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 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}