use std::fs::File;
use std::io::{self, BufReader, IsTerminal, Read as IoRead, Write};
use std::path::{Path, PathBuf};
use std::process;
use std::sync::Mutex;
use clap::Parser;
use rand::Rng;
use rayon::prelude::*;
use stryke::ast::Program;
use stryke::error::{ErrorKind, PerlError};
use stryke::interpreter::Interpreter;
use stryke::perl_fs::{
decode_utf8_or_latin1, read_file_text_perl_compat, read_line_perl_compat,
read_logical_line_perl_compat,
};
mod repl;
#[derive(Parser, Debug, Default)]
#[command(name = "stryke", version, about, long_about = None)]
#[command(disable_version_flag = true, disable_help_flag = true)]
#[command(override_usage = "stryke [switches] [--] [programfile] [arguments]")]
pub(crate) struct Cli {
#[arg(short = '0', value_name = "OCTAL")]
input_separator: Option<Option<String>>,
#[arg(short = 'a')]
auto_split: bool,
#[arg(short = 'C', value_name = "NUMBER/LIST")]
unicode_features: Option<Option<String>>,
#[arg(short = 'c')]
check_only: bool,
#[arg(long = "lint", alias = "check")]
lint: bool,
#[arg(long = "disasm", alias = "disassemble")]
disasm: bool,
#[arg(long = "ast")]
dump_ast: bool,
#[arg(long = "fmt")]
format_source: bool,
#[arg(long = "profile")]
profile: bool,
#[arg(long = "flame")]
flame: bool,
#[arg(long = "no-jit")]
no_jit: bool,
#[arg(long = "explain", value_name = "CODE")]
explain: Option<String>,
#[arg(short = 'd', value_name = "MOD")]
debugger: Option<Option<String>>,
#[arg(short = 'D', value_name = "FLAGS")]
debug_flags: Option<Option<String>>,
#[arg(short = 'e')]
execute: Vec<String>,
#[arg(short = 'E')]
execute_features: Vec<String>,
#[arg(short = 'f')]
no_sitecustomize: bool,
#[arg(short = 'F', value_name = "PATTERN")]
field_separator: Option<String>,
#[arg(short = 'g')]
slurp: bool,
#[arg(short = 'i', value_name = "EXTENSION")]
inplace: Option<Option<String>>,
#[arg(short = 'I', value_name = "DIRECTORY")]
include: Vec<String>,
#[arg(short = 'l', value_name = "OCTNUM")]
line_ending: Option<Option<String>>,
#[arg(short = 'M', value_name = "MODULE")]
use_module: Vec<String>,
#[arg(short = 'm', value_name = "MODULE")]
use_module_no_import: Vec<String>,
#[arg(short = 'n')]
line_mode: bool,
#[arg(short = 'p')]
print_mode: bool,
#[arg(short = 's')]
switch_parsing: bool,
#[arg(short = 'S')]
path_lookup: bool,
#[arg(short = 't')]
taint_warn: bool,
#[arg(short = 'T')]
taint_check: bool,
#[arg(short = 'u')]
dump_core: bool,
#[arg(short = 'U')]
unsafe_ops: bool,
#[arg(short = 'v')]
show_version: bool,
#[arg(short = 'V', value_name = "CONFIGVAR")]
show_config: Option<Option<String>>,
#[arg(short = 'w')]
warnings: bool,
#[arg(short = 'W')]
all_warnings: bool,
#[arg(short = 'x', value_name = "DIRECTORY")]
extract: Option<Option<String>>,
#[arg(short = 'X')]
no_warnings: bool,
#[arg(short = 'h', long = "help")]
help: bool,
#[arg(short = 'j', long = "threads", value_name = "N")]
threads: Option<usize>,
#[arg(long = "compat")]
compat: bool,
#[arg(long = "script")]
force_script: bool,
#[arg(value_name = "SCRIPT")]
script: Option<String>,
#[arg(value_name = "ARGS", trailing_var_arg = true)]
args: Vec<String>,
}
fn expand_perl_bundled_argv(args: Vec<String>) -> Vec<String> {
if args.is_empty() {
return args;
}
let mut out = vec![args[0].clone()];
let mut seen_dd = false;
for arg in args.into_iter().skip(1) {
if seen_dd {
out.push(arg);
continue;
}
if arg == "--" {
seen_dd = true;
out.push(arg);
continue;
}
match expand_perl_bundled_token(&arg) {
Some(parts) => out.extend(parts),
None => out.push(arg),
}
}
out
}
fn expand_perl_bundled_token(arg: &str) -> Option<Vec<String>> {
match arg {
"-help" | "--help" => return Some(vec!["-h".to_string()]),
"-version" | "--version" => return Some(vec!["-v".to_string()]),
_ => {}
}
if arg == "-" || !arg.starts_with('-') || arg.starts_with("--") {
return None;
}
let s = arg.strip_prefix('-')?;
if s.is_empty() || s.len() == 1 {
return None;
}
if s.starts_with('>') || s.starts_with('~') {
return None;
}
if let Some(rest) = s.strip_prefix('0') {
let rest_ok = rest.chars().all(|c| matches!(c, '0'..='7'));
if rest_ok {
return None;
}
}
let mut out = Vec::new();
let b = s.as_bytes();
let mut i = 0usize;
while i < b.len() {
match b[i] {
b'0' if i == 0 => {
let mut j = i + 1;
while j < b.len() && matches!(b[j], b'0'..=b'7') {
j += 1;
}
out.push("-0".to_string());
if j > i + 1 {
out.push(s[i + 1..j].to_string());
}
i = j;
}
b'e' | b'E' => {
let flag = if b[i] == b'e' { "-e" } else { "-E" };
out.push(flag.to_string());
if i + 1 < b.len() {
out.push(s[i + 1..].to_string());
}
return Some(out);
}
b'l' => {
out.push("-l".to_string());
i += 1;
let start = i;
while i < b.len() && matches!(b[i], b'0'..=b'7') {
i += 1;
}
if i > start {
out.push(s[start..i].to_string());
}
}
b'F' | b'M' | b'm' | b'I' | b'd' | b'D' | b'x' | b'C' => {
let ch = b[i] as char;
out.push(format!("-{ch}"));
i += 1;
if i < b.len() {
out.push(s[i..].to_string());
}
return Some(out);
}
b'V' => {
out.push("-V".to_string());
i += 1;
if i < b.len() {
let rest = &s[i..];
let rest = rest.strip_prefix(':').unwrap_or(rest);
out.push(rest.to_string());
}
return Some(out);
}
b'i' => {
out.push("-i".to_string());
i += 1;
if i < b.len() && matches!(b[i], b'e' | b'E') {
continue;
}
if i < b.len() && b[i] == b'.' {
let start = i;
while i < b.len() && !matches!(b[i], b'e' | b'E') {
i += 1;
}
out.push(s[start..i].to_string());
}
}
_ => {
out.push(format!("-{}", b[i] as char));
i += 1;
}
}
}
Some(out)
}
fn print_cyberpunk_help() {
let version = env!("CARGO_PKG_VERSION");
let bin = env!("CARGO_BIN_NAME");
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
const C: &str = "\x1b[36m"; const M: &str = "\x1b[35m"; const R: &str = "\x1b[31m"; const Y: &str = "\x1b[33m"; const G: &str = "\x1b[32m"; const N: &str = "\x1b[0m";
println!("{C} ███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗{N}");
println!("{C} ██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝{N}");
println!("{M} ███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗ {N}");
println!("{M} ╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝ {N}");
println!("{R} ███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗{N}");
println!("{R} ╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝{N}");
println!("{C} ┌──────────────────────────────────────────────────┐{N}");
println!("{C} │ STATUS: ONLINE // CORES: {threads:<2} // SIGNAL: ██░ │{N}");
println!("{C} └──────────────────────────────────────────────────┘{N}");
println!("{M} >> PARALLEL PERL5 INTERPRETER // RUST-POWERED v{version} <<{N}");
println!();
println!();
println!("A highly parallel Perl 5 interpreter written in Rust");
println!();
println!("{Y} USAGE:{N} {bin} 'CODE' {G}//{N} -e is optional");
println!("{Y} {N} {bin} [switches] [--] [programfile] [arguments]");
println!();
println!("{C} ── EXECUTION ──────────────────────────────────────────{N}");
println!(" 'CODE' {G}//{N} Inline code — no -e needed if arg looks like code");
println!(" -e CODE {G}//{N} Explicit inline (required with -n/-p/-l/-a)");
println!(" -E CODE {G}//{N} Like -e, but enables all optional features");
println!(" --script {G}//{N} Force arg to be a file (skip code detection)");
println!(" -c {G}//{N} Check syntax only (parse; no compile/run)");
println!(" --lint / --check {G}//{N} Parse + compile bytecode without running");
println!(
" --disasm / --disassemble {G}//{N} Print bytecode disassembly to stderr before VM run"
);
println!(" --ast {G}//{N} Dump parsed AST as JSON and exit (no execution)");
println!(" --fmt {G}//{N} Pretty-print parsed Perl to stdout and exit");
println!(
" --explain CODE {G}//{N} Print expanded hint for an error code (e.g. E0001) and exit"
);
println!(
" --profile {G}//{N} Wall-clock profile stderr (VM op lines; flamegraph-ready)"
);
println!(
" --flame {G}//{N} Flamegraph: terminal bars (TTY) or SVG (piped to file)"
);
println!(" --no-jit {G}//{N} Disable Cranelift JIT (bytecode interpreter only)");
println!(
" --compat {G}//{N} Perl 5 strict-compat: disable all stryke extensions"
);
println!(" -d[t][:MOD] {G}//{N} Run program under debugger or module Devel::MOD");
println!(" -D[number/letters] {G}//{N} Set debugging flags");
println!(" -u {G}//{N} Dump core after parsing program");
println!("{C} ── INPUT PROCESSING ─────────────────────────────────{N}");
println!(" -n {G}//{N} Assume \"while (<>) {{...}}\" loop around program");
println!(" -p {G}//{N} Like -n but print line also, like sed");
println!(" -a {G}//{N} Autosplit mode (splits $_ into @F)");
println!(" -F/pattern/ {G}//{N} split() pattern for -a switch");
println!(" -l[octnum] {G}//{N} Enable line ending processing");
println!(" -0[octal] {G}//{N} Specify record separator (\\0 if no arg)");
println!(" -g {G}//{N} Slurp all input at once (alias for -0777)");
println!(" -i[extension] {G}//{N} Edit <> files in place (backup if ext supplied; multiple files in parallel)");
println!("{C} ── MODULES & PATHS ──────────────────────────────────{N}");
println!(" -M MODULE {G}//{N} Execute \"use module...\" before program");
println!(
" -m MODULE {G}//{N} Execute \"use module ()\" before program (no import)"
);
println!(" -I DIRECTORY {G}//{N} Specify @INC directory (several allowed)");
println!(" -f {G}//{N} Don't do $sitelib/sitecustomize.pl at startup");
println!(" -S {G}//{N} Look for programfile using PATH");
println!(" -x[directory] {G}//{N} Ignore text before #!perl line");
println!("{C} ── UNICODE & SAFETY ─────────────────────────────────{N}");
println!(" -C[number/list] {G}//{N} Enable listed Unicode features");
println!(" -t {G}//{N} Enable tainting warnings");
println!(" -T {G}//{N} Enable tainting checks");
println!(" -U {G}//{N} Allow unsafe operations");
println!(" -s {G}//{N} Enable switch parsing for programfile args");
println!("{C} ── WARNINGS ─────────────────────────────────────────{N}");
println!(" -w {G}//{N} Enable many useful warnings");
println!(" -W {G}//{N} Enable all warnings");
println!(" -X {G}//{N} Disable all warnings");
println!("{C} ── INFO ─────────────────────────────────────────────{N}");
println!(" -v {G}//{N} Print version, patchlevel and license");
println!(" -V[:configvar] {G}//{N} Print configuration summary");
println!(" -h, --help {G}//{N} Print help");
println!("{C} ── TOOLCHAIN ─────────────────────────────────────────{N}");
println!(
" --lsp {G}//{N} Language Server (JSON-RPC on stdio); must be the only arg after {bin}"
);
println!(
" build SCRIPT [-o OUT] {G}//{N} AOT: copy this binary with SCRIPT embedded (standalone exe)"
);
println!(" docs [TOPIC] {G}//{N} Built-in docs (stryke docs pmap, stryke docs |>, stryke docs)");
println!(
" serve [PORT] [SCRIPT] {G}//{N} HTTP server (stryke serve, stryke serve 8080 app.stk)"
);
println!(
" --remote-worker {G}//{N} Persistent cluster worker (stdio); only arg after {bin}"
);
println!(
" --remote-worker-v1 {G}//{N} Legacy one-shot worker (stdio); only arg after {bin}"
);
if matches!(bin, "stryke" | "st") {
println!(
" (no switches, TTY stdin) {G}//{N} Interactive REPL (readline; exit with quit or EOF)"
);
}
println!("{C} ── PARALLEL EXTENSIONS (stryke) ─────────────────────{N}");
println!(" -j N {G}//{N} Set number of parallel threads (rayon)");
println!(
" pmap {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel map; optional stderr progress bar"
);
println!(
" pmap_chunked N {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel map in batches of N items per thread"
);
println!(
" pcache {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel memoize (key = stringified topic)"
);
println!(
" par_lines PATH, CODE [, progress => EXPR] {G}//{N} mmap + parallel line scan (tree-walker)"
);
println!(
" par_walk PATH, CODE [, progress => EXPR] {G}//{N} parallel recursive dir walk; topic is each path"
);
println!(
" par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR] {G}//{N} parallel in-place regex replace per file (g)"
);
println!(
" pipeline @list ->filter/map/take/collect {G}//{N} Lazy iterator (runs on collect); chain ->pmap/pgrep/pfor/pmap_chunked/psort/pcache/preduce/… like top-level p*"
);
println!(
" par_pipeline @list same chain; filter/map parallel on collect (order kept); par_pipeline(source=>…,stages=>…,workers=>…) channel stages"
);
println!(
" async {{BLOCK}} {G}//{N} Run block on a worker thread; returns a task handle"
);
println!(" spawn {{BLOCK}} {G}//{N} Same as async (Rust-style); join with await");
println!(" await EXPR {G}//{N} Join async task or pass through non-task value");
println!(
" pgrep {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel grep across all cores"
);
println!(
" pfor {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel foreach across all cores"
);
println!(
" psort {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel sort across all cores"
);
println!(
" @list |> reduce {{BLOCK}} {G}//{N} Sequential left fold ($a accum, $b next element); also reduce {{BLOCK}} @list"
);
println!(
" @list |> preduce {{BLOCK}} [, progress => EXPR] {G}//{N} Parallel tree fold (rayon; associative ops only); also preduce {{BLOCK}} @list"
);
println!(
" @list |> preduce_init EXPR, {{BLOCK}} [, progress => EXPR] {G}//{N} Parallel fold with identity; also preduce_init EXPR, {{BLOCK}} @list"
);
println!(
" @list |> pmap_reduce {{MAP}} {{REDUCE}} [, progress => EXPR] {G}//{N} Fused parallel map + tree reduce; also pmap_reduce {{MAP}} {{REDUCE}} @list"
);
println!(
" fan [N] {{BLOCK}} [, progress => EXPR] {G}//{N} Execute BLOCK N times (default N = rayon pool; $_ = index); progress may follow }} without a comma"
);
println!(
" fan_cap [N] {{BLOCK}} [, progress => EXPR] {G}//{N} Like fan; returns list of block return values (index order)"
);
println!("{C} ── TYPING (stryke) ───────────────────────────────────{N}");
println!(
" typed my \\$x : Int|Str|Float {G}//{N} Optional scalar types; runtime checks on assign"
);
println!(
" fn (\\$a: Int, \\$b: Str) {{}} {G}//{N} Typed sub params; runtime checks on call"
);
println!("{C} ── SERIALIZATION (stryke) ───────────────────────────────{N}");
println!(
" str \\$val / stringify \\$val {G}//{N} Convert any value to parseable stryke literal"
);
println!(" eval str \\$fn {G}//{N} Round-trip: serialize + deserialize coderefs");
println!("{C} ── POSITIONAL ─────────────────────────────────────────{N}");
println!(" [programfile] {G}//{N} Perl script to execute");
println!(" [arguments] {G}//{N} Arguments passed to script (@ARGV)");
println!();
println!();
println!("{C} ── SYSTEM ─────────────────────────────────────────{N}");
println!("{M} v{version} {N}// {Y}(c) MenkeTechnologies{N}");
println!("{M} There is more than one way to do it — in parallel.{N}");
println!("{Y} >>> PARSE. EXECUTE. PARALLELIZE. OWN YOUR CORES. <<<{N}");
println!("{C} ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{N}");
}
pub(crate) fn module_prelude(cli: &Cli) -> String {
let mut full_code = String::new();
for module in &cli.use_module {
if let Some((mod_name, args)) = module.split_once('=') {
full_code.push_str(&format!(
"use {} qw({});\n",
mod_name,
args.replace(',', " ")
));
} else {
full_code.push_str(&format!("use {};\n", module));
}
}
for module in &cli.use_module_no_import {
if let Some(rest) = module.strip_prefix('-') {
full_code.push_str(&format!("no {};\n", rest));
} else {
full_code.push_str(&format!("use {} ();\n", module));
}
}
full_code
}
fn parse_cli_prelude(args: &[String]) -> Option<Cli> {
if args.len() <= 1 {
return None;
}
if args[1..].iter().any(|s| s == "--") {
return None;
}
for k in (1..=args.len()).rev() {
let trial: Vec<String> = if k == args.len() {
args.to_vec()
} else {
let mut t = args[..k].to_vec();
t.push("--".to_string());
t.extend(args[k..].iter().cloned());
t
};
let Some(cli) = Cli::try_parse_from(&trial).ok() else {
continue;
};
if cli.args.as_slice() == args[k..].as_ref() {
return Some(cli);
}
}
None
}
fn normalize_argv_after_dash_e(cli: &mut Cli) {
if (!cli.execute.is_empty() || !cli.execute_features.is_empty()) && cli.script.is_some() {
let mut v = vec![cli.script.take().unwrap()];
v.append(&mut cli.args);
cli.args = v;
}
}
fn adjacent_temp_path(target: &Path) -> PathBuf {
let dir = target.parent().unwrap_or_else(|| Path::new("."));
let name = target
.file_name()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "file".to_string());
let rnd: u32 = rand::thread_rng().gen();
dir.join(format!("{name}.stryke-tmp-{rnd}"))
}
fn commit_in_place_edit(path: &Path, inplace_edit: &str, new_content: &str) -> std::io::Result<()> {
let tmp = adjacent_temp_path(path);
std::fs::write(&tmp, new_content)?;
if !inplace_edit.is_empty() {
let backup = PathBuf::from(format!("{}{}", path.display(), inplace_edit));
let _ = std::fs::remove_file(&backup);
std::fs::rename(path, &backup)?;
}
std::fs::rename(&tmp, path)?;
Ok(())
}
fn line_mode_input_record(cli: &Cli, l: String) -> String {
if cli.line_ending.is_some() {
l
} else {
format!("{}\n", l)
}
}
fn line_content_from_stdin_read_line(buf: &str) -> String {
buf.strip_suffix("\r\n")
.or_else(|| buf.strip_suffix('\n'))
.or_else(|| buf.strip_suffix('\r'))
.unwrap_or(buf)
.to_string()
}
fn run_line_mode_loop(
cli: &Cli,
interp: &mut Interpreter,
program: &Program,
slurp: bool,
) -> Result<(), PerlError> {
let inplace = cli.inplace.is_some();
let use_argv_files = !interp.argv.is_empty();
let suppressed_stdout_for_inplace = inplace && use_argv_files;
let print_to_stdout = cli.print_mode && !suppressed_stdout_for_inplace;
let parallel_argv_inplace = inplace && use_argv_files;
if slurp {
if use_argv_files {
if parallel_argv_inplace {
let template = Mutex::new(interp.line_mode_worker_clone());
let paths = interp.argv.clone();
paths.into_par_iter().try_for_each(|path| {
let mut local = template
.lock()
.expect("line-mode template mutex poisoned")
.line_mode_worker_clone();
local.line_number = 0;
local.argv_current_file = path.clone();
let content = read_file_text_perl_compat(&path).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Can't open {}: {}", path, e),
0,
"-e",
)
})?;
if let Some(output) = local.process_line(&content, program, true)? {
commit_in_place_edit(Path::new(&path), &local.inplace_edit, &output)
.map_err(|e| PerlError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
}
Ok(())
})?;
} else {
for path in interp.argv.clone() {
interp.line_number = 0;
interp.argv_current_file = path.clone();
let content = read_file_text_perl_compat(&path).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Can't open {}: {}", path, e),
0,
"-e",
)
})?;
if let Some(output) = interp.process_line(&content, program, true)? {
if inplace {
commit_in_place_edit(Path::new(&path), &interp.inplace_edit, &output)
.map_err(|e| {
PerlError::new(ErrorKind::IO, e.to_string(), 0, "-e")
})?;
} else if cli.print_mode {
print!("{}", output);
let _ = io::stdout().flush();
}
}
}
}
} else {
let mut input = String::new();
let mut raw = Vec::new();
let _ = IoRead::read_to_end(&mut io::stdin(), &mut raw);
input.push_str(&decode_utf8_or_latin1(&raw));
if let Some(output) = interp.process_line(&input, program, true)? {
if print_to_stdout {
print!("{}", output);
let _ = io::stdout().flush();
}
}
}
return Ok(());
}
if use_argv_files {
if parallel_argv_inplace {
let template = Mutex::new(interp.line_mode_worker_clone());
let paths = interp.argv.clone();
paths.into_par_iter().try_for_each(|path| {
let mut local = template
.lock()
.expect("line-mode template mutex poisoned")
.line_mode_worker_clone();
local.line_number = 0;
local.argv_current_file = path.clone();
let file = File::open(&path).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Can't open {}: {}", path, e),
0,
"-e",
)
})?;
let mut reader = BufReader::new(file);
let mut accumulated = String::new();
let mut pending: Option<String> = None;
loop {
let l = if let Some(s) = pending.take() {
s
} else {
match read_logical_line_perl_compat(&mut reader).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Error reading {}: {}", path, e),
0,
"-e",
)
})? {
None => break,
Some(s) => s,
}
};
let is_last = match read_logical_line_perl_compat(&mut reader).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Error reading {}: {}", path, e),
0,
"-e",
)
})? {
None => true,
Some(next) => {
pending = Some(next);
false
}
};
let input = line_mode_input_record(cli, l);
if let Some(output) = local.process_line(&input, program, is_last)? {
accumulated.push_str(&output);
}
}
commit_in_place_edit(Path::new(&path), &local.inplace_edit, &accumulated)
.map_err(|e| PerlError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
Ok(())
})?;
} else {
for path in interp.argv.clone() {
interp.line_number = 0;
interp.argv_current_file = path.clone();
let file = File::open(&path).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Can't open {}: {}", path, e),
0,
"-e",
)
})?;
let mut reader = BufReader::new(file);
let mut accumulated = String::new();
let mut pending: Option<String> = None;
loop {
let l = if let Some(s) = pending.take() {
s
} else {
match read_logical_line_perl_compat(&mut reader).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Error reading {}: {}", path, e),
0,
"-e",
)
})? {
None => break,
Some(s) => s,
}
};
let is_last = match read_logical_line_perl_compat(&mut reader).map_err(|e| {
PerlError::new(
ErrorKind::IO,
format!("Error reading {}: {}", path, e),
0,
"-e",
)
})? {
None => true,
Some(next) => {
pending = Some(next);
false
}
};
let input = line_mode_input_record(cli, l);
if let Some(output) = interp.process_line(&input, program, is_last)? {
if print_to_stdout {
print!("{}", output);
let _ = io::stdout().flush();
}
if inplace {
accumulated.push_str(&output);
}
}
}
if inplace {
commit_in_place_edit(Path::new(&path), &interp.inplace_edit, &accumulated)
.map_err(|e| PerlError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
}
}
}
} else {
interp.line_mode_stdin_pending.clear();
loop {
let mut current = String::new();
let n = if let Some(queued) = interp.line_mode_stdin_pending.pop_front() {
current = queued;
current.len()
} else {
let mut lock = io::stdin().lock();
read_line_perl_compat(&mut lock, &mut current).map_err(|e| {
PerlError::new(ErrorKind::IO, format!("Error reading stdin: {e}"), 0, "-e")
})?
};
if n == 0 {
break;
}
let (is_last, peek_line) = {
let mut lock = io::stdin().lock();
let mut peek = String::new();
let n = read_line_perl_compat(&mut lock, &mut peek).map_err(|e| {
PerlError::new(ErrorKind::IO, format!("Error reading stdin: {e}"), 0, "-e")
})?;
if n == 0 {
(true, None)
} else {
(false, Some(peek))
}
};
if let Some(pl) = peek_line {
interp.line_mode_stdin_pending.push_back(pl);
}
let l = line_content_from_stdin_read_line(¤t);
let input = line_mode_input_record(cli, l);
match interp.process_line(&input, program, is_last) {
Ok(Some(output)) => {
if print_to_stdout {
print!("{}", output);
let _ = io::stdout().flush();
}
}
Ok(None) => {}
Err(e) => return Err(e),
}
}
}
Ok(())
}
pub(crate) fn configure_interpreter(cli: &Cli, interp: &mut Interpreter, filename: &str) {
interp.set_file(filename);
interp.warnings = (cli.warnings || cli.all_warnings) && !cli.no_warnings;
interp.auto_split = cli.auto_split;
interp.field_separator = cli.field_separator.clone();
interp.program_name = filename.to_string();
if let Some(ref sep) = cli.input_separator {
match sep.as_deref() {
None | Some("") => interp.irs = Some("\0".to_string()),
Some("777") => interp.irs = None, Some(oct_str) => {
if let Ok(val) = u32::from_str_radix(oct_str, 8) {
if let Some(ch) = char::from_u32(val) {
interp.irs = Some(ch.to_string());
}
}
}
}
}
if let Some(ref octnum) = cli.line_ending {
match octnum.as_deref() {
None | Some("") => {
interp.ors = "\n".to_string();
}
Some(oct_str) => {
if let Ok(val) = u32::from_str_radix(oct_str, 8) {
if let Some(ch) = char::from_u32(val) {
interp.ors = ch.to_string();
}
}
}
}
}
if (cli.taint_check || cli.taint_warn) && cli.warnings {
eprintln!("stryke: taint mode acknowledged but not enforced");
}
if let Some(ref ext_opt) = cli.inplace {
interp.inplace_edit = ext_opt.clone().unwrap_or_default();
}
let mut argv: Vec<String> =
if cli.script.is_some() || !cli.execute.is_empty() || !cli.execute_features.is_empty() {
cli.args.clone()
} else {
Vec::new()
};
if cli.switch_parsing {
let mut switches_done = false;
let mut remaining = Vec::new();
for arg in &argv {
if switches_done || !arg.starts_with('-') || arg == "--" {
if arg == "--" {
switches_done = true;
} else {
remaining.push(arg.clone());
}
} else {
let switch = &arg[1..];
if let Some((name, val)) = switch.split_once('=') {
let _ = interp
.scope
.set_scalar(name, stryke::value::PerlValue::string(val.to_string()));
} else {
let _ = interp
.scope
.set_scalar(switch, stryke::value::PerlValue::integer(1));
}
}
}
argv = remaining;
}
interp.argv = argv.clone();
interp.scope.declare_array(
"ARGV",
argv.into_iter()
.map(stryke::value::PerlValue::string)
.collect(),
);
let mut inc_paths: Vec<String> = cli.include.clone();
let vendor = stryke::vendor_perl_inc_path();
if vendor.is_dir() {
stryke::perl_inc::push_unique_string_paths(
&mut inc_paths,
vec![vendor.to_string_lossy().into_owned()],
);
}
stryke::perl_inc::push_unique_string_paths(
&mut inc_paths,
stryke::perl_inc::paths_from_system_perl(),
);
if filename != "-e" && filename != "-" && filename != "repl" {
if let Some(parent) = std::path::Path::new(filename).parent() {
if !parent.as_os_str().is_empty() {
stryke::perl_inc::push_unique_string_paths(
&mut inc_paths,
vec![parent.to_string_lossy().into_owned()],
);
}
}
}
if let Ok(extra) = std::env::var("STRYKE_INC") {
let extra: Vec<String> = std::env::split_paths(&extra)
.map(|p| p.to_string_lossy().into_owned())
.collect();
stryke::perl_inc::push_unique_string_paths(&mut inc_paths, extra);
}
stryke::perl_inc::push_unique_string_paths(&mut inc_paths, vec![".".to_string()]);
let inc_dirs: Vec<stryke::value::PerlValue> = inc_paths
.into_iter()
.map(stryke::value::PerlValue::string)
.collect();
interp.scope.declare_array("INC", inc_dirs);
if cli.debugger.is_some() {
eprintln!("stryke: debugger not yet implemented, running normally");
}
}
fn emit_profiler_report(
p: &mut stryke::profiler::Profiler,
flame_out: &Option<File>,
flame_tty: bool,
) {
if let Some(f) = flame_out {
let mut w = io::BufWriter::new(f);
if let Err(e) = p.render_flame_svg(&mut w) {
eprintln!("stryke --flame: {}", e);
}
} else if flame_tty {
p.render_flame_tty();
} else {
p.print_report();
}
}
fn main() {
if let Ok(exe) = std::env::current_exe() {
if let Some(embedded) = stryke::aot::try_load_embedded(&exe) {
let argv: Vec<String> = std::env::args().skip(1).collect();
process::exit(run_embedded_script(embedded, argv));
}
}
let args = expand_perl_bundled_argv(std::env::args().collect());
if args.len() == 2 && args[1] == "--remote-worker" {
process::exit(stryke::remote_wire::run_remote_worker_session());
}
if args.len() == 2 && args[1] == "--remote-worker-v1" {
process::exit(stryke::remote_wire::run_remote_worker_stdio());
}
if args.len() == 2 && args[1] == "--lsp" {
process::exit(stryke::run_lsp_stdio());
}
if args.len() >= 2 && args[1] == "build" {
process::exit(run_build_subcommand(&args[2..]));
}
if args.len() >= 2 && args[1] == "convert" {
process::exit(run_convert_subcommand(&args[2..]));
}
if args.len() >= 2 && args[1] == "deconvert" {
process::exit(run_deconvert_subcommand(&args[2..]));
}
if args.len() >= 2 && args[1] == "docs" {
process::exit(run_doc_subcommand(&args[2..]));
}
if args.len() >= 2 && args[1] == "serve" {
process::exit(run_serve_subcommand(&args[2..]));
}
let arg1_is_code_not_flag =
args.len() >= 2 && args[1].starts_with('-') && looks_like_code(&args[1]);
let mut cli = if args.len() >= 2
&& (!args[1].starts_with('-') || arg1_is_code_not_flag)
&& !args[1].is_empty()
&& args[2..].iter().all(|a| !a.starts_with('-'))
{
Cli {
script: Some(args[1].clone()),
args: if args.len() > 2 {
args[2..].to_vec()
} else {
Vec::new()
},
..Default::default()
}
} else {
parse_cli_prelude(&args).unwrap_or_else(|| Cli::parse_from(&args))
};
normalize_argv_after_dash_e(&mut cli);
if cli.compat {
stryke::set_compat_mode(true);
}
if cli.help {
print_cyberpunk_help();
return;
}
if cli.show_version {
println!(
"This is stryke v{} — A highly parallel Perl 5 interpreter (Rust)\n",
env!("CARGO_PKG_VERSION")
);
println!("Built with rayon for parallel map/grep/for/sort");
println!(
"Threads available: {}\n",
std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1)
);
println!(
"Copyright 2026 MenkeTechnologies. Licensed under MIT.\n\n\
This is free software; you can redistribute it and/or modify it\n\
under the terms of the MIT License."
);
return;
}
if let Some(ref configvar) = cli.show_config {
print_config(configvar.as_deref());
return;
}
if let Some(code) = &cli.explain {
match stryke::error::explain_error(code) {
Some(text) => println!("{}", text),
None => {
eprintln!("stryke: unknown explain code {:?}", code);
process::exit(1);
}
}
return;
}
if let Some(n) = cli.threads {
rayon::ThreadPoolBuilder::new()
.num_threads(n)
.build_global()
.ok();
}
let is_repl = matches!(env!("CARGO_BIN_NAME"), "stryke" | "st")
&& cli.script.is_none()
&& cli.execute.is_empty()
&& cli.execute_features.is_empty()
&& !cli.line_mode
&& !cli.print_mode
&& !cli.check_only
&& !cli.lint
&& !cli.disasm
&& !cli.dump_ast
&& !cli.format_source
&& !cli.profile
&& !cli.flame
&& !cli.dump_core
&& cli.explain.is_none()
&& io::stdin().is_terminal();
if is_repl {
repl::run(&cli);
return;
}
let slurp = cli.slurp
|| cli
.input_separator
.as_ref()
.is_some_and(|v| v.as_deref() == Some("777"));
let (raw_script, filename): (String, String) = if !cli.execute.is_empty() {
(cli.execute.join("; "), "-e".to_string())
} else if !cli.execute_features.is_empty() {
(cli.execute_features.join("; "), "-E".to_string())
} else if let Some(ref script) = cli.script {
if script == "-" {
let mut code = Vec::new();
let _ = IoRead::read_to_end(&mut io::stdin(), &mut code);
let code = decode_utf8_or_latin1(&code);
(code, "-".to_string())
} else {
let script_path = if cli.path_lookup {
find_in_path(script).unwrap_or_else(|| script.clone())
} else {
script.clone()
};
match read_file_text_perl_compat(&script_path) {
Ok(content) => (content, script_path),
Err(_) if !cli.force_script && looks_like_code(&script_path) => {
(script_path, "-e".to_string())
}
Err(e) => {
eprintln!("Can't open perl script \"{}\": {}", script_path, e);
process::exit(2);
}
}
}
} else if cli.line_mode || cli.print_mode {
(String::new(), "-".to_string())
} else {
let mut code = Vec::new();
let _ = IoRead::read_to_end(&mut io::stdin(), &mut code);
let code = decode_utf8_or_latin1(&code);
(code, "-".to_string())
};
let (program_text, data_opt) = stryke::data_section::split_data_section(&raw_script);
let code = strip_shebang_and_extract(&program_text, cli.extract.is_some());
let mut full_code = module_prelude(&cli);
full_code.push_str(&code);
let is_one_liner = !cli.execute.is_empty() || !cli.execute_features.is_empty();
let pec_on = stryke::pec::cache_enabled()
&& !cli.line_mode
&& !cli.print_mode
&& !cli.lint
&& !cli.check_only
&& !cli.dump_ast
&& !cli.format_source
&& !cli.profile
&& !cli.flame
&& !is_one_liner
&& !filename.is_empty();
let pec_fp_opt: Option<[u8; 32]> = if pec_on {
Some(stryke::pec::source_fingerprint(
false, &filename, &full_code,
))
} else {
None
};
let cached_bundle = pec_fp_opt
.as_ref()
.and_then(|fp| stryke::pec::try_load(fp, false).ok().flatten());
let (program, pec_precompiled) = if let Some(bundle) = cached_bundle {
(bundle.program, Some(bundle.chunk))
} else {
let parsed = match stryke::parse_with_file(&full_code, &filename) {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
process::exit(255);
}
};
(parsed, None)
};
if cli.dump_ast {
match serde_json::to_string_pretty(&program) {
Ok(json) => println!("{}", json),
Err(e) => {
eprintln!("stryke: failed to serialize AST to JSON: {}", e);
process::exit(1);
}
}
return;
}
if cli.format_source {
println!("{}", stryke::convert::convert_program(&program));
return;
}
if cli.lint {
let mut interp = Interpreter::new();
if cli.no_jit {
interp.vm_jit_enabled = false;
}
configure_interpreter(&cli, &mut interp, &filename);
if let Some(data) = data_opt {
interp.install_data_handle(data);
}
match stryke::lint_program(&program, &mut interp) {
Ok(()) => {
eprintln!("{} compile OK", filename);
return;
}
Err(e) => {
eprintln!("{}", e);
process::exit(255);
}
}
}
if cli.check_only {
eprintln!("{} syntax OK", filename);
return;
}
if cli.dump_core {
eprintln!("{} syntax OK (dump not supported)", filename);
return;
}
let mut interp = Interpreter::new();
if cli.no_jit {
interp.vm_jit_enabled = false;
}
if cli.disasm {
interp.disasm_bytecode = true;
}
if cli.profile || cli.flame {
interp.profiler = Some(stryke::profiler::Profiler::new(filename.clone()));
}
interp.pec_precompiled_chunk = pec_precompiled;
interp.pec_cache_fingerprint = if pec_on { pec_fp_opt } else { None };
configure_interpreter(&cli, &mut interp, &filename);
if let Some(data) = data_opt {
interp.install_data_handle(data);
}
let flame_is_tty = cli.flame && io::stdout().is_terminal();
#[cfg(unix)]
let flame_stdout: Option<File> = if cli.flame && !flame_is_tty {
use std::os::unix::io::FromRawFd;
let saved = unsafe { libc::dup(1) };
if saved >= 0 {
unsafe { libc::dup2(2, 1) };
Some(unsafe { File::from_raw_fd(saved) })
} else {
None
}
} else {
None
};
#[cfg(not(unix))]
let flame_stdout: Option<File> = None;
if cli.line_mode || cli.print_mode {
if cli.line_ending.is_some() {
interp.ors = "\n".to_string();
}
interp.line_mode_skip_main = true;
if let Err(e) = interp.execute(&program) {
interp.line_mode_skip_main = false;
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
if let ErrorKind::Exit(code) = e.kind {
process::exit(code);
}
eprintln!("{}", e);
process::exit(255);
}
interp.line_mode_skip_main = false;
if let Err(e) = run_line_mode_loop(&cli, &mut interp, &program, slurp) {
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
if let ErrorKind::Exit(code) = e.kind {
process::exit(code);
}
eprintln!("{}", e);
process::exit(255);
}
if let Err(e) = interp.run_end_blocks() {
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
if let ErrorKind::Exit(code) = e.kind {
process::exit(code);
}
eprintln!("{}", e);
process::exit(255);
}
let _ = interp.run_global_teardown();
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
} else {
match interp.execute(&program) {
Ok(_) => {
let _ = interp.run_global_teardown();
let _ = io::stdout().flush();
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
}
Err(e) => match e.kind {
ErrorKind::Exit(code) => {
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
process::exit(code);
}
ErrorKind::Die => {
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
eprint!("{}", e);
process::exit(255);
}
_ => {
if let Some(mut p) = interp.profiler.take() {
emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
}
eprintln!("{}", e);
process::exit(255);
}
},
}
}
}
fn run_embedded_script(embedded: stryke::aot::EmbeddedScript, argv: Vec<String>) -> i32 {
let pec_on = stryke::pec::cache_enabled();
let pec_fp = if pec_on {
Some(stryke::pec::source_fingerprint(
false,
&embedded.name,
&embedded.source,
))
} else {
None
};
let cached = pec_fp
.as_ref()
.and_then(|fp| stryke::pec::try_load(fp, false).ok().flatten());
let (program, pec_precompiled) = if let Some(bundle) = cached {
(bundle.program, Some(bundle.chunk))
} else {
let parsed = match stryke::parse_with_file(&embedded.source, &embedded.name) {
Ok(p) => p,
Err(e) => {
eprintln!("{}", e);
return 255;
}
};
(parsed, None)
};
let mut interp = Interpreter::new();
interp.set_file(&embedded.name);
interp.program_name = embedded.name.clone();
interp.argv = argv.clone();
interp.scope.declare_array(
"ARGV",
argv.into_iter()
.map(stryke::value::PerlValue::string)
.collect(),
);
interp.scope.declare_array(
"INC",
vec![stryke::value::PerlValue::string(".".to_string())],
);
interp.pec_precompiled_chunk = pec_precompiled;
interp.pec_cache_fingerprint = pec_fp;
match interp.execute(&program) {
Ok(_) => {
let _ = interp.run_global_teardown();
let _ = io::stdout().flush();
0
}
Err(e) => match e.kind {
ErrorKind::Exit(code) => code,
ErrorKind::Die => {
eprint!("{}", e);
255
}
_ => {
eprintln!("{}", e);
255
}
},
}
}
fn run_build_subcommand(args: &[String]) -> i32 {
let mut script: Option<String> = None;
let mut out: Option<String> = None;
let mut i = 0usize;
while i < args.len() {
match args[i].as_str() {
"-o" | "--output" => {
i += 1;
if i >= args.len() {
eprintln!("stryke build: -o requires an argument");
return 2;
}
out = Some(args[i].clone());
}
"-h" | "--help" => {
println!("usage: stryke build SCRIPT [-o OUTPUT]");
println!();
println!(
"Compile a Perl script into a standalone executable binary. The output is"
);
println!(
"a copy of this stryke binary with the script source embedded as a compressed"
);
println!(
"trailer. `scp` the result to any compatible machine and run it directly —"
);
println!("no perl, no stryke, no @INC setup required.");
println!();
println!("Examples:");
println!(" stryke build app.pl # → ./app");
println!(" stryke build app.pl -o /usr/local/bin/app");
return 0;
}
s if script.is_none() && !s.starts_with('-') => script = Some(s.to_string()),
other => {
eprintln!("stryke build: unknown argument: {}", other);
eprintln!("usage: stryke build SCRIPT [-o OUTPUT]");
return 2;
}
}
i += 1;
}
let Some(script) = script else {
eprintln!("stryke build: missing SCRIPT");
eprintln!("usage: stryke build SCRIPT [-o OUTPUT]");
return 2;
};
let script_path = PathBuf::from(&script);
let out_path = PathBuf::from(out.unwrap_or_else(|| {
script_path
.file_stem()
.map(|s| s.to_string_lossy().into_owned())
.unwrap_or_else(|| "a.out".to_string())
}));
match stryke::aot::build(&script_path, &out_path) {
Ok(p) => {
eprintln!("stryke build: wrote {}", p.display());
0
}
Err(e) => {
eprintln!("{}", e);
1
}
}
}
fn run_convert_subcommand(args: &[String]) -> i32 {
let mut files: Vec<String> = Vec::new();
let mut in_place = false;
let mut output_delim: Option<char> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-i" | "--in-place" => in_place = true,
"-d" | "--output-delim" => {
i += 1;
if i >= args.len() {
eprintln!("stryke convert: --output-delim requires an argument");
return 2;
}
let delim_str = &args[i];
if delim_str.chars().count() != 1 {
eprintln!(
"stryke convert: --output-delim must be a single character, got {:?}",
delim_str
);
return 2;
}
output_delim = delim_str.chars().next();
}
"-h" | "--help" => {
println!("usage: stryke convert [-i] [-d DELIM] FILE...");
println!();
println!("Convert standard Perl source to idiomatic stryke syntax:");
println!(" - Nested calls → |> pipe-forward chains");
println!(" - map/grep/sort/join LIST → LIST |> map/grep/sort/join");
println!(" - No trailing semicolons");
println!(" - 4-space indentation");
println!(" - #!/usr/bin/env stryke shebang");
println!();
println!("Options:");
println!(" -i, --in-place Write .stk files alongside originals");
println!(" -d, --output-delim Delimiter for s///, tr///, m// (default: preserve original)");
println!();
println!("Examples:");
println!(" stryke convert app.pl # print to stdout");
println!(" stryke convert -i lib/*.pm # write lib/*.stk");
println!(" stryke convert -d '|' app.pl # use | as delimiter: s|old|new|g");
return 0;
}
s if s.starts_with('-') => {
eprintln!("stryke convert: unknown option: {}", s);
eprintln!("usage: stryke convert [-i] [-d DELIM] FILE...");
return 2;
}
s => files.push(s.to_string()),
}
i += 1;
}
if files.is_empty() {
eprintln!("stryke convert: no input files");
eprintln!("usage: stryke convert [-i] [-d DELIM] FILE...");
return 2;
}
let opts = stryke::convert::ConvertOptions { output_delim };
let mut errors = 0;
for f in &files {
let code = match std::fs::read_to_string(f) {
Ok(c) => c,
Err(e) => {
eprintln!("stryke convert: {}: {}", f, e);
errors += 1;
continue;
}
};
let program = match stryke::parse_with_file(&code, f) {
Ok(p) => p,
Err(e) => {
eprintln!("stryke convert: {}: {}", f, e);
errors += 1;
continue;
}
};
let converted = stryke::convert_to_stryke_with_options(&program, &opts);
if in_place {
let out_path = std::path::Path::new(f).with_extension("pr");
if let Err(e) = std::fs::write(&out_path, &converted) {
eprintln!("stryke convert: {}: {}", out_path.display(), e);
errors += 1;
}
} else {
println!("{}", converted);
}
}
if errors > 0 {
1
} else {
0
}
}
fn run_serve_subcommand(args: &[String]) -> i32 {
if !args.is_empty() && (args[0] == "-h" || args[0] == "--help") {
eprintln!("usage: stryke serve [PORT] [SCRIPT | -e CODE]");
eprintln!();
eprintln!(" stryke serve serve $PWD on port 8000");
eprintln!(" stryke serve PORT serve $PWD as static files");
eprintln!(" stryke serve PORT SCRIPT run script (must call serve())");
eprintln!(" stryke serve PORT -e CODE one-liner handler");
eprintln!();
eprintln!(" Handler receives $req (hashref: method, path, query, headers, body, peer)");
eprintln!(" and returns: string (200 OK), key-value pairs, hashref, or undef (404).");
eprintln!();
eprintln!("examples:");
eprintln!(
" stryke serve # static file server on 8000"
);
eprintln!(
" stryke serve 8080 # static file server"
);
eprintln!(" stryke serve 8080 app.stk # script handler");
eprintln!(" stryke serve 3000 -e '\"hello \" . $req->{{path}}' # one-liner");
eprintln!(" stryke serve 8080 -e 'status => 200, body => json_encode(+{{ok => 1}})'");
return 0;
}
let (port, rest) = if !args.is_empty() && args[0].parse::<u16>().is_ok() {
(args[0].clone(), &args[1..])
} else {
("8000".to_string(), args)
};
let static_dir = if rest.is_empty() {
Some(
std::env::current_dir()
.unwrap_or_default()
.to_string_lossy()
.to_string(),
)
} else if rest[0] != "-e" && Path::new(&rest[0]).is_dir() {
Some(
std::fs::canonicalize(&rest[0])
.unwrap_or_else(|_| PathBuf::from(&rest[0]))
.to_string_lossy()
.to_string(),
)
} else {
None
};
let code = if let Some(dir) = static_dir {
let dir_escaped = dir.replace('\\', "\\\\").replace('"', "\\\"");
eprintln!("stryke: serving {} on http://0.0.0.0:{}", dir, port);
format!(
r#"
chdir "{dir_escaped}"
my %mime = (
html => "text/html; charset=utf-8",
htm => "text/html; charset=utf-8",
css => "text/css; charset=utf-8",
js => "application/javascript; charset=utf-8",
mjs => "application/javascript; charset=utf-8",
json => "application/json; charset=utf-8",
xml => "text/xml; charset=utf-8",
md => "text/markdown; charset=utf-8",
txt => "text/plain; charset=utf-8",
toml => "application/toml; charset=utf-8",
pl => "text/x-perl; charset=utf-8",
pr => "text/x-perl; charset=utf-8",
pm => "text/x-perl; charset=utf-8",
png => "image/png",
jpg => "image/jpeg",
jpeg => "image/jpeg",
gif => "image/gif",
svg => "image/svg+xml",
webp => "image/webp",
avif => "image/avif",
ico => "image/x-icon",
woff2 => "font/woff2",
woff => "font/woff",
ttf => "font/ttf",
mp3 => "audio/mpeg",
ogg => "audio/ogg",
mp4 => "video/mp4",
webm => "video/webm",
zip => "application/zip",
gz => "application/gzip",
wasm => "application/wasm",
pdf => "application/pdf"
)
fn mime_for($path) {{
my $ext = $path =~ /\.([^.]+)$/ ? lc($1) : ""
$mime{{$ext}} // "text/plain"
}}
fn dir_listing($url_path, $fs_path) {{
$url_path .= "/" unless $url_path =~ m|/$|
my $prefix = $fs_path eq "." ? "" : "$fs_path/"
my @entries
push @entries, ".." unless $url_path eq "/"
push @entries, dirs($fs_path)
push @entries, filesf($fs_path)
my $html = ""
for my $e (@entries) {{
my $full = $e eq ".." ? ".." : "$prefix$e"
my $name = $e
my $href = $url_path . $name
if (-d $full) {{
$html .= "<li class=\"dir\"><a href=\"$href/\">$name/</a></li>"
}} else {{
my $sz = (stat($full))[7] // 0
$html .= "<li><a href=\"$href\">$name</a> <span style=\"color:#888\">($sz bytes)</span></li>"
}}
}}
"<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
. "<title>Directory listing for $url_path</title>"
. "<style>body{{font-family:monospace;margin:2em}}a{{text-decoration:none}}a:hover{{text-decoration:underline}}li{{padding:2px 0}}.dir{{font-weight:bold}}</style>"
. "</head><body><h1>Directory listing for $url_path</h1><hr><ul>"
. $html
. "</ul><hr><p style=\"color:#888\">stryke/{port}</p></body></html>"
}}
serve {port}, fn ($req) {{
my $url_path = $req->{{path}}
$url_path =~ s|\.\./||g
my $fs_path = $url_path =~ s|^/||r
$fs_path = "." if $fs_path eq ""
if (-d $fs_path) {{
my $idx = $fs_path eq "." ? "index.html" : "$fs_path/index.html"
if (-f $idx) {{
+{{ status => 200, body => cat($idx), headers => +{{ "content-type" => "text/html; charset=utf-8" }} }}
}} else {{
+{{ status => 200, body => dir_listing($url_path, $fs_path), headers => +{{ "content-type" => "text/html; charset=utf-8" }} }}
}}
}} elsif (-f $fs_path) {{
+{{ status => 200, body => cat($fs_path), headers => +{{ "content-type" => mime_for($fs_path) }} }}
}} else {{
+{{ status => 404, body => "404 Not Found: $url_path\n" }}
}}
}}
"#
)
} else if rest[0] == "-e" {
if rest.len() < 2 {
eprintln!("stryke serve: -e requires an argument");
return 1;
}
let handler_body = rest[1..].join(" ");
format!("serve {}, fn ($req) {{ {} }}", port, handler_body)
} else {
let script_path = &rest[0];
match std::fs::read_to_string(script_path) {
Ok(src) => {
format!("$ENV{{STRYKE_PORT}} = {}\n{}", port, src)
}
Err(e) => {
eprintln!("stryke serve: {}: {}", script_path, e);
return 1;
}
}
};
let mut interp = stryke::interpreter::Interpreter::new();
match stryke::parse_and_run_string(&code, &mut interp) {
Ok(_) => 0,
Err(e) => {
if let stryke::error::ErrorKind::Exit(code) = e.kind {
return code;
}
eprintln!("{}", e);
255
}
}
}
#[allow(non_snake_case)]
fn run_doc_subcommand(args: &[String]) -> i32 {
let theme = DocTheme {
C: "\x1b[36m",
G: "\x1b[32m",
Y: "\x1b[1;33m",
M: "\x1b[35m",
B: "\x1b[1m",
D: "\x1b[2m",
N: "\x1b[0m",
};
let DocTheme {
C,
G,
Y,
M,
B,
D,
N,
} = theme;
let mut entries: Vec<(&str, &str, String)> = Vec::new();
let mut seen = std::collections::HashSet::new();
let mut seen_text_ptrs = std::collections::HashSet::new();
for &(category, topics) in stryke::lsp::DOC_CATEGORIES {
for &topic in topics {
if let Some(text) = stryke::lsp::doc_text_for(topic) {
let ptr = text.as_ptr() as usize;
if !seen_text_ptrs.insert(ptr) {
seen.insert(topic);
continue; }
let rendered = render_page_content(topic, text, C, G, D, N);
entries.push((category, topic, rendered));
seen.insert(topic);
}
}
}
for topic in stryke::lsp::doc_topics() {
if seen.contains(topic) {
continue;
}
if let Some(text) = stryke::lsp::doc_text_for(topic) {
let ptr = text.as_ptr() as usize;
if !seen_text_ptrs.insert(ptr) {
continue; }
let rendered = render_page_content(topic, text, C, G, D, N);
entries.push(("Other", topic, rendered));
}
}
if entries.is_empty() {
eprintln!("stryke docs: no documentation pages found");
return 1;
}
let content_area = term_height().saturating_sub(14).max(4);
let mut pages = build_fixed_pages(&entries, content_area);
let entry_count = entries.len();
let chapter_count = stryke::lsp::DOC_CATEGORIES.len();
let mut intro = format!(
"\
{D}>> THE STRYKE ENCYCLOPEDIA // INTERACTIVE REFERENCE SYSTEM <<{N}\n\
\n\
{B}A comprehensive reference for every stryke builtin, keyword,{N}\n\
{B}and extension. {G}{entry_count}{N} {B}topics across {G}{chapter_count}{N} {B}chapters.{N}\n\
\n\
{D}── GETTING STARTED ─────────────────────────────────────────────{N}\n\
\n\
{C}j{N} / {C}n{N} / {C}space{N} next page\n\
{C}k{N} / {C}p{N} previous page\n\
{C}]{N} / {C}[{N} next / previous chapter\n\
{C}d{N} / {C}u{N} forward / back 5 pages\n\
{C}g{N} / {C}G{N} first / last page\n\
{C}t{N} table of contents\n\
{C}/{N} search all pages\n\
{C}:{N} jump to page number\n\
{C}r{N} random page\n\
{C}?{N} full keybinding help\n\
{C}q{N} quit\n\
\n\
{D}── CHAPTERS ───────────────────────────────────────────────────{N}\n\
"
);
for (i, &(cat, topics)) in stryke::lsp::DOC_CATEGORIES.iter().enumerate() {
intro.push_str(&format!(
" {C}{:>2}.{N} {B}{:<32}{N} {D}{} topics{N}\n",
i + 1,
cat,
topics.len(),
));
}
intro.push_str(&format!(
"\n {D}press {C}j{D} or {C}space{D} to begin >>>{N}\n"
));
let intro_page = pad_to_height(&intro, content_area);
pages.insert(0, ("Introduction".to_string(), intro_page, Vec::new()));
let total = pages.len();
if args.first().map(|s| s.as_str()) == Some("-h")
|| args.first().map(|s| s.as_str()) == Some("--help")
{
println!();
doc_print_banner(theme);
doc_print_hline('┌', '┐', theme);
doc_print_boxline(
&format!(" {G}STATUS: ONLINE{N} {D}//{N} {C}SIGNAL: {G}████████{D}░░{N} {D}//{N} {M}STRYKE DOCS{N}"),
theme,
);
doc_print_hline('└', '┘', theme);
println!(" {D}>> THE STRYKE ENCYCLOPEDIA // INTERACTIVE REFERENCE SYSTEM <<{N}");
println!();
println!(" {B}USAGE:{N} stryke docs {D}[OPTIONS] [PAGE|TOPIC]{N}");
println!();
doc_print_separator("OPTIONS", theme);
println!(" {C}-h, --help{N} {D}// Show this help{N}");
println!(" {C}-t, --toc{N} {D}// Table of contents{N}");
println!(" {C}-s, --search <pattern>{N} {D}// Search pages{N}");
println!(" {C}-l, --list{N} {D}// List all pages{N}");
println!(
" {C}TOPIC{N} {D}// Jump to topic (stryke docs pmap){N}"
);
println!(" {C}PAGE{N} {D}// Jump to page number{N}");
println!();
doc_print_separator("NAVIGATION (vim-style)", theme);
println!(" {C}j / n / l / enter / space{N} {D}// Next page{N}");
println!(" {C}k / p / h{N} {D}// Previous page{N}");
println!(" {C}d{N} {D}// Forward 5 pages{N}");
println!(" {C}u{N} {D}// Back 5 pages{N}");
println!(" {C}g / 0{N} {D}// First page{N}");
println!(" {C}G / ${N} {D}// Last page{N}");
println!(" {C}] / }}{N} {D}// Next chapter{N}");
println!(" {C}[ / {{{N} {D}// Previous chapter{N}");
println!(" {C}t{N} {D}// Table of contents{N}");
println!(" {C}/ <pattern>{N} {D}// Search pages{N}");
println!(" {C}:<number>{N} {D}// Jump to page{N}");
println!(" {C}r{N} {D}// Random page{N}");
println!(" {C}?{N} {D}// Keybinding help{N}");
println!(" {C}q{N} {D}// Quit{N}");
println!();
doc_print_separator("EXAMPLES", theme);
println!(" {C}stryke docs{N} {D}// start from page 1{N}");
println!(" {C}stryke docs --toc{N} {D}// table of contents{N}");
println!(" {C}stryke docs 42{N} {D}// jump to page 42{N}");
println!(" {C}stryke docs pmap{N} {D}// jump to pmap{N}");
println!(" {C}stryke docs --search parallel{N} {D}// find parallel pages{N}");
println!();
return 0;
}
if args.first().map(|s| s.as_str()) == Some("-t")
|| args.first().map(|s| s.as_str()) == Some("--toc")
{
doc_print_toc_entries(&entries, &pages, theme);
return 0;
}
if args.first().map(|s| s.as_str()) == Some("-l")
|| args.first().map(|s| s.as_str()) == Some("--list")
{
for (i, (_, topic, _)) in entries.iter().enumerate() {
println!("{:>3}. {}", i + 1, topic);
}
return 0;
}
if (args.first().map(|s| s.as_str()) == Some("-s")
|| args.first().map(|s| s.as_str()) == Some("--search"))
&& args.len() >= 2
{
let pat = args[1].to_lowercase();
let mut found = 0;
for (i, (cat, topic, text)) in entries.iter().enumerate() {
if topic.to_lowercase().contains(&pat)
|| cat.to_lowercase().contains(&pat)
|| text.to_lowercase().contains(&pat)
{
println!(" {C}{:>3}.{N} {B}{}{N} {D}({}){N}", i + 1, topic, cat);
found += 1;
}
}
if found == 0 {
println!(" {Y}no results for '{}'{N}", pat);
}
return 0;
}
let mut start_page: usize = 0;
if !args.is_empty() {
let arg = &args[0];
if let Ok(n) = arg.parse::<usize>() {
if n >= 1 && n <= total {
start_page = n - 1;
}
} else {
let lower = arg.to_lowercase();
let entry_idx = entries
.iter()
.position(|(_, t, _)| t.to_lowercase() == lower)
.or_else(|| {
entries
.iter()
.position(|(_, t, _)| t.to_lowercase().contains(&lower))
});
match entry_idx {
Some(eidx) => {
start_page = pages
.iter()
.position(|(_, _, indices)| indices.contains(&eidx))
.unwrap_or(0);
}
None => {
eprintln!("stryke docs: no documentation for '{}'", arg);
eprintln!("run 'stryke docs -h' for help");
return 1;
}
}
}
}
if !io::stdout().is_terminal() {
print!("{}", pages[start_page].1);
return 0;
}
doc_interactive_loop(&pages, &entries, &intro, start_page, total, theme)
}
fn pad_to_height(text: &str, height: usize) -> String {
let lines: Vec<&str> = text.lines().collect();
let mut buf: Vec<&str> = Vec::with_capacity(height);
for line in lines.iter().take(height) {
buf.push(line);
}
while buf.len() < height {
buf.push("");
}
buf.join("\r\n")
}
fn build_fixed_pages(
entries: &[(&str, &str, String)],
max_lines: usize,
) -> Vec<(String, String, Vec<usize>)> {
let mut pages: Vec<(String, String, Vec<usize>)> = Vec::new();
let mut i = 0;
while i < entries.len() {
let cat = entries[i].0.to_string();
let mut end = (i + 2).min(entries.len());
if end < entries.len() && entries[end].0 == cat {
let lines: usize = (i..=end).map(|j| entries[j].2.lines().count() + 1).sum();
if lines <= max_lines {
end += 1;
}
}
if let Some(pos) = entries[i + 1..end].iter().position(|e| e.0 != cat) {
end = i + 1 + pos;
}
let mut buf = String::new();
let mut indices = Vec::new();
for (j, entry) in entries.iter().enumerate().take(end).skip(i) {
if j > i {
buf.push('\n');
}
buf.push_str(&entry.2);
indices.push(j);
}
pages.push((cat, buf, indices));
i = end;
}
pages
}
fn find_page_for_entry(pages: &[(String, String, Vec<usize>)], entry_idx: usize) -> usize {
for (pi, (_cat, _content, indices)) in pages.iter().enumerate() {
if indices.contains(&entry_idx) {
return pi;
}
}
0
}
static SIGWINCH_RECEIVED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
#[cfg(unix)]
extern "C" fn sigwinch_handler(_sig: libc::c_int) {
SIGWINCH_RECEIVED.store(true, std::sync::atomic::Ordering::Relaxed);
}
#[allow(non_snake_case)]
#[derive(Clone, Copy)]
struct DocTheme<'a> {
C: &'a str,
G: &'a str,
Y: &'a str,
M: &'a str,
B: &'a str,
D: &'a str,
N: &'a str,
}
#[cfg(unix)]
fn doc_interactive_loop(
pages: &[(String, String, Vec<usize>)],
entries: &[(&str, &str, String)],
intro_raw: &str,
start: usize,
total: usize,
theme: DocTheme,
) -> i32 {
let DocTheme {
C, G, M, B, D, N, ..
} = theme;
use std::os::unix::io::AsRawFd;
let stdin_fd = io::stdin().as_raw_fd();
let mut old_termios: libc::termios = unsafe { std::mem::zeroed() };
unsafe { libc::tcgetattr(stdin_fd, &mut old_termios) };
let mut raw = old_termios;
unsafe { libc::cfmakeraw(&mut raw) };
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
let old_sigwinch = unsafe {
libc::signal(
libc::SIGWINCH,
sigwinch_handler as *const () as libc::sighandler_t,
)
};
let mut pages = pages.to_vec();
let mut total = total;
let mut current: usize = start;
macro_rules! rprint {
() => { print!("\r\n"); };
($($arg:tt)*) => { print!("{}\r\n", format!($($arg)*)); };
}
let render = |cur: usize, pages: &[(String, String, Vec<usize>)], total: usize| {
let (ref cat, ref content, ref indices) = pages[cur];
let topic_list: String = indices
.iter()
.take(3)
.map(|&i| entries[i].1)
.collect::<Vec<_>>()
.join(", ");
let topic_display = if indices.len() > 3 {
format!("{} +{}", topic_list, indices.len() - 3)
} else {
topic_list
};
let term_h = term_height();
print!("\x1b[H\x1b[2J");
print!("\x1b[1;1H"); rprint!();
rprint!(" {C}███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗{N}");
rprint!(" {C}██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝{N}");
rprint!(" {M}███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗ {N}");
rprint!(" {M}╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝ {N}");
rprint!(" {C}███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗{N}");
rprint!(" {C}╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝{N}");
print!(" {D}┌");
for _ in 0..74 {
print!("─");
}
print!("┐{N}\r\n");
let status = format!(
" {G}{:>3}/{}{N} {D}//{N} {C}{}{N} {D}//{N} {M}{}{N}",
cur + 1,
total,
topic_display,
cat,
);
let vis_len = strip_ansi_len(&status);
let pad = 74_usize.saturating_sub(vis_len);
print!(" {D}│{N}{status}{:>pad$}{D}│{N}\r\n", "", pad = pad);
print!(" {D}└");
for _ in 0..74 {
print!("─");
}
print!("┘{N}\r\n");
let content_start = 12;
let footer_rows = 3; let max_content = if term_h > content_start + footer_rows {
term_h - content_start - footer_rows
} else {
1
};
print!("\x1b[{};1H", content_start);
for (li, line) in content.lines().enumerate() {
if li >= max_content {
break; }
print!("{line}\r\n");
}
print!("\x1b[{};1H", term_h - 2);
print!(" {D}");
for _ in 0..76 {
print!("─");
}
print!("{N}\r\n");
print!(" {C}j{N}/{C}n{N} next {C}k{N}/{C}p{N} prev {C}d{N}/{C}u{N} ±5 {C}]{N}/{C}[{N} chapter {C}t{N} toc {C}/{N} search {C}:{N}num {C}r{N} rand {C}?{N} help {C}q{N} quit\r\n");
print!(" {D}>>>{N} ");
let _ = io::stdout().flush();
};
render(current, &pages, total);
loop {
let mut buf = [0u8; 1];
let nread = unsafe { libc::read(stdin_fd, buf.as_mut_ptr() as *mut libc::c_void, 1) };
if nread != 1 {
if SIGWINCH_RECEIVED.swap(false, std::sync::atomic::Ordering::Relaxed) {
let entry_idx = pages[current].2.first().copied().unwrap_or(0);
let th = term_height();
let content_area = th.saturating_sub(14).max(4);
let mut rebuilt = build_fixed_pages(entries, content_area);
let intro_page = pad_to_height(intro_raw, content_area);
rebuilt.insert(0, ("Introduction".to_string(), intro_page, Vec::new()));
pages = rebuilt;
total = pages.len();
current = if entry_idx == 0 && current == 0 {
0
} else {
find_page_for_entry(&pages, entry_idx).min(total - 1)
};
render(current, &pages, total);
continue;
}
break;
}
let key = buf[0];
match key {
b'j' | b'n' | b'l' | b' ' | b'\n' | b'\r' if current < total - 1 => {
current += 1;
}
b'k' | b'p' | b'h' => {
current = current.saturating_sub(1);
}
b'g' | b'0' => current = 0,
b'G' | b'$' => current = total - 1,
b'd' => {
current = (current + 5).min(total - 1);
}
b'u' => {
current = current.saturating_sub(5);
}
b']' | b'}' => {
let cur_cat = &pages[current].0;
while current < total - 1 {
current += 1;
if pages[current].0 != *cur_cat {
break;
}
}
}
b'[' | b'{' => {
let cur_cat = pages[current].0.clone();
while current > 0 {
current -= 1;
if pages[current].0 != cur_cat {
break;
}
}
}
b'r' => {
current = rand::thread_rng().gen_range(0..total);
}
b't' => {
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
print!("\x1b[H\x1b[2J");
doc_print_toc_entries(entries, &pages, theme);
print!(" {D}enter page number or press enter to return >>>{N} ");
let _ = io::stdout().flush();
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
if let Ok(n) = line.trim().parse::<usize>() {
if n >= 1 && n <= total {
current = n - 1;
}
}
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
}
b'/' => {
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
print!("\r {C}/{N}");
let _ = io::stdout().flush();
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
let pat = line.trim().to_lowercase();
if !pat.is_empty() {
let start_from = (current + 1) % total;
let mut found = false;
for i in 0..total {
let idx = (start_from + i) % total;
let (ref cat, ref content, _) = pages[idx];
if cat.to_lowercase().contains(&pat)
|| content.to_lowercase().contains(&pat)
{
current = idx;
found = true;
break;
}
}
let _ = found; }
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
}
b':' => {
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
print!("\r {C}:{N}");
let _ = io::stdout().flush();
let mut line = String::new();
let _ = io::stdin().read_line(&mut line);
if let Ok(n) = line.trim().parse::<usize>() {
if n >= 1 && n <= total {
current = n - 1;
}
}
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
}
b'?' => {
print!("\x1b[H\x1b[2J");
rprint!();
rprint!(" {D}── KEYBINDINGS ────────────────────────────────────────────────────{N}");
rprint!();
rprint!(" {B}Navigation{N}");
rprint!(" {C}j n l space enter{N} {D}next page{N}");
rprint!(" {C}k p h{N} {D}previous page{N}");
rprint!(" {C}d{N} {D}forward 5 pages{N}");
rprint!(" {C}u{N} {D}back 5 pages{N}");
rprint!(" {C}g 0{N} {D}first page{N}");
rprint!(" {C}G ${N} {D}last page{N}");
rprint!(" {C}] }}{N} {D}next chapter{N}");
rprint!(" {C}[ {{{N} {D}previous chapter{N}");
rprint!();
rprint!(" {B}Search & Jump{N}");
rprint!(" {C}/{N} {D}search pages{N}");
rprint!(" {C}:{N} {D}go to page number{N}");
rprint!(" {C}t{N} {D}table of contents{N}");
rprint!(" {C}r{N} {D}random page{N}");
rprint!();
rprint!(" {B}Other{N}");
rprint!(" {C}?{N} {D}this help{N}");
rprint!(" {C}q Q{N} {D}quit{N}");
rprint!();
rprint!(" {D}press any key to return{N}");
let _ = io::stdout().flush();
let mut b2 = [0u8; 1];
let _ = unsafe { libc::read(stdin_fd, b2.as_mut_ptr() as *mut _, 1) };
}
b'q' | b'Q' | 0x03 => {
break;
}
_ => {}
}
render(current, &pages, total);
}
unsafe { libc::signal(libc::SIGWINCH, old_sigwinch) };
unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
print!("\x1b[H\x1b[2J");
let _ = io::stdout().flush();
0
}
#[cfg(not(unix))]
fn doc_interactive_loop(
pages: &[(String, String, Vec<usize>)],
_entries: &[(&str, &str, String)],
_intro_raw: &str,
start: usize,
_total: usize,
_theme: DocTheme,
) -> i32 {
print!("{}", pages[start].1);
0
}
fn term_height() -> usize {
#[cfg(unix)]
{
let mut ws = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
if unsafe { libc::ioctl(2, libc::TIOCGWINSZ, &mut ws) } == 0 && ws.ws_row > 0 {
return ws.ws_row as usize;
}
}
24
}
fn term_width() -> usize {
#[cfg(unix)]
{
let mut ws = libc::winsize {
ws_row: 0,
ws_col: 0,
ws_xpixel: 0,
ws_ypixel: 0,
};
if unsafe { libc::ioctl(2, libc::TIOCGWINSZ, &mut ws) } == 0 && ws.ws_col > 0 {
return ws.ws_col as usize;
}
}
80
}
fn strip_ansi_len(s: &str) -> usize {
let mut len = 0;
let mut in_esc = false;
for c in s.chars() {
if c == '\x1b' {
in_esc = true;
} else if in_esc {
if c == 'm' {
in_esc = false;
}
} else {
len += 1;
}
}
len
}
fn doc_print_banner(theme: DocTheme) {
let DocTheme { C, M, N, .. } = theme;
println!(" {C}███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗{N}");
println!(" {C}██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝{N}");
println!(" {M}███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗ {N}");
println!(" {M}╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝ {N}");
println!(" {C}███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗{N}");
println!(" {C}╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝{N}");
}
fn doc_print_hline(left: char, right: char, theme: DocTheme) {
let DocTheme { D, N, .. } = theme;
print!(" {D}{left}");
for _ in 0..74 {
print!("─");
}
println!("{right}{N}");
}
fn doc_print_boxline(content: &str, theme: DocTheme) {
let DocTheme { D, N, .. } = theme;
let stripped = content
.bytes()
.fold((Vec::new(), false), |(mut acc, in_esc), b| {
if b == 0x1b {
(acc, true)
} else if in_esc {
(acc, b != b'm')
} else {
acc.push(b);
(acc, false)
}
})
.0;
let visible = String::from_utf8_lossy(&stripped).chars().count();
let inner: usize = 74;
let pad = inner.saturating_sub(visible);
println!(" {D}│{N}{content}{:>pad$}{D}│{N}", "", pad = pad);
}
fn doc_print_separator(label: &str, theme: DocTheme) {
let DocTheme { D, N, .. } = theme;
let trail = 72usize.saturating_sub(label.len());
print!(" {D}── {label} ");
for _ in 0..trail {
print!("─");
}
println!("{N}");
}
fn doc_print_toc_entries(
entries: &[(&str, &str, String)],
pages: &[(String, String, Vec<usize>)],
theme: DocTheme,
) {
let DocTheme {
C, G, M, B, D, N, ..
} = theme;
let topic_count = entries.len();
let page_count = pages.len();
println!();
doc_print_banner(theme);
doc_print_hline('┌', '┐', theme);
doc_print_boxline(
&format!(
" {G}TABLE OF CONTENTS{N} {D}//{N} {C}{topic_count} topics, {page_count} pages{N} {D}//{N} {M}The stryke Encyclopedia{N}"
),
theme,
);
doc_print_hline('└', '┘', theme);
println!();
let mut last_cat = "";
for (entry_idx, (cat, topic, _)) in entries.iter().enumerate() {
if *cat != last_cat {
println!();
println!(" {B}{cat}{N}");
last_cat = cat;
}
let page_num = pages
.iter()
.position(|(_, _, indices)| indices.contains(&entry_idx))
.map(|p| p + 1)
.unwrap_or(0);
println!(
" {C}{:>3}.{N} {:<30} {D}p.{}{N}",
entry_idx + 1,
topic,
page_num
);
}
println!();
}
fn word_wrap(text: &str, max_vis: usize) -> Vec<String> {
if max_vis == 0 {
return vec![text.to_string()];
}
let mut lines: Vec<String> = Vec::new();
let mut cur = String::new();
let mut vis = 0usize;
for word in text.split(' ') {
let wvis = strip_ansi_len(word);
if vis > 0 && vis + 1 + wvis > max_vis {
lines.push(cur);
cur = word.to_string();
vis = wvis;
} else {
if vis > 0 {
cur.push(' ');
vis += 1;
}
cur.push_str(word);
vis += wvis;
}
}
if !cur.is_empty() || lines.is_empty() {
lines.push(cur);
}
lines
}
#[allow(non_snake_case)]
fn render_page_content(topic: &str, text: &str, C: &str, G: &str, D: &str, N: &str) -> String {
let max_vis = term_width().saturating_sub(4).max(40); let mut out = String::with_capacity(text.len() + 512);
out.push_str(&format!(" {C}{topic}{N}\n"));
out.push_str(&format!(
" {D}{}{N}\n",
"─".repeat(topic.len().max(20).min(max_vis))
));
let mut in_code = false;
for line in text.split('\n') {
if line.starts_with("```") {
in_code = !in_code;
continue;
}
if in_code {
out.push_str(&format!(" {G} {line}{N}\n"));
} else if line.trim().is_empty() {
out.push('\n');
} else {
let rendered = render_inline_code(line, C, N);
for wrapped in word_wrap(&rendered, max_vis) {
out.push_str(&format!(" {wrapped}\n"));
}
}
}
out
}
fn render_inline_code(line: &str, color: &str, reset: &str) -> String {
let mut out = String::with_capacity(line.len() + 64);
let mut in_tick = false;
for ch in line.chars() {
if ch == '`' {
if in_tick {
out.push_str(reset);
} else {
out.push_str(color);
}
in_tick = !in_tick;
} else {
out.push(ch);
}
}
out
}
fn run_deconvert_subcommand(args: &[String]) -> i32 {
let mut files: Vec<String> = Vec::new();
let mut in_place = false;
let mut output_delim: Option<char> = None;
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"-i" | "--in-place" => in_place = true,
"-d" | "--output-delim" => {
i += 1;
if i >= args.len() {
eprintln!("stryke deconvert: --output-delim requires an argument");
return 2;
}
let delim_str = &args[i];
if delim_str.chars().count() != 1 {
eprintln!(
"stryke deconvert: --output-delim must be a single character, got {:?}",
delim_str
);
return 2;
}
output_delim = delim_str.chars().next();
}
"-h" | "--help" => {
println!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
println!();
println!("Convert stryke .stk files back to standard Perl .pl syntax:");
println!(" - Pipe chains and thread macros → nested function calls");
println!(" - fn → sub");
println!(" - p → say");
println!(" - Adds trailing semicolons");
println!(" - #!/usr/bin/env perl shebang prepended");
println!();
println!("Options:");
println!(" -i, --in-place Write .pl files alongside originals");
println!(" -d, --output-delim Delimiter for s///, tr///, m// (default: preserve original)");
println!();
println!("Examples:");
println!(" stryke deconvert app.stk # print to stdout");
println!(" stryke deconvert -i lib/*.stk # write lib/*.pl");
println!(
" stryke deconvert -d '|' app.stk # use | as delimiter: s|old|new|g"
);
return 0;
}
s if s.starts_with('-') => {
eprintln!("stryke deconvert: unknown option: {}", s);
eprintln!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
return 2;
}
s => files.push(s.to_string()),
}
i += 1;
}
if files.is_empty() {
eprintln!("stryke deconvert: no input files");
eprintln!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
return 2;
}
let opts = stryke::deconvert::DeconvertOptions { output_delim };
let mut errors = 0;
for f in &files {
let code = match std::fs::read_to_string(f) {
Ok(c) => c,
Err(e) => {
eprintln!("stryke deconvert: {}: {}", f, e);
errors += 1;
continue;
}
};
let program = match stryke::parse_with_file(&code, f) {
Ok(p) => p,
Err(e) => {
eprintln!("stryke deconvert: {}: {}", f, e);
errors += 1;
continue;
}
};
let deconverted = stryke::deconvert_to_perl_with_options(&program, &opts);
if in_place {
let out_path = std::path::Path::new(f).with_extension("pl");
if let Err(e) = std::fs::write(&out_path, &deconverted) {
eprintln!("stryke deconvert: {}: {}", out_path.display(), e);
errors += 1;
}
} else {
println!("{}", deconverted);
}
}
if errors > 0 {
1
} else {
0
}
}
fn strip_shebang_and_extract(content: &str, extract: bool) -> String {
if extract {
let mut found = false;
let mut lines = Vec::new();
for line in content.lines() {
if !found {
if line.starts_with("#!") && line.contains("perl") {
found = true;
}
continue;
}
if line == "__END__" || line == "__DATA__" {
break;
}
lines.push(line);
}
lines.join("\n")
} else if content.starts_with("#!") {
if let Some(pos) = content.find('\n') {
content[pos + 1..].to_string()
} else {
String::new()
}
} else {
content.to_string()
}
}
fn looks_like_code(s: &str) -> bool {
s.contains(' ')
|| s.contains(';')
|| s.contains('|')
|| s.contains('{')
|| s.contains('(')
|| s.contains('$')
|| s.contains('@')
|| s.contains('>')
}
fn find_in_path(script: &str) -> Option<String> {
if std::path::Path::new(script).is_absolute() || script.contains('/') {
return Some(script.to_string());
}
if let Ok(path_var) = std::env::var("PATH") {
for dir in path_var.split(':') {
let full = format!("{}/{}", dir, script);
if std::path::Path::new(&full).exists() {
return Some(full);
}
}
}
None
}
fn print_config(configvar: Option<&str>) {
let version = env!("CARGO_PKG_VERSION");
let arch = std::env::consts::ARCH;
let os = std::env::consts::OS;
let threads = std::thread::available_parallelism()
.map(|n| n.get())
.unwrap_or(1);
if let Some(var) = configvar {
let val = match var {
"version" | "api_version" => version.to_string(),
"archname" => format!("{}-{}", arch, os),
"osname" => os.to_string(),
"threads" => threads.to_string(),
"useithreads" | "usethreads" => "define".to_string(),
"use64bitint" | "use64bitall" => "define".to_string(),
"cc" => "rustc".to_string(),
"optimize" => "-O3 -lto".to_string(),
"prefix" | "installprefix" => "/usr/local".to_string(),
"perlpath" => "stryke".to_string(),
_ => {
eprintln!("Unknown config variable: {}", var);
return;
}
};
println!("{}='{}'", var, val);
} else {
println!("Summary of stryke v{} configuration:\n", version);
println!(" Platform:");
println!(" osname={}, archname={}-{}", os, arch, os);
println!(" Compiler:");
println!(" cc=rustc, optimize=-O3 -lto");
println!(" Threading:");
println!(" useithreads=define, threads={}", threads);
println!(" Integer/Float:");
println!(" use64bitint=define, use64bitall=define");
println!(" Parallel extensions:");
println!(" rayon=define, pmap=define, pmap_chunked=define, pipeline=define, par_pipeline=define, async=define, await=define, pgrep=define, pfor=define, psort=define, reduce=define, preduce=define, preduce_init=define, jit=define");
println!(" Install:");
println!(" perlpath=stryke");
}
}
#[cfg(test)]
mod cli_argv_tests {
use super::{expand_perl_bundled_argv, normalize_argv_after_dash_e, parse_cli_prelude, Cli};
use clap::Parser;
fn args(v: &[&str]) -> Vec<String> {
v.iter().map(|s| (*s).to_string()).collect()
}
#[test]
fn prelude_inserts_double_dash_before_script_argv_long_flags() {
let a = args(&["stryke", "s.pl", "--regex", "--foo"]);
let cli = parse_cli_prelude(&a).expect("expected prelude parse");
assert_eq!(cli.script.as_deref(), Some("s.pl"));
assert_eq!(cli.args, vec!["--regex".to_string(), "--foo".to_string()]);
}
#[test]
fn prelude_with_dash_w_before_script() {
let a = args(&["stryke", "-w", "s.pl", "--regex"]);
let cli = parse_cli_prelude(&a).expect("expected prelude parse");
assert!(cli.warnings);
assert_eq!(cli.script.as_deref(), Some("s.pl"));
assert_eq!(cli.args, vec!["--regex".to_string()]);
}
#[test]
fn prelude_dash_e_then_argv_with_long_flag() {
let a = args(&["stryke", "-e", "1", "foo", "--regex"]);
let mut cli = parse_cli_prelude(&a).expect("expected prelude parse");
normalize_argv_after_dash_e(&mut cli);
assert_eq!(cli.execute, vec!["1"]);
assert!(cli.script.is_none());
assert_eq!(cli.args, vec!["foo".to_string(), "--regex".to_string()]);
}
#[test]
fn explicit_user_double_dash_skips_prelude() {
let a = args(&["stryke", "--", "s.pl", "x"]);
assert!(parse_cli_prelude(&a).is_none());
}
#[test]
fn bundled_lane_le_lne_maps_to_split_switches() {
for (flag, code, expect_a, expect_n) in [
("-lane", "print 1", true, true),
("-le", "print 2", false, false),
("-lne", "print 3", false, true),
("-lnE", "say 4", false, true),
] {
let a = expand_perl_bundled_argv(args(&["stryke", flag, code]));
let cli = Cli::try_parse_from(&a).expect("parse bundled flags");
assert!(
cli.line_ending.is_some(),
"{flag}: expected -l (line ending)"
);
assert_eq!(cli.auto_split, expect_a, "{flag}: autosplit (-a)");
assert_eq!(cli.line_mode, expect_n, "{flag}: line loop (-n)");
if flag.contains('E') {
assert_eq!(cli.execute_features, vec![code]);
assert!(cli.execute.is_empty());
} else {
assert_eq!(cli.execute, vec![code]);
assert!(cli.execute_features.is_empty());
}
}
}
#[test]
fn bundled_lpe_preserves_print_mode() {
let a = expand_perl_bundled_argv(args(&["stryke", "-lpe", "print 1"]));
let cli = Cli::try_parse_from(&a).expect("parse");
assert!(cli.print_mode);
assert_eq!(cli.execute, vec!["print 1"]);
}
#[test]
fn bundled_0777_not_split() {
let a = expand_perl_bundled_argv(args(&["stryke", "-0777", "-e", "1"]));
assert!(
a.contains(&"-0777".to_string()),
"expected -0777 kept intact: {a:?}"
);
}
#[test]
fn bundled_0ne_splits_like_perl() {
let a = expand_perl_bundled_argv(args(&["stryke", "-0ne", "print 1"]));
let cli = Cli::try_parse_from(&a).expect("parse");
assert_eq!(cli.execute, vec!["print 1"]);
assert!(cli.line_mode);
}
#[test]
fn bundled_f_colon_takes_rest_of_token() {
let a = expand_perl_bundled_argv(args(&["stryke", "-F:", "-anE", "say $F[0]"]));
let cli = Cli::try_parse_from(&a).expect("parse");
assert_eq!(cli.field_separator.as_deref(), Some(":"));
assert!(cli.auto_split);
assert!(cli.line_mode);
assert_eq!(cli.execute_features, vec!["say $F[0]"]);
}
#[test]
fn bundled_f_comma_takes_rest_of_token() {
let a = expand_perl_bundled_argv(args(&["stryke", "-F,", "-anE", "print 1"]));
let cli = Cli::try_parse_from(&a).expect("parse");
assert_eq!(cli.field_separator.as_deref(), Some(","));
}
#[test]
fn help_alias_not_bundled_as_h_e_l_p() {
let a = expand_perl_bundled_argv(args(&["stryke", "-help"]));
let cli = Cli::try_parse_from(&a).expect("parse");
assert!(cli.help);
}
#[test]
fn thread_operator_not_bundled() {
let a = expand_perl_bundled_argv(args(&["stryke", "->> 1 p"]));
assert_eq!(a, args(&["stryke", "->> 1 p"]));
let b = expand_perl_bundled_argv(args(&["stryke", "~> 1 p"]));
assert_eq!(b, args(&["stryke", "~> 1 p"]));
}
}