#![allow(rustdoc::private_intra_doc_links)]
#![allow(rustdoc::broken_intra_doc_links)]
pub mod aot;
pub mod ast;
pub mod builtins;
pub mod bytecode;
pub mod capture;
pub mod cluster;
pub mod compiler;
pub mod convert;
mod crypt_util;
pub mod data_section;
pub mod deconvert;
pub mod deparse;
pub mod english;
pub mod error;
mod fib_like_tail;
pub mod fmt;
pub mod format;
pub mod interpreter;
mod jit;
mod jwt;
pub mod lexer;
pub mod list_util;
pub mod lsp;
mod map_grep_fast;
mod map_stream;
pub mod mro;
mod nanbox;
mod native_codec;
pub mod native_data;
pub mod pack;
pub mod par_lines;
mod par_list;
pub mod par_pipeline;
pub mod par_walk;
pub mod parallel_trace;
pub mod parser;
pub mod pcache;
pub mod pchannel;
pub mod pec;
mod pending_destroy;
pub mod perl_decode;
pub mod perl_fs;
pub mod perl_inc;
mod perl_regex;
pub mod perl_signal;
mod pmap_progress;
pub mod ppool;
pub mod profiler;
pub mod pwatch;
pub mod remote_wire;
pub mod rust_ffi;
pub mod rust_sugar;
pub mod scope;
mod sort_fast;
pub mod special_vars;
pub mod static_analysis;
pub mod token;
pub mod value;
pub mod vm;
pub use interpreter::{
perl_bracket_version, FEAT_SAY, FEAT_STATE, FEAT_SWITCH, FEAT_UNICODE_STRINGS,
};
use error::{PerlError, PerlResult};
use interpreter::Interpreter;
use std::sync::atomic::{AtomicBool, Ordering};
static COMPAT_MODE: AtomicBool = AtomicBool::new(false);
pub fn set_compat_mode(on: bool) {
COMPAT_MODE.store(on, Ordering::Relaxed);
}
#[inline]
pub fn compat_mode() -> bool {
COMPAT_MODE.load(Ordering::Relaxed)
}
use value::PerlValue;
pub fn format_program(p: &ast::Program) -> String {
fmt::format_program(p)
}
pub fn convert_to_stryke(p: &ast::Program) -> String {
convert::convert_program(p)
}
pub fn convert_to_stryke_with_options(p: &ast::Program, opts: &convert::ConvertOptions) -> String {
convert::convert_program_with_options(p, opts)
}
pub fn deconvert_to_perl(p: &ast::Program) -> String {
deconvert::deconvert_program(p)
}
pub fn deconvert_to_perl_with_options(
p: &ast::Program,
opts: &deconvert::DeconvertOptions,
) -> String {
deconvert::deconvert_program_with_options(p, opts)
}
pub fn parse(code: &str) -> PerlResult<ast::Program> {
parse_with_file(code, "-e")
}
pub fn parse_with_file(code: &str, file: &str) -> PerlResult<ast::Program> {
let desugared = if compat_mode() {
code.to_string()
} else {
rust_sugar::desugar_rust_blocks(code)
};
let mut lexer = lexer::Lexer::new_with_file(&desugared, file);
let tokens = lexer.tokenize()?;
let mut parser = parser::Parser::new_with_file(tokens, file);
parser.parse_program()
}
pub fn parse_and_run_string(code: &str, interp: &mut Interpreter) -> PerlResult<PerlValue> {
let file = interp.file.clone();
parse_and_run_string_in_file(code, interp, &file)
}
pub fn parse_and_run_string_in_file(
code: &str,
interp: &mut Interpreter,
file: &str,
) -> PerlResult<PerlValue> {
let program = parse_with_file(code, file)?;
let saved = interp.file.clone();
interp.file = file.to_string();
let r = interp.execute(&program);
interp.file = saved;
let v = r?;
interp.drain_pending_destroys(0)?;
Ok(v)
}
pub fn vendor_perl_inc_path() -> std::path::PathBuf {
std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor/perl")
}
pub fn run_lsp_stdio() -> i32 {
match lsp::run_stdio() {
Ok(()) => 0,
Err(e) => {
eprintln!("fo --lsp: {e}");
1
}
}
}
pub fn run(code: &str) -> PerlResult<PerlValue> {
let program = parse(code)?;
let mut interp = Interpreter::new();
let v = interp.execute(&program)?;
interp.run_global_teardown()?;
Ok(v)
}
pub fn try_vm_execute(
program: &ast::Program,
interp: &mut Interpreter,
) -> Option<PerlResult<PerlValue>> {
if let Err(e) = interp.prepare_program_top_level(program) {
return Some(Err(e));
}
if let Some(chunk) = interp.pec_precompiled_chunk.take() {
return Some(run_compiled_chunk(chunk, interp));
}
let comp = compiler::Compiler::new()
.with_source_file(interp.file.clone())
.with_strict_vars(interp.strict_vars);
match comp.compile_program(program) {
Ok(chunk) => {
if let Some(fp) = interp.pec_cache_fingerprint.take() {
let bundle =
pec::PecBundle::new(interp.strict_vars, fp, program.clone(), chunk.clone());
let _ = pec::try_save(&bundle);
}
match run_compiled_chunk(chunk, interp) {
Ok(result) => Some(Ok(result)),
Err(e) => {
let msg = e.message.as_str();
if msg.starts_with("VM: unimplemented op")
|| msg.starts_with("Unimplemented builtin")
{
None
} else {
Some(Err(e))
}
}
}
}
Err(compiler::CompileError::Frozen { line, detail }) => {
Some(Err(PerlError::runtime(detail, line)))
}
Err(compiler::CompileError::Unsupported(_)) => None,
}
}
fn run_compiled_chunk(chunk: bytecode::Chunk, interp: &mut Interpreter) -> PerlResult<PerlValue> {
interp.clear_flip_flop_state();
interp.prepare_flip_flop_vm_slots(chunk.flip_flop_slots);
if interp.disasm_bytecode {
eprintln!("{}", chunk.disassemble());
}
interp.clear_begin_end_blocks_after_vm_compile();
for def in &chunk.struct_defs {
interp
.struct_defs
.insert(def.name.clone(), std::sync::Arc::new(def.clone()));
}
for def in &chunk.enum_defs {
interp
.enum_defs
.insert(def.name.clone(), std::sync::Arc::new(def.clone()));
}
for def in &chunk.trait_defs {
interp
.trait_defs
.insert(def.name.clone(), std::sync::Arc::new(def.clone()));
}
for def in &chunk.class_defs {
for parent_name in &def.extends {
if let Some(parent_def) = interp.class_defs.get(parent_name) {
if parent_def.is_final {
return Err(crate::error::PerlError::runtime(
format!("cannot extend final class `{}`", parent_name),
0,
));
}
for m in &def.methods {
if let Some(parent_method) = parent_def.method(&m.name) {
if parent_method.is_final {
return Err(crate::error::PerlError::runtime(
format!(
"cannot override final method `{}` from class `{}`",
m.name, parent_name
),
0,
));
}
}
}
}
}
for trait_name in &def.implements {
if let Some(trait_def) = interp.trait_defs.get(trait_name) {
for required in trait_def.required_methods() {
let has_method = def.methods.iter().any(|m| m.name == required.name);
if !has_method {
return Err(crate::error::PerlError::runtime(
format!(
"class `{}` implements trait `{}` but does not define required method `{}`",
def.name, trait_name, required.name
),
0,
));
}
}
}
}
for sf in &def.static_fields {
let val = if let Some(ref expr) = sf.default {
match interp.eval_expr(expr) {
Ok(v) => v,
Err(crate::interpreter::FlowOrError::Error(e)) => return Err(e),
Err(_) => crate::value::PerlValue::UNDEF,
}
} else {
crate::value::PerlValue::UNDEF
};
let key = format!("{}::{}", def.name, sf.name);
interp.scope.declare_scalar(&key, val);
}
interp
.class_defs
.insert(def.name.clone(), std::sync::Arc::new(def.clone()));
}
let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
let mut vm = vm::VM::new(&chunk, interp);
vm.set_jit_enabled(vm_jit);
match vm.execute() {
Ok(val) => {
interp.drain_pending_destroys(0)?;
Ok(val)
}
Err(e)
if e.message.starts_with("VM: unimplemented op")
|| e.message.starts_with("Unimplemented builtin") =>
{
Err(PerlError::runtime(e.message, 0))
}
Err(e) => Err(e),
}
}
pub fn lint_program(program: &ast::Program, interp: &mut Interpreter) -> PerlResult<()> {
interp.prepare_program_top_level(program)?;
static_analysis::analyze_program(program, &interp.file)?;
if interp.strict_refs || interp.strict_subs || interp.strict_vars {
return Ok(());
}
let comp = compiler::Compiler::new().with_source_file(interp.file.clone());
match comp.compile_program(program) {
Ok(_) => Ok(()),
Err(e) => Err(compile_error_to_perl(e)),
}
}
fn compile_error_to_perl(e: compiler::CompileError) -> PerlError {
match e {
compiler::CompileError::Unsupported(msg) => {
PerlError::runtime(format!("compile: {}", msg), 0)
}
compiler::CompileError::Frozen { line, detail } => PerlError::runtime(detail, line),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_executes_last_expression_value() {
let p = parse("2 + 2;").expect("parse");
assert!(!p.statements.is_empty());
let _ = run("2 + 2;").expect("run");
}
#[test]
fn run_propagates_parse_errors() {
assert!(run("sub f {").is_err());
}
#[test]
fn interpreter_scope_persists_global_scalar_across_execute_tree_calls() {
let mut interp = Interpreter::new();
let assign = parse("$persist_test = 100;").expect("parse assign");
interp.execute_tree(&assign).expect("assign");
let read = parse("$persist_test").expect("parse read");
let v = interp.execute_tree(&read).expect("read");
assert_eq!(v.to_int(), 100);
}
#[test]
fn parse_empty_program() {
let p = parse("").expect("empty input should parse");
assert!(p.statements.is_empty());
}
#[test]
fn parse_expression_statement() {
let p = parse("2 + 2;").expect("parse");
assert!(!p.statements.is_empty());
}
#[test]
fn parse_semicolon_only_statements() {
parse(";;;").expect("semicolons only");
}
#[test]
fn parse_subroutine_declaration() {
parse("sub foo { return 1; }").expect("sub");
}
#[test]
fn parse_if_with_block() {
parse("if (1) { 2 }").expect("if");
}
#[test]
fn parse_fails_on_invalid_syntax() {
assert!(parse("sub f {").is_err());
}
#[test]
fn parse_qw_word_list() {
parse("my @a = qw(x y z);").expect("qw list");
}
#[test]
fn parse_c_style_for_loop() {
parse("for (my $i = 0; $i < 3; $i = $i + 1) { 1; }").expect("c-style for");
}
#[test]
fn parse_package_statement() {
parse("package Foo::Bar; 1;").expect("package");
}
#[test]
fn parse_unless_block() {
parse("unless (0) { 1; }").expect("unless");
}
#[test]
fn parse_if_elsif_else() {
parse("if (0) { 1; } elsif (1) { 2; } else { 3; }").expect("if elsif");
}
#[test]
fn parse_q_constructor() {
parse(r#"my $s = q{braces};"#).expect("q{}");
parse(r#"my $t = qq(double);"#).expect("qq()");
}
#[test]
fn parse_regex_literals() {
parse("m/foo/;").expect("m//");
parse("s/foo/bar/g;").expect("s///");
}
#[test]
fn parse_begin_and_end_blocks() {
parse("BEGIN { 1; }").expect("BEGIN");
parse("END { 1; }").expect("END");
}
#[test]
fn parse_transliterate_y() {
parse("$_ = 'a'; y/a/A/;").expect("y//");
}
#[test]
fn parse_foreach_with_my_iterator() {
parse("foreach my $x (1, 2) { $x; }").expect("foreach my");
}
#[test]
fn parse_our_declaration() {
parse("our $g = 1;").expect("our");
}
#[test]
fn parse_local_declaration() {
parse("local $x = 1;").expect("local");
}
#[test]
fn parse_use_no_statements() {
parse("use strict;").expect("use");
parse("no warnings;").expect("no");
}
#[test]
fn parse_sub_with_prototype() {
parse("sub sum ($$) { return $_0 + $_1; }").expect("sub prototype");
parse("sub try (&;@) { my ( $try, @code_refs ) = @_; }").expect("prototype @ slurpy");
}
#[test]
fn parse_list_expression_in_parentheses() {
parse("my @a = (1, 2, 3);").expect("list");
}
#[test]
fn parse_require_expression() {
parse("require strict;").expect("require");
}
#[test]
fn parse_do_string_eval_form() {
parse(r#"do "foo.pl";"#).expect("do string");
}
#[test]
fn parse_package_qualified_name() {
parse("package Foo::Bar::Baz;").expect("package ::");
}
#[test]
fn parse_my_multiple_declarations() {
parse("my ($a, $b, $c);").expect("my list");
}
#[test]
fn parse_eval_block_statement() {
parse("eval { 1; };").expect("eval block");
}
#[test]
fn parse_say_statement() {
parse("say 42;").expect("say");
}
#[test]
fn parse_chop_scalar() {
parse("chop $s;").expect("chop");
}
#[test]
fn vendor_perl_inc_path_points_at_vendor_perl() {
let p = vendor_perl_inc_path();
assert!(
p.ends_with("vendor/perl"),
"unexpected vendor path: {}",
p.display()
);
}
#[test]
fn format_program_roundtrips_simple_expression() {
let p = parse("$x + 1;").expect("parse");
let out = format_program(&p);
assert!(!out.trim().is_empty());
}
}
#[cfg(test)]
mod builtins_extended_tests;
#[cfg(test)]
mod lib_api_extended_tests;
#[cfg(test)]
mod parallel_api_tests;
#[cfg(test)]
mod parse_smoke_extended;
#[cfg(test)]
mod parse_smoke_batch2;
#[cfg(test)]
mod parse_smoke_batch3;
#[cfg(test)]
mod parse_smoke_batch4;
#[cfg(test)]
mod crate_api_tests;
#[cfg(test)]
mod parser_shape_tests;
#[cfg(test)]
mod interpreter_unit_tests;
#[cfg(test)]
mod run_semantics_tests;
#[cfg(test)]
mod run_semantics_more;