Skip to main content

stet_core/
context.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Execution context: stacks, storage, operator table, and state.
6
7use std::io::Write;
8
9use crate::device::OutputDevice;
10use crate::dict::DictKey;
11use crate::display_list::{DisplayList, GroupParams, SoftMaskParams};
12use crate::dual_array_store::DualArrayStore;
13use crate::dual_dict_store::DualDictStore;
14use crate::dual_string_store::DualStringStore;
15use crate::error::PsError;
16use crate::file_store::FileStore;
17use crate::graphics_state::{GraphicsState, Matrix, PathSegment, PatternData};
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
107pub struct Context {
108    // Stacks
109    pub o_stack: Stack,
110    pub e_stack: Stack,
111    pub d_stack: Vec<EntityId>,
112
113    // Storage
114    pub strings: DualStringStore,
115    pub arrays: DualArrayStore,
116    pub dicts: DualDictStore,
117    pub names: NameTable,
118    pub files: FileStore,
119
120    // Loop state storage (indexed by EntityId)
121    pub loops: Vec<LoopState>,
122
123    // Operator table
124    pub operators: Vec<OpEntry>,
125
126    // Well-known dict IDs
127    pub systemdict: EntityId,
128    pub globaldict: EntityId,
129    pub userdict: EntityId,
130    pub errordict: EntityId,
131    pub dollar_error: EntityId,
132
133    // State
134    pub rand_state: u64,
135    pub rand_seed: i32,
136    /// Current source line number (1-based), updated during scanning.
137    pub current_source_line: u32,
138    /// Packing mode for array/procedure creation (setpacking/currentpacking).
139    pub packing_mode: bool,
140    /// Echo mode for %lineedit/%statementedit (PLRM echo operator).
141    pub echo: bool,
142
143    // Pre-interned names
144    pub name_cache: NameCache,
145
146    // Output: writer for print/= operators (allows capture in tests)
147    pub stdout: Box<dyn Write>,
148
149    // VM save/restore
150    pub save_stack: SaveStack,
151    /// Save stack depth when the current job started (for startjob condition 3).
152    pub job_start_save_depth: usize,
153
154    // VM allocation mode: true = global, false = local
155    pub vm_alloc_mode: bool,
156
157    /// Binary object format (0-4). Default 0.
158    pub object_format: i32,
159
160    // Error dispatch state
161    pub current_operator: Option<NameId>,
162    pub in_error_handler: bool,
163    /// True during init script execution — relaxes access checks.
164    pub initializing: bool,
165    /// When true, PS programs can change HWResolution via setpagedevice.
166    /// Set by WASM frontend; CLI leaves false to keep DPI under user control.
167    pub allow_ps_resolution: bool,
168
169    /// Process exit code requested by the running PS program via the
170    /// `.quitwithcode` operator. `None` means "use the default" (0 on
171    /// success). The CLI reads this on `PsError::Quit` and propagates
172    /// to `std::process::exit`.
173    pub exit_code: Option<i32>,
174
175    // Graphics state
176    pub gstate: GraphicsState,
177    pub gstate_stack: Vec<crate::graphics_state::GstateEntry>,
178    /// Storage for gstate objects (PsValue::Gstate indexes into this).
179    pub gstate_store: Vec<GraphicsState>,
180    pub device: Option<Box<dyn OutputDevice>>,
181    pub display_list: DisplayList,
182    /// Stack of active transparency-group capture frames. While non-empty,
183    /// paint operators emit into the topmost frame's display list instead
184    /// of `display_list`. `endtransparencygroup` pops the top frame and
185    /// emits a [`stet_graphics::display_list::DisplayElement::Group`] into
186    /// the next-innermost target. See `op_begintransparencygroup` /
187    /// `op_endtransparencygroup` in `stet-ops::transparency_ops`.
188    pub group_stack: Vec<GroupFrame>,
189    /// `group_stack.len()` recorded at each `save`. `restore` consults
190    /// this to refuse a revert that would unwind across an unbalanced
191    /// `begintransparencygroup` / `endtransparencygroup` pair.
192    pub save_group_depths: rustc_hash::FxHashMap<u32, usize>,
193    /// Registry of OCGs (PDF Optional Content Groups) declared via the
194    /// `defineocg` operator. Keyed by the interned `NameId` of the
195    /// human-readable layer name from the OCG dict's `/Name` entry.
196    /// `beginoptionalcontent` looks up an OCG by name to obtain the
197    /// `ocg_id` and `default_visible` it embeds into the emitted
198    /// [`stet_graphics::display_list::OcgVisibility::Single`].
199    pub ocg_registry: rustc_hash::FxHashMap<NameId, OcgRecord>,
200    /// Monotonic counter feeding [`OcgRecord::ocg_id`]. Each call to
201    /// `defineocg` increments this; ids never recycle.
202    pub next_ocg_id: u32,
203    /// Accumulator for `pdfmark` authoring records. Drained by the PDF
204    /// output device at end-of-job; ignored by non-PDF devices.
205    /// Document-global: `save` / `restore` do not roll this back. See
206    /// [`crate::pdfmark`].
207    pub pdfmark_buffer: crate::pdfmark::PdfMarkBuffer,
208    /// When `Some`, each showpage clones the display list here before consuming it.
209    /// Used by the WASM frontend to retain display lists for viewport re-rendering.
210    /// Each entry is (DisplayList, dpi) where dpi is from the pagedevice HWResolution.
211    pub capture_display_lists: Option<Vec<(DisplayList, f64)>>,
212    /// When `Some`, each showpage sends a clone of the display list through this channel.
213    /// Used by the CLI viewer for incremental display list delivery.
214    /// Tuple: `(DisplayList, dpi, page_width, page_height,
215    /// effective_cmyk_bytes, cmyk_proofing)`. PostScript pages always pass
216    /// `None`/`false` (no PDF/X concept); PDF pages may set these from the
217    /// document's OutputIntent context.
218    pub display_list_sender: Option<
219        std::sync::mpsc::Sender<(
220            DisplayList,
221            f64,
222            u32,
223            u32,
224            Option<std::sync::Arc<Vec<u8>>>,
225            bool,
226        )>,
227    >,
228    pub page_width: u32,
229    pub page_height: u32,
230    pub output_path: Option<String>,
231    /// Page filter: if set, only render pages in this set (1-based).
232    pub page_filter: Option<std::collections::HashSet<i32>>,
233    /// Factory closure for creating raster devices (registered by CLI).
234    #[allow(clippy::type_complexity)]
235    pub device_factory: Option<Box<dyn Fn(u32, u32) -> Box<dyn OutputDevice>>>,
236
237    // Font system
238    pub font_directory: EntityId,
239    pub font_resource_path: Option<String>,
240    pub next_fid: i32,
241
242    // Resource system
243    pub global_resources: EntityId,
244    pub local_resources: EntityId,
245    pub category_registry: EntityId,
246    pub resource_base_path: Option<String>,
247
248    // Parameter system
249    pub user_params: EntityId,
250    pub system_params: EntityId,
251
252    // Internal dict (lazily created for `internaldict` operator)
253    pub internaldict: Option<EntityId>,
254
255    // ICC color profile cache
256    pub icc_cache: crate::icc::IccCache,
257
258    // Synchronous procedure execution (set by engine crate)
259    pub exec_sync_fn: Option<ExecSyncFn>,
260
261    // Character width set by setcachedevice/setcharwidth during BuildChar execution
262    pub char_width: Option<(f64, f64)>,
263    // Mode 1 metrics from setcachedevice2: ((w1x, w1y), (vx, vy))
264    pub char_width_mode1: Option<((f64, f64), (f64, f64))>,
265
266    // Glyph path cache: per-font charstring interpretation results
267    pub glyph_caches: rustc_hash::FxHashMap<EntityId, crate::glyph_cache::GlyphCache>,
268    // Type 3 cache mode: set by setcachedevice/setcharwidth during BuildChar
269    pub char_cache_mode: Option<crate::glyph_cache::Type3CacheMode>,
270
271    // CID passed from cshow to nested show call for Type 0 composite fonts
272    pub cshow_pending_cid: Option<i32>,
273
274    // Pattern/form support
275    /// Storage for pattern instances created by `makepattern`.
276    pub pattern_store: Vec<PatternData>,
277    /// Cache of form display lists keyed by dict EntityId.
278    pub form_cache: rustc_hash::FxHashMap<EntityId, DisplayList>,
279
280    // Timing
281    pub start_time: Option<std::time::Instant>,
282
283    // Name resolution cache: invalidated on begin/end/def
284    pub dict_version: u64,
285    /// Name resolution cache indexed by NameId. Each entry is (dict_version, resolved_object).
286    /// Public for inline cache checks in the eval loop's hot path.
287    pub name_resolve_cache: Vec<(u64, PsObject)>,
288
289    /// When set, the eval loop aborts with `PsError::Quit` on the next iteration.
290    /// Used by the interactive viewer to cancel an in-flight parse when the
291    /// user drops a new file.
292    pub interrupt_flag: Option<std::sync::Arc<std::sync::atomic::AtomicBool>>,
293
294    /// When true, each successful `showpage` / `copypage` sets `interrupt_flag`
295    /// after capturing the display list, so the eval loop yields back to the
296    /// caller one page at a time. The caller clears the flag and re-enters
297    /// `eval` to drive the next page. Used by the WASM viewer to stream
298    /// multi-page PostScript documents: page 1 renders while pages 2..N are
299    /// still pending interpretation. Requires `interrupt_flag` to be set.
300    pub yield_after_showpage: bool,
301}
302
303/// One frame on `Context::group_stack`. Captures paint operators emitted
304/// between a `begin*` and a matching close. The active capture target is
305/// always [`Self::display_list`]; what happens to it on close depends on
306/// [`Self::kind`].
307pub struct GroupFrame {
308    /// Paint operators emitted while this frame is on top of
309    /// `group_stack`. The semantics on close depend on `kind`.
310    pub display_list: DisplayList,
311    /// What this frame represents — transparency group, soft-mask
312    /// builder, or post-`endsoftmask` masked-content scope.
313    pub kind: GroupKind,
314    /// `gstate.clip_path_version` snapshot taken when the frame opened.
315    /// Reserved for future use (e.g. detecting clip changes that
316    /// crossed the boundary).
317    pub saved_clip_path_version: u32,
318    /// `gstate_stack.len()` at the moment the frame opened. Used by
319    /// `gsave` / `grestore` and `restore` to refuse pops that would
320    /// orphan this frame.
321    pub saved_gsave_depth: usize,
322}
323
324/// What a [`GroupFrame`] is capturing, determining what gets emitted
325/// when it closes.
326pub enum GroupKind {
327    /// Opened by `begintransparencygroup`. On close, the captured
328    /// `display_list` becomes the children of a
329    /// `DisplayElement::Group` with these `params`.
330    Transparency { params: GroupParams },
331    /// Opened by `beginsoftmask`. While active, paint ops emit into the
332    /// frame's `display_list` to build the mask form. `endsoftmask`
333    /// transmutes the frame to [`Self::Masked`] without popping.
334    SoftMask { params: SoftMaskParams },
335    /// Implicitly opened by `endsoftmask`. While active, paint ops emit
336    /// into `display_list` as the *content* the mask attenuates. On
337    /// `clearsoftmask` the frame pops and emits a
338    /// `DisplayElement::SoftMasked` carrying `mask`, the captured
339    /// content, and `params`.
340    Masked {
341        mask: DisplayList,
342        params: SoftMaskParams,
343    },
344    /// Opened by `beginoptionalcontent`. On close, the captured
345    /// `display_list` becomes the children of a
346    /// `DisplayElement::OcgGroup` whose visibility is
347    /// `OcgVisibility::Single { ocg_id, default_visible }`.
348    OptionalContent { ocg_id: u32, default_visible: bool },
349}
350
351/// One entry in `Context::ocg_registry`. `defineocg` allocates these
352/// and indexes them by the OCG's interned `NameId` so
353/// `beginoptionalcontent` can resolve a name back to its `ocg_id` and
354/// the `default_visible` flag the producer set.
355#[derive(Clone, Debug)]
356pub struct OcgRecord {
357    /// Monotonic id assigned by `defineocg`. Embedded into
358    /// `OcgVisibility::Single` on the display list.
359    pub ocg_id: u32,
360    /// Initial visibility, used by the renderer when no `LayerSet`
361    /// override exists for this OCG.
362    pub default_visible: bool,
363}
364
365impl Context {
366    /// Execute a PostScript procedure synchronously and return.
367    pub fn exec_sync(&mut self, proc_obj: PsObject) -> Result<(), PsError> {
368        let f = self.exec_sync_fn.expect("exec_sync not initialized");
369        f(self, proc_obj)
370    }
371
372    /// Create a new context with empty stacks and stores.
373    /// Call `build_system_dict` afterward to populate operators.
374    pub fn new() -> Self {
375        let mut names = NameTable::new();
376
377        let name_cache = NameCache {
378            n_def: names.intern(b"def"),
379            n_true: names.intern(b"true"),
380            n_false: names.intern(b"false"),
381            n_null: names.intern(b"null"),
382            n_mark: names.intern(b"mark"),
383            n_font_name: names.intern(b"FontName"),
384            n_font_type: names.intern(b"FontType"),
385            n_font_matrix: names.intern(b"FontMatrix"),
386            n_font_bbox: names.intern(b"FontBBox"),
387            n_encoding: names.intern(b"Encoding"),
388            n_char_strings: names.intern(b"CharStrings"),
389            n_private: names.intern(b"Private"),
390            n_fid: names.intern(b"FID"),
391            n_paint_type: names.intern(b"PaintType"),
392            n_subrs: names.intern(b"Subrs"),
393            n_len_iv: names.intern(b"lenIV"),
394            n_notdef: names.intern(b".notdef"),
395            n_metrics: names.intern(b"Metrics"),
396            n_font_directory: names.intern(b"FontDirectory"),
397            // Resource system
398            n_find_resource: names.intern(b"FindResource"),
399            n_define_resource: names.intern(b"DefineResource"),
400            n_undef_resource: names.intern(b"UndefineResource"),
401            n_resource_status: names.intern(b"ResourceStatus"),
402            n_resource_for_all: names.intern(b"ResourceForAll"),
403            n_category: names.intern(b"Category"),
404            n_instance_type: names.intern(b"InstanceType"),
405            n_resource_dir: names.intern(b"ResourceDir"),
406            n_resource_ext: names.intern(b"ResourceExtension"),
407            n_build_char: names.intern(b"BuildChar"),
408            n_build_glyph: names.intern(b"BuildGlyph"),
409            n_stroke_width: names.intern(b"StrokeWidth"),
410            n_wmode: names.intern(b"WMode"),
411        };
412
413        let mut strings = DualStringStore::new();
414        let mut dicts = DualDictStore::new();
415
416        // Only systemdict is pre-allocated in Rust — it's needed to register native
417        // operators. All other well-known dicts (globaldict, userdict, errordict, $error,
418        // FontDirectory) are created by the init scripts in sysdict.ps.
419        let systemdict = dicts.allocate_with(400, b"systemdict", 0, true, 0);
420        let globaldict = dicts.allocate_with(100, b"globaldict", 0, true, 0);
421        let userdict = dicts.allocate(200, b"userdict");
422        let errordict = dicts.allocate(50, b"errordict");
423        let dollar_error = dicts.allocate(20, b"$error");
424        let font_directory = dicts.allocate(50, b"FontDirectory");
425
426        // Resource system dicts (global VM)
427        let global_resources = dicts.allocate_with(20, b"GlobalResources", 0, true, 0);
428        let local_resources = dicts.allocate(20, b"LocalResources");
429        let category_registry = dicts.allocate_with(30, b"CategoryRegistry", 0, true, 0);
430
431        // Parameter dicts — pre-populate user_params with recognized keys.
432        // setuserparams only updates existing keys; unknown keys are
433        // ignored per PLRM.
434        let user_params = dicts.allocate(25, b"UserParams");
435        for key_name in [
436            "MaxDictStack",
437            "MaxExecStack",
438            "MaxOpStack",
439            "MaxFontItem",
440            "MaxFormItem",
441            "MaxPatternItem",
442            "MaxUPathItem",
443            "MaxScreenItem",
444            "MaxSuperScreen",
445            "MinFontCompress",
446            "MaxLocalVM",
447            "VMReclaim",
448            "VMThreshold",
449            "UCacheBLimit",
450        ] {
451            dicts.put(
452                user_params,
453                DictKey::Name(names.intern(key_name.as_bytes())),
454                PsObject::int(0),
455            );
456        }
457        dicts.put(
458            user_params,
459            DictKey::Name(names.intern(b"JobName")),
460            PsObject::string(strings.allocate_from(b""), 0),
461        );
462        dicts.put(
463            user_params,
464            DictKey::Name(names.intern(b"ExecutionHistory")),
465            PsObject::bool(false),
466        );
467        dicts.put(
468            user_params,
469            DictKey::Name(names.intern(b"ExecutionHistorySize")),
470            PsObject::int(20),
471        );
472        dicts.put(
473            user_params,
474            DictKey::Name(names.intern(b"IdiomRecognition")),
475            PsObject::bool(true),
476        );
477        dicts.put(
478            user_params,
479            DictKey::Name(names.intern(b"AccurateScreens")),
480            PsObject::bool(false),
481        );
482        dicts.put(
483            user_params,
484            DictKey::Name(names.intern(b"HalftoneMode")),
485            PsObject::int(0),
486        );
487
488        let system_params = dicts.allocate(30, b"SystemParams");
489        // Cache size limits (PLRM Table C.2 - system parameters)
490        for (key, val) in [
491            ("MaxFontCache", 67108864),
492            ("MaxFormCache", 131072),
493            ("MaxPatternCache", 131072),
494            ("MaxUPathCache", 131072),
495            ("MaxScreenStorage", 524288),
496            ("MaxDisplayList", 2097152),
497            ("MaxDisplayAndSourceList", 4194304),
498            ("MaxSourceList", 2097152),
499            ("MaxImageBuffer", 524288),
500            ("MaxOutlineCache", 65536),
501            ("MaxStoredScreenCache", 0),
502            // Read-only current cache usage counters
503            ("CurFontCache", 0),
504            ("CurFormCache", 0),
505            ("CurPatternCache", 0),
506            ("CurUPathCache", 0),
507            ("CurScreenStorage", 0),
508            ("CurSourceList", 0),
509            ("CurStoredScreenCache", 0),
510            ("CurOutlineCache", 0),
511            ("PageCount", 0),
512            ("Revision", 1),
513        ] {
514            dicts.put(
515                system_params,
516                DictKey::Name(names.intern(key.as_bytes())),
517                PsObject::int(val),
518            );
519        }
520        let printer_str = strings.allocate_from(b"stet");
521        dicts.put(
522            system_params,
523            DictKey::Name(names.intern(b"PrinterName")),
524            PsObject::string(printer_str, 6),
525        );
526        let realfmt_str = strings.allocate_from(b"IEE");
527        dicts.put(
528            system_params,
529            DictKey::Name(names.intern(b"RealFormat")),
530            PsObject::string(realfmt_str, 3),
531        );
532        let pw_str = strings.allocate_from(b"0");
533        dicts.put(
534            system_params,
535            DictKey::Name(names.intern(b"SystemParamsPassword")),
536            PsObject::string(pw_str, 1),
537        );
538        let pw_str2 = strings.allocate_from(b"0");
539        dicts.put(
540            system_params,
541            DictKey::Name(names.intern(b"StartJobPassword")),
542            PsObject::string(pw_str2, 1),
543        );
544        dicts.put(
545            system_params,
546            DictKey::Name(names.intern(b"LicenseID")),
547            PsObject::int(0),
548        );
549
550        // Put self-referencing entries
551        let sd_obj = PsObject::dict(systemdict);
552        dicts.put(
553            systemdict,
554            DictKey::Name(names.intern(b"systemdict")),
555            sd_obj,
556        );
557
558        let ud_obj = PsObject::dict(userdict);
559        dicts.put(systemdict, DictKey::Name(names.intern(b"userdict")), ud_obj);
560
561        let gd_obj = PsObject::dict(globaldict);
562        dicts.put(
563            systemdict,
564            DictKey::Name(names.intern(b"globaldict")),
565            gd_obj,
566        );
567
568        let ed_obj = PsObject::dict(errordict);
569        dicts.put(
570            systemdict,
571            DictKey::Name(names.intern(b"errordict")),
572            ed_obj,
573        );
574
575        let de_obj = PsObject::dict(dollar_error);
576        dicts.put(systemdict, DictKey::Name(names.intern(b"$error")), de_obj);
577
578        let fd_obj = PsObject::dict(font_directory);
579        dicts.put(
580            systemdict,
581            DictKey::Name(name_cache.n_font_directory),
582            fd_obj,
583        );
584
585        // Register constants in systemdict
586        dicts.put(
587            systemdict,
588            DictKey::Name(names.intern(b"true")),
589            PsObject::bool(true),
590        );
591        dicts.put(
592            systemdict,
593            DictKey::Name(names.intern(b"false")),
594            PsObject::bool(false),
595        );
596        dicts.put(
597            systemdict,
598            DictKey::Name(names.intern(b"null")),
599            PsObject::null(),
600        );
601
602        // mark — literal mark object
603        dicts.put(
604            systemdict,
605            DictKey::Name(names.intern(b"mark")),
606            PsObject::mark(),
607        );
608
609        // [ is an alias for mark
610        dicts.put(
611            systemdict,
612            DictKey::Name(names.intern(b"[")),
613            PsObject::mark(),
614        );
615
616        // << is a dict mark (distinct from [ mark so ] doesn't match it)
617        dicts.put(
618            systemdict,
619            DictKey::Name(names.intern(b"<<")),
620            PsObject::dict_mark(),
621        );
622
623        // version and languagelevel
624        dicts.put(
625            systemdict,
626            DictKey::Name(names.intern(b"languagelevel")),
627            PsObject::int(3),
628        );
629
630        // Dictionary stack: systemdict, globaldict, userdict
631        let d_stack = vec![systemdict, globaldict, userdict];
632
633        Self {
634            o_stack: Stack::new(500),
635            e_stack: Stack::new(250),
636            d_stack,
637            strings,
638            arrays: DualArrayStore::new(),
639            dicts,
640            names,
641            files: FileStore::new(),
642            loops: Vec::new(),
643            operators: Vec::new(),
644            systemdict,
645            globaldict,
646            userdict,
647            errordict,
648            dollar_error,
649            rand_state: 0,
650            rand_seed: 0,
651            current_source_line: 1,
652            packing_mode: false,
653            echo: false,
654            name_cache,
655            stdout: Box::new(std::io::stdout()),
656            save_stack: SaveStack::new(),
657            job_start_save_depth: 0,
658            vm_alloc_mode: false,
659            object_format: 0,
660            current_operator: None,
661            in_error_handler: false,
662            initializing: true,
663            allow_ps_resolution: false,
664            exit_code: None,
665            gstate: GraphicsState::new(),
666            gstate_stack: Vec::new(),
667            gstate_store: Vec::new(),
668            device: None,
669            display_list: DisplayList::new(),
670            group_stack: Vec::new(),
671            save_group_depths: rustc_hash::FxHashMap::default(),
672            ocg_registry: rustc_hash::FxHashMap::default(),
673            next_ocg_id: 0,
674            pdfmark_buffer: crate::pdfmark::PdfMarkBuffer::new(),
675            capture_display_lists: None,
676            display_list_sender: None,
677            page_width: 612,
678            page_height: 792,
679            output_path: None,
680            page_filter: None,
681            device_factory: None,
682            font_directory,
683            font_resource_path: None,
684            next_fid: 0,
685            global_resources,
686            local_resources,
687            category_registry,
688            resource_base_path: None,
689            user_params,
690            system_params,
691            internaldict: None,
692            icc_cache: crate::icc::IccCache::new(),
693            exec_sync_fn: None,
694            char_width: None,
695            char_width_mode1: None,
696            glyph_caches: rustc_hash::FxHashMap::default(),
697            char_cache_mode: None,
698            cshow_pending_cid: None,
699            pattern_store: Vec::new(),
700            form_cache: rustc_hash::FxHashMap::default(),
701            #[cfg(not(target_arch = "wasm32"))]
702            start_time: Some(std::time::Instant::now()),
703            #[cfg(target_arch = "wasm32")]
704            start_time: None,
705            dict_version: 0,
706            name_resolve_cache: Vec::new(),
707            interrupt_flag: None,
708            yield_after_showpage: false,
709        }
710    }
711
712    /// Create a context that captures stdout to a buffer (for testing).
713    pub fn new_with_output(output: Box<dyn Write>) -> Self {
714        let mut ctx = Self::new();
715        ctx.stdout = output;
716        ctx
717    }
718
719    // --- Dictionary stack operations ---
720
721    /// Look up a name in the dictionary stack (top to bottom).
722    #[inline]
723    pub fn dict_load(&mut self, key: &DictKey) -> Option<PsObject> {
724        // Fast path: check name resolution cache
725        if let DictKey::Name(name_id) = key {
726            let idx = name_id.0 as usize;
727            if idx < self.name_resolve_cache.len() {
728                let (ver, obj) = self.name_resolve_cache[idx];
729                if ver == self.dict_version {
730                    return Some(obj);
731                }
732            }
733        }
734
735        // Slow path: search dict stack
736        for &dict_id in self.d_stack.iter().rev() {
737            if let Some(val) = self.dicts.get(dict_id, key) {
738                // Cache the result for Name keys
739                if let DictKey::Name(name_id) = key {
740                    let idx = name_id.0 as usize;
741                    if idx >= self.name_resolve_cache.len() {
742                        self.name_resolve_cache
743                            .resize(idx + 64, (u64::MAX, PsObject::null()));
744                    }
745                    self.name_resolve_cache[idx] = (self.dict_version, val);
746                }
747                return Some(val);
748            }
749        }
750        None
751    }
752
753    /// Invalidate the name resolution cache (call on begin/end/def).
754    #[inline]
755    pub fn invalidate_name_cache(&mut self) {
756        self.dict_version = self.dict_version.wrapping_add(1);
757    }
758
759    /// Look up and return `(dict_entity, value)` pair.
760    pub fn dict_where(&self, key: &DictKey) -> Option<(EntityId, PsObject)> {
761        for &dict_id in self.d_stack.iter().rev() {
762            if let Some(val) = self.dicts.get(dict_id, key) {
763                return Some((dict_id, val));
764            }
765        }
766        None
767    }
768
769    /// Store in current dict (top of d_stack).
770    pub fn dict_def(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError> {
771        let current = *self.d_stack.last().ok_or(PsError::DictStackUnderflow)?;
772        self.cow_check_dict(current);
773        self.invalidate_name_cache();
774        self.dicts.put(current, key, value);
775        Ok(())
776    }
777
778    /// Store in first dict that contains key, or current dict if not found.
779    pub fn dict_store(&mut self, key: DictKey, value: PsObject) -> Result<(), PsError> {
780        self.invalidate_name_cache();
781        for &dict_id in self.d_stack.iter().rev() {
782            if self.dicts.known(dict_id, &key) {
783                self.cow_check_dict(dict_id);
784                self.dicts.put(dict_id, key, value);
785                return Ok(());
786            }
787        }
788        // Not found — store in current dict
789        self.dict_def(key, value)
790    }
791
792    /// Convert a `PsObject` to a `DictKey`.
793    pub fn make_dict_key(&mut self, obj: &PsObject) -> Result<DictKey, PsError> {
794        match obj.value {
795            PsValue::Name(id) => Ok(DictKey::Name(id)),
796            PsValue::Int(v) => Ok(DictKey::Int(v)),
797            PsValue::Real(v) => Ok(DictKey::Real(v.to_bits())),
798            PsValue::Bool(v) => Ok(DictKey::Bool(v)),
799            PsValue::String { entity, start, len } => {
800                // Intern string as name — PostScript treats string and name
801                // keys as equivalent in dict lookups.
802                let bytes = self.strings.get(entity, start, len).to_vec();
803                let name_id = self.names.intern(&bytes);
804                Ok(DictKey::Name(name_id))
805            }
806            PsValue::Operator(op) => Ok(DictKey::Operator(op.0)),
807            PsValue::Array { entity, start, len } | PsValue::PackedArray { entity, start, len } => {
808                Ok(DictKey::Identity(entity.0, start, len))
809            }
810            PsValue::Dict(entity) => Ok(DictKey::Identity(entity.0, 0, 0)),
811            PsValue::Null => Err(PsError::TypeCheck),
812            _ => Err(PsError::TypeCheck),
813        }
814    }
815
816    /// Allocate a new loop state, returning its EntityId.
817    pub fn alloc_loop(&mut self, state: LoopState) -> EntityId {
818        let id = EntityId(self.loops.len() as u32);
819        self.loops.push(state);
820        id
821    }
822
823    /// Get a loop state by EntityId.
824    pub fn get_loop(&self, entity: EntityId) -> &LoopState {
825        &self.loops[entity.0 as usize]
826    }
827
828    /// Get a mutable loop state by EntityId.
829    pub fn get_loop_mut(&mut self, entity: EntityId) -> &mut LoopState {
830        &mut self.loops[entity.0 as usize]
831    }
832
833    /// Return the display list paint operators should currently append to.
834    ///
835    /// While a transparency group is active (`group_stack` non-empty),
836    /// the topmost frame's display list is returned. Otherwise the
837    /// page-level `display_list` is returned. Every paint-emitting
838    /// operator must route through this helper to keep group capture
839    /// correct.
840    #[inline]
841    pub fn current_display_list_mut(&mut self) -> &mut DisplayList {
842        if let Some(frame) = self.group_stack.last_mut() {
843            &mut frame.display_list
844        } else {
845            &mut self.display_list
846        }
847    }
848
849    /// Read-only counterpart to [`Self::current_display_list_mut`].
850    #[inline]
851    pub fn current_display_list(&self) -> &DisplayList {
852        if let Some(frame) = self.group_stack.last() {
853            &frame.display_list
854        } else {
855            &self.display_list
856        }
857    }
858
859    /// Take the display list, optionally capturing a clone for viewport re-rendering.
860    ///
861    /// This replaces `std::mem::take(&mut ctx.display_list)` at showpage/copypage
862    /// call sites. When `capture_display_lists` is active, a clone is saved
863    /// along with the current page DPI from the pagedevice HWResolution.
864    pub fn take_display_list(&mut self) -> DisplayList {
865        if self.capture_display_lists.is_some() {
866            let dpi = self.current_page_dpi();
867            if let Some(ref mut captures) = self.capture_display_lists {
868                captures.push((self.display_list.clone(), dpi));
869            }
870        }
871        if let Some(ref sender) = self.display_list_sender {
872            let dpi = self.current_page_dpi();
873            // Use the device's actual page size (device pixels), not
874            // self.page_width/page_height which are point values.
875            let (w, h) = self
876                .device
877                .as_ref()
878                .map(|d| d.page_size())
879                .unwrap_or((self.page_width, self.page_height));
880            // PS interpreter output: no PDF-specific CMYK profile in play, so
881            // the viewer uses its CLI-level default. PDF/X proofing is a
882            // PDF-only concept; PostScript always sends `false`.
883            let _ = sender.send((self.display_list.clone(), dpi, w, h, None, false));
884        }
885        // Page-boundary yield: once the display list for this page has been
886        // captured (above), signal the eval loop to return so the caller can
887        // hand the page off to a renderer before interpreting the next one.
888        if self.yield_after_showpage
889            && let Some(ref flag) = self.interrupt_flag
890        {
891            flag.store(true, std::sync::atomic::Ordering::Relaxed);
892        }
893        std::mem::take(&mut self.display_list)
894    }
895
896    /// Read the current page DPI from the pagedevice HWResolution, defaulting to 72.
897    pub fn current_page_dpi(&self) -> f64 {
898        use crate::dict::DictKey;
899        if let Some(pd) = self.gstate.page_device
900            && let Some(name_id) = self.names.find(b"HWResolution")
901            && let Some(obj) = self.dicts.get(pd, &DictKey::Name(name_id))
902            && let PsValue::Array { entity, .. } = obj.value
903        {
904            let first = self.arrays.get_element(entity, 0);
905            return match first.value {
906                PsValue::Real(r) => r,
907                PsValue::Int(i) => i as f64,
908                _ => 72.0,
909            };
910        }
911        72.0
912    }
913
914    // --- VM save/restore ---
915
916    /// Perform a `save`: snapshot the current VM state.
917    /// Returns a Save PsObject.
918    pub fn vm_save(&mut self) -> PsObject {
919        let d_depth = self.d_stack.len();
920        let gstate_snapshot = self.gstate.clone();
921        let gstate_stack_snapshot = self.gstate_stack.clone();
922        let (_level, save_id) = self.save_stack.save(
923            d_depth,
924            self.packing_mode,
925            self.vm_alloc_mode,
926            self.object_format,
927            gstate_snapshot,
928            gstate_stack_snapshot,
929        );
930
931        // Implicit gsave: push current gstate marked as save-created (per PLRM).
932        // grestoreall stops at this entry; grestore skips it.
933        self.gstate_stack.push(crate::graphics_state::GstateEntry {
934            state: self.gstate.clone(),
935            saved_by_save: true,
936        });
937
938        PsObject {
939            value: PsValue::Save(SaveLevel(save_id)),
940            flags: crate::object::ObjFlags::literal(),
941        }
942    }
943
944    /// Perform a `restore`: revert VM to the given save state.
945    pub fn vm_restore(&mut self, save_id: u32) -> Result<(), PsError> {
946        // Validate save_id
947        if !self.save_stack.is_valid(save_id) {
948            return Err(PsError::InvalidRestore);
949        }
950
951        // Per PLRM: "restore can reset VM to the state represented by any
952        // save object that is still valid, not necessarily the one produced
953        // by the most recent save."  Pop the target level AND all newer
954        // levels, undoing COW records from newest to target.
955        let levels = self
956            .save_stack
957            .restore_to(save_id)
958            .ok_or(PsError::InvalidRestore)?;
959
960        // Undo COW records from newest level to oldest (reverse order).
961        // Each level's records are also processed in reverse.
962        // After swapping offsets, reset save_level to 0 so future COW
963        // checks at the same save level don't incorrectly skip the backup.
964        for level in levels.iter().rev() {
965            for record in level.records.iter().rev() {
966                match record.store_type {
967                    StoreType::String => {
968                        self.strings.swap_offsets(record.src, record.copy);
969                        self.strings.entity_meta_mut(record.src).save_level = 0;
970                    }
971                    StoreType::Array => {
972                        self.arrays.swap_offsets(record.src, record.copy);
973                        self.arrays.entity_meta_mut(record.src).save_level = 0;
974                    }
975                    StoreType::Dict => {
976                        self.dicts.swap_offsets(record.src, record.copy);
977                        self.dicts.entity_meta_mut(record.src).save_level = 0;
978                    }
979                }
980            }
981        }
982
983        // Restore context parameters from the TARGET save level (first in vec)
984        let target = &levels[0];
985        self.packing_mode = target.packing_mode;
986        self.vm_alloc_mode = target.vm_alloc_mode;
987        self.object_format = target.object_format;
988
989        // Restore graphics state from the target level
990        self.gstate = target.gstate.clone();
991        self.gstate_stack = target.gstate_stack.clone();
992
993        // Restore d_stack depth from the target level
994        self.d_stack.truncate(target.d_stack_depth);
995
996        self.invalidate_name_cache();
997        Ok(())
998    }
999
1000    // --- COW check methods ---
1001
1002    /// Check if a string entity needs COW before mutation.
1003    /// If yes, creates a backup copy and records it.
1004    pub fn cow_check_string(&mut self, entity: EntityId) {
1005        let current_level = self.save_stack.current_level();
1006        if current_level == 0 {
1007            return; // No save active
1008        }
1009
1010        if entity.is_global() {
1011            return; // Global entities skip local COW
1012        }
1013        let meta = self.strings.entity_meta(entity);
1014        if meta.save_level >= current_level {
1015            return; // Already copied at this level
1016        }
1017
1018        // Perform COW copy
1019        let copy_id = self.strings.cow_copy(entity);
1020        self.strings.entity_meta_mut(entity).save_level = current_level;
1021
1022        self.save_stack.add_record(SaveRecord {
1023            src: entity,
1024            copy: copy_id,
1025            store_type: StoreType::String,
1026        });
1027    }
1028
1029    /// Check if an array entity needs COW before mutation.
1030    pub fn cow_check_array(&mut self, entity: EntityId) {
1031        let current_level = self.save_stack.current_level();
1032        if current_level == 0 {
1033            return;
1034        }
1035
1036        if entity.is_global() {
1037            return;
1038        }
1039        let meta = self.arrays.entity_meta(entity);
1040        if meta.save_level >= current_level {
1041            return;
1042        }
1043
1044        let copy_id = self.arrays.cow_copy(entity);
1045        self.arrays.entity_meta_mut(entity).save_level = current_level;
1046
1047        self.save_stack.add_record(SaveRecord {
1048            src: entity,
1049            copy: copy_id,
1050            store_type: StoreType::Array,
1051        });
1052    }
1053
1054    /// Check if a dict entity needs COW before mutation.
1055    pub fn cow_check_dict(&mut self, entity: EntityId) {
1056        let current_level = self.save_stack.current_level();
1057        if current_level == 0 {
1058            return;
1059        }
1060
1061        if entity.is_global() {
1062            return;
1063        }
1064        let meta = self.dicts.entity_meta(entity);
1065        if meta.save_level >= current_level {
1066            return;
1067        }
1068
1069        let copy_id = self.dicts.cow_copy(entity);
1070        self.dicts.entity_meta_mut(entity).save_level = current_level;
1071
1072        self.save_stack.add_record(SaveRecord {
1073            src: entity,
1074            copy: copy_id,
1075            store_type: StoreType::Dict,
1076        });
1077    }
1078
1079    // --- Token conversion ---
1080
1081    /// Convert a tokenizer token into a PsObject.
1082    pub fn token_to_object(&mut self, token: crate::tokenizer::Token) -> Result<PsObject, PsError> {
1083        use crate::tokenizer::Token;
1084        match token {
1085            Token::Int(v) => Ok(PsObject::int(v)),
1086            Token::Real(v) => Ok(PsObject::real(v)),
1087            Token::String(bytes) => {
1088                let save_level = self.save_stack.current_level();
1089                let global = self.vm_alloc_mode;
1090                let created = self.save_stack.last_save_id();
1091                let entity = self
1092                    .strings
1093                    .allocate_with(bytes.len(), save_level, global, created);
1094                self.strings
1095                    .get_mut(entity, 0, bytes.len() as u32)
1096                    .copy_from_slice(&bytes);
1097                let mut obj = PsObject::string(entity, bytes.len() as u32);
1098                if global {
1099                    obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, false, true, true);
1100                }
1101                Ok(obj)
1102            }
1103            Token::Name(bytes, is_exec) => {
1104                let id = self.names.intern(&bytes);
1105                if is_exec {
1106                    Ok(PsObject::name_exec(id))
1107                } else {
1108                    Ok(PsObject::name_lit(id))
1109                }
1110            }
1111            Token::LiteralName(bytes) => {
1112                let id = self.names.intern(&bytes);
1113                Ok(PsObject::name_lit(id))
1114            }
1115            Token::ImmediateName(bytes) => {
1116                let id = self.names.intern(&bytes);
1117                let key = DictKey::Name(id);
1118                self.dict_load(&key).ok_or(PsError::Undefined)
1119            }
1120            Token::ArrayBegin => {
1121                let id = self.names.intern(b"[");
1122                Ok(PsObject::name_exec(id))
1123            }
1124            Token::ArrayEnd => {
1125                let id = self.names.intern(b"]");
1126                Ok(PsObject::name_exec(id))
1127            }
1128            Token::DictBegin => {
1129                let id = self.names.intern(b"<<");
1130                Ok(PsObject::name_exec(id))
1131            }
1132            Token::DictEnd => {
1133                let id = self.names.intern(b">>");
1134                Ok(PsObject::name_exec(id))
1135            }
1136            Token::ProcBegin | Token::ProcEnd | Token::Eof | Token::BinaryTokenByte(_) => {
1137                Err(PsError::SyntaxError)
1138            }
1139        }
1140    }
1141
1142    /// Reset local VM stores (for job boundary cleanup).
1143    /// Full implementation deferred until job server loop is built.
1144    pub fn reset_local_vm(&mut self) {
1145        self.strings.reset_local();
1146        self.arrays.reset_local();
1147        self.dicts.reset_local();
1148    }
1149}
1150
1151impl Default for Context {
1152    fn default() -> Self {
1153        Self::new()
1154    }
1155}
1156
1157#[cfg(test)]
1158mod tests {
1159    use super::*;
1160
1161    #[test]
1162    fn test_context_creation() {
1163        let ctx = Context::new();
1164        assert!(ctx.o_stack.is_empty());
1165        assert!(ctx.e_stack.is_empty());
1166        assert_eq!(ctx.d_stack.len(), 3); // systemdict, globaldict, userdict
1167    }
1168
1169    #[test]
1170    fn test_dict_def_and_load() {
1171        let mut ctx = Context::new();
1172        let key = DictKey::Name(ctx.names.intern(b"foo"));
1173        ctx.dict_def(key.clone(), PsObject::int(42)).unwrap();
1174
1175        let val = ctx.dict_load(&key).unwrap();
1176        assert_eq!(val.as_i32(), Some(42));
1177    }
1178
1179    #[test]
1180    fn test_dict_where() {
1181        let mut ctx = Context::new();
1182        let key = DictKey::Name(ctx.names.intern(b"true"));
1183        let result = ctx.dict_where(&key);
1184        assert!(result.is_some());
1185        let (dict_id, val) = result.unwrap();
1186        assert_eq!(dict_id, ctx.systemdict);
1187        assert!(matches!(val.value, PsValue::Bool(true)));
1188    }
1189
1190    #[test]
1191    fn test_dict_store_existing() {
1192        let mut ctx = Context::new();
1193        let key = DictKey::Name(ctx.names.intern(b"myvar"));
1194
1195        // Define in userdict
1196        ctx.dict_def(key.clone(), PsObject::int(1)).unwrap();
1197
1198        // Store should update the existing entry in userdict
1199        ctx.dict_store(key.clone(), PsObject::int(2)).unwrap();
1200
1201        let val = ctx.dict_load(&key).unwrap();
1202        assert_eq!(val.as_i32(), Some(2));
1203    }
1204
1205    #[test]
1206    fn test_save_restore_basic() {
1207        let mut ctx = Context::new();
1208        let key = DictKey::Name(ctx.names.intern(b"testvar"));
1209
1210        // Define before save
1211        ctx.dict_def(key.clone(), PsObject::int(1)).unwrap();
1212
1213        // Save
1214        let save_obj = ctx.vm_save();
1215        let save_id = match save_obj.value {
1216            PsValue::Save(SaveLevel(id)) => id,
1217            _ => panic!("Expected Save"),
1218        };
1219
1220        // Modify after save
1221        ctx.dict_def(key.clone(), PsObject::int(2)).unwrap();
1222        assert_eq!(ctx.dict_load(&key).unwrap().as_i32(), Some(2));
1223
1224        // Restore
1225        ctx.vm_restore(save_id).unwrap();
1226        assert_eq!(ctx.dict_load(&key).unwrap().as_i32(), Some(1));
1227    }
1228
1229    #[test]
1230    fn test_save_restore_string() {
1231        let mut ctx = Context::new();
1232
1233        let entity = ctx.strings.allocate_from(b"hello");
1234
1235        // Save
1236        let save_obj = ctx.vm_save();
1237        let save_id = match save_obj.value {
1238            PsValue::Save(SaveLevel(id)) => id,
1239            _ => panic!("Expected Save"),
1240        };
1241
1242        // Modify after save
1243        ctx.cow_check_string(entity);
1244        ctx.strings.put_byte(entity, 0, b'H');
1245        assert_eq!(ctx.strings.get(entity, 0, 5), b"Hello");
1246
1247        // Restore
1248        ctx.vm_restore(save_id).unwrap();
1249        assert_eq!(ctx.strings.get(entity, 0, 5), b"hello");
1250    }
1251
1252    #[test]
1253    fn test_save_restore_array() {
1254        let mut ctx = Context::new();
1255
1256        let items = [PsObject::int(1), PsObject::int(2), PsObject::int(3)];
1257        let entity = ctx.arrays.allocate_from(&items);
1258
1259        let save_obj = ctx.vm_save();
1260        let save_id = match save_obj.value {
1261            PsValue::Save(SaveLevel(id)) => id,
1262            _ => panic!("Expected Save"),
1263        };
1264
1265        ctx.cow_check_array(entity);
1266        ctx.arrays.set_element(entity, 1, PsObject::int(99));
1267        assert_eq!(ctx.arrays.get_element(entity, 1).as_i32(), Some(99));
1268
1269        ctx.vm_restore(save_id).unwrap();
1270        assert_eq!(ctx.arrays.get_element(entity, 1).as_i32(), Some(2));
1271    }
1272
1273    #[test]
1274    fn test_invalid_restore() {
1275        let mut ctx = Context::new();
1276        // Restore without save
1277        assert_eq!(ctx.vm_restore(999), Err(PsError::InvalidRestore));
1278    }
1279}