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