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