strykelang 0.12.21

A highly parallel Perl 5 interpreter written in Rust
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
# stryke Package Registry Architecture

## Overview

Stryke's package manager picks the proven winners from a decade of design experiments and skips the legacy mistakes. Specifically:

- **Cargo's model**: single TOML manifest, single deterministic lockfile, immutable registry, semver-aware resolver, integrated CLI for build/test/doc/publish, workspaces as a first-class concept.
- **uv's execution**: Rust-native, parallel resolver, parallel fetch/extract/verify, lockfile-first, milliseconds not minutes.
- **Nix's reproducibility**: every dep hash-pinned in the lockfile, byte-identical results on every machine, no "works on my box" failures.
- **Bundler's restraint**: lockfile is sacred, regenerate explicitly, no surprise version drift between installs.
- **npm's one good idea**: a `[scripts]` table for project-local task running.

Skipped on purpose: per-project deps tree (`node_modules`/`vendor`/`packages`), install-time code execution (`build.rs`/`postinstall`), hoisting, phantom deps, peer deps, mutable registries (no left-pad re-runs), centralized monocultures (private registries are first-class).

**Killer feature**: `s build --release` AOT-compiles your entire program — your code, every dep, the stdlib — through Cranelift to **native machine code**. Output is a single statically-linked ELF/Mach-O/PE binary in `target/release/`. No interpreter on the target machine. No JIT warmup. No bytecode at runtime. Just raw native code, the same kind of artifact Go and Rust produce. SFTP it to a server and run. Perl-grade ergonomics, Go-grade binaries. See [Build Outputs](#build-outputs-one-native-binary-ship-anywhere).

## Project Root Stays Clean

The existing stryke project layout (see `examples/project/`):

```
myproject/
  stryke.toml               # manifest
  stryke.lock               # exact versions + integrity hashes
  main.stk                  # entry point (run with `s` or `s main.stk`)
  lib/                      # module sources, accessed via `require` or `use`
    scanner.stk
    reporter.stk
    ai/
      classifier.stk
  bin/                      # additional executables (auto-discovered as [bin])
    myapp-helper.stk
  t/                        # tests, run with `s test t/`
    test_scanner.stk
    test_reporter.stk
  benches/                  # benchmarks, run with `s bench`
    bench_scanner.stk
    bench_hot_loop.stk
  examples/                 # example programs, built but not published
    quickstart.stk
  target/                   # build outputs (auto-created, gitignored)
    release/
      myapp                 # ← native machine code, statically linked, scp-ready
    debug/
      myapp                 # ← native machine code with debug symbols
    cache/                  # bytecode cache for `s run` JIT (dev only)
  README.md
```

Only `stryke.toml`, `stryke.lock`, and `main.stk` are required. Everything else is convention-discovered when present. Deps live in `~/.stryke/store/` and resolve through the lock file at load time — no directory full of someone else's code in your project tree.

`benches/` is a first-class concern, not an afterthought. Stryke is among the fastest interpreted languages in existence — beating LuaJIT on loop/array/regex workloads, competitive on others — so bench infrastructure ships with the package manager from day one. `s bench` runs every `benches/bench_*.stk` file, captures timing, and emits a comparable report. Performance regressions are caught at PR time, not in production.

## Global Store

```
~/.stryke/
  store/                    # one extracted copy per name@version
    http@1.0.0/
      lib/
      stryke.toml
    json@2.1.0/
    crypto@0.5.0/
  bin/                      # global CLI tools (s install -g)
    mytool -> ../store/mytool@1.0.0/bin/mytool
  cache/                    # downloaded tarballs awaiting extraction
    http-1.0.0.tar.zst
  git/                      # cloned git deps
    github.com-user-mylib-abc123/
  index/                    # registry index mirror (sparse, like cargo's)
```

Paths are human-readable (`name@version`) rather than nix-style hash paths. Hash-pinning happens in the lockfile, not the directory name — you get nix's reproducibility without nix's opaque paths.

## Manifest: stryke.toml

```toml
[package]
name = "myapp"
version = "0.1.0"
description = "My stryke application"
authors = ["user@example.com"]
license = "MIT"
repository = "https://github.com/user/myapp"
edition = "2026"             # language edition pin

[deps]
http = "1.0"                 # semver range
json = "2.1.0"               # exact
crypto = { version = "0.5", features = ["aes"] }
local-lib = { path = "../my-local-lib" }
git-lib = { git = "https://github.com/user/lib", tag = "v1.0.0" }

[dev-deps]
test-utils = "1.0"

[groups.bench]               # bundler-style groups, beyond dev/prod
criterion = "0.5"

[features]
default = ["json"]
json = ["dep:json"]
yaml = ["dep:yaml"]
full = ["json", "yaml"]

[scripts]                    # npm's one good idea
test = "s test t/"
bench = "s bench benches/"
build = "s build --release"  # → target/release/myapp (fat exe)
lint = "s check lib/"

[bin]
myapp = "main.stk"           # executable entry point at project root

# A package that has lib/ is a library. No [lib] table needed —
# the lib/ directory tree is auto-discovered and published as-is.
# Consumers access modules via `use Foo::Bar` → lib/Foo/Bar.stk in the store.

[workspace]                  # first-class from day 1
members = ["crates/*"]

[workspace.deps]             # shared versions across the workspace
http = "1.0"
```

Features are scoped per-package, not unified workspace-wide. A consumer turning on `feature = "yaml"` does not silently flip it on for every other package in the graph — cargo's biggest footgun, fixed.

## Lock File: stryke.lock

```toml
# Auto-generated. Do not edit.
version = 1
stryke = "0.1.0"
resolved = "2026-04-26T12:00:00Z"

[[package]]
name = "http"
version = "1.0.0"
source = "registry+https://registry.stryke.dev"
integrity = "sha256-abc123..."
features = ["default"]
deps = ["json@2.1.0"]

[[package]]
name = "json"
version = "2.1.0"
source = "registry+https://registry.stryke.dev"
integrity = "sha256-def456..."
deps = []

[[package]]
name = "crypto"
version = "0.5.0"
source = "registry+https://registry.stryke.dev"
integrity = "sha256-789ghi..."
features = ["aes"]
deps = ["json@2.1.0"]
```

Sorted deterministically. Two `s install`s from the same lock file on different machines produce byte-identical store contents.

## Commands

One binary, one mental model. Every project task happens through `s`:

```bash
# Project lifecycle
s init                       # interactive new package in cwd
s new myapp                  # new package in ./myapp
s build                      # build (interpreter cache or AOT)
s run                        # build + run main bin
s run myapp                  # run a specific [bin]
s test                       # run tests
s bench                      # run benches
s doc                        # generate docs
s check                      # type/lint without execution
s fmt                        # format
s clean                      # clear local caches

# Dependencies
s install                    # install per stryke.lock
s add http                   # add dep, update lock
s add http@1.0.0             # exact version
s add http --dev             # dev dep
s add http --group=bench     # arbitrary group
s remove http
s update                     # all deps within semver
s update http                # specific
s tree                       # full transitive graph
s outdated                   # what could be bumped
s audit                      # check vuln DB

# Run scripts from [scripts]
s run test
s run build

# Publishing
s publish                    # push to registry
s yank 1.0.0                 # mark version unusable, never delete
s search http                # query registry
s info http                  # package metadata

# Global tools
s install -g mytool
s uninstall -g mytool
s list -g

# Workspace
s vendor                     # opt-in, materialize deps to ./vendor/
                             # (only for offline distribution; never default)
```

`s vendor` exists for one specific use case — shipping a tarball that builds offline without registry access. It is opt-in, never automatic, and is *not* how normal development works.

## Resolution Algorithm

1. Parse `stryke.toml`, collect direct deps + workspace deps + features in scope.
2. Build dependency graph with version constraints.
3. Resolve using PubGrub (same algorithm uv and modern cargo use): preference for highest compatible, deterministic backtracking, clear conflict reports.
4. Verify each `(name, version)` against the lock file's integrity hash if present.
5. Check store for existing extractions.
6. Fetch missing tarballs to `~/.stryke/cache/` (parallel, rayon).
7. Verify hashes (parallel).
8. Extract to `~/.stryke/store/{name}@{version}/` (parallel).
9. Write `stryke.lock`.

No step in this pipeline executes code from any package. Install is pure data movement plus hash verification. The first time a package's code runs is when *your* code imports it, not when `s install` finishes.

## Module Resolution

Two mechanisms, both supported:

**Path-based `require`** (current convention in `examples/project/`):
```stryke
require "./lib/scanner.stk"
```
Resolved relative to the current file. Unaffected by the package manager — works the same whether the file lives in your project, a workspace member, or an extracted store package.

**Namespaced `use`** (for external deps):
```stryke
use Foo::Bar
```
Resolution order:
1. `lib/Foo/Bar.stk` (project-local).
2. Look up `foo` in `stryke.lock`, load `~/.stryke/store/foo@{version}/lib/Bar.stk`.
3. `@INC` system paths.

Lock file is the index — name → version → store path. No symlink farm, no hoisted phantom deps, no "which copy of `lodash` did I actually get."

## Store Sharing

```
project-a/stryke.lock → json@2.1.0 → ~/.stryke/store/json@2.1.0
project-b/stryke.lock → json@2.1.0 → ~/.stryke/store/json@2.1.0
```

Every `(name, version)` exists exactly once on disk per machine. Two thousand projects depending on `json@2.1.0` consume one copy of `json@2.1.0`.

## Build Outputs: One Native Binary, Ship Anywhere

Stryke's flagship capability. `s build --release` is **ahead-of-time native compilation**, full stop. Your code, every dep, and the stdlib all flow through Cranelift to native machine code (x86-64, aarch64, riscv64, etc.) and out to a single statically-linked executable.

What ends up in the binary:

- Your `main.stk` and `lib/` modules — AOT-compiled to native machine code.
- Every dep from `stryke.lock` — same, statically linked from the store.
- The minimal stryke runtime (GC, panic handler, syscall shims) — linked in like Go's runtime, not interpreted.
- Native assets declared in `[package.assets]` — embedded as `.rodata`.

What is **not** in the binary:

- No interpreter.
- No JIT.
- No bytecode at runtime.
- No `.stk` source files.
- No external `.so`/`.dylib`/`.dll` dependencies (unless you opted into dynamic linking).

The output is a real ELF / Mach-O / PE binary. `file myapp` reports it as a native executable. `objdump -d` shows real machine code. It's indistinguishable from a Go or Rust binary on the wire.

### Default Output Paths

```
target/release/<binary>          # native machine code, optimized, statically linked
target/debug/<binary>            # native machine code, debug symbols, less optimization
target/<triple>/release/<binary> # cross-compiled (e.g. x86_64-linux-gnu)
target/cache/*.stkc              # bytecode cache for `s run` JIT mode (dev only)
```

`target/` is auto-created on first build and auto-added to `.gitignore` by `s init`/`s new`. Cargo convention — your existing `.gitignore` rules already cover it.

### Two Execution Modes, One Toolchain

| Mode | Command | Engine | Use case |
|---|---|---|---|
| **JIT** | `s run main.stk` | Cranelift JIT in the VM | Fast iteration, scripts, REPL |
| **AOT** | `s build --release` | Cranelift AOT → linker | Shipping a binary |

The JIT mode is what stryke runs day-to-day during development — sub-millisecond startup, hot paths reach native speed within microseconds. The AOT mode is what you ship to production. Same compiler backend, different output target.

### The SFTP Workflow

```bash
s build --release                              # → target/release/myapp
scp target/release/myapp prod-host:/usr/local/bin/
ssh prod-host /usr/local/bin/myapp
```

That's it. No `pip install`, no `bundle install`, no `npm ci`, no Docker layer cache, no glibc gymnastics (musl target available), no PATH manipulation, no virtualenv. Typical binary is 5-20MB depending on dep count and embedded assets. Startup is microseconds — no interpreter to load, no JIT to warm, no bytecode to deserialize. Just `execve()` and run.

### Cross-Compilation

```bash
s build --release --target=x86_64-linux-gnu       # macOS dev → Linux prod
s build --release --target=aarch64-apple-darwin
s build --release --target=x86_64-pc-windows-gnu
s build --release --target=aarch64-linux-musl     # static linux, no glibc
s build --release --target=wasm32-wasi            # WASM module
```

Cranelift handles the codegen for every target ISA. Linkers and sysroots managed by `s` itself — no `cross` tool, no Docker hack, no `apt install gcc-aarch64-linux-gnu` ceremony.

### Why This Matters

The interpreted-language deployment story has been broken for twenty years:

- **Python**: ship `.py` + `requirements.txt` + pray the target has the right Python. PyInstaller bundles an interpreter and adds 100MB. Nuitka does real C compilation but is fragile on real codebases.
- **Ruby**: ship `.rb` + Gemfile + pray. ruby-packer exists, sees no adoption.
- **Node.js**: ship JS + `package.json` + materialize 200MB of `node_modules/`. `pkg`/`nexe` bundle V8 — still an interpreter, just self-contained.
- **PHP/Perl**: shell out to the system interpreter, hope for the best.

The compiled languages won the deployment war by definition:

- **Go**: native binary, ~10MB, zero deps, the model the entire ops industry lives on.
- **Rust**: native binary, ~5MB stripped, same story.
- **Zig**: native binary, even smaller, cross-compiles trivially.

Stryke is the first language to land on the **compiled side** while keeping interpreted-language ergonomics — sigils, dynamic dispatch, runtime introspection, REPL, hot reload during dev. You get Perl-grade source code productivity, Go-grade binaries on the way out.

`s build` defaults to release for this reason. The killer use case is "ship the binary," not "iterate on the bytecode" — that's what `s run` is for.

## Speed

Designed to match uv. Every install-path step that can parallelize, does:

- Concurrent registry index fetches (sparse index, like cargo 1.70+).
- Concurrent tarball downloads.
- Concurrent SHA-256 verification (rayon).
- Concurrent extraction.
- Resolver itself is parallel where the constraint graph allows.

No build step at install time. No native compilation. Cold install of a 50-dep project should be sub-second on broadband, warm install (everything in store) should be tens of milliseconds.

## Security

- **Immutable registry.** Once `name@version` is published, the bytes are fixed forever. `s yank` marks a version as do-not-resolve but never removes its content. left-pad scenarios are structurally impossible.
- **No install-time code execution.** No `build.rs`, no `postinstall`, no lifecycle hooks. Installing a package cannot run code. Compromised packages can only attack consumers that actually import and execute them.
- **Hash-pinned everything.** Lock file integrity hashes are checked before extraction. Tampered tarballs fail install loud, not silent.
- **Sigstore-style signing** (future). Publishers sign releases; consumers can require signatures from trusted publishers.
- **`s audit`** checks the dep graph against a vulnerability database (RustSec-style advisory feed).
- **Namespacing**: package names are `org/name` to prevent typosquatting. No flat global namespace.

## Reproducibility

Given a `stryke.lock`, two installs on different machines produce identical store contents — bit-for-bit. Verified by:

- Source URL pinned per package.
- SHA-256 of every tarball pinned.
- Resolver version pinned in the lock file (`stryke = "0.1.0"`).
- Resolution timestamp recorded for audit.

This is nix-grade reproducibility without nix-grade UX cost.

## Offline Mode

```bash
s install --offline          # only use cached packages
```

Works if all deps exist in `~/.stryke/store/` or `~/.stryke/cache/`. Combined with `s vendor`, lets you ship a fully offline-buildable archive.

## Workspaces

First-class from day one, not retrofitted:

```toml
# stryke.toml at workspace root
[workspace]
members = ["crates/*"]

[workspace.deps]             # versions inherited by all members
http = "1.0"
json = "2.1.0"

[workspace.package]          # metadata inherited by all members
license = "MIT"
authors = ["user@example.com"]
edition = "2026"
```

Members reference shared versions with `http.workspace = true`. Single lockfile at workspace root. One `s install` resolves the entire monorepo.

## Path Dependencies

```toml
[deps]
mylib = { path = "../mylib" }
```

Path deps load straight from the filesystem, bypassing the store. Edits reflect immediately on the next `s run`.

## Git Dependencies

```toml
[deps]
mylib = { git = "https://github.com/user/mylib" }
mylib = { git = "https://github.com/user/mylib", branch = "dev" }
mylib = { git = "https://github.com/user/mylib", tag = "v1.0.0" }
mylib = { git = "https://github.com/user/mylib", rev = "abc123" }
```

Cloned to `~/.stryke/git/` cache, resolved to a specific commit hash recorded in the lock file. Git deps are pinned in the lock just as tightly as registry deps.

## Registry Protocol

```
https://registry.stryke.dev/
  /api/v1/index/{name}                    # sparse index, single package
  /api/v1/packages/{name}/{version}       # metadata
  /api/v1/packages/{name}/{version}/dl    # tarball
  /api/v1/packages/{name}/{version}/yank  # yank (auth required)
```

Sparse index from day one (cargo took years to ship this). Mirroring, private registries (`registry = "https://my-co.example/"` per dep), auth tokens — standard.

The registry rule that defines the ecosystem: **published versions are immutable**. Yank, never unpublish.

## Implementation Order

1. ✅ `stryke.toml` parser (deps, scripts, bin, workspace). **SHIPPED**
2. ✅ `~/.stryke/store/` and `~/.stryke/cache/` layout. **SHIPPED**
3. ✅ `s install` for path deps only — proves the resolution loop. **SHIPPED**
4. ✅ `s add` / `s remove`. **SHIPPED**
5. ✅ `stryke.lock` generation with integrity hashes. **SHIPPED**
6. ✅ Module resolution integration (lock-driven, store paths). **SHIPPED**
7. ⏳ PubGrub semver resolver — **deferred until registry deployed**.
8. ⏳ Parallel fetch/verify/extract — **deferred until registry deployed**.
9. ⏳ Git deps — **deferred** (clear unimplemented error today).
10. ⏳ Features — partial: per-package feature flags parse and round-trip; resolver-side activation lands with the registry resolver.
11. ✅ Workspaces with shared deps inheritance. **SHIPPED** — `[workspace]` + `members = ["crates/*"]` glob + `{ workspace = true }` inheritance + single root lockfile.
12. ✅ `s install -g` for CLI tools. **SHIPPED** — `s install -g PATH`, `s uninstall -g NAME`, `s list -g`. Launchers go to `~/.stryke/bin/`.
13. ⏳ Sparse registry protocol + first registry deployment. **CLI stubs shipped** (`s search`, `s publish [--dry-run]`, `s yank`); endpoint deployment is the next chunk.
14. ✅ `s publish` (dry-run), `s yank` (stub), `s audit` (stub feed). **CLI shipped, feed/endpoint deferred.**
15. ⏳ Sigstore signing — **deferred until registry deployed**.

Plus the operational commands the RFC's command list calls out: ✅ `s update`, ✅ `s outdated`, ✅ `s vendor`, ✅ `s clean`, ✅ `s run SCRIPT` (npm-style task runner from `[scripts]`).

## Non-Goals

- npm compatibility.
- Node.js interop.
- Peer dependencies.
- Per-project deps directory (no `node_modules`, `vendor`, or `packages` — store-only).
- Hoisting (irrelevant when there is no per-project tree).
- Install-time code execution (no `build.rs`, no `postinstall`).
- Mutable registry (no unpublish, only yank).
- Workspace-wide feature unification (cargo's footgun).
- Phantom deps. Period.