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