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
8pub mod aot;
9pub mod ast;
10pub mod builtins;
11pub mod bytecode;
12pub mod capture;
13pub mod cluster;
14pub mod compiler;
15pub mod convert;
16mod crypt_util;
17pub mod data_section;
18pub mod debugger;
19pub mod deconvert;
20pub mod deparse;
21pub mod english;
22pub mod error;
23mod fib_like_tail;
24pub mod fmt;
25pub mod format;
26pub mod interpreter;
27mod jit;
28mod jwt;
29pub mod lexer;
30pub mod list_util;
31pub mod lsp;
32mod map_grep_fast;
33mod map_stream;
34pub mod mro;
35mod nanbox;
36mod native_codec;
37pub mod native_data;
38pub mod pack;
39pub mod par_lines;
40mod par_list;
41pub mod par_pipeline;
42pub mod par_walk;
43pub mod parallel_trace;
44pub mod parser;
45pub mod pcache;
46pub mod pchannel;
47pub mod pec;
48mod pending_destroy;
49pub mod perl_decode;
50pub mod perl_fs;
51pub mod perl_inc;
52mod perl_regex;
53pub mod perl_signal;
54mod pmap_progress;
55pub mod ppool;
56pub mod profiler;
57pub mod pwatch;
58pub mod remote_wire;
59pub mod rust_ffi;
60pub mod rust_sugar;
61pub mod scope;
62mod sort_fast;
63pub mod special_vars;
64pub mod static_analysis;
65pub mod token;
66pub mod value;
67pub mod vm;
68
69pub use interpreter::{
70    perl_bracket_version, FEAT_SAY, FEAT_STATE, FEAT_SWITCH, FEAT_UNICODE_STRINGS,
71};
72
73use error::{PerlError, PerlResult};
74use interpreter::Interpreter;
75
76// ── Perl 5 strict-compat mode (`--compat`) ──────────────────────────────────
77
78use std::sync::atomic::{AtomicBool, Ordering};
79
80/// When `true`, all stryke extensions are disabled and only stock Perl 5
81/// syntax / builtins are accepted.  Set once from the CLI driver and read by
82/// the parser, compiler, and interpreter.
83static COMPAT_MODE: AtomicBool = AtomicBool::new(false);
84
85/// Enable Perl 5 strict-compatibility mode (disables all stryke extensions).
86pub fn set_compat_mode(on: bool) {
87    COMPAT_MODE.store(on, Ordering::Relaxed);
88}
89
90/// Returns `true` when `--compat` is active.
91#[inline]
92pub fn compat_mode() -> bool {
93    COMPAT_MODE.load(Ordering::Relaxed)
94}
95use value::PerlValue;
96
97/// Parse a string of Perl code and return the AST.
98/// Pretty-print a parsed program as Perl-like source (`stryke --fmt`).
99pub fn format_program(p: &ast::Program) -> String {
100    fmt::format_program(p)
101}
102
103/// Convert a parsed program to stryke syntax with `|>` pipes and no semicolons.
104pub fn convert_to_stryke(p: &ast::Program) -> String {
105    convert::convert_program(p)
106}
107
108/// Convert a parsed program to stryke syntax with custom options.
109pub fn convert_to_stryke_with_options(p: &ast::Program, opts: &convert::ConvertOptions) -> String {
110    convert::convert_program_with_options(p, opts)
111}
112
113/// Deconvert a parsed stryke program back to standard Perl .pl syntax.
114pub fn deconvert_to_perl(p: &ast::Program) -> String {
115    deconvert::deconvert_program(p)
116}
117
118/// Deconvert a parsed stryke program back to standard Perl .pl syntax with options.
119pub fn deconvert_to_perl_with_options(
120    p: &ast::Program,
121    opts: &deconvert::DeconvertOptions,
122) -> String {
123    deconvert::deconvert_program_with_options(p, opts)
124}
125
126pub fn parse(code: &str) -> PerlResult<ast::Program> {
127    parse_with_file(code, "-e")
128}
129
130/// Parse with a **source path** for lexer/parser diagnostics (`… at FILE line N`), e.g. a script
131/// path or a required `.pm` absolute path. Use [`parse`] for snippets where `-e` is appropriate.
132pub fn parse_with_file(code: &str, file: &str) -> PerlResult<ast::Program> {
133    // `rust { ... }` FFI blocks are desugared at source level into BEGIN-wrapped builtin
134    // calls — the parity roadmap forbids new `StmtKind` variants for new behavior, so this
135    // pre-pass is the right shape. No-op for programs that don't mention `rust`.
136    let desugared = if compat_mode() {
137        code.to_string()
138    } else {
139        rust_sugar::desugar_rust_blocks(code)
140    };
141    let mut lexer = lexer::Lexer::new_with_file(&desugared, file);
142    let tokens = lexer.tokenize()?;
143    let mut parser = parser::Parser::new_with_file(tokens, file);
144    parser.parse_program()
145}
146
147/// Parse and execute a string of Perl code within an existing interpreter.
148/// Tries bytecode VM first, falls back to tree-walker on unsupported features.
149/// Uses [`Interpreter::file`] for both parse diagnostics and `__FILE__` during this execution.
150pub fn parse_and_run_string(code: &str, interp: &mut Interpreter) -> PerlResult<PerlValue> {
151    let file = interp.file.clone();
152    parse_and_run_string_in_file(code, interp, &file)
153}
154
155/// Like [`parse_and_run_string`], but parse errors and `__FILE__` for this run use `file` (e.g. a
156/// required module path). Restores [`Interpreter::file`] after execution.
157pub fn parse_and_run_string_in_file(
158    code: &str,
159    interp: &mut Interpreter,
160    file: &str,
161) -> PerlResult<PerlValue> {
162    let program = parse_with_file(code, file)?;
163    let saved = interp.file.clone();
164    interp.file = file.to_string();
165    let r = interp.execute(&program);
166    interp.file = saved;
167    let v = r?;
168    interp.drain_pending_destroys(0)?;
169    Ok(v)
170}
171
172/// Crate-root `vendor/perl` (e.g. `List/Util.pm`). The `stryke` / `stryke` driver prepends this to
173/// `@INC` when the directory exists so in-tree pure-Perl modules shadow XS-only core stubs.
174pub fn vendor_perl_inc_path() -> std::path::PathBuf {
175    std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor/perl")
176}
177
178/// Language server over stdio (`stryke --lsp`). Returns a process exit code.
179pub fn run_lsp_stdio() -> i32 {
180    match lsp::run_stdio() {
181        Ok(()) => 0,
182        Err(e) => {
183            eprintln!("stryke --lsp: {e}");
184            1
185        }
186    }
187}
188
189/// Parse and execute a string of Perl code with a fresh interpreter.
190pub fn run(code: &str) -> PerlResult<PerlValue> {
191    let program = parse(code)?;
192    let mut interp = Interpreter::new();
193    let v = interp.execute(&program)?;
194    interp.run_global_teardown()?;
195    Ok(v)
196}
197
198/// Try to compile and run via bytecode VM. Returns None if compilation fails.
199///
200/// **`.pec` bytecode cache integration.** When `interp.pec_precompiled_chunk` is populated
201/// (set by the `stryke` driver from a [`crate::pec::try_load`] hit), this function skips
202/// `compile_program` entirely and runs the preloaded chunk. On cache miss the compiler
203/// runs normally and, if `interp.pec_cache_fingerprint` is set, the fresh chunk + program
204/// are persisted as a `.pec` bundle so the next warm start can skip both parse and compile.
205pub fn try_vm_execute(
206    program: &ast::Program,
207    interp: &mut Interpreter,
208) -> Option<PerlResult<PerlValue>> {
209    if let Err(e) = interp.prepare_program_top_level(program) {
210        return Some(Err(e));
211    }
212
213    // Fast path: chunk loaded from a `.pec` cache hit. Consume the slot with `.take()` so a
214    // subsequent re-entry (e.g. nested `do FILE`) does not reuse a stale chunk. On cache hit
215    // we cannot fall back to the tree walker mid-run — surface any "VM unimplemented op" as
216    // a real error (in practice unreachable: the chunk was produced by `compile_program`,
217    // which only emits ops the VM implements).
218    if let Some(chunk) = interp.pec_precompiled_chunk.take() {
219        return Some(run_compiled_chunk(chunk, interp));
220    }
221
222    // `use strict 'vars'` is enforced at compile time by the compiler (see
223    // `Compiler::check_strict_scalar_access` and siblings). `strict refs` / `strict subs` are
224    // enforced by the tree helpers that the VM already delegates into (symbolic deref,
225    // `call_named_sub`, etc.), so they work transitively.
226    let comp = compiler::Compiler::new()
227        .with_source_file(interp.file.clone())
228        .with_strict_vars(interp.strict_vars);
229    match comp.compile_program(program) {
230        Ok(chunk) => {
231            // Persist after a cache miss so the next warm start can skip both parse and
232            // compile. Save failures are swallowed: a broken cache is an optimization loss,
233            // not a runtime error.
234            if let Some(fp) = interp.pec_cache_fingerprint.take() {
235                let bundle =
236                    pec::PecBundle::new(interp.strict_vars, fp, program.clone(), chunk.clone());
237                let _ = pec::try_save(&bundle);
238            }
239            match run_compiled_chunk(chunk, interp) {
240                Ok(result) => Some(Ok(result)),
241                Err(e) => {
242                    let msg = e.message.as_str();
243                    if msg.starts_with("VM: unimplemented op")
244                        || msg.starts_with("Unimplemented builtin")
245                    {
246                        None
247                    } else {
248                        Some(Err(e))
249                    }
250                }
251            }
252        }
253        // `CompileError::Frozen` is a hard compile-time error (strict pragma violations, frozen
254        // lvalue writes, unknown goto labels). Promote it to a user-visible runtime error so
255        // the VM path matches `perl` — without this promotion the fallback would run the tree
256        // interpreter, which sometimes silently accepts the same construct (e.g. strict_vars
257        // isn't enforced on scalar assignment in the tree path).
258        Err(compiler::CompileError::Frozen { line, detail }) => {
259            Some(Err(PerlError::runtime(detail, line)))
260        }
261        // `Unsupported` just means "this VM compiler doesn't handle this construct yet" — fall
262        // back to the tree interpreter.
263        Err(compiler::CompileError::Unsupported(_)) => None,
264    }
265}
266
267/// Shared execution tail used by both the cache-hit and compile paths in
268/// [`try_vm_execute`]. Pulled out so the `.pec` fast path does not duplicate the
269/// flip-flop / BEGIN-END / struct-def wiring every VM run depends on.
270fn run_compiled_chunk(chunk: bytecode::Chunk, interp: &mut Interpreter) -> PerlResult<PerlValue> {
271    interp.clear_flip_flop_state();
272    interp.prepare_flip_flop_vm_slots(chunk.flip_flop_slots);
273    if interp.disasm_bytecode {
274        eprintln!("{}", chunk.disassemble());
275    }
276    interp.clear_begin_end_blocks_after_vm_compile();
277    for def in &chunk.struct_defs {
278        interp
279            .struct_defs
280            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
281    }
282    for def in &chunk.enum_defs {
283        interp
284            .enum_defs
285            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
286    }
287    // Load traits before classes so trait enforcement can reference them
288    for def in &chunk.trait_defs {
289        interp
290            .trait_defs
291            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
292    }
293    for def in &chunk.class_defs {
294        let mut def = def.clone();
295        // Final class/method enforcement
296        for parent_name in &def.extends.clone() {
297            if let Some(parent_def) = interp.class_defs.get(parent_name) {
298                if parent_def.is_final {
299                    return Err(crate::error::PerlError::runtime(
300                        format!("cannot extend final class `{}`", parent_name),
301                        0,
302                    ));
303                }
304                for m in &def.methods {
305                    if let Some(parent_method) = parent_def.method(&m.name) {
306                        if parent_method.is_final {
307                            return Err(crate::error::PerlError::runtime(
308                                format!(
309                                    "cannot override final method `{}` from class `{}`",
310                                    m.name, parent_name
311                                ),
312                                0,
313                            ));
314                        }
315                    }
316                }
317            }
318        }
319        // Trait contract enforcement + default method inheritance
320        for trait_name in &def.implements.clone() {
321            if let Some(trait_def) = interp.trait_defs.get(trait_name) {
322                for required in trait_def.required_methods() {
323                    let has_method = def.methods.iter().any(|m| m.name == required.name);
324                    if !has_method {
325                        return Err(crate::error::PerlError::runtime(
326                            format!(
327                                "class `{}` implements trait `{}` but does not define required method `{}`",
328                                def.name, trait_name, required.name
329                            ),
330                            0,
331                        ));
332                    }
333                }
334                // Inherit default methods from trait (methods with bodies)
335                for tm in &trait_def.methods {
336                    if tm.body.is_some() && !def.methods.iter().any(|m| m.name == tm.name) {
337                        def.methods.push(tm.clone());
338                    }
339                }
340            }
341        }
342        // Abstract method enforcement: concrete subclasses must implement
343        // all abstract methods (body-less methods) from abstract parents
344        if !def.is_abstract {
345            for parent_name in &def.extends.clone() {
346                if let Some(parent_def) = interp.class_defs.get(parent_name) {
347                    if parent_def.is_abstract {
348                        for m in &parent_def.methods {
349                            if m.body.is_none() && !def.methods.iter().any(|dm| dm.name == m.name) {
350                                return Err(crate::error::PerlError::runtime(
351                                    format!(
352                                        "class `{}` must implement abstract method `{}` from `{}`",
353                                        def.name, m.name, parent_name
354                                    ),
355                                    0,
356                                ));
357                            }
358                        }
359                    }
360                }
361            }
362        }
363        // Initialize static fields
364        for sf in &def.static_fields {
365            let val = if let Some(ref expr) = sf.default {
366                match interp.eval_expr(expr) {
367                    Ok(v) => v,
368                    Err(crate::interpreter::FlowOrError::Error(e)) => return Err(e),
369                    Err(_) => crate::value::PerlValue::UNDEF,
370                }
371            } else {
372                crate::value::PerlValue::UNDEF
373            };
374            let key = format!("{}::{}", def.name, sf.name);
375            interp.scope.declare_scalar(&key, val);
376        }
377        interp
378            .class_defs
379            .insert(def.name.clone(), std::sync::Arc::new(def));
380    }
381    let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
382    let mut vm = vm::VM::new(&chunk, interp);
383    vm.set_jit_enabled(vm_jit);
384    match vm.execute() {
385        Ok(val) => {
386            interp.drain_pending_destroys(0)?;
387            Ok(val)
388        }
389        // On cache-hit path we cannot fall back to the tree walker (we no longer hold the
390        // fresh Program the caller passed). For the cold-compile path, the compiler would
391        // have already returned `Unsupported` for anything the VM cannot run, so this
392        // branch is effectively unreachable there. Either way, surface as a runtime error.
393        Err(e)
394            if e.message.starts_with("VM: unimplemented op")
395                || e.message.starts_with("Unimplemented builtin") =>
396        {
397            Err(PerlError::runtime(e.message, 0))
398        }
399        Err(e) => Err(e),
400    }
401}
402
403/// Parse + register top-level subs / `use` (same as the VM path), then compile to bytecode without running.
404/// Also runs static analysis to detect undefined variables and subroutines.
405pub fn lint_program(program: &ast::Program, interp: &mut Interpreter) -> PerlResult<()> {
406    interp.prepare_program_top_level(program)?;
407    static_analysis::analyze_program(program, &interp.file)?;
408    if interp.strict_refs || interp.strict_subs || interp.strict_vars {
409        return Ok(());
410    }
411    let comp = compiler::Compiler::new().with_source_file(interp.file.clone());
412    match comp.compile_program(program) {
413        Ok(_) => Ok(()),
414        Err(e) => Err(compile_error_to_perl(e)),
415    }
416}
417
418fn compile_error_to_perl(e: compiler::CompileError) -> PerlError {
419    match e {
420        compiler::CompileError::Unsupported(msg) => {
421            PerlError::runtime(format!("compile: {}", msg), 0)
422        }
423        compiler::CompileError::Frozen { line, detail } => PerlError::runtime(detail, line),
424    }
425}
426
427#[cfg(test)]
428mod tests {
429    use super::*;
430
431    #[test]
432    fn run_executes_last_expression_value() {
433        // Statement-only programs may yield 0 via the VM path; assert parse + run succeed.
434        let p = parse("2 + 2;").expect("parse");
435        assert!(!p.statements.is_empty());
436        let _ = run("2 + 2;").expect("run");
437    }
438
439    #[test]
440    fn run_propagates_parse_errors() {
441        assert!(run("sub f {").is_err());
442    }
443
444    #[test]
445    fn interpreter_scope_persists_global_scalar_across_execute_tree_calls() {
446        let mut interp = Interpreter::new();
447        let assign = parse("$persist_test = 100;").expect("parse assign");
448        interp.execute_tree(&assign).expect("assign");
449        let read = parse("$persist_test").expect("parse read");
450        let v = interp.execute_tree(&read).expect("read");
451        assert_eq!(v.to_int(), 100);
452    }
453
454    #[test]
455    fn parse_empty_program() {
456        let p = parse("").expect("empty input should parse");
457        assert!(p.statements.is_empty());
458    }
459
460    #[test]
461    fn parse_expression_statement() {
462        let p = parse("2 + 2;").expect("parse");
463        assert!(!p.statements.is_empty());
464    }
465
466    #[test]
467    fn parse_semicolon_only_statements() {
468        parse(";;;").expect("semicolons only");
469    }
470
471    #[test]
472    fn parse_subroutine_declaration() {
473        parse("sub foo { return 1; }").expect("sub");
474    }
475
476    #[test]
477    fn parse_if_with_block() {
478        parse("if (1) { 2 }").expect("if");
479    }
480
481    #[test]
482    fn parse_fails_on_invalid_syntax() {
483        assert!(parse("sub f {").is_err());
484    }
485
486    #[test]
487    fn parse_qw_word_list() {
488        parse("my @a = qw(x y z);").expect("qw list");
489    }
490
491    #[test]
492    fn parse_c_style_for_loop() {
493        parse("for (my $i = 0; $i < 3; $i = $i + 1) { 1; }").expect("c-style for");
494    }
495
496    #[test]
497    fn parse_package_statement() {
498        parse("package Foo::Bar; 1;").expect("package");
499    }
500
501    #[test]
502    fn parse_unless_block() {
503        parse("unless (0) { 1; }").expect("unless");
504    }
505
506    #[test]
507    fn parse_if_elsif_else() {
508        parse("if (0) { 1; } elsif (1) { 2; } else { 3; }").expect("if elsif");
509    }
510
511    #[test]
512    fn parse_q_constructor() {
513        parse(r#"my $s = q{braces};"#).expect("q{}");
514        parse(r#"my $t = qq(double);"#).expect("qq()");
515    }
516
517    #[test]
518    fn parse_regex_literals() {
519        parse("m/foo/;").expect("m//");
520        parse("s/foo/bar/g;").expect("s///");
521    }
522
523    #[test]
524    fn parse_begin_and_end_blocks() {
525        parse("BEGIN { 1; }").expect("BEGIN");
526        parse("END { 1; }").expect("END");
527    }
528
529    #[test]
530    fn parse_transliterate_y() {
531        parse("$_ = 'a'; y/a/A/;").expect("y//");
532    }
533
534    #[test]
535    fn parse_foreach_with_my_iterator() {
536        parse("foreach my $x (1, 2) { $x; }").expect("foreach my");
537    }
538
539    #[test]
540    fn parse_our_declaration() {
541        parse("our $g = 1;").expect("our");
542    }
543
544    #[test]
545    fn parse_local_declaration() {
546        parse("local $x = 1;").expect("local");
547    }
548
549    #[test]
550    fn parse_use_no_statements() {
551        parse("use strict;").expect("use");
552        parse("no warnings;").expect("no");
553    }
554
555    #[test]
556    fn parse_sub_with_prototype() {
557        parse("sub sum ($$) { return $_0 + $_1; }").expect("sub prototype");
558        parse("sub try (&;@) { my ( $try, @code_refs ) = @_; }").expect("prototype @ slurpy");
559    }
560
561    #[test]
562    fn parse_list_expression_in_parentheses() {
563        parse("my @a = (1, 2, 3);").expect("list");
564    }
565
566    #[test]
567    fn parse_require_expression() {
568        parse("require strict;").expect("require");
569    }
570
571    #[test]
572    fn parse_do_string_eval_form() {
573        parse(r#"do "foo.pl";"#).expect("do string");
574    }
575
576    #[test]
577    fn parse_package_qualified_name() {
578        parse("package Foo::Bar::Baz;").expect("package ::");
579    }
580
581    #[test]
582    fn parse_my_multiple_declarations() {
583        parse("my ($a, $b, $c);").expect("my list");
584    }
585
586    #[test]
587    fn parse_eval_block_statement() {
588        parse("eval { 1; };").expect("eval block");
589    }
590
591    #[test]
592    fn parse_say_statement() {
593        parse("say 42;").expect("say");
594    }
595
596    #[test]
597    fn parse_chop_scalar() {
598        parse("chop $s;").expect("chop");
599    }
600
601    #[test]
602    fn vendor_perl_inc_path_points_at_vendor_perl() {
603        let p = vendor_perl_inc_path();
604        assert!(
605            p.ends_with("vendor/perl"),
606            "unexpected vendor path: {}",
607            p.display()
608        );
609    }
610
611    #[test]
612    fn format_program_roundtrips_simple_expression() {
613        let p = parse("$x + 1;").expect("parse");
614        let out = format_program(&p);
615        assert!(!out.trim().is_empty());
616    }
617}
618
619#[cfg(test)]
620mod builtins_extended_tests;
621
622#[cfg(test)]
623mod lib_api_extended_tests;
624
625#[cfg(test)]
626mod parallel_api_tests;
627
628#[cfg(test)]
629mod parse_smoke_extended;
630
631#[cfg(test)]
632mod parse_smoke_batch2;
633
634#[cfg(test)]
635mod parse_smoke_batch3;
636
637#[cfg(test)]
638mod parse_smoke_batch4;
639
640#[cfg(test)]
641mod crate_api_tests;
642
643#[cfg(test)]
644mod parser_shape_tests;
645
646#[cfg(test)]
647mod interpreter_unit_tests;
648
649#[cfg(test)]
650mod run_semantics_tests;
651
652#[cfg(test)]
653mod run_semantics_more;