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    Int(i32),
218    Real(f64),
219
220    // Interned name (index into NameTable)
221    Name(NameId),
222
223    // Composite types (arena-backed)
224    String {
225        entity: EntityId,
226        start: u32,
227        len: u32,
228    },
229    Array {
230        entity: EntityId,
231        start: u32,
232        len: u32,
233    },
234    PackedArray {
235        entity: EntityId,
236        start: u32,
237        len: u32,
238    },
239    Dict(EntityId),
240
241    // Executable types
242    Operator(OpCode),
243
244    // Special types
245    File(EntityId),
246    Save(SaveLevel),
247    FontID(i32),
248    /// Gstate object (index into Context.gstate_store)
249    Gstate(u32),
250
251    // Control flow (internal, not user-visible)
252    Stopped,
253    Loop(EntityId),
254    HardReturn,
255    /// Marker that conditionally pops the dict stack when reached (used by
256    /// resource operators to clean up after dispatching to PS-defined category
257    /// procedures). Carries the expected entity so we only pop if it's still
258    /// on top — the PS procedure may have already called `end`.
259    DictEnd(EntityId),
260
261    /// Procedure cursor on the exec stack — tracks position within a procedure
262    /// being executed. The eval loop advances `pos` one element at a time.
263    ExecArray {
264        entity: EntityId,
265        start: u32,
266        len: u32,
267        pos: u32,
268    },
269}
270
271/// A PostScript object: a tagged value with metadata flags.
272///
273/// `PsObject` is `Clone + Copy` — it's a value type containing indices
274/// into arena stores, not heap references.
275#[derive(Clone, Copy, Debug, PartialEq)]
276pub struct PsObject {
277    pub value: PsValue,
278    pub flags: ObjFlags,
279}
280
281impl PsObject {
282    // --- Convenience constructors ---
283
284    pub fn int(v: i32) -> Self {
285        Self {
286            value: PsValue::Int(v),
287            flags: ObjFlags::literal(),
288        }
289    }
290
291    pub fn real(v: f64) -> Self {
292        Self {
293            value: PsValue::Real(v),
294            flags: ObjFlags::literal(),
295        }
296    }
297
298    pub fn bool(v: bool) -> Self {
299        Self {
300            value: PsValue::Bool(v),
301            flags: ObjFlags::literal(),
302        }
303    }
304
305    pub fn null() -> Self {
306        Self {
307            value: PsValue::Null,
308            flags: ObjFlags::literal(),
309        }
310    }
311
312    pub fn mark() -> Self {
313        Self {
314            value: PsValue::Mark,
315            flags: ObjFlags::literal(),
316        }
317    }
318
319    /// Dict mark from `<<` — distinct from `[`/`mark` marks.
320    pub fn dict_mark() -> Self {
321        Self {
322            value: PsValue::DictMark,
323            flags: ObjFlags::literal(),
324        }
325    }
326
327    /// Literal name: `/foo`
328    pub fn name_lit(id: NameId) -> Self {
329        Self {
330            value: PsValue::Name(id),
331            flags: ObjFlags::literal(),
332        }
333    }
334
335    /// Executable name: `foo`
336    pub fn name_exec(id: NameId) -> Self {
337        Self {
338            value: PsValue::Name(id),
339            flags: ObjFlags::executable(),
340        }
341    }
342
343    pub fn operator(op: OpCode) -> Self {
344        Self {
345            value: PsValue::Operator(op),
346            flags: ObjFlags::executable(),
347        }
348    }
349
350    /// Literal string.
351    pub fn string(entity: EntityId, len: u32) -> Self {
352        Self {
353            value: PsValue::String {
354                entity,
355                start: 0,
356                len,
357            },
358            flags: ObjFlags::literal_composite(),
359        }
360    }
361
362    /// Literal array.
363    pub fn array(entity: EntityId, len: u32) -> Self {
364        Self {
365            value: PsValue::Array {
366                entity,
367                start: 0,
368                len,
369            },
370            flags: ObjFlags::literal_composite(),
371        }
372    }
373
374    /// Executable array (procedure body).
375    pub fn procedure(entity: EntityId, len: u32) -> Self {
376        Self {
377            value: PsValue::Array {
378                entity,
379                start: 0,
380                len,
381            },
382            flags: ObjFlags::executable_composite(),
383        }
384    }
385
386    /// Dict object.
387    pub fn dict(entity: EntityId) -> Self {
388        Self {
389            value: PsValue::Dict(entity),
390            flags: ObjFlags::literal_composite(),
391        }
392    }
393
394    /// Stopped marker (internal).
395    pub fn stopped_mark() -> Self {
396        Self {
397            value: PsValue::Stopped,
398            flags: ObjFlags::executable(),
399        }
400    }
401
402    /// Loop marker (internal).
403    pub fn loop_mark(entity: EntityId) -> Self {
404        Self {
405            value: PsValue::Loop(entity),
406            flags: ObjFlags::executable(),
407        }
408    }
409
410    /// HardReturn marker (internal).
411    pub fn hard_return() -> Self {
412        Self {
413            value: PsValue::HardReturn,
414            flags: ObjFlags::executable(),
415        }
416    }
417
418    /// DictEnd marker (internal) — conditionally pops the dict stack when reached.
419    pub fn dict_end(entity: EntityId) -> Self {
420        Self {
421            value: PsValue::DictEnd(entity),
422            flags: ObjFlags::executable(),
423        }
424    }
425
426    // --- Type queries ---
427
428    pub fn is_numeric(&self) -> bool {
429        matches!(self.value, PsValue::Int(_) | PsValue::Real(_))
430    }
431
432    pub fn is_int(&self) -> bool {
433        matches!(self.value, PsValue::Int(_))
434    }
435
436    pub fn is_real(&self) -> bool {
437        matches!(self.value, PsValue::Real(_))
438    }
439
440    pub fn is_bool(&self) -> bool {
441        matches!(self.value, PsValue::Bool(_))
442    }
443
444    pub fn is_array_type(&self) -> bool {
445        matches!(
446            self.value,
447            PsValue::Array { .. } | PsValue::PackedArray { .. }
448        )
449    }
450
451    pub fn is_composite(&self) -> bool {
452        self.flags.is_composite()
453    }
454
455    /// Check if this object is in global VM using authoritative entity tag bits
456    /// for composite types, falling back to ObjFlags for simple types.
457    pub fn is_global_vm(&self) -> bool {
458        match self.value {
459            PsValue::Dict(e) => e.is_global(),
460            PsValue::Array { entity, .. } | PsValue::PackedArray { entity, .. } => {
461                entity.is_global()
462            }
463            PsValue::String { entity, .. } => entity.is_global(),
464            _ => self.flags.is_global(),
465        }
466    }
467
468    /// PostScript type name as bytes (e.g. `b"integertype"`).
469    pub fn type_name(&self) -> &'static [u8] {
470        match self.value {
471            PsValue::Int(_) => b"integertype",
472            PsValue::Real(_) => b"realtype",
473            PsValue::Bool(_) => b"booleantype",
474            PsValue::Null => b"nulltype",
475            PsValue::Mark | PsValue::DictMark => b"marktype",
476            PsValue::Name(_) => b"nametype",
477            PsValue::String { .. } => b"stringtype",
478            PsValue::Array { .. } => b"arraytype",
479            PsValue::PackedArray { .. } => b"packedarraytype",
480            PsValue::Dict(_) => b"dicttype",
481            PsValue::Operator(_) => b"operatortype",
482            PsValue::File(_) => b"filetype",
483            PsValue::Save(_) => b"savetype",
484            PsValue::FontID(_) => b"fonttype",
485            PsValue::Gstate(_) => b"gstatetype",
486            _ => b"nulltype", // internal types
487        }
488    }
489
490    // --- Numeric extraction ---
491
492    /// Extract as `f64` (works for both Int and Real).
493    pub fn as_f64(&self) -> Option<f64> {
494        match self.value {
495            PsValue::Int(v) => Some(v as f64),
496            PsValue::Real(v) => Some(v),
497            _ => None,
498        }
499    }
500
501    /// Extract as `i32` (Int only).
502    pub fn as_i32(&self) -> Option<i32> {
503        match self.value {
504            PsValue::Int(v) => Some(v),
505            _ => None,
506        }
507    }
508}
509
510#[cfg(test)]
511mod tests {
512    use super::*;
513
514    #[test]
515    fn test_obj_flags_basic() {
516        let f = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, false, true);
517        assert_eq!(f.access(), ObjFlags::ACCESS_UNLIMITED);
518        assert!(f.is_executable());
519        assert!(!f.is_global());
520        assert!(f.is_composite());
521    }
522
523    #[test]
524    fn test_obj_flags_set_literal() {
525        let mut f = ObjFlags::executable();
526        assert!(f.is_executable());
527        f.set_literal();
528        assert!(f.is_literal());
529    }
530
531    #[test]
532    fn test_ps_object_int() {
533        let obj = PsObject::int(42);
534        assert!(obj.is_int());
535        assert!(obj.is_numeric());
536        assert!(!obj.is_real());
537        assert_eq!(obj.as_i32(), Some(42));
538        assert_eq!(obj.as_f64(), Some(42.0));
539        assert_eq!(obj.type_name(), b"integertype");
540    }
541
542    #[test]
543    fn test_ps_object_real() {
544        let obj = PsObject::real(2.5);
545        assert!(obj.is_real());
546        assert!(obj.is_numeric());
547        assert_eq!(obj.as_f64(), Some(2.5));
548        assert_eq!(obj.as_i32(), None);
549        assert_eq!(obj.type_name(), b"realtype");
550    }
551
552    #[test]
553    fn test_ps_object_copy_semantics() {
554        let a = PsObject::int(10);
555        let b = a; // Copy
556        assert_eq!(a.as_i32(), Some(10));
557        assert_eq!(b.as_i32(), Some(10));
558    }
559
560    #[test]
561    fn test_ps_object_procedure() {
562        let obj = PsObject::procedure(EntityId(0), 3);
563        assert!(obj.flags.is_executable());
564        assert!(obj.flags.is_composite());
565        assert!(obj.is_array_type());
566    }
567}