strykelang 0.7.0

A highly parallel Perl 5 interpreter written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
//! Crate root — see [`README.md`](https://github.com/MenkeTechnologies/stryke) for overview.
// `cargo doc` with `RUSTDOCFLAGS=-D warnings` (CI) flags intra-doc links to private items and
// a few shorthand links (`MethodCall`, `Op::…`) that do not resolve as paths. Suppress until
// docs are normalized to `crate::…` paths and public-only links.
#![allow(rustdoc::private_intra_doc_links)]
#![allow(rustdoc::broken_intra_doc_links)]

pub mod aot;
pub mod ast;
pub mod builtins;
pub mod bytecode;
pub mod capture;
pub mod cluster;
pub mod compiler;
pub mod convert;
mod crypt_util;
pub mod data_section;
pub mod debugger;
pub mod deconvert;
pub mod deparse;
pub mod english;
pub mod error;
mod fib_like_tail;
pub mod fmt;
pub mod format;
pub mod interpreter;
mod jit;
mod jwt;
pub mod lexer;
pub mod list_util;
pub mod lsp;
mod map_grep_fast;
mod map_stream;
pub mod mro;
mod nanbox;
mod native_codec;
pub mod native_data;
pub mod pack;
pub mod par_lines;
mod par_list;
pub mod par_pipeline;
pub mod par_walk;
pub mod parallel_trace;
pub mod parser;
pub mod pcache;
pub mod pchannel;
pub mod pec;
mod pending_destroy;
pub mod perl_decode;
pub mod perl_fs;
pub mod perl_inc;
mod perl_regex;
pub mod perl_signal;
mod pmap_progress;
pub mod ppool;
pub mod profiler;
pub mod pwatch;
pub mod remote_wire;
pub mod rust_ffi;
pub mod rust_sugar;
pub mod scope;
mod sort_fast;
pub mod special_vars;
pub mod static_analysis;
pub mod token;
pub mod value;
pub mod vm;

pub use interpreter::{
    perl_bracket_version, FEAT_SAY, FEAT_STATE, FEAT_SWITCH, FEAT_UNICODE_STRINGS,
};

use error::{PerlError, PerlResult};
use interpreter::Interpreter;

// ── Perl 5 strict-compat mode (`--compat`) ──────────────────────────────────

use std::sync::atomic::{AtomicBool, Ordering};

/// When `true`, all stryke extensions are disabled and only stock Perl 5
/// syntax / builtins are accepted.  Set once from the CLI driver and read by
/// the parser, compiler, and interpreter.
static COMPAT_MODE: AtomicBool = AtomicBool::new(false);

/// Enable Perl 5 strict-compatibility mode (disables all stryke extensions).
pub fn set_compat_mode(on: bool) {
    COMPAT_MODE.store(on, Ordering::Relaxed);
}

/// Returns `true` when `--compat` is active.
#[inline]
pub fn compat_mode() -> bool {
    COMPAT_MODE.load(Ordering::Relaxed)
}
use value::PerlValue;

/// Parse a string of Perl code and return the AST.
/// Pretty-print a parsed program as Perl-like source (`stryke --fmt`).
pub fn format_program(p: &ast::Program) -> String {
    fmt::format_program(p)
}

/// Convert a parsed program to stryke syntax with `|>` pipes and no semicolons.
pub fn convert_to_stryke(p: &ast::Program) -> String {
    convert::convert_program(p)
}

/// Convert a parsed program to stryke syntax with custom options.
pub fn convert_to_stryke_with_options(p: &ast::Program, opts: &convert::ConvertOptions) -> String {
    convert::convert_program_with_options(p, opts)
}

/// Deconvert a parsed stryke program back to standard Perl .pl syntax.
pub fn deconvert_to_perl(p: &ast::Program) -> String {
    deconvert::deconvert_program(p)
}

/// Deconvert a parsed stryke program back to standard Perl .pl syntax with options.
pub fn deconvert_to_perl_with_options(
    p: &ast::Program,
    opts: &deconvert::DeconvertOptions,
) -> String {
    deconvert::deconvert_program_with_options(p, opts)
}

pub fn parse(code: &str) -> PerlResult<ast::Program> {
    parse_with_file(code, "-e")
}

