The whole language in 10 minutes.
Penelope has seven runtime value tags: int, bool, str, unit, closure, list, dict.
42 // int
true // bool
"hello" // str
list_new(1, 2, 3) // list
dict_set(dict_new(), "k", 1) // dict
fn(x) { x + 1 } // closure
let x = 10;
let y = x + 5;
print(to_str(y)); // 15
let add = fn(a, b) { a + b };
print(to_str(add(2, 3))); // 5
// Closures capture lexical scope
let mk_counter = fn(start) {
fn(n) { start + n }
};
let add10 = mk_counter(10);
print(to_str(add10(5))); // 15
let abs = fn(n) {
if (n < 0) { 0 - n } else { n }
};
print(to_str(abs(0 - 7))); // 7
pause is a runtime primitive. When evaluated, it serializes the entire program state and exits the process. A later pen resume picks up exactly where it left off.
// pause.pen
let x = 10;
let y = pause;
print(to_str(x + y));
$ bin/penelope run pause.pen
paused at ip 2 → pause.penz
# process exits — could be hours, days, years later
$ bin/penelope resume pause.penz
10
# x survived; bare pause returns unit; the print fires on resume
All side effects (print, file I/O, network, time, randomness) flow through an effect log. On resume, completed effects are replayed from the log — they don't re-execute. This guarantees idempotency across pause boundaries.
let response = net_fetch("https://example.test/decision");
let ok = wait_for("approval");
write_file("/tmp/audit.log", response);
print("audit complete");
The 8 effects: print, net_fetch, now, random_int, read_file, write_file, wait_until, wait_for.
When a program hits wait_for(name), the runtime pauses. A later pen resume --event name=value injects the value and resumes.
$ pen run hitl.pen # pauses on wait_for("approval")
$ pen resume hitl.penz --event approval=true
$ pen fork pause.penz pause-copy.penz
# Now you have two snapshots. Resume each independently — they diverge.
$ pen inspect pause.penz
Snapshot: pause.penz
version: 3
pausedAtIP: 2 (line 3 col 9)
frames: 1
[0] bindings: { x }
effects: 0 entries
value stack (0): (empty)
Compile with -O2 to enable all 5 passes (constant folding, dead-code elimination, inline caches, function inlining, peephole):
$ pen build -O2 fib.pen
wrote fib.penc (14 opcodes, 2 constants, -O2)
$ pen disasm fib.penc