A language that knows how to wait. pause as a first-class primitive.
In ordinary languages, a running program's mid-execution state is ephemeral. If the process dies, the state dies with it. You write checkpoint files, queues, retries, idempotency layers to survive failure.
Modern durable-execution frameworks (Temporal, Inngest, Restate, DBOS) add that infrastructure on top of ordinary languages. They work — and they force you to structure programs around activity/step/await boundaries.
Penelope dissolves these boundaries by making pause/resume a language primitive, not a framework feature.
let x = 10;
let y = pause; // process exits, state saved to disk
print(to_str(x + y)); // a later process resumes, prints 15
The variable x survives across processes. The user wrote no checkpoint, no restore, no await. Just let y = pause. That is the entire ergonomic claim.
Execution is data. A running program is a value.
If runtime state is just JSON, it can be persisted, forked, inspected, migrated, compared, transmitted, scheduled — like any other value. Everything else in Penelope follows from this axiom.
1. A 24-hour approval workflow. Pause until a human says yes; the process can die in between; effects are replayed on resume so audits don't double-fire.
print("Requesting $5000 approval");
let ok = wait_for("approval"); // process exits here, possibly for hours
if (ok) {
let r = net_fetch("https://api.bank/transfer");
write_file("/var/log/audit.log", r);
}
2. Pattern matching with guards and destructuring. Match on literals, lists, dicts, or-alternatives, with optional if guards.
let classify = fn(req) {
match req {
"GET" | "HEAD" => "safe",
n if str_starts_with(n, "POST") => "writes",
_ => "other",
}
};
3. Effect types: pure fn rejects accidental I/O. The type checker tracks every function's effect set; pure hard-fails on any non-empty set.
let square = pure fn(n) { n * n }; // OK
let bad = pure fn(n) { print(to_str(n)); n };
// type error: pure fn body has effects [io] — remove 'pure' or the effectful operations
Five phases shipped. The list below is what's usable today, organized by what a user actually does:
| Capability | What it gives you | Where |
|---|---|---|
| Pause/resume | pause as a first-class expression. Snapshot is a JSON value (gzipped by default). pen run / resume / fork / edit. | Tour |
| Effect system | 8 built-in effects (print, net_fetch, now, random_int, read_file, write_file, wait_until, wait_for) with per-call replay log. Resume never double-fires. | Tour |
| Pattern matching | Literal / wildcard / var / or / guard / list (incl. ...rest) / dict patterns. Match arms inherit the enclosing tail-position flag for TCO. | Tour |
| Effect types | Every fn type carries an inferred effect set. pure fn enforces an empty set. pen check --show-effects shows every fn's signature. | Tour |
| Modules | import "./path.pen"; — file-relative paths, dedup, cycle-safe. Cross-import source maps so diagnostics point at the original file:line. | Tour |
| String interpolation | "hello ${name}, you are ${age + 1}" — arbitrary expressions inside ${...}, desugared to + chains with to_str wrapping. | Tour |
| Bytecode VM + 5-pass optimizer | Stack-based, 18 opcodes, TCO. -O0/-O1/-O2 for constant folding, DCE, inline caches, function inlining, peephole. | Snapshot |
| JIT | Bytecode → JS Function at runtime. Op args baked as literals, BIN_OP specialized per operator. ~2.4× faster than the -O2 interpreter on fib(25). | JIT |
| WASM backend | Penelope-implemented WASM emitter (std/wasm.pen, ~5400 lines of pen) compiles a large subset of Penelope — ints/bools, strings, lists/dicts, closures + first-class fns, match (all 8 pattern kinds), and 6 effects via host imports — to a valid WebAssembly module. Runs anywhere WASM runs — Node, browsers, Cloudflare Workers, Wasmtime. | WASM |
| Self-hosting | Lexer + parser + compiler written in Penelope (std/lexer.pen, parser.pen, compiler.pen). Three-stage fixpoint proof via pen self-test. | Phase 4 |
| Live editing | pen edit <snap.penz> remaps a paused VMState onto a recompiled program via source-position lookup, with opcode-kind sanity checks. | Phase 4 |
| Time-travel debugger | DAP stepBack + reverseContinue. VMState deep-cloned before every advance into a bounded history stack. | Debugger |
| Distributed runtime | Coordinator + workers, HTTP/JSON protocol, lease + heartbeat, dead-worker recovery via FileStore. Multi-process out of the box. | Distributed |
| Observability | OpenTelemetry-shaped Tracer hook in the VM. fn_call / fn_return / effect / pause / resume / error events. pen run --trace emits JSON-lines. | Production |
| Editor integration | Full VSCode extension: syntax, LSP (hover, completions, go-to-def, diagnostics), DAP debugger (breakpoints, variables, time-travel), snippets. | Debugger |
| Tooling | pen fmt (comment-preserving) · pen test (doctest) · pen doc (markdown from ///) · pen graph (DOT) · pen new (scaffold) · pen repl | CLI |
613 tests across 55 files. Zero production dependencies (only typescript + vitest as devDeps). Hand-written everything.
git clone https://github.com/airingursb/Penelope
cd Penelope
npm install && npm run build
# Run a program
bin/penelope run examples/01-toplevel-pause.pen
# Compile to bytecode and disassemble
bin/penelope build -O2 examples/09-fib.pen
bin/penelope disasm examples/09-fib.penc
# Type-check, show effect signatures
bin/penelope check examples/08-24h-agent.pen --show-effects
# Benchmark interpreter vs JIT
bin/penelope bench examples/09-fib.pen
# Self-hosting proof (Penelope compiles Penelope)
bin/penelope self-test
# REPL
bin/penelope repl
# Distributed runtime (3 shells)
bin/penelope coordinator --port 7077 --store ./jobs
bin/penelope worker --coord http://localhost:7077
bin/penelope submit my-workflow.pen --coord http://localhost:7077 --wait
See the Tour for the language, the CLI reference for the full command surface, and the Examples gallery for runnable programs.