1use std::fs::File;
2use std::io::{self, BufReader, IsTerminal, Read as IoRead, Write};
3use std::path::{Path, PathBuf};
4use std::process;
5use std::sync::Mutex;
6
7use clap::Parser;
8use rand::Rng;
9use rayon::prelude::*;
10
11use crate::ast::Program;
12use crate::error::{ErrorKind, StrykeError};
13use crate::perl_fs::{
14 decode_utf8_or_latin1, read_file_text_perl_compat, read_line_perl_compat_with_sep,
15 read_logical_line_perl_compat_with_sep,
16};
17use crate::vm_helper::VMHelper;
18
19use crate::repl;
20
21#[derive(Parser, Debug, Default)]
23#[command(name = "stryke", version, about, long_about = None)]
24#[command(disable_version_flag = true, disable_help_flag = true)]
25#[command(override_usage = "stryke [switches] [--] [programfile] [arguments]")]
26pub struct Cli {
27 #[arg(short = '0', value_name = "OCTAL")]
29 input_separator: Option<Option<String>>,
30
31 #[arg(short = 'a')]
33 auto_split: bool,
34
35 #[arg(short = 'C', value_name = "NUMBER/LIST")]
37 unicode_features: Option<Option<String>>,
38
39 #[arg(short = 'c')]
41 check_only: bool,
42
43 #[arg(long = "lint", alias = "check")]
45 lint: bool,
46
47 #[arg(long = "disasm", alias = "disassemble")]
49 disasm: bool,
50
51 #[arg(long = "ast")]
53 dump_ast: bool,
54
55 #[arg(long = "dump-tokens")]
57 dump_tokens: bool,
58
59 #[arg(long = "dump-bytecode")]
61 dump_bytecode: bool,
62
63 #[arg(long = "tiers")]
65 tiers: bool,
66
67 #[arg(long = "fmt")]
69 format_source: bool,
70
71 #[arg(long = "from-zsh")]
73 from_zsh: bool,
74
75 #[arg(long = "profile")]
77 profile: bool,
78
79 #[arg(long = "flame")]
81 flame: bool,
82
83 #[arg(long = "no-jit")]
85 no_jit: bool,
86
87 #[arg(long = "explain", value_name = "CODE")]
89 explain: Option<String>,
90
91 #[arg(short = 'd', value_name = "MOD")]
93 debugger: Option<Option<String>>,
94
95 #[arg(short = 'D', value_name = "FLAGS")]
97 debug_flags: Option<Option<String>>,
98
99 #[arg(short = 'e')]
101 execute: Vec<String>,
102
103 #[arg(short = 'E')]
105 execute_features: Vec<String>,
106
107 #[arg(short = 'f')]
109 no_sitecustomize: bool,
110
111 #[arg(short = 'F', value_name = "PATTERN")]
113 field_separator: Option<String>,
114
115 #[arg(short = 'g')]
117 slurp: bool,
118
119 #[arg(short = 'i', value_name = "EXTENSION")]
121 inplace: Option<Option<String>>,
122
123 #[arg(short = 'I', value_name = "DIRECTORY")]
125 include: Vec<String>,
126
127 #[arg(short = 'l', value_name = "OCTNUM")]
129 line_ending: Option<Option<String>>,
130
131 #[arg(short = 'M', value_name = "MODULE")]
133 use_module: Vec<String>,
134
135 #[arg(short = 'm', value_name = "MODULE")]
137 use_module_no_import: Vec<String>,
138
139 #[arg(short = 'n')]
141 line_mode: bool,
142
143 #[arg(short = 'p')]
145 print_mode: bool,
146
147 #[arg(short = 's')]
149 switch_parsing: bool,
150
151 #[arg(short = 'S')]
153 path_lookup: bool,
154
155 #[arg(short = 't')]
157 taint_warn: bool,
158
159 #[arg(short = 'T')]
161 taint_check: bool,
162
163 #[arg(short = 'u')]
165 dump_core: bool,
166
167 #[arg(short = 'U')]
169 unsafe_ops: bool,
170
171 #[arg(short = 'v')]
173 show_version: bool,
174
175 #[arg(short = 'V', value_name = "CONFIGVAR")]
177 show_config: Option<Option<String>>,
178
179 #[arg(short = 'w')]
181 warnings: bool,
182
183 #[arg(short = 'W')]
185 all_warnings: bool,
186
187 #[arg(short = 'x', value_name = "DIRECTORY")]
189 extract: Option<Option<String>>,
190
191 #[arg(short = 'X')]
193 no_warnings: bool,
194
195 #[arg(short = 'h', long = "help")]
197 help: bool,
198
199 #[arg(short = 'j', long = "threads", value_name = "N")]
201 threads: Option<usize>,
202
203 #[arg(long = "compat")]
205 compat: bool,
206
207 #[arg(long = "static")]
210 static_typing: bool,
211
212 #[arg(long = "no-interop")]
214 no_interop: bool,
215
216 #[arg(long = "script")]
218 force_script: bool,
219
220 #[arg(value_name = "SCRIPT")]
222 script: Option<String>,
223
224 #[arg(value_name = "ARGS", trailing_var_arg = true)]
226 args: Vec<String>,
227}
228
229fn expand_perl_bundled_argv(args: Vec<String>) -> Vec<String> {
232 if args.is_empty() {
233 return args;
234 }
235 let mut out = vec![args[0].clone()];
236 let mut seen_dd = false;
237 for arg in args.into_iter().skip(1) {
238 if seen_dd {
239 out.push(arg);
240 continue;
241 }
242 if arg == "--" {
243 seen_dd = true;
244 out.push(arg);
245 continue;
246 }
247 match expand_perl_bundled_token(&arg) {
248 Some(parts) => out.extend(parts),
249 None => out.push(arg),
250 }
251 }
252 out
253}
254
255fn expand_perl_bundled_token(arg: &str) -> Option<Vec<String>> {
257 match arg {
258 "-help" | "--help" => return Some(vec!["-h".to_string()]),
259 "-version" | "--version" => return Some(vec!["-v".to_string()]),
260 _ => {}
261 }
262 if arg == "-" || !arg.starts_with('-') || arg.starts_with("--") {
263 return None;
264 }
265 let s = arg.strip_prefix('-')?;
266 if s.is_empty() || s.len() == 1 {
267 return None;
268 }
269 if s.starts_with('>') || s.starts_with('~') {
271 return None;
272 }
273 if let Some(rest) = s.strip_prefix('0') {
275 let rest_ok = rest.chars().all(|c| matches!(c, '0'..='7'));
276 if rest_ok {
277 return None;
278 }
279 }
280 let mut out = Vec::new();
281 let b = s.as_bytes();
282 let mut i = 0usize;
283 while i < b.len() {
284 match b[i] {
285 b'0' if i == 0 => {
286 let mut j = i + 1;
287 while j < b.len() && matches!(b[j], b'0'..=b'7') {
288 j += 1;
289 }
290 out.push("-0".to_string());
291 if j > i + 1 {
292 out.push(s[i + 1..j].to_string());
293 }
294 i = j;
295 }
296 b'e' | b'E' => {
297 let flag = if b[i] == b'e' { "-e" } else { "-E" };
298 out.push(flag.to_string());
299 if i + 1 < b.len() {
300 out.push(s[i + 1..].to_string());
301 }
302 return Some(out);
303 }
304 b'l' => {
305 out.push("-l".to_string());
306 i += 1;
307 let start = i;
308 while i < b.len() && matches!(b[i], b'0'..=b'7') {
309 i += 1;
310 }
311 if i > start {
312 out.push(s[start..i].to_string());
313 }
314 }
315 b'F' | b'M' | b'm' | b'I' | b'd' | b'D' | b'x' | b'C' => {
326 let ch = b[i] as char;
327 out.push(format!("-{ch}"));
328 i += 1;
329 if i < b.len() {
330 out.push(s[i..].to_string());
331 }
332 return Some(out);
333 }
334 b'V' => {
335 out.push("-V".to_string());
337 i += 1;
338 if i < b.len() {
339 let rest = &s[i..];
341 let rest = rest.strip_prefix(':').unwrap_or(rest);
342 out.push(rest.to_string());
343 }
344 return Some(out);
345 }
346 b'i' => {
347 out.push("-i".to_string());
348 i += 1;
349 if i < b.len() && matches!(b[i], b'e' | b'E') {
350 continue;
351 }
352 if i < b.len() && b[i] == b'.' {
353 let start = i;
354 while i < b.len() && !matches!(b[i], b'e' | b'E') {
355 i += 1;
356 }
357 out.push(s[start..i].to_string());
358 }
359 }
360 _ => {
361 out.push(format!("-{}", b[i] as char));
362 i += 1;
363 }
364 }
365 }
366 Some(out)
367}
368
369fn print_cyberpunk_help() {
370 let version = env!("CARGO_PKG_VERSION");
371 let bin = invoked_as();
375 let bin = bin.as_str();
376
377 const C: &str = "\x1b[36m"; const M: &str = "\x1b[35m"; const Y: &str = "\x1b[33m"; const G: &str = "\x1b[32m"; const N: &str = "\x1b[0m"; repl::print_cyberpunk_banner();
385 println!();
386 println!();
387 println!("A highly parallel Perl 5 interpreter written in Rust");
388 println!();
389 println!("{Y} USAGE:{N} {bin} 'CODE' {G}//{N} -e is optional");
390 println!("{Y} {N} {bin} [switches] [--] [programfile] [arguments]");
391 println!();
392 println!("{C} ── EXECUTION ──────────────────────────────────────────{N}");
393 println!(" 'CODE' {G}//{N} Inline code — no -e needed if arg looks like code");
394 println!(" -e CODE {G}//{N} Explicit inline (required with -n/-p/-l/-a)");
395 println!(" -E CODE {G}//{N} Like -e, but enables all optional features");
396 println!(" BUILTIN [ARGS] {G}//{N} Call builtin fn directly: stryke pin, stryke basename /a/b");
397 println!(
398 " --script {G}//{N} Force script lookup when name conflicts with builtin"
399 );
400 println!(" -c {G}//{N} Check syntax only (parse; no compile/run)");
401 println!(" --lint / --check {G}//{N} Parse + compile bytecode without running");
402 println!(
403 " --disasm / --disassemble {G}//{N} Print bytecode disassembly to stderr before VM run"
404 );
405 println!(" --ast {G}//{N} Dump parsed AST as JSON and exit (no execution)");
406 println!(
407 " --dump-tokens {G}//{N} Print the lexer token stream and exit (no execution)"
408 );
409 println!(
410 " --dump-bytecode {G}//{N} Print the compiled fusevm bytecode ops and exit (no execution)"
411 );
412 println!(" --fmt {G}//{N} Pretty-print parsed Perl to stdout and exit");
413 println!(
414 " --from-zsh {G}//{N} Transpile a zsh script to stryke on stdout and exit"
415 );
416 println!(
417 " --explain CODE {G}//{N} Print expanded hint for an error code (e.g. E0001) and exit"
418 );
419 println!(
420 " --profile {G}//{N} Wall-clock profile stderr (VM op lines; flamegraph-ready)"
421 );
422 println!(
423 " --flame {G}//{N} Flamegraph: terminal bars (TTY) or SVG (piped to file)"
424 );
425 println!(" --no-jit {G}//{N} Disable Cranelift JIT (bytecode interpreter only)");
426 println!(
427 " --compat {G}//{N} Perl 5 strict-compat: disable all stryke extensions"
428 );
429 println!(
430 " --static {G}//{N} Mandatory static typing: require types everywhere (Kotlin-style)"
431 );
432 println!(
433 " --no-interop {G}//{N} Reject Perl-isms (sub/say/reverse, $a/$b), force idiomatic stryke ($_0/$_1)"
434 );
435 println!(" -d[t][:MOD] {G}//{N} Run program under debugger or module Devel::MOD");
436 println!(" -D[number/letters] {G}//{N} Set debugging flags");
437 println!(" -u {G}//{N} Dump core after parsing program");
438 println!("{C} ── INPUT PROCESSING ─────────────────────────────────{N}");
439 println!(" -n {G}//{N} Assume \"while (<>) {{...}}\" loop around program");
440 println!(" -p {G}//{N} Like -n but print line also, like sed");
441 println!(" -a {G}//{N} Autosplit mode (splits $_ into @F)");
442 println!(" -F/pattern/ {G}//{N} split() pattern for -a switch");
443 println!(" -l[octnum] {G}//{N} Enable line ending processing");
444 println!(" -0[octal] {G}//{N} Specify record separator (\\0 if no arg)");
445 println!(" -g {G}//{N} Slurp all input at once (alias for -0777)");
446 println!(" -i[extension] {G}//{N} Edit <> files in place (backup if ext supplied; multiple files in parallel)");
447 println!("{C} ── MODULES & PATHS ──────────────────────────────────{N}");
448 println!(" -M MODULE {G}//{N} Execute \"use module...\" before program");
449 println!(
450 " -m MODULE {G}//{N} Execute \"use module ()\" before program (no import)"
451 );
452 println!(" -I DIRECTORY {G}//{N} Specify @INC directory (several allowed)");
453 println!(" -f {G}//{N} Don't do $sitelib/sitecustomize.pl at startup");
454 println!(" -S {G}//{N} Look for programfile using PATH");
455 println!(" -x[directory] {G}//{N} Ignore text before #!perl line");
456 println!("{C} ── UNICODE & SAFETY ─────────────────────────────────{N}");
457 println!(" -C[number/list] {G}//{N} Enable listed Unicode features");
458 println!(" -t {G}//{N} Enable tainting warnings");
459 println!(" -T {G}//{N} Enable tainting checks");
460 println!(" -U {G}//{N} Allow unsafe operations");
461 println!(" -s {G}//{N} Enable switch parsing for programfile args");
462 println!("{C} ── WARNINGS ─────────────────────────────────────────{N}");
463 println!(" -w {G}//{N} Enable many useful warnings");
464 println!(" -W {G}//{N} Enable all warnings");
465 println!(" -X {G}//{N} Disable all warnings");
466 println!("{C} ── INFO ─────────────────────────────────────────────{N}");
467 println!(" -v {G}//{N} Print version, patchlevel and license");
468 println!(" -V[:configvar] {G}//{N} Print configuration summary");
469 println!(" -h, --help {G}//{N} Print help");
470 println!("{C} ── TOOLCHAIN ─────────────────────────────────────────{N}");
471 println!(
472 " --lsp {G}//{N} Language Server (JSON-RPC on stdio); must be the only arg after {bin}"
473 );
474 println!(
475 " build SCRIPT [-o OUT] {G}//{N} AOT: copy this binary with SCRIPT embedded (standalone exe)"
476 );
477 println!(" docs [TOPIC] {G}//{N} Built-in docs (stryke docs pmap, stryke docs |>, stryke docs)");
478 println!(
479 " serve [PORT] [SCRIPT] {G}//{N} HTTP server (stryke serve, stryke serve 8080 app.stk)"
480 );
481 println!(" fmt [-i] FILE... {G}//{N} Format source files (stryke fmt -i .)");
482 println!(
483 " minify [-i] FILE... {G}//{N} Strip comments / POD / blank lines, collapse to one line w/ `;` (stryke minify app.stk)"
484 );
485 println!(
486 " bench [FILE|DIR] {G}//{N} Run benchmarks from bench/ or benches/ (stryke bench)"
487 );
488 println!(
489 " init [NAME] {G}//{N} Scaffold project in cwd (stryke.toml + lib/, t/, benches/)"
490 );
491 println!(
492 " new NAME {G}//{N} Scaffold a new project at ./NAME/ (same layout as init)"
493 );
494 println!(
495 " install [--offline] {G}//{N} Resolve manifest deps, populate stryke.lock + ~/.stryke/store"
496 );
497 println!(
498 " add NAME[@VER] [...] {G}//{N} Add a dep to stryke.toml; flags: --dev --group=N --path=DIR --features=A,B"
499 );
500 println!(" remove NAME {G}//{N} Drop a dep from stryke.toml; reruns install");
501 println!(" tree {G}//{N} Print resolved dep graph from stryke.lock");
502 println!(" info NAME {G}//{N} Show lockfile entry + store path for a dep");
503 println!(
504 " pkg <subcommand> {G}//{N} Dispatcher for the package commands above (init/new/install/add/remove/tree/info)"
505 );
506 println!(
507 " repl [--load FILE] {G}//{N} Interactive REPL with optional pre-load (stryke repl)"
508 );
509 println!(
510 " --remote-worker {G}//{N} Persistent cluster worker (stdio); only arg after {bin}"
511 );
512 println!(
513 " --remote-worker-v1 {G}//{N} Legacy one-shot worker (stdio); only arg after {bin}"
514 );
515 println!("{C} ── PARALLEL EXTENSIONS (stryke) ─────────────────────{N}");
516 println!(" -j N {G}//{N} Set number of parallel threads (rayon)");
517 println!(
518 " pmap {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel map; optional stderr progress bar"
519 );
520 println!(
521 " pmap_chunked N {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel map in batches of N items per thread"
522 );
523 println!(
524 " pcache {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel memoize (key = stringified topic)"
525 );
526 println!(
527 " par_lines PATH, CODE [, progress => EXPR] {G}//{N} mmap + parallel line scan (interpreter)"
528 );
529 println!(
530 " par_walk PATH, CODE [, progress => EXPR] {G}//{N} parallel recursive dir walk; topic is each path"
531 );
532 println!(
533 " par_sed PATTERN, REPLACEMENT, FILES... [, progress => EXPR] {G}//{N} parallel in-place regex replace per file (g)"
534 );
535 println!(
536 " 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*"
537 );
538 println!(
539 " par_pipeline @list same chain; filter/map parallel on collect (order kept); par_pipeline(source=>…,stages=>…,workers=>…) channel stages"
540 );
541 println!(
542 " async {{BLOCK}} {G}//{N} Run block on a worker thread; returns a task handle"
543 );
544 println!(" spawn {{BLOCK}} {G}//{N} Same as async (Rust-style); join with await");
545 println!(" await EXPR {G}//{N} Join async task or pass through non-task value");
546 println!(
547 " pgrep {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel grep across all cores"
548 );
549 println!(
550 " pfor {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel foreach across all cores"
551 );
552 println!(
553 " psort {{BLOCK}} @list [, progress => EXPR] {G}//{N} Parallel sort across all cores"
554 );
555 println!(
556 " @list |> reduce {{BLOCK}} {G}//{N} Sequential left fold ($a accum, $b next element); also reduce {{BLOCK}} @list"
557 );
558 println!(
559 " @list |> preduce {{BLOCK}} [, progress => EXPR] {G}//{N} Parallel tree fold (rayon; associative ops only); also preduce {{BLOCK}} @list"
560 );
561 println!(
562 " @list |> preduce_init EXPR, {{BLOCK}} [, progress => EXPR] {G}//{N} Parallel fold with identity; also preduce_init EXPR, {{BLOCK}} @list"
563 );
564 println!(
565 " @list |> pmap_reduce {{MAP}} {{REDUCE}} [, progress => EXPR] {G}//{N} Fused parallel map + tree reduce; also pmap_reduce {{MAP}} {{REDUCE}} @list"
566 );
567 println!(
568 " fan [N] {{BLOCK}} [, progress => EXPR] {G}//{N} Execute BLOCK N times (default N = rayon pool; $_ = index); progress may follow }} without a comma"
569 );
570 println!(
571 " fan_cap [N] {{BLOCK}} [, progress => EXPR] {G}//{N} Like fan; returns list of block return values (index order)"
572 );
573 println!("{C} ── TYPING (stryke) ───────────────────────────────────{N}");
574 println!(
575 " typed my \\$x : Int|Str|Float {G}//{N} Optional scalar types; runtime checks on assign"
576 );
577 println!(
578 " fn (\\$a: Int, \\$b: Str) {{}} {G}//{N} Typed sub params; runtime checks on call"
579 );
580 println!("{C} ── SERIALIZATION (stryke) ───────────────────────────────{N}");
581 println!(
582 " str \\$val / stringify \\$val {G}//{N} Convert any value to parseable stryke literal"
583 );
584 println!(" eval str \\$fn {G}//{N} Round-trip: serialize + deserialize coderefs");
585 println!("{C} ── POSITIONAL ─────────────────────────────────────────{N}");
586 println!(" [programfile] {G}//{N} Perl script to execute");
587 println!(" [arguments] {G}//{N} Arguments passed to script (@ARGV)");
588 println!();
589 println!();
590 println!("{C} ── SYSTEM ─────────────────────────────────────────{N}");
591 println!("{M} v{version} {N}// {Y}(c) MenkeTechnologies{N}");
592 println!("{M} There is more than one way to do it — in parallel.{N}");
593 println!("{Y} >>> PARSE. EXECUTE. PARALLELIZE. OWN YOUR CORES. <<<{N}");
594 println!("{C} ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░{N}");
595}
596
597pub(crate) fn module_prelude(cli: &Cli) -> String {
599 let mut full_code = String::new();
600 for module in &cli.use_module {
601 if let Some((mod_name, args)) = module.split_once('=') {
602 full_code.push_str(&format!(
603 "use {} qw({});\n",
604 mod_name,
605 args.replace(',', " ")
606 ));
607 } else {
608 full_code.push_str(&format!("use {};\n", module));
609 }
610 }
611 for module in &cli.use_module_no_import {
612 if let Some(rest) = module.strip_prefix('-') {
613 full_code.push_str(&format!("no {};\n", rest));
614 } else {
615 full_code.push_str(&format!("use {} ();\n", module));
616 }
617 }
618 full_code
619}
620
621fn parse_cli_prelude(args: &[String]) -> Option<Cli> {
626 if args.len() <= 1 {
627 return None;
628 }
629 if args[1..].iter().any(|s| s == "--") {
631 return None;
632 }
633 for k in (1..=args.len()).rev() {
634 let trial: Vec<String> = if k == args.len() {
635 args.to_vec()
636 } else {
637 let mut t = args[..k].to_vec();
638 t.push("--".to_string());
639 t.extend(args[k..].iter().cloned());
640 t
641 };
642 let Some(cli) = Cli::try_parse_from(&trial).ok() else {
643 continue;
644 };
645 if cli.args.as_slice() == args[k..].as_ref() {
646 return Some(cli);
647 }
648 }
649 None
650}
651
652fn normalize_argv_after_dash_e(cli: &mut Cli) {
655 if (!cli.execute.is_empty() || !cli.execute_features.is_empty()) && cli.script.is_some() {
656 let mut v = vec![cli.script.take().unwrap()];
657 v.append(&mut cli.args);
658 cli.args = v;
659 }
660}
661
662fn adjacent_temp_path(target: &Path) -> PathBuf {
664 let dir = target.parent().unwrap_or_else(|| Path::new("."));
665 let name = target
666 .file_name()
667 .map(|s| s.to_string_lossy().into_owned())
668 .unwrap_or_else(|| "file".to_string());
669 let rnd: u32 = rand::thread_rng().gen();
670 dir.join(format!("{name}.stryke-tmp-{rnd}"))
671}
672
673fn commit_in_place_edit(path: &Path, inplace_edit: &str, new_content: &str) -> std::io::Result<()> {
675 let tmp = adjacent_temp_path(path);
676 std::fs::write(&tmp, new_content)?;
677 if !inplace_edit.is_empty() {
678 let backup = PathBuf::from(format!("{}{}", path.display(), inplace_edit));
679 let _ = std::fs::remove_file(&backup);
680 std::fs::rename(path, &backup)?;
681 }
682 std::fs::rename(&tmp, path)?;
683 Ok(())
684}
685
686fn line_mode_input_record(cli: &Cli, l: String, sep_byte: Option<u8>) -> String {
691 if cli.line_ending.is_none() {
692 return l;
693 }
694 let mut bytes = l.into_bytes();
696 match sep_byte {
697 Some(b'\n') => {
698 if bytes.last() == Some(&b'\n') {
699 bytes.pop();
700 if bytes.last() == Some(&b'\r') {
701 bytes.pop();
702 }
703 }
704 }
705 Some(byte) => {
706 if bytes.last() == Some(&byte) {
707 bytes.pop();
708 }
709 }
710 None => {
712 if bytes.last() == Some(&b'\n') {
713 bytes.pop();
714 }
715 }
716 }
717 String::from_utf8(bytes).unwrap_or_default()
718}
719
720fn irs_separator_byte(irs: &Option<String>) -> Option<u8> {
731 match irs {
732 None => Some(b'\n'),
733 Some(s) => s.as_bytes().first().copied().or(Some(b'\n')),
734 }
735}
736
737fn run_line_mode_loop(
738 cli: &Cli,
739 interp: &mut VMHelper,
740 program: &Program,
741 slurp: bool,
742) -> Result<(), StrykeError> {
743 let sep_byte = irs_separator_byte(&interp.irs);
744 let inplace = cli.inplace.is_some();
745 let use_argv_files = !interp.argv.is_empty();
746 let suppressed_stdout_for_inplace = inplace && use_argv_files;
747 let print_to_stdout = cli.print_mode && !suppressed_stdout_for_inplace;
748 let parallel_argv_inplace = inplace && use_argv_files;
751
752 if slurp {
753 if use_argv_files {
754 if parallel_argv_inplace {
755 let template = Mutex::new(interp.line_mode_worker_clone());
756 let paths = interp.argv.clone();
757 paths.into_par_iter().try_for_each(|path| {
758 let mut local = template
759 .lock()
760 .expect("line-mode template mutex poisoned")
761 .line_mode_worker_clone();
762 local.line_number = 0;
763 local.argv_current_file = path.clone();
764 let content = read_file_text_perl_compat(&path).map_err(|e| {
765 StrykeError::new(
766 ErrorKind::IO,
767 format!("Can't open {}: {}", path, e),
768 0,
769 "-e",
770 )
771 })?;
772 if let Some(output) = local.process_line(&content, program, true)? {
773 commit_in_place_edit(Path::new(&path), &local.inplace_edit, &output)
774 .map_err(|e| StrykeError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
775 }
776 Ok(())
777 })?;
778 } else {
779 for path in interp.argv.clone() {
780 interp.line_number = 0;
781 interp.argv_current_file = path.clone();
782 let content = read_file_text_perl_compat(&path).map_err(|e| {
783 StrykeError::new(
784 ErrorKind::IO,
785 format!("Can't open {}: {}", path, e),
786 0,
787 "-e",
788 )
789 })?;
790 if let Some(output) = interp.process_line(&content, program, true)? {
791 if inplace {
792 commit_in_place_edit(Path::new(&path), &interp.inplace_edit, &output)
793 .map_err(|e| {
794 StrykeError::new(ErrorKind::IO, e.to_string(), 0, "-e")
795 })?;
796 } else if cli.print_mode {
797 print!("{}", output);
798 let _ = io::stdout().flush();
799 }
800 }
801 }
802 }
803 } else {
804 let mut input = String::new();
805 let mut raw = Vec::new();
806 let _ = IoRead::read_to_end(&mut io::stdin(), &mut raw);
807 input.push_str(&decode_utf8_or_latin1(&raw));
808 if let Some(output) = interp.process_line(&input, program, true)? {
809 if print_to_stdout {
810 print!("{}", output);
811 let _ = io::stdout().flush();
812 }
813 }
814 }
815 return Ok(());
816 }
817
818 if use_argv_files {
819 if parallel_argv_inplace {
820 let template = Mutex::new(interp.line_mode_worker_clone());
821 let paths = interp.argv.clone();
822 paths.into_par_iter().try_for_each(|path| {
823 let mut local = template
824 .lock()
825 .expect("line-mode template mutex poisoned")
826 .line_mode_worker_clone();
827 local.line_number = 0;
828 local.argv_current_file = path.clone();
829 let file = File::open(&path).map_err(|e| {
830 StrykeError::new(
831 ErrorKind::IO,
832 format!("Can't open {}: {}", path, e),
833 0,
834 "-e",
835 )
836 })?;
837 let mut reader = BufReader::new(file);
838 let mut accumulated = String::new();
839 let mut pending: Option<String> = None;
840 loop {
841 let l = if let Some(s) = pending.take() {
842 s
843 } else {
844 match read_logical_line_perl_compat_with_sep(&mut reader, sep_byte)
845 .map_err(|e| {
846 StrykeError::new(
847 ErrorKind::IO,
848 format!("Error reading {}: {}", path, e),
849 0,
850 "-e",
851 )
852 })? {
853 None => break,
854 Some(s) => s,
855 }
856 };
857 let is_last =
858 match read_logical_line_perl_compat_with_sep(&mut reader, sep_byte)
859 .map_err(|e| {
860 StrykeError::new(
861 ErrorKind::IO,
862 format!("Error reading {}: {}", path, e),
863 0,
864 "-e",
865 )
866 })? {
867 None => true,
868 Some(next) => {
869 pending = Some(next);
870 false
871 }
872 };
873 let input = line_mode_input_record(cli, l, sep_byte);
874 if let Some(output) = local.process_line(&input, program, is_last)? {
875 accumulated.push_str(&output);
876 }
877 }
878 commit_in_place_edit(Path::new(&path), &local.inplace_edit, &accumulated)
879 .map_err(|e| StrykeError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
880 Ok(())
881 })?;
882 } else {
883 for path in interp.argv.clone() {
884 interp.line_number = 0;
885 interp.argv_current_file = path.clone();
886 let file = File::open(&path).map_err(|e| {
887 StrykeError::new(
888 ErrorKind::IO,
889 format!("Can't open {}: {}", path, e),
890 0,
891 "-e",
892 )
893 })?;
894 let mut reader = BufReader::new(file);
895 let mut accumulated = String::new();
896 let mut pending: Option<String> = None;
897 loop {
898 let l = if let Some(s) = pending.take() {
899 s
900 } else {
901 match read_logical_line_perl_compat_with_sep(&mut reader, sep_byte)
902 .map_err(|e| {
903 StrykeError::new(
904 ErrorKind::IO,
905 format!("Error reading {}: {}", path, e),
906 0,
907 "-e",
908 )
909 })? {
910 None => break,
911 Some(s) => s,
912 }
913 };
914 let is_last =
915 match read_logical_line_perl_compat_with_sep(&mut reader, sep_byte)
916 .map_err(|e| {
917 StrykeError::new(
918 ErrorKind::IO,
919 format!("Error reading {}: {}", path, e),
920 0,
921 "-e",
922 )
923 })? {
924 None => true,
925 Some(next) => {
926 pending = Some(next);
927 false
928 }
929 };
930 let input = line_mode_input_record(cli, l, sep_byte);
931 if let Some(output) = interp.process_line(&input, program, is_last)? {
932 if print_to_stdout {
933 print!("{}", output);
934 let _ = io::stdout().flush();
935 }
936 if inplace {
937 accumulated.push_str(&output);
938 }
939 }
940 }
941 if inplace {
942 commit_in_place_edit(Path::new(&path), &interp.inplace_edit, &accumulated)
943 .map_err(|e| StrykeError::new(ErrorKind::IO, e.to_string(), 0, "-e"))?;
944 }
945 }
946 }
947 } else {
948 interp.line_mode_stdin_pending.clear();
955 loop {
956 let mut current = String::new();
957 let n = if let Some(queued) = interp.line_mode_stdin_pending.pop_front() {
958 current = queued;
959 current.len()
960 } else {
961 let mut lock = io::stdin().lock();
962 read_line_perl_compat_with_sep(&mut lock, &mut current, sep_byte).map_err(|e| {
963 StrykeError::new(ErrorKind::IO, format!("Error reading stdin: {e}"), 0, "-e")
964 })?
965 };
966 if n == 0 {
967 break;
968 }
969 let (is_last, peek_line) = {
970 let mut lock = io::stdin().lock();
971 let mut peek = String::new();
972 let n = read_line_perl_compat_with_sep(&mut lock, &mut peek, sep_byte).map_err(
973 |e| {
974 StrykeError::new(
975 ErrorKind::IO,
976 format!("Error reading stdin: {e}"),
977 0,
978 "-e",
979 )
980 },
981 )?;
982 if n == 0 {
983 (true, None)
984 } else {
985 (false, Some(peek))
986 }
987 };
988 if let Some(pl) = peek_line {
989 interp.line_mode_stdin_pending.push_back(pl);
990 }
991 let input = line_mode_input_record(cli, current, sep_byte);
995 match interp.process_line(&input, program, is_last) {
996 Ok(Some(output)) => {
997 if print_to_stdout {
998 print!("{}", output);
999 let _ = io::stdout().flush();
1000 }
1001 }
1002 Ok(None) => {}
1003 Err(e) => return Err(e),
1004 }
1005 }
1006 }
1007 Ok(())
1008}
1009
1010pub(crate) fn configure_interpreter(cli: &Cli, interp: &mut VMHelper, filename: &str) {
1011 interp.set_file(filename);
1012 interp.warnings = (cli.warnings || cli.all_warnings) && !cli.no_warnings;
1013 interp.auto_split = cli.auto_split;
1014 interp.field_separator = cli.field_separator.clone();
1015 interp.program_name = filename.to_string();
1016
1017 if let Some(ref sep) = cli.input_separator {
1018 match sep.as_deref() {
1019 None | Some("") => interp.irs = Some("\0".to_string()),
1020 Some("777") => interp.irs = None, Some(oct_str) => {
1022 if let Ok(val) = u32::from_str_radix(oct_str, 8) {
1023 if let Some(ch) = char::from_u32(val) {
1024 interp.irs = Some(ch.to_string());
1025 }
1026 }
1027 }
1028 }
1029 }
1030
1031 if let Some(ref octnum) = cli.line_ending {
1032 match octnum.as_deref() {
1033 None | Some("") => {
1034 interp.ors = "\n".to_string();
1035 }
1036 Some(oct_str) => {
1037 if let Ok(val) = u32::from_str_radix(oct_str, 8) {
1038 if let Some(ch) = char::from_u32(val) {
1039 interp.ors = ch.to_string();
1040 }
1041 }
1042 }
1043 }
1044 }
1045
1046 if (cli.taint_check || cli.taint_warn) && cli.warnings {
1047 eprintln!("stryke: taint mode acknowledged but not enforced");
1048 }
1049
1050 if let Some(ref ext_opt) = cli.inplace {
1051 interp.inplace_edit = ext_opt.clone().unwrap_or_default();
1052 }
1053
1054 let mut argv: Vec<String> =
1057 if cli.script.is_some() || !cli.execute.is_empty() || !cli.execute_features.is_empty() {
1058 cli.args.clone()
1059 } else {
1060 Vec::new()
1061 };
1062
1063 if cli.switch_parsing {
1064 let mut switches_done = false;
1065 let mut remaining = Vec::new();
1066 for arg in &argv {
1067 if switches_done || !arg.starts_with('-') || arg == "--" {
1068 if arg == "--" {
1069 switches_done = true;
1070 } else {
1071 remaining.push(arg.clone());
1072 }
1073 } else {
1074 let switch = &arg[1..];
1075 if let Some((name, val)) = switch.split_once('=') {
1076 let _ = interp
1077 .scope
1078 .set_scalar(name, crate::value::StrykeValue::string(val.to_string()));
1079 } else {
1080 let _ = interp
1081 .scope
1082 .set_scalar(switch, crate::value::StrykeValue::integer(1));
1083 }
1084 }
1085 }
1086 argv = remaining;
1087 }
1088
1089 interp.argv = argv.clone();
1090 interp.scope.declare_array(
1091 "ARGV",
1092 argv.into_iter()
1093 .map(crate::value::StrykeValue::string)
1094 .collect(),
1095 );
1096
1097 let mut inc_paths: Vec<String> = cli.include.clone();
1100 let vendor = crate::vendor_perl_inc_path();
1101 if vendor.is_dir() {
1102 crate::perl_inc::push_unique_string_paths(
1103 &mut inc_paths,
1104 vec![vendor.to_string_lossy().into_owned()],
1105 );
1106 }
1107 crate::perl_inc::push_unique_string_paths(
1108 &mut inc_paths,
1109 crate::perl_inc::paths_from_system_perl(),
1110 );
1111 if filename != "-e" && filename != "-" && filename != "repl" {
1112 if let Some(parent) = std::path::Path::new(filename).parent() {
1113 if !parent.as_os_str().is_empty() {
1114 crate::perl_inc::push_unique_string_paths(
1115 &mut inc_paths,
1116 vec![parent.to_string_lossy().into_owned()],
1117 );
1118 }
1119 }
1120 }
1121 if let Ok(extra) = std::env::var("STRYKE_INC") {
1122 let extra: Vec<String> = std::env::split_paths(&extra)
1123 .map(|p| p.to_string_lossy().into_owned())
1124 .collect();
1125 crate::perl_inc::push_unique_string_paths(&mut inc_paths, extra);
1126 }
1127 crate::perl_inc::push_unique_string_paths(&mut inc_paths, vec![".".to_string()]);
1128 let inc_dirs: Vec<crate::value::StrykeValue> = inc_paths
1129 .into_iter()
1130 .map(crate::value::StrykeValue::string)
1131 .collect();
1132 interp.scope.declare_array("INC", inc_dirs);
1133
1134 if cli.debugger.is_some() {
1135 let mut dbg = crate::debugger::Debugger::new();
1138 if filename != "-e" && filename != "-" && filename != "repl" {
1139 dbg.set_file(filename);
1140 if let Ok(src) = std::fs::read_to_string(filename) {
1141 dbg.load_source(&src);
1142 }
1143 }
1144 interp.debugger = Some(dbg);
1145 }
1146}
1147
1148fn emit_profiler_report(
1154 p: &mut crate::profiler::Profiler,
1155 flame_out: &Option<File>,
1156 flame_tty: bool,
1157) {
1158 if let Some(f) = flame_out {
1159 let mut w = io::BufWriter::new(f);
1161 if let Err(e) = p.render_flame_svg(&mut w) {
1162 eprintln!("stryke --flame: {}", e);
1163 }
1164 } else if flame_tty {
1165 p.render_flame_tty();
1167 } else {
1168 p.print_report();
1170 }
1171}
1172
1173pub fn run_argv(argv: &[String]) -> i32 {
1186 crate::hosted::run(|| run(argv))
1187}
1188
1189thread_local! {
1190 static INVOKED_AS: std::cell::RefCell<String> =
1197 const { std::cell::RefCell::new(String::new()) };
1198}
1199
1200fn invoked_as() -> String {
1202 INVOKED_AS.with(|n| {
1203 let n = n.borrow();
1204 if n.is_empty() {
1205 "stryke".to_string()
1206 } else {
1207 n.clone()
1208 }
1209 })
1210}
1211
1212fn run(full_argv: &[String]) -> i32 {
1213 INVOKED_AS.with(|n| {
1214 *n.borrow_mut() = full_argv
1215 .first()
1216 .map(|a| {
1217 Path::new(a)
1218 .file_name()
1219 .map_or(a.clone(), |b| b.to_string_lossy().into_owned())
1220 })
1221 .unwrap_or_default();
1222 });
1223
1224 #[cfg(unix)]
1237 if !crate::hosted::is_hosted() {
1238 unsafe {
1239 libc::signal(libc::SIGPIPE, libc::SIG_DFL);
1240 }
1241 }
1242
1243 if let Ok(exe) = std::env::current_exe() {
1248 if let Some(payload) = crate::aot::try_load_embedded(&exe) {
1249 let argv: Vec<String> = full_argv.iter().skip(1).cloned().collect();
1250 match payload {
1251 crate::aot::EmbeddedPayload::Script(embedded) => {
1252 crate::hosted::exit(run_embedded_script(embedded, argv));
1253 }
1254 crate::aot::EmbeddedPayload::Bundle(bundle) => {
1255 crate::hosted::exit(run_embedded_bundle(bundle, argv));
1256 }
1257 }
1258 }
1259 }
1260
1261 repl::ensure_default_config_seeded();
1267
1268 let mut args = expand_perl_bundled_argv(full_argv.to_vec());
1269
1270 let record_requested = {
1279 let pos = args.iter().position(|a| a == "--record");
1280 if let Some(i) = pos {
1281 args.remove(i);
1282 std::env::set_var("STRYKE_RECORD", "1");
1283 true
1284 } else {
1285 false
1286 }
1287 };
1288 if record_requested || crate::perf_recorder::recording_enabled_in_env() {
1289 let path = crate::perf_recorder::classify_invocation(&args);
1290 crate::perf_recorder::install(path, args.clone());
1291 }
1292
1293 if args.len() == 2 && args[1] == "--test-worker" {
1299 crate::hosted::exit(crate::cli_runners::run_test_worker_loop());
1300 }
1301
1302 if args.len() == 2 && args[1] == "--remote-worker" {
1303 crate::hosted::exit(crate::remote_wire::run_remote_worker_session());
1307 }
1308 if args.len() == 2 && args[1] == "--remote-worker-v1" {
1309 crate::hosted::exit(crate::remote_wire::run_remote_worker_stdio());
1310 }
1311
1312 if args.len() == 2 && args[1] == "--lsp" {
1313 crate::hosted::exit(crate::run_lsp_stdio());
1314 }
1315
1316 if args.len() >= 2 && args[1] == "--dap" {
1317 let rest: Vec<String> = args.iter().skip(2).cloned().collect();
1320 crate::hosted::exit(crate::dap::run_with_args(&rest));
1321 }
1322
1323 if args.len() >= 2 && args[1] == "agent" {
1325 if args.len() >= 3 && (args[2] == "--help" || args[2] == "-h") {
1326 crate::agent::print_help();
1327 crate::hosted::exit(0);
1328 }
1329 let mut config_path: Option<&str> = None;
1330 let mut controller_override: Option<String> = None;
1331 let mut port_override: Option<u16> = None;
1332 let mut i = 2;
1333 while i < args.len() {
1334 match args[i].as_str() {
1335 "-c" | "--config" if i + 1 < args.len() => {
1336 config_path = Some(&args[i + 1]);
1337 i += 2;
1338 }
1339 "--controller" if i + 1 < args.len() => {
1340 controller_override = Some(args[i + 1].clone());
1341 i += 2;
1342 }
1343 "--port" if i + 1 < args.len() => {
1344 port_override = args[i + 1].parse().ok();
1345 i += 2;
1346 }
1347 _ => i += 1,
1348 }
1349 }
1350 crate::hosted::exit(crate::agent::run_agent_with_overrides(
1351 config_path,
1352 controller_override.as_deref(),
1353 port_override,
1354 ));
1355 }
1356
1357 if args.len() >= 2 && args[1] == "controller" {
1359 if args.len() >= 3 && (args[2] == "--help" || args[2] == "-h") {
1360 crate::controller::print_help();
1361 crate::hosted::exit(0);
1362 }
1363 let mut bind = "0.0.0.0";
1364 let mut port = 9999u16;
1365 let mut i = 2;
1366 while i < args.len() {
1367 match args[i].as_str() {
1368 "--bind" | "-b" if i + 1 < args.len() => {
1369 bind = &args[i + 1];
1370 i += 2;
1371 }
1372 "--port" | "-p" if i + 1 < args.len() => {
1373 port = args[i + 1].parse().unwrap_or(9999);
1374 i += 2;
1375 }
1376 _ => i += 1,
1377 }
1378 }
1379 crate::hosted::exit(crate::controller::run_controller(bind, port));
1380 }
1381
1382 if args.len() >= 2 && args[1] == "ai" {
1386 crate::hosted::exit(run_ai_subcommand(&args[2..]));
1387 }
1388
1389 if args.len() >= 2 && args[1] == "build" {
1392 crate::hosted::exit(run_build_subcommand(&args[2..]));
1393 }
1394
1395 if args.len() >= 2 && args[1] == "convert" {
1397 crate::hosted::exit(run_convert_subcommand(&args[2..]));
1398 }
1399
1400 if args.len() >= 2 && args[1] == "deconvert" {
1402 crate::hosted::exit(run_deconvert_subcommand(&args[2..]));
1403 }
1404
1405 if args.len() >= 2 && (args[1] == "docs" || args[1] == "help" || args[1] == "h") {
1409 crate::hosted::exit(run_doc_subcommand(&args[2..]));
1410 }
1411
1412 if args.len() >= 2 && args[1] == "fmt" {
1414 crate::hosted::exit(run_fmt_subcommand(&args[2..]));
1415 }
1416
1417 if args.len() >= 2 && args[1] == "minify" {
1418 crate::hosted::exit(run_minify_subcommand(&args[2..]));
1419 }
1420
1421 if args.len() >= 2 && args[1] == "bench" {
1423 crate::hosted::exit(run_bench_subcommand(&args[0], &args[2..]));
1424 }
1425
1426 if args.len() >= 2 && args[1] == "init" {
1428 crate::hosted::exit(crate::pkg::commands::cmd_init(
1429 args.get(2).map(|s| s.as_str()),
1430 ));
1431 }
1432
1433 if args.len() >= 2 && args[1] == "new" {
1435 if args.len() < 3 {
1436 eprintln!("usage: stryke new NAME");
1437 crate::hosted::exit(1);
1438 }
1439 crate::hosted::exit(crate::pkg::commands::cmd_new(&args[2]));
1440 }
1441
1442 if args.len() >= 2 && args[1] == "add" {
1444 crate::hosted::exit(crate::pkg::commands::cmd_add(&args[2..]));
1445 }
1446 if args.len() >= 2 && args[1] == "remove" {
1447 crate::hosted::exit(crate::pkg::commands::cmd_remove(&args[2..]));
1448 }
1449 if args.len() >= 2 && matches!(args[1].as_str(), "install" | "i") {
1450 let rest = &args[2..];
1451 if rest.iter().any(|a| a == "-g" || a == "--global") {
1452 let filtered: Vec<String> = rest
1453 .iter()
1454 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1455 .cloned()
1456 .collect();
1457 crate::hosted::exit(crate::pkg::commands::cmd_install_global(&filtered));
1458 } else {
1459 crate::hosted::exit(crate::pkg::commands::cmd_install(rest));
1460 }
1461 }
1462 if args.len() >= 2 && args[1] == "tree" {
1463 crate::hosted::exit(crate::pkg::commands::cmd_tree(&args[2..]));
1464 }
1465 if args.len() >= 2 && args[1] == "info" {
1466 crate::hosted::exit(crate::pkg::commands::cmd_info(&args[2..]));
1467 }
1468 if args.len() >= 2 && matches!(args[1].as_str(), "update" | "up" | "upgrade") {
1469 if args[2..].iter().any(|a| a == "-g" || a == "--global") {
1470 let filtered: Vec<String> = args[2..]
1471 .iter()
1472 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1473 .cloned()
1474 .collect();
1475 crate::hosted::exit(crate::pkg::commands::cmd_upgrade_global(&filtered));
1476 }
1477 if args[1] == "upgrade" {
1478 crate::hosted::exit(crate::pkg::commands::cmd_upgrade_project(&args[2..]));
1479 }
1480 crate::hosted::exit(crate::pkg::commands::cmd_update(&args[2..]));
1481 }
1482 if args.len() >= 2 && args[1] == "outdated" {
1483 crate::hosted::exit(crate::pkg::commands::cmd_outdated(&args[2..]));
1484 }
1485 if args.len() >= 2 && args[1] == "audit" {
1486 crate::hosted::exit(crate::pkg::commands::cmd_audit(&args[2..]));
1487 }
1488 if args.len() >= 2 && args[1] == "vendor" {
1489 crate::hosted::exit(crate::pkg::commands::cmd_vendor(&args[2..]));
1490 }
1491 if args.len() >= 2 && args[1] == "clean" {
1492 crate::hosted::exit(crate::pkg::commands::cmd_clean(&args[2..]));
1493 }
1494 if args.len() >= 2 && args[1] == "search" {
1495 crate::hosted::exit(crate::pkg::commands::cmd_search(&args[2..]));
1496 }
1497 if args.len() >= 2 && matches!(args[1].as_str(), "publish" | "pub") {
1498 crate::hosted::exit(crate::pkg::commands::cmd_publish(&args[2..]));
1499 }
1500 if args.len() >= 2 && args[1] == "yank" {
1501 crate::hosted::exit(crate::pkg::commands::cmd_yank(&args[2..]));
1502 }
1503 if args.len() >= 2 && matches!(args[1].as_str(), "uninstall" | "un") {
1505 let rest = &args[2..];
1506 if rest.iter().any(|a| a == "-g" || a == "--global") {
1507 let filtered: Vec<String> = rest
1508 .iter()
1509 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1510 .cloned()
1511 .collect();
1512 crate::hosted::exit(crate::pkg::commands::cmd_uninstall_global(&filtered));
1513 } else {
1514 eprintln!("s uninstall: pass -g for global tools (no per-project uninstall yet)");
1515 crate::hosted::exit(1);
1516 }
1517 }
1518 if args.len() >= 2 && matches!(args[1].as_str(), "list" | "ls") {
1519 let rest = &args[2..];
1520 if rest.iter().any(|a| a == "-g" || a == "--global") {
1521 let filtered: Vec<String> = rest
1522 .iter()
1523 .filter(|a| !matches!(a.as_str(), "-g" | "--global"))
1524 .cloned()
1525 .collect();
1526 crate::hosted::exit(crate::pkg::commands::cmd_list_global(&filtered));
1527 } else {
1528 eprintln!("s list: pass -g to list global tools");
1529 crate::hosted::exit(1);
1530 }
1531 }
1532 if args.len() >= 2 && args[1] == "pkg" {
1533 crate::hosted::exit(crate::pkg::commands::dispatch(&args[2..]));
1534 }
1535
1536 if args.len() >= 2 && args[1] == "repl" {
1538 crate::hosted::exit(run_repl_subcommand(&args[2..]));
1539 }
1540
1541 if args.len() >= 2 && args[1] == "run" {
1546 if args.len() >= 3 && !args[2].starts_with('-') && !args[2].ends_with(".stk") {
1550 let candidate = &args[2];
1551 if let Ok(cwd) = std::env::current_dir() {
1552 if let Some(root) = crate::pkg::commands::find_project_root(&cwd) {
1553 let mp = root.join(crate::pkg::commands::MANIFEST_FILE);
1554 if let Ok(m) = crate::pkg::manifest::Manifest::from_path(&mp) {
1555 if m.scripts.contains_key(candidate) {
1556 crate::hosted::exit(crate::pkg::commands::cmd_run_script(&args[2..]));
1557 }
1558 }
1559 }
1560 }
1561 }
1562
1563 let script = if args.len() >= 3 {
1564 args[2].clone()
1565 } else {
1566 if std::path::Path::new("main.stk").exists() {
1568 "main.stk".to_string()
1569 } else if std::path::Path::new("src/main.stk").exists() {
1570 "src/main.stk".to_string()
1571 } else {
1572 eprintln!("stryke run: no main.stk found (checked ./main.stk and ./src/main.stk)");
1573 crate::hosted::exit(1);
1574 }
1575 };
1576 let exe = std::env::current_exe().unwrap_or_else(|_| std::path::PathBuf::from(&args[0]));
1578 let mut cmd = process::Command::new(exe);
1579 cmd.arg(&script);
1580 if args.len() > 3 {
1581 cmd.args(&args[3..]);
1582 }
1583 let status = cmd.status().unwrap_or_else(|e| {
1584 eprintln!("stryke run: {}", e);
1585 crate::hosted::exit(1);
1586 });
1587 crate::hosted::exit(status.code().unwrap_or(1));
1588 }
1589
1590 if args.len() >= 2 && args[1] == "prun" {
1592 crate::hosted::exit(run_prun_subcommand(&args[0], &args[2..]));
1593 }
1594
1595 let first_positional_idx = {
1598 let mut i = 1;
1599 let flags_with_value = ["-j", "-I", "-M", "-e", "-d"];
1600 while i < args.len() {
1601 if args[i].starts_with('-') {
1602 if flags_with_value.iter().any(|f| args[i] == *f) {
1603 i += 2; } else {
1605 i += 1; }
1607 } else {
1608 break;
1609 }
1610 }
1611 if i < args.len() {
1612 Some(i)
1613 } else {
1614 None
1615 }
1616 };
1617 let is_test_subcmd = first_positional_idx
1618 .map(|i| args[i] == "test" || args[i] == "t")
1619 .unwrap_or(false);
1620 if is_test_subcmd {
1621 let subcmd_idx = first_positional_idx.unwrap();
1622 let after_subcmd = subcmd_idx + 1;
1623 let raw_targets: Vec<String> = if after_subcmd < args.len() {
1626 args[after_subcmd..]
1627 .iter()
1628 .filter(|a| {
1629 let s = a.as_str();
1630 s != "--no-interop"
1631 && s != "--quiet"
1632 && s != "-q"
1633 && s != "--fork"
1634 && s != "--inproc"
1635 && s != "--pool"
1636 })
1637 .cloned()
1638 .collect()
1639 } else {
1640 Vec::new()
1641 };
1642 let j_threads = args
1643 .iter()
1644 .position(|a| a == "-j")
1645 .and_then(|i| args.get(i + 1).cloned());
1646 let no_interop = args.iter().any(|a| a == "--no-interop");
1647 let quiet = args[subcmd_idx + 1..]
1648 .iter()
1649 .any(|a| a == "--quiet" || a == "-q");
1650 let want_fork = args.iter().any(|a| a == "--fork");
1664 let want_inproc = args.iter().any(|a| a == "--inproc");
1665 if want_inproc {
1666 crate::hosted::exit(crate::cli_runners::run_tests_with_mode(
1667 &raw_targets,
1668 j_threads.as_deref(),
1669 no_interop,
1670 quiet,
1671 false,
1672 ));
1673 }
1674 if want_fork {
1675 crate::hosted::exit(crate::cli_runners::run_tests_with_mode(
1676 &raw_targets,
1677 j_threads.as_deref(),
1678 no_interop,
1679 quiet,
1680 true,
1681 ));
1682 }
1683 crate::hosted::exit(crate::cli_runners::run_tests_pool(
1685 &raw_targets,
1686 j_threads.as_deref(),
1687 no_interop,
1688 quiet,
1689 ));
1690 }
1691
1692 if args.len() >= 2 && args[1] == "serve" {
1694 crate::hosted::exit(run_serve_subcommand(&args[2..]));
1695 }
1696
1697 if args.len() >= 2 && args[1] == "check" {
1699 crate::hosted::exit(run_check_subcommand(&args[2..]));
1700 }
1701
1702 if args.len() >= 2 && args[1] == "disasm" {
1704 crate::hosted::exit(run_disasm_subcommand(&args[2..]));
1705 }
1706
1707 if args.len() >= 2 && args[1] == "profile" {
1709 crate::hosted::exit(run_profile_subcommand(&args[0], &args[2..]));
1710 }
1711
1712 if args.len() >= 2 && args[1] == "lsp" {
1714 crate::hosted::exit(crate::run_lsp_stdio());
1715 }
1716
1717 if args.len() >= 2 && args[1] == "completions" {
1719 crate::hosted::exit(run_completions_subcommand(&args[2..]));
1720 }
1721
1722 if args.len() >= 2 && args[1] == "ast" {
1724 crate::hosted::exit(run_ast_subcommand(&args[2..]));
1725 }
1726
1727 if args.len() >= 2 && args[1] == "gen-docs" {
1733 crate::hosted::exit(run_gen_docs_subcommand(&args[2..]));
1734 }
1735
1736 let force_script = args.iter().any(|a| a == "--script");
1744 if args.len() >= 2
1745 && !args[1].starts_with('-')
1746 && !force_script
1747 && crate::builtins::is_builtin(&args[1])
1748 && !Path::new(&args[1]).exists()
1749 {
1750 let builtin_name = &args[1];
1752 let builtin_args: Vec<String> = args[2..].to_vec();
1753 crate::hosted::exit(run_builtin_subcommand(builtin_name, &builtin_args));
1754 }
1755
1756 let arg1_is_code_not_flag =
1763 args.len() >= 2 && args[1].starts_with('-') && looks_like_code(&args[1]);
1764 let mut cli = if args.len() >= 2
1765 && (!args[1].starts_with('-') || arg1_is_code_not_flag)
1766 && !args[1].is_empty()
1767 && args[2..].iter().all(|a| !a.starts_with('-'))
1768 {
1769 Cli {
1770 script: Some(args[1].clone()),
1771 args: if args.len() > 2 {
1772 args[2..].to_vec()
1773 } else {
1774 Vec::new()
1775 },
1776 ..Default::default()
1777 }
1778 } else {
1779 parse_cli_prelude(&args).unwrap_or_else(|| Cli::parse_from(&args))
1780 };
1781 normalize_argv_after_dash_e(&mut cli);
1782
1783 if cli.compat {
1785 crate::set_compat_mode(true);
1786 }
1787 if cli.no_interop {
1788 crate::set_no_interop_mode(true);
1789 }
1790 if cli.static_typing {
1791 crate::set_static_mode(true);
1792 }
1793
1794 if cli.help {
1795 print_cyberpunk_help();
1796 return 0;
1797 }
1798
1799 if cli.show_version {
1800 println!(
1801 "This is stryke v{} — A highly parallel Perl 5 interpreter (Rust)\n",
1802 env!("CARGO_PKG_VERSION")
1803 );
1804 println!("Built with rayon for parallel map/grep/for/sort");
1805 println!(
1806 "Threads available: {}\n",
1807 std::thread::available_parallelism()
1808 .map(|n| n.get())
1809 .unwrap_or(1)
1810 );
1811 println!(
1812 "Copyright 2026 MenkeTechnologies. Licensed under MIT.\n\n\
1813 This is free software; you can redistribute it and/or modify it\n\
1814 under the terms of the MIT License."
1815 );
1816 return 0;
1817 }
1818
1819 if let Some(ref configvar) = cli.show_config {
1820 print_config(configvar.as_deref());
1821 return 0;
1822 }
1823
1824 if let Some(code) = &cli.explain {
1825 match crate::error::explain_error(code) {
1826 Some(text) => println!("{}", text),
1827 None => {
1828 eprintln!("stryke: unknown explain code {:?}", code);
1829 crate::hosted::exit(1);
1830 }
1831 }
1832 return 0;
1833 }
1834
1835 if let Some(n) = cli.threads {
1837 rayon::ThreadPoolBuilder::new()
1838 .num_threads(n)
1839 .build_global()
1840 .ok();
1841 }
1842
1843 if let Some(script) = cli.script.as_ref() {
1848 if cli.execute.is_empty()
1849 && cli.execute_features.is_empty()
1850 && !cli.line_mode
1851 && !cli.print_mode
1852 && !cli.args.is_empty()
1853 {
1854 let is_stk_ext = |p: &str| {
1855 p.ends_with(".stk") || p.ends_with(".pl") || p.ends_with(".pm") || p.ends_with(".t")
1856 };
1857 let is_existing_script = |p: &str| is_stk_ext(p) && Path::new(p).is_file();
1858
1859 if is_existing_script(script) && cli.args.iter().all(|a| is_existing_script(a)) {
1860 let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(&args[0]));
1861 let mut all_files = vec![script.clone()];
1862 all_files.extend(cli.args.iter().cloned());
1863
1864 let failed = if cli.threads.is_some() {
1866 use std::sync::atomic::{AtomicUsize, Ordering};
1867 let failed = AtomicUsize::new(0);
1868 all_files.par_iter().for_each(|f| {
1869 let status = process::Command::new(&exe).arg(f).status();
1870 match status {
1871 Ok(s) if !s.success() => {
1872 failed.fetch_add(1, Ordering::Relaxed);
1873 }
1874 Err(e) => {
1875 eprintln!("{}: {}", f, e);
1876 failed.fetch_add(1, Ordering::Relaxed);
1877 }
1878 _ => {}
1879 }
1880 });
1881 failed.load(Ordering::Relaxed)
1882 } else {
1883 let mut failed = 0usize;
1885 for f in &all_files {
1886 let status = process::Command::new(&exe).arg(f).status();
1887 match status {
1888 Ok(s) if !s.success() => failed += 1,
1889 Err(e) => {
1890 eprintln!("{}: {}", f, e);
1891 failed += 1;
1892 }
1893 _ => {}
1894 }
1895 }
1896 failed
1897 };
1898 crate::hosted::exit(if failed > 0 { 1 } else { 0 });
1899 }
1900 }
1901 }
1902
1903 let runtime_bin_name = args
1906 .first()
1907 .and_then(|p| Path::new(p).file_name())
1908 .map(|n| n.to_string_lossy().to_string())
1909 .unwrap_or_default();
1910 let is_stryke_bin = matches!(runtime_bin_name.as_str(), "stryke" | "st" | "s");
1916 let is_repl = is_stryke_bin
1917 && cli.script.is_none()
1918 && cli.execute.is_empty()
1919 && cli.execute_features.is_empty()
1920 && !cli.line_mode
1921 && !cli.print_mode
1922 && !cli.check_only
1923 && !cli.lint
1924 && !cli.disasm
1925 && !cli.dump_ast
1926 && !cli.dump_tokens
1927 && !cli.dump_bytecode
1928 && !cli.tiers
1929 && !cli.format_source
1930 && !cli.profile
1931 && !cli.flame
1932 && !cli.dump_core
1933 && cli.explain.is_none()
1934 && io::stdin().is_terminal();
1935
1936 if is_repl {
1937 repl::run(&cli);
1938 return 0;
1939 }
1940
1941 let slurp = cli.slurp
1943 || cli
1944 .input_separator
1945 .as_ref()
1946 .is_some_and(|v| v.as_deref() == Some("777"));
1947
1948 let (raw_script, filename): (String, String) = if !cli.execute.is_empty() {
1950 (cli.execute.join("; "), "-e".to_string())
1951 } else if !cli.execute_features.is_empty() {
1952 (cli.execute_features.join("; "), "-E".to_string())
1953 } else if let Some(ref script) = cli.script {
1954 if script == "-" {
1955 let mut code = Vec::new();
1957 let _ = IoRead::read_to_end(&mut io::stdin(), &mut code);
1958 let code = decode_utf8_or_latin1(&code);
1959 (code, "-".to_string())
1960 } else {
1961 let script_path = if cli.path_lookup {
1962 find_in_path(script).unwrap_or_else(|| script.clone())
1963 } else {
1964 script.clone()
1965 };
1966 match read_file_text_perl_compat(&script_path) {
1967 Ok(content) => (content, script_path),
1968 Err(_) if !cli.force_script && looks_like_code(&script_path) => {
1969 (script_path, "-e".to_string())
1971 }
1972 Err(e) => {
1973 eprintln!("Can't open perl script \"{}\": {}", script_path, e);
1974 crate::hosted::exit(2);
1975 }
1976 }
1977 }
1978 } else if cli.line_mode || cli.print_mode {
1979 (String::new(), "-".to_string())
1980 } else {
1981 let mut code = Vec::new();
1982 let _ = IoRead::read_to_end(&mut io::stdin(), &mut code);
1984 let code = decode_utf8_or_latin1(&code);
1985 (code, "-".to_string())
1986 };
1987
1988 let (program_text, data_opt) = crate::data_section::split_data_section(&raw_script);
1989 let code = strip_shebang_and_extract(&program_text, cli.extract.is_some());
1990
1991 if cli.from_zsh {
1995 let ns = crate::zsh_convert::namespace_from_path(&filename);
1996 let (stryke_src, warnings) = crate::zsh_convert::convert_zsh(&code, &ns);
1997 for w in &warnings {
1998 eprintln!("stryke --from-zsh: {}", w);
1999 }
2000 print!("{}", stryke_src);
2001 return 0;
2002 }
2003
2004 let mut full_code = module_prelude(&cli);
2005 full_code.push_str(&code);
2006
2007 if cli.dump_tokens {
2010 let desugared = if crate::compat_mode() {
2011 full_code.clone()
2012 } else {
2013 let s = crate::rust_sugar::desugar_rust_blocks(&full_code);
2014 crate::ai_sugar::desugar(&s)
2015 };
2016 let mut lexer = crate::lexer::Lexer::new_with_file(&desugared, &filename);
2017 match lexer.tokenize() {
2018 Ok(tokens) => {
2019 for (tok, line) in &tokens {
2020 println!("{:>5} {:?}", line, tok);
2021 }
2022 }
2023 Err(e) => {
2024 eprintln!("{}", e);
2025 crate::hosted::exit(255);
2026 }
2027 }
2028 return 0;
2029 }
2030
2031 if cli.tiers {
2036 match crate::tiers::report(&full_code) {
2037 Ok(r) => println!("{r}"),
2038 Err(e) => {
2039 eprintln!("stryke: --tiers: {e}");
2040 crate::hosted::exit(1);
2041 }
2042 }
2043 return 0;
2044 }
2045
2046 let is_one_liner = !cli.execute.is_empty() || !cli.execute_features.is_empty();
2048 let cache_eligible = !cli.line_mode
2049 && !cli.print_mode
2050 && !cli.lint
2051 && !cli.check_only
2052 && !cli.dump_ast
2053 && !cli.dump_tokens
2054 && !cli.dump_bytecode
2055 && !cli.tiers
2056 && !cli.format_source
2057 && !cli.profile
2058 && !cli.flame
2059 && !is_one_liner
2060 && !filename.is_empty();
2061
2062 let script_path = std::path::Path::new(&filename);
2063 let cached = if cache_eligible && script_path.exists() {
2064 crate::script_cache::try_load(script_path)
2065 } else {
2066 None
2067 };
2068
2069 let (program, cached_chunk, needs_cache_save) = if let Some(c) = cached {
2070 (c.program, Some(c.chunk), false)
2071 } else {
2072 let parsed = match crate::parse_with_file(&full_code, &filename) {
2073 Ok(p) => p,
2074 Err(e) => {
2075 eprintln!("{}", e);
2076 crate::hosted::exit(255);
2077 }
2078 };
2079 (parsed, None, cache_eligible)
2080 };
2081
2082 if cli.dump_ast {
2083 match serde_json::to_string_pretty(&program) {
2084 Ok(json) => println!("{}", json),
2085 Err(e) => {
2086 eprintln!("stryke: failed to serialize AST to JSON: {}", e);
2087 crate::hosted::exit(1);
2088 }
2089 }
2090 return 0;
2091 }
2092
2093 if cli.dump_bytecode {
2094 let mut interp = VMHelper::new();
2097 if cli.no_jit {
2098 interp.vm_jit_enabled = false;
2099 }
2100 interp.set_file(&filename);
2101 if let Err(e) = crate::lint_program(&program, &mut interp) {
2102 eprintln!("{}", e);
2103 crate::hosted::exit(255);
2104 }
2105 let comp = crate::compiler::Compiler::new().with_source_file(filename.clone());
2106 match comp.compile_program(&program) {
2107 Ok(chunk) => println!("{:#?}", chunk.ops),
2108 Err(e) => {
2109 eprintln!("compile error: {:?}", e);
2110 crate::hosted::exit(1);
2111 }
2112 }
2113 return 0;
2114 }
2115
2116 if cli.format_source {
2117 println!("{}", crate::convert::convert_program(&program));
2119 return 0;
2120 }
2121
2122 if cli.lint {
2123 let mut interp = VMHelper::new();
2124 if cli.no_jit {
2125 interp.vm_jit_enabled = false;
2126 }
2127 configure_interpreter(&cli, &mut interp, &filename);
2128 if let Some(data) = data_opt {
2129 interp.install_data_handle(data);
2130 }
2131 match crate::lint_program(&program, &mut interp) {
2132 Ok(()) => {
2133 eprintln!("{} compile OK", filename);
2134 return 0;
2135 }
2136 Err(e) => {
2137 eprintln!("{}", e);
2138 crate::hosted::exit(255);
2139 }
2140 }
2141 }
2142
2143 if cli.check_only {
2144 eprintln!("{} syntax OK", filename);
2145 return 0;
2146 }
2147
2148 if cli.dump_core {
2149 eprintln!("{} syntax OK (dump not supported)", filename);
2150 return 0;
2151 }
2152
2153 let mut interp = VMHelper::new();
2154 if cli.no_jit {
2155 interp.vm_jit_enabled = false;
2156 }
2157 if cli.disasm {
2158 interp.disasm_bytecode = true;
2159 }
2160 if cli.profile || cli.flame {
2161 interp.profiler = Some(crate::profiler::Profiler::new(filename.clone()));
2162 }
2163 interp.cached_chunk = cached_chunk;
2166 interp.cache_script_path = if needs_cache_save {
2167 Some(script_path.to_path_buf())
2168 } else {
2169 None
2170 };
2171 configure_interpreter(&cli, &mut interp, &filename);
2172 if let Some(data) = data_opt {
2173 interp.install_data_handle(data);
2174 }
2175
2176 let flame_is_tty = cli.flame && io::stdout().is_terminal();
2180 #[cfg(unix)]
2181 let flame_stdout: Option<File> = if cli.flame && !flame_is_tty {
2182 use std::os::unix::io::FromRawFd;
2183 let saved = unsafe { libc::dup(1) };
2184 if saved >= 0 {
2185 unsafe { libc::dup2(2, 1) };
2186 Some(unsafe { File::from_raw_fd(saved) })
2187 } else {
2188 None
2189 }
2190 } else {
2191 None
2192 };
2193 #[cfg(not(unix))]
2194 let flame_stdout: Option<File> = None;
2195
2196 if cli.line_mode || cli.print_mode {
2198 if cli.line_ending.is_some() {
2199 interp.ors = "\n".to_string();
2200 }
2201
2202 interp.line_mode_skip_main = true;
2205 if let Err(e) = interp.execute(&program) {
2206 interp.line_mode_skip_main = false;
2207 if let Some(mut p) = interp.profiler.take() {
2208 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2209 }
2210 if let ErrorKind::Exit(code) = e.kind {
2211 crate::hosted::exit(code);
2212 }
2213 eprintln!("{}", e);
2214 crate::hosted::exit(255);
2215 }
2216 interp.line_mode_skip_main = false;
2217
2218 if let Err(e) = run_line_mode_loop(&cli, &mut interp, &program, slurp) {
2219 if let Some(mut p) = interp.profiler.take() {
2220 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2221 }
2222 if let ErrorKind::Exit(code) = e.kind {
2223 crate::hosted::exit(code);
2224 }
2225 eprintln!("{}", e);
2226 crate::hosted::exit(255);
2227 }
2228 if let Err(e) = interp.run_end_blocks() {
2229 if let Some(mut p) = interp.profiler.take() {
2230 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2231 }
2232 if let ErrorKind::Exit(code) = e.kind {
2233 crate::hosted::exit(code);
2234 }
2235 eprintln!("{}", e);
2236 crate::hosted::exit(255);
2237 }
2238 let _ = interp.run_global_teardown();
2239 if let Some(mut p) = interp.profiler.take() {
2240 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2241 }
2242 } else {
2243 match interp.execute(&program) {
2245 Ok(_) => {
2246 let _ = interp.run_global_teardown();
2247 let _ = io::stdout().flush();
2248 if let Some(mut p) = interp.profiler.take() {
2249 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2250 }
2251 if interp
2255 .test_run_failed
2256 .load(std::sync::atomic::Ordering::Relaxed)
2257 {
2258 crate::hosted::exit(1);
2259 }
2260 }
2261 Err(e) => match e.kind {
2262 ErrorKind::Exit(code) => {
2263 if let Some(mut p) = interp.profiler.take() {
2264 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2265 }
2266 crate::hosted::exit(code);
2267 }
2268 ErrorKind::Die => {
2269 if let Some(mut p) = interp.profiler.take() {
2270 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2271 }
2272 eprint!("{}", e);
2273 crate::hosted::exit(255);
2274 }
2275 _ => {
2276 if let Some(mut p) = interp.profiler.take() {
2277 emit_profiler_report(&mut p, &flame_stdout, flame_is_tty);
2278 }
2279 eprintln!("{}", e);
2280 crate::hosted::exit(255);
2281 }
2282 },
2283 }
2284 }
2285
2286 0
2289}
2290
2291fn run_embedded_script(embedded: crate::aot::EmbeddedScript, argv: Vec<String>) -> i32 {
2296 let program = match crate::parse_with_file(&embedded.source, &embedded.name) {
2298 Ok(p) => p,
2299 Err(e) => {
2300 eprintln!("{}", e);
2301 return 255;
2302 }
2303 };
2304 let mut interp = VMHelper::new();
2305 interp.set_file(&embedded.name);
2306 interp.program_name = embedded.name.clone();
2307 interp.argv = argv.clone();
2308 interp.scope.declare_array(
2309 "ARGV",
2310 argv.into_iter()
2311 .map(crate::value::StrykeValue::string)
2312 .collect(),
2313 );
2314 interp.scope.declare_array(
2315 "INC",
2316 vec![crate::value::StrykeValue::string(".".to_string())],
2317 );
2318 match interp.execute(&program) {
2319 Ok(_) => {
2320 let _ = interp.run_global_teardown();
2321 let _ = io::stdout().flush();
2322 if interp
2325 .test_run_failed
2326 .load(std::sync::atomic::Ordering::Relaxed)
2327 {
2328 1
2329 } else {
2330 0
2331 }
2332 }
2333 Err(e) => match e.kind {
2334 ErrorKind::Exit(code) => code,
2335 ErrorKind::Die => {
2336 eprint!("{}", e);
2337 255
2338 }
2339 _ => {
2340 eprintln!("{}", e);
2341 255
2342 }
2343 },
2344 }
2345}
2346
2347fn run_embedded_bundle(bundle: crate::aot::EmbeddedBundle, argv: Vec<String>) -> i32 {
2350 let entry_source = match bundle.files.get(&bundle.entry) {
2351 Some(s) => s.clone(),
2352 None => {
2353 eprintln!("stryke: bundle missing entry point: {}", bundle.entry);
2354 return 255;
2355 }
2356 };
2357
2358 let program = match crate::parse_with_file(&entry_source, &bundle.entry) {
2359 Ok(p) => p,
2360 Err(e) => {
2361 eprintln!("{}", e);
2362 return 255;
2363 }
2364 };
2365
2366 let mut interp = VMHelper::new();
2367 interp.set_file(&bundle.entry);
2368 interp.program_name = bundle.entry.clone();
2369 interp.argv = argv.clone();
2370 interp.scope.declare_array(
2371 "ARGV",
2372 argv.into_iter()
2373 .map(crate::value::StrykeValue::string)
2374 .collect(),
2375 );
2376 interp.scope.declare_array(
2377 "INC",
2378 vec![crate::value::StrykeValue::string(".".to_string())],
2379 );
2380
2381 for (path, source) in &bundle.files {
2382 interp.register_virtual_module(path.clone(), source.clone());
2383 }
2384
2385 match interp.execute(&program) {
2386 Ok(_) => {
2387 let _ = interp.run_global_teardown();
2388 let _ = io::stdout().flush();
2389 if interp
2392 .test_run_failed
2393 .load(std::sync::atomic::Ordering::Relaxed)
2394 {
2395 1
2396 } else {
2397 0
2398 }
2399 }
2400 Err(e) => match e.kind {
2401 ErrorKind::Exit(code) => code,
2402 ErrorKind::Die => {
2403 eprint!("{}", e);
2404 255
2405 }
2406 _ => {
2407 eprintln!("{}", e);
2408 255
2409 }
2410 },
2411 }
2412}
2413
2414fn run_builtin_subcommand(name: &str, argv: &[String]) -> i32 {
2417 use crate::value::StrykeValue;
2418 use crate::vm_helper::VMHelper;
2419
2420 let mut interp = VMHelper::new();
2421 let argv_values: Vec<StrykeValue> = argv
2422 .iter()
2423 .map(|s| StrykeValue::string(s.clone()))
2424 .collect();
2425 let _ = interp.scope.set_array("ARGV", argv_values);
2426
2427 let wrap_dd = matches!(name, "perfview" | "pfv");
2432 let inner = if argv.is_empty() {
2433 format!("{}()", name)
2434 } else {
2435 format!("{}(@ARGV)", name)
2436 };
2437 let code = if wrap_dd {
2438 format!("p ddump({})", inner)
2439 } else {
2440 format!("p {}", inner)
2441 };
2442
2443 let program = match crate::parse_with_file(&code, "-e") {
2444 Ok(p) => p,
2445 Err(e) => {
2446 eprintln!("{}", e);
2447 return 255;
2448 }
2449 };
2450
2451 match interp.execute(&program) {
2452 Ok(_) => {
2453 let _ = interp.run_global_teardown();
2454 let _ = io::stdout().flush();
2455 if interp
2458 .test_run_failed
2459 .load(std::sync::atomic::Ordering::Relaxed)
2460 {
2461 1
2462 } else {
2463 0
2464 }
2465 }
2466 Err(e) => match e.kind {
2467 ErrorKind::Exit(code) => code,
2468 ErrorKind::Die => {
2469 eprint!("{}", e);
2470 255
2471 }
2472 _ => {
2473 eprintln!("{}", e);
2474 255
2475 }
2476 },
2477 }
2478}
2479
2480fn run_check_subcommand(args: &[String]) -> i32 {
2483 if args.is_empty() {
2484 eprintln!("usage: stryke check FILE...");
2485 eprintln!();
2486 eprintln!("Parse and compile stryke/perl files without executing.");
2487 eprintln!("Reports warnings and errors with file:line:col format.");
2488 eprintln!();
2489 eprintln!("Options:");
2490 eprintln!(" -q, --quiet Only output errors, no success messages");
2491 eprintln!(" --json Output diagnostics as JSON (one object per line)");
2492 return 0;
2493 }
2494
2495 let mut files: Vec<String> = Vec::new();
2496 let mut quiet = false;
2497 let mut json_output = false;
2498 let mut i = 0;
2499 while i < args.len() {
2500 match args[i].as_str() {
2501 "-q" | "--quiet" => quiet = true,
2502 "--json" => json_output = true,
2503 "-h" | "--help" => {
2504 eprintln!("usage: stryke check FILE...");
2505 return 0;
2506 }
2507 s if !s.starts_with('-') => files.push(s.to_string()),
2508 other => {
2509 eprintln!("stryke check: unknown option: {}", other);
2510 return 2;
2511 }
2512 }
2513 i += 1;
2514 }
2515
2516 if files.is_empty() {
2517 eprintln!("stryke check: no files specified");
2518 return 2;
2519 }
2520
2521 let mut errors = 0;
2522 for file in &files {
2523 let source = match std::fs::read_to_string(file) {
2524 Ok(s) => s,
2525 Err(e) => {
2526 if json_output {
2527 println!(
2528 r#"{{"file":"{}","line":0,"col":0,"severity":"error","message":"{}"}}"#,
2529 file,
2530 e.to_string().replace('"', "\\\"")
2531 );
2532 } else {
2533 eprintln!("{}:0:0: error: {}", file, e);
2534 }
2535 errors += 1;
2536 continue;
2537 }
2538 };
2539
2540 let program = match crate::parse_with_file(&source, file) {
2541 Ok(p) => p,
2542 Err(e) => {
2543 if json_output {
2544 println!(
2545 r#"{{"file":"{}","line":{},"col":0,"severity":"error","message":"{}"}}"#,
2546 file,
2547 e.line,
2548 e.to_string().replace('"', "\\\"").replace('\n', "\\n")
2549 );
2550 } else {
2551 eprintln!("{}:{}:0: error: {}", file, e.line, e);
2552 }
2553 errors += 1;
2554 continue;
2555 }
2556 };
2557
2558 let mut interp = VMHelper::new();
2559 interp.set_file(file);
2560 match crate::lint_program(&program, &mut interp) {
2561 Ok(()) => {
2562 if !quiet && !json_output {
2563 eprintln!("{}: OK", file);
2564 }
2565 }
2566 Err(e) => {
2567 if json_output {
2568 println!(
2569 r#"{{"file":"{}","line":{},"col":0,"severity":"error","message":"{}"}}"#,
2570 file,
2571 e.line,
2572 e.to_string().replace('"', "\\\"").replace('\n', "\\n")
2573 );
2574 } else {
2575 eprintln!("{}:{}:0: error: {}", file, e.line, e);
2576 }
2577 errors += 1;
2578 }
2579 }
2580 }
2581
2582 if errors > 0 {
2583 if !quiet && !json_output {
2584 eprintln!();
2585 eprintln!(
2586 "{} error{} in {} file{}",
2587 errors,
2588 if errors == 1 { "" } else { "s" },
2589 files.len(),
2590 if files.len() == 1 { "" } else { "s" }
2591 );
2592 }
2593 1
2594 } else {
2595 if !quiet && !json_output && files.len() > 1 {
2596 eprintln!();
2597 eprintln!("All {} files OK", files.len());
2598 }
2599 0
2600 }
2601}
2602
2603fn run_disasm_subcommand(args: &[String]) -> i32 {
2605 let mut file: Option<String> = None;
2606 let mut show_jit = false;
2607 let mut i = 0;
2608 while i < args.len() {
2609 match args[i].as_str() {
2610 "--jit" => show_jit = true,
2611 "-h" | "--help" => {
2612 println!("usage: stryke disasm [--jit] FILE");
2613 println!();
2614 println!("Disassemble stryke bytecode for a file.");
2615 println!();
2616 println!("Options:");
2617 println!(" --jit Also show Cranelift IR (when JIT is enabled)");
2618 return 0;
2619 }
2620 s if !s.starts_with('-') && file.is_none() => file = Some(s.to_string()),
2621 other => {
2622 eprintln!("stryke disasm: unknown option: {}", other);
2623 return 2;
2624 }
2625 }
2626 i += 1;
2627 }
2628
2629 let Some(file) = file else {
2630 eprintln!("usage: stryke disasm [--jit] FILE");
2631 return 2;
2632 };
2633
2634 let source = match std::fs::read_to_string(&file) {
2635 Ok(s) => s,
2636 Err(e) => {
2637 eprintln!("stryke disasm: {}: {}", file, e);
2638 return 1;
2639 }
2640 };
2641
2642 let program = match crate::parse_with_file(&source, &file) {
2643 Ok(p) => p,
2644 Err(e) => {
2645 eprintln!("{}", e);
2646 return 1;
2647 }
2648 };
2649
2650 let mut interp = VMHelper::new();
2651 interp.set_file(&file);
2652 if let Err(e) = crate::lint_program(&program, &mut interp) {
2653 eprintln!("{}", e);
2654 return 1;
2655 }
2656
2657 let comp = crate::compiler::Compiler::new().with_source_file(file.clone());
2658 let chunk = match comp.compile_program(&program) {
2659 Ok(c) => c,
2660 Err(e) => {
2661 eprintln!("compile error: {:?}", e);
2662 return 1;
2663 }
2664 };
2665
2666 println!("=== Bytecode for {} ===", file);
2667 println!("{}", chunk.disassemble());
2668
2669 if show_jit {
2670 println!();
2671 println!("=== Cranelift IR ===");
2672 println!("(JIT IR dump not yet implemented)");
2673 }
2674
2675 0
2676}
2677
2678fn run_prun_subcommand(exe_arg: &str, args: &[String]) -> i32 {
2680 if args.is_empty() || args[0] == "-h" || args[0] == "--help" {
2681 println!("usage: stryke prun FILE...");
2682 println!();
2683 println!("Run multiple stryke files in parallel using all available cores.");
2684 println!();
2685 println!("Examples:");
2686 println!(" stryke prun *.stk # run all .stk files in parallel");
2687 println!(" stryke prun a.stk b.stk c.stk # run specific files in parallel");
2688 println!();
2689 println!("For sequential execution, use: stryke *.stk");
2690 println!("For parallel with thread limit: stryke -j4 *.stk");
2691 return if args.is_empty() { 2 } else { 0 };
2692 }
2693
2694 let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(exe_arg));
2695 let files: Vec<&String> = args.iter().filter(|a| !a.starts_with('-')).collect();
2696
2697 if files.is_empty() {
2698 eprintln!("stryke prun: no files specified");
2699 return 2;
2700 }
2701
2702 use std::sync::atomic::{AtomicUsize, Ordering};
2703 let failed = AtomicUsize::new(0);
2704 let total = files.len();
2705
2706 eprintln!(
2707 "\x1b[36mRunning {} file{} in parallel\x1b[0m",
2708 total,
2709 if total == 1 { "" } else { "s" }
2710 );
2711
2712 files.par_iter().for_each(|f| {
2713 let status = process::Command::new(&exe)
2714 .arg(f)
2715 .stdout(process::Stdio::inherit())
2716 .stderr(process::Stdio::inherit())
2717 .status();
2718 match status {
2719 Ok(s) if !s.success() => {
2720 failed.fetch_add(1, Ordering::Relaxed);
2721 }
2722 Err(e) => {
2723 eprintln!("{}: {}", f, e);
2724 failed.fetch_add(1, Ordering::Relaxed);
2725 }
2726 _ => {}
2727 }
2728 });
2729
2730 let failed_count = failed.load(Ordering::Relaxed);
2731 if failed_count > 0 {
2732 eprintln!("\x1b[31m✗ {} of {} failed\x1b[0m", failed_count, total);
2733 1
2734 } else {
2735 eprintln!("\x1b[32m✓ All {} completed\x1b[0m", total);
2736 0
2737 }
2738}
2739
2740fn run_profile_subcommand(exe_arg: &str, args: &[String]) -> i32 {
2742 let mut file: Option<String> = None;
2743 let mut output: Option<String> = None;
2744 let mut flame = false;
2745 let mut json = false;
2746 let mut i = 0;
2747 while i < args.len() {
2748 match args[i].as_str() {
2749 "-o" | "--output" => {
2750 i += 1;
2751 if i >= args.len() {
2752 eprintln!("stryke profile: -o requires an argument");
2753 return 2;
2754 }
2755 output = Some(args[i].clone());
2756 }
2757 "--flame" => flame = true,
2758 "--json" => json = true,
2759 "-h" | "--help" => {
2760 println!("usage: stryke profile [OPTIONS] FILE");
2761 println!();
2762 println!("Run a file with profiling enabled and output structured data.");
2763 println!();
2764 println!("Options:");
2765 println!(" -o, --output FILE Write output to FILE instead of stdout/stderr");
2766 println!(" --flame Generate flamegraph SVG");
2767 println!(" --json Output profile data as JSON");
2768 return 0;
2769 }
2770 s if !s.starts_with('-') && file.is_none() => file = Some(s.to_string()),
2771 other => {
2772 eprintln!("stryke profile: unknown option: {}", other);
2773 return 2;
2774 }
2775 }
2776 i += 1;
2777 }
2778
2779 let Some(file) = file else {
2780 eprintln!("usage: stryke profile [OPTIONS] FILE");
2781 return 2;
2782 };
2783
2784 let exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from(exe_arg));
2785 let mut cmd = process::Command::new(exe);
2786
2787 if flame {
2788 cmd.arg("--flame");
2789 } else {
2790 cmd.arg("--profile");
2791 }
2792 cmd.arg(&file);
2793
2794 if let Some(ref out) = output {
2795 if flame {
2796 let out_file = match std::fs::File::create(out) {
2797 Ok(f) => f,
2798 Err(e) => {
2799 eprintln!("stryke profile: cannot create {}: {}", out, e);
2800 return 1;
2801 }
2802 };
2803 cmd.stdout(out_file);
2804 }
2805 }
2806
2807 let status = match cmd.status() {
2808 Ok(s) => s,
2809 Err(e) => {
2810 eprintln!("stryke profile: {}", e);
2811 return 1;
2812 }
2813 };
2814
2815 if json && !flame {
2816 eprintln!("(--json profile output not yet implemented; use --flame -o file.svg for structured output)");
2817 }
2818
2819 status.code().unwrap_or(1)
2820}
2821
2822fn run_completions_subcommand(args: &[String]) -> i32 {
2824 let shell = args.first().map(|s| s.as_str()).unwrap_or("");
2825 match shell {
2826 "zsh" | "" => {
2827 let completions = include_str!("../completions/_stryke");
2828 println!("{}", completions);
2829 0
2830 }
2831 "-h" | "--help" => {
2832 println!("usage: stryke completions [SHELL]");
2833 println!();
2834 println!("Emit shell completions to stdout.");
2835 println!();
2836 println!("Supported shells:");
2837 println!(" zsh (default)");
2838 println!();
2839 println!("Examples:");
2840 println!(" stryke completions zsh > /usr/local/share/zsh/site-functions/_stryke");
2841 println!(" stryke completions >> ~/.zshrc");
2842 0
2843 }
2844 other => {
2845 eprintln!("stryke completions: unsupported shell: {}", other);
2846 eprintln!("Supported: zsh");
2847 2
2848 }
2849 }
2850}
2851
2852fn run_ast_subcommand(args: &[String]) -> i32 {
2854 if args.is_empty() || args[0] == "-h" || args[0] == "--help" {
2855 println!("usage: stryke ast FILE");
2856 println!();
2857 println!("Parse a file and dump the AST as JSON to stdout.");
2858 return if args.is_empty() { 2 } else { 0 };
2859 }
2860
2861 let file = &args[0];
2862 let source = match std::fs::read_to_string(file) {
2863 Ok(s) => s,
2864 Err(e) => {
2865 eprintln!("{}: {}", file, e);
2866 return 1;
2867 }
2868 };
2869
2870 let program = match crate::parse_with_file(&source, file) {
2871 Ok(p) => p,
2872 Err(e) => {
2873 eprintln!("{}", e);
2874 return 1;
2875 }
2876 };
2877
2878 match serde_json::to_string_pretty(&program) {
2879 Ok(json) => {
2880 println!("{}", json);
2881 0
2882 }
2883 Err(e) => {
2884 eprintln!("stryke ast: failed to serialize: {}", e);
2885 1
2886 }
2887 }
2888}
2889
2890fn run_gen_docs_subcommand(args: &[String]) -> i32 {
2897 if args.first().map(|s| s.as_str()) == Some("-h")
2898 || args.first().map(|s| s.as_str()) == Some("--help")
2899 {
2900 println!("usage: stryke gen-docs [PATH] [--out DIR]");
2901 println!();
2902 println!("Walk PATH (default `.`) for `.stk`, `.pl`, `.pm` sources and");
2903 println!("generate Markdown module docs for each. Output goes under");
2904 println!("--out DIR (default `docs/`), mirroring the source layout.");
2905 println!();
2906 println!("Each output file contains the same content produced by");
2907 println!("`stryke --gen-docs FILE` on a single source.");
2908 return 0;
2909 }
2910
2911 let mut path: Option<String> = None;
2913 let mut out_dir: String = "docs".to_string();
2914 let mut i = 0;
2915 while i < args.len() {
2916 match args[i].as_str() {
2917 "--out" | "-o" => {
2918 if i + 1 >= args.len() {
2919 eprintln!("stryke gen-docs: --out requires a directory argument");
2920 return 2;
2921 }
2922 out_dir = args[i + 1].clone();
2923 i += 2;
2924 }
2925 other if !other.starts_with('-') && path.is_none() => {
2926 path = Some(other.to_string());
2927 i += 1;
2928 }
2929 other => {
2930 eprintln!("stryke gen-docs: unexpected argument: {other}");
2931 return 2;
2932 }
2933 }
2934 }
2935 let root = std::path::PathBuf::from(path.unwrap_or_else(|| ".".to_string()));
2936
2937 if !root.exists() {
2938 eprintln!("stryke gen-docs: path does not exist: {}", root.display());
2939 return 1;
2940 }
2941
2942 let mut sources: Vec<std::path::PathBuf> = Vec::new();
2943 collect_doc_sources(&root, &mut sources);
2944 sources.sort();
2945
2946 if sources.is_empty() {
2947 eprintln!(
2948 "stryke gen-docs: no `.stk` / `.pl` / `.pm` files found under {}",
2949 root.display()
2950 );
2951 return 1;
2952 }
2953
2954 let out_root = std::path::PathBuf::from(&out_dir);
2955 if let Err(e) = std::fs::create_dir_all(&out_root) {
2956 eprintln!(
2957 "stryke gen-docs: cannot create output dir {}: {}",
2958 out_root.display(),
2959 e
2960 );
2961 return 1;
2962 }
2963
2964 let mut index: Vec<(String, String)> = Vec::new(); let mut errors: Vec<String> = Vec::new();
2966 for src in &sources {
2967 let source = match std::fs::read_to_string(src) {
2968 Ok(s) => s,
2969 Err(e) => {
2970 errors.push(format!("{}: {}", src.display(), e));
2971 continue;
2972 }
2973 };
2974 let program = match crate::parse_with_file(&source, &src.to_string_lossy()) {
2975 Ok(p) => p,
2976 Err(e) => {
2977 errors.push(format!("{}: parse error: {}", src.display(), e));
2978 continue;
2979 }
2980 };
2981 let md = crate::docs::generate_markdown(&src.to_string_lossy(), &source, &program);
2982
2983 let rel = src.strip_prefix(&root).unwrap_or(src);
2985 let mut out_path = out_root.join(rel);
2986 out_path.set_extension("md");
2987 if let Some(parent) = out_path.parent() {
2988 if let Err(e) = std::fs::create_dir_all(parent) {
2989 errors.push(format!("{}: mkdir failed: {}", parent.display(), e));
2990 continue;
2991 }
2992 }
2993 if let Err(e) = std::fs::write(&out_path, &md) {
2994 errors.push(format!("{}: write failed: {}", out_path.display(), e));
2995 continue;
2996 }
2997
2998 let title = md
3001 .lines()
3002 .next()
3003 .and_then(|l| l.strip_prefix("# Module: "))
3004 .unwrap_or_else(|| {
3005 src.file_stem()
3006 .and_then(|s| s.to_str())
3007 .unwrap_or("(unknown)")
3008 })
3009 .to_string();
3010 let rel_md = out_path
3011 .strip_prefix(&out_root)
3012 .unwrap_or(&out_path)
3013 .to_string_lossy()
3014 .into_owned();
3015 index.push((title, rel_md));
3016 println!("{}", out_path.display());
3017 }
3018
3019 let mut idx = String::new();
3021 idx.push_str("# Module index\n\n");
3022 for (title, rel) in &index {
3023 idx.push_str(&format!("- [{title}]({rel})\n"));
3024 }
3025 let idx_path = out_root.join("index.md");
3026 if let Err(e) = std::fs::write(&idx_path, idx) {
3027 errors.push(format!("{}: write failed: {}", idx_path.display(), e));
3028 } else {
3029 println!("{}", idx_path.display());
3030 }
3031
3032 if !errors.is_empty() {
3033 eprintln!("stryke gen-docs: {} error(s):", errors.len());
3034 for e in &errors {
3035 eprintln!(" {e}");
3036 }
3037 return 1;
3038 }
3039 0
3040}
3041
3042fn collect_doc_sources(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
3046 const SKIP: &[&str] = &[
3047 ".git",
3048 "target",
3049 "node_modules",
3050 ".cargo",
3051 ".idea",
3052 ".vscode",
3053 "build",
3054 "dist",
3055 ];
3056 let Ok(entries) = std::fs::read_dir(dir) else {
3057 return;
3058 };
3059 for entry in entries.flatten() {
3060 let path = entry.path();
3061 if path.is_dir() {
3062 if let Some(name) = path.file_name().and_then(|s| s.to_str()) {
3063 if SKIP.contains(&name) || name.starts_with('.') {
3064 continue;
3065 }
3066 }
3067 collect_doc_sources(&path, out);
3068 } else if let Some(ext) = path.extension().and_then(|s| s.to_str()) {
3069 if matches!(ext, "stk" | "pl" | "pm") {
3070 out.push(path);
3071 }
3072 }
3073 }
3074}
3075
3076fn run_ai_subcommand(args: &[String]) -> i32 {
3087 use std::io::Read;
3088 let mut prompt: Option<String> = None;
3089 let mut model: Option<String> = None;
3090 let mut system: Option<String> = None;
3091 let mut stream = false;
3092 let mut json_out = false;
3093 let mut mode_image = false;
3094 let mut mode_transcribe = false;
3095 let mut mode_speak = false;
3096 let mut output: Option<String> = None;
3097 let mut size: Option<String> = None;
3098 let mut voice: Option<String> = None;
3099 let mut quality: Option<String> = None;
3100 let mut language: Option<String> = None;
3101
3102 let mut i = 0;
3103 while i < args.len() {
3104 match args[i].as_str() {
3105 "--model" if i + 1 < args.len() => {
3106 model = Some(args[i + 1].clone());
3107 i += 2;
3108 }
3109 "--system" if i + 1 < args.len() => {
3110 system = Some(args[i + 1].clone());
3111 i += 2;
3112 }
3113 "--stream" => {
3114 stream = true;
3115 i += 1;
3116 }
3117 "--json" => {
3118 json_out = true;
3119 i += 1;
3120 }
3121 "--image" => {
3122 mode_image = true;
3123 i += 1;
3124 }
3125 "--transcribe" => {
3126 mode_transcribe = true;
3127 i += 1;
3128 }
3129 "--speak" => {
3130 mode_speak = true;
3131 i += 1;
3132 }
3133 "-o" | "--output" if i + 1 < args.len() => {
3134 output = Some(args[i + 1].clone());
3135 i += 2;
3136 }
3137 "--size" if i + 1 < args.len() => {
3138 size = Some(args[i + 1].clone());
3139 i += 2;
3140 }
3141 "--voice" if i + 1 < args.len() => {
3142 voice = Some(args[i + 1].clone());
3143 i += 2;
3144 }
3145 "--quality" if i + 1 < args.len() => {
3146 quality = Some(args[i + 1].clone());
3147 i += 2;
3148 }
3149 "--language" if i + 1 < args.len() => {
3150 language = Some(args[i + 1].clone());
3151 i += 2;
3152 }
3153 "--help" | "-h" => {
3154 println!(
3155 "Usage: stryke ai [PROMPT] [FLAGS]\n\
3156 \n\
3157 Modes (mutually exclusive — default is text completion):\n\
3158 <none> chat completion → stdout\n\
3159 --image text-to-image (DALL-E 3 / gpt-image-1)\n\
3160 --transcribe Whisper transcription (PROMPT = audio path)\n\
3161 --speak OpenAI TTS (PROMPT = text to speak)\n\
3162 \n\
3163 Common flags:\n\
3164 --model MODEL override the model (default from stryke.toml)\n\
3165 --system TEXT set the system prompt (chat mode)\n\
3166 --stream stream tokens to stdout (chat mode)\n\
3167 --json emit a JSON object with usd/tokens/response\n\
3168 -o, --output write binary output to a path (image/speak)\n\
3169 \n\
3170 Image flags: --size 1024x1024 --quality hd -o out.png\n\
3171 Speak flags: --voice alloy -o out.mp3\n\
3172 Transcribe: --language en"
3173 );
3174 return 0;
3175 }
3176 other if !other.starts_with("--") => {
3177 let mut combined = other.to_string();
3178 i += 1;
3179 while i < args.len() && !args[i].starts_with("--") {
3180 combined.push(' ');
3181 combined.push_str(&args[i]);
3182 i += 1;
3183 }
3184 prompt = Some(combined);
3185 }
3186 other => {
3187 eprintln!("stryke ai: unknown flag `{}`", other);
3188 return 2;
3189 }
3190 }
3191 }
3192
3193 let mode_count = [mode_image, mode_transcribe, mode_speak]
3194 .iter()
3195 .filter(|b| **b)
3196 .count();
3197 if mode_count > 1 {
3198 eprintln!("stryke ai: --image / --transcribe / --speak are mutually exclusive");
3199 return 2;
3200 }
3201
3202 if mode_image {
3203 return run_ai_image_cli(prompt, model, output, size, quality);
3204 }
3205 if mode_transcribe {
3206 return run_ai_transcribe_cli(prompt, model, output, language);
3207 }
3208 if mode_speak {
3209 return run_ai_speak_cli(prompt, model, output, voice);
3210 }
3211
3212 if prompt.is_none() {
3213 let mut buf = String::new();
3214 if std::io::stdin().read_to_string(&mut buf).is_err() {
3215 eprintln!("stryke ai: failed to read prompt from stdin");
3216 return 1;
3217 }
3218 let trimmed = buf.trim().to_string();
3219 if trimmed.is_empty() {
3220 eprintln!("stryke ai: no prompt given (argv or stdin)");
3221 return 1;
3222 }
3223 prompt = Some(trimmed);
3224 }
3225 let prompt = prompt.unwrap();
3226
3227 let mut script = String::new();
3231 if stream {
3232 script.push_str(
3233 "my $state = +{ buf => \"\" };\n\
3234 my %opts = ( on_chunk => sub { print $_[0]; STDOUT->flush } );\n",
3235 );
3236 } else {
3237 script.push_str("my %opts = ();\n");
3238 }
3239 if let Some(m) = &model {
3240 script.push_str(&format!("$opts{{model}} = q{{{}}};\n", m));
3241 }
3242 if let Some(s) = &system {
3243 script.push_str(&format!("$opts{{system}} = q{{{}}};\n", s));
3244 }
3245 if stream {
3246 script.push_str("my $resp = stream_prompt(do { local $_ = q{__PROMPT__}; $_ }, %opts);\n");
3247 } else {
3248 script.push_str("my $resp = ai(do { local $_ = q{__PROMPT__}; $_ }, %opts);\n");
3249 }
3250 if json_out {
3251 script.push_str(
3252 "my $c = ai_cost();\n\
3253 use JSON ();\n\
3254 print JSON::encode_json(+{ response => $resp, usd => $c->{usd}, input_tokens => $c->{input_tokens}, output_tokens => $c->{output_tokens} });\n\
3255 print \"\\n\";\n",
3256 );
3257 } else if !stream {
3258 script.push_str("print $resp, \"\\n\";\n");
3259 } else {
3260 script.push_str("print \"\\n\";\n");
3261 }
3262 let final_script = script.replace(
3263 "__PROMPT__",
3264 &prompt.replace('\\', "\\\\").replace('}', "\\}"),
3265 );
3266
3267 let mut interp = VMHelper::new();
3268 match crate::parse_and_run_string(&final_script, &mut interp) {
3269 Ok(_) => 0,
3270 Err(e) => {
3271 eprintln!("stryke ai: {}", e);
3272 1
3273 }
3274 }
3275}
3276
3277fn run_ai_image_cli(
3280 prompt: Option<String>,
3281 model: Option<String>,
3282 output: Option<String>,
3283 size: Option<String>,
3284 quality: Option<String>,
3285) -> i32 {
3286 let prompt = match prompt {
3287 Some(p) => p,
3288 None => {
3289 eprintln!("stryke ai --image: PROMPT required");
3290 return 2;
3291 }
3292 };
3293 let mut script = String::from("my %opts;\n");
3294 if let Some(m) = model {
3295 script.push_str(&format!("$opts{{model}} = q{{{}}};\n", m));
3296 }
3297 if let Some(s) = size {
3298 script.push_str(&format!("$opts{{size}} = q{{{}}};\n", s));
3299 }
3300 if let Some(q) = quality {
3301 script.push_str(&format!("$opts{{quality}} = q{{{}}};\n", q));
3302 }
3303 if let Some(o) = &output {
3304 script.push_str(&format!("$opts{{output}} = q{{{}}};\n", o));
3305 script.push_str(&format!(
3306 "my $bytes = ai_image(q{{{}}}, %opts);\nprint STDERR \"wrote \", q{{{}}}, \" (\", length($bytes), \" bytes)\\n\";\n",
3307 prompt.replace('\\', "\\\\").replace('}', "\\}"),
3308 o
3309 ));
3310 } else {
3311 script.push_str(&format!(
3313 "my $bytes = ai_image(q{{{}}}, %opts);\nuse MIME::Base64 ();\nprint MIME::Base64::encode_base64($bytes);\n",
3314 prompt.replace('\\', "\\\\").replace('}', "\\}")
3315 ));
3316 }
3317 run_ai_cli_script(&script)
3318}
3319
3320fn run_ai_transcribe_cli(
3322 prompt: Option<String>,
3323 model: Option<String>,
3324 output: Option<String>,
3325 language: Option<String>,
3326) -> i32 {
3327 let path = match prompt {
3328 Some(p) => p,
3329 None => {
3330 eprintln!("stryke ai --transcribe: PATH to audio file required");
3331 return 2;
3332 }
3333 };
3334 let mut script = String::from("my %opts;\n");
3335 if let Some(m) = model {
3336 script.push_str(&format!("$opts{{model}} = q{{{}}};\n", m));
3337 }
3338 if let Some(l) = language {
3339 script.push_str(&format!("$opts{{language}} = q{{{}}};\n", l));
3340 }
3341 script.push_str(&format!(
3342 "my $text = ai_transcribe(q{{{}}}, %opts);\n",
3343 path.replace('\\', "\\\\").replace('}', "\\}"),
3344 ));
3345 if let Some(o) = output {
3346 script.push_str(&format!(
3347 "open my $fh, '>', q{{{}}} or die qq{{open $!: }};\nprint $fh $text;\nclose $fh;\nprint STDERR qq{{wrote }}, q{{{}}}, qq{{ (}}, length($text), qq{{ chars)\\n}};\n",
3348 o, o
3349 ));
3350 } else {
3351 script.push_str("print $text, \"\\n\";\n");
3352 }
3353 run_ai_cli_script(&script)
3354}
3355
3356fn run_ai_speak_cli(
3358 prompt: Option<String>,
3359 model: Option<String>,
3360 output: Option<String>,
3361 voice: Option<String>,
3362) -> i32 {
3363 let text = match prompt {
3364 Some(p) => p,
3365 None => {
3366 eprintln!("stryke ai --speak: TEXT required");
3367 return 2;
3368 }
3369 };
3370 let mut script = String::from("my %opts;\n");
3371 if let Some(m) = model {
3372 script.push_str(&format!("$opts{{model}} = q{{{}}};\n", m));
3373 }
3374 if let Some(v) = voice {
3375 script.push_str(&format!("$opts{{voice}} = q{{{}}};\n", v));
3376 }
3377 let out_path = output.unwrap_or_else(|| "speech.mp3".to_string());
3378 script.push_str(&format!("$opts{{output}} = q{{{}}};\n", out_path));
3379 script.push_str(&format!(
3380 "my $bytes = ai_speak(q{{{}}}, %opts);\nprint STDERR qq{{wrote }}, q{{{}}}, qq{{ (}}, length($bytes), qq{{ bytes)\\n}};\n",
3381 text.replace('\\', "\\\\").replace('}', "\\}"),
3382 out_path
3383 ));
3384 run_ai_cli_script(&script)
3385}
3386
3387fn run_ai_cli_script(script: &str) -> i32 {
3389 let mut interp = VMHelper::new();
3390 match crate::parse_and_run_string(script, &mut interp) {
3391 Ok(_) => 0,
3392 Err(e) => {
3393 eprintln!("stryke ai: {}", e);
3394 1
3395 }
3396 }
3397}
3398
3399fn run_build_subcommand(args: &[String]) -> i32 {
3400 let mut script: Option<String> = None;
3401 let mut project_dir: Option<String> = None;
3402 let mut out: Option<String> = None;
3403 let mut mcp_server = false;
3404 let mut native = false;
3405 let mut i = 0usize;
3406 while i < args.len() {
3407 match args[i].as_str() {
3408 "--native" | "-n" => {
3409 native = true;
3410 }
3411 "-o" | "--output" => {
3412 i += 1;
3413 if i >= args.len() {
3414 eprintln!("stryke build: -o requires an argument");
3415 return 2;
3416 }
3417 out = Some(args[i].clone());
3418 }
3419 "--project" | "-p" => {
3420 i += 1;
3421 if i >= args.len() {
3422 eprintln!("stryke build: --project requires a directory argument");
3423 return 2;
3424 }
3425 project_dir = Some(args[i].clone());
3426 }
3427 "--mcp-server" => {
3428 mcp_server = true;
3429 }
3430 "-h" | "--help" => {
3431 println!("usage: stryke build SCRIPT [-o OUTPUT] [--mcp-server]");
3432 println!(" stryke build --project DIR [-o OUTPUT] [--mcp-server]");
3433 println!();
3434 println!(
3435 "Compile a Perl script into a standalone executable binary. The output is"
3436 );
3437 println!(
3438 "a copy of this stryke binary with the script source embedded as a compressed"
3439 );
3440 println!(
3441 "trailer. `scp` the result to any compatible machine and run it directly —"
3442 );
3443 println!("no perl, no stryke, no @INC setup required.");
3444 println!();
3445 println!("Options:");
3446 println!(
3447 " --native, -n Compile to native machine code via fusevm's Cranelift"
3448 );
3449 println!(
3450 " AOT (no embedded source). Covers the arithmetic/string/"
3451 );
3452 println!(
3453 " scalar/array/hash/print subset; needs libstryke.a beside"
3454 );
3455 println!(" stryke (or $STRYKE_AOT_RUNTIME_LIB).");
3456 println!(" --project DIR Bundle main.stk + lib/*.stk (excludes t/ tests)");
3457 println!(" --mcp-server Wrap as an MCP server: after running the user's");
3458 println!(" script (which calls `tool fn ...`), the binary");
3459 println!(" serves the registered tools over stdio JSON-RPC.");
3460 println!();
3461 println!("Examples:");
3462 println!(" stryke build app.pl # → ./app");
3463 println!(" stryke build app.pl -o /usr/local/bin/app");
3464 println!(" stryke build --project ./myapp -o myapp # bundle project");
3465 println!(" stryke build tools.stk --mcp-server -o my-mcp-server");
3466 return 0;
3467 }
3468 s if script.is_none() && project_dir.is_none() && !s.starts_with('-') => {
3469 script = Some(s.to_string())
3470 }
3471 other => {
3472 eprintln!("stryke build: unknown argument: {}", other);
3473 eprintln!("usage: stryke build SCRIPT [-o OUTPUT]");
3474 eprintln!(" stryke build --project DIR [-o OUTPUT]");
3475 return 2;
3476 }
3477 }
3478 i += 1;
3479 }
3480
3481 let script = if mcp_server {
3486 let Some(orig) = script.as_ref() else {
3487 eprintln!("stryke build --mcp-server: requires SCRIPT (project mode adds it to main.stk if you really need that)");
3488 return 2;
3489 };
3490 match wrap_for_mcp_server(orig, out.as_deref()) {
3491 Ok(path) => Some(path),
3492 Err(e) => {
3493 eprintln!("stryke build --mcp-server: {}", e);
3494 return 1;
3495 }
3496 }
3497 } else {
3498 script
3499 };
3500
3501 if native && project_dir.is_some() {
3502 eprintln!("stryke build --native: not supported with --project (single SCRIPT only)");
3503 return 2;
3504 }
3505
3506 if let Some(dir) = project_dir {
3507 let project_path = PathBuf::from(&dir);
3508 let out_path = PathBuf::from(out.unwrap_or_else(|| {
3509 project_path
3510 .file_name()
3511 .map(|s| s.to_string_lossy().into_owned())
3512 .unwrap_or_else(|| "a.out".to_string())
3513 }));
3514 match crate::aot::build_project(&project_path, &out_path) {
3515 Ok(p) => {
3516 eprintln!("stryke build: wrote {}", p.display());
3517 0
3518 }
3519 Err(e) => {
3520 eprintln!("{}", e);
3521 1
3522 }
3523 }
3524 } else {
3525 let Some(script) = script else {
3526 eprintln!("stryke build: missing SCRIPT or --project DIR");
3527 eprintln!("usage: stryke build SCRIPT [-o OUTPUT]");
3528 eprintln!(" stryke build --project DIR [-o OUTPUT]");
3529 return 2;
3530 };
3531 let script_path = PathBuf::from(&script);
3532 let out_path = PathBuf::from(out.unwrap_or_else(|| {
3533 script_path
3534 .file_stem()
3535 .map(|s| s.to_string_lossy().into_owned())
3536 .unwrap_or_else(|| "a.out".to_string())
3537 }));
3538 let result = if native {
3539 crate::aot_native::build_native(&script_path, &out_path)
3542 } else {
3543 crate::aot::build(&script_path, &out_path)
3544 };
3545 match result {
3546 Ok(p) => {
3547 eprintln!("stryke build: wrote {}", p.display());
3548 0
3549 }
3550 Err(e) => {
3551 eprintln!("{}", e);
3552 1
3553 }
3554 }
3555 }
3556}
3557
3558fn wrap_for_mcp_server(orig: &str, out: Option<&str>) -> Result<String, String> {
3563 use std::io::Write;
3564 let orig_abs =
3565 std::fs::canonicalize(orig).map_err(|e| format!("canonicalize {}: {}", orig, e))?;
3566 let dir = match out {
3567 Some(o) => PathBuf::from(o)
3568 .parent()
3569 .map(|p| p.to_path_buf())
3570 .unwrap_or_else(std::env::temp_dir),
3571 None => std::env::temp_dir(),
3572 };
3573 let pid = std::process::id();
3574 let nanos = std::time::SystemTime::now()
3575 .duration_since(std::time::UNIX_EPOCH)
3576 .map(|d| d.subsec_nanos())
3577 .unwrap_or(0);
3578 let wrapper = dir.join(format!("stryke-mcp-wrapper-{}-{}.stk", pid, nanos));
3579 let server_name = std::path::Path::new(orig)
3580 .file_stem()
3581 .map(|s| s.to_string_lossy().into_owned())
3582 .unwrap_or_else(|| "stryke-mcp".to_string());
3583 let body = format!(
3584 "# Auto-generated by `stryke build --mcp-server`. Loads the user's\n\
3585 # script (which registers tools via `tool fn ...`), then enters\n\
3586 # the MCP stdio JSON-RPC loop.\n\
3587 require {orig_abs:?};\n\
3588 mcp_serve_registered_tools({server_name:?});\n",
3589 );
3590 let mut f = std::fs::File::create(&wrapper)
3591 .map_err(|e| format!("create {}: {}", wrapper.display(), e))?;
3592 f.write_all(body.as_bytes())
3593 .map_err(|e| format!("write {}: {}", wrapper.display(), e))?;
3594 Ok(wrapper.to_string_lossy().into_owned())
3595}
3596
3597fn run_convert_subcommand(args: &[String]) -> i32 {
3599 let mut files: Vec<String> = Vec::new();
3600 let mut in_place = false;
3601 let mut output_delim: Option<char> = None;
3602 let mut i = 0;
3603 while i < args.len() {
3604 match args[i].as_str() {
3605 "-i" | "--in-place" => in_place = true,
3606 "-d" | "--output-delim" => {
3607 i += 1;
3608 if i >= args.len() {
3609 eprintln!("stryke convert: --output-delim requires an argument");
3610 return 2;
3611 }
3612 let delim_str = &args[i];
3613 if delim_str.chars().count() != 1 {
3614 eprintln!(
3615 "stryke convert: --output-delim must be a single character, got {:?}",
3616 delim_str
3617 );
3618 return 2;
3619 }
3620 output_delim = delim_str.chars().next();
3621 }
3622 "-h" | "--help" => {
3623 println!("usage: stryke convert [-i] [-d DELIM] FILE...");
3624 println!();
3625 println!("Convert standard Perl source to idiomatic stryke syntax:");
3626 println!(" - Nested calls → |> pipe-forward chains");
3627 println!(" - map/grep/sort/join LIST → LIST |> map/grep/sort/join");
3628 println!(" - No trailing semicolons");
3629 println!(" - 4-space indentation");
3630 println!(" - #!/usr/bin/env stryke shebang");
3631 println!();
3632 println!("Options:");
3633 println!(" -i, --in-place Write .stk files alongside originals");
3634 println!(" -d, --output-delim Delimiter for s///, tr///, m// (default: preserve original)");
3635 println!();
3636 println!("Examples:");
3637 println!(" stryke convert app.pl # print to stdout");
3638 println!(" stryke convert -i lib/*.pm # write lib/*.stk");
3639 println!(" stryke convert -d '|' app.pl # use | as delimiter: s|old|new|g");
3640 return 0;
3641 }
3642 s if s.starts_with('-') => {
3643 eprintln!("stryke convert: unknown option: {}", s);
3644 eprintln!("usage: stryke convert [-i] [-d DELIM] FILE...");
3645 return 2;
3646 }
3647 s => files.push(s.to_string()),
3648 }
3649 i += 1;
3650 }
3651 if files.is_empty() {
3652 eprintln!("stryke convert: no input files");
3653 eprintln!("usage: stryke convert [-i] [-d DELIM] FILE...");
3654 return 2;
3655 }
3656 let opts = crate::convert::ConvertOptions {
3657 output_delim,
3658 ..Default::default()
3659 };
3660 let mut errors = 0;
3661 for f in &files {
3662 let code = match std::fs::read_to_string(f) {
3663 Ok(c) => c,
3664 Err(e) => {
3665 eprintln!("stryke convert: {}: {}", f, e);
3666 errors += 1;
3667 continue;
3668 }
3669 };
3670 let program = match crate::parse_with_file(&code, f) {
3671 Ok(p) => p,
3672 Err(e) => {
3673 eprintln!("stryke convert: {}: {}", f, e);
3674 errors += 1;
3675 continue;
3676 }
3677 };
3678 let converted = crate::convert_to_stryke_with_options(&program, &opts);
3679 if in_place {
3680 let out_path = std::path::Path::new(f).with_extension("pr");
3681 if let Err(e) = std::fs::write(&out_path, &converted) {
3682 eprintln!("stryke convert: {}: {}", out_path.display(), e);
3683 errors += 1;
3684 }
3685 } else {
3686 println!("{}", converted);
3687 }
3688 }
3689 if errors > 0 {
3690 1
3691 } else {
3692 0
3693 }
3694}
3695
3696fn run_serve_subcommand(args: &[String]) -> i32 {
3700 if !args.is_empty() && (args[0] == "-h" || args[0] == "--help") {
3701 eprintln!("usage: stryke serve [PORT] [SCRIPT | -e CODE]");
3702 eprintln!();
3703 eprintln!(" stryke serve serve $PWD on port 8000");
3704 eprintln!(" stryke serve PORT serve $PWD as static files");
3705 eprintln!(" stryke serve PORT SCRIPT run script (must call serve())");
3706 eprintln!(" stryke serve PORT -e CODE one-liner handler");
3707 eprintln!();
3708 eprintln!(" Handler receives $req (hashref: method, path, query, headers, body, peer)");
3709 eprintln!(" and returns: string (200 OK), key-value pairs, hashref, or undef (404).");
3710 eprintln!();
3711 eprintln!("examples:");
3712 eprintln!(
3713 " stryke serve # static file server on 8000"
3714 );
3715 eprintln!(
3716 " stryke serve 8080 # static file server"
3717 );
3718 eprintln!(" stryke serve 8080 app.stk # script handler");
3719 eprintln!(" stryke serve 3000 -e '\"hello \" . $req->{{path}}' # one-liner");
3720 eprintln!(" stryke serve 8080 -e 'status => 200, body => json_encode(+{{ok => 1}})'");
3721 return 0;
3722 }
3723
3724 let (port, rest) = if !args.is_empty() && args[0].parse::<u16>().is_ok() {
3726 (args[0].clone(), &args[1..])
3727 } else {
3728 ("8000".to_string(), args)
3729 };
3730
3731 let static_dir = if rest.is_empty() {
3733 Some(
3734 std::env::current_dir()
3735 .unwrap_or_default()
3736 .to_string_lossy()
3737 .to_string(),
3738 )
3739 } else if rest[0] != "-e" && Path::new(&rest[0]).is_dir() {
3740 Some(
3741 std::fs::canonicalize(&rest[0])
3742 .unwrap_or_else(|_| PathBuf::from(&rest[0]))
3743 .to_string_lossy()
3744 .to_string(),
3745 )
3746 } else {
3747 None
3748 };
3749
3750 let code = if let Some(dir) = static_dir {
3751 let dir_escaped = dir.replace('\\', "\\\\").replace('"', "\\\"");
3752 eprintln!("stryke: serving {} on http://0.0.0.0:{}", dir, port);
3753 format!(
3754 r#"
3755chdir "{dir_escaped}"
3756
3757my %mime = (
3758 html => "text/html; charset=utf-8",
3759 htm => "text/html; charset=utf-8",
3760 css => "text/css; charset=utf-8",
3761 js => "application/javascript; charset=utf-8",
3762 mjs => "application/javascript; charset=utf-8",
3763 json => "application/json; charset=utf-8",
3764 xml => "text/xml; charset=utf-8",
3765 md => "text/markdown; charset=utf-8",
3766 txt => "text/plain; charset=utf-8",
3767 toml => "application/toml; charset=utf-8",
3768 pl => "text/x-perl; charset=utf-8",
3769 pr => "text/x-perl; charset=utf-8",
3770 pm => "text/x-perl; charset=utf-8",
3771 png => "image/png",
3772 jpg => "image/jpeg",
3773 jpeg => "image/jpeg",
3774 gif => "image/gif",
3775 svg => "image/svg+xml",
3776 webp => "image/webp",
3777 avif => "image/avif",
3778 ico => "image/x-icon",
3779 woff2 => "font/woff2",
3780 woff => "font/woff",
3781 ttf => "font/ttf",
3782 mp3 => "audio/mpeg",
3783 ogg => "audio/ogg",
3784 mp4 => "video/mp4",
3785 webm => "video/webm",
3786 zip => "application/zip",
3787 gz => "application/gzip",
3788 wasm => "application/wasm",
3789 pdf => "application/pdf"
3790)
3791
3792fn mime_for($path) {{
3793 my $ext = $path =~ /\.([^.]+)$/ ? lc($1) : ""
3794 $mime{{$ext}} // "text/plain"
3795}}
3796
3797fn dir_listing($url_path, $fs_path) {{
3798 $url_path .= "/" unless $url_path =~ m|/$|
3799 my $prefix = $fs_path eq "." ? "" : "$fs_path/"
3800 my @entries
3801 push @entries, ".." unless $url_path eq "/"
3802 push @entries, dirs($fs_path)
3803 push @entries, filesf($fs_path)
3804 my $html = ""
3805 for my $e (@entries) {{
3806 my $full = $e eq ".." ? ".." : "$prefix$e"
3807 my $name = $e
3808 my $href = $url_path . $name
3809 if (-d $full) {{
3810 $html .= "<li class=\"dir\"><a href=\"$href/\">$name/</a></li>"
3811 }} else {{
3812 my $sz = (stat($full))[7] // 0
3813 $html .= "<li><a href=\"$href\">$name</a> <span style=\"color:#888\">($sz bytes)</span></li>"
3814 }}
3815 }}
3816 "<!DOCTYPE html><html><head><meta charset=\"utf-8\">"
3817 . "<title>Directory listing for $url_path</title>"
3818 . "<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>"
3819 . "</head><body><h1>Directory listing for $url_path</h1><hr><ul>"
3820 . $html
3821 . "</ul><hr><p style=\"color:#888\">stryke/{port}</p></body></html>"
3822}}
3823
3824serve {port}, fn ($req) {{
3825 my $url_path = $req->{{path}}
3826 $url_path =~ s|\.\./||g
3827 my $fs_path = $url_path =~ s|^/||r
3828 $fs_path = "." if $fs_path eq ""
3829
3830 if (-d $fs_path) {{
3831 my $idx = $fs_path eq "." ? "index.html" : "$fs_path/index.html"
3832 if (-f $idx) {{
3833 +{{ status => 200, body => cat($idx), headers => +{{ "content-type" => "text/html; charset=utf-8" }} }}
3834 }} else {{
3835 +{{ status => 200, body => dir_listing($url_path, $fs_path), headers => +{{ "content-type" => "text/html; charset=utf-8" }} }}
3836 }}
3837 }} elsif (-f $fs_path) {{
3838 +{{ status => 200, body => cat($fs_path), headers => +{{ "content-type" => mime_for($fs_path) }} }}
3839 }} else {{
3840 +{{ status => 404, body => "404 Not Found: $url_path\n" }}
3841 }}
3842}}
3843"#
3844 )
3845 } else if rest[0] == "-e" {
3846 if rest.len() < 2 {
3847 eprintln!("stryke serve: -e requires an argument");
3848 return 1;
3849 }
3850 let handler_body = rest[1..].join(" ");
3851 format!("serve {}, fn ($req) {{ {} }}", port, handler_body)
3852 } else {
3853 let script_path = &rest[0];
3856 match std::fs::read_to_string(script_path) {
3857 Ok(src) => {
3858 format!("$ENV{{STRYKE_PORT}} = {}\n{}", port, src)
3859 }
3860 Err(e) => {
3861 eprintln!("stryke serve: {}: {}", script_path, e);
3862 return 1;
3863 }
3864 }
3865 };
3866
3867 let mut interp = crate::vm_helper::VMHelper::new();
3868 match crate::parse_and_run_string(&code, &mut interp) {
3869 Ok(_) => 0,
3870 Err(e) => {
3871 if let crate::error::ErrorKind::Exit(code) = e.kind {
3872 return code;
3873 }
3874 eprintln!("{}", e);
3875 255
3876 }
3877 }
3878}
3879
3880#[allow(non_snake_case)]
3881fn run_doc_subcommand(args: &[String]) -> i32 {
3889 let theme = DocTheme {
3890 C: "\x1b[36m",
3891 G: "\x1b[32m",
3892 Y: "\x1b[1;33m",
3893 M: "\x1b[35m",
3894 B: "\x1b[1m",
3895 D: "\x1b[2m",
3896 N: "\x1b[0m",
3897 };
3898 let DocTheme {
3899 C,
3900 G,
3901 Y,
3902 M,
3903 B,
3904 D,
3905 N,
3906 } = theme;
3907
3908 let mut entries: Vec<(&str, &str, String)> = Vec::new();
3932 let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
3933 for &(chapter, topics) in crate::lsp::DOC_CATEGORIES {
3934 for &t in topics {
3935 if seen.contains(t) {
3936 continue;
3937 }
3938 if let Some(text) = crate::lsp::doc_text_for(t) {
3939 let rendered = render_page_content(t, text, C, G, D, N);
3940 entries.push((chapter, t, rendered));
3941 seen.insert(t);
3942 }
3943 }
3944 }
3945 let mut leftover: Vec<(&'static str, &'static str)> = crate::builtins::category_map_iter()
3956 .filter(|(name, _)| !seen.contains(name))
3957 .map(|(name, cat)| {
3958 let chapter = if cat.is_empty() { "Other" } else { cat };
3959 (chapter, name)
3960 })
3961 .collect();
3962 leftover.sort_by(|a, b| a.0.cmp(b.0).then(a.1.cmp(b.1)));
3963 for (chapter, name) in leftover {
3964 if seen.contains(name) {
3965 continue;
3966 }
3967 if let Some(text) = crate::lsp::doc_text_for(name) {
3968 let rendered = render_page_content(name, text, C, G, D, N);
3969 entries.push((chapter, name, rendered));
3970 seen.insert(name);
3971 }
3972 }
3973 for topic in crate::lsp::doc_topics() {
3978 if seen.contains(topic) {
3979 continue;
3980 }
3981 if crate::builtins::is_callable_spelling(topic) {
3984 continue;
3985 }
3986 if topic.starts_with("CORE::") || topic.starts_with("main::") {
3996 continue;
3997 }
3998 if let Some(text) = crate::lsp::doc_text_for(topic) {
3999 let rendered = render_page_content(topic, text, C, G, D, N);
4000 entries.push(("Other", topic, rendered));
4001 seen.insert(topic);
4002 }
4003 }
4004 if entries.is_empty() {
4005 eprintln!("stryke docs: no documentation pages found");
4006 return 1;
4007 }
4008
4009 let content_area = term_height().saturating_sub(14).max(4);
4012 let mut pages = build_fixed_pages(&entries, content_area);
4013
4014 let entry_count = entries.len();
4019 let mut chapter_counts: Vec<(&str, usize)> = Vec::new();
4020 for (cat, _, _) in &entries {
4021 if let Some(slot) = chapter_counts.iter_mut().find(|(c, _)| *c == *cat) {
4022 slot.1 += 1;
4023 } else {
4024 chapter_counts.push((*cat, 1));
4025 }
4026 }
4027 let chapter_count = chapter_counts.len();
4028 let mut intro = format!(
4029 "\
4030 {D}>> THE STRYKE ENCYCLOPEDIA // INTERACTIVE REFERENCE SYSTEM <<{N}\n\
4031\n\
4032 {B}A comprehensive reference for every stryke builtin, keyword,{N}\n\
4033 {B}and extension. {G}{entry_count}{N} {B}topics across {G}{chapter_count}{N} {B}chapters.{N}\n\
4034\n\
4035 {D}── GETTING STARTED ─────────────────────────────────────────────{N}\n\
4036\n\
4037 {C}j{N} / {C}n{N} / {C}space{N} next page\n\
4038 {C}k{N} / {C}p{N} previous page\n\
4039 {C}]{N} / {C}[{N} next / previous chapter\n\
4040 {C}d{N} / {C}u{N} forward / back 5 pages\n\
4041 {C}g{N} / {C}G{N} first / last page\n\
4042 {C}t{N} table of contents\n\
4043 {C}/{N} search all pages\n\
4044 {C}:{N} jump to page number\n\
4045 {C}r{N} random page\n\
4046 {C}?{N} full keybinding help\n\
4047 {C}q{N} quit\n\
4048\n\
4049 {D}── CHAPTERS ───────────────────────────────────────────────────{N}\n\
4050"
4051 );
4052 let major_chapters: std::collections::HashSet<&str> =
4058 crate::lsp::DOC_CATEGORIES.iter().map(|(c, _)| *c).collect();
4059 let mut major_topics = 0usize;
4060 let mut minor_topics = 0usize;
4061 let mut minor_chapter_count = 0usize;
4062 let mut display_idx = 0usize;
4063 for (cat, count) in chapter_counts.iter() {
4064 if major_chapters.contains(cat) {
4065 display_idx += 1;
4066 major_topics += count;
4067 intro.push_str(&format!(
4068 " {C}{:>2}.{N} {B}{:<40}{N} {D}{} topics{N}\n",
4069 display_idx, cat, count,
4070 ));
4071 } else {
4072 minor_chapter_count += 1;
4073 minor_topics += count;
4074 }
4075 }
4076 if minor_chapter_count > 0 {
4077 intro.push_str(&format!(
4078 " {D}…{N} {B}{:<40}{N} {D}{} topics{N}\n",
4079 format!("+ {} more chapters", minor_chapter_count),
4080 minor_topics,
4081 ));
4082 }
4083 let _ = major_topics;
4084 intro.push_str(&format!(
4085 "\n {D}press {C}t{D} for full table of contents, {C}j{D} or {C}space{D} to begin >>>{N}\n"
4086 ));
4087 let intro_page = pad_to_height(&intro, content_area);
4089 pages.insert(0, ("Introduction".to_string(), intro_page, Vec::new()));
4090 let total = pages.len();
4091
4092 if args.first().map(|s| s.as_str()) == Some("-h")
4093 || args.first().map(|s| s.as_str()) == Some("--help")
4094 {
4095 println!();
4096 doc_print_banner(theme);
4097 doc_print_hline('┌', '┐', theme);
4098 doc_print_boxline(
4099 &format!(" {G}STATUS: ONLINE{N} {D}//{N} {C}SIGNAL: {G}████████{D}░░{N} {D}//{N} {M}STRYKE DOCS{N}"),
4100 theme,
4101 );
4102 doc_print_hline('└', '┘', theme);
4103 println!(" {D}>> THE STRYKE ENCYCLOPEDIA // INTERACTIVE REFERENCE SYSTEM <<{N}");
4104 println!();
4105 println!(" {B}USAGE:{N} stryke docs {D}[OPTIONS] [PAGE|TOPIC]{N}");
4106 println!();
4107 doc_print_separator("OPTIONS", theme);
4108 println!(" {C}-h, --help{N} {D}// Show this help{N}");
4109 println!(" {C}-t, --toc{N} {D}// Table of contents{N}");
4110 println!(" {C}-s, --search <pattern>{N} {D}// Search pages{N}");
4111 println!(" {C}-l, --list{N} {D}// List all pages (one per primary + keywords){N}");
4112 println!(" {C}-L, --list-all{N} {D}// Every callable spelling (primaries + aliases + CORE::*, main::*) — for shell tab-complete{N}");
4113 println!(
4114 " {C}TOPIC{N} {D}// Jump to topic (stryke docs pmap){N}"
4115 );
4116 println!(" {C}PAGE{N} {D}// Jump to page number{N}");
4117 println!();
4118 doc_print_separator("NAVIGATION (vim-style)", theme);
4119 println!(" {C}j / n / l / enter / space{N} {D}// Next page{N}");
4120 println!(" {C}k / p / h{N} {D}// Previous page{N}");
4121 println!(" {C}d{N} {D}// Forward 5 pages{N}");
4122 println!(" {C}u{N} {D}// Back 5 pages{N}");
4123 println!(" {C}g / 0{N} {D}// First page{N}");
4124 println!(" {C}G / ${N} {D}// Last page{N}");
4125 println!(" {C}] / }}{N} {D}// Next chapter{N}");
4126 println!(" {C}[ / {{{N} {D}// Previous chapter{N}");
4127 println!(" {C}t{N} {D}// Table of contents{N}");
4128 println!(" {C}/ <pattern>{N} {D}// Search pages{N}");
4129 println!(" {C}:<number>{N} {D}// Jump to page{N}");
4130 println!(" {C}r{N} {D}// Random page{N}");
4131 println!(" {C}?{N} {D}// Keybinding help{N}");
4132 println!(" {C}q{N} {D}// Quit{N}");
4133 println!();
4134 doc_print_separator("EXAMPLES", theme);
4135 println!(" {C}stryke docs{N} {D}// start from page 1{N}");
4136 println!(" {C}stryke docs --toc{N} {D}// table of contents{N}");
4137 println!(" {C}stryke docs 42{N} {D}// jump to page 42{N}");
4138 println!(" {C}stryke docs pmap{N} {D}// jump to pmap{N}");
4139 println!(" {C}stryke docs --search parallel{N} {D}// find parallel pages{N}");
4140 println!();
4141 return 0;
4142 }
4143
4144 if args.first().map(|s| s.as_str()) == Some("-t")
4146 || args.first().map(|s| s.as_str()) == Some("--toc")
4147 {
4148 doc_print_toc_entries(&entries, &pages, theme);
4149 return 0;
4150 }
4151
4152 if args.first().map(|s| s.as_str()) == Some("-l")
4156 || args.first().map(|s| s.as_str()) == Some("--list")
4157 {
4158 for (i, (_, topic, _)) in entries.iter().enumerate() {
4159 println!("{:>3}. {}", i + 1, topic);
4160 }
4161 return 0;
4162 }
4163
4164 if args.first().map(|s| s.as_str()) == Some("--list-all")
4178 || args.first().map(|s| s.as_str()) == Some("-L")
4179 {
4180 let mut all: std::collections::BTreeSet<String> = std::collections::BTreeSet::new();
4181 for (_, topic, _) in &entries {
4183 all.insert(topic.to_string());
4184 }
4185 for (name, _) in crate::builtins::category_map_iter() {
4187 all.insert(name.to_string());
4188 }
4189 for entry in crate::builtins::aliases_hash_map().keys() {
4190 all.insert(entry.clone());
4191 }
4192 let bare_callables: Vec<String> = crate::builtins::category_map_iter()
4194 .map(|(n, _)| n.to_string())
4195 .chain(
4196 crate::builtins::aliases_hash_map()
4197 .keys()
4198 .cloned()
4199 .collect::<Vec<_>>(),
4200 )
4201 .collect();
4202 for n in &bare_callables {
4203 all.insert(format!("CORE::{n}"));
4204 all.insert(format!("main::{n}"));
4205 }
4206 for line in include_str!("lsp_completion_words.txt").lines() {
4210 let line = line.trim();
4211 if line.is_empty() || line.starts_with('#') {
4212 continue;
4213 }
4214 all.insert(line.to_string());
4218 }
4219 for (i, name) in all.iter().enumerate() {
4220 println!("{:>5}. {}", i + 1, name);
4221 }
4222 return 0;
4223 }
4224
4225 if (args.first().map(|s| s.as_str()) == Some("-s")
4227 || args.first().map(|s| s.as_str()) == Some("--search"))
4228 && args.len() >= 2
4229 {
4230 let pat = args[1].to_lowercase();
4231 let mut found = 0;
4232 for (i, (cat, topic, text)) in entries.iter().enumerate() {
4233 if topic.to_lowercase().contains(&pat)
4234 || cat.to_lowercase().contains(&pat)
4235 || text.to_lowercase().contains(&pat)
4236 {
4237 println!(" {C}{:>3}.{N} {B}{}{N} {D}({}){N}", i + 1, topic, cat);
4238 found += 1;
4239 }
4240 }
4241 if found == 0 {
4242 println!(" {Y}no results for '{}'{N}", pat);
4243 }
4244 return 0;
4245 }
4246
4247 let mut start_page: usize = 0;
4253 let mut topic_entry_idx: Option<usize> = None;
4254 if !args.is_empty() {
4255 let arg = &args[0];
4256 if let Ok(n) = arg.parse::<usize>() {
4258 if n >= 1 && n <= total {
4259 start_page = n - 1;
4260 }
4261 } else {
4262 let resolved: String = if let Some(rest) = arg.strip_prefix("CORE::") {
4277 rest.to_string()
4278 } else if let Some(rest) = arg.strip_prefix("main::") {
4279 rest.to_string()
4280 } else if let Some(rest) = arg.strip_prefix("crate::") {
4281 let sigil = format!("%crate::{}", rest);
4290 let sigil_low = sigil.to_lowercase();
4291 if entries
4292 .iter()
4293 .any(|(_, t, _)| t.to_lowercase() == sigil_low)
4294 {
4295 sigil
4296 } else {
4297 rest.to_string()
4298 }
4299 } else {
4300 arg.clone()
4301 };
4302 let lower = resolved.to_lowercase();
4303 let entry_idx = entries
4304 .iter()
4305 .position(|(_, t, _)| t.to_lowercase() == lower)
4306 .or_else(|| {
4307 if let Some(primary) = crate::builtins::primary_for_alias(&resolved) {
4313 let lp = primary.to_lowercase();
4314 if let Some(i) = entries.iter().position(|(_, t, _)| t.to_lowercase() == lp)
4315 {
4316 return Some(i);
4317 }
4318 }
4319 None
4320 })
4321 .or_else(|| {
4322 let sigil_form = format!("%{}", resolved);
4326 let lp = sigil_form.to_lowercase();
4327 entries.iter().position(|(_, t, _)| t.to_lowercase() == lp)
4328 })
4329 .or_else(|| {
4330 if lower.len() < 3 {
4336 return None;
4337 }
4338 entries
4339 .iter()
4340 .position(|(_, t, _)| t.to_lowercase().starts_with(&lower))
4341 });
4342 match entry_idx {
4343 Some(eidx) => {
4344 start_page = pages
4346 .iter()
4347 .position(|(_, _, indices)| indices.contains(&eidx))
4348 .unwrap_or(0);
4349 topic_entry_idx = Some(eidx);
4350 }
4351 None => {
4352 if let Some(text) = crate::lsp::doc_text_for(&resolved) {
4357 let rendered = render_page_content(&resolved, text, C, G, D, N);
4358 print!("{}", rendered);
4359 if !rendered.ends_with('\n') {
4360 println!();
4361 }
4362 return 0;
4363 }
4364 eprintln!("stryke docs: no documentation for '{}'", arg);
4365 eprintln!("run 'stryke docs -h' for help");
4366 return 1;
4367 }
4368 }
4369 }
4370 }
4371
4372 let target_specified = !args.is_empty();
4382 let no_tty_env =
4383 std::env::var_os("STRYKE_NO_TTY").is_some() || std::env::var_os("NO_TTY").is_some();
4384 let interactive_ok =
4385 !target_specified && io::stdout().is_terminal() && io::stdin().is_terminal() && !no_tty_env;
4386 if !interactive_ok {
4387 if let Some(eidx) = topic_entry_idx {
4392 print!("{}", entries[eidx].2);
4393 if !entries[eidx].2.ends_with('\n') {
4396 println!();
4397 }
4398 } else {
4399 print!("{}", pages[start_page].1);
4400 }
4401 return 0;
4402 }
4403
4404 doc_interactive_loop(&pages, &entries, &intro, start_page, total, theme)
4405}
4406
4407fn pad_to_height(text: &str, height: usize) -> String {
4409 let lines: Vec<&str> = text.lines().collect();
4410 let mut buf: Vec<&str> = Vec::with_capacity(height);
4411 for line in lines.iter().take(height) {
4412 buf.push(line);
4413 }
4414 while buf.len() < height {
4415 buf.push("");
4416 }
4417 buf.join("\r\n")
4418}
4419
4420fn build_fixed_pages(
4424 entries: &[(&str, &str, String)],
4425 max_lines: usize,
4426) -> Vec<(String, String, Vec<usize>)> {
4427 let mut pages: Vec<(String, String, Vec<usize>)> = Vec::new();
4428 let mut i = 0;
4429 while i < entries.len() {
4430 let cat = entries[i].0.to_string();
4431 let mut end = (i + 2).min(entries.len());
4433 if end < entries.len() && entries[end].0 == cat {
4435 let lines: usize = (i..=end).map(|j| entries[j].2.lines().count() + 1).sum();
4436 if lines <= max_lines {
4437 end += 1;
4438 }
4439 }
4440 if let Some(pos) = entries[i + 1..end].iter().position(|e| e.0 != cat) {
4442 end = i + 1 + pos;
4443 }
4444 let mut buf = String::new();
4445 let mut indices = Vec::new();
4446 for (j, entry) in entries.iter().enumerate().take(end).skip(i) {
4447 if j > i {
4448 buf.push('\n');
4449 }
4450 buf.push_str(&entry.2);
4451 indices.push(j);
4452 }
4453 pages.push((cat, buf, indices));
4454 i = end;
4455 }
4456 pages
4457}
4458
4459fn find_page_for_entry(pages: &[(String, String, Vec<usize>)], entry_idx: usize) -> usize {
4461 for (pi, (_cat, _content, indices)) in pages.iter().enumerate() {
4462 if indices.contains(&entry_idx) {
4463 return pi;
4464 }
4465 }
4466 0
4467}
4468
4469static SIGWINCH_RECEIVED: std::sync::atomic::AtomicBool = std::sync::atomic::AtomicBool::new(false);
4471
4472#[cfg(unix)]
4474extern "C" fn sigwinch_handler(_sig: libc::c_int) {
4475 SIGWINCH_RECEIVED.store(true, std::sync::atomic::Ordering::Relaxed);
4476}
4477
4478#[allow(non_snake_case)]
4479#[derive(Clone, Copy)]
4480struct DocTheme<'a> {
4481 C: &'a str,
4482 G: &'a str,
4483 Y: &'a str,
4484 M: &'a str,
4485 B: &'a str,
4486 D: &'a str,
4487 N: &'a str,
4488}
4489
4490#[cfg(unix)]
4492fn doc_interactive_loop(
4493 pages: &[(String, String, Vec<usize>)],
4494 entries: &[(&str, &str, String)],
4495 intro_raw: &str,
4496 start: usize,
4497 total: usize,
4498 theme: DocTheme,
4499) -> i32 {
4500 let DocTheme {
4501 C, G, M, B, D, N, ..
4502 } = theme;
4503 use std::os::unix::io::AsRawFd;
4504
4505 let stdin_fd = io::stdin().as_raw_fd();
4506 let mut old_termios: libc::termios = unsafe { std::mem::zeroed() };
4508 unsafe { libc::tcgetattr(stdin_fd, &mut old_termios) };
4509 let mut raw = old_termios;
4510 unsafe { libc::cfmakeraw(&mut raw) };
4511 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
4512
4513 let old_sigwinch = unsafe {
4515 libc::signal(
4516 libc::SIGWINCH,
4517 sigwinch_handler as *const () as libc::sighandler_t,
4518 )
4519 };
4520
4521 let mut pages = pages.to_vec();
4523 let mut total = total;
4524 let mut current: usize = start;
4525
4526 macro_rules! rprint {
4528 () => { print!("\r\n"); };
4529 ($($arg:tt)*) => { print!("{}\r\n", format!($($arg)*)); };
4530 }
4531
4532 let render = |cur: usize, pages: &[(String, String, Vec<usize>)], total: usize| {
4533 let (ref cat, ref content, ref indices) = pages[cur];
4534 let topic_list: String = indices
4536 .iter()
4537 .take(3)
4538 .map(|&i| entries[i].1)
4539 .collect::<Vec<_>>()
4540 .join(", ");
4541 let topic_display = if indices.len() > 3 {
4542 format!("{} +{}", topic_list, indices.len() - 3)
4543 } else {
4544 topic_list
4545 };
4546 let term_h = term_height();
4547
4548 print!("\x1b[H\x1b[2J");
4550
4551 print!("\x1b[1;1H"); rprint!();
4554 rprint!(" {C}███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗{N}");
4555 rprint!(" {C}██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝{N}");
4556 rprint!(" {M}███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗ {N}");
4557 rprint!(" {M}╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝ {N}");
4558 rprint!(" {C}███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗{N}");
4559 rprint!(" {C}╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝{N}");
4560 print!(" {D}┌");
4562 for _ in 0..74 {
4563 print!("─");
4564 }
4565 print!("┐{N}\r\n");
4566 let status = format!(
4567 " {G}{:>3}/{}{N} {D}//{N} {C}{}{N} {D}//{N} {M}{}{N}",
4568 cur + 1,
4569 total,
4570 topic_display,
4571 cat,
4572 );
4573 let vis_len = strip_ansi_len(&status);
4574 let pad = 74_usize.saturating_sub(vis_len);
4575 print!(" {D}│{N}{status}{:>pad$}{D}│{N}\r\n", "", pad = pad);
4576 print!(" {D}└");
4577 for _ in 0..74 {
4578 print!("─");
4579 }
4580 print!("┘{N}\r\n");
4581
4582 let content_start = 12;
4584 let footer_rows = 3; let max_content = if term_h > content_start + footer_rows {
4586 term_h - content_start - footer_rows
4587 } else {
4588 1
4589 };
4590 print!("\x1b[{};1H", content_start);
4591 for (li, line) in content.lines().enumerate() {
4592 if li >= max_content {
4593 break; }
4595 print!("{line}\r\n");
4596 }
4597
4598 print!("\x1b[{};1H", term_h - 2);
4600 print!(" {D}");
4601 for _ in 0..76 {
4602 print!("─");
4603 }
4604 print!("{N}\r\n");
4605 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");
4606 print!(" {D}>>>{N} ");
4607 let _ = io::stdout().flush();
4608 };
4609
4610 render(current, &pages, total);
4611
4612 loop {
4613 let mut buf = [0u8; 1];
4614 let nread = unsafe { libc::read(stdin_fd, buf.as_mut_ptr() as *mut libc::c_void, 1) };
4615 if nread != 1 {
4616 if SIGWINCH_RECEIVED.swap(false, std::sync::atomic::Ordering::Relaxed) {
4618 let entry_idx = pages[current].2.first().copied().unwrap_or(0);
4619 let th = term_height();
4620 let content_area = th.saturating_sub(14).max(4);
4621 let mut rebuilt = build_fixed_pages(entries, content_area);
4622 let intro_page = pad_to_height(intro_raw, content_area);
4623 rebuilt.insert(0, ("Introduction".to_string(), intro_page, Vec::new()));
4624 pages = rebuilt;
4625 total = pages.len();
4626 current = if entry_idx == 0 && current == 0 {
4627 0
4628 } else {
4629 find_page_for_entry(&pages, entry_idx).min(total - 1)
4630 };
4631 render(current, &pages, total);
4632 continue;
4633 }
4634 break;
4635 }
4636 let key = buf[0];
4637 match key {
4638 b'j' | b'n' | b'l' | b' ' | b'\n' | b'\r' if current < total - 1 => {
4640 current += 1;
4641 }
4642 b'k' | b'p' | b'h' => {
4644 current = current.saturating_sub(1);
4645 }
4646 b'g' | b'0' => current = 0,
4648 b'G' | b'$' => current = total - 1,
4650 b'd' => {
4652 current = (current + 5).min(total - 1);
4653 }
4654 b'u' => {
4656 current = current.saturating_sub(5);
4657 }
4658 b']' | b'}' => {
4660 let cur_cat = &pages[current].0;
4661 while current < total - 1 {
4662 current += 1;
4663 if pages[current].0 != *cur_cat {
4664 break;
4665 }
4666 }
4667 }
4668 b'[' | b'{' => {
4670 let cur_cat = pages[current].0.clone();
4671 while current > 0 {
4672 current -= 1;
4673 if pages[current].0 != cur_cat {
4674 break;
4675 }
4676 }
4677 }
4678 b'r' => {
4680 current = rand::thread_rng().gen_range(0..total);
4681 }
4682 b't' => {
4684 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
4686 print!("\x1b[H\x1b[2J");
4687 doc_print_toc_entries(entries, &pages, theme);
4688 print!(" {D}enter page number or press enter to return >>>{N} ");
4689 let _ = io::stdout().flush();
4690 let mut line = String::new();
4691 let _ = io::stdin().read_line(&mut line);
4692 if let Ok(n) = line.trim().parse::<usize>() {
4693 if n >= 1 && n <= total {
4694 current = n - 1;
4695 }
4696 }
4697 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
4698 }
4699 b'/' => {
4701 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
4702 print!("\r {C}/{N}");
4703 let _ = io::stdout().flush();
4704 let mut line = String::new();
4705 let _ = io::stdin().read_line(&mut line);
4706 let pat = line.trim().to_lowercase();
4707 if !pat.is_empty() {
4708 let start_from = (current + 1) % total;
4710 let mut found = false;
4711 for i in 0..total {
4712 let idx = (start_from + i) % total;
4713 let (ref cat, ref content, _) = pages[idx];
4714 if cat.to_lowercase().contains(&pat)
4715 || content.to_lowercase().contains(&pat)
4716 {
4717 current = idx;
4718 found = true;
4719 break;
4720 }
4721 }
4722 let _ = found; }
4724 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
4725 }
4726 b':' => {
4728 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
4729 print!("\r {C}:{N}");
4730 let _ = io::stdout().flush();
4731 let mut line = String::new();
4732 let _ = io::stdin().read_line(&mut line);
4733 if let Ok(n) = line.trim().parse::<usize>() {
4734 if n >= 1 && n <= total {
4735 current = n - 1;
4736 }
4737 }
4738 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &raw) };
4739 }
4740 b'?' => {
4742 print!("\x1b[H\x1b[2J");
4743 rprint!();
4744 rprint!(" {D}── KEYBINDINGS ────────────────────────────────────────────────────{N}");
4745 rprint!();
4746 rprint!(" {B}Navigation{N}");
4747 rprint!(" {C}j n l space enter{N} {D}next page{N}");
4748 rprint!(" {C}k p h{N} {D}previous page{N}");
4749 rprint!(" {C}d{N} {D}forward 5 pages{N}");
4750 rprint!(" {C}u{N} {D}back 5 pages{N}");
4751 rprint!(" {C}g 0{N} {D}first page{N}");
4752 rprint!(" {C}G ${N} {D}last page{N}");
4753 rprint!(" {C}] }}{N} {D}next chapter{N}");
4754 rprint!(" {C}[ {{{N} {D}previous chapter{N}");
4755 rprint!();
4756 rprint!(" {B}Search & Jump{N}");
4757 rprint!(" {C}/{N} {D}search pages{N}");
4758 rprint!(" {C}:{N} {D}go to page number{N}");
4759 rprint!(" {C}t{N} {D}table of contents{N}");
4760 rprint!(" {C}r{N} {D}random page{N}");
4761 rprint!();
4762 rprint!(" {B}Other{N}");
4763 rprint!(" {C}?{N} {D}this help{N}");
4764 rprint!(" {C}q Q{N} {D}quit{N}");
4765 rprint!();
4766 rprint!(" {D}press any key to return{N}");
4767 let _ = io::stdout().flush();
4768 let mut b2 = [0u8; 1];
4769 let _ = unsafe { libc::read(stdin_fd, b2.as_mut_ptr() as *mut _, 1) };
4770 }
4771 b'q' | b'Q' | 0x03 => {
4773 break;
4774 }
4775 _ => {}
4776 }
4777 render(current, &pages, total);
4778 }
4779
4780 unsafe { libc::signal(libc::SIGWINCH, old_sigwinch) };
4782 unsafe { libc::tcsetattr(stdin_fd, libc::TCSANOW, &old_termios) };
4783 print!("\x1b[H\x1b[2J");
4784 let _ = io::stdout().flush();
4785 0
4786}
4787
4788#[cfg(not(unix))]
4789fn doc_interactive_loop(
4790 pages: &[(String, String, Vec<usize>)],
4791 _entries: &[(&str, &str, String)],
4792 _intro_raw: &str,
4793 start: usize,
4794 _total: usize,
4795 _theme: DocTheme,
4796) -> i32 {
4797 print!("{}", pages[start].1);
4799 0
4800}
4801
4802fn term_height() -> usize {
4803 #[cfg(unix)]
4804 {
4805 let mut ws = libc::winsize {
4806 ws_row: 0,
4807 ws_col: 0,
4808 ws_xpixel: 0,
4809 ws_ypixel: 0,
4810 };
4811 if unsafe { libc::ioctl(2, libc::TIOCGWINSZ, &mut ws) } == 0 && ws.ws_row > 0 {
4812 return ws.ws_row as usize;
4813 }
4814 }
4815 24
4816}
4817
4818fn term_width() -> usize {
4819 #[cfg(unix)]
4820 {
4821 let mut ws = libc::winsize {
4822 ws_row: 0,
4823 ws_col: 0,
4824 ws_xpixel: 0,
4825 ws_ypixel: 0,
4826 };
4827 if unsafe { libc::ioctl(2, libc::TIOCGWINSZ, &mut ws) } == 0 && ws.ws_col > 0 {
4828 return ws.ws_col as usize;
4829 }
4830 }
4831 80
4832}
4833
4834fn strip_ansi_len(s: &str) -> usize {
4835 let mut len = 0;
4836 let mut in_esc = false;
4837 for c in s.chars() {
4838 if c == '\x1b' {
4839 in_esc = true;
4840 } else if in_esc {
4841 if c == 'm' {
4842 in_esc = false;
4843 }
4844 } else {
4845 len += 1;
4846 }
4847 }
4848 len
4849}
4850
4851fn doc_print_banner(theme: DocTheme) {
4852 let DocTheme { C, M, N, .. } = theme;
4853 println!(" {C}███████╗████████╗██████╗ ██╗ ██╗██╗ ██╗███████╗{N}");
4854 println!(" {C}██╔════╝╚══██╔══╝██╔══██╗╚██╗ ██╔╝██║ ██╔╝██╔════╝{N}");
4855 println!(" {M}███████╗ ██║ ██████╔╝ ╚████╔╝ █████╔╝ █████╗ {N}");
4856 println!(" {M}╚════██║ ██║ ██╔══██╗ ╚██╔╝ ██╔═██╗ ██╔══╝ {N}");
4857 println!(" {C}███████║ ██║ ██║ ██║ ██║ ██║ ██╗███████╗{N}");
4858 println!(" {C}╚══════╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝{N}");
4859}
4860
4861fn doc_print_hline(left: char, right: char, theme: DocTheme) {
4862 let DocTheme { D, N, .. } = theme;
4863 print!(" {D}{left}");
4864 for _ in 0..74 {
4865 print!("─");
4866 }
4867 println!("{right}{N}");
4868}
4869
4870fn doc_print_boxline(content: &str, theme: DocTheme) {
4871 let DocTheme { D, N, .. } = theme;
4872 let stripped = content
4874 .bytes()
4875 .fold((Vec::new(), false), |(mut acc, in_esc), b| {
4876 if b == 0x1b {
4877 (acc, true)
4878 } else if in_esc {
4879 (acc, b != b'm')
4880 } else {
4881 acc.push(b);
4882 (acc, false)
4883 }
4884 })
4885 .0;
4886 let visible = String::from_utf8_lossy(&stripped).chars().count();
4887 let inner: usize = 74;
4888 let pad = inner.saturating_sub(visible);
4889 println!(" {D}│{N}{content}{:>pad$}{D}│{N}", "", pad = pad);
4890}
4891
4892fn doc_print_separator(label: &str, theme: DocTheme) {
4893 let DocTheme { D, N, .. } = theme;
4894 let trail = 72usize.saturating_sub(label.len());
4895 print!(" {D}── {label} ");
4896 for _ in 0..trail {
4897 print!("─");
4898 }
4899 println!("{N}");
4900}
4901
4902fn doc_print_toc_entries(
4903 entries: &[(&str, &str, String)],
4904 pages: &[(String, String, Vec<usize>)],
4905 theme: DocTheme,
4906) {
4907 let DocTheme {
4908 C, G, M, B, D, N, ..
4909 } = theme;
4910 let topic_count = entries.len();
4911 let page_count = pages.len();
4912 println!();
4913 doc_print_banner(theme);
4914 doc_print_hline('┌', '┐', theme);
4915 doc_print_boxline(
4916 &format!(
4917 " {G}TABLE OF CONTENTS{N} {D}//{N} {C}{topic_count} topics, {page_count} pages{N} {D}//{N} {M}The stryke Encyclopedia{N}"
4918 ),
4919 theme,
4920 );
4921 doc_print_hline('└', '┘', theme);
4922 println!();
4923 let mut last_cat = "";
4924 for (entry_idx, (cat, topic, _)) in entries.iter().enumerate() {
4925 if *cat != last_cat {
4926 println!();
4927 println!(" {B}{cat}{N}");
4928 last_cat = cat;
4929 }
4930 let page_num = pages
4932 .iter()
4933 .position(|(_, _, indices)| indices.contains(&entry_idx))
4934 .map(|p| p + 1)
4935 .unwrap_or(0);
4936 println!(
4937 " {C}{:>3}.{N} {:<30} {D}p.{}{N}",
4938 entry_idx + 1,
4939 topic,
4940 page_num
4941 );
4942 }
4943 println!();
4944}
4945
4946fn word_wrap(text: &str, max_vis: usize) -> Vec<String> {
4950 if max_vis == 0 {
4951 return vec![text.to_string()];
4952 }
4953 let mut lines: Vec<String> = Vec::new();
4954 let mut cur = String::new();
4955 let mut vis = 0usize;
4956
4957 for word in text.split(' ') {
4958 let wvis = strip_ansi_len(word);
4959 if vis > 0 && vis + 1 + wvis > max_vis {
4960 lines.push(cur);
4962 cur = word.to_string();
4963 vis = wvis;
4964 } else {
4965 if vis > 0 {
4966 cur.push(' ');
4967 vis += 1;
4968 }
4969 cur.push_str(word);
4970 vis += wvis;
4971 }
4972 }
4973 if !cur.is_empty() || lines.is_empty() {
4974 lines.push(cur);
4975 }
4976 lines
4977}
4978
4979#[allow(non_snake_case)]
4983fn render_page_content(topic: &str, text: &str, C: &str, G: &str, D: &str, N: &str) -> String {
4984 let max_vis = term_width().saturating_sub(4).max(40); let mut out = String::with_capacity(text.len() + 512);
4986 out.push_str(&format!(" {C}{topic}{N}\n"));
4987 out.push_str(&format!(
4988 " {D}{}{N}\n",
4989 "─".repeat(topic.len().max(20).min(max_vis))
4990 ));
4991 let mut in_code = false;
4992 for line in text.split('\n') {
4993 if line.starts_with("```") {
4994 in_code = !in_code;
4995 continue;
4996 }
4997 if in_code {
4998 out.push_str(&format!(" {G} {line}{N}\n"));
4999 } else if line.trim().is_empty() {
5000 out.push('\n');
5001 } else {
5002 let rendered = render_inline_code(line, C, N);
5003 for wrapped in word_wrap(&rendered, max_vis) {
5004 out.push_str(&format!(" {wrapped}\n"));
5005 }
5006 }
5007 }
5008 out
5009}
5010
5011fn render_inline_code(line: &str, color: &str, reset: &str) -> String {
5013 let mut out = String::with_capacity(line.len() + 64);
5014 let mut in_tick = false;
5015 for ch in line.chars() {
5016 if ch == '`' {
5017 if in_tick {
5018 out.push_str(reset);
5019 } else {
5020 out.push_str(color);
5021 }
5022 in_tick = !in_tick;
5023 } else {
5024 out.push(ch);
5025 }
5026 }
5027 out
5028}
5029
5030fn run_deconvert_subcommand(args: &[String]) -> i32 {
5032 let mut files: Vec<String> = Vec::new();
5033 let mut in_place = false;
5034 let mut output_delim: Option<char> = None;
5035 let mut i = 0;
5036 while i < args.len() {
5037 match args[i].as_str() {
5038 "-i" | "--in-place" => in_place = true,
5039 "-d" | "--output-delim" => {
5040 i += 1;
5041 if i >= args.len() {
5042 eprintln!("stryke deconvert: --output-delim requires an argument");
5043 return 2;
5044 }
5045 let delim_str = &args[i];
5046 if delim_str.chars().count() != 1 {
5047 eprintln!(
5048 "stryke deconvert: --output-delim must be a single character, got {:?}",
5049 delim_str
5050 );
5051 return 2;
5052 }
5053 output_delim = delim_str.chars().next();
5054 }
5055 "-h" | "--help" => {
5056 println!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
5057 println!();
5058 println!("Convert stryke .stk files back to standard Perl .pl syntax:");
5059 println!(" - Pipe chains and thread macros → nested function calls");
5060 println!(" - fn → sub");
5061 println!(" - p → say");
5062 println!(" - Adds trailing semicolons");
5063 println!(" - #!/usr/bin/env perl shebang prepended");
5064 println!();
5065 println!("Options:");
5066 println!(" -i, --in-place Write .pl files alongside originals");
5067 println!(" -d, --output-delim Delimiter for s///, tr///, m// (default: preserve original)");
5068 println!();
5069 println!("Examples:");
5070 println!(" stryke deconvert app.stk # print to stdout");
5071 println!(" stryke deconvert -i lib/*.stk # write lib/*.pl");
5072 println!(
5073 " stryke deconvert -d '|' app.stk # use | as delimiter: s|old|new|g"
5074 );
5075 return 0;
5076 }
5077 s if s.starts_with('-') => {
5078 eprintln!("stryke deconvert: unknown option: {}", s);
5079 eprintln!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
5080 return 2;
5081 }
5082 s => files.push(s.to_string()),
5083 }
5084 i += 1;
5085 }
5086 if files.is_empty() {
5087 eprintln!("stryke deconvert: no input files");
5088 eprintln!("usage: stryke deconvert [-i] [-d DELIM] FILE...");
5089 return 2;
5090 }
5091 let opts = crate::deconvert::DeconvertOptions { output_delim };
5092 let mut errors = 0;
5093 for f in &files {
5094 let code = match std::fs::read_to_string(f) {
5095 Ok(c) => c,
5096 Err(e) => {
5097 eprintln!("stryke deconvert: {}: {}", f, e);
5098 errors += 1;
5099 continue;
5100 }
5101 };
5102 let program = match crate::parse_with_file(&code, f) {
5103 Ok(p) => p,
5104 Err(e) => {
5105 eprintln!("stryke deconvert: {}: {}", f, e);
5106 errors += 1;
5107 continue;
5108 }
5109 };
5110 let deconverted = crate::deconvert_to_perl_with_options(&program, &opts);
5111 if in_place {
5112 let out_path = std::path::Path::new(f).with_extension("pl");
5113 if let Err(e) = std::fs::write(&out_path, &deconverted) {
5114 eprintln!("stryke deconvert: {}: {}", out_path.display(), e);
5115 errors += 1;
5116 }
5117 } else {
5118 println!("{}", deconverted);
5119 }
5120 }
5121 if errors > 0 {
5122 1
5123 } else {
5124 0
5125 }
5126}
5127
5128fn strip_shebang_and_extract(content: &str, extract: bool) -> String {
5130 if extract {
5131 let mut found = false;
5133 let mut lines = Vec::new();
5134 for line in content.lines() {
5135 if !found {
5136 if line.starts_with("#!") && line.contains("perl") {
5137 found = true;
5138 }
5140 continue;
5141 }
5142 if line == "__END__" || line == "__DATA__" {
5144 break;
5145 }
5146 lines.push(line);
5147 }
5148 lines.join("\n")
5149 } else if content.starts_with("#!") {
5150 if let Some(pos) = content.find('\n') {
5151 content[pos + 1..].to_string()
5152 } else {
5153 String::new()
5154 }
5155 } else {
5156 content.to_string()
5157 }
5158}
5159
5160fn run_fmt_subcommand(args: &[String]) -> i32 {
5162 let mut files: Vec<String> = Vec::new();
5163 let mut in_place = false;
5164 let mut i = 0;
5165 while i < args.len() {
5166 match args[i].as_str() {
5167 "-i" | "--in-place" => in_place = true,
5168 "-h" | "--help" => {
5169 println!("usage: stryke fmt [-i] FILE...");
5170 println!();
5171 println!("Format stryke source files (parse → pretty-print).");
5172 println!();
5173 println!("Options:");
5174 println!(" -i, --in-place Rewrite files in place (default: print to stdout)");
5175 println!();
5176 println!("Examples:");
5177 println!(" stryke fmt app.stk # print formatted source to stdout");
5178 println!(" stryke fmt -i lib/*.stk # rewrite files in place");
5179 println!(" stryke fmt -i . # format all .stk files recursively");
5180 return 0;
5181 }
5182 s if s.starts_with('-') => {
5183 eprintln!("stryke fmt: unknown option: {}", s);
5184 eprintln!("usage: stryke fmt [-i] FILE...");
5185 return 2;
5186 }
5187 s => files.push(s.to_string()),
5188 }
5189 i += 1;
5190 }
5191 if files.is_empty() {
5192 eprintln!("stryke fmt: no input files");
5193 eprintln!("usage: stryke fmt [-i] FILE...");
5194 return 2;
5195 }
5196 let mut expanded: Vec<String> = Vec::new();
5198 for f in &files {
5199 let p = std::path::Path::new(f);
5200 if p.is_dir() {
5201 collect_stryke_files(p, &mut expanded);
5202 } else {
5203 expanded.push(f.clone());
5204 }
5205 }
5206 if expanded.is_empty() {
5207 eprintln!("stryke fmt: no .stk/.pl/.pm files found");
5208 return 1;
5209 }
5210 let mut errors = 0;
5211 for f in &expanded {
5212 let code = match std::fs::read_to_string(f) {
5213 Ok(c) => c,
5214 Err(e) => {
5215 eprintln!("stryke fmt: {}: {}", f, e);
5216 errors += 1;
5217 continue;
5218 }
5219 };
5220 let program = match crate::parse_with_file(&code, f) {
5221 Ok(p) => p,
5222 Err(e) => {
5223 eprintln!("stryke fmt: {}: {}", f, e);
5224 errors += 1;
5225 continue;
5226 }
5227 };
5228 let formatted = crate::convert_to_stryke(&program);
5229 if in_place {
5230 if formatted == code {
5231 continue; }
5233 if let Err(e) = std::fs::write(f, &formatted) {
5234 eprintln!("stryke fmt: {}: {}", f, e);
5235 errors += 1;
5236 } else {
5237 eprintln!(" formatted {}", f);
5238 }
5239 } else {
5240 print!("{}", formatted);
5241 }
5242 }
5243 if errors > 0 {
5244 1
5245 } else {
5246 0
5247 }
5248}
5249
5250fn run_minify_subcommand(args: &[String]) -> i32 {
5254 let mut files: Vec<String> = Vec::new();
5255 let mut in_place = false;
5256 let mut i = 0;
5257 while i < args.len() {
5258 match args[i].as_str() {
5259 "-i" | "--in-place" => in_place = true,
5260 "-h" | "--help" => {
5261 println!("usage: stryke minify [-i] FILE...");
5262 println!();
5263 println!("Minify stryke source: strip comments / POD / blank lines, collapse");
5264 println!("statements onto a single line with `;` separators. Output parses");
5265 println!("identically to the input.");
5266 println!();
5267 println!("Options:");
5268 println!(" -i, --in-place Rewrite files in place (default: print to stdout)");
5269 println!();
5270 println!("Examples:");
5271 println!(" stryke minify app.stk # print minified source to stdout");
5272 println!(" stryke minify -i lib/*.stk # rewrite files in place");
5273 println!(
5274 " stryke minify -i . # minify all .stk files recursively"
5275 );
5276 return 0;
5277 }
5278 s if s.starts_with('-') => {
5279 eprintln!("stryke minify: unknown option: {}", s);
5280 eprintln!("usage: stryke minify [-i] FILE...");
5281 return 2;
5282 }
5283 s => files.push(s.to_string()),
5284 }
5285 i += 1;
5286 }
5287 if files.is_empty() {
5288 eprintln!("stryke minify: no input files");
5289 eprintln!("usage: stryke minify [-i] FILE...");
5290 return 2;
5291 }
5292 let mut expanded: Vec<String> = Vec::new();
5293 for f in &files {
5294 let p = std::path::Path::new(f);
5295 if p.is_dir() {
5296 collect_stryke_files(p, &mut expanded);
5297 } else {
5298 expanded.push(f.clone());
5299 }
5300 }
5301 if expanded.is_empty() {
5302 eprintln!("stryke minify: no .stk/.pl/.pm files found");
5303 return 1;
5304 }
5305 let mut errors = 0;
5306 for f in &expanded {
5307 let code = match std::fs::read_to_string(f) {
5308 Ok(c) => c,
5309 Err(e) => {
5310 eprintln!("stryke minify: {}: {}", f, e);
5311 errors += 1;
5312 continue;
5313 }
5314 };
5315 let minified = match crate::minify::minify_source(&code) {
5316 Ok(s) => s,
5317 Err(e) => {
5318 eprintln!("stryke minify: {}: {}", f, e);
5319 errors += 1;
5320 continue;
5321 }
5322 };
5323 if in_place {
5324 if minified == code {
5325 continue;
5326 }
5327 if let Err(e) = std::fs::write(f, &minified) {
5328 eprintln!("stryke minify: {}: {}", f, e);
5329 errors += 1;
5330 } else {
5331 eprintln!(
5332 " minified {} ({} → {} bytes)",
5333 f,
5334 code.len(),
5335 minified.len()
5336 );
5337 }
5338 } else {
5339 println!("{}", minified);
5340 }
5341 }
5342 if errors > 0 {
5343 1
5344 } else {
5345 0
5346 }
5347}
5348
5349fn collect_stryke_files(dir: &std::path::Path, out: &mut Vec<String>) {
5351 let Ok(entries) = std::fs::read_dir(dir) else {
5352 return;
5353 };
5354 let mut paths: Vec<std::path::PathBuf> =
5355 entries.filter_map(|e| e.ok()).map(|e| e.path()).collect();
5356 paths.sort();
5357 for p in paths {
5358 if p.is_dir() {
5359 let name = p
5361 .file_name()
5362 .map(|n| n.to_string_lossy().to_string())
5363 .unwrap_or_default();
5364 if name.starts_with('.') || name == "target" || name == "node_modules" {
5365 continue;
5366 }
5367 collect_stryke_files(&p, out);
5368 } else if let Some(ext) = p.extension() {
5369 let ext = ext.to_string_lossy();
5370 if ext == "stk" || ext == "pl" || ext == "pm" {
5371 out.push(p.to_string_lossy().to_string());
5372 }
5373 }
5374 }
5375}
5376
5377fn run_bench_subcommand(argv0: &str, args: &[String]) -> i32 {
5379 if !args.is_empty() && (args[0] == "-h" || args[0] == "--help") {
5380 println!("usage: stryke bench [FILE|DIR]");
5381 println!();
5382 println!("Discover and run benchmark files. Looks for bench_*.stk / b_*.stk");
5383 println!("in bench/ or benches/ directories (or a specified path).");
5384 println!();
5385 println!("Each file is run and timed. Use the `bench {{ }}` builtin inside");
5386 println!("files for micro-benchmarks with iteration counts and ops/sec.");
5387 println!();
5388 println!("Examples:");
5389 println!(" stryke bench # auto-discover bench/ or benches/");
5390 println!(" stryke bench bench/bench_sort.stk # run a single benchmark");
5391 println!(" stryke bench benches/ # run all in a directory");
5392 return 0;
5393 }
5394 let target = if !args.is_empty() {
5395 args[0].clone()
5396 } else if std::path::Path::new("bench").is_dir() {
5397 "bench".to_string()
5398 } else if std::path::Path::new("benches").is_dir() {
5399 "benches".to_string()
5400 } else {
5401 eprintln!("stryke bench: no bench/ or benches/ directory found");
5402 return 1;
5403 };
5404 let target_path = std::path::Path::new(&target);
5405 let bench_files: Vec<String> = if target_path.is_dir() {
5406 let mut files: Vec<String> = std::fs::read_dir(target_path)
5407 .unwrap_or_else(|e| {
5408 eprintln!("stryke bench: {}: {}", target, e);
5409 crate::hosted::exit(1);
5410 })
5411 .filter_map(|e| e.ok())
5412 .map(|e| e.path().to_string_lossy().to_string())
5413 .filter(|p| {
5414 let name = std::path::Path::new(p)
5415 .file_name()
5416 .map(|n| n.to_string_lossy().to_string())
5417 .unwrap_or_default();
5418 (name.starts_with("bench_") || name.starts_with("b_"))
5419 && (name.ends_with(".stk") || name.ends_with(".st") || name.ends_with(".pl"))
5420 })
5421 .collect();
5422 files.sort();
5423 files
5424 } else {
5425 vec![target]
5426 };
5427 if bench_files.is_empty() {
5428 eprintln!("stryke bench: no benchmark files found (bench_*.stk or b_*.stk)");
5429 return 1;
5430 }
5431 let total = bench_files.len();
5432 let mut failed = 0;
5433 eprintln!(
5434 "\x1b[36mRunning {} benchmark{}\x1b[0m\n",
5435 total,
5436 if total == 1 { "" } else { "s" }
5437 );
5438 let exe = std::env::current_exe()
5439 .ok()
5440 .filter(|p| p.exists())
5441 .or_else(|| std::fs::canonicalize(argv0).ok())
5442 .unwrap_or_else(|| PathBuf::from(argv0));
5443 for f in &bench_files {
5444 let name = std::path::Path::new(f)
5445 .file_name()
5446 .map(|n| n.to_string_lossy().to_string())
5447 .unwrap_or_else(|| f.clone());
5448 eprint!("\x1b[1m── {} ──\x1b[0m ", name);
5449 let script_abs = std::fs::canonicalize(f).unwrap_or_else(|_| PathBuf::from(f));
5450 let project_root = script_abs
5451 .parent()
5452 .and_then(|p| p.parent())
5453 .unwrap_or(std::path::Path::new("."));
5454 let start = std::time::Instant::now();
5455 let output = process::Command::new(&exe)
5456 .arg(&script_abs)
5457 .args(args.get(1..).unwrap_or(&[]))
5458 .current_dir(project_root)
5459 .stderr(process::Stdio::piped())
5460 .stdout(process::Stdio::piped())
5461 .output();
5462 let elapsed = start.elapsed();
5463 match output {
5464 Ok(out) => {
5465 let stderr = String::from_utf8_lossy(&out.stderr);
5466 let stdout = String::from_utf8_lossy(&out.stdout);
5467 if out.status.success() {
5468 eprintln!("\x1b[32m{:.3}s\x1b[0m", elapsed.as_secs_f64());
5469 } else {
5470 eprintln!("\x1b[31mFAILED ({:.3}s)\x1b[0m", elapsed.as_secs_f64());
5471 failed += 1;
5472 }
5473 if !stderr.is_empty() {
5475 eprint!("{}", stderr);
5476 }
5477 if !stdout.is_empty() {
5478 print!("{}", stdout);
5479 }
5480 }
5481 Err(e) => {
5482 eprintln!("\x1b[31mfailed to run: {}\x1b[0m", e);
5483 failed += 1;
5484 }
5485 }
5486 eprintln!();
5487 }
5488 eprintln!("═══════════════════════════════");
5489 if failed == 0 {
5490 eprintln!(
5491 "\x1b[32m✓ All {} benchmark{} completed\x1b[0m",
5492 total,
5493 if total == 1 { "" } else { "s" }
5494 );
5495 0
5496 } else {
5497 eprintln!(
5498 "\x1b[31m✗ {} of {} benchmark{} failed\x1b[0m",
5499 failed,
5500 total,
5501 if total == 1 { "" } else { "s" }
5502 );
5503 1
5504 }
5505}
5506
5507fn run_repl_subcommand(args: &[String]) -> i32 {
5509 let mut load_file: Option<String> = None;
5510 let mut i = 0;
5511 while i < args.len() {
5512 match args[i].as_str() {
5513 "--load" | "-l" => {
5514 i += 1;
5515 if i >= args.len() {
5516 eprintln!("stryke repl: --load requires a file argument");
5517 return 2;
5518 }
5519 load_file = Some(args[i].clone());
5520 }
5521 "-h" | "--help" => {
5522 println!("usage: stryke repl [--load FILE]");
5523 println!();
5524 println!("Start the interactive REPL (readline, history, tab completion).");
5525 println!();
5526 println!("Options:");
5527 println!(" -l, --load FILE Evaluate FILE before entering the REPL");
5528 println!();
5529 println!("Examples:");
5530 println!(" stryke repl # start REPL");
5531 println!(" stryke repl --load lib.stk # pre-load a library, then REPL");
5532 return 0;
5533 }
5534 other => {
5535 eprintln!("stryke repl: unknown option: {}", other);
5536 eprintln!("usage: stryke repl [--load FILE]");
5537 return 2;
5538 }
5539 }
5540 i += 1;
5541 }
5542 let mut cli = Cli::default();
5544 if let Some(ref path) = load_file {
5545 if !std::path::Path::new(path).exists() {
5546 eprintln!("stryke repl: file not found: {}", path);
5547 return 1;
5548 }
5549 cli.execute.push(format!("require {:?}", path));
5551 }
5552 repl::run(&cli);
5553 0
5554}
5555
5556fn looks_like_code(s: &str) -> bool {
5559 s.contains(' ')
5561 || s.contains(';')
5562 || s.contains('|')
5563 || s.contains('{')
5564 || s.contains('(')
5565 || s.contains('$')
5566 || s.contains('@')
5567 || s.contains('>')
5568}
5569
5570fn find_in_path(script: &str) -> Option<String> {
5572 if std::path::Path::new(script).is_absolute() || script.contains('/') {
5573 return Some(script.to_string());
5574 }
5575 if let Ok(path_var) = std::env::var("PATH") {
5576 for dir in path_var.split(':') {
5577 let full = format!("{}/{}", dir, script);
5578 if std::path::Path::new(&full).exists() {
5579 return Some(full);
5580 }
5581 }
5582 }
5583 None
5584}
5585
5586fn print_config(configvar: Option<&str>) {
5588 let version = env!("CARGO_PKG_VERSION");
5589 let arch = std::env::consts::ARCH;
5590 let os = std::env::consts::OS;
5591 let threads = std::thread::available_parallelism()
5592 .map(|n| n.get())
5593 .unwrap_or(1);
5594
5595 if let Some(var) = configvar {
5596 let val = match var {
5598 "version" | "api_version" => version.to_string(),
5599 "archname" => format!("{}-{}", arch, os),
5600 "osname" => os.to_string(),
5601 "threads" => threads.to_string(),
5602 "useithreads" | "usethreads" => "define".to_string(),
5603 "use64bitint" | "use64bitall" => "define".to_string(),
5604 "cc" => "rustc".to_string(),
5605 "optimize" => "-O3 -lto".to_string(),
5606 "prefix" | "installprefix" => "/usr/local".to_string(),
5607 "perlpath" => "stryke".to_string(),
5608 _ => {
5609 eprintln!("Unknown config variable: {}", var);
5610 return;
5611 }
5612 };
5613 println!("{}='{}'", var, val);
5614 } else {
5615 println!("Summary of stryke v{} configuration:\n", version);
5616 println!(" Platform:");
5617 println!(" osname={}, archname={}-{}", os, arch, os);
5618 println!(" Compiler:");
5619 println!(" cc=rustc, optimize=-O3 -lto");
5620 println!(" Threading:");
5621 println!(" useithreads=define, threads={}", threads);
5622 println!(" Integer/Float:");
5623 println!(" use64bitint=define, use64bitall=define");
5624 println!(" Parallel extensions:");
5625 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");
5626 println!(" Install:");
5627 println!(" perlpath=stryke");
5628 }
5629}
5630
5631#[cfg(test)]
5632mod cli_argv_tests {
5633 use super::{expand_perl_bundled_argv, normalize_argv_after_dash_e, parse_cli_prelude, Cli};
5634 use clap::Parser;
5635
5636 fn args(v: &[&str]) -> Vec<String> {
5637 v.iter().map(|s| (*s).to_string()).collect()
5638 }
5639
5640 #[test]
5641 fn prelude_inserts_double_dash_before_script_argv_long_flags() {
5642 let a = args(&["stryke", "s.pl", "--regex", "--foo"]);
5643 let cli = parse_cli_prelude(&a).expect("expected prelude parse");
5644 assert_eq!(cli.script.as_deref(), Some("s.pl"));
5645 assert_eq!(cli.args, vec!["--regex".to_string(), "--foo".to_string()]);
5646 }
5647
5648 #[test]
5649 fn prelude_with_dash_w_before_script() {
5650 let a = args(&["stryke", "-w", "s.pl", "--regex"]);
5651 let cli = parse_cli_prelude(&a).expect("expected prelude parse");
5652 assert!(cli.warnings);
5653 assert_eq!(cli.script.as_deref(), Some("s.pl"));
5654 assert_eq!(cli.args, vec!["--regex".to_string()]);
5655 }
5656
5657 #[test]
5658 fn prelude_dash_e_then_argv_with_long_flag() {
5659 let a = args(&["stryke", "-e", "1", "foo", "--regex"]);
5660 let mut cli = parse_cli_prelude(&a).expect("expected prelude parse");
5661 normalize_argv_after_dash_e(&mut cli);
5662 assert_eq!(cli.execute, vec!["1"]);
5663 assert!(cli.script.is_none());
5664 assert_eq!(cli.args, vec!["foo".to_string(), "--regex".to_string()]);
5665 }
5666
5667 #[test]
5668 fn explicit_user_double_dash_skips_prelude() {
5669 let a = args(&["stryke", "--", "s.pl", "x"]);
5670 assert!(parse_cli_prelude(&a).is_none());
5671 }
5672
5673 #[test]
5674 fn bundled_lane_le_lne_maps_to_split_switches() {
5675 for (flag, code, expect_a, expect_n) in [
5676 ("-lane", "print 1", true, true),
5677 ("-le", "print 2", false, false),
5678 ("-lne", "print 3", false, true),
5679 ("-lnE", "p 4", false, true),
5680 ] {
5681 let a = expand_perl_bundled_argv(args(&["stryke", flag, code]));
5682 let cli = Cli::try_parse_from(&a).expect("parse bundled flags");
5683 assert!(
5684 cli.line_ending.is_some(),
5685 "{flag}: expected -l (line ending)"
5686 );
5687 assert_eq!(cli.auto_split, expect_a, "{flag}: autosplit (-a)");
5688 assert_eq!(cli.line_mode, expect_n, "{flag}: line loop (-n)");
5689 if flag.contains('E') {
5690 assert_eq!(cli.execute_features, vec![code]);
5691 assert!(cli.execute.is_empty());
5692 } else {
5693 assert_eq!(cli.execute, vec![code]);
5694 assert!(cli.execute_features.is_empty());
5695 }
5696 }
5697 }
5698
5699 #[test]
5700 fn bundled_lpe_preserves_print_mode() {
5701 let a = expand_perl_bundled_argv(args(&["stryke", "-lpe", "print 1"]));
5702 let cli = Cli::try_parse_from(&a).expect("parse");
5703 assert!(cli.print_mode);
5704 assert_eq!(cli.execute, vec!["print 1"]);
5705 }
5706
5707 #[test]
5708 fn bundled_0777_not_split() {
5709 let a = expand_perl_bundled_argv(args(&["stryke", "-0777", "-e", "1"]));
5710 assert!(
5711 a.contains(&"-0777".to_string()),
5712 "expected -0777 kept intact: {a:?}"
5713 );
5714 }
5715
5716 #[test]
5717 fn bundled_0ne_splits_like_perl() {
5718 let a = expand_perl_bundled_argv(args(&["stryke", "-0ne", "print 1"]));
5719 let cli = Cli::try_parse_from(&a).expect("parse");
5720 assert_eq!(cli.execute, vec!["print 1"]);
5721 assert!(cli.line_mode);
5722 }
5723
5724 #[test]
5725 fn bundled_f_colon_takes_rest_of_token() {
5726 let a = expand_perl_bundled_argv(args(&["stryke", "-F:", "-anE", "say $F[0]"]));
5727 let cli = Cli::try_parse_from(&a).expect("parse");
5728 assert_eq!(cli.field_separator.as_deref(), Some(":"));
5729 assert!(cli.auto_split);
5730 assert!(cli.line_mode);
5731 assert_eq!(cli.execute_features, vec!["say $F[0]"]);
5732 }
5733
5734 #[test]
5735 fn bundled_f_comma_takes_rest_of_token() {
5736 let a = expand_perl_bundled_argv(args(&["stryke", "-F,", "-anE", "print 1"]));
5737 let cli = Cli::try_parse_from(&a).expect("parse");
5738 assert_eq!(cli.field_separator.as_deref(), Some(","));
5739 }
5740
5741 #[test]
5742 fn help_alias_not_bundled_as_h_e_l_p() {
5743 let a = expand_perl_bundled_argv(args(&["stryke", "-help"]));
5744 let cli = Cli::try_parse_from(&a).expect("parse");
5745 assert!(cli.help);
5746 }
5747
5748 #[test]
5749 fn thread_operator_not_bundled() {
5750 let a = expand_perl_bundled_argv(args(&["stryke", "->> 1 p"]));
5752 assert_eq!(a, args(&["stryke", "->> 1 p"]));
5753
5754 let b = expand_perl_bundled_argv(args(&["stryke", "~> 1 p"]));
5755 assert_eq!(b, args(&["stryke", "~> 1 p"]));
5756 }
5757}