/// Parse with a **source path** for lexer/parser diagnostics (`… at FILE line N`), e.g. a script
/// path or a required `.pm` absolute path. Use [`parse`] for snippets where `-e` is appropriate.
pub fn parse_with_file(code: &str, file: &str) -> PerlResult<ast::Program> {
    // `rust { ... }` FFI blocks are desugared at source level into BEGIN-wrapped builtin
    // calls — the parity roadmap forbids new `StmtKind` variants for new behavior, so this
    // pre-pass is the right shape. No-op for programs that don't mention `rust`.
    let desugared = if compat_mode() {
        code.to_string()
    } else {
        rust_sugar::desugar_rust_blocks(code)
    };
    let mut lexer = lexer::Lexer::new_with_file(&desugared, file);
    let tokens = lexer.tokenize()?;
    let mut parser = parser::Parser::new_with_file(tokens, file);
    parser.parse_program()
}

/// Parse and execute a string of Perl code within an existing interpreter.
/// Tries bytecode VM first, falls back to tree-walker on unsupported features.
/// Uses [`Interpreter::file`] for both parse diagnostics and `__FILE__` during this execution.
pub fn parse_and_run_string(code: &str, interp: &mut Interpreter) -> PerlResult<PerlValue> {
    let file = interp.file.clone();
    parse_and_run_string_in_file(code, interp, &file)
}

/// Like [`parse_and_run_string`], but parse errors and `__FILE__` for this run use `file` (e.g. a
/// required module path). Restores [`Interpreter::file`] after execution.
pub fn parse_and_run_string_in_file(
    code: &str,
    interp: &mut Interpreter,
    file: &str,
) -> PerlResult<PerlValue> {
    let program = parse_with_file(code, file)?;
    let saved = interp.file.clone();
    interp.file = file.to_string();
    let r = interp.execute(&program);
    interp.file = saved;
    let v = r?;
    interp.drain_pending_destroys(0)?;
    Ok(v)
}

/// Crate-root `vendor/perl` (e.g. `List/Util.pm`). The `stryke` / `stryke` driver prepends this to
/// `@INC` when the directory exists so in-tree pure-Perl modules shadow XS-only core stubs.
pub fn vendor_perl_inc_path() -> std::path::PathBuf {
    std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("vendor/perl")
}

/// Language server over stdio (`stryke --lsp`). Returns a process exit code.
pub fn run_lsp_stdio() -> i32 {
    match lsp::run_stdio() {
        Ok(()) => 0,
        Err(e) => {
            eprintln!("stryke --lsp: {e}");
            1
        }
    }
}

/// Parse and execute a string of Perl code with a fresh interpreter.
pub fn run(code: &str) -> PerlResult<PerlValue> {
    let program = parse(code)?;
    let mut interp = Interpreter::new();
    let v = interp.execute(&program)?;
    interp.run_global_teardown()?;
    Ok(v)
}

