Penelope

A language that knows how to wait. pause as a first-class primitive.

The thesis

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.

The foundational axiom

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.

Three more programs

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

What's in

Five phases shipped. The list below is what's usable today, organized by what a user actually does:

CapabilityWhat it gives youWhere
Pause/resumepause as a first-class expression. Snapshot is a JSON value (gzipped by default). pen run / resume / fork / edit.Tour
Effect system8 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 matchingLiteral / wildcard / var / or / guard / list (incl. ...rest) / dict patterns. Match arms inherit the enclosing tail-position flag for TCO.Tour
Effect typesEvery fn type carries an inferred effect set. pure fn enforces an empty set. pen check --show-effects shows every fn's signature.Tour
Modulesimport "./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 optimizerStack-based, 18 opcodes, TCO. -O0/-O1/-O2 for constant folding, DCE, inline caches, function inlining, peephole.Snapshot
JITBytecode → 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 backendPenelope-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-hostingLexer + parser + compiler written in Penelope (std/lexer.pen, parser.pen, compiler.pen). Three-stage fixpoint proof via pen self-test.Phase 4
Live editingpen edit <snap.penz> remaps a paused VMState onto a recompiled program via source-position lookup, with opcode-kind sanity checks.Phase 4
Time-travel debuggerDAP stepBack + reverseContinue. VMState deep-cloned before every advance into a bounded history stack.Debugger
Distributed runtimeCoordinator + workers, HTTP/JSON protocol, lease + heartbeat, dead-worker recovery via FileStore. Multi-process out of the box.Distributed
ObservabilityOpenTelemetry-shaped Tracer hook in the VM. fn_call / fn_return / effect / pause / resume / error events. pen run --trace emits JSON-lines.Production
Editor integrationFull VSCode extension: syntax, LSP (hover, completions, go-to-def, diagnostics), DAP debugger (breakpoints, variables, time-travel), snippets.Debugger
Toolingpen fmt (comment-preserving) · pen test (doctest) · pen doc (markdown from ///) · pen graph (DOT) · pen new (scaffold) · pen replCLI

613 tests across 55 files. Zero production dependencies (only typescript + vitest as devDeps). Hand-written everything.

Quick start

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.