Skip to main content

stet_core/
context.rs

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