/// Try to compile and run via bytecode VM. Returns None if compilation fails.
///
/// **`.pec` bytecode cache integration.** When `interp.pec_precompiled_chunk` is populated
/// (set by the `stryke` driver from a [`crate::pec::try_load`] hit), this function skips
/// `compile_program` entirely and runs the preloaded chunk. On cache miss the compiler
/// runs normally and, if `interp.pec_cache_fingerprint` is set, the fresh chunk + program
/// are persisted as a `.pec` bundle so the next warm start can skip both parse and compile.
pub fn try_vm_execute(
    program: &ast::Program,
    interp: &mut Interpreter,
) -> Option<PerlResult<PerlValue>> {
    if let Err(e) = interp.prepare_program_top_level(program) {
        return Some(Err(e));
    }

    // Fast path: chunk loaded from a `.pec` cache hit. Consume the slot with `.take()` so a
    // subsequent re-entry (e.g. nested `do FILE`) does not reuse a stale chunk. On cache hit
    // we cannot fall back to the tree walker mid-run — surface any "VM unimplemented op" as
    // a real error (in practice unreachable: the chunk was produced by `compile_program`,
    // which only emits ops the VM implements).
    if let Some(chunk) = interp.pec_precompiled_chunk.take() {
        return Some(run_compiled_chunk(chunk, interp));
    }

    // `use strict 'vars'` is enforced at compile time by the compiler (see
    // `Compiler::check_strict_scalar_access` and siblings). `strict refs` / `strict subs` are
    // enforced by the tree helpers that the VM already delegates into (symbolic deref,
    // `call_named_sub`, etc.), so they work transitively.
    let comp = compiler::Compiler::new()
        .with_source_file(interp.file.clone())
        .with_strict_vars(interp.strict_vars);
    match comp.compile_program(program) {
        Ok(chunk) => {
            // Persist after a cache miss so the next warm start can skip both parse and
            // compile. Save failures are swallowed: a broken cache is an optimization loss,
            // not a runtime error.
            if let Some(fp) = interp.pec_cache_fingerprint.take() {
                let bundle =
                    pec::PecBundle::new(interp.strict_vars, fp, program.clone(), chunk.clone());
                let _ = pec::try_save(&bundle);
            }
            match run_compiled_chunk(chunk, interp) {
                Ok(result) => Some(Ok(result)),
                Err(e) => {
                    let msg = e.message.as_str();
                    if msg.starts_with("VM: unimplemented op")
                        || msg.starts_with("Unimplemented builtin")
                    {
                        None
                    } else {
                        Some(Err(e))
                    }
                }
            }
        }
        // `CompileError::Frozen` is a hard compile-time error (strict pragma violations, frozen
        // lvalue writes, unknown goto labels). Promote it to a user-visible runtime error so
        // the VM path matches `perl` — without this promotion the fallback would run the tree
        // interpreter, which sometimes silently accepts the same construct (e.g. strict_vars
        // isn't enforced on scalar assignment in the tree path).
        Err(compiler::CompileError::Frozen { line, detail }) => {
            Some(Err(PerlError::runtime(detail, line)))
        }
        // `Unsupported` just means "this VM compiler doesn't handle this construct yet" — fall
        // back to the tree interpreter.
        Err(compiler::CompileError::Unsupported(_)) => None,
    }
}

/// Shared execution tail used by both the cache-hit and compile paths in
/// [`try_vm_execute`]. Pulled out so the `.pec` fast path does not duplicate the
/// flip-flop / BEGIN-END / struct-def wiring every VM run depends on.
fn run_compiled_chunk(chunk: bytecode::Chunk, interp: &mut Interpreter) -> PerlResult<PerlValue> {
    interp.clear_flip_flop_state();
    interp.prepare_flip_flop_vm_slots(chunk.flip_flop_slots);
    if interp.disasm_bytecode {
        eprintln!("{}", chunk.disassemble());
    }
    interp.clear_begin_end_blocks_after_vm_compile();
    for def in &chunk.struct_defs {
        interp
            .struct_defs
            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
    }
    for def in &chunk.enum_defs {
        interp
            .enum_defs
            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
    }
    // Load traits before classes so trait enforcement can reference them
    for def in &chunk.trait_defs {
        interp
            .trait_defs
            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
    }
    for def in &chunk.class_defs {
        // Final class/method enforcement
        for parent_name in &def.extends {
            if let Some(parent_def) = interp.class_defs.get(parent_name) {
                if parent_def.is_final {
                    return Err(crate::error::PerlError::runtime(
                        format!("cannot extend final class `{}`", parent_name),
                        0,
                    ));
                }
                for m in &def.methods {
                    if let Some(parent_method) = parent_def.method(&m.name) {
                        if parent_method.is_final {
                            return Err(crate::error::PerlError::runtime(
                                format!(
                                    "cannot override final method `{}` from class `{}`",
                                    m.name, parent_name
                                ),
                                0,
                            ));
                        }
                    }
                }
            }
        }
        // Trait contract enforcement
        for trait_name in &def.implements {
            if let Some(trait_def) = interp.trait_defs.get(trait_name) {
                for required in trait_def.required_methods() {
                    let has_method = def.methods.iter().any(|m| m.name == required.name);
                    if !has_method {
                        return Err(crate::error::PerlError::runtime(
                            format!(
                                "class `{}` implements trait `{}` but does not define required method `{}`",
                                def.name, trait_name, required.name
                            ),
                            0,
                        ));
                    }
                }
            }
        }
        // Initialize static fields
        for sf in &def.static_fields {
            let val = if let Some(ref expr) = sf.default {
                match interp.eval_expr(expr) {
                    Ok(v) => v,
                    Err(crate::interpreter::FlowOrError::Error(e)) => return Err(e),
                    Err(_) => crate::value::PerlValue::UNDEF,
                }
            } else {
                crate::value::PerlValue::UNDEF
            };
            let key = format!("{}::{}", def.name, sf.name);
            interp.scope.declare_scalar(&key, val);
        }
        interp
            .class_defs
            .insert(def.name.clone(), std::sync::Arc::new(def.clone()));
    }
    let vm_jit = interp.vm_jit_enabled && interp.profiler.is_none();
    let mut vm = vm::VM::new(&chunk, interp);
    vm.set_jit_enabled(vm_jit);
    match vm.execute() {
        Ok(val) => {
            interp.drain_pending_destroys(0)?;
            Ok(val)
        }
        // On cache-hit path we cannot fall back to the tree walker (we no longer hold the
        // fresh Program the caller passed). For the cold-compile path, the compiler would
        // have already returned `Unsupported` for anything the VM cannot run, so this
        // branch is effectively unreachable there. Either way, surface as a runtime error.
        Err(e)
            if e.message.starts_with("VM: unimplemented op")
                || e.message.starts_with("Unimplemented builtin") =>
        {
            Err(PerlError::runtime(e.message, 0))
        }
        Err(e) => Err(e),
    }
}

