// STRYKELANG — ENGINEERING REPORT

Parallel Perl 5 interpreter in Rust · NaN-boxed bytecode VM · Cranelift JIT · Rayon work-stealing · pipe-forward (|>) and thread macro (~>)

>_EXECUTIVE SUMMARY

strykelang is a single-binary Perl 5 implementation written in Rust. The interpreter compiles source to a 351-opcode bytecode, runs it on a register+stack hybrid VM, and JITs hot blocks through Cranelift to native code. Parallel primitives (pmap, pgrep, pflat_map, pfor) ride on rayon work-stealing. Synchronization spans the full stack: mysync / oursync for lockless atomic shared state, mutex() + semaphore($n) with parking_lot::Mutex + Condvar for explicit blocking critical sections / bounded concurrency, and POSIX flock($fh, OP) for cross-process advisory file locks. Pipe-forward (|>) and the thread macro (~>) extend Perl 5 syntax with Clojure / Racket / Scala threading semantics. Editor tooling rides on the same binary — stryke --lsp for Language Server Protocol, stryke --dap for Debug Adapter Protocol (line breakpoints, step over/into/out, recursive variable drill-down) — consumed by a first-class JetBrains plugin under editors/intellij/. 350k production Rust lines + 227k .stk lines + 10,453 builtins (11,229 keys in %all) + 1,349 Rosetta tasks + 173 Exercism solutions — matching surface coverage that took dynamic languages with multi-decade histories.

479,076
Rust Lines
227,213
Stryke Lines (.stk)
10,453
Builtins (Primary)
13,450
Test Functions
351
VM Opcodes
345
AST Variants
19,934
Parity Cases
2,960
Example Scripts
134
Direct Dependencies
1,801
Git Commits

Source Distribution — 479,076 lines

350,013 strykelang / 127,928 tests / 1,135 tooling · 73.1% strykelang

Production: 215 files across strykelang/ (including strykelang/pkg/ + strykelang/bins/) excluding *_tests.rs. Tests: 20 inline *_tests.rs modules in strykelang/ + 390 integration modules in tests/. Tooling: build.rs + benches/ + fuzz/.


~SCALE & POSITION

