Penelope — Tour

The whole language in 10 minutes.

Values

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 bindings

let x = 10;
let y = x + 5;
print(to_str(y));  // 15

Functions are values

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

Conditionals

let abs = fn(n) {
  if (n < 0) { 0 - n } else { n }
};
print(to_str(abs(0 - 7)));  // 7

The headline feature: pause

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

Effects: I/O that survives pause

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.

wait_for: external value injection

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

Forking: two futures from one snapshot

$ pen fork pause.penz pause-copy.penz
# Now you have two snapshots. Resume each independently — they diverge.

Time travel via inspect

$ 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)

Optimizations

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