stet_core/context.rs
1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Execution context: stacks, storage, operator table, and state.
6
7use std::io::Write;
8
9use crate::device::OutputDevice;
10use crate::dict::DictKey;
11use crate::display_list::{DisplayList, GroupParams, SoftMaskParams};
12use crate::dual_array_store::DualArrayStore;
13use crate::dual_dict_store::DualDictStore;
14use crate::dual_string_store::DualStringStore;
15use crate::error::PsError;
16use crate::file_store::FileStore;
17use crate::graphics_state::{GraphicsState, Matrix, PathSegment, PatternData, PsPath};
18use crate::name::NameTable;
19use crate::object::{EntityId, NameId, ObjFlags, PsObject, PsValue, SaveLevel};
20use crate::save_stack::{SaveRecord, SaveStack, StoreType};
21use crate::stack::Stack;
22
23/// Operator table entry: function pointer + name.
24pub struct OpEntry {
25 pub func: fn(&mut Context) -> Result<(), PsError>,
26 pub name: NameId,
27}
28
29/// Pre-interned `NameId`s for frequently-used names in hot paths.
30pub struct NameCache {
31 pub n_def: NameId,
32 pub n_true: NameId,
33 pub n_false: NameId,
34 pub n_null: NameId,
35 pub n_mark: NameId,
36 // Font-related names
37 pub n_font_name: NameId,
38 pub n_font_type: NameId,
39 pub n_font_matrix: NameId,
40 pub n_font_bbox: NameId,
41 pub n_encoding: NameId,
42 pub n_char_strings: NameId,
43 pub n_private: NameId,
44 pub n_fid: NameId,
45 pub n_paint_type: NameId,
46 pub n_subrs: NameId,
47 pub n_len_iv: NameId,
48 pub n_notdef: NameId,
49 pub n_metrics: NameId,
50 pub n_font_directory: NameId,
51 // Resource system names
52 pub n_find_resource: NameId,
53 pub n_define_resource: NameId,
54 pub n_undef_resource: NameId,
55 pub n_resource_status: NameId,
56 pub n_resource_for_all: NameId,
57 pub n_category: NameId,
58 pub n_instance_type: NameId,
59 pub n_resource_dir: NameId,
60 pub n_resource_ext: NameId,
61 // Type 3 font names
62 pub n_build_char: NameId,
63 pub n_build_glyph: NameId,
64 // PaintType 2 / WMode support
65 pub n_stroke_width: NameId,
66 pub n_wmode: NameId,
67}
68
69/// Loop state for `for`, `repeat`, `loop`, and `forall`.
70pub struct LoopState {
71 pub loop_type: LoopType,
72 pub proc_entity: EntityId,
73 pub proc_start: u32,
74 pub proc_len: u32,
75
76 // for/repeat state
77 pub counter: f64,
78 pub increment: f64,
79 pub limit: f64,
80 pub use_int: bool,
81
82 // forall state
83 pub source: PsObject,
84 pub index: u32,
85 /// Snapshot of dict keys for dict forall (avoids re-collecting every iteration).
86 pub dict_keys: Option<Vec<DictKey>>,
87
88 // pathforall state
89 pub path_segments: Option<Vec<PathSegment>>,
90 pub path_procs: Option<[PsObject; 4]>, // [move, line, curve, close]
91 pub path_ictm: Option<Matrix>,
92}
93
94/// Type of loop iteration.
95pub enum LoopType {
96 For,
97 Repeat,
98 Loop,
99 Forall,
100 PathForall,
101}
102
103/// Function pointer type for synchronous procedure execution.
104/// Set by the engine crate to enable inline PS procedure calls from operators.
105pub type ExecSyncFn = fn(&mut Context, PsObject) -> Result<(), PsError>;
106
107/// Key for [`Context::cie_decode_cache`].
108///
109/// The two leading words are a 128-bit structural fingerprint of the decode
110/// procedure (including whatever the dict stack currently binds its
111/// executable names to); the remainder is the sample count and the endpoints
112/// of the sampled range, as raw bits so the key stays hashable.
113pub type CieDecodeKey = (u64, u64, u32, u64, u64);
114
115pub struct Context {
116 // Stacks
117 pub o_stack: Stack,
118 pub e_stack: Stack,
119 pub d_stack: Vec<EntityId>,
120
121 // Storage
122 pub strings: DualStringStore,
123 pub arrays: DualArrayStore,
124 pub dicts: DualDictStore,
125 pub names: NameTable,
126 pub files: FileStore,
127
128 // Loop state storage (indexed by EntityId)
129 pub loops: Vec<LoopState>,
130
131 // Operator table
132 pub operators: Vec<OpEntry>,
133
134 // Well-known dict IDs
135 pub systemdict: EntityId,
136 pub globaldict: EntityId,
137 pub userdict: EntityId,
138 pub errordict: EntityId,
139 pub dollar_error: EntityId,
140
141 // State
142 pub rand_state: u64,
143 /// Last value handed to `srand`, returned by `rrand`. Widened with
144 /// [`PsValue::Int`]: `rrand` must return exactly what `srand` was given.
145 pub rand_seed: i64,
146 /// Current source line number (1-based), updated during scanning.
147 pub current_source_line: u32,
148 /// Packing mode for array/procedure creation (setpacking/currentpacking).
149 pub packing_mode: bool,
150 /// Echo mode for %lineedit/%statementedit (PLRM echo operator).
151 pub echo: bool,
152
153 // Pre-interned names
154 pub name_cache: NameCache,
155
156 // Output: writer for print/= operators (allows capture in tests)
157 pub stdout: Box<dyn Write>,
158
159 // VM save/restore
160 pub save_stack: SaveStack,
161 /// Save stack depth when the current job started (for startjob condition 3).
162 pub job_start_save_depth: usize,
163
164 // VM allocation mode: true = global, false = local
165 pub vm_alloc_mode: bool,
166
167 /// Binary object format (0-4). Default 0.
168 pub object_format: i32,
169
170 // Error dispatch state
171 pub current_operator: Option<NameId>,
172 /// Whether `nulldevice` was installed at any point during this job.
173 ///
174 /// Sticky for the job, and deliberately not cleared by a `grestore` that
175 /// puts a real device back: it records intent, not current state. A
176 /// program that asked for the null device said it wants no output, so
177 /// marks it leaves unemitted at end of job are expected rather than a
178 /// dropped page worth reporting. The PS test suite is the motivating
179 /// case — its files paint into `gsave nulldevice ... grestore` to
180 /// exercise operators, never call `showpage`, and must not be nagged
181 /// about it.
182 pub null_device_used: bool,
183 pub in_error_handler: bool,
184 /// True during init script execution — relaxes access checks.
185 pub initializing: bool,
186 /// When true, PS programs can change HWResolution via setpagedevice.
187 /// Set by WASM frontend; CLI leaves false to keep DPI under user control.
188 pub allow_ps_resolution: bool,
189
190 /// Process exit code requested by the running PS program via the
191 /// `.quitwithcode` operator. `None` means "use the default" (0 on
192 /// success). The CLI reads this on `PsError::Quit` and propagates
193 /// to `std::process::exit`.
194 pub exit_code: Option<i32>,
195
196 // Graphics state
197 pub gstate: GraphicsState,
198 pub gstate_stack: Vec<crate::graphics_state::GstateEntry>,
199 /// Storage for gstate objects (PsValue::Gstate indexes into this).
200 pub gstate_store: Vec<GraphicsState>,
201 pub device: Option<Box<dyn OutputDevice>>,
202 pub display_list: DisplayList,
203 /// Stack of active transparency-group capture frames. While non-empty,
204 /// paint operators emit into the topmost frame's display list instead
205 /// of `display_list`. `endtransparencygroup` pops the top frame and
206 /// emits a [`stet_graphics::display_list::DisplayElement::Group`] into
207 /// the next-innermost target. See `op_begintransparencygroup` /
208 /// `op_endtransparencygroup` in `stet-ops::transparency_ops`.
209 pub group_stack: Vec<GroupFrame>,
210 /// `group_stack.len()` recorded at each `save`. `restore` consults
211 /// this to refuse a revert that would unwind across an unbalanced
212 /// `begintransparencygroup` / `endtransparencygroup` pair.
213 pub save_group_depths: rustc_hash::FxHashMap<u32, usize>,
214 /// Registry of OCGs (PDF Optional Content Groups) declared via the
215 /// `defineocg` operator. Keyed by the interned `NameId` of the
216 /// human-readable layer name from the OCG dict's `/Name` entry.
217 /// `beginoptionalcontent` looks up an OCG by name to obtain the
218 /// `ocg_id` and `default_visible` it embeds into the emitted
219 /// [`stet_graphics::display_list::OcgVisibility::Single`].
220 pub ocg_registry: rustc_hash::FxHashMap<NameId, OcgRecord>,
221 /// Monotonic counter feeding [`OcgRecord::ocg_id`]. Each call to
222 /// `defineocg` increments this; ids never recycle.
223 pub next_ocg_id: u32,
224 /// Document-level structural data — outline, annotations, metadata,
225 /// page boxes, etc. — parallel IR to [`display_list`](Self::display_list).
226 /// PostScript `pdfmark` operators populate this; the PDF output device
227 /// drains it at end-of-job; non-PDF devices ignore it. Document-global:
228 /// `save` / `restore` do not roll this back. See
229 /// [`stet_graphics::document_structure`].
230 pub doc_structure: stet_graphics::document_structure::DocumentStructure,
231 /// When `Some`, each showpage clones the display list here before consuming it.
232 /// Used by the WASM frontend to retain display lists for viewport re-rendering.
233 /// Each entry is (DisplayList, dpi) where dpi is from the pagedevice HWResolution.
234 pub capture_display_lists: Option<Vec<(DisplayList, f64)>>,
235 /// When `Some`, each showpage sends a clone of the display list through this channel.
236 /// Used by the CLI viewer for incremental display list delivery.
237 /// Tuple: `(DisplayList, dpi, page_width, page_height,
238 /// effective_cmyk_bytes, cmyk_proofing)`. PostScript pages always pass
239 /// `None`/`false` (no PDF/X concept); PDF pages may set these from the
240 /// document's OutputIntent context.
241 pub display_list_sender: Option<
242 std::sync::mpsc::Sender<(
243 DisplayList,
244 f64,
245 u32,
246 u32,
247 Option<std::sync::Arc<Vec<u8>>>,
248 bool,
249 )>,
250 >,
251 pub page_width: u32,
252 pub page_height: u32,
253 pub output_path: Option<String>,
254 /// Page filter: if set, only render pages in this set (1-based).
255 pub page_filter: Option<std::collections::HashSet<i32>>,
256 /// Factory closure for creating raster devices (registered by CLI).
257 #[allow(clippy::type_complexity)]
258 pub device_factory: Option<Box<dyn Fn(u32, u32) -> Box<dyn OutputDevice>>>,
259
260 // Font system
261 pub font_directory: EntityId,
262 pub font_resource_path: Option<String>,
263 pub next_fid: i32,
264
265 // Resource system
266 pub global_resources: EntityId,
267 pub local_resources: EntityId,
268 pub category_registry: EntityId,
269 pub resource_base_path: Option<String>,
270
271 // Parameter system
272 pub user_params: EntityId,
273 pub system_params: EntityId,
274
275 /// Backing dict for the `internaldict` operator.
276 ///
277 /// Created during bootstrap rather than on first use. `Context` holds the
278 /// `EntityId` for the whole life of the interpreter, so the dict has to
279 /// outlive every `restore`; creating it lazily inside a save bracket would
280 /// leave this handle pointing at storage that `restore` reclaims. Entries
281 /// written into it after a `save` are still reverted normally, by the
282 /// dict's own copy-on-write.
283 pub internaldict: EntityId,
284
285 // ICC color profile cache
286 pub icc_cache: crate::icc::IccCache,
287
288 // Synchronous procedure execution (set by engine crate)
289 pub exec_sync_fn: Option<ExecSyncFn>,
290
291 // Character width set by setcachedevice/setcharwidth during BuildChar execution
292 pub char_width: Option<(f64, f64)>,
293 // Mode 1 metrics from setcachedevice2: ((w1x, w1y), (vx, vy))
294 pub char_width_mode1: Option<((f64, f64), (f64, f64))>,
295
296 // Glyph path cache: per-font charstring interpretation results
297 pub glyph_caches: rustc_hash::FxHashMap<EntityId, crate::glyph_cache::GlyphCache>,
298 // Type 3 cache mode: set by setcachedevice/setcharwidth during BuildChar
299 pub char_cache_mode: Option<crate::glyph_cache::Type3CacheMode>,
300
301 // CID passed from cshow to nested show call for Type 0 composite fonts
302 pub cshow_pending_cid: Option<i32>,
303
304 /// Set while a Type 3 glyph procedure runs under `charpath`.
305 ///
306 /// PLRM: `charpath` "obtains the path for the glyph outlines that would
307 /// result if string were shown"; for a Type 3 font that means running the
308 /// glyph procedure without painting. While this is `Some`, `fill`,
309 /// `eofill` and `stroke` contribute the path they were given here instead
310 /// of marking the page — in particular `stroke` contributes the path as
311 /// constructed, since `charpath`'s own boolean operand, not the glyph
312 /// procedure, decides whether the result gets stroked.
313 pub charpath_capture: Option<PsPath>,
314
315 // Pattern/form support
316 /// Storage for pattern instances created by `makepattern`.
317 pub pattern_store: Vec<PatternData>,
318 /// Cache of form display lists keyed by dict EntityId.
319 pub form_cache: rustc_hash::FxHashMap<EntityId, DisplayList>,
320
321 /// Memo of sampled CIE decode tables, keyed by a structural fingerprint
322 /// of the decode procedure together with the sampled range.
323 ///
324 /// Sampling one table runs the procedure 256 times, and a CIE colour
325 /// space installs up to six of them, so a file that re-installs the same
326 /// space per page (what `pdftops` emits for every ICCBased space) pays
327 /// thousands of `exec_sync` calls per page. Worse, decode procedures
328 /// routinely contain inline array literals, and every evaluation
329 /// allocates a fresh array in the non-reclaiming array arena — turning
330 /// the repeated sampling into unbounded memory growth.
331 ///
332 /// Memoising is sound because the PLRM already requires a CIE decode
333 /// procedure to be a pure function of its single input: sampling it at
334 /// 256 points and interpolating (which this code has always done) is the
335 /// same assumption. See `stet_ops::color_ops::eval_decode_table_range`.
336 pub cie_decode_cache: rustc_hash::FxHashMap<CieDecodeKey, Vec<f64>>,
337
338 // Timing
339 pub start_time: Option<std::time::Instant>,
340
341 // Name resolution cache: invalidated on begin/end/def
342 pub dict_version: u64,
343 /// Name resolution cache indexed by NameId. Each entry is (dict_version, resolved_object).
344 /// Public for inline cache checks in the eval loop's hot path.
345 pub name_resolve_cache: Vec<(u64, PsObject)>,
346
347 /// When set, the eval loop aborts with `PsError::Quit` on the next iteration.
348 /// Used by the interactive viewer to cancel an in-flight parse when the
349 /// user drops a new file.
350 pub interrupt_flag: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
351
352 /// When true, each successful `showpage` / `copypage` sets `interrupt_flag`
353 /// after capturing the display list, so the eval loop yields back to the
354 /// caller one page at a time. The caller clears the flag and re-enters
355 /// `eval` to drive the next page. Used by the WASM viewer to stream
356 /// multi-page PostScript documents: page 1 renders while pages 2..N are
357 /// still pending interpretation. Requires `interrupt_flag` to be set.
358 pub yield_after_showpage: bool,
359}
360
361/// One frame on `Context::group_stack`. Captures paint operators emitted
362/// between a `begin*` and a matching close. The active capture target is
363/// always [`Self::display_list`]; what happens to it on close depends on
364/// [`Self::kind`].
365pub struct GroupFrame {
366 /// Paint operators emitted while this frame is on top of
367 /// `group_stack`. The semantics on close depend on `kind`.
368 pub display_list: DisplayList,
369 /// What this frame represents — transparency group, soft-mask
370 /// builder, or post-`endsoftmask` masked-content scope.
371 pub kind: GroupKind,
372 /// `gstate.clip_path_version` snapshot taken when the frame opened.
373 /// Reserved for future use (e.g. detecting clip changes that
374 /// crossed the boundary).
375 pub saved_clip_path_version: u32,
376 /// `gstate_stack.len()` at the moment the frame opened. Used by
377 /// `gsave` / `grestore` and `restore` to refuse pops that would
378 /// orphan this frame.
379 pub saved_gsave_depth: usize,
380}
381
382/// What a [`GroupFrame`] is capturing, determining what gets emitted
383/// when it closes.
384pub enum GroupKind {
385 /// Opened by `begintransparencygroup`. On close, the captured
386 /// `display_list` becomes the children of a
387 /// `DisplayElement::Group` with these `params`.
388 Transparency { params: GroupParams },
389 /// Opened by `beginsoftmask`. While active, paint ops emit into the
390 /// frame's `display_list` to build the mask form. `endsoftmask`
391 /// transmutes the frame to [`Self::Masked`] without popping.
392 SoftMask { params: SoftMaskParams },
393 /// Implicitly opened by `endsoftmask`. While active, paint ops emit
394 /// into `display_list` as the *content* the mask attenuates. On
395 /// `clearsoftmask` the frame pops and emits a
396 /// `DisplayElement::SoftMasked` carrying `mask`, the captured
397 /// content, and `params`.
398 Masked {
399 mask: DisplayList,
400 params: SoftMaskParams,
401 },
402 /// Opened by `beginoptionalcontent`. On close, the captured
403 /// `display_list` becomes the children of a
404 /// `DisplayElement::OcgGroup` whose visibility is
405 /// `OcgVisibility::Single { ocg_id, default_visible }`.
406 OptionalContent { ocg_id: u32, default_visible: bool },
407}
408
409/// One entry in `Context::ocg_registry`. `defineocg` allocates these
410/// and indexes them by the OCG's interned `NameId` so
411/// `beginoptionalcontent` can resolve a name back to its `ocg_id` and
412/// the `default_visible` flag the producer set.
413#[derive(Clone, Debug)]
414pub struct OcgRecord {
415 /// Monotonic id assigned by `defineocg`. Embedded into
416 /// `OcgVisibility::Single` on the display list.
417 pub ocg_id: u32,
418 /// Initial visibility, used by the renderer when no `LayerSet`
419 /// override exists for this OCG.
420 pub default_visible: bool,
421}
422
423/// Does `data` contain a complete zlib/deflate stream?
424///
425/// Used to stop draining a procedure data source that feeds `FlateDecode` and
426/// cycles rather than ever returning the empty end-of-data string.
427fn is_flate_stream_complete(data: &[u8]) -> bool {
428 let mut decomp = flate2::Decompress::new(true);
429 let mut out = [0u8; 8192];
430 let mut pos = 0;
431 loop {
432 if pos >= data.len() {
433 return false;
434 }
435 match decomp.decompress(&data[pos..], &mut out, flate2::FlushDecompress::None) {
436 Ok(flate2::Status::StreamEnd) => return true,
437 Ok(_) => {
438 let new_pos = decomp.total_in() as usize;
439 if new_pos == pos {
440 return false;
441 }
442 pos = new_pos;
443 }
444 Err(_) => return false,
445 }
446 }
447}
448
449impl Context {
450 /// Execute a PostScript procedure synchronously and return.
451 pub fn exec_sync(&mut self, proc_obj: PsObject) -> Result<(), PsError> {
452 let f = self.exec_sync_fn.expect("exec_sync not initialized");
453 f(self, proc_obj)
454 }
455
456 /// Run any not-yet-executed procedure data source underneath `entity`,
457 /// replacing it with the bytes it produces.
458 ///
459 /// A filter's data source may be a procedure (PLRM 3.8.4), which the
460 /// filter is supposed to call for more data as the consumer reads. Running
461 /// it when `filter` is *called* instead is observably wrong: the procedure
462 /// runs against whatever is on the operand stack at that moment. `pdftops`
463 /// builds an inline image as
464 ///
465 /// ```postscript
466 /// << /ImageType 1 ... /DataSource { pdfImStr } /LZWDecode filter >> imagemask
467 /// ```
468 ///
469 /// so `filter` is reached while the enclosing `<< ... >>` is still on the
470 /// stack, and `pdfImStr` — which reads its `array index` state off the
471 /// stack — would pick up the half-built dictionary instead.
472 ///
473 /// So the procedure is run here, from the read path, when the consumer's
474 /// operands are the ones in place. **Every entry point that reads from a
475 /// file must call this first**; see [`FileHandle::PendingProc`].
476 ///
477 /// The drain is one-shot rather than incremental: the procedure is run to
478 /// completion and the result installed as a plain byte source. That keeps
479 /// the filters themselves unchanged — none of them has to cope with a
480 /// source that is temporarily dry — while still running the procedure at
481 /// the right moment, which is what the bug above is about.
482 pub fn pump_proc_sources(&mut self, entity: EntityId) -> Result<(), PsError> {
483 // A chain can hold more than one procedure source, so loop until the
484 // walk reports none left. `pending_proc_source` short-circuits on a
485 // counter, so this costs one integer compare on the overwhelmingly
486 // common path where no procedure source exists at all.
487 while let Some((src, proc, flate_above)) = self.files.pending_proc_source(entity) {
488 let data = self.drain_proc_source(proc, flate_above)?;
489 self.files.install_proc_data(src, data);
490 }
491 Ok(())
492 }
493
494 /// Call a procedure data source until it signals end of data.
495 ///
496 /// Per PLRM the procedure pushes a string each call and an empty string
497 /// means end of data. `flate_above` additionally stops once the collected
498 /// bytes form a complete deflate stream: a procedure feeding `FlateDecode`
499 /// may cycle indefinitely rather than ever returning the empty string.
500 ///
501 /// KNOWN LIMITATION: not every procedure signals end of data at all.
502 /// `pdftops` emits paging readers of the form
503 ///
504 /// ```postscript
505 /// { dup 65535 ge { pop 1 add 0 } if 2 index 2 index get 1 index get exch 1 add exch }
506 /// ```
507 ///
508 /// which walk an array of blocks and simply run off the end — they rely on
509 /// the *consumer* to stop asking once the image has all its rows, and a
510 /// full drain has no such stopping point. Running the procedure truly on
511 /// demand, one call per refill, is what those need. That requires
512 /// `refill_filter` to be able to re-enter the interpreter, and every
513 /// `refill_*` to tell "source temporarily dry" apart from EOF so it does
514 /// not latch `eof` on the first short read.
515 fn drain_proc_source(
516 &mut self,
517 procedure: PsObject,
518 flate_above: bool,
519 ) -> Result<Vec<u8>, PsError> {
520 /// Cap on what one procedure data source may produce (64 MB).
521 const MAX_PROC_BYTES: usize = 64 * 1024 * 1024;
522
523 let mut data = Vec::new();
524 loop {
525 let depth_before = self.o_stack.len();
526 self.exec_sync(procedure)?;
527
528 if self.o_stack.len() <= depth_before {
529 break;
530 }
531 let result = self.o_stack.peek(0)?;
532 match result.value {
533 PsValue::String { entity, start, len } => {
534 let bytes = self.strings.get(entity, start, len).to_vec();
535 self.o_stack.pop()?;
536 if bytes.is_empty() {
537 break; // end of data per PLRM
538 }
539 data.extend_from_slice(&bytes);
540 if flate_above && is_flate_stream_complete(&data) {
541 break;
542 }
543 if data.len() >= MAX_PROC_BYTES {
544 break;
545 }
546 }
547 // Anything other than a string is treated as end of data.
548 _ => break,
549 }
550 }
551 Ok(data)
552 }
553
554 /// Create a new context with empty stacks and stores.
555 /// Call `build_system_dict` afterward to populate operators.
556 pub fn new() -> Self {
557 let mut names = NameTable::new();
558
559 let name_cache = NameCache {
560 n_def: names.intern(b"def"),
561 n_true: names.intern(b"true"),
562 n_false: names.intern(b"false"),
563 n_null: names.intern(b"null"),
564 n_mark: names.intern(b"mark"),
565 n_font_name: names.intern(b"FontName"),
566 n_font_type: names.intern(b"FontType"),
567 n_font_matrix: names.intern(b"FontMatrix"),
568 n_font_bbox: names.intern(b"FontBBox"),
569 n_encoding: names.intern(b"Encoding"),
570 n_char_strings: names.intern(b"CharStrings"),
571 n_private: names.intern(b"Private"),
572 n_fid: names.intern(b"FID"),
573 n_paint_type: names.intern(b"PaintType"),
574 n_subrs: names.intern(b"Subrs"),
575 n_len_iv: names.intern(b"lenIV"),
576 n_notdef: names.intern(b".notdef"),
577 n_metrics: names.intern(b"Metrics"),
578 n_font_directory: names.intern(b"FontDirectory"),
579 // Resource system
580 n_find_resource: names.intern(b"FindResource"),
581 n_define_resource: names.intern(b"DefineResource"),
582 n_undef_resource: names.intern(b"UndefineResource"),
583 n_resource_status: names.intern(b"ResourceStatus"),
584 n_resource_for_all: names.intern(b"ResourceForAll"),
585 n_category: names.intern(b"Category"),
586 n_instance_type: names.intern(b"InstanceType"),
587 n_resource_dir: names.intern(b"ResourceDir"),
588 n_resource_ext: names.intern(b"ResourceExtension"),
589 n_build_char: names.intern(b"BuildChar"),
590 n_build_glyph: names.intern(b"BuildGlyph"),
591 n_stroke_width: names.intern(b"StrokeWidth"),
592 n_wmode: names.intern(b"WMode"),
593 };
594
595 let mut strings = DualStringStore::new();
596 let mut dicts = DualDictStore::new();
597
598 // Only systemdict is pre-allocated in Rust — it's needed to register native
599 // operators. All other well-known dicts (globaldict, userdict, errordict, $error,
600 // FontDirectory) are created by the init scripts in sysdict.ps.
601 //
602 // `allocate_at_level_zero` is correct here and only here: no `save` can be
603 // outstanding during bootstrap, so `save_level = 0` / `created_after_save = 0`
604 // is the truth rather than a mis-stamp, and these entities sit below every
605 // future save's high-water mark. Everywhere else, use the VM-aware helpers in
606 // `stet_ops::vm_ops`.
607 let systemdict = dicts.allocate_with(400, b"systemdict", 0, true, 0);
608 let globaldict = dicts.allocate_with(100, b"globaldict", 0, true, 0);
609 let userdict = dicts.allocate_at_level_zero(200, b"userdict");
610 let errordict = dicts.allocate_at_level_zero(50, b"errordict");
611 let dollar_error = dicts.allocate_at_level_zero(20, b"$error");
612 let font_directory = dicts.allocate_at_level_zero(50, b"FontDirectory");
613
614 // Resource system dicts (global VM)
615 let global_resources = dicts.allocate_with(20, b"GlobalResources", 0, true, 0);
616 let local_resources = dicts.allocate_at_level_zero(20, b"LocalResources");
617 let internaldict = dicts.allocate_at_level_zero(50, b"internaldict");
618 let category_registry = dicts.allocate_with(30, b"CategoryRegistry", 0, true, 0);
619
620 // Parameter dicts — pre-populate user_params with recognized keys.
621 // setuserparams only updates existing keys; unknown keys are
622 // ignored per PLRM.
623 let user_params = dicts.allocate_at_level_zero(25, b"UserParams");
624 for key_name in [
625 "MaxDictStack",
626 "MaxExecStack",
627 "MaxOpStack",
628 "MaxFontItem",
629 "MaxFormItem",
630 "MaxPatternItem",
631 "MaxUPathItem",
632 "MaxScreenItem",
633 "MaxSuperScreen",
634 "MinFontCompress",
635 "MaxLocalVM",
636 "VMReclaim",
637 "VMThreshold",
638 "UCacheBLimit",
639 ] {
640 dicts.put(
641 user_params,
642 DictKey::Name(names.intern(key_name.as_bytes())),
643 PsObject::int(0),
644 );
645 }
646 dicts.put(
647 user_params,
648 DictKey::Name(names.intern(b"JobName")),
649 PsObject::string(strings.allocate_from_at_level_zero(b""), 0),
650 );
651 dicts.put(
652 user_params,
653 DictKey::Name(names.intern(b"ExecutionHistory")),
654 PsObject::bool(false),
655 );
656 dicts.put(
657 user_params,
658 DictKey::Name(names.intern(b"ExecutionHistorySize")),
659 PsObject::int(20),
660 );
661 dicts.put(
662 user_params,
663 DictKey::Name(names.intern(b"IdiomRecognition")),
664 PsObject::bool(true),
665 );
666 dicts.put(
667 user_params,
668 DictKey::Name(names.intern(b"AccurateScreens")),
669 PsObject::bool(false),
670 );
671 dicts.put(
672 user_params,
673 DictKey::Name(names.intern(b"HalftoneMode")),
674 PsObject::int(0),
675 );
676
677 let system_params = dicts.allocate_at_level_zero(30, b"SystemParams");
678 // Cache size limits (PLRM Table C.2 - system parameters)
679 for (key, val) in [
680 ("MaxFontCache", 67108864),
681 ("MaxFormCache", 131072),
682 ("MaxPatternCache", 131072),
683 ("MaxUPathCache", 131072),
684 ("MaxScreenStorage", 524288),
685 ("MaxDisplayList", 2097152),
686 ("MaxDisplayAndSourceList", 4194304),
687 ("MaxSourceList", 2097152),
688 ("MaxImageBuffer", 524288),
689 ("MaxOutlineCache", 65536),
690 ("MaxStoredScreenCache", 0),
691 // Read-only current cache usage counters
692 ("CurFontCache", 0),
693 ("CurFormCache", 0),
694 ("CurPatternCache", 0),
695 ("CurUPathCache", 0),
696 ("CurScreenStorage", 0),
697 ("CurSourceList", 0),
698 ("CurStoredScreenCache", 0),
699 ("CurOutlineCache", 0),
700 ("PageCount", 0),
701 ("Revision", 1),
702 ] {
703 dicts.put(
704 system_params,
705 DictKey::Name(names.intern(key.as_bytes())),
706 PsObject::int(val),
707 );
708 }
709 let printer_name = b"stet";
710 let printer_str = strings.allocate_from_at_level_zero(printer_name);
711 dicts.put(
712 system_params,
713 DictKey::Name(names.intern(b"PrinterName")),
714 PsObject::string(printer_str, printer_name.len() as u32),
715 );
716 // PLRM: RealFormat names the internal real representation.
717 let real_format = b"IEEE";
718 let realfmt_str = strings.allocate_from_at_level_zero(real_format);
719 dicts.put(
720 system_params,
721 DictKey::Name(names.intern(b"RealFormat")),
722 PsObject::string(realfmt_str, real_format.len() as u32),
723 );
724 let pw_str = strings.allocate_from_at_level_zero(b"0");
725 dicts.put(
726 system_params,
727 DictKey::Name(names.intern(b"SystemParamsPassword")),
728 PsObject::string(pw_str, 1),
729 );
730 let pw_str2 = strings.allocate_from_at_level_zero(b"0");
731 dicts.put(
732 system_params,
733 DictKey::Name(names.intern(b"StartJobPassword")),
734 PsObject::string(pw_str2, 1),
735 );
736 dicts.put(
737 system_params,
738 DictKey::Name(names.intern(b"LicenseID")),
739 PsObject::int(0),
740 );
741
742 // Put self-referencing entries
743 let sd_obj = PsObject::dict(systemdict);
744 dicts.put(
745 systemdict,
746 DictKey::Name(names.intern(b"systemdict")),
747 sd_obj,
748 );
749
750 let ud_obj = PsObject::dict(userdict);
751 dicts.put(systemdict, DictKey::Name(names.intern(b"userdict")), ud_obj);
752
753 let gd_obj = PsObject::dict(globaldict);
754 dicts.put(
755 systemdict,
756 DictKey::Name(names.intern(b"globaldict")),
757 gd_obj,
758 );
759
760 let ed_obj = PsObject::dict(errordict);
761 dicts.put(
762 systemdict,
763 DictKey::Name(names.intern(b"errordict")),
764 ed_obj,
765 );
766
767 let de_obj = PsObject::dict(dollar_error);
768 dicts.put(systemdict, DictKey::Name(names.intern(b"$error")), de_obj);
769
770 let fd_obj = PsObject::dict(font_directory);
771 dicts.put(
772 systemdict,
773 DictKey::Name(name_cache.n_font_directory),
774 fd_obj,
775 );
776
777 // Register constants in systemdict
778 dicts.put(
779 systemdict,
780 DictKey::Name(names.intern(b"true")),
781 PsObject::bool(true),
782 );
783 dicts.put(
784 systemdict,
785 DictKey::Name(names.intern(b"false")),
786 PsObject::bool(false),
787 );
788 dicts.put(
789 systemdict,
790 DictKey::Name(names.intern(b"null")),
791 PsObject::null(),
792 );
793
794 // mark — literal mark object
795 dicts.put(
796 systemdict,
797 DictKey::Name(names.intern(b"mark")),
798 PsObject::mark(),
799 );
800
801 // [ is an alias for mark
802 dicts.put(
803 systemdict,
804 DictKey::Name(names.intern(b"[")),
805 PsObject::mark(),
806 );
807
808 // << is a dict mark (distinct from [ mark so ] doesn't match it)
809 dicts.put(
810 systemdict,
811 DictKey::Name(names.intern(b"<<")),
812 PsObject::dict_mark(),
813 );
814
815 // version and languagelevel
816 dicts.put(
817 systemdict,
818 DictKey::Name(names.intern(b"languagelevel")),
819 PsObject::int(3),
820 );
821
822 // Dictionary stack: systemdict, globaldict, userdict
823 let d_stack = vec![systemdict, globaldict, userdict];
824
825 Self {
826 o_stack: Stack::new(500),
827 e_stack: Stack::new(250),
828 d_stack,
829 strings,
830 arrays: DualArrayStore::new(),
831 dicts,
832 names,
833 files: FileStore::new(),
834 loops: Vec::new(),
835 operators: Vec::new(),
836 systemdict,
837 globaldict,
838 userdict,
839 errordict,
840 dollar_error,
841 rand_state: 0,
842 rand_seed: 0,
843 current_source_line: 1,
844 packing_mode: false,
845 echo: false,
846 name_cache,
847 stdout: Box::new(std::io::stdout()),
848 save_stack: SaveStack::new(),
849 job_start_save_depth: 0,
850 vm_alloc_mode: false,
851 object_format: 0,
852 current_operator: None,
853 null_device_used: false,
854 in_error_handler: false,
855 initializing: true,
856 allow_ps_resolution: false,
857 exit_code: None,
858 gstate: GraphicsState::new(),
859 gstate_stack: Vec::new(),
860 gstate_store: Vec::new(),
861 device: None,
862 display_list: DisplayList::new(),
863 group_stack: Vec::new(),
864 save_group_depths: rustc_hash::FxHashMap::default(),
865 ocg_registry: rustc_hash::FxHashMap::default(),
866 next_ocg_id: 0,
867 doc_structure: stet_graphics::document_structure::DocumentStructure::new(),
868 capture_display_lists: None,
869 display_list_sender: None,
870 page_width: 612,
871 page_height: 792,
872 output_path: None,
873 page_filter: None,
874 device_factory: None,
875 font_directory,
876 font_resource_path: None,
877 next_fid: 0,
878 global_resources,
879 local_resources,
880 category_registry,
881 resource_base_path: None,
882 user_params,
883 system_params,
884 internaldict,
885 icc_cache: crate::icc::IccCache::new(),
886 exec_sync_fn: None,
887 char_width: None,
888 char_width_mode1: None,
889 glyph_caches: rustc_hash::FxHashMap::default(),
890 char_cache_mode: None,
891 cshow_pending_cid: None,
892 charpath_capture: None,
893 pattern_store: Vec::new(),
894 form_cache: rustc_hash::FxHashMap::default(),
895 cie_decode_cache: rustc_hash::FxHashMap::default(),
896 #[cfg(not(target_arch = "wasm32"))]
897 start_time: Some(std::time::Instant::now()),
898 #[cfg(target_arch = "wasm32")]
899 start_time: None,
900 dict_version: 0,
901 name_resolve_cache: Vec::new(),
902 interrupt_flag: None,
903 yield_after_showpage: false,
904 }
905 }
906
907 /// Create a context that captures stdout to a buffer (for testing).
908 pub fn new_with_output(output: Box<dyn Write>) -> Self {
909 let mut ctx = Self::new();
910 ctx.stdout = output;
911 ctx
912 }
913
914 // --- Dictionary stack operations ---
915
916 /// Look up a name in the dictionary stack (top to bottom).
917 #[inline]
918 pub fn dict_load(&mut self, key: &DictKey) -> Option<PsObject> {
919 // Fast path: check name resolution cache
920 if let DictKey::Name(name_id) = key {
921 let idx = name_id.0 as usize;
922 if idx < self.name_resolve_cache.len() {
923 let (ver, obj) = self.name_resolve_cache[idx];
924 if ver == self.dict_version {
925 return Some(obj);
926 }
927 }
928 }
929
930 // Slow path: search dict stack
931 for &dict_id in self.d_stack.iter().rev() {
932 if let Some(val) = self.dicts.get(dict_id, key) {
933 // Cache the result for Name keys
934 if let DictKey::Name(name_id) = key {
935 let idx = name_id.0 as usize;
936 if idx >= self.name_resolve_cache.len() {
937 self.name_resolve_cache
938 .resize(idx + 64, (u64::MAX, PsObject::null()));
939 }
940 self.name_resolve_cache[idx] = (self.dict_version, val);
941 }
942 return Some(val);
943 }
944 }
945 None
946 }
947
948 /// Invalidate the name resolution cache (call on begin/end/def).
949 #[inline]
950 pub fn invalidate_name_cache(&mut self) {
951 self.dict_version = self.dict_version.wrapping_add(1);
952 }
953
954 /// Look up and return `(dict_entity, value)` pair.
955 pub fn dict_where(&self, key: &DictKey) -> Option<(EntityId, PsObject)> {
956 for &dict_id in self.d_stack.iter().rev() {
957 if let Some(val) = self.dicts.get(dict_id, key) {
958 return Some((dict_id, val));
959 }
960 }
961 None
962 }
963
964 /// Store in current dict (top of d_stack).
965 pub fn dict_def(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError> {
966 let current = *self.d_stack.last().ok_or(PsError::DictStackUnderflow)?;
967 self.cow_check_dict(current);
968 self.invalidate_name_cache();
969 self.dicts.put(current, key, value);
970 Ok(())
971 }
972
973 /// Store in first dict that contains key, or current dict if not found.
974 pub fn dict_store(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError> {
975 self.invalidate_name_cache();
976 for &dict_id in self.d_stack.iter().rev() {
977 if self.dicts.known(dict_id, &key) {
978 self.cow_check_dict(dict_id);
979 self.dicts.put(dict_id, key, value);
980 return Ok(());
981 }
982 }
983 // Not found — store in current dict
984 self.dict_def(key, value)
985 }
986
987 /// Convert a `PsObject` to a `DictKey`.
988 pub fn make_dict_key(&mut self, obj: &PsObject) -> Result<DictKey, PsError> {
989 match obj.value {
990 PsValue::Name(id) => Ok(DictKey::Name(id)),
991 PsValue::Int(v) => Ok(DictKey::Int(v)),
992 PsValue::Real(v) => Ok(DictKey::Real(v.to_bits())),
993 PsValue::Bool(v) => Ok(DictKey::Bool(v)),
994 PsValue::String { entity, start, len } => {
995 // Intern string as name — PostScript treats string and name
996 // keys as equivalent in dict lookups.
997 let bytes = self.strings.get(entity, start, len).to_vec();
998 let name_id = self.names.intern(&bytes);
999 Ok(DictKey::Name(name_id))
1000 }
1001 PsValue::Operator(op) => Ok(DictKey::Operator(op.0)),
1002 PsValue::Array { entity, start, len } | PsValue::PackedArray { entity, start, len } => {
1003 Ok(DictKey::Identity(entity.0, start, len))
1004 }
1005 PsValue::Dict(entity) => Ok(DictKey::Identity(entity.0, 0, 0)),
1006 PsValue::Null => Err(PsError::TypeCheck),
1007 _ => Err(PsError::TypeCheck),
1008 }
1009 }
1010
1011 /// Allocate a new loop state, returning its EntityId.
1012 pub fn alloc_loop(&mut self, state: LoopState) -> EntityId {
1013 let id = EntityId(self.loops.len() as u32);
1014 self.loops.push(state);
1015 id
1016 }
1017
1018 /// Get a loop state by EntityId.
1019 pub fn get_loop(&self, entity: EntityId) -> &LoopState {
1020 &self.loops[entity.0 as usize]
1021 }
1022
1023 /// Get a mutable loop state by EntityId.
1024 pub fn get_loop_mut(&mut self, entity: EntityId) -> &mut LoopState {
1025 &mut self.loops[entity.0 as usize]
1026 }
1027
1028 /// Return the display list paint operators should currently append to.
1029 ///
1030 /// While a transparency group is active (`group_stack` non-empty),
1031 /// the topmost frame's display list is returned. Otherwise the
1032 /// page-level `display_list` is returned. Every paint-emitting
1033 /// operator must route through this helper to keep group capture
1034 /// correct.
1035 #[inline]
1036 pub fn current_display_list_mut(&mut self) -> &mut DisplayList {
1037 if let Some(frame) = self.group_stack.last_mut() {
1038 &mut frame.display_list
1039 } else {
1040 &mut self.display_list
1041 }
1042 }
1043
1044 /// Read-only counterpart to [`Self::current_display_list_mut`].
1045 #[inline]
1046 pub fn current_display_list(&self) -> &DisplayList {
1047 if let Some(frame) = self.group_stack.last() {
1048 &frame.display_list
1049 } else {
1050 &self.display_list
1051 }
1052 }
1053
1054 /// Take the display list, optionally capturing a clone for viewport re-rendering.
1055 ///
1056 /// This replaces `std::mem::take(&mut ctx.display_list)` at showpage/copypage
1057 /// call sites. When `capture_display_lists` is active, a clone is saved
1058 /// along with the current page DPI from the pagedevice HWResolution.
1059 pub fn take_display_list(&mut self) -> DisplayList {
1060 if self.capture_display_lists.is_some() {
1061 let dpi = self.current_page_dpi();
1062 if let Some(ref mut captures) = self.capture_display_lists {
1063 captures.push((self.display_list.clone(), dpi));
1064 }
1065 }
1066 if let Some(ref sender) = self.display_list_sender {
1067 let dpi = self.current_page_dpi();
1068 // Use the device's actual page size (device pixels), not
1069 // self.page_width/page_height which are point values.
1070 let (w, h) = self
1071 .device
1072 .as_ref()
1073 .map(|d| d.page_size())
1074 .unwrap_or((self.page_width, self.page_height));
1075 // PS interpreter output: no PDF-specific CMYK profile in play, so
1076 // the viewer uses its CLI-level default. PDF/X proofing is a
1077 // PDF-only concept; PostScript always sends `false`.
1078 let _ = sender.send((self.display_list.clone(), dpi, w, h, None, false));
1079 }
1080 // Page-boundary yield: once the display list for this page has been
1081 // captured (above), signal the eval loop to return so the caller can
1082 // hand the page off to a renderer before interpreting the next one.
1083 if self.yield_after_showpage
1084 && let Some(ref flag) = self.interrupt_flag
1085 {
1086 flag.store(true, std::sync::atomic::Ordering::Relaxed);
1087 }
1088 std::mem::take(&mut self.display_list)
1089 }
1090
1091 /// Read the current page DPI from the pagedevice HWResolution, defaulting to 72.
1092 pub fn current_page_dpi(&self) -> f64 {
1093 use crate::dict::DictKey;
1094 if let Some(pd) = self.gstate.page_device
1095 && let Some(name_id) = self.names.find(b"HWResolution")
1096 && let Some(obj) = self.dicts.get(pd, &DictKey::Name(name_id))
1097 && let PsValue::Array { entity, .. } = obj.value
1098 {
1099 let first = self.arrays.get_element(entity, 0);
1100 return match first.value {
1101 PsValue::Real(r) => r,
1102 PsValue::Int(i) => i as f64,
1103 _ => 72.0,
1104 };
1105 }
1106 72.0
1107 }
1108
1109 // --- VM save/restore ---
1110
1111 /// Perform a `save`: snapshot the current VM state.
1112 /// Returns a Save PsObject.
1113 /// Current high-water marks of local VM, for reclamation on restore.
1114 fn vm_marks(&self) -> crate::save_stack::VmMarks {
1115 crate::save_stack::VmMarks {
1116 string_data: self.strings.local.data_len(),
1117 string_entities: self.strings.local.entities.len(),
1118 array_data: self.arrays.local.allocated_objects(),
1119 array_entities: self.arrays.local.entities.len(),
1120 dict_slots: self.dicts.local.dict_slots(),
1121 dict_entities: self.dicts.local.entities.len(),
1122 }
1123 }
1124
1125 pub fn vm_save(&mut self) -> PsObject {
1126 let d_depth = self.d_stack.len();
1127 let gstate_snapshot = self.gstate.clone();
1128 let gstate_stack_snapshot = self.gstate_stack.clone();
1129 let marks = self.vm_marks();
1130 let (_level, save_id) = self.save_stack.save(crate::save_stack::SaveSnapshot {
1131 d_stack_depth: d_depth,
1132 packing_mode: self.packing_mode,
1133 vm_alloc_mode: self.vm_alloc_mode,
1134 object_format: self.object_format,
1135 gstate: gstate_snapshot,
1136 gstate_stack: gstate_stack_snapshot,
1137 gstate_store_len: self.gstate_store.len(),
1138 marks,
1139 });
1140
1141 // Implicit gsave: push current gstate marked as save-created (per PLRM).
1142 // grestoreall stops at this entry; grestore skips it.
1143 self.gstate_stack.push(crate::graphics_state::GstateEntry {
1144 state: self.gstate.clone(),
1145 saved_by_save: true,
1146 });
1147
1148 PsObject {
1149 value: PsValue::Save(SaveLevel(save_id)),
1150 flags: crate::object::ObjFlags::literal(),
1151 }
1152 }
1153
1154 /// Perform a `restore`: revert VM to the given save state.
1155 pub fn vm_restore(&mut self, save_id: u32) -> Result<(), PsError> {
1156 // Validate save_id
1157 if !self.save_stack.is_valid(save_id) {
1158 return Err(PsError::InvalidRestore);
1159 }
1160
1161 // Per PLRM: "restore can reset VM to the state represented by any
1162 // save object that is still valid, not necessarily the one produced
1163 // by the most recent save." Pop the target level AND all newer
1164 // levels, undoing COW records from newest to target.
1165 let levels = self
1166 .save_stack
1167 .restore_to(save_id)
1168 .ok_or(PsError::InvalidRestore)?;
1169
1170 // Undo COW records from newest level to oldest (reverse order).
1171 // Each level's records are also processed in reverse.
1172 // After swapping offsets, reset save_level to 0 so future COW
1173 // checks at the same save level don't incorrectly skip the backup.
1174 for level in levels.iter().rev() {
1175 for record in level.records.iter().rev() {
1176 match record.store_type {
1177 StoreType::String => {
1178 self.strings.swap_offsets(record.src, record.copy);
1179 self.strings.entity_meta_mut(record.src).save_level = 0;
1180 }
1181 StoreType::Array => {
1182 self.arrays.swap_offsets(record.src, record.copy);
1183 self.arrays.entity_meta_mut(record.src).save_level = 0;
1184 }
1185 StoreType::Dict => {
1186 self.dicts.swap_offsets(record.src, record.copy);
1187 self.dicts.entity_meta_mut(record.src).save_level = 0;
1188 }
1189 }
1190 }
1191 }
1192
1193 // Restore context parameters from the TARGET save level (first in vec)
1194 let target = &levels[0];
1195 self.packing_mode = target.packing_mode;
1196 self.vm_alloc_mode = target.vm_alloc_mode;
1197 self.object_format = target.object_format;
1198
1199 // Restore graphics state from the target level
1200 self.gstate = target.gstate.clone();
1201 self.gstate_stack = target.gstate_stack.clone();
1202
1203 // Reclaim gstate objects created after the save. `check_invalidrestore`
1204 // has already refused the restore if any of them is still reachable, so
1205 // truncating here can only drop slots nothing can name.
1206 self.gstate_store.truncate(target.gstate_store_len);
1207
1208 // Restore d_stack depth from the target level
1209 self.d_stack.truncate(target.d_stack_depth);
1210
1211 // Reclaim everything the restored levels allocated in local VM.
1212 //
1213 // Safe by the same PLRM 3.7.3.2 rule `check_invalidrestore` enforces:
1214 // nothing reachable may still refer to a composite created after the
1215 // save. The COW swaps above are what make it hold for pre-save objects
1216 // that were mutated -- `cow_copy` leaves the surviving data at its
1217 // original offset, below the mark, and parks the discarded mutated copy
1218 // above it. Global VM is untouched by save/restore, so only local
1219 // stores are truncated.
1220 let marks = target.marks;
1221
1222 // EntityIds become reusable the moment the tables shrink, so anything
1223 // keyed by one has to be dropped first -- otherwise a future entity
1224 // reusing the index would hit a stale entry belonging to a dead object.
1225 // Both caches below are keyed by dict entities.
1226 let dict_mark = marks.dict_entities;
1227 let live_dict = |e: &EntityId| e.is_global() || e.raw_index() < dict_mark;
1228 self.glyph_caches.retain(|entity, _| live_dict(entity));
1229 self.form_cache.retain(|entity, _| live_dict(entity));
1230
1231 self.strings
1232 .local
1233 .truncate_to(marks.string_data, marks.string_entities);
1234 self.arrays
1235 .local
1236 .truncate_to(marks.array_data, marks.array_entities);
1237 self.dicts
1238 .local
1239 .truncate_to(marks.dict_slots, marks.dict_entities);
1240
1241 self.invalidate_name_cache();
1242 self.close_restored_proc_sources();
1243 self.debug_assert_no_dangling_refs();
1244 Ok(())
1245 }
1246
1247 /// Close any procedure data source whose procedure the restore just
1248 /// reclaimed.
1249 ///
1250 /// `filter` may be handed a procedure and the resulting file read only
1251 /// later; if a `restore` falls in between, the procedure's array is gone.
1252 /// PLRM 3.7.3 has `restore` close files opened since the `save`, which is
1253 /// precisely the right outcome — a subsequent read reports end-of-file
1254 /// rather than following a retired entity id.
1255 fn close_restored_proc_sources(&mut self) {
1256 for (entity, proc) in self.files.pending_proc_handles() {
1257 if !self.entity_is_live(&proc) {
1258 self.files.close_pending_proc(entity);
1259 }
1260 }
1261 }
1262
1263 /// Does `obj`'s composite still exist, or did a `restore` retire it?
1264 fn entity_is_live(&self, obj: &PsObject) -> bool {
1265 match obj.value {
1266 PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => {
1267 let len = if entity.is_global() {
1268 self.arrays.global.entities.len()
1269 } else {
1270 self.arrays.local.entities.len()
1271 };
1272 entity.raw_index() < len
1273 }
1274 _ => true,
1275 }
1276 }
1277
1278 /// Panic if anything that survived a `restore` names storage the restore
1279 /// released.
1280 ///
1281 /// Reclaiming local VM means retiring entity ids, and an id that outlives
1282 /// its storage is a use-after-free that surfaces as a panic deep in an
1283 /// unrelated operator. `check_invalidrestore` is supposed to prevent it by
1284 /// raising `invalidrestore` first, but it only scans the operand,
1285 /// execution, and dictionary stacks — the graphics state, `gstate_store`,
1286 /// and the entity-keyed caches are not covered. This turns the gap from an
1287 /// argument into something every debug-build test run checks.
1288 ///
1289 /// Debug builds only: the sweep is O(size of VM), far too expensive for
1290 /// release. Run the PS suite or a corpus sweep under a debug build when
1291 /// touching anything that holds an `EntityId` across a `restore` — the
1292 /// release build compiles this out entirely.
1293 #[inline]
1294 fn debug_assert_no_dangling_refs(&self) {
1295 #[cfg(debug_assertions)]
1296 {
1297 let dangling = crate::vm_audit::audit_dangling_refs(self);
1298 assert!(
1299 dangling.is_empty(),
1300 "restore left {} dangling reference(s):\n {}",
1301 dangling.len(),
1302 dangling
1303 .iter()
1304 .map(|d| d.to_string())
1305 .collect::<Vec<_>>()
1306 .join("\n ")
1307 );
1308 }
1309 }
1310
1311 // --- COW check methods ---
1312
1313 /// Check if a string entity needs COW before mutation.
1314 /// If yes, creates a backup copy and records it.
1315 pub fn cow_check_string(&mut self, entity: EntityId) {
1316 let current_level = self.save_stack.current_level();
1317 if current_level == 0 {
1318 return; // No save active
1319 }
1320
1321 if entity.is_global() {
1322 return; // Global entities skip local COW
1323 }
1324 let meta = self.strings.entity_meta(entity);
1325 if meta.save_level >= current_level {
1326 return; // Already copied at this level
1327 }
1328
1329 // Perform COW copy
1330 let copy_id = self.strings.cow_copy(entity);
1331 self.strings.entity_meta_mut(entity).save_level = current_level;
1332
1333 self.save_stack.add_record(SaveRecord {
1334 src: entity,
1335 copy: copy_id,
1336 store_type: StoreType::String,
1337 });
1338 }
1339
1340 /// Check if an array entity needs COW before mutation.
1341 pub fn cow_check_array(&mut self, entity: EntityId) {
1342 let current_level = self.save_stack.current_level();
1343 if current_level == 0 {
1344 return;
1345 }
1346
1347 if entity.is_global() {
1348 return;
1349 }
1350 let meta = self.arrays.entity_meta(entity);
1351 if meta.save_level >= current_level {
1352 return;
1353 }
1354
1355 let copy_id = self.arrays.cow_copy(entity);
1356 self.arrays.entity_meta_mut(entity).save_level = current_level;
1357
1358 self.save_stack.add_record(SaveRecord {
1359 src: entity,
1360 copy: copy_id,
1361 store_type: StoreType::Array,
1362 });
1363 }
1364
1365 /// Check if a dict entity needs COW before mutation.
1366 /// Store into a dictionary, copy-on-writing it first.
1367 ///
1368 /// Prefer this over a bare `ctx.dicts.put` for any write into a dictionary
1369 /// the current operation did not itself allocate — `FontDirectory`, the
1370 /// resource dictionaries, `userdict`, a caller-supplied dict. Writing
1371 /// straight through bypasses save/restore: if the dictionary predates the
1372 /// current `save`, no backup is taken and `restore` will not revert the
1373 /// entry. That leaves the dictionary holding a value the restore released.
1374 ///
1375 /// [`cow_check_dict`](Self::cow_check_dict) is cheap and idempotent — it
1376 /// returns immediately at save level 0, for global entities, and for
1377 /// dictionaries already copied at this level — so there is no reason to
1378 /// skip it when in doubt.
1379 pub fn dict_put_cow(&mut self, entity: EntityId, key: DictKey, value: PsObject) {
1380 self.cow_check_dict(entity);
1381 self.dicts.put(entity, key, value);
1382 }
1383
1384 pub fn cow_check_dict(&mut self, entity: EntityId) {
1385 let current_level = self.save_stack.current_level();
1386 if current_level == 0 {
1387 return;
1388 }
1389
1390 if entity.is_global() {
1391 return;
1392 }
1393 let meta = self.dicts.entity_meta(entity);
1394 if meta.save_level >= current_level {
1395 return;
1396 }
1397
1398 let copy_id = self.dicts.cow_copy(entity);
1399 self.dicts.entity_meta_mut(entity).save_level = current_level;
1400
1401 self.save_stack.add_record(SaveRecord {
1402 src: entity,
1403 copy: copy_id,
1404 store_type: StoreType::Dict,
1405 });
1406 }
1407
1408 // --- Token conversion ---
1409
1410 /// Convert a tokenizer token into a PsObject.
1411 pub fn token_to_object(&mut self, token: crate::tokenizer::Token) -> Result<PsObject, PsError> {
1412 use crate::tokenizer::Token;
1413 match token {
1414 Token::Int(v) => Ok(PsObject::int(v)),
1415 Token::Real(v) => Ok(PsObject::real(v)),
1416 Token::String(bytes) => {
1417 let save_level = self.save_stack.current_level();
1418 let global = self.vm_alloc_mode;
1419 let created = self.save_stack.last_save_id();
1420 let entity = self
1421 .strings
1422 .allocate_with(bytes.len(), save_level, global, created);
1423 self.strings
1424 .get_mut(entity, 0, bytes.len() as u32)
1425 .copy_from_slice(&bytes);
1426 let mut obj = PsObject::string(entity, bytes.len() as u32);
1427 if global {
1428 obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, false, true, true);
1429 }
1430 Ok(obj)
1431 }
1432 Token::Name(bytes, is_exec) => {
1433 let id = self.names.intern(&bytes);
1434 if is_exec {
1435 Ok(PsObject::name_exec(id))
1436 } else {
1437 Ok(PsObject::name_lit(id))
1438 }
1439 }
1440 Token::LiteralName(bytes) => {
1441 let id = self.names.intern(&bytes);
1442 Ok(PsObject::name_lit(id))
1443 }
1444 Token::ImmediateName(bytes) => {
1445 let id = self.names.intern(&bytes);
1446 let key = DictKey::Name(id);
1447 self.dict_load(&key).ok_or(PsError::Undefined)
1448 }
1449 Token::ArrayBegin => {
1450 let id = self.names.intern(b"[");
1451 Ok(PsObject::name_exec(id))
1452 }
1453 Token::ArrayEnd => {
1454 let id = self.names.intern(b"]");
1455 Ok(PsObject::name_exec(id))
1456 }
1457 Token::DictBegin => {
1458 let id = self.names.intern(b"<<");
1459 Ok(PsObject::name_exec(id))
1460 }
1461 Token::DictEnd => {
1462 let id = self.names.intern(b">>");
1463 Ok(PsObject::name_exec(id))
1464 }
1465 Token::ProcBegin | Token::ProcEnd | Token::Eof | Token::BinaryTokenByte(_) => {
1466 Err(PsError::SyntaxError)
1467 }
1468 }
1469 }
1470
1471 /// Reset local VM stores (for job boundary cleanup).
1472 /// Full implementation deferred until job server loop is built.
1473 pub fn reset_local_vm(&mut self) {
1474 self.strings.reset_local();
1475 self.arrays.reset_local();
1476 self.dicts.reset_local();
1477 }
1478}
1479
1480impl Default for Context {
1481 fn default() -> Self {
1482 Self::new()
1483 }
1484}
1485
1486#[cfg(test)]
1487mod tests {
1488 use super::*;
1489
1490 #[test]
1491 fn test_context_creation() {
1492 let ctx = Context::new();
1493 assert!(ctx.o_stack.is_empty());
1494 assert!(ctx.e_stack.is_empty());
1495 assert_eq!(ctx.d_stack.len(), 3); // systemdict, globaldict, userdict
1496 }
1497
1498 #[test]
1499 fn test_dict_def_and_load() {
1500 let mut ctx = Context::new();
1501 let key = DictKey::Name(ctx.names.intern(b"foo"));
1502 ctx.dict_def(key.clone(), PsObject::int(42)).unwrap();
1503
1504 let val = ctx.dict_load(&key).unwrap();
1505 assert_eq!(val.as_i32(), Some(42));
1506 }
1507
1508 #[test]
1509 fn test_dict_where() {
1510 let mut ctx = Context::new();
1511 let key = DictKey::Name(ctx.names.intern(b"true"));
1512 let result = ctx.dict_where(&key);
1513 assert!(result.is_some());
1514 let (dict_id, val) = result.unwrap();
1515 assert_eq!(dict_id, ctx.systemdict);
1516 assert!(matches!(val.value, PsValue::Bool(true)));
1517 }
1518
1519 #[test]
1520 fn test_dict_store_existing() {
1521 let mut ctx = Context::new();
1522 let key = DictKey::Name(ctx.names.intern(b"myvar"));
1523
1524 // Define in userdict
1525 ctx.dict_def(key.clone(), PsObject::int(1)).unwrap();
1526
1527 // Store should update the existing entry in userdict
1528 ctx.dict_store(key.clone(), PsObject::int(2)).unwrap();
1529
1530 let val = ctx.dict_load(&key).unwrap();
1531 assert_eq!(val.as_i32(), Some(2));
1532 }
1533
1534 #[test]
1535 fn test_save_restore_basic() {
1536 let mut ctx = Context::new();
1537 let key = DictKey::Name(ctx.names.intern(b"testvar"));
1538
1539 // Define before save
1540 ctx.dict_def(key.clone(), PsObject::int(1)).unwrap();
1541
1542 // Save
1543 let save_obj = ctx.vm_save();
1544 let save_id = match save_obj.value {
1545 PsValue::Save(SaveLevel(id)) => id,
1546 _ => panic!("Expected Save"),
1547 };
1548
1549 // Modify after save
1550 ctx.dict_def(key.clone(), PsObject::int(2)).unwrap();
1551 assert_eq!(ctx.dict_load(&key).unwrap().as_i32(), Some(2));
1552
1553 // Restore
1554 ctx.vm_restore(save_id).unwrap();
1555 assert_eq!(ctx.dict_load(&key).unwrap().as_i32(), Some(1));
1556 }
1557
1558 #[test]
1559 fn test_save_restore_string() {
1560 let mut ctx = Context::new();
1561
1562 let entity = ctx.strings.allocate_from_at_level_zero(b"hello");
1563
1564 // Save
1565 let save_obj = ctx.vm_save();
1566 let save_id = match save_obj.value {
1567 PsValue::Save(SaveLevel(id)) => id,
1568 _ => panic!("Expected Save"),
1569 };
1570
1571 // Modify after save
1572 ctx.cow_check_string(entity);
1573 ctx.strings.put_byte(entity, 0, b'H');
1574 assert_eq!(ctx.strings.get(entity, 0, 5), b"Hello");
1575
1576 // Restore
1577 ctx.vm_restore(save_id).unwrap();
1578 assert_eq!(ctx.strings.get(entity, 0, 5), b"hello");
1579 }
1580
1581 #[test]
1582 fn test_save_restore_array() {
1583 let mut ctx = Context::new();
1584
1585 let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
1586 let entity = ctx.arrays.allocate_from_at_level_zero(&items);
1587
1588 let save_obj = ctx.vm_save();
1589 let save_id = match save_obj.value {
1590 PsValue::Save(SaveLevel(id)) => id,
1591 _ => panic!("Expected Save"),
1592 };
1593
1594 ctx.cow_check_array(entity);
1595 ctx.arrays.set_element(entity, 1, PsObject::int(99));
1596 assert_eq!(ctx.arrays.get_element(entity, 1).as_i32(), Some(99));
1597
1598 ctx.vm_restore(save_id).unwrap();
1599 assert_eq!(ctx.arrays.get_element(entity, 1).as_i32(), Some(2));
1600 }
1601
1602 #[test]
1603 fn test_invalid_restore() {
1604 let mut ctx = Context::new();
1605 // Restore without save
1606 assert_eq!(ctx.vm_restore(999), Err(PsError::InvalidRestore));
1607 }
1608
1609 /// The debug-build guard has to actually fire, or it is decoration.
1610 ///
1611 /// Planted in `userdict` rather than in one of the entity-keyed caches:
1612 /// `userdict` predates the save, so it survives the restore, and writing
1613 /// to it without copy-on-write leaves the reference behind with no backup
1614 /// to revert. That is the shape of the bug class fixed in 04ddc25, and it
1615 /// is a path no amount of cache purging can cover.
1616 #[test]
1617 #[cfg(debug_assertions)]
1618 #[should_panic(expected = "dangling reference")]
1619 fn vm_restore_rejects_a_dangling_reference() {
1620 let mut ctx = Context::new();
1621 let save_obj = ctx.vm_save();
1622 let save_id = match save_obj.value {
1623 PsValue::Save(SaveLevel(id)) => id,
1624 _ => panic!("Expected Save"),
1625 };
1626 let retired = EntityId(ctx.dicts.local.entities.len() as u32);
1627 let key = DictKey::Name(ctx.names.intern(b"stale"));
1628 ctx.dicts.put(ctx.userdict, key, PsObject::dict(retired));
1629 let _ = ctx.vm_restore(save_id);
1630 }
1631
1632 /// `gstate` objects index `gstate_store`, which `restore` now rewinds to
1633 /// its length at save time.
1634 #[test]
1635 fn restore_rewinds_gstate_store() {
1636 let mut ctx = Context::new();
1637 ctx.gstate_store.push(ctx.gstate.clone());
1638 let save_obj = ctx.vm_save();
1639 let save_id = match save_obj.value {
1640 PsValue::Save(SaveLevel(id)) => id,
1641 _ => panic!("Expected Save"),
1642 };
1643 ctx.gstate_store.push(ctx.gstate.clone());
1644 assert_eq!(ctx.gstate_store.len(), 2);
1645 ctx.vm_restore(save_id).unwrap();
1646 assert_eq!(ctx.gstate_store.len(), 1);
1647 }
1648}