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