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}