Every subcommand of bin/penelope.
$ pen build [-O0|-O1|-O2] foo.pen # source → foo.penc bytecode
$ pen exec foo.penc # run pre-compiled bytecode
$ pen run [-O0|-O1|-O2] foo.pen # compile in memory + run (auto-build)
$ pen resume foo.penz [--time N] [--no-replay] [--event N=V]
$ pen fork src.penz dst.penz # cp snapshot
$ pen disasm foo.penc # pretty-print bytecode
$ pen inspect foo.penz # render v3 snapshot
$ pen check foo.pen # static type check
$ pen profile [-O0|-O1|-O2] foo.pen # opcode counts + hot ips
$ pen bench foo.pen # compare -O0/-O1/-O2 timings
$ pen repl # interactive REPL
$ pen fmt [--write] foo.pen # AST-based code formatter
$ pen test foo.pen # doctest runner (// EXPECT: lines)
$ pen run --watch foo.pen # re-run on file change
| Flag | Where | Effect |
|---|---|---|
-O0 / -O1 / -O2 | build, run, profile | Optimization level (default -O1) |
--time N | run, resume | Override now() to N (ms since epoch) |
--no-replay | run, resume | Skip effect log; re-execute everything |
--event NAME=VAL | resume | Inject value for wait_for("NAME") |
--watch | run | Re-run on file change; clears screen between runs |
--write | fmt | Rewrite the file in place (without it: prints to stdout) |
NO_COLOR=1 env | any | Disable ANSI colors in error output |
Idempotent formatter — fmt(fmt(x)) === fmt(x). Round-trips through the AST, so comments are dropped (they don't survive parsing).
$ pen fmt examples/10-sort.pen # prints to stdout
$ pen fmt --write examples/10-sort.pen # rewrites in place
Annotate expected stdout lines with // EXPECT: <text> or // EXPECTS: <prefix>. pen test runs the program and asserts each emitted line matches in order.
// add.pen
print(to_str(1 + 2));
// EXPECT: 3
print("ok");
// EXPECT: ok
$ pen test add.pen
✓ add.pen (2 expectations)
Re-runs on file save. Clears the terminal between runs. Useful for tight feedback loops.
$ pen run --watch examples/09-fib.pen
watching examples/09-fib.pen (Ctrl-C to exit)
6765
[15:23:01] waiting for changes
# edit file...
6766
[15:23:14] waiting for changes
All compile-time and run-time errors come with a Rust-style source pointer:
$ pen run broken.pen
error: undefined variable 'foo'
--> broken.pen:3:9
|
3 | let x = foo + 1;
| ^
# Build with optimizations, then run the bytecode directly
$ pen build -O2 examples/09-fib.pen
wrote examples/09-fib.penc (14 opcodes, 2 constants, -O2)
$ pen exec examples/09-fib.penc
6765
# Run + pause + resume cycle
$ pen run examples/07-wait-for.pen
waiting for approval
paused at ip 5 → examples/07-wait-for.penz
$ pen resume examples/07-wait-for.penz --event approval=true
got: true
# Profile a recursive program
$ pen profile examples/09-fib.pen
profile: examples/09-fib.pen (-O1, 15.47 ms)
opcode counts (top 20):
LOAD_VAR 76618 22.6%
BIN_OP 54726 16.1%
...
# Static type check
$ pen check broken.pen
type error: binop '+' requires int+int or str+str, got int+str at line 1 col 11
1 type error
# Interactive
$ pen repl
pen> let x = 42
pen> x * 2
84