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 =
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            // Rehydrate persistent cache even when incremental workspace is already up-to-date.
156            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        // Try cache for inline/stdin payloads
248        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        // Store in cache before running
309        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}