Skip to main content

stryke/
lib.rs

1//! Crate root — see [`README.md`](https://github.com/MenkeTechnologies/stryke) for overview.
2// `cargo doc` with `RUSTDOCFLAGS=-D warnings` (CI) flags intra-doc links to private items and
3// a few shorthand links (`MethodCall`, `Op::…`) that do not resolve as paths. Suppress until
4// docs are normalized to `crate::…` paths and public-only links.
5#![allow(rustdoc::private_intra_doc_links)]
6#![allow(rustdoc::broken_intra_doc_links)]
7#![allow(clippy::needless_range_loop)]
8
9pub mod agent;
10pub mod ai;
11pub mod ai_sugar;
12pub mod aop;
13pub mod aot;
14pub mod ast;
15pub mod builtins;
16pub mod bytecode;
17pub mod capture;
18pub mod cluster;
19pub mod compiler;
20pub mod controller;
21pub mod convert;
22mod crypt_util;
23pub mod data_section;
24pub mod debugger;
25pub mod deconvert;
26pub mod deparse;
27pub mod english;
28pub mod error;
29mod fib_like_tail;
30pub mod fmt;
31pub mod format;
32mod jit;
33mod jwt;
34pub mod lexer;
35pub mod list_builtins;
36pub mod lsp;
37mod map_grep_fast;
38mod map_stream;
39pub mod mcp;
40pub mod mro;
41mod nanbox;
42mod native_codec;
43pub mod native_data;
44pub mod pack;
45pub mod par_lines;
46mod par_list;
47pub mod par_pipeline;
48pub mod par_walk;
49pub mod parallel_trace;
50pub mod parser;
51pub mod pcache;
52pub mod pchannel;
53mod pending_destroy;
54pub mod perl_decode;
55pub mod perl_fs;
56pub mod perl_inc;
57#[cfg(unix)]
58pub mod perl_pty;
59mod perl_regex;
60pub mod perl_signal;
61pub mod pkg;
62mod pmap_progress;
63pub mod ppool;
64pub mod profiler;
65pub mod pwatch;
66pub mod remote_wire;
67pub mod rust_ffi;
68pub mod rust_sugar;
69pub mod scope;
70pub mod script_cache;
71pub mod secrets;
72pub mod serialize_normalize;
73mod sort_fast;
74pub mod special_vars;
75pub mod static_analysis;
76pub mod stress;
77pub mod token;
78pub mod value;
79pub mod vm;
80pub mod vm_helper;
81pub mod web;
82pub mod web_orm;
83
84// Re-export shell components from the zsh crate
85pub use zsh::exec as shell_exec;
86pub use zsh::fds as shell_fds;
87pub use zsh::history as shell_history;
88pub use zsh::jobs as shell_jobs;
89pub use zsh::lexer as zsh_lex;
90pub use zsh::parser as shell_parse;
91pub use zsh::parser as zsh_parse;
92pub use zsh::signals as shell_signal;
93pub use zsh::tokens as zsh_tokens;
94pub use zsh::zle as shell_zle;
95pub use zsh::zwc as shell_zwc;
96
97pub use vm_helper::{
98    perl_bracket_version, FEAT_SAY, FEAT_STATE, FEAT_SWITCH, FEAT_UNICODE_STRINGS,
99};
100
101use error::{PerlError, PerlResult};
102use vm_helper::VMHelper;
103
104// ── Perl 5 strict-compat mode (`--compat`) ──────────────────────────────────
105
106use std::sync::atomic::{AtomicBool, Ordering};
107
108/// When `true`, all stryke extensions are disabled and only stock Perl 5
109/// syntax / builtins are accepted.  Set once from the CLI driver and read by
110/// the parser, compiler, and interpreter.
111static COMPAT_MODE: AtomicBool = AtomicBool::new(false);
112
113/// When `true`, Perl-isms (`sub`, `say`, `reverse`) are rejected — forces
114/// idiomatic stryke (`fn`, `p`, `rev`). Used with `--no-interop` to train
115/// bots or enforce style.
116static NO_INTEROP_MODE: AtomicBool = AtomicBool::new(false);
117
118/// When `true`, integer arithmetic that overflows i64 promotes to `BigInt`
119/// instead of falling back to `f64`. Activated by `use bigint;` and
120/// deactivated by `no bigint;`. Independent of `COMPAT_MODE` so a script
121/// can opt into bigint semantics without dragging in the rest of compat.
122static BIGINT_PRAGMA: AtomicBool = AtomicBool::new(false);
123
124/// Enable Perl 5 strict-compatibility mode (disables all stryke extensions).
125pub fn set_compat_mode(on: bool) {
126    COMPAT_MODE.store(on, Ordering::Relaxed);
127}
128
129/// Returns `true` when `--compat` is active.
130#[inline]
131pub fn compat_mode() -> bool {
132    COMPAT_MODE.load(Ordering::Relaxed)
133}
134
135/// Enable bigint pragma (`use bigint;`) — integer overflow promotes to
136/// `BigInt` instead of demoting to `f64`.
137pub fn set_bigint_pragma(on: bool) {
138    BIGINT_PRAGMA.store(on, Ordering::Relaxed);
139}
140
141/// Returns `true` when `use bigint;` is active in this script.
142#[inline]
143pub fn bigint_pragma() -> bool {
144    BIGINT_PRAGMA.load(Ordering::Relaxed)
145}
146
147/// Enable no-interop mode (rejects Perl-isms, forces idiomatic stryke).
148pub fn set_no_interop_mode(on: bool) {
149    NO_INTEROP_MODE.store(on, Ordering::Relaxed);
150}
151
152/// Returns `true` when `--no-interop` is active.
153#[inline]
154pub fn no_interop_mode() -> bool {
155    NO_INTEROP_MODE.load(Ordering::Relaxed)
156}
157use value::PerlValue;
158
159/// Parse a string of Perl code and return the AST.
160/// Pretty-print a parsed program as Perl-like source (`stryke --fmt`).
161pub fn format_program(p: &ast::Program) -> String {
162    fmt::format_program(p)
163}
164
165/// Convert a parsed program to stryke syntax with `|>` pipes and no semicolons.
166pub fn convert_to_stryke(p: &ast::Program) -> String {
167    convert::convert_program(p)
168}
169
170/// Convert a parsed program to stryke syntax with custom options.
171pub fn convert_to_stryke_with_options(p: &ast::Program, opts: &convert::ConvertOptions) -> String {
172    convert::convert_program_with_options(p, opts)
173}
174
175/// Deconvert a parsed stryke program back to standard Perl .pl syntax.
176pub fn deconvert_to_perl(p: &ast::Program) -> String {
177    deconvert::deconvert_program(p)
178}
179
180/// Deconvert a parsed stryke program back to standard Perl .pl syntax with options.
181pub fn deconvert_to_perl_with_options(
182    p: &ast::Program,
183    opts: &deconvert::DeconvertOptions,
184) -> String {
185    deconvert::deconvert_program_with_options(p, opts)
186}
187
188pub fn parse(code: &str) -> PerlResult<ast::Program> {
189    parse_with_file(code, "-e")
190}
191
192/// Parse with a **source path** for lexer/parser diagnostics (`… at FILE line N`), e.g. a script
193/// path or a required `.pm` absolute path. Use [`parse`] for snippets where `-e` is appropriate.
194pub fn parse_with_file(code: &str, file: &str) -> PerlResult<ast::Program> {
195    parse_with_file_inner(code, file, false)
196}
197
198/// Like [`parse_with_file`], but marks the parser as loading a module. Modules are allowed to
199/// shadow stryke builtins (e.g. `sub blessed { ... }` in Scalar::Util.pm) unless `--no-interop`.
200pub fn parse_module_with_file(code: &str, file: &str) -> PerlResult<ast::Program> {
201    parse_with_file_inner(code, file, true)
202}
203
204fn parse_with_file_inner(code: &str, file: &str, is_module: bool) -> PerlResult<ast::Program> {
205    // `rust { ... }` FFI blocks are desugared at source level into BEGIN-wrapped builtin
206    // calls — the parity roadmap forbids new `StmtKind` variants for new behavior, so this
207    // pre-pass is the right shape. No-op for programs that don't mention `rust`.
208    let desugared = if compat_mode() {
209        code.to_string()
210    } else {
211        let s = rust_sugar::desugar_rust_blocks(code);
212        ai_sugar::desugar(&s)
213    };
214    let mut lexer = lexer::Lexer::new_with_file(&desugared, file);
215    let tokens = lexer.tokenize()?;
216    let bare_positional_indices = std::mem::take(&mut lexer.bare_positional_indices);
217    let mut parser = parser::Parser::new_with_file(tokens, file);
218    parser.bare_positional_indices = bare_positional_indices;
219    parser.parsing_module = is_module;
220    parser.parse_program()
221}
222
223/// Parse and execute a string of Perl code within an existing interpreter.
224/// Compile and execute via the bytecode VM.
225/// Uses [`VMHelper::file`] for both parse diagnostics and `__FILE__` during this execution.
226pub fn parse_and_run_string(code: &str, interp: &mut VMHelper) -> PerlResult<PerlValue> {
227    let file = interp.file.clone();
228    parse_and_run_string_in_file(code, interp, &file)
229}
230
231/// Like [`parse_and_run_string`], but parse errors and `__FILE__` for this run use `file` (e.g. a
232/// required module path). Restores [`VMHelper::file`] after execution.
233pub fn parse_and_run_string_in_file(
234    code: &str,
235    interp: &mut VMHelper,
236    file: &str,
237) -> PerlResult<PerlValue> {
238    parse_and_run_string_in_file_inner(code, interp, file, false)
239}
240
241/// Like [`parse_and_run_string_in_file`], but marks parsing as a module load. Allows shadowing
242/// stryke builtins (e.g. `sub blessed { ... }`) unless `--no-interop` is active.
243pub fn parse_and_run_module_in_file(
244    code: &str,
245    interp: &mut VMHelper,
246    file: &str,
247) -> PerlResult<PerlValue> {
248    parse_and_run_string_in_file_inner(code, interp, file, true)
249}
250
251fn parse_and_run_string_in_file_inner(
252    code: &str,
253    interp: &mut VMHelper,
254    file: &str,
255    is_module: bool,
256) -> PerlResult<PerlValue> {
257    let program = if is_module {
258        parse_module_with_file(code, file)?
259    } else {
260        parse_with_file(code, file)?
261    };
262    let saved = interp.file.clone();
263    interp.file = file.to_string();
264    let r = interp.execute(&program);
265    interp.file = saved;
266    let v = r?;
267    interp.drain_pending_destroys(0)?;
268    Ok(v)
269}
270
271/// Crate-root `vendor/perl` (e.g. `List/Util.pm`). The `stryke` / `stryke` driver prepends this to
272/// `@INC` when the directory exists so in-tree pure-Perl modules shadow XS-only core stubs.
273pub fn vendor_perl_inc_path() -> std::path::PathBuf {
274    std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor/perl")
275}
276
277/// Language server over stdio (`stryke --lsp`). Returns a process exit code.
278pub fn run_lsp_stdio() -> i32 {
279    match lsp::run_stdio() {
280        Ok(()) => 0,
281        Err(e) => {
282            eprintln!("stryke --lsp: {e}");
283            1
284        }
285    }
286}
287
288/// Parse and execute a string of Perl code with a fresh interpreter.
289pub fn run(code: &str) -> PerlResult<PerlValue> {
290    let program = parse(code)?;
291    let mut interp = VMHelper::new();
292    let v = interp.execute(&program)?;
293    interp.run_global_teardown()?;
294    Ok(v)
295}
296
297/// Try to compile and run via bytecode VM. Returns None if compilation fails.
298///
299/// **rkyv bytecode cache.** When `interp.cached_chunk` is populated (from a cache
300/// hit), this function skips `compile_program` entirely and runs the preloaded
301/// chunk. On cache miss the compiler runs normally and, if `interp.cache_script_path`
302/// is set, the fresh chunk + program are persisted to the rkyv shard so the next
303/// run skips lex/parse/compile entirely.
304pub fn try_vm_execute(
305    program: &ast::Program,
306    interp: &mut VMHelper,
307) -> Option<PerlResult<PerlValue>> {
308    if let Err(e) = interp.prepare_program_top_level(program) {
309        return Some(Err(e));
310    }
311
312    // Fast path: chunk loaded from the bytecode cache hit. Consume the slot with `.take()` so a
313    // subsequent re-entry (e.g. nested `do FILE`) does not reuse a stale chunk.
314    if let Some(chunk) = interp.cached_chunk.take() {
315        return Some(run_compiled_chunk(chunk, interp));
316    }
317
318    // `use strict 'vars'` is enforced at compile time by the compiler (see
319    // `Compiler::check_strict_scalar_access` and siblings). `strict refs` / `strict subs` are
320    // enforced by the tree helpers that the VM already delegates into (symbolic deref,
321    // `call_named_sub`, etc.), so they work transitively.
322    let comp = compiler::Compiler::new()
323        .with_source_file(interp.file.clone())
324        .with_strict_vars(interp.strict_vars);
325    let chunk = match comp.compile_program(program) {
326        Ok(chunk) => chunk,
327        Err(compiler::CompileError::Frozen { line, detail }) => {
328            let err = if detail.starts_with("Global symbol") {
329                PerlError::syntax(detail, line)
330            } else {
331                PerlError::runtime(detail, line)
332            };
333            return Some(Err(err));
334        }
335        Err(compiler::CompileError::Unsupported(reason)) => {
336            return Some(Err(PerlError::runtime(
337                format!("VM compile error (unsupported): {}", reason),
338                0,
339            )));
340        }
341    };
342
343    // Save to the bytecode cache (mtime-based, skips lex/parse/compile on 2+ runs)
344    if let Some(path) = interp.cache_script_path.take() {
345        let _ = script_cache::try_save(&path, program, &chunk);
346    }
347    Some(run_compiled_chunk(chunk, interp))
348}
349
350/// Shared execution tail used by both the cache-hit and compile paths in
351/// [`try_vm_execute`]. Pulled out so the rkyv-cache fast path does not duplicate
352/// the flip-flop / BEGIN-END / struct-def wiring every VM run depends on.
353fn run_compiled_chunk(chunk: bytecode::Chunk, interp: &mut VMHelper) -> PerlResult<PerlValue> {
354    interp.clear_flip_flop_state();
355    interp.prepare_flip_flop_vm_slots(chunk.flip_flop_slots);
356    if interp.disasm_bytecode {
357        eprintln!("{}", chunk.disassemble());
358    }
359    interp.clear_begin_end_blocks_after_vm_compile();
360    for def in &chunk.struct_defs {
361        interp
362            .struct_defs
363            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
364    }
365    for def in &chunk.enum_defs {
366        interp
367            .enum_defs
368            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
369    }
370    // Load traits before classes so trait enforcement can reference them
371    for def in &chunk.trait_defs {
372        interp
373            .trait_defs
374            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
375    }
376    for def in &chunk.class_defs {
377        let mut def = def.clone();
378        // Final class/method enforcement
379        for parent_name in &def.extends.clone() {
380            if let Some(parent_def) = interp.class_defs.get(parent_name) {
381                if parent_def.is_final {
382                    return Err(crate::error::PerlError::runtime(
383                        format!("cannot extend final class `{}`", parent_name),
384                        0,
385                    ));
386                }
387                for m in &def.methods {
388                    if let Some(parent_method) = parent_def.method(&m.name) {
389                        if parent_method.is_final {
390                            return Err(crate::error::PerlError::runtime(
391                                format!(
392                                    "cannot override final method `{}` from class `{}`",
393                                    m.name, parent_name
394                                ),
395                                0,
396                            ));
397                        }
398                    }
399                }
400            }
401        }
402        // Trait contract enforcement + default method inheritance
403        for trait_name in &def.implements.clone() {
404            if let Some(trait_def) = interp.trait_defs.get(trait_name) {
405                for required in trait_def.required_methods() {
406                    let has_method = def.methods.iter().any(|m| m.name == required.name);
407                    if !has_method {
408                        return Err(crate::error::PerlError::runtime(
409                            format!(
410                                "class `{}` implements trait `{}` but does not define required method `{}`",
411                                def.name, trait_name, required.name
412                            ),
413                            0,
414                        ));
415                    }
416                }
417                // Inherit default methods from trait (methods with bodies)
418                for tm in &trait_def.methods {
419                    if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
420                        def.methods.push(tm.clone());
421                    }
422                }
423            }
424        }
425        // Abstract method enforcement: concrete subclasses must implement
426        // all abstract methods (body-less methods) from abstract parents
427        if !def.is_abstract {
428            for parent_name in &def.extends.clone() {
429                if let Some(parent_def) = interp.class_defs.get(parent_name) {
430                    if parent_def.is_abstract {
431                        for m in &parent_def.methods {
432                            if m.body.is_none() && !def.methods.iter().any(|dm| dm.name == m.name) {
433                                return Err(crate::error::PerlError::runtime(
434                                    format!(
435                                        "class `{}` must implement abstract method `{}` from `{}`",
436                                        def.name, m.name, parent_name
437                                    ),
438                                    0,
439                                ));
440                            }
441                        }
442                    }
443                }
444            }
445        }
446        // Initialize static fields
447        for sf in &def.static_fields {
448            let val = if let Some(ref expr) = sf.default {
449                match interp.eval_expr(expr) {
450                    Ok(v) => v,
451                    Err(crate::vm_helper::FlowOrError::Error(e)) => return Err(e),
452                    Err(_) => crate::value::PerlValue::UNDEF,
453                }
454            } else {
455                crate::value::PerlValue::UNDEF
456            };
457            let key = format!("{}::{}", def.name, sf.name);
458            interp.scope.declare_scalar(&key, val);
459        }
460        // Register class methods into subs so method dispatch finds them.
461        for m in &def.methods {
462            if let Some(ref body) = m.body {
463                let fq = format!("{}::{}", def.name, m.name);
464                let sub = std::sync::Arc::new(crate::value::PerlSub {
465                    name: fq.clone(),
466                    params: m.params.clone(),
467                    body: body.clone(),
468                    closure_env: None,
469                    prototype: None,
470                    fib_like: None,
471                });
472                interp.subs.insert(fq, sub);
473            }
474        }
475        // Set @ClassName::ISA so MRO/isa resolution works.
476        if !def.extends.is_empty() {
477            let isa_key = format!("{}::ISA", def.name);
478            let parents: Vec<crate::value::PerlValue> = def
479                .extends
480                .iter()
481                .map(|p| crate::value::PerlValue::string(p.clone()))
482                .collect();
483            interp.scope.declare_array(&isa_key, parents);
484        }
485        interp
486            .class_defs
487            .insert(def.name.clone(), std::sync::Arc::new(def));
488    }
489    let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
490    let mut vm = vm::VM::new(&chunk, interp);
491    vm.set_jit_enabled(vm_jit);
492    match vm.execute() {
493        Ok(val) => {
494            interp.drain_pending_destroys(0)?;
495            Ok(val)
496        }
497        // On cache-hit path, surface VM errors directly (we no longer hold the
498        // fresh Program the caller passed). For the cold-compile path, the compiler would
499        // have already returned `Unsupported` for anything the VM cannot run, so this
500        // branch is effectively unreachable there. Either way, surface as a runtime error.
501        Err(e)
502            if e.message.starts_with("VM: unimplemented op")
503                || e.message.starts_with("Unimplemented builtin") =>
504        {
505            Err(PerlError::runtime(e.message, 0))
506        }
507        Err(e) => Err(e),
508    }
509}
510
511/// Compile program and run only the prelude (BEGIN/CHECK/INIT phase blocks) via the VM.
512/// Stores the compiled chunk on `interp.line_mode_chunk` for per-line re-execution.
513pub fn compile_and_run_prelude(program: &ast::Program, interp: &mut VMHelper) -> PerlResult<()> {
514    interp.prepare_program_top_level(program)?;
515    let comp = compiler::Compiler::new()
516        .with_source_file(interp.file.clone())
517        .with_strict_vars(interp.strict_vars);
518    let mut chunk = match comp.compile_program(program) {
519        Ok(chunk) => chunk,
520        Err(compiler::CompileError::Frozen { line, detail }) => {
521            let err = if detail.starts_with("Global symbol") {
522                PerlError::syntax(detail, line)
523            } else {
524                PerlError::runtime(detail, line)
525            };
526            return Err(err);
527        }
528        Err(compiler::CompileError::Unsupported(reason)) => {
529            return Err(PerlError::runtime(
530                format!("VM compile error (unsupported): {}", reason),
531                0,
532            ));
533        }
534    };
535
536    interp.clear_flip_flop_state();
537    interp.prepare_flip_flop_vm_slots(chunk.flip_flop_slots);
538    if interp.disasm_bytecode {
539        eprintln!("{}", chunk.disassemble());
540    }
541    interp.clear_begin_end_blocks_after_vm_compile();
542    for def in &chunk.struct_defs {
543        interp
544            .struct_defs
545            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
546    }
547    for def in &chunk.enum_defs {
548        interp
549            .enum_defs
550            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
551    }
552    for def in &chunk.trait_defs {
553        interp
554            .trait_defs
555            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
556    }
557    for def in &chunk.class_defs {
558        interp
559            .class_defs
560            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
561    }
562    // Register class methods.
563    for def in &chunk.class_defs {
564        for m in &def.methods {
565            if let Some(ref body) = m.body {
566                let fq = format!("{}::{}", def.name, m.name);
567                let sub = std::sync::Arc::new(crate::value::PerlSub {
568                    name: fq.clone(),
569                    params: m.params.clone(),
570                    body: body.clone(),
571                    closure_env: None,
572                    prototype: None,
573                    fib_like: None,
574                });
575                interp.subs.insert(fq, sub);
576            }
577        }
578    }
579
580    let body_ip = chunk.body_start_ip;
581    if body_ip > 0 && body_ip < chunk.ops.len() {
582        // Run only the prelude: temporarily place Halt at body start.
583        let saved_op = chunk.ops[body_ip].clone();
584        chunk.ops[body_ip] = bytecode::Op::Halt;
585        let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
586        let mut vm = vm::VM::new(&chunk, interp);
587        vm.set_jit_enabled(vm_jit);
588        let _ = vm.execute()?;
589        chunk.ops[body_ip] = saved_op;
590    }
591
592    interp.line_mode_chunk = Some(chunk);
593    Ok(())
594}
595
596/// Execute the body portion of a pre-compiled chunk for one input line.
597/// Sets `$_` to `line_str`, runs from `body_start_ip` to Halt, returns `$_` for `-p` output.
598pub fn run_line_body(
599    chunk: &bytecode::Chunk,
600    interp: &mut VMHelper,
601    line_str: &str,
602    is_last_input_line: bool,
603) -> PerlResult<Option<String>> {
604    interp.line_mode_eof_pending = is_last_input_line;
605    let result: PerlResult<Option<String>> = (|| {
606        interp.line_number += 1;
607        interp
608            .scope
609            .set_topic(value::PerlValue::string(line_str.to_string()));
610
611        if interp.auto_split {
612            let sep = interp.field_separator.as_deref().unwrap_or(" ");
613            let re = regex::Regex::new(sep).unwrap_or_else(|_| regex::Regex::new(" ").unwrap());
614            let fields: Vec<value::PerlValue> = re
615                .split(line_str)
616                .map(|s| value::PerlValue::string(s.to_string()))
617                .collect();
618            interp.scope.set_array("F", fields)?;
619        }
620
621        let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
622        let mut vm = vm::VM::new(chunk, interp);
623        vm.set_jit_enabled(vm_jit);
624        vm.ip = chunk.body_start_ip;
625        let _ = vm.execute()?;
626
627        let mut out = interp.scope.get_scalar("_").to_string();
628        out.push_str(&interp.ors);
629        Ok(Some(out))
630    })();
631    interp.line_mode_eof_pending = false;
632    result
633}
634
635/// Parse + register top-level subs / `use` (same as the VM path), then compile to bytecode without running.
636/// Also runs static analysis to detect undefined variables and subroutines.
637pub fn lint_program(program: &ast::Program, interp: &mut VMHelper) -> PerlResult<()> {
638    interp.prepare_program_top_level(program)?;
639    static_analysis::analyze_program(program, &interp.file)?;
640    if interp.strict_refs || interp.strict_subs || interp.strict_vars {
641        return Ok(());
642    }
643    let comp = compiler::Compiler::new().with_source_file(interp.file.clone());
644    match comp.compile_program(program) {
645        Ok(_) => Ok(()),
646        Err(e) => Err(compile_error_to_perl(e)),
647    }
648}
649
650fn compile_error_to_perl(e: compiler::CompileError) -> PerlError {
651    match e {
652        compiler::CompileError::Unsupported(msg) => {
653            PerlError::runtime(format!("compile: {}", msg), 0)
654        }
655        compiler::CompileError::Frozen { line, detail } => {
656            // strict-vars violations (`Global symbol "$x" requires explicit
657            // package name…`) are compile-time errors in perl, so emit them
658            // as `Syntax` so the formatter appends `Execution of -e aborted
659            // due to compilation errors.` for parity.
660            if detail.starts_with("Global symbol") {
661                PerlError::syntax(detail, line)
662            } else {
663                PerlError::runtime(detail, line)
664            }
665        }
666    }
667}
668
669#[cfg(test)]
670mod tests {
671    use super::*;
672
673    #[test]
674    fn run_executes_last_expression_value() {
675        // Statement-only programs may yield 0 via the VM path; assert parse + run succeed.
676        let p = parse("2 + 2").expect("parse");
677        assert!(!p.statements.is_empty());
678        let _ = run("2 + 2").expect("run");
679    }
680
681    #[test]
682    fn run_propagates_parse_errors() {
683        assert!(run("sub f {").is_err());
684    }
685
686    #[test]
687    fn interpreter_scope_persists_global_scalar_across_execute_calls() {
688        let mut interp = VMHelper::new();
689        let assign = parse("$persist_test = 100").expect("parse assign");
690        interp.execute(&assign).expect("assign");
691        let read = parse("$persist_test").expect("parse read");
692        let v = interp.execute(&read).expect("read");
693        assert_eq!(v.to_int(), 100);
694    }
695
696    #[test]
697    fn parse_empty_program() {
698        let p = parse("").expect("empty input should parse");
699        assert!(p.statements.is_empty());
700    }
701
702    #[test]
703    fn parse_expression_statement() {
704        let p = parse("2 + 2").expect("parse");
705        assert!(!p.statements.is_empty());
706    }
707
708    #[test]
709    fn parse_semicolon_only_statements() {
710        parse(";;").expect("semicolons only");
711    }
712
713    #[test]
714    fn parse_if_with_block() {
715        parse("if (1) { 2 }").expect("if");
716    }
717
718    #[test]
719    fn parse_fails_on_invalid_syntax() {
720        assert!(parse("sub f {").is_err());
721    }
722
723    #[test]
724    fn parse_qw_word_list() {
725        parse("my @a = qw(x y z)").expect("qw list");
726    }
727
728    #[test]
729    fn parse_c_style_for_loop() {
730        parse("for (my $i = 0; $i < 3; $i = $i + 1) { 1; }").expect("c-style for");
731    }
732
733    #[test]
734    fn parse_package_statement() {
735        parse("package Foo::Bar; 1").expect("package");
736    }
737
738    #[test]
739    fn parse_unless_block() {
740        parse("unless (0) { 1; }").expect("unless");
741    }
742
743    #[test]
744    fn parse_if_elsif_else() {
745        parse("if (0) { 1; } elsif (1) { 2; } else { 3; }").expect("if elsif");
746    }
747
748    #[test]
749    fn parse_q_constructor() {
750        parse(r#"my $s = q{braces}"#).expect("q{}");
751        parse(r#"my $t = qq(double)"#).expect("qq()");
752    }
753
754    #[test]
755    fn parse_regex_literals() {
756        parse("m/foo/").expect("m//");
757        parse("s/foo/bar/g").expect("s///");
758    }
759
760    #[test]
761    fn parse_begin_and_end_blocks() {
762        parse("BEGIN { 1; }").expect("BEGIN");
763        parse("END { 1; }").expect("END");
764    }
765
766    #[test]
767    fn parse_transliterate_y() {
768        parse("$_ = 'a'; y/a/A/").expect("y//");
769    }
770
771    #[test]
772    fn parse_foreach_with_my_iterator() {
773        parse("foreach my $x (1, 2) { $x; }").expect("foreach my");
774    }
775
776    #[test]
777    fn parse_our_declaration() {
778        parse("our $g = 1").expect("our");
779    }
780
781    #[test]
782    fn parse_local_declaration() {
783        parse("local $x = 1").expect("local");
784    }
785
786    #[test]
787    fn parse_use_no_statements() {
788        parse("use strict").expect("use");
789        parse("no warnings").expect("no");
790    }
791
792    #[test]
793    fn parse_sub_with_prototype() {
794        parse("fn add2 ($$) { return $_0 + $_1; }").expect("fn prototype");
795        parse("fn try_block (&;@) { my ( $try, @code_refs ) = @_; }").expect("prototype @ slurpy");
796    }
797
798    #[test]
799    fn parse_list_expression_in_parentheses() {
800        parse("my @a = (1, 2, 3)").expect("list");
801    }
802
803    #[test]
804    fn parse_require_expression() {
805        parse("require strict").expect("require");
806    }
807
808    #[test]
809    fn parse_do_string_eval_form() {
810        parse(r#"do "foo.pl""#).expect("do string");
811    }
812
813    #[test]
814    fn parse_package_qualified_name() {
815        parse("package Foo::Bar::Baz").expect("package ::");
816    }
817
818    #[test]
819    fn parse_my_multiple_declarations() {
820        parse("my ($a, $b, $c)").expect("my list");
821    }
822
823    #[test]
824    fn parse_eval_block_statement() {
825        parse("eval { 1; }").expect("eval block");
826    }
827
828    #[test]
829    fn parse_p_statement() {
830        parse("p 42").expect("p");
831    }
832
833    #[test]
834    fn parse_chop_scalar() {
835        parse("chop $s").expect("chop");
836    }
837
838    #[test]
839    fn vendor_perl_inc_path_points_at_vendor_perl() {
840        let p = vendor_perl_inc_path();
841        assert!(
842            p.ends_with("vendor/perl"),
843            "unexpected vendor path: {}",
844            p.display()
845        );
846    }
847
848    #[test]
849    fn format_program_roundtrips_simple_expression() {
850        let p = parse("$x + 1").expect("parse");
851        let out = format_program(&p);
852        assert!(!out.trim().is_empty());
853    }
854}
855
856#[cfg(test)]
857mod builtins_extended_tests;
858
859#[cfg(test)]
860mod lib_api_extended_tests;
861
862#[cfg(test)]
863mod cache_bench;
864
865#[cfg(test)]
866mod parallel_api_tests;
867
868#[cfg(test)]
869mod parse_smoke_extended;
870
871#[cfg(test)]
872mod parse_smoke_batch2;
873
874#[cfg(test)]
875mod parse_smoke_batch3;
876
877#[cfg(test)]
878mod parse_smoke_batch4;
879
880#[cfg(test)]
881mod crate_api_tests;
882
883#[cfg(test)]
884mod parser_shape_tests;
885
886#[cfg(test)]
887mod interpreter_unit_tests;
888
889#[cfg(test)]
890mod run_semantics_tests;
891
892#[cfg(test)]
893mod run_semantics_more;
894
895#[cfg(test)]
896mod value_extra_tests;
897
898#[cfg(test)]
899mod lexer_extra_tests;
900
901#[cfg(test)]
902mod parser_extra_tests;
903
904#[cfg(test)]
905mod builtins_extra_tests;
906
907#[cfg(test)]
908mod keywords_hash_tests;
909
910#[cfg(test)]
911mod thread_extra_tests;
912
913#[cfg(test)]
914mod error_extra_tests;
915
916#[cfg(test)]
917mod oo_extra_tests;
918
919#[cfg(test)]
920mod regex_extra_tests;
921
922#[cfg(test)]
923mod aot_extra_tests;