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
| Module | What it is |
|---|---|
src/lexer.rs | tokenizer (unit tests in-module) |
src/object.rs | Object/Value/Dict; PsArray owns the iterative deep-drop |
src/interp.rs | the 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.rs | graphics state, paths, clip chains, tiny-skia rasterization, multi-page |
src/file.rs | file objects and the decode-filter stack |
src/font.rs, src/type1.rs | the show machinery and the three glyph sources |
src/svg.rs, src/pdf.rs | export recorders mirroring the paint pipeline |
src/window.rs, src/spool.rs | the live window, spool mode, the windowed REPL (desktop only) |
src/wasm.rs, web/pscat.js | the browser build — see JavaScript |
src/halftone.rs | clustered-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.