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