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