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