Extending the interpreter

The repo is built to be picked up mid-stream — by a person or by a coding agent. Start with HANDOFF.md (orientation and gotchas), then AGENTS.md (working conventions). Contribute lists the rest.

Ghostscript is the oracle

Every semantics question gets settled the same way: write a minimal snippet, run it in gs, pin the answer in a test comment, then implement. This workflow caught real design errors more than once — glyph caching by encoded name, the token-delimiter rule, rectclip clearing the current path. When pscat and gs disagree, the disagreement gets a minimal repro in tests/ before it gets a fix.

gs -dNODISPLAY -q <<'EOF'
newpath 100 100 moveto  5 5 10 20 rectfill
{currentpoint} stopped { (path lost) = } { (path kept) = pop pop } ifelse
EOF

Adding an operator

The src/ops/ modules are deliberately formulaic — copying the shape of ops/arith.rs is most of the job:

  1. Pick the module matching the PLRM's operator grouping (or add one); register in its install with op(dict, "name", func).
  2. Operators are fn(&mut Interp) -> Result<(), PsError>; pop operands (pop_f64, pop_int, pop + match on Value), do the work, push results. Errors use the standard PostScript names.
  3. If the operator mutates an array or dict, call journal_array/journal_dict first so restore can roll it back (strings exempt by spec — read docs/dev/VM.md before touching mutators).
  4. If it needs to run a PostScript procedure mid-operator (callbacks, tint transforms), do not call the machine recursively — push a Frame::PostOp continuation. The Separation color implementation is the pattern.
  5. Tests: tests/eval.rs-style operand-stack cases for language ops, pixel tests for rendering, and a gs pin for anything with semantic latitude.

The gates

Before calling anything done:

cargo test                      # all suites, including gs cross-checks where installed
cargo clippy --all-targets      # clean, no exceptions
cargo fmt --check

Rendering changes get pixel or golden tests (tests/golden.rs renders the examples in both pscat and gs and compares block-downsampled output). Commit per coherent chunk, keep the README and docs/dev/HANDOFF.md in step with the change, and leave a short note in notes/ for whoever picks it up next.

Which model for which task (agent sessions)

Tasks are labelled with the least-capable model expected to do them well: Haiku for mechanical, well-specified work; Sonnet for the workhorse middle; Opus for anything touching the object model, the machine's control flow, or profiling-guided rewrites. The escalation rule: if a smaller-tagged task turns out to touch object.rs or the frame machinery, stop and record the finding in a note under notes/ rather than improvising.

Gotchas that will bite you