Architecture

Three load-bearing ideas explain most of the code. The full design writeups live in the repo — ARCHITECTURE.md, VM.md, FONTS.md — this page is the orientation.

1. The machine is an explicit frame stack

The interpreter (src/interp.rs) never calls itself. Anything that must interleave with PostScript execution is a Frame on an explicit execution stack: scanner frames, loops, stopped boundaries, the show family, images, and a generic PostOp continuation for operators that need a PostScript procedure's result (Separation tint transforms use it). Frame order is program order.

That single decision buys the things pscat is for: stringwidth pushes its result at frame-pop; Type 3 BuildChar and kshow procedures nest arbitrarily; runaway recursion is a catchable execstackoverflow instead of a blown Rust stack; and — the point — the live window and the browser build execute a step budget per frame, so you watch programs draw glyph by glyph. The same begin_source/step_n API drives the winit window, the wasm build, spool mode, and the windowed REPL.

2. Files share one read cursor

The scanner pulls bytes through a file handle; currentfile returns the same handle; filters are files layered on files, decoding on demand so each layer consumes exactly the source bytes its consumer needed (src/file.rs). This one property is what makes real PostScript run: currentfile eexec (Type 1 fonts), the N RD <binary> charstring idiom, currentfile buf readhexstring prologs, and inline image data all fall out of it.

Two conventions were pinned against Ghostscript and must not regress: a token terminated by whitespace consumes exactly that one delimiter byte, and image drains a filter-chain source layer by layer to each end-of-data marker on completion.

3. Every glyph source feeds one transform pipeline

Glyph-space outline → FontMatrix (composed by scalefont/makefont, cached at setfont) → CTM at the pen → a device-space path → the ordinary fill/clip machinery. TrueType outlines (the bundled metric-compatible base fonts), Type 1 charstrings (a hint-ignoring interpreter with flex and seac), and Type 3 glyph procedures all end in the same place. A new glyph source should too.

The VM: save/restore as journaling

save/restore is object-granularity copy-on-write journaling, not snapshotting: the first mutation of an array or dict after a save records the old value; restore replays the journal backwards and does a grestoreall to the save point. Strings are exempt by spec (PLRM §3.7.3.2 — pinned in Ghostscript first, like everything else). Names are interned (src/name.rs): dicts key by a u32 through a one-multiply hasher, and name execution carries only the id.

Crate map

ModuleWhat it is
src/lexer.rstokenizer (unit tests in-module)
src/object.rsObject/Value/Dict; PsArray owns the iterative deep-drop
src/interp.rsthe frame-stack machine; begin_source/step_n; the save/restore journal
src/ops/operators, grouped like the operator summary in the PostScript Language Reference Manual (PLRM)
src/gfx.rsgraphics state, paths, clip chains, tiny-skia rasterization, multi-page
src/file.rsfile objects and the decode-filter stack
src/font.rs, src/type1.rsthe show machinery and the three glyph sources
src/svg.rs, src/pdf.rsexport recorders mirroring the paint pipeline
src/window.rs, src/spool.rsthe live window, spool mode, the windowed REPL (desktop only)
src/wasm.rs, web/pscat.jsthe browser build — see JavaScript
src/halftone.rsclustered-dot screening for the laser-printer look

Deliberate deviations

All documented in place and in NOTES.md; the standing ones: stroke width approximates anisotropic pens as √|det CTM|; access control (executeonly) isn't modeled; Type 1 hints are ignored; errordict handlers aren't consulted; integers are i64; on wasm the clocks read 0. Where the PLRM allows latitude, found files should render, not error — and every such choice is recorded.