Skip to main content

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