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