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