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