/// Parse + register top-level subs / `use` (same as the VM path), then compile to bytecode without running.
/// Also runs static analysis to detect undefined variables and subroutines.
pub fn lint_program(program: &ast::Program, interp: &mut Interpreter) -> PerlResult<()> {
    interp.prepare_program_top_level(program)?;
    static_analysis::analyze_program(program, &interp.file)?;
    if interp.strict_refs || interp.strict_subs || interp.strict_vars {
        return Ok(());
    }
    let comp = compiler::Compiler::new().with_source_file(interp.file.clone());
    match comp.compile_program(program) {
        Ok(_) => Ok(()),
        Err(e) => Err(compile_error_to_perl(e)),
    }
}

fn compile_error_to_perl(e: compiler::CompileError) -> PerlError {
    match e {
        compiler::CompileError::Unsupported(msg) => {
            PerlError::runtime(format!("compile: {}", msg), 0)
        }
        compiler::CompileError::Frozen { line, detail } => PerlError::runtime(detail, line),
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn run_executes_last_expression_value() {
        // Statement-only programs may yield 0 via the VM path; assert parse + run succeed.
        let p = parse("2 + 2;").expect("parse");
        assert!(!p.statements.is_empty());
        let _ = run("2 + 2;").expect("run");
    }

    #[test]
    fn run_propagates_parse_errors() {
        assert!(run("sub f {").is_err());
    }

    #[test]
    fn interpreter_scope_persists_global_scalar_across_execute_tree_calls() {
        let mut interp = Interpreter::new();
        let assign = parse("$persist_test = 100;").expect("parse assign");
        interp.execute_tree(&assign).expect("assign");
        let read = parse("$persist_test").expect("parse read");
        let v = interp.execute_tree(&read).expect("read");
        assert_eq!(v.to_int(), 100);
    }

    #[test]
    fn parse_empty_program() {
        let p = parse("").expect("empty input should parse");
        assert!(p.statements.is_empty());
    }

    #[test]
    fn parse_expression_statement() {
        let p = parse("2 + 2;").expect("parse");
        assert!(!p.statements.is_empty());
    }

    #[test]
    fn parse_semicolon_only_statements() {
        parse(";;;").expect("semicolons only");
    }

    #[test]
    fn parse_subroutine_declaration() {
        parse("sub foo { return 1; }").expect("sub");
    }

    #[test]
    fn parse_if_with_block() {
        parse("if (1) { 2 }").expect("if");
    }

    #[test]
    fn parse_fails_on_invalid_syntax() {
        assert!(parse("sub f {").is_err());
    }

    #[test]
    fn parse_qw_word_list() {
        parse("my @a = qw(x y z);").expect("qw list");
    }

    #[test]
    fn parse_c_style_for_loop() {
        parse("for (my $i = 0; $i < 3; $i = $i + 1) { 1; }").expect("c-style for");
    }

    #[test]
    fn parse_package_statement() {
        parse("package Foo::Bar; 1;").expect("package");
    }

    #[test]
    fn parse_unless_block() {
        parse("unless (0) { 1; }").expect("unless");
    }

    #[test]
    fn parse_if_elsif_else() {
        parse("if (0) { 1; } elsif (1) { 2; } else { 3; }").expect("if elsif");
    }

    #[test]
    fn parse_q_constructor() {
        parse(r#"my $s = q{braces};"#).expect("q{}");
        parse(r#"my $t = qq(double);"#).expect("qq()");
    }

    #[test]
    fn parse_regex_literals() {
        parse("m/foo/;").expect("m//");
        parse("s/foo/bar/g;").expect("s///");
    }

    #[test]
    fn parse_begin_and_end_blocks() {
        parse("BEGIN { 1; }").expect("BEGIN");
        parse("END { 1; }").expect("END");
    }

    #[test]
    fn parse_transliterate_y() {
        parse("$_ = 'a'; y/a/A/;").expect("y//");
    }

    #[test]
    fn parse_foreach_with_my_iterator() {
        parse("foreach my $x (1, 2) { $x; }").expect("foreach my");
    }

    #[test]
    fn parse_our_declaration() {
        parse("our $g = 1;").expect("our");
    }

    #[test]
    fn parse_local_declaration() {
        parse("local $x = 1;").expect("local");
    }

    #[test]
    fn parse_use_no_statements() {
        parse("use strict;").expect("use");
        parse("no warnings;").expect("no");
    }

    #[test]
    fn parse_sub_with_prototype() {
        parse("sub sum ($$) { return $_0 + $_1; }").expect("sub prototype");
        parse("sub try (&;@) { my ( $try, @code_refs ) = @_; }").expect("prototype @ slurpy");
    }

    #[test]
    fn parse_list_expression_in_parentheses() {
        parse("my @a = (1, 2, 3);").expect("list");
    }

    #[test]
    fn parse_require_expression() {
        parse("require strict;").expect("require");
    }

    #[test]
    fn parse_do_string_eval_form() {
        parse(r#"do "foo.pl";"#).expect("do string");
    }

    #[test]
    fn parse_package_qualified_name() {
        parse("package Foo::Bar::Baz;").expect("package ::");
    }

    #[test]
    fn parse_my_multiple_declarations() {
        parse("my ($a, $b, $c);").expect("my list");
    }

    #[test]
    fn parse_eval_block_statement() {
        parse("eval { 1; };").expect("eval block");
    }

    #[test]
    fn parse_say_statement() {
        parse("say 42;").expect("say");
    }

    #[test]
    fn parse_chop_scalar() {
        parse("chop $s;").expect("chop");
    }

    #[test]
    fn vendor_perl_inc_path_points_at_vendor_perl() {
        let p = vendor_perl_inc_path();
        assert!(
            p.ends_with("vendor/perl"),
            "unexpected vendor path: {}",
            p.display()
        );
    }

    #[test]
    fn format_program_roundtrips_simple_expression() {
        let p = parse("$x + 1;").expect("parse");
        let out = format_program(&p);
        assert!(!out.trim().is_empty());
    }
}

#[cfg(test)]
mod builtins_extended_tests;

#[cfg(test)]
mod lib_api_extended_tests;

#[cfg(test)]
mod parallel_api_tests;

#[cfg(test)]
mod parse_smoke_extended;

#[cfg(test)]
mod parse_smoke_batch2;

#[cfg(test)]
mod parse_smoke_batch3;

#[cfg(test)]
mod parse_smoke_batch4;

#[cfg(test)]
mod crate_api_tests;

#[cfg(test)]
mod parser_shape_tests;

#[cfg(test)]
mod interpreter_unit_tests;

#[cfg(test)]
mod run_semantics_tests;

#[cfg(test)]
mod run_semantics_more;