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