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