Skip to main content

stet_engine/
eval.rs

1// stet - A PostScript Interpreter
2// Copyright (c) 2026 Scott Bowman
3// SPDX-License-Identifier: Apache-2.0 OR MIT
4
5//! Core execution engine — the eval loop that drives PostScript execution.
6
7use stet_core::context::Context;
8use stet_core::dict::DictKey;
9use stet_core::error::PsError;
10use stet_core::object::{EntityId, ObjFlags, PsObject, PsValue};
11use stet_core::tokenizer::{Token, Tokenizer, stream_next_token};
12
13/// Main eval loop: pop objects from the execution stack and execute them.
14///
15/// All PostScript errors are routed through `dispatch_error` → errordict
16/// so they produce standard PS error output. Only `Quit` and un-caught
17/// `Stop` propagate to the caller.
18pub fn eval(ctx: &mut Context) -> Result<(), PsError> {
19    while let Some(mut obj) = ctx.e_stack.try_pop() {
20        if let Some(ref flag) = ctx.interrupt_flag
21            && flag.load(std::sync::atomic::Ordering::Relaxed)
22        {
23            return Err(PsError::Quit);
24        }
25        // Deferred objects (nested procs from exec_procedure) → push to operand stack
26        // Clear the deferred flag so the executable flag is preserved.
27        if obj.flags.is_deferred() {
28            obj.flags.clear_deferred();
29            if let Err(e) = ctx.o_stack.push(obj) {
30                dispatch_error(ctx, &e)?;
31            }
32            continue;
33        }
34
35        // Literal objects (except internal markers) → push to operand stack
36        if obj.flags.is_literal()
37            && !matches!(
38                obj.value,
39                PsValue::Stopped
40                    | PsValue::Loop(_)
41                    | PsValue::HardReturn
42                    | PsValue::DictEnd(_)
43                    | PsValue::ExecArray { .. }
44            )
45        {
46            if let Err(e) = ctx.o_stack.push(obj) {
47                dispatch_error(ctx, &e)?;
48            }
49            continue;
50        }
51
52        match eval_one(ctx, obj) {
53            Ok(()) => {}
54            Err(PsError::Quit) => return Ok(()),
55            Err(PsError::Stop) => {
56                if unwind_to_stopped(ctx).is_ok() {
57                    if let Err(e) = ctx.o_stack.push(PsObject::bool(true)) {
58                        dispatch_error(ctx, &e)?;
59                    }
60                } else {
61                    return Err(PsError::Stop);
62                }
63            }
64            Err(PsError::Exit) => {
65                if let Err(e) = unwind_to_loop(ctx) {
66                    dispatch_error(ctx, &e)?;
67                }
68            }
69            Err(e) => {
70                dispatch_error(ctx, &e)?;
71            }
72        }
73    }
74    Ok(())
75}
76
77/// Synchronously execute a PostScript procedure and return.
78///
79/// Pushes the procedure onto the e_stack and runs the eval loop until the
80/// e_stack returns to its original depth. Used by operators that need to
81/// call PS procedures as callbacks (filter/image data sources, tint
82/// transforms, BuildChar, etc.).
83pub fn exec_sync(ctx: &mut Context, proc_obj: PsObject) -> Result<(), PsError> {
84    let base_depth = ctx.e_stack.len();
85    ctx.e_stack.push(proc_obj)?;
86
87    while ctx.e_stack.len() > base_depth {
88        if let Some(ref flag) = ctx.interrupt_flag
89            && flag.load(std::sync::atomic::Ordering::Relaxed)
90        {
91            return Err(PsError::Quit);
92        }
93        let Some(mut obj) = ctx.e_stack.try_pop() else {
94            break;
95        };
96
97        if obj.flags.is_deferred() {
98            obj.flags.clear_deferred();
99            ctx.o_stack.push(obj)?;
100            continue;
101        }
102
103        if obj.flags.is_literal()
104            && !matches!(
105                obj.value,
106                PsValue::Stopped
107                    | PsValue::Loop(_)
108                    | PsValue::HardReturn
109                    | PsValue::DictEnd(_)
110                    | PsValue::ExecArray { .. }
111            )
112        {
113            ctx.o_stack.push(obj)?;
114            continue;
115        }
116
117        match eval_one(ctx, obj) {
118            Ok(()) => {}
119            Err(PsError::Quit) => return Ok(()),
120            Err(PsError::Stop) => {
121                // Only unwind within our scope
122                if unwind_to_stopped_bounded(ctx, base_depth).is_ok() {
123                    ctx.o_stack.push(PsObject::bool(true))?;
124                } else {
125                    return Err(PsError::Stop);
126                }
127            }
128            Err(PsError::Exit) => {
129                if unwind_to_loop_bounded(ctx, base_depth).is_err() {
130                    return Err(PsError::Exit);
131                }
132            }
133            Err(e) => {
134                dispatch_error(ctx, &e)?;
135            }
136        }
137    }
138
139    Ok(())
140}
141
142/// Unwind to nearest `Stopped` marker, but don't go below `min_depth`.
143fn unwind_to_stopped_bounded(ctx: &mut Context, min_depth: usize) -> Result<(), PsError> {
144    while ctx.e_stack.len() > min_depth {
145        if let Some(obj) = ctx.e_stack.try_pop() {
146            match obj.value {
147                PsValue::Stopped => return Ok(()),
148                PsValue::DictEnd(expected) => {
149                    pop_dict_end(ctx, expected);
150                }
151                _ => {}
152            }
153        }
154    }
155    Err(PsError::Stop)
156}
157
158/// Unwind to nearest `Loop` marker, but don't go below `min_depth`.
159fn unwind_to_loop_bounded(ctx: &mut Context, min_depth: usize) -> Result<(), PsError> {
160    while ctx.e_stack.len() > min_depth {
161        if let Some(obj) = ctx.e_stack.try_pop() {
162            match obj.value {
163                PsValue::Loop(_) => return Ok(()),
164                PsValue::Stopped => {
165                    ctx.e_stack.push(obj)?;
166                    return Err(PsError::InvalidExit);
167                }
168                PsValue::DictEnd(expected) => {
169                    pop_dict_end(ctx, expected);
170                }
171                _ => {}
172            }
173        }
174    }
175    Err(PsError::InvalidExit)
176}
177
178/// Process a single object from the execution stack.
179///
180/// Returns errors that the caller routes through `dispatch_error`.
181fn eval_one(ctx: &mut Context, obj: PsObject) -> Result<(), PsError> {
182    match obj.value {
183        // Simple types always push (even if executable flag is set)
184        PsValue::Int(_)
185        | PsValue::Real(_)
186        | PsValue::Bool(_)
187        | PsValue::Null
188        | PsValue::Mark
189        | PsValue::DictMark => {
190            ctx.o_stack.push(obj)?;
191        }
192
193        // Operators → dispatch (current_operator set lazily on error)
194        PsValue::Operator(opcode) => {
195            let func = ctx.operators[opcode.0 as usize].func;
196            if let Err(e) = func(ctx) {
197                ctx.current_operator = Some(ctx.operators[opcode.0 as usize].name);
198                return Err(e);
199            }
200        }
201
202        // Executable names → dictionary lookup
203        PsValue::Name(name_id) => {
204            let key = DictKey::Name(name_id);
205            match ctx.dict_load(&key) {
206                Some(val) => {
207                    if val.flags.is_executable() {
208                        ctx.e_stack.push(val)?;
209                    } else {
210                        ctx.o_stack.push(val)?;
211                    }
212                }
213                None => {
214                    ctx.current_operator = Some(name_id);
215                    return Err(PsError::Undefined);
216                }
217            }
218        }
219
220        // Executable arrays (procedures) → push elements in reverse
221        PsValue::Array { entity, start, len } => {
222            exec_procedure(ctx, entity, start, len)?;
223        }
224
225        // Executable strings → tokenize one token, advance start/len in place.
226        // SAFETY: String store data is not modified or reallocated during
227        // tokenization — we use a raw pointer to break the borrow conflict
228        // with &mut Context needed by scan_token_from_bytes.
229        PsValue::String { entity, start, len } => {
230            let bytes = ctx.strings.get(entity, start, len);
231            let (ptr, byte_len) = (bytes.as_ptr(), bytes.len());
232            let bytes = unsafe { std::slice::from_raw_parts(ptr, byte_len) };
233            if let Some((tok_obj, consumed, is_immediate, auto_exec)) =
234                scan_token_from_bytes(ctx, bytes)?
235            {
236                let newlines = count_newlines(&bytes[..consumed]);
237                ctx.current_source_line += newlines;
238
239                // Push continuation with advanced start and reduced len
240                let remaining = len - consumed as u32;
241                if remaining > 0 {
242                    ctx.e_stack.push(PsObject {
243                        value: PsValue::String {
244                            entity,
245                            start: start + consumed as u32,
246                            len: remaining,
247                        },
248                        flags: ObjFlags::executable_composite(),
249                    })?;
250                }
251
252                if auto_exec {
253                    ctx.e_stack.push(tok_obj)?;
254                } else {
255                    dispatch_scanned_token(ctx, tok_obj, is_immediate)?;
256                }
257            }
258        }
259
260        // Procedure cursor → tight inner loop over consecutive elements.
261        //
262        // Instead of processing one element per eval loop iteration (which
263        // requires pushing/popping the cursor on e_stack each time), we loop
264        // through elements inline. We only yield back to the eval loop when
265        // an operator pushes to e_stack (control flow like `if`, `exec`,
266        // loops) or when we encounter a non-operator executable value.
267        PsValue::ExecArray {
268            entity,
269            start,
270            len,
271            pos,
272        } => {
273            let mut cur_pos = pos;
274            let ea_flags = obj.flags;
275
276            // Dispatch an operator inline. Returns true if the eval loop
277            // should take over (operator pushed to e_stack). On error,
278            // pushes continuation cursor so error recovery can resume.
279            //
280            // current_operator is set lazily — only on error — to avoid
281            // 2 writes per operator call in the hot path.
282            macro_rules! dispatch_op {
283                ($opcode:expr) => {{
284                    let e_depth = ctx.e_stack.len();
285                    let func = ctx.operators[$opcode.0 as usize].func;
286                    let result = func(ctx);
287                    match result {
288                        Ok(()) => {
289                            if ctx.e_stack.len() > e_depth {
290                                // Operator pushed to e_stack (if/exec/loop/etc).
291                                // Insert our continuation below what was pushed.
292                                if cur_pos < len {
293                                    ctx.e_stack.insert_at(
294                                        e_depth,
295                                        PsObject {
296                                            value: PsValue::ExecArray {
297                                                entity,
298                                                start,
299                                                len,
300                                                pos: cur_pos,
301                                            },
302                                            flags: ea_flags,
303                                        },
304                                    )?;
305                                }
306                                true // yield to eval loop
307                            } else {
308                                false // continue inner loop
309                            }
310                        }
311                        Err(e) => {
312                            // Set current_operator lazily on error
313                            ctx.current_operator = Some(ctx.operators[$opcode.0 as usize].name);
314                            if cur_pos < len {
315                                ctx.e_stack.push(PsObject {
316                                    value: PsValue::ExecArray {
317                                        entity,
318                                        start,
319                                        len,
320                                        pos: cur_pos,
321                                    },
322                                    flags: ea_flags,
323                                })?;
324                            }
325                            return Err(e);
326                        }
327                    }
328                }};
329            }
330
331            'ea_loop: loop {
332                let elem = ctx.arrays.get_element(entity, start + cur_pos);
333                cur_pos += 1;
334
335                match elem.value {
336                    PsValue::Operator(opcode) => {
337                        if dispatch_op!(opcode) {
338                            break 'ea_loop;
339                        }
340                    }
341
342                    PsValue::Name(name_id) if elem.flags.is_executable() => {
343                        // Inline name cache check — avoids DictKey construction
344                        // and dict_load function call overhead on cache hits.
345                        let idx = name_id.0 as usize;
346                        let val = if idx < ctx.name_resolve_cache.len() {
347                            let (ver, cached) = ctx.name_resolve_cache[idx];
348                            if ver == ctx.dict_version {
349                                cached
350                            } else {
351                                match ctx.dict_load(&DictKey::Name(name_id)) {
352                                    Some(v) => v,
353                                    None => {
354                                        ctx.current_operator = Some(name_id);
355                                        if cur_pos < len {
356                                            ctx.e_stack.push(PsObject {
357                                                value: PsValue::ExecArray {
358                                                    entity,
359                                                    start,
360                                                    len,
361                                                    pos: cur_pos,
362                                                },
363                                                flags: ea_flags,
364                                            })?;
365                                        }
366                                        return Err(PsError::Undefined);
367                                    }
368                                }
369                            }
370                        } else {
371                            match ctx.dict_load(&DictKey::Name(name_id)) {
372                                Some(v) => v,
373                                None => {
374                                    ctx.current_operator = Some(name_id);
375                                    if cur_pos < len {
376                                        ctx.e_stack.push(PsObject {
377                                            value: PsValue::ExecArray {
378                                                entity,
379                                                start,
380                                                len,
381                                                pos: cur_pos,
382                                            },
383                                            flags: ea_flags,
384                                        })?;
385                                    }
386                                    return Err(PsError::Undefined);
387                                }
388                            }
389                        };
390
391                        match val.value {
392                            PsValue::Operator(opcode) => {
393                                if dispatch_op!(opcode) {
394                                    break 'ea_loop;
395                                }
396                            }
397                            _ => {
398                                // Non-operator: push cursor and dispatch value
399                                if cur_pos < len {
400                                    ctx.e_stack.push(PsObject {
401                                        value: PsValue::ExecArray {
402                                            entity,
403                                            start,
404                                            len,
405                                            pos: cur_pos,
406                                        },
407                                        flags: ea_flags,
408                                    })?;
409                                }
410                                if val.flags.is_executable() {
411                                    ctx.e_stack.push(val)?;
412                                } else {
413                                    ctx.o_stack.push(val)?;
414                                }
415                                break 'ea_loop;
416                            }
417                        }
418                    }
419
420                    _ => {
421                        if elem.is_array_type() && elem.flags.is_executable() {
422                            // Nested procedure body → o_stack (for if/exec to pick up)
423                            ctx.o_stack.push(elem)?;
424                        } else if matches!(
425                            elem.value,
426                            PsValue::Int(_)
427                                | PsValue::Real(_)
428                                | PsValue::Bool(_)
429                                | PsValue::Null
430                                | PsValue::Mark
431                                | PsValue::DictMark
432                        ) || elem.flags.is_literal()
433                        {
434                            // Literals and simple values → o_stack directly
435                            ctx.o_stack.push(elem)?;
436                        } else {
437                            // Rare: executable non-name/non-operator → e_stack, yield
438                            if cur_pos < len {
439                                ctx.e_stack.push(PsObject {
440                                    value: PsValue::ExecArray {
441                                        entity,
442                                        start,
443                                        len,
444                                        pos: cur_pos,
445                                    },
446                                    flags: ea_flags,
447                                })?;
448                            }
449                            ctx.e_stack.push(elem)?;
450                            break 'ea_loop;
451                        }
452                    }
453                }
454
455                if cur_pos >= len {
456                    break;
457                }
458            }
459        }
460
461        // Executable file → tokenize one token from FileStore
462        PsValue::File(file_entity) => {
463            // Flush deferred newlines from the previous token so `line`
464            // reports the line this token is on, not the previous one.
465            ctx.files.flush_pending_newlines(file_entity);
466
467            // Fast path: StringSource files have all data in memory.
468            // We grab a raw pointer to avoid copying the remaining bytes on
469            // every token read (which was an O(n^2) bottleneck for large files).
470            // SAFETY: The StringSource data Vec is not modified or reallocated
471            // during tokenization — only `pos` is advanced afterward.
472            let remaining = ctx.files.get_remaining_bytes(file_entity);
473            let (ptr, len) = (remaining.as_ptr(), remaining.len());
474            if len > 0 {
475                let remaining = unsafe { std::slice::from_raw_parts(ptr, len) };
476                if let Some((tok_obj, consumed, is_immediate, auto_exec)) =
477                    scan_token_from_bytes(ctx, remaining)?
478                {
479                    let newlines = count_newlines(&remaining[..consumed]);
480                    ctx.current_source_line += newlines;
481                    ctx.files.add_pending_newlines(file_entity, newlines);
482                    ctx.files.advance_position(file_entity, consumed);
483                    if consumed < remaining.len() {
484                        ctx.e_stack.push(obj)?;
485                    }
486                    if auto_exec {
487                        ctx.e_stack.push(tok_obj)?;
488                    } else {
489                        dispatch_scanned_token(ctx, tok_obj, is_immediate)?;
490                    }
491                }
492            } else if ctx.files.is_readable(file_entity) {
493                // Streaming path: filter/real files — byte-at-a-time tokenization
494                if let Some((token, newlines)) = stream_next_token(&mut ctx.files, file_entity)? {
495                    ctx.current_source_line += newlines;
496                    ctx.files.add_pending_newlines(file_entity, newlines);
497                    let is_immediate = matches!(token, Token::ImmediateName(_));
498                    let (tok_obj, auto_exec) = if let Token::BinaryTokenByte(tag) = token {
499                        let result =
500                            stet_core::binary_token::parse_from_stream(ctx, tag, file_entity)?;
501                        match result {
502                            stet_core::binary_token::BinaryTokenResult::Single(o) => (o, false),
503                            stet_core::binary_token::BinaryTokenResult::Sequence(o) => (o, true),
504                        }
505                    } else if matches!(token, Token::ProcBegin) {
506                        (stream_parse_procedure(ctx, file_entity)?, false)
507                    } else {
508                        (token_to_object(ctx, token)?, false)
509                    };
510                    if ctx.files.is_readable(file_entity) {
511                        ctx.e_stack.push(obj)?;
512                    }
513                    if auto_exec {
514                        ctx.e_stack.push(tok_obj)?;
515                    } else {
516                        dispatch_scanned_token(ctx, tok_obj, is_immediate)?;
517                    }
518                }
519            }
520        }
521
522        // Stopped marker → push false (normal completion, no stop)
523        PsValue::Stopped => {
524            ctx.o_stack.push(PsObject::bool(false))?;
525        }
526
527        // Loop state → advance loop
528        PsValue::Loop(loop_entity) => {
529            advance_loop(ctx, loop_entity)?;
530        }
531
532        // HardReturn → just consume (exit current procedure level)
533        PsValue::HardReturn => {}
534
535        // DictEnd → conditionally pop the dict stack (resource operator cleanup)
536        PsValue::DictEnd(expected) => {
537            pop_dict_end(ctx, expected);
538        }
539
540        // Everything else → push to operand stack
541        _ => {
542            ctx.o_stack.push(obj)?;
543        }
544    }
545    Ok(())
546}
547
548/// Push a procedure cursor onto the execution stack.
549///
550/// Instead of expanding all procedure elements onto the e_stack, we push a
551/// single `ExecArray` cursor that the eval loop advances one element at a time.
552/// Each procedure occupies exactly one exec-stack item.
553fn exec_procedure(
554    ctx: &mut Context,
555    entity: EntityId,
556    start: u32,
557    len: u32,
558) -> Result<(), PsError> {
559    if len == 0 {
560        return Ok(());
561    }
562    ctx.e_stack.push(PsObject {
563        value: PsValue::ExecArray {
564            entity,
565            start,
566            len,
567            pos: 0,
568        },
569        flags: ObjFlags::executable_composite(),
570    })?;
571    Ok(())
572}
573
574/// Scan one token from a byte slice, handling procedures and immediate names.
575///
576/// Returns `(token_object, bytes_consumed, is_immediate, auto_exec)` or `None` at EOF.
577/// `auto_exec` is true for BOS sequences which should be pushed to e_stack.
578fn scan_token_from_bytes(
579    ctx: &mut Context,
580    bytes: &[u8],
581) -> Result<Option<(PsObject, usize, bool, bool)>, PsError> {
582    let mut tokenizer = Tokenizer::new(bytes);
583    match tokenizer.next_token()? {
584        Some(Token::BinaryTokenByte(tag)) => {
585            let pos = tokenizer.position();
586            let (result, consumed) =
587                stet_core::binary_token::parse_from_slice(ctx, tag, &bytes[pos..])?;
588            let total = pos + consumed;
589            match result {
590                stet_core::binary_token::BinaryTokenResult::Single(obj) => {
591                    Ok(Some((obj, total, false, false)))
592                }
593                stet_core::binary_token::BinaryTokenResult::Sequence(obj) => {
594                    Ok(Some((obj, total, false, true)))
595                }
596            }
597        }
598        Some(token) => {
599            let is_immediate = matches!(token, Token::ImmediateName(_));
600            // Numbers and executable names: consume one trailing whitespace (PLRM)
601            let eats_whitespace =
602                matches!(token, Token::Int(_) | Token::Real(_) | Token::Name(_, _));
603            let tok_obj = if matches!(token, Token::ProcBegin) {
604                parse_procedure(ctx, &mut tokenizer)?
605            } else {
606                token_to_object(ctx, token)?
607            };
608            let mut consumed = tokenizer.position();
609            if eats_whitespace && consumed < bytes.len() && is_ps_whitespace(bytes[consumed]) {
610                consumed += 1;
611            }
612            Ok(Some((tok_obj, consumed, is_immediate, false)))
613        }
614        None => Ok(None),
615    }
616}
617
618/// PostScript whitespace check for trailing-whitespace consumption.
619fn is_ps_whitespace(b: u8) -> bool {
620    matches!(b, b'\0' | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
621}
622
623/// Dispatch a freshly-scanned token to the appropriate stack.
624///
625/// Executable names → e_stack (for dict lookup), everything else → o_stack.
626/// Immediate names (`//name`) skip execution since they were already resolved.
627fn dispatch_scanned_token(
628    ctx: &mut Context,
629    tok_obj: PsObject,
630    is_immediate: bool,
631) -> Result<(), PsError> {
632    if matches!(tok_obj.value, PsValue::Name(_)) && tok_obj.flags.is_executable() && !is_immediate {
633        ctx.e_stack.push(tok_obj)?;
634    } else {
635        ctx.o_stack.push(tok_obj)?;
636    }
637    Ok(())
638}
639
640/// Parse a `{ ... }` procedure body from a streaming file source.
641///
642/// Called when the streaming tokenizer returns `ProcBegin`. Reads tokens
643/// from the file byte-by-byte until the matching `}`.
644fn stream_parse_procedure(ctx: &mut Context, file_entity: EntityId) -> Result<PsObject, PsError> {
645    let mut elements = Vec::new();
646
647    loop {
648        match stream_next_token(&mut ctx.files, file_entity)? {
649            None => return Err(PsError::SyntaxError), // unterminated
650            Some((Token::ProcEnd, _)) => break,
651            Some((Token::ProcBegin, _)) => {
652                let nested = stream_parse_procedure(ctx, file_entity)?;
653                elements.push(nested);
654            }
655            Some((Token::BinaryTokenByte(tag), _)) => {
656                let result = stet_core::binary_token::parse_from_stream(ctx, tag, file_entity)?;
657                let obj = match result {
658                    stet_core::binary_token::BinaryTokenResult::Single(o) => o,
659                    stet_core::binary_token::BinaryTokenResult::Sequence(o) => o,
660                };
661                elements.push(obj);
662            }
663            Some((token, _)) => {
664                let obj = token_to_object(ctx, token)?;
665                elements.push(obj);
666            }
667        }
668    }
669
670    let len = elements.len();
671    let save_level = ctx.save_stack.current_level();
672    let global = ctx.vm_alloc_mode;
673    let created = ctx.save_stack.last_save_id();
674    let entity = ctx.arrays.allocate_with(len, save_level, global, created);
675    let dest = ctx.arrays.get_mut(entity, 0, len as u32);
676    dest.copy_from_slice(&elements);
677
678    let mut obj = PsObject::procedure(entity, len as u32);
679    if global {
680        obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, true, true);
681    }
682    Ok(obj)
683}
684
685/// Count newline characters in a byte slice.
686/// Handles CR, LF, and CR-LF (treated as one newline).
687fn count_newlines(bytes: &[u8]) -> u32 {
688    let mut count = 0u32;
689    let mut i = 0;
690    while i < bytes.len() {
691        match bytes[i] {
692            b'\r' => {
693                count += 1;
694                // CR-LF counts as one newline
695                if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
696                    i += 1;
697                }
698            }
699            b'\n' | b'\x0c' => {
700                count += 1;
701            }
702            _ => {}
703        }
704        i += 1;
705    }
706    count
707}
708
709/// Advance a loop iteration (for, repeat, loop, forall).
710fn advance_loop(ctx: &mut Context, loop_entity: EntityId) -> Result<(), PsError> {
711    use stet_core::context::LoopType;
712
713    let loop_state = ctx.get_loop(loop_entity);
714    let loop_type = match loop_state.loop_type {
715        LoopType::For => 0,
716        LoopType::Repeat => 1,
717        LoopType::Loop => 2,
718        LoopType::Forall => 3,
719        LoopType::PathForall => 4,
720    };
721    let proc_entity = loop_state.proc_entity;
722    let proc_start = loop_state.proc_start;
723    let proc_len = loop_state.proc_len;
724
725    match loop_type {
726        0 => {
727            // For loop
728            let counter = ctx.get_loop(loop_entity).counter;
729            let increment = ctx.get_loop(loop_entity).increment;
730            let limit = ctx.get_loop(loop_entity).limit;
731            let use_int = ctx.get_loop(loop_entity).use_int;
732
733            let done = if increment > 0.0 {
734                counter > limit
735            } else {
736                counter < limit
737            };
738
739            if done {
740                return Ok(());
741            }
742
743            // Push counter value
744            if use_int {
745                ctx.o_stack.push(PsObject::int(counter as i32))?;
746            } else {
747                ctx.o_stack.push(PsObject::real(counter))?;
748            }
749
750            // Update counter for next iteration
751            let new_counter = counter + increment;
752            ctx.get_loop_mut(loop_entity).counter = new_counter;
753
754            // Push loop marker back, then procedure
755            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
756            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
757        }
758        1 => {
759            // Repeat loop
760            let counter = ctx.get_loop(loop_entity).counter;
761            if counter <= 0.0 {
762                return Ok(());
763            }
764            ctx.get_loop_mut(loop_entity).counter = counter - 1.0;
765            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
766            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
767        }
768        2 => {
769            // Infinite loop
770            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
771            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
772        }
773        3 => {
774            // Forall
775            advance_forall(ctx, loop_entity, proc_entity, proc_start, proc_len)?;
776        }
777        4 => {
778            // PathForall
779            advance_pathforall(ctx, loop_entity)?;
780        }
781        _ => unreachable!(),
782    }
783
784    Ok(())
785}
786
787/// Advance a forall iteration.
788fn advance_forall(
789    ctx: &mut Context,
790    loop_entity: EntityId,
791    proc_entity: EntityId,
792    proc_start: u32,
793    proc_len: u32,
794) -> Result<(), PsError> {
795    let source = ctx.get_loop(loop_entity).source;
796    let index = ctx.get_loop(loop_entity).index;
797
798    match source.value {
799        PsValue::Array { entity, start, len } | PsValue::PackedArray { entity, start, len } => {
800            if index >= len {
801                return Ok(());
802            }
803            let elem = ctx.arrays.get_element(entity, start + index);
804            ctx.o_stack.push(elem)?;
805            ctx.get_loop_mut(loop_entity).index = index + 1;
806            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
807            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
808        }
809        PsValue::String { entity, start, len } => {
810            if index >= len {
811                return Ok(());
812            }
813            let byte = ctx.strings.get_byte(entity, start + index);
814            ctx.o_stack.push(PsObject::int(byte as i32))?;
815            ctx.get_loop_mut(loop_entity).index = index + 1;
816            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
817            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
818        }
819        PsValue::Dict(dict_entity) => {
820            // Keys were snapshotted at forall creation time.
821            // SAFETY: dict_keys is always Some for dict forall loops.
822            let keys = ctx.get_loop(loop_entity).dict_keys.as_ref().unwrap();
823            if (index as usize) >= keys.len() {
824                return Ok(());
825            }
826            let key = keys[index as usize].clone();
827            let val = ctx.dicts.get(dict_entity, &key).unwrap_or(PsObject::null());
828
829            // Push key and value
830            let key_obj = dict_key_to_object(ctx, &key);
831            ctx.o_stack.push(key_obj)?;
832            ctx.o_stack.push(val)?;
833
834            ctx.get_loop_mut(loop_entity).index = index + 1;
835            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
836            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
837        }
838        _ => return Err(PsError::TypeCheck),
839    }
840
841    Ok(())
842}
843
844/// Advance a pathforall iteration: process one path segment per call.
845fn advance_pathforall(ctx: &mut Context, loop_entity: EntityId) -> Result<(), PsError> {
846    use stet_core::geometry::PathSegment;
847
848    let index = ctx.get_loop(loop_entity).index as usize;
849
850    // Check if we've exhausted all segments
851    let seg_len = ctx
852        .get_loop(loop_entity)
853        .path_segments
854        .as_ref()
855        .map_or(0, |s| s.len());
856    if index >= seg_len {
857        return Ok(());
858    }
859
860    // Extract segment data, ictm, and proc for this iteration
861    let loop_state = ctx.get_loop(loop_entity);
862    let seg = loop_state.path_segments.as_ref().unwrap()[index].clone();
863    let ictm = loop_state.path_ictm.unwrap();
864    let procs = loop_state.path_procs.unwrap();
865
866    // Determine which proc to call and push the arguments
867    let proc = match seg {
868        PathSegment::MoveTo(dx, dy) => {
869            let (ux, uy) = ictm.transform_point(dx, dy);
870            ctx.o_stack.push(PsObject::real(ux))?;
871            ctx.o_stack.push(PsObject::real(uy))?;
872            procs[0] // move_proc
873        }
874        PathSegment::LineTo(dx, dy) => {
875            let (ux, uy) = ictm.transform_point(dx, dy);
876            ctx.o_stack.push(PsObject::real(ux))?;
877            ctx.o_stack.push(PsObject::real(uy))?;
878            procs[1] // line_proc
879        }
880        PathSegment::CurveTo {
881            x1,
882            y1,
883            x2,
884            y2,
885            x3,
886            y3,
887        } => {
888            let (ux1, uy1) = ictm.transform_point(x1, y1);
889            let (ux2, uy2) = ictm.transform_point(x2, y2);
890            let (ux3, uy3) = ictm.transform_point(x3, y3);
891            ctx.o_stack.push(PsObject::real(ux1))?;
892            ctx.o_stack.push(PsObject::real(uy1))?;
893            ctx.o_stack.push(PsObject::real(ux2))?;
894            ctx.o_stack.push(PsObject::real(uy2))?;
895            ctx.o_stack.push(PsObject::real(ux3))?;
896            ctx.o_stack.push(PsObject::real(uy3))?;
897            procs[2] // curve_proc
898        }
899        PathSegment::ClosePath => {
900            procs[3] // close_proc
901        }
902    };
903
904    // Advance index for next iteration
905    ctx.get_loop_mut(loop_entity).index = (index + 1) as u32;
906
907    // Push loop marker back, then the callback procedure
908    ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
909    let (proc_entity, proc_start, proc_len) = match proc.value {
910        PsValue::Array { entity, start, len } => (entity, start, len),
911        _ => return Err(PsError::TypeCheck),
912    };
913    exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
914
915    Ok(())
916}
917
918/// Convert a DictKey back to a PsObject for forall iteration.
919fn dict_key_to_object(ctx: &mut Context, key: &DictKey) -> PsObject {
920    match key {
921        DictKey::Name(id) => PsObject::name_lit(*id),
922        DictKey::Int(v) => PsObject::int(*v),
923        DictKey::Real(bits) => PsObject::real(f64::from_bits(*bits)),
924        DictKey::Bool(v) => PsObject::bool(*v),
925        DictKey::String(bytes) => {
926            let entity = ctx.strings.allocate_from(bytes);
927            PsObject::string(entity, bytes.len() as u32)
928        }
929        DictKey::Operator(op) => {
930            use stet_core::object::OpCode;
931            PsObject::operator(OpCode(*op))
932        }
933        DictKey::Identity(eid, _start, len) => {
934            // Return as array (best approximation for identity keys)
935            PsObject::array(EntityId(*eid), *len)
936        }
937    }
938}
939
940/// Dispatch a PostScript error via errordict.
941///
942/// PLRM error dispatch:
943/// 1. Look up the error name in errordict → get handler procedure
944/// 2. Handler populates `$error` dict and calls `stop`
945/// 3. If no handler found, or if already in error handler, fall back to eprintln
946fn dispatch_error(ctx: &mut Context, error: &PsError) -> Result<(), PsError> {
947    // Guard against infinite recursion
948    if ctx.in_error_handler {
949        use std::io::Write;
950        let _ = writeln!(ctx.stdout, "Error (in handler): {}", error);
951        return Ok(());
952    }
953
954    let error_name = error.to_string();
955    let error_name_id = ctx.names.intern(error_name.as_bytes());
956    let error_key = DictKey::Name(error_name_id);
957
958    // Look up error handler in errordict
959    if let Some(handler) = ctx.dicts.get(ctx.errordict, &error_key)
960        && handler.flags.is_executable()
961    {
962        ctx.in_error_handler = true;
963
964        // Push offending command name on operand stack — the errordict handler
965        // pushes the error name, then `.error` expects `command errorname` on
966        // the stack. Use the current operator if available, otherwise the error
967        // name itself.
968        let cmd_name = ctx.current_operator.unwrap_or(error_name_id);
969        ctx.current_operator = None;
970        ctx.o_stack.push(PsObject::name_lit(cmd_name))?;
971
972        // Push handler on e_stack for natural execution by the eval loop.
973        // The handler (e.g. `{ /undefined //.error exec }`) will populate
974        // $error and call `stop`, which propagates naturally to be caught by
975        // any enclosing `stopped` context.
976        ctx.e_stack.push(handler)?;
977
978        ctx.in_error_handler = false;
979        return Ok(());
980    }
981
982    // Fallback: no handler found, print to stdout (like stderr)
983    use std::io::Write;
984    let _ = writeln!(ctx.stdout, "Error: {}", error);
985    Ok(())
986}
987
988/// Unwind execution stack to the nearest `Stopped` marker.
989/// DictEnd markers encountered during unwinding conditionally pop the dict
990/// stack (only if the expected dict is still on top).
991fn unwind_to_stopped(ctx: &mut Context) -> Result<(), PsError> {
992    while let Some(obj) = ctx.e_stack.try_pop() {
993        match obj.value {
994            PsValue::Stopped => return Ok(()),
995            PsValue::DictEnd(expected) => {
996                pop_dict_end(ctx, expected);
997            }
998            _ => {}
999        }
1000    }
1001    // No stopped marker found — propagate
1002    Err(PsError::Stop)
1003}
1004
1005/// Unwind execution stack to the nearest `Loop` marker.
1006fn unwind_to_loop(ctx: &mut Context) -> Result<(), PsError> {
1007    while let Some(obj) = ctx.e_stack.try_pop() {
1008        match obj.value {
1009            PsValue::Loop(_) => return Ok(()),
1010            PsValue::Stopped => {
1011                // Don't pop past stopped — push it back and dispatch error
1012                ctx.e_stack.push(obj)?;
1013                return Err(PsError::InvalidExit);
1014            }
1015            PsValue::DictEnd(expected) => {
1016                pop_dict_end(ctx, expected);
1017            }
1018            _ => {}
1019        }
1020    }
1021    Err(PsError::InvalidExit)
1022}
1023
1024/// Conditionally pop the dict stack for a DictEnd marker.
1025/// Only pops if the top of d_stack is still the expected entity — the PS
1026/// procedure may have already called `end` to clean up (e.g., on error paths).
1027fn pop_dict_end(ctx: &mut Context, expected: EntityId) {
1028    if ctx.d_stack.last() == Some(&expected) {
1029        ctx.d_stack.pop();
1030        ctx.invalidate_name_cache();
1031    }
1032}
1033
1034/// Convert a token to a PsObject. Delegates to `Context::token_to_object`.
1035pub fn token_to_object(ctx: &mut Context, token: Token) -> Result<PsObject, PsError> {
1036    ctx.token_to_object(token)
1037}
1038
1039/// Tokenize a byte stream and execute it.
1040///
1041/// Creates a file-backed source in the FileStore and pushes it as an
1042/// executable `File` on the exec stack. The eval loop tokenizes and
1043/// executes one token at a time, ensuring `//name` immediate lookups
1044/// see earlier definitions and ALL errors route through errordict.
1045///
1046/// Using a File (not an executable string) means `currentfile` can find
1047/// the source — this is correct for top-level execution of PS files.
1048pub fn parse_and_exec(ctx: &mut Context, source: &[u8]) -> Result<(), PsError> {
1049    let file_entity = ctx.files.create_string_source(source.to_vec());
1050    ctx.e_stack.push(PsObject {
1051        value: PsValue::File(file_entity),
1052        flags: ObjFlags::executable_composite(),
1053    })?;
1054    eval(ctx)
1055}
1056
1057/// Execute PostScript source loaded from a named file.
1058///
1059/// Like `parse_and_exec`, but records the file path on the StringSource
1060/// entity so that `resolve_filename` can find the file's parent directory
1061/// when resolving relative paths in nested `run`/`file` calls.
1062pub fn parse_and_exec_file(ctx: &mut Context, source: &[u8], path: &str) -> Result<(), PsError> {
1063    let file_entity = ctx.files.create_string_source(source.to_vec());
1064    // Record the canonical path so resolve_filename can extract its directory.
1065    let canonical = std::path::Path::new(path)
1066        .canonicalize()
1067        .unwrap_or_else(|_| std::path::PathBuf::from(path));
1068    ctx.files
1069        .set_name(file_entity, canonical.to_string_lossy().to_string());
1070    ctx.e_stack.push(PsObject {
1071        value: PsValue::File(file_entity),
1072        flags: ObjFlags::executable_composite(),
1073    })?;
1074    eval(ctx)
1075}
1076
1077/// Parse a `{ ... }` procedure body (recursive for nested procedures).
1078fn parse_procedure(ctx: &mut Context, tokenizer: &mut Tokenizer) -> Result<PsObject, PsError> {
1079    let mut elements = Vec::new();
1080
1081    loop {
1082        match tokenizer.next_token()? {
1083            Some(Token::ProcEnd) => break,
1084            Some(Token::ProcBegin) => {
1085                let nested = parse_procedure(ctx, tokenizer)?;
1086                elements.push(nested);
1087            }
1088            Some(Token::BinaryTokenByte(tag)) => {
1089                let pos = tokenizer.position();
1090                // SAFETY: tokenizer borrows from a stable slice; we access the
1091                // underlying input via position.
1092                let input_bytes = tokenizer.remaining_from(pos);
1093                let (result, consumed) =
1094                    stet_core::binary_token::parse_from_slice(ctx, tag, input_bytes)?;
1095                tokenizer.advance(consumed);
1096                let obj = match result {
1097                    stet_core::binary_token::BinaryTokenResult::Single(o) => o,
1098                    stet_core::binary_token::BinaryTokenResult::Sequence(o) => o,
1099                };
1100                elements.push(obj);
1101            }
1102            Some(token) => {
1103                let obj = token_to_object(ctx, token)?;
1104                elements.push(obj);
1105            }
1106            None => return Err(PsError::SyntaxError), // unterminated procedure
1107        }
1108    }
1109
1110    let len = elements.len();
1111    let save_level = ctx.save_stack.current_level();
1112    let global = ctx.vm_alloc_mode;
1113    let created = ctx.save_stack.last_save_id();
1114    let entity = ctx.arrays.allocate_with(len, save_level, global, created);
1115    let dest = ctx.arrays.get_mut(entity, 0, len as u32);
1116    dest.copy_from_slice(&elements);
1117
1118    let mut obj = PsObject::procedure(entity, len as u32);
1119    if global {
1120        obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, true, true);
1121    }
1122    Ok(obj)
1123}
1124
1125#[cfg(test)]
1126mod tests {
1127    use super::*;
1128    use std::io::Write;
1129
1130    #[test]
1131    fn test_parse_procedure() {
1132        let mut ctx = Context::new();
1133        let mut tokenizer = Tokenizer::new(b"1 2 add }");
1134        let proc_obj = parse_procedure(&mut ctx, &mut tokenizer).unwrap();
1135        assert!(proc_obj.flags.is_executable());
1136        assert!(proc_obj.is_array_type());
1137        match proc_obj.value {
1138            PsValue::Array { len, .. } => assert_eq!(len, 3),
1139            _ => panic!("Expected array"),
1140        }
1141    }
1142
1143    #[test]
1144    fn test_nested_procedure() {
1145        let mut ctx = Context::new();
1146        let mut tokenizer = Tokenizer::new(b"{ 1 add } exec }");
1147        let proc_obj = parse_procedure(&mut ctx, &mut tokenizer).unwrap();
1148        match proc_obj.value {
1149            PsValue::Array { len, .. } => assert_eq!(len, 2), // { 1 add } and exec
1150            _ => panic!("Expected array"),
1151        }
1152    }
1153
1154    // --- Phase 2 integration tests (done-when criteria) ---
1155
1156    fn setup_ctx() -> (Context, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
1157        let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1158        let writer = buf.clone();
1159
1160        struct ArcWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
1161        impl Write for ArcWriter {
1162            fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
1163                self.0.lock().unwrap().extend_from_slice(data);
1164                Ok(data.len())
1165            }
1166            fn flush(&mut self) -> std::io::Result<()> {
1167                Ok(())
1168            }
1169        }
1170
1171        let mut ctx = Context::new_with_output(Box::new(ArcWriter(writer)));
1172        stet_ops::build_system_dict(&mut ctx);
1173        (ctx, buf)
1174    }
1175
1176    fn run_ps(source: &[u8]) -> String {
1177        let (mut ctx, buf) = setup_ctx();
1178        parse_and_exec(&mut ctx, source).ok();
1179        String::from_utf8(buf.lock().unwrap().clone()).unwrap()
1180    }
1181
1182    /// Done-when #2: save/restore reverts definitions.
1183    /// `save /s exch def /x 1 def s restore x =` — x should be undefined after restore
1184    #[test]
1185    fn test_save_restore_reverts_def() {
1186        let (mut ctx, buf) = setup_ctx();
1187        // Define x before save for baseline
1188        let result = parse_and_exec(&mut ctx, b"save /s exch def /x 1 def s restore");
1189        assert!(result.is_ok());
1190        // x should be undefined now — trying to use it should trigger an error
1191        let output = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
1192        // Verify x is not defined by checking that a lookup would fail
1193        let x_id = ctx.names.intern(b"x");
1194        let key = DictKey::Name(x_id);
1195        assert!(
1196            ctx.dict_load(&key).is_none(),
1197            "x should be undefined after restore"
1198        );
1199        drop(output);
1200    }
1201
1202    /// Done-when #3: save/restore reverts array mutations.
1203    /// Define array before save, mutate after save, restore reverts mutation.
1204    #[test]
1205    fn test_save_restore_reverts_array() {
1206        let output = run_ps(b"[1 2 3] /a exch def save /s exch def a 1 99 put s restore a 1 get =");
1207        assert_eq!(output.trim(), "2");
1208    }
1209
1210    /// Done-when #4: File round-trip
1211    #[test]
1212    fn test_file_round_trip() {
1213        // Use forward slashes even on Windows — PostScript string-literal
1214        // syntax treats `\` as an escape character, so a native Windows
1215        // path like `C:\Users\...` would be mangled inside `(...)`.
1216        // Rust's fs APIs on Windows accept `/` just fine.
1217        let path = std::env::temp_dir()
1218            .join("stet_phase2_file_test.txt")
1219            .to_string_lossy()
1220            .replace('\\', "/");
1221        let source = format!(
1222            "({}) (w) file /f exch def f (hello world) writestring f closefile \
1223             ({}) (r) file /f exch def f 11 string readstring pop print f closefile",
1224            path, path
1225        );
1226        let output = run_ps(source.as_bytes());
1227        assert_eq!(output, "hello world");
1228        std::fs::remove_file(&path).ok();
1229    }
1230
1231    /// Done-when #5: `{ 1 0 div } stopped { (caught\n) print } if` → prints "caught"
1232    #[test]
1233    fn test_stopped_catches_error() {
1234        let output = run_ps(b"{ 1 0 div } stopped { (caught\n) print } if");
1235        assert_eq!(output.trim(), "caught");
1236    }
1237
1238    /// Done-when #6: `true setglobal 3 array gcheck` → returns true
1239    #[test]
1240    fn test_setglobal_gcheck() {
1241        let output = run_ps(b"true setglobal 3 array gcheck =");
1242        assert_eq!(output.trim(), "true");
1243    }
1244
1245    /// Done-when #7: `vmstatus` returns three integers
1246    #[test]
1247    fn test_vmstatus() {
1248        let output = run_ps(b"vmstatus = = =");
1249        let lines: Vec<&str> = output.trim().lines().collect();
1250        assert_eq!(lines.len(), 3, "vmstatus should push 3 values");
1251        // All should be parseable as integers
1252        for line in &lines {
1253            assert!(
1254                line.trim().parse::<i32>().is_ok(),
1255                "Expected integer: {}",
1256                line
1257            );
1258        }
1259    }
1260
1261    /// Test that error dispatch populates $error and calls stop
1262    #[test]
1263    fn test_error_dispatch_stop() {
1264        let output = run_ps(b"{ 1 0 div } stopped { (error caught\n) print } if");
1265        assert!(output.contains("error caught"));
1266    }
1267
1268    /// Test nested save/restore
1269    #[test]
1270    fn test_nested_save_restore() {
1271        let output = run_ps(
1272            b"/x 10 def \
1273              save /s1 exch def \
1274              /x 20 def \
1275              save /s2 exch def \
1276              /x 30 def \
1277              x = \
1278              s2 restore \
1279              x = \
1280              s1 restore \
1281              x =",
1282        );
1283        let lines: Vec<&str> = output.trim().lines().collect();
1284        assert_eq!(lines, vec!["30", "20", "10"]);
1285    }
1286
1287    /// Test that global entities are not affected by restore.
1288    /// The array is allocated in global mode and defined before save,
1289    /// so the mutation survives restore.
1290    #[test]
1291    fn test_global_survives_restore() {
1292        let output = run_ps(
1293            b"true setglobal \
1294              3 array /ga exch def \
1295              false setglobal \
1296              save /s exch def \
1297              ga 0 42 put \
1298              s restore \
1299              ga 0 get =",
1300        );
1301        assert_eq!(output.trim(), "42");
1302    }
1303}