Skip to main content

stet_core/
object.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! PostScript object representation.
6//!
7//! `PsObject` is the fundamental unit — a tagged value with metadata flags.
8//! Objects are `Clone + Copy` (value types with arena indices, not heap references).
9
10/// Packed object metadata (1 byte).
11///
12/// Layout:
13/// - Bits 0-2: access level (0-4)
14/// - Bit 3: executable (0=literal, 1=executable)
15/// - Bit 4: global (0=local, 1=global)
16/// - Bit 5: composite (0=simple, 1=composite)
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct ObjFlags(u8);
19
20impl ObjFlags {
21    pub const LITERAL: u8 = 0;
22    pub const EXECUTABLE: u8 = 1 << 3;
23
24    pub const ACCESS_NONE: u8 = 0;
25    pub const ACCESS_EXECUTE_ONLY: u8 = 1;
26    pub const ACCESS_READ_ONLY: u8 = 2;
27    pub const ACCESS_WRITE_ONLY: u8 = 3;
28    pub const ACCESS_UNLIMITED: u8 = 4;
29
30    const ACCESS_MASK: u8 = 0b0000_0111;
31    const EXEC_BIT: u8 = 1 << 3;
32    const GLOBAL_BIT: u8 = 1 << 4;
33    const COMPOSITE_BIT: u8 = 1 << 5;
34    const DEFERRED_BIT: u8 = 1 << 6;
35
36    /// Create new flags with specified attributes.
37    pub fn new(access: u8, executable: bool, global: bool, composite: bool) -> Self {
38        let mut bits = access & Self::ACCESS_MASK;
39        if executable {
40            bits |= Self::EXEC_BIT;
41        }
42        if global {
43            bits |= Self::GLOBAL_BIT;
44        }
45        if composite {
46            bits |= Self::COMPOSITE_BIT;
47        }
48        Self(bits)
49    }
50
51    /// Convenience: literal simple object with unlimited access.
52    pub fn literal() -> Self {
53        Self::new(Self::ACCESS_UNLIMITED, false, false, false)
54    }
55
56    /// Convenience: executable simple object with unlimited access.
57    pub fn executable() -> Self {
58        Self::new(Self::ACCESS_UNLIMITED, true, false, false)
59    }
60
61    /// Convenience: literal composite object with unlimited access.
62    pub fn literal_composite() -> Self {
63        Self::new(Self::ACCESS_UNLIMITED, false, false, true)
64    }
65
66    /// Convenience: executable composite object with unlimited access.
67    pub fn executable_composite() -> Self {
68        Self::new(Self::ACCESS_UNLIMITED, true, false, true)
69    }
70
71    pub fn access(self) -> u8 {
72        self.0 & Self::ACCESS_MASK
73    }
74
75    pub fn is_executable(self) -> bool {
76        self.0 & Self::EXEC_BIT != 0
77    }
78
79    pub fn is_literal(self) -> bool {
80        !self.is_executable()
81    }
82
83    pub fn is_global(self) -> bool {
84        self.0 & Self::GLOBAL_BIT != 0
85    }
86
87    pub fn is_composite(self) -> bool {
88        self.0 & Self::COMPOSITE_BIT != 0
89    }
90
91    pub fn set_executable(&mut self) {
92        self.0 |= Self::EXEC_BIT;
93    }
94
95    pub fn set_literal(&mut self) {
96        self.0 &= !Self::EXEC_BIT;
97    }
98
99    pub fn set_access(&mut self, access: u8) {
100        self.0 = (self.0 & !Self::ACCESS_MASK) | (access & Self::ACCESS_MASK);
101    }
102
103    /// Check if this object is deferred (should be pushed to o_stack from e_stack).
104    ///
105    /// Used by `exec_procedure` to mark nested executable arrays that should be
106    /// pushed to the operand stack rather than executed when encountered on the
107    /// execution stack. The executable flag remains set so operators like `if`
108    /// and `ifelse` still accept them.
109    pub fn is_deferred(self) -> bool {
110        self.0 & Self::DEFERRED_BIT != 0
111    }
112
113    /// Mark this object as deferred.
114    pub fn set_deferred(&mut self) {
115        self.0 |= Self::DEFERRED_BIT;
116    }
117
118    /// Clear the deferred flag.
119    pub fn clear_deferred(&mut self) {
120        self.0 &= !Self::DEFERRED_BIT;
121    }
122
123    /// Require read access (>= READ_ONLY). Returns InvalidAccess if not.
124    #[inline]
125    pub fn require_read(self) -> Result<(), crate::error::PsError> {
126        if self.access() >= Self::ACCESS_READ_ONLY {
127            Ok(())
128        } else {
129            Err(crate::error::PsError::InvalidAccess)
130        }
131    }
132
133    /// Require write access (>= UNLIMITED) for non-file composites. Returns InvalidAccess if not.
134    #[inline]
135    pub fn require_write(self) -> Result<(), crate::error::PsError> {
136        if self.access() >= Self::ACCESS_UNLIMITED {
137            Ok(())
138        } else {
139            Err(crate::error::PsError::InvalidAccess)
140        }
141    }
142
143    /// Require file write access (>= WRITE_ONLY). Returns InvalidAccess if not.
144    #[inline]
145    pub fn require_file_write(self) -> Result<(), crate::error::PsError> {
146        if self.access() >= Self::ACCESS_WRITE_ONLY {
147            Ok(())
148        } else {
149            Err(crate::error::PsError::InvalidAccess)
150        }
151    }
152}
153
154/// Index into the name interning table.
155#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
156pub struct NameId(pub u32);
157
158/// Index into an arena store (strings, arrays, dicts, loop states).
159///
160/// Bit 31 is the global VM tag: set = global, clear = local.
161/// Bits 0-30 are the index into the store's entity table.
162#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
163pub struct EntityId(pub u32);
164
165impl EntityId {
166    const GLOBAL_BIT: u32 = 1 << 31;
167    const INDEX_MASK: u32 = !(1 << 31);
168
169    /// Create a local VM entity ID.
170    pub fn local(index: u32) -> Self {
171        debug_assert!(
172            index & Self::GLOBAL_BIT == 0,
173            "index overflows into tag bit"
174        );
175        EntityId(index)
176    }
177
178    /// Create a global VM entity ID.
179    pub fn global(index: u32) -> Self {
180        debug_assert!(
181            index & Self::GLOBAL_BIT == 0,
182            "index overflows into tag bit"
183        );
184        EntityId(index | Self::GLOBAL_BIT)
185    }
186
187    /// Check if this entity is in global VM.
188    #[inline]
189    pub fn is_global(self) -> bool {
190        self.0 & Self::GLOBAL_BIT != 0
191    }
192
193    /// Get the raw index (bits 0-30) for indexing into a store's entity table.
194    #[inline]
195    pub fn raw_index(self) -> usize {
196        (self.0 & Self::INDEX_MASK) as usize
197    }
198}
199
200/// Index into the operator dispatch table.
201#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
202pub struct OpCode(pub u16);
203
204/// Save/restore nesting level.
205#[derive(Clone, Copy, Debug, PartialEq, Eq)]
206pub struct SaveLevel(pub u32);
207
208/// The value payload of a PostScript object.
209#[derive(Clone, Copy, Debug, PartialEq)]
210pub enum PsValue {
211    // Simple types (no arena allocation)
212    Null,
213    Mark,
214    /// Dict mark from `<<` — distinguished from `Mark` so `]` only matches `[`-marks.
215    DictMark,
216    Bool(bool),
217    /// A PostScript integer.
218    ///
219    /// 64-bit, matching Ghostscript. PLRM Appendix B's 32-bit range is stated
220    /// as a limit "typical of PostScript implementations from Adobe Systems"
221    /// running "on 32-bit machines", which "do not necessarily apply to all
222    /// PostScript implementations" — not a conformance requirement. 32 bits
223    /// breaks the standard LCG idiom
224    /// (`seed 1103515245 mul 12345 add 2147483648 mod`) that PostScript
225    /// programs use for pseudo-randomness: the product overflows, promotes to
226    /// `Real`, and `mod` then raises `typecheck`. Widening the fallback is not
227    /// an option — the product needs 55 bits and `f64` carries 53, so the
228    /// sequence would silently diverge from every other interpreter.
229    ///
230    /// Costs nothing: `Real(f64)` already forces an 8-byte payload.
231    Int(i64),
232    Real(f64),
233
234    // Interned name (index into NameTable)
235    Name(NameId),
236
237    // Composite types (arena-backed)
238    String {
239        entity: EntityId,
240        start: u32,
241        len: u32,
242    },
243    Array {
244        entity: EntityId,
245        start: u32,
246        len: u32,
247    },
248    PackedArray {
249        entity: EntityId,
250        start: u32,
251        len: u32,
252    },
253    Dict(EntityId),
254
255    // Executable types
256    Operator(OpCode),
257
258    // Special types
259    File(EntityId),
260    Save(SaveLevel),
261    FontID(i32),
262    /// Gstate object (index into Context.gstate_store)
263    Gstate(u32),
264
265    // Control flow (internal, not user-visible)
266    Stopped,
267    Loop(EntityId),
268    HardReturn,
269    /// Marker that conditionally pops the dict stack when reached (used by
270    /// resource operators to clean up after dispatching to PS-defined category
271    /// procedures). Carries the expected entity so we only pop if it's still
272    /// on top — the PS procedure may have already called `end`.
273    DictEnd(EntityId),
274
275    /// Procedure cursor on the exec stack — tracks position within a procedure
276    /// being executed. The eval loop advances `pos` one element at a time.
277    ExecArray {
278        entity: EntityId,
279        start: u32,
280        len: u32,
281        pos: u32,
282    },
283}
284
285/// A PostScript object: a tagged value with metadata flags.
286///
287/// `PsObject` is `Clone + Copy` — it's a value type containing indices
288/// into arena stores, not heap references.
289#[derive(Clone, Copy, Debug, PartialEq)]
290pub struct PsObject {
291    pub value: PsValue,
292    pub flags: ObjFlags,
293}
294
295impl PsObject {
296    // --- Convenience constructors ---
297
298    /// Build an integer object.
299    ///
300    /// Generic over anything that widens losslessly to `i64` so the many
301    /// `i32`/`u8`/`u16` call sites need no cast.
302    pub fn int(v: impl Into<i64>) -> Self {
303        let v: i64 = v.into();
304        Self {
305            value: PsValue::Int(v),
306            flags: ObjFlags::literal(),
307        }
308    }
309
310    pub fn real(v: f64) -> Self {
311        Self {
312            value: PsValue::Real(v),
313            flags: ObjFlags::literal(),
314        }
315    }
316
317    pub fn bool(v: bool) -> Self {
318        Self {
319            value: PsValue::Bool(v),
320            flags: ObjFlags::literal(),
321        }
322    }
323
324    pub fn null() -> Self {
325        Self {
326            value: PsValue::Null,
327            flags: ObjFlags::literal(),
328        }
329    }
330
331    pub fn mark() -> Self {
332        Self {
333            value: PsValue::Mark,
334            flags: ObjFlags::literal(),
335        }
336    }
337
338    /// Dict mark from `<<` — distinct from `[`/`mark` marks.
339    pub fn dict_mark() -> Self {
340        Self {
341            value: PsValue::DictMark,
342            flags: ObjFlags::literal(),
343        }
344    }
345
346    /// Literal name: `/foo`
347    pub fn name_lit(id: NameId) -> Self {
348        Self {
349            value: PsValue::Name(id),
350            flags: ObjFlags::literal(),
351        }
352    }
353
354    /// Executable name: `foo`
355    pub fn name_exec(id: NameId) -> Self {
356        Self {
357            value: PsValue::Name(id),
358            flags: ObjFlags::executable(),
359        }
360    }
361
362    pub fn operator(op: OpCode) -> Self {
363        Self {
364            value: PsValue::Operator(op),
365            flags: ObjFlags::executable(),
366        }
367    }
368
369    /// Literal string.
370    pub fn string(entity: EntityId, len: u32) -> Self {
371        Self {
372            value: PsValue::String {
373                entity,
374                start: 0,
375                len,
376            },
377            flags: ObjFlags::literal_composite(),
378        }
379    }
380
381    /// Literal array.
382    pub fn array(entity: EntityId, len: u32) -> Self {
383        Self {
384            value: PsValue::Array {
385                entity,
386                start: 0,
387                len,
388            },
389            flags: ObjFlags::literal_composite(),
390        }
391    }
392
393    /// Executable array (procedure body).
394    pub fn procedure(entity: EntityId, len: u32) -> Self {
395        Self {
396            value: PsValue::Array {
397                entity,
398                start: 0,
399                len,
400            },
401            flags: ObjFlags::executable_composite(),
402        }
403    }
404
405    /// Dict object.
406    pub fn dict(entity: EntityId) -> Self {
407        Self {
408            value: PsValue::Dict(entity),
409            flags: ObjFlags::literal_composite(),
410        }
411    }
412
413    /// Stopped marker (internal).
414    pub fn stopped_mark() -> Self {
415        Self {
416            value: PsValue::Stopped,
417            flags: ObjFlags::executable(),
418        }
419    }
420
421    /// Loop marker (internal).
422    pub fn loop_mark(entity: EntityId) -> Self {
423        Self {
424            value: PsValue::Loop(entity),
425            flags: ObjFlags::executable(),
426        }
427    }
428
429    /// HardReturn marker (internal).
430    pub fn hard_return() -> Self {
431        Self {
432            value: PsValue::HardReturn,
433            flags: ObjFlags::executable(),
434        }
435    }
436
437    /// DictEnd marker (internal) — conditionally pops the dict stack when reached.
438    pub fn dict_end(entity: EntityId) -> Self {
439        Self {
440            value: PsValue::DictEnd(entity),
441            flags: ObjFlags::executable(),
442        }
443    }
444
445    // --- Type queries ---
446
447    pub fn is_numeric(&self) -> bool {
448        matches!(self.value, PsValue::Int(_) | PsValue::Real(_))
449    }
450
451    pub fn is_int(&self) -> bool {
452        matches!(self.value, PsValue::Int(_))
453    }
454
455    pub fn is_real(&self) -> bool {
456        matches!(self.value, PsValue::Real(_))
457    }
458
459    pub fn is_bool(&self) -> bool {
460        matches!(self.value, PsValue::Bool(_))
461    }
462
463    pub fn is_array_type(&self) -> bool {
464        matches!(
465            self.value,
466            PsValue::Array { .. } | PsValue::PackedArray { .. }
467        )
468    }
469
470    pub fn is_composite(&self) -> bool {
471        self.flags.is_composite()
472    }
473
474    /// Check if this object is in global VM using authoritative entity tag bits
475    /// for composite types, falling back to ObjFlags for simple types.
476    pub fn is_global_vm(&self) -> bool {
477        match self.value {
478            PsValue::Dict(e) => e.is_global(),
479            PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => {
480                entity.is_global()
481            }
482            PsValue::String { entity, .. } => entity.is_global(),
483            _ => self.flags.is_global(),
484        }
485    }
486
487    /// PostScript type name as bytes (e.g. `b"integertype"`).
488    pub fn type_name(&self) -> &'static [u8] {
489        match self.value {
490            PsValue::Int(_) => b"integertype",
491            PsValue::Real(_) => b"realtype",
492            PsValue::Bool(_) => b"booleantype",
493            PsValue::Null => b"nulltype",
494            PsValue::Mark | PsValue::DictMark => b"marktype",
495            PsValue::Name(_) => b"nametype",
496            PsValue::String { .. } => b"stringtype",
497            PsValue::Array { .. } => b"arraytype",
498            PsValue::PackedArray { .. } => b"packedarraytype",
499            PsValue::Dict(_) => b"dicttype",
500            PsValue::Operator(_) => b"operatortype",
501            PsValue::File(_) => b"filetype",
502            PsValue::Save(_) => b"savetype",
503            PsValue::FontID(_) => b"fonttype",
504            PsValue::Gstate(_) => b"gstatetype",
505            _ => b"nulltype", // internal types
506        }
507    }
508
509    // --- Numeric extraction ---
510
511    /// Extract as `f64` (works for both Int and Real).
512    pub fn as_f64(&self) -> Option<f64> {
513        match self.value {
514            PsValue::Int(v) => Some(v as f64),
515            PsValue::Real(v) => Some(v),
516            _ => None,
517        }
518    }
519
520    /// Extract as `i32` (Int only), rejecting values outside `i32` range.
521    ///
522    /// PostScript integers are `i64` (see [`PsValue::Int`]), but many callers
523    /// need an `i32` — array and string indices, character codes, operand
524    /// counts. Those are all genuinely bounded, and a value too large to be
525    /// one of them should fail the caller's range check rather than wrap
526    /// silently, so this returns `None` instead of truncating.
527    pub fn as_i32(&self) -> Option<i32> {
528        match self.value {
529            PsValue::Int(v) => i32::try_from(v).ok(),
530            _ => None,
531        }
532    }
533
534    /// Extract as `i64` (Int only) — the full PostScript integer range.
535    pub fn as_i64(&self) -> Option<i64> {
536        match self.value {
537            PsValue::Int(v) => Some(v),
538            _ => None,
539        }
540    }
541}
542
543#[cfg(test)]
544mod tests {
545    use super::*;
546
547    #[test]
548    fn test_obj_flags_basic() {
549        let f = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, false, true);
550        assert_eq!(f.access(), ObjFlags::ACCESS_UNLIMITED);
551        assert!(f.is_executable());
552        assert!(!f.is_global());
553        assert!(f.is_composite());
554    }
555
556    #[test]
557    fn test_obj_flags_set_literal() {
558        let mut f = ObjFlags::executable();
559        assert!(f.is_executable());
560        f.set_literal();
561        assert!(f.is_literal());
562    }
563
564    #[test]
565    fn test_ps_object_int() {
566        let obj = PsObject::int(42);
567        assert!(obj.is_int());
568        assert!(obj.is_numeric());
569        assert!(!obj.is_real());
570        assert_eq!(obj.as_i32(), Some(42));
571        assert_eq!(obj.as_f64(), Some(42.0));
572        assert_eq!(obj.type_name(), b"integertype");
573    }
574
575    #[test]
576    fn test_ps_object_real() {
577        let obj = PsObject::real(2.5);
578        assert!(obj.is_real());
579        assert!(obj.is_numeric());
580        assert_eq!(obj.as_f64(), Some(2.5));
581        assert_eq!(obj.as_i32(), None);
582        assert_eq!(obj.type_name(), b"realtype");
583    }
584
585    #[test]
586    fn test_ps_object_copy_semantics() {
587        let a = PsObject::int(10);
588        let b = a; // Copy
589        assert_eq!(a.as_i32(), Some(10));
590        assert_eq!(b.as_i32(), Some(10));
591    }
592
593    #[test]
594    fn test_ps_object_procedure() {
595        let obj = PsObject::procedure(EntityId(0), 3);
596        assert!(obj.flags.is_executable());
597        assert!(obj.flags.is_composite());
598        assert!(obj.is_array_type());
599    }
600}