Skip to main content

run/engine/
rust.rs

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            // Favor faster compile turnaround for REPL/runner scenarios.
46            .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            // Rehydrate persistent cache even when incremental workspace is already up-to-date.
158            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        // Try cache for inline/stdin payloads
250        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        // Store in cache before running
311        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}