Quantitative comparison against the established dynamic-language reference set. Source-LOC figures are approximate (counted from each project's public source tree, excluding vendored deps and external bindings). Stryke is single-author single-language-binary; the others span tens to hundreds of contributors over multi-decade histories.

Language First release Years Production source Builtins (primary) Native JIT Rosetta complete?
stryke 2026 0.09 350,013 Rust + 227,213 stk 10,453 Cranelift 1,349 / 1,349 (100%)
Lua 5.4 1993 ~32 ~13,000 (C) ~80 no (LuaJIT is separate) partial
LuaJIT 2005 ~20 ~85,000 (C+asm) ~80 tracing n/a (rides on Lua)
Tcl 8.6 1988 ~37 ~150,000 (C) ~90 no complete
Crystal 2014 ~12 ~250,000 (Crystal) stdlib classes LLVM AOT partial
Perl 5 1994 ~31 ~700,000 (C+Perl) ~250 keywords no complete
Python 3 2008 ~17 ~1,200,000 (C+Py) ~150 builtins no (3.13 experimental) complete

By Test Surface

13,450 #[test] functions + 19,934 Perl-parity cases + 12,372 assert_eq / assert_ok checks in examples/rosetta + examples/project. The cross-implementation parity suite is the critical part — only mature dynamic langs ship one (Perl, Ruby, Python) and stryke is the only one running its parity suite against another implementation (perl) on every CI cycle.

By Built-in Capability

Things in stryke's binary that the reference set doesn't ship in-binary: Cranelift JIT, distributed-compute primitive (cluster/pmap_on), package manager (s pkg), web framework, language-level AI (ai/tool fn/MCP), AOT static-binary deployment (s build). Encyclopedic-stdlib axis: 10,453 builtins (11,229 spellings) vs ~80–250 for the reference set.

Rosetta Code Completion

Clearing all 1,349 published tasks is a third-party-verifiable completeness bar that takes most languages a decade or more to reach — differential is the encyclopedic-stdlib axis: tasks that other languages need third-party libraries for are one-builtin calls in stryke.

#Builtin Count vs Other Bareword-Fn Languages

Stryke's 10,453 primary builtins exceed every other language whose stdlib is exposed as bareword fns — including Wolfram Language, the only prior holder of "world's largest builtin set" branding. APL/J/K/q have hundreds of primitives, but as glyphs (+/, , ~:) rather than barewords; R has ~2k+, but split across library()'d packages rather than always-loaded. Stryke is the only bareword-fn language past the 10k threshold.

LanguageBareword BuiltinsSource / Probe
stryke10,453len(keys %b) at runtime — dynamic, not hardcoded
Wolfram Language v14.1~7,000Stephen Wolfram blog ("7,000 or so" at v14.1, +89 net new from v14.0's 6,602)
Wolfram Language v14.3 (est.)7,100–7,300Extrapolated from per-version deltas; exact count not published
Ruby Kernel~250Kernel.methods.size
Perl 5 core~220perlfunc manpage
TCL~100info commands
Lua base + std~100_G + standard tables
Bash~80enable -a | wc -l
Python builtins~70dir(__builtins__) — stdlib is module.fn, not bareword
AWK~30gawk manual

Wolfram is the only language that has marketed builtin count as a competitive metric. Stryke's 10,453 clears Wolfram v14.1's stated count by ~3,260 and clears the high end of a plausible v14.3 band (~7,300) by ~3,135. Bareword-fn rivalry past the 10k mark: stryke is alone in the category.

Density: builtins per byte of binary

ToolchainBuiltinsInstall SizeBytes / Builtin
stryke (single static binary)11,22944 MB~4.0 KB
Wolfram Engine v14 (free tier)~7,000~5–6 GB~750 KB
MATLAB R2025a + toolboxesthousands30+ GBn/a

~175× denser than Wolfram per builtin/byte. The single-binary qualifier matters: no kernel boot, no paclet manager, no licensing daemon, no GUI runtime, no library() resolution — ~/.cargo/bin/s is one statically-linked Rust binary that cold-starts in <10 ms with 11,139 callable spellings (10,453 builtins + 686 aliases) plus 90 keywords reflected in %all (11,229 keys total). Caveat: bytes-per-builtin is partly apples-to-oranges — Wolfram's GBs include datasets, image / video native code, and the Wolfram Notebook GUI. The defensible composite claim is "11,139 callable names + 44 MB single binary + sub-10 ms cold start", which no other language clears simultaneously.


#SUBSYSTEM BREAKDOWN

Source partitioned by role. Builtins dominate at 31.8% — the language ships with 10,453 primaries in %b (11,229 keys in %all = 10,453 primaries + 686 aliases + 90 keywords) spanning string / array / hash / regex / I/O / process / OO / format / pack / crypto / codec / parallel / sync (mutex / semaphore / flock) / HTTP / database / network / AI / web / math / physics / chemistry / biology / signal / finance domains. Core pipeline (lexer→parser→compiler→VM→JIT) is ~17.6% of the Rust slice.

SubsystemKey FilesLines%ShareDescription
Builtinsbuiltins*, list_builtins, math_wolfram*152,54731.8%
10,453 builtins in %b / 11,229 keys in %all: string, array, hash, regex, math, physics, chemistry, biology, signal, finance, ML, geometry, special functions, I/O, file, process, OO, format, pack, crypto, codec, network, HTTP, JWT
VM & Compilervm, compiler, bytecode24,4085.1%
351-opcode bytecode, register+stack VM, AST→bytecode compiler with constant folding / register allocation / peephole, opcode disassembler
VM Runtime State (vm_helper)vm_helper24,1794.7%
Persistent state container for the bytecode VM: scope graph, sub table, error context, debugger hooks, special variables ($_, $!, $/, …), reflection-hash population, package-stash refresh, AOP intercept registry, I/O handle map. Not a tree-walking interpreter — stryke has none. The absence is pinned by tests/tree_walker_absent_aop.rs as an architectural invariant.
Parserparser, ast, token27,0895.7%
Recursive-descent parser, 212 parse_* functions, AST variants across multiple enums, operator precedence climbing (or/and/not at lowest tier per perlop), heredocs, prototype parsing, builtin-name whitelist
AI / Webai, ai_sugar, web, web_orm, mcp14,4863.0%
Anthropic / OpenAI client, tool-use protocol, AI sugar (ai as native verb), MCP server / client, web framework (route DSL, controllers, ORM, sessions, templates), JSON / form / multipart parsers
Value Systemvalue, nanbox, scope, capture, convert, deconvert10,9422.3%
NaN-boxed StrykeValue: int / float / string / array / hash / code / regex / undef. Scoping (pad slots, closures, local/my/our/oursync), type coercion, capture cells
Codecs / Datanative_codec, native_data, map_stream, map_grep_fast, sort_fast, sketches10,2682.1%
JSON / YAML / TOML / CSV / MessagePack encode-decode, streaming map/grep iterators, optimized sort, probabilistic sketches (HLL, t-digest, count-min)
Distributioncluster, controller, agent, remote_wire, pkg/*, script_cache, pcache12,0262.5%
Multi-host cluster execution, agent / controller protocol, wire format, package manager (pkg — manifest, lockfile, store, resolver), bytecode cache
LSPlsp, lsp_symbols, lsp_extras, lsp_docs_domains, static_analysis23,5104.9%
Language Server Protocol over stdio (stryke --lsp) — diagnostics (strict-vars on by default with full Perl/AOP/block-param exemption set), hover descriptions (with full key tables for hash-returning builtins, suppressed inside string literals), completion (sigil vars, subs, classes/structs/enums/traits with proper CompletionItemKind, enum variants, constants, loop labels, hash-key completion driven by 20+ builtin return schemas, parse-error recovery during typing, suffix-only insertText for qualified subs), semantic tokens, signature help, code actions (Extract Variable/Constant/Parameter/Function, caret-only, inside-string aware), goto-definition, document symbols, package-aware rename (AST-based for fields/methods, no textual fallback, cross-file via require graph BFS, defensive ::-strip on newName). OOP-aware: $self->X/$obj->X walks extends + impl chains plus a universal-method whitelist; constructor key validation across inheritance; positional vs keyed auto-detection. Cross-file require walks up to a project root with a sibling lib/. Consumed by VS Code, Neovim, Helix, and the JetBrains plugin.
DAP Debuggerdap, debugger2,9030.6%
Debug Adapter Protocol server over stdio or TCP (stryke --dap [HOST:PORT]) sharing the same Debugger state machine as the TTY REPL (stryke -d). Line + function breakpoints, step over/into/out, recursive Variables panel with class / struct / enum / set / sketch drill-down, Evaluate dialog with scalar-prelude injection, real-time Console output. Consumed by editors/intellij/.
CLI & REPLmain, repl, cli_runners, getopts8,9891.9%
CLI entry: argv parsing, script loading, one-liner mode (-e/-E/-n/-p), reedline-based REPL with utop-style menu, columnar completion, history; subcommand runners and Getopt::Long-compatible flag parser
JITjit5,4621.1%
Cranelift block-level JIT: hot-path detection, native code emission for tight numeric loops, deoptimization stubs back to bytecode VM
Format / Deparsefmt, deparse, format, minify4,9721.0%
perltidy-compatible code formatter, AST→source deparser (B::Deparse equivalent), Perl format/write system, source minifier
Parallel Runtimepar_lines, par_list, par_pipeline, par_walk, ppool, pchannel, pwatch, pmap_progress, parallel_trace, stress, builtins_sync4,7711.0%
Rayon work-stealing: pmap, pgrep, pflat_map, pfor, parallel file walking, async pipelines, progress tracking, channels, stress-testing primitives (heat, fire). Blocking sync primitives backed by parking_lot::Mutex + Condvar: mutex / mutex_lock / mutex_try_lock, semaphore / sem_acquire / sem_release / sem_try_acquire; cross-process flock($fh, OP) for advisory file locks (POSIX flock(2)).
Perl Compatperl_decode, perl_fs, perl_inc, perl_regex, perl_signal, perl_pty, special_vars, english4,2050.9%
@INC, %ENV, %SIG, file ops, Perl regex engine, signal handling, PTY support, magic special variables, use English
Lexerlexer4,4460.9%
Tokenizer for Perl 5 + stryke extensions: heredocs, regex literals, q/qq/qw/qr, string interpolation, Unicode identifiers, sigil disambiguation
Misc / Errorerror, mro, lib, bench_fusion, fib_like_tail, pending_destroy, kvstore, banner, doc_render, docs, bins/{gen_docs,s,st}, serialize_normalize, parse_smoke_*, run_semantics_more6,3821.3%
Rich error reporter, MRO (C3 / DFS), library entry, fusion-pass optimizers, finalizer queue, key-value store, banner, doc generator + renderer, thin bin shims, smoke tests
Toolingprofiler, perf_recorder, stryke_log1,4500.3%
Sampling profiler, perf event recorder, structured logger. (Static analyzer counts under the LSP row; the step debugger lives under the DAP Debugger row above so the TTY -d front-end and the DAP server count together.)
FFI / AOTrust_ffi, rust_sugar, aot, data_section, aop2,0890.4%
Embed-Rust closure FFI, AOT bytecode caching, data-section relocations, AOP intercepts
Crypto / Packcrypt_util, jwt, pack, secrets1,8540.4%
Symmetric / asymmetric encryption, password hashing, JWT encode/decode, pack/unpack templates, secret-store helpers
Teststests/*, *_tests.rs127,92826.7%
13,450 tests (#[test] fns across tests/ + strykelang/) across 410 Rust modules (20 inline + 390 integration): parser, lexer, builtins, parallel, sync (21 mutex / semaphore tests in builtins_sync_tests.rs), regex, values, OO, semantics, CLI, LSP, DAP, runtime, fix-regression suite, and 265 behavioral pin suites (incl. reduce_pin, split_pin, zip_pad_pin)
Build / Benchbuild.rs, benches/, fuzz/1,1350.2%
Build-time reflection generator (BUILTIN_ARMS, CATEGORY_MAP, DESCRIPTIONS), JIT vs interpreter micro-bench, libfuzzer harnesses
TOTAL479,076100%

$TOP 20 FILES BY SIZE

The 20 largest source files account for 54.2% of the codebase (Rust under strykelang/ + tests/ + tooling slice). Builtins is by far the largest single file: every Perl 5 core function plus stryke extensions (HTTP, JSON, parallel, codec, OO, AI, web) lives here, dispatched through a single match table generated at build time.

FileLinesRole
strykelang/math_wolfram*.rs58,80382 batch files (math_wolfram, math_wolfram2…82) include!'d into builtins.rs: 4,893 fns spanning Wolfram-class math, physics, chemistry, biology, signal, finance, ML, geometry, special functions, calendrical, astronomy, BLAS/LAPACK, sabermetrics, Excel/financial, GIS, robotics/control, actuarial, epidemiology, archive/encoding, music theory, geology, logic/SAT/SMT, compilers/parsing, linguistics, Postgres SQL/JSON, Redis, scipy.special, economics/game theory
strykelang/builtins.rs43,786Core builtin dispatch table covering string/array/hash/regex/math/I/O/file/process/OO domains; routes 10,453 builtins in %b (11,229 keys in %all)
strykelang/parser.rs23,231Recursive-descent parser; precedence climbing, heredocs; whitelists every builtin spelling
strykelang/vm_helper.rs24,179Runtime: scope graph, evaluation context, error reporter, debugger hooks (used by both -d TTY REPL and --dap server), special-variable handling, reflection-hash population, package-stash refresh
strykelang/lsp.rs13,270LSP server core — hover, completion (hash-key from builtin schemas, type/enum/constant/loop-label categories, qualified-suffix insertText, parse-error recovery, { trigger), diagnostics, semantic tokens, signature help, code actions (Extract V/C/P/M, caret-only, inside-string aware), goto/refs/rename (package-aware, AST-only field rename, defensive ::-strip on newName, cross-file via require graph), document symbols. Hover suppressed inside string literals.
strykelang/builtins_extended.rs12,643Extended builtins: crypto, JWT, codecs (JSON/YAML/TOML/CSV/MsgPack), HTTP, email, QR, compression
strykelang/vm.rs10,660Bytecode VM: register+stack hybrid, 351 opcodes, hot-path detection, exception unwinder, debugger should_stop hook on every op
strykelang/compiler.rs10,072AST→bytecode: constant folding, register allocation, peephole optimization, basic-block formation
tests/suite/fix_regressions.rs7,654Regression test suite — one test per fixed bug; pin behaviors so refactors can’t silently re-break
strykelang/ai.rs6,631Anthropic / OpenAI API client, tool-use protocol, batch / pmap fan-out, MCP integration, vision / audio modalities
strykelang/main.rs5,507CLI: argv, script loading, one-liner mode (-e/-E/-n/-p), shebang dispatch, builtin-as-command, --lsp / --dap / -d dispatch
strykelang/run_semantics_tests.rs5,468Perl-semantics correctness suite: behaviors that must match perl byte-for-byte
strykelang/jit.rs5,462Cranelift JIT: IR generation, native code emission, deoptimization stubs, calling-convention bridge
strykelang/value.rs5,253StrykeValue: NaN-boxed union, conversions, stringify / numify, magic context handling
strykelang/crate_api_tests.rs5,164Public crate-API test suite: validates the embeddable interface (stryke::eval, VMHelper, ...)
strykelang/fusevm_bridge.rs4,944Bridge to the shared fusevm runtime: translates a conservative universal-integer subset of stryke bytecode to fusevm::Op for execution on fusevm::VM (the VM substrate shared with zshrs)
strykelang/pkg/commands.rs4,825s pkg subcommand implementations — manifest, lockfile, store, resolver; each public fn returns an exit code wired straight into process::exit
strykelang/lexer.rs4,309Tokenizer: heredocs, regex literals, q/qq/qw/qr, string interpolation, sigil disambiguation
strykelang/lsp_extras.rs4,165Additional LSP capabilities: semanticTokens/full token coloring true to stryke lexical structure (keywords / builtins / sigil vars / strings), plus extended protocol surfaces beyond the lsp.rs core
strykelang/builtins_net.rs3,863Network / IP / CIDR / MAC primitives: parsing, predicates, conversions, canonical forms, reverse-DNS — pure stdlib IpAddr, no external crates
TOP 20 SUBTOTAL259,44454.2% of 479,076-line Rust slice

@EXECUTION PIPELINE

Source flows through 5 stages. Tier 1 is the bytecode VM; tier 2 is Cranelift JIT'd native code for hot blocks. Both tiers share the same NaN-boxed value representation, so deoptimization (JIT→VM fallback on type miss) is a frame-pointer swap, not a re-marshal.

  Source (.stk / .pl / -e '...')
       │
       ▼
  ┌─────────────┐     ┌─────────────┐     ┌──────────────┐
  │  lexer.rs   │────▶│  parser.rs  │────▶│ compiler.rs  │
  │  (4,187)    │     │  (22,790)   │     │  (9,894)     │
  │  Tokenizer  │     │  AST node   │     │  351 opcodes │
  │  heredocs   │     │  variants   │     │  const fold  │
  │  regex lit  │     │  prec climb │     │  reg alloc   │
  │  q/qq/qw/qr │     │  whitelist  │     │  peephole    │
  └─────────────┘     └─────────────┘     └──────┬───────┘
                                                  │
                              ┌────────────────────┤
                              │                    │
                              ▼                    ▼
                     ┌──────────────┐     ┌──────────────┐
                     │    vm.rs     │     │   jit.rs     │
                     │   (11,648)   │     │  (5,174)     │
                     │  Bytecode VM │     │  Cranelift   │
                     │  reg + stack │     │  block JIT   │
                     │  hot detect  │────▶│  native code │
                     │  exc unwind  │◀────│  deopt stubs │
                     └──────┬───────┘     └──────────────┘
                            │
                            ▼
                  ┌───────────────────┐
                  │   builtins.rs     │
                  │   (43,786)        │
                  │   10,453 builtins  │
                  │   11,229 spellings │
                  │   + extended      │
                  │   (12,643)        │
                  │   + math_wolfram* │
                  │   (58,672 / 82 f) │
                  └─────────┬─────────┘
                            │
           ┌────────────────┼─────────────────┐
           │                │                 │
    ┌──────▼──────┐  ┌──────▼──────┐  ┌──────▼──────┐
    │  value.rs   │  │  scope.rs   │  │  parallel   │
    │  (5,050)    │  │  (3,270)    │  │  (4,803)    │
    │  NaN-boxed  │  │  my/local/  │  │  pmap/pgrep │
    │  StrykeValue  │  │  oursync    │  │  rayon work │
    └─────────────┘  └─────────────┘  └─────────────┘

&BUILTIN INVENTORY

Builtins are partitioned at build time by build.rs, which scans strykelang/parser.rs and strykelang/builtins.rs to generate CORE_CATEGORY_MAP (Perl 5 core), EXT_CATEGORY_MAP (stryke extensions), and ALL_CATEGORY_MAP (every callable spelling, primaries plus aliases); language keywords (if, my, sub, fn, …) live in a separate hand-curated KEYWORDS table. The reflection tables back the runtime %b / %k / %all / %pc / %e / %a / %d / %c / %p hashes (plus their %stryke::* long-form aliases) under the disjoint-union invariant %all = %a + %b + %k. At runtime stryke also exposes %parameters (zsh-style live binding view) and per-package stashes (%main::, %Foo::) refreshed lazily on read.

10,453
Builtins (Primary)
686
Aliases
11,229
Keys in %all
186
Perl 5 Core
10,249
Stryke Extensions
4,371
LSP Descriptions
7,054
Match Arms
317
Categories

118 hot builtins are reachable through dedicated BuiltinId opcodes in bytecode.rs — the rest go through the generic dispatch table. The 686-alias overhead exists so canonical Perl spellings (localtime), abbreviations (tj), and stryke-style snake_case alternatives all coexist without disambiguation. Disjoint union: keys %a + keys %b + keys %k == keys %all.

// CATEGORIES

String (byte)

Perl 5 layer: length, substr, index, rindex, pos — byte-indexed, kept for compat. chomp, uc, lc, sprintf, reverse, split, join, tr///, s///, m//

String (codepoint)

Stryke layer: len, $s[i], $s[a:b], cindex, crindex — codepoint-indexed, all share a coordinate system. Never mix with byte-layer outputs.

Array

push, pop, shift, unshift, map, grep, sort, reverse, splice, wantarray

Hash

keys, values, each, exists, delete, tied, tie, untie

Numeric

abs, int, sqrt, sin, cos, atan2, exp, log, hex, oct, rand, srand

I/O

open, close, read, write, print, printf, say, readline, seek, tell, eof

File

stat, lstat, chmod, chown, unlink, rename, symlink, readlink, mkdir, rmdir, file-test ops

Process

fork, exec, system, wait, waitpid, kill, getpid, getppid, setsid

Regex

m//, s///, qr//, tr///, pos, named captures, lookahead/behind, fancy-regex backrefs

OO

bless, ref, isa, can, SUPER::, MRO C3 / DFS, roles, traits, class blocks

Format / Pack

pack, unpack, format, write, printf, sprintf with full Perl 5 format strings

Codec

to_json, from_json, to_yaml, from_yaml, to_toml, to_csv, to_msgpack, to_html, to_pdf

Crypto

AES, ChaCha20, RSA, Ed25519, P-256/384, X25519, Argon2, scrypt, bcrypt, PBKDF2, BLAKE3, SHA-2/3, JWT

Network

http_get, http_post, tcp_connect, tcp_listen, DNS lookup, URL parse, IP utilities

Parallel

pmap, pgrep, pflat_map, pfor, par_lines, par_walk, work-stealing iterators, channels

Sync Primitives

mutex() + mutex_lock / mutex_unlock / mutex_try_lock / mutex_is_locked (intra-process, parking_lot::Mutex + Condvar, no busy-wait). semaphore($n) + sem_acquire / sem_release / sem_try_acquire / sem_permits / sem_limit (counting semaphore, same backing). flock($fh, OP) wraps POSIX flock(2) for cross-process advisory locks (LOCK_SH=1, LOCK_EX=2, LOCK_NB|LOCK_EX=4, LOCK_UN=8). Pair with defer for RAII.

Filesystem Stream

fr (file rec), fw (file write), fa (file append), slurp, spurt, walk

Reflection

%b / %k / %all / %pc / %e / %a / %d / %c / %p (nine reflection hashes — eight build-time, plus %k hand-curated keyword table), %parameters (live bindings, zsh $parameters), %main:: / %Pkg:: (package stashes), caller, __PACKAGE__, __SUB__

// STRING COORDINATES — BYTES VS CODEPOINTS

Stryke runs string code in two coordinate systems. Perl 5 builtins stay byte-indexed for binary-protocol and .pm-source compat. Stryke extensions are codepoint-indexed so search positions and slice bounds line up. Never auto-converted — mixing across systems silently misaligns on any non-ASCII content.

OperationStryke (codepoints)Perl 5 (bytes)
Lengthlen $slength $s
Index char$s[$i]substr $s, $i, 1
Slice$s[$a:$b] (inclusive)substr $s, $start, $len
Search forwardcindex $s, $needle [, $from]index $s, $needle [, $from]
Search backwardcrindex $s, $needle [, $from]rindex $s, $needle [, $from]

For "hello ─ world" (15 bytes, 13 codepoints): length = 15, len = 13, index $s, "world" = 9, cindex $s, "world" = 7.


!VM & BYTECODE

351 opcodes grouped into 27 functional categories. The VM is a register+stack hybrid: frame-local scalar slots provide O(1) access for hot variables (no string lookup), while the operand stack handles temporaries and complex expressions. Frame-local slots win on tight loops; the stack wins on irregular control flow.

// OPCODE CATEGORIES

Constants

LoadInt, LoadFloat, LoadConst, LoadUndef

Stack

Pop, Dup, Swap, Roll

Scalars

Name-pool indexed get/set, $_ magic, local/my/our binding

Arrays

NewArray, Push, Pop, Shift, Unshift, indexed get/set

Hashes

NewHash, get/set/delete, exists, each, keys, values

Arithmetic

Add, Sub, Mul, Div, Mod, Pow, Neg, integer/float dispatch

String

Concat, Repeat, Length, interpolation, escape decoding

Comparison

Numeric (==, !=, <, ...) + string (eq, ne, lt, ...) + spaceship

Logical / Bitwise

And, Or, Not, BitAnd, BitOr, BitXor, shifts

Control Flow

Absolute jumps, conditional branches, last/next/redo labels

Functions

Call, TailCall, Return, prototype dispatch

Try / Catch

VM exception handling: try_recover_from_exception, finally blocks, error propagation

Frame Slots

O(1) frame-local scalar access — bypasses pad lookup for hot registers

Streaming Map

Lazy iterator opcodes for |> and ~> chains: produce pull-side, no intermediate vec


~CRANELIFT JIT

Tier-2 compiler. The VM detects hot blocks (loops with stable types) and hands them to jit.rs, which lowers bytecode to Cranelift IR, then to native machine code. NaN-boxed StrykeValue stays the same shape across tiers, so a JIT→VM deopt is frame-pointer-swap-and-go — no value re-boxing.

Cranelift Backend

x86-64 + aarch64 native code generation. Same IR backend as Wasmtime — production-grade register allocator, optimizer, and code emitter.

Block-Level Compilation

Cranelift compiles individual basic blocks of bytecode, not whole functions. Lower latency than method-JIT; warm-up cost amortized across invocations.

Hot-Path Detection

VM tracks per-block invocation counts and type stability. Once a threshold is crossed and types haven’t shifted, the block is compiled.

Deoptimization

Type guard miss in JIT'd code branches back into the VM at the same bytecode offset. Stack and frame state survive the transition.

Numeric Specialization

Tight integer / float loops get specialized arithmetic with no boxing. NaN-tag bits are checked once per iteration, not per op.

Builtin Inlining

The 118 bytecoded builtins reachable through BuiltinId can be inlined directly into JIT code, skipping the dispatch table entirely.


%PARALLEL RUNTIME

11 modules listed below sum to 2,968 lines; the full parallel subsystem (helpers + stress.rs + orchestration) is 4,803 lines in the roll-up table. Every parallel builtin is a streaming iterator under the hood: pmap, pgrep, and pflat_map produce lazy results that flow into pipe-forward chains without materializing intermediate vectors. builtins_sync.rs adds blocking mutex + counting semaphore primitives backed by parking_lot::Mutex + Condvar for cases where lockless mysync isn't enough (compound multi-statement critical sections, bounded-concurrency throttles).

FileLinesRole
pmap_progress.rs755Progress reporting for long-running parallel maps; ETA estimation; cancellation tokens
par_pipeline.rs642Multi-stage parallel pipelines: stage fusion, cross-stage work-stealing
ppool.rs265Worker pool wrapper around rayon: thread-count tuning, panic isolation
pchannel.rs251Bounded MPMC channels for cross-stage handoff in par_pipeline
par_list.rs232Parallel list operations: pmap, pgrep, pflat_map, pfor on plain arrays
pwatch.rs205Filesystem-watcher integration: parallel reload on file change
builtins_sync.rs306Blocking sync primitives: mutex() / mutex_lock / mutex_unlock / mutex_try_lock / mutex_is_locked; semaphore($n) / sem_acquire / sem_release / sem_try_acquire / sem_permits / sem_limit. parking_lot::Mutex + Condvar — no busy-wait. Cross-process locking via the Perl-compat flock($fh, OP) builtin in builtins.rs:39185.
par_lines.rs144Parallel line-oriented file processing: chunked reads, line-boundary recovery
par_walk.rs69Parallel filesystem walking: walk, fr (file rec), parallel find
parallel_trace.rs61Tracer that records work-steal events for debugging contention
pcache.rs38Per-worker thread-local result cache; avoids cross-thread coordination on hot reads
PARALLEL TOTAL2,96811 modules

^PARSER & AST

Recursive-descent parser with operator-precedence climbing. 196 parse_* functions cover the full Perl 5 grammar plus stryke extensions: pipe-forward (|>), thread macro (~>), match patterns, class blocks, named arguments, retry / backoff syntax, defer, try/catch/finally.

EnumVariantsRole
ExprKind191Every expression form: literals, ops, calls, slices, regex, formats, captures
StmtKind47Statements: if, while, for, foreach, sub, package, use, BEGIN, ...
BinOp33Binary operators: arithmetic, comparison, logical, bitwise, string, regex bind
PerlTypeName10Type signatures: Int, Str, Array, Hash, Code, Ref, ...
UnaryOp7Unary: -, !, ~, defined, not, file-test, sigil-deref
MatchPattern6Pattern-match arms: literal, range, struct, array, alternation, guard
SubSigParam5Subroutine signature parameters: positional, named, slurpy, default, ...
Sigil / StringPart / MatchArrayElem / GrepBuiltinKeyword4 eachLexical detail enums for sigils, interpolation parts, match elements, grep keywords
20 enums total343Full AST surface area

*TESTS & PARITY

Two complementary suites. Unit + integration tests are #[test] functions in Rust; parity tests are .pl scripts that must produce byte-identical output between system perl and stryke. CI runs both on every push.

13,450
#[test] Functions
3,703
Unit Tests
9,633
Integration Tests
19,934
Parity Cases
410
Rust Test Modules
2,960
Example Scripts

// TEST SURFACES

Parity vs Perl

19,934 .pl cases under parity/cases/. CI runs each through both perl and stryke; any output mismatch fails the job. Catches semantic drift the unit tests miss.

Fix Regressions

tests/suite/fix_regressions.rs at 7,654 lines. One test per fixed bug. Refactors can’t silently re-break old behavior.

Crate API

strykelang/crate_api_tests.rs at 5,164 lines. Validates the embeddable interface: stryke::eval, VMHelper, public types stay stable across versions.

Run Semantics

strykelang/run_semantics_tests.rs at 5,468 lines. Behaviors that must match perl byte-for-byte: special variables, autovivification, list context.

Rosetta

1,648 .stk programs in examples/rosetta/t/ — canonical cross-language tasks (sort, parse JSON, fetch URL) ported into stryke. 100% of list.txt tasks complete (1,349 / 1,349).

Stryke Project

examples/project/ — full multi-module project with 183 stryke files (24,549 LOC) exercising the package layout, lib/, t/.


>REPL & INTERACTIVE SHELL

The s binary doubles as a Perl 5 interpreter and a Unix shell. Reedline-backed REPL with persistent history, tab-completion against the full 22,071-name corpus, ANSI-colored cyberpunk banner, and 80+ shell-style coreutils builtins (ls, cat, grep, cd, pushd, whoami, curl_get, tree) that are in-process Rust function calls, not subprocess spawns. No fork, no PATH walk, no exec — every result is a stryke value ready for the next pipeline stage.

22,071
Tab-complete corpus
< 10 ms
Cold start to prompt
10,453
Builtins in scope
4,371
LSP hover docs
317
Categories in %c
85
Keywords in %k

// STARTUP BANNER

The banner is computed at REPL launch from live reflection — never hard-coded. Numbers shift as builtins land or get removed.

// SHELL-LIKE BUILTINS (NO FORK, NO EXEC)

Filesystem (21)

cd pwd ls cat rm mv cp ln mkdir rmdir touch chmod chown realpath basename dirname mktemp mktempdir tree find glob — all wrap std::fs / tempfile in-process. Output is a stryke value, composable with |>.

Directory Stack (3)

pushd / popd / dir_stack backed by a process-wide Mutex<Vec<PathBuf>>. zsh-style swap-on-bare-pushd.

Identity (8)

whoami groups id hostname getpwuid getpwnam getgrgid getgrnamlibc-backed, no subprocess.

Process (10)

ps top kill nice renice time sleep wait fork exec via libc / nix.

Text Processing (16)

grep sort uniq wc head tail cut tr tac rev_lines shuf column comm diff myers_diff patience_diff — pure Rust.

Networking (9)

curl_get curl_post http_fetch http_post fetch_async fetch_json fetch_url_tool openurl xdg_open via ureq + OS launcher.

Archives (6)

tar zip unzip xz gzip zstdtar / zip / xz2 / flate2 / zstd crates, all already deps.

Terminal (12)

clear cls reset beep ring_bell set_title set_term_title term_size term_width term_height tty_raw tty_cooked — termios ioctl + ANSI escapes.

REPL State (5)

history repl_alias repl_unalias set_alias unset_alias — process-wide tables, reedline history bridge via crate::builtins::repl_history_push().

Search (4)

which whereis type command$PATH walker.

Encoding (5)

iconv strftime date_iso_format format_bytes format_durationchrono + hand-rolled.

Inspection (7)

perfview docs lsp_completion_words methods_for which_all fields to_hash — introspect every reflection hash.

// DESIGN DECISIONS

DecisionRationale
In-process builtins, not subprocess wrappersA subprocess fork+exec costs ~1-3 ms on Linux/macOS. Composing 5 of them in a pipeline = 10-15 ms of overhead. Stryke's cat |> grep |> sort |> uniq stays inside the VM — no syscall boundaries, output of one stage is a heap-resident StrykeValue passed to the next.
Reedline frontend, not raw readlinePure-Rust line editor, no libreadline linkage (which would need GPL or BSD-readline alternatives). Native multi-line editing, syntax highlighting, history search, completer hooks.
Tab complete from %all via lsp_completion_words.txt22,071 names cached at build time. Reedline calls into the completer with each prefix, gets back a sorted candidate list. Includes every primary, alias, keyword, special variable, file-test op, and zsh-style glob qualifier.
repl_alias instead of aliasBare alias is reserved for Perl typeglob assignment (*alias = \&original); shadowing it with a builtin would break tests/suite/behavior_pin_2026_05_d.rs::typeglob_assigns_coderef_alias. Same pattern for unaliasrepl_unalias.
No source builtinPerl already has do FILE (eval-in-current-scope) and require FILE (eval-once with INC tracking). Adding a third spelling violates the "no dups" rule.
Stack-shared dir stack, not per-tabpushd / popd mutate process cwd via chdir(2), which is process-wide on POSIX. The stack mirrors that — one Mutex<Vec<PathBuf>> for the whole interpreter. Tab- or window-scoped stacks would lie about what chdir did.
Banner numbers are computed liveReflection hashes (%b, %a, %all, %k, %d, %c) are populated at compile time by build.rs from the actual try_builtin dispatch arms. The startup banner reads scalar(keys %b) etc. — if a builtin is added, the next REPL launch shows the updated count without touching banner.rs.

// COMPARISON

REPLBuiltins in scopeTab-complete corpusCold startPipelines in-process
bash 5~150~250~5 msNo (fork+exec per stage)
zsh 5~140 + zinit-loaded~2k via compinit~30 ms (cold)No
fish 3~200~1k~25 msNo
nushell 0.x~350~400~50 msYes (pipe is in-VM)
python -i7171 + imported~40 msNo (subprocess module spawns)
node0 (require-first)REPL-internal~80 msNo
stryke10,45322,071< 10 msYes (every |> stage is a heap value)

Nushell is the only direct architectural peer (in-process pipelines, structured values) but it's an order of magnitude smaller in builtin surface and slower to cold-start. No other REPL passes the combination of shell-grade pipelines + 10k+ builtin surface + sub-10 ms launch.


:EDITOR TOOLING — LSP / DAP / JETBRAINS

The same stryke binary that runs scripts also serves Language Server Protocol and Debug Adapter Protocol. No separate stryke-lsp or stryke-dap artifacts — editors point their LSP/DAP clients at stryke --lsp / stryke --dap and get hover docs, completion, semantic tokens, breakpoints, step over/into/out, and recursive variable drill-down. The TTY debugger (stryke -d) and the DAP server share a single Debugger state machine.

7,538
LSP server LOC
1,492
DAP server LOC
837
Debugger core LOC
11
JetBrains IDEs supported
44
Color scheme slots
9
Reflection tool-window tabs

// LSP SERVER — stryke --lsp

// DAP DEBUGGER — stryke --dap [HOST:PORT]

Standard Debug Adapter Protocol over stdio or TCP. Same Debugger state machine the TTY REPL (stryke -d) uses — breakpoints, step modes, scope inspection, call-stack tracking, all shared. Two front-ends on one core.

CapabilityDetail
Line breakpointsGutter clicks fire on the exact source line. Lexer line-tracking fixed so class declarations don't drift the line counter and silently drop BPs.
Function breakpointsStop on entry to any named sub.
Step over / into / outDepth-tracking on every UDF entry/exit so step-over stays in the current frame instead of diving into call sites.
Pause / Continue / Run-to-CursorStandard transport actions.
Recursive Variables panelEvery hash, array, struct, enum variant, class instance, set, and sketch (TDigestSketch, BloomFilter, HllSketch, CmsSketch, TopKSketch) expands inline through variablesReference. Class fields show visibility markers (+ public / # protected / - private) plus the __class / __isa chain.
Sort orderUser my $foo vars first, then topic family ($_, $_0, $_1, …), then $a / $b, then runtime builtins. Compiler-synthetic names (__foreach_i__ etc.) hidden.
Evaluate dialogScalar prelude injection so $a * $b returns the right value from the paused frame.
Console outputp / print / say stream into the IDE Console in real-time (DAP mode forces $| = 1 autoflush).

Supported DAP requests: initialize, setBreakpoints (line), setFunctionBreakpoints, setExceptionBreakpoints, configurationDone, launch, threads, stackTrace, scopes, variables, continue, next, stepIn, stepOut, pause, evaluate, terminate, disconnect. Events: initialized, stopped, output, process, thread, exited, terminated.

// JETBRAINS PLUGIN — editors/intellij/

First-class JVM plugin built on IntelliJ Platform Gradle Plugin 2.16.0, Kotlin 2.0.21, JDK 17. Supports the entire JetBrains paid lineup: RustRover, IDEA Ultimate, GoLand, PyCharm Pro, WebStorm, RubyMine, PhpStorm, CLion, Rider, DataGrip, Aqua. Community editions are not supported — LSP API is paid-only on the JetBrains side.

// OTHER EDITORS

EditorIntegration
Neovimeditors/stryke.lua + ALE / vim-lsp / coc.nvim via stryke --lsp
Vimeditors/stryke.vim — syntax + indent + filetype detection
Helixeditors/helix-languages.toml drop-in
VS Codecoc + editors/vscode-settings.json bridge

.EXAMPLES & SOLVED PROBLEMS

Stryke ships with the largest curated corpus of cross-language exemplars in any single-author Perl 5 work-alike: 2,960 .stk programs under examples/ totaling 227,213 LOC. All 1,349 Rosetta Code tasks in list.txt are solved (100%) — every test compiles and runs against the current build, so any builtin regression surfaces as a failing test before it reaches the parity suite.

2,960
.stk Programs
227,213
examples/ LOC
1,349
Rosetta Solved
100%
Rosetta Complete
173
Exercism Solutions
12,372
assert_eq / assert_ok

// CORPUS BREAKDOWN

CorpusPathFilesLOCStatus
Rosetta Codeexamples/rosetta/t/1,64873,624All 1,349 of 1,349 problems in list.txt solved (100%). Avg 45 LOC/test. Largest: heavy_light 189, red_black_tree 179, baum_welch 177, strassen 168, delaunay 167.
Sample Projectexamples/project/18324,549Multi-module mini-project: lib/ namespaces + t/ test layout exercising the package manager, use resolution, namespace dispatch.
Exercismexamples/exercism/3466,064173 exercises (one solution + tests each). Polyglot .exercism.json compatible.
Top-levelexamples/*.stk781113,899Quick-start demos + worked end-to-end programs (TF-IDF search, mini-SQL executor, Kalman filter, Raft simulator, HTTP router with middleware, pub/sub bus, CSV ETL, …).
Polyglot portsexamples/ruby/, examples/scheme/29,077Reference ports (not counted in Rosetta / Exercism rows).
EXAMPLES TOTAL2,960227,2135 groups under examples/ · ~77 LOC/file avg

The Rosetta corpus is a complete cross-language reference port at 1,349/1,349 (100%). The full test runner (s test examples/project/t examples/rosetta/t) executes 1,751 .stk test files and 12,211 assert_eq / assert_ok checks across those trees; any builtin drift surfaces as a failing test before it reaches the parity suite. New Rosetta tasks land as fast as the canonical site adds them.


+DEPENDENCIES

135 direct dependencies, 680 transitive crates in Cargo.lock. Every dep is audited against the “will this still build cleanly in 2030” bar — foundational crates (serde, regex, libc, nix) over fashionable ones.

Core Runtime

rayon, crossbeam, parking_lot, dashmap, indexmap, thiserror

JIT

cranelift-codegen, cranelift-frontend, cranelift-jit, cranelift-module

Regex

regex, fancy-regex — standard + lookahead/behind / backref engine

Codec

serde_json, serde_yaml, toml, csv, rmp-serde (MessagePack)

Crypto

aes, chacha20, rsa, ed25519-dalek, x25519-dalek, p256, p384, k256, argon2, scrypt, bcrypt, sha2, sha3, blake3, ripemd, md4, md-5

HTTP / Network

ureq, scraper, roxmltree, uuid, TCP / DNS via libc+nix

Output

svg2pdf, pdf-extract, id3, mp3lame-encoder, qrcode

CLI

clap (no default features), itoa, caseless, glob, libc

LSP

lsp-server, serde, serde_derive

System

sysinfo, nix, libc, rand, lru, itertools, totp-lite


;PUBLIC API SURFACE

strykelang ships as both a binary (stryke, st, s) and an embeddable Rust crate (stryke on docs.rs). The public surface stays small relative to internals — embedders get the high-level entry points; the rest is implementation.

SurfaceCountNotes
Public functions874Across the whole repo — pub fn / pub(crate) fn at the crate root
Public structs97Plus internal structs (count varies as internals grow)
Public enums42Plus internal enums (count varies as internals grow)
Impl blocks189Across both data and trait impls
Total fns (any vis)18,041Including methods, closures, and inline fns

?KEY DESIGN DECISIONS

Why stryke looks the way it does. Each call-out below is a decision the implementation could have gone either way on, with the rationale for the path taken.

NaN-Boxed StrykeValue

Every value is one 64-bit word. Tag bits live in the unused mantissa of an IEEE-754 NaN. Saves a pointer indirection on every read; keeps the JIT’s register allocator happy with uniform value width.

Bytecode VM, Not Tree-Walker

An AST tree-walker is simpler but pays a virtual-call cost per node. Bytecode collapses that into a dense dispatch loop, which the CPU’s branch predictor can actually warm up to.

Cranelift, Not LLVM

LLVM gives slightly better steady-state code but compiles 10× slower and pulls in a giant C++ dependency. Cranelift is pure Rust, fast to compile, and ships in Wasmtime — production-tested.

Rayon, Not Tokio

Stryke parallelism is data-parallel, not async-IO-parallel. Rayon’s work-stealing fits pmap/pgrep exactly; tokio’s task model would force every callback into a future.

Streaming Iterators

pmap doesn’t materialize a vec before pgrep consumes it. map_stream.rs threads results pull-side through pipe-forward chains; only the final sink pays the allocation.

Single Cargo Package

Stryke is one crate, three binaries (stryke/st/s) and a library. No workspace, no internal sub-crates — rebuilds are smaller, dependency graph is shallow, embedders see a flat API.

Build-Time Reflection

build.rs scans the dispatcher source and generates %builtins, %aliases, %descriptions. %keywords is a hand-curated parallel table for syntactic names (if, my, sub, fn, …), keeping %builtins callable-only. Source is the single truth; LSP descriptions can’t drift from the actual dispatcher.

Pipe-Forward + Thread Macro

|> and ~> aren’t cosmetic — they’re first-class parser productions that compile into streaming-iterator opcodes. Reading left-to-right is the point; allocation-free chains are the dividend.

AOP at the Call Site

before / after / around register advice into a single Vec<Intercept> on the interpreter. vm_dispatch_user_call takes a one-line glob-match guard before the existing fast-path, so calls with no intercepts pay zero overhead. Around is AspectJ-style: the block’s value is the call’s return; proceed() invokes the original. Same surface as zshrs intercept — one design across CLI and language.

rkyv KV Store (world-first)

First-class CRUD store with rkyv as the on-disk codec. kv_open/kv_put/kv_get/kv_del/kv_exists/kv_keys/kv_scan/kv_len/kv_commit/kv_batch/kv_close/kv_stats ship as native builtins, dispatched via kvstore.rs::builtin_kv_*. Reads are mmap + validate + cast — same primitive script_cache.rs:454 already uses for cached bytecode; no parse, no allocate per row. SQLite-shaped ergonomics, beats SQLite on reads for any store that fits comfortably in RAM. Atomic rewrite on commit (tmp + rename + fsync), versioned format header (STKV magic + format_version) so future stryke versions reject mismatched archives instead of silently mis-deserialising. All-or-nothing kv_batch with in-memory snapshot rollback. World-first: no other scripting language ships a zero-copy archive KV as a core builtin. Phase 2 ships stryke kvd server + remote kv_connect client over the same archive bytes.

Sketch Algebra (world-first)

Probabilistic data structures are first-class operands for + | & ^ -. $bloom_a + $bloom_b = Bloom union, $hll_a + $hll_b = HyperLogLog union, $cms_a + $cms_b = Count-Min counter sum, $topk_a + $topk_b = SpaceSaving merge, $td_a + $td_b = t-digest centroid merge, $rb_a & $rb_b / | / ^ / - = Roaring set algebra. Operators are functional — operand sketches are never mutated. Dispatch lives in vm.rs’s Op::Add / BitOr / BitAnd / BitXor / Sub arms, falling through to try_sketch_binop in sketches.rs before the default numeric path. Shape mismatch falls back cleanly — no panic, no silent truncation. No other language treats these as syntactic primitives.