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