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                // The tokenizer only gets the FileStore, so a filter whose data
495                // source is a procedure has to be run before it starts.
496                ctx.pump_proc_sources(file_entity)?;
497                if let Some((token, newlines)) = stream_next_token(&mut ctx.files, file_entity)? {
498                    ctx.current_source_line += newlines;
499                    ctx.files.add_pending_newlines(file_entity, newlines);
500                    let is_immediate = matches!(token, Token::ImmediateName(_));
501                    let (tok_obj, auto_exec) = if let Token::BinaryTokenByte(tag) = token {
502                        let result =
503                            stet_core::binary_token::parse_from_stream(ctx, tag, file_entity)?;
504                        match result {
505                            stet_core::binary_token::BinaryTokenResult::Single(o) => (o, false),
506                            stet_core::binary_token::BinaryTokenResult::Sequence(o) => (o, true),
507                        }
508                    } else if matches!(token, Token::ProcBegin) {
509                        (stream_parse_procedure(ctx, file_entity)?, false)
510                    } else {
511                        (token_to_object(ctx, token)?, false)
512                    };
513                    if ctx.files.is_readable(file_entity) {
514                        ctx.e_stack.push(obj)?;
515                    }
516                    if auto_exec {
517                        ctx.e_stack.push(tok_obj)?;
518                    } else {
519                        dispatch_scanned_token(ctx, tok_obj, is_immediate)?;
520                    }
521                }
522            }
523        }
524
525        // Stopped marker → push false (normal completion, no stop)
526        PsValue::Stopped => {
527            ctx.o_stack.push(PsObject::bool(false))?;
528        }
529
530        // Loop state → advance loop
531        PsValue::Loop(loop_entity) => {
532            advance_loop(ctx, loop_entity)?;
533        }
534
535        // HardReturn → just consume (exit current procedure level)
536        PsValue::HardReturn => {}
537
538        // DictEnd → conditionally pop the dict stack (resource operator cleanup)
539        PsValue::DictEnd(expected) => {
540            pop_dict_end(ctx, expected);
541        }
542
543        // Everything else → push to operand stack
544        _ => {
545            ctx.o_stack.push(obj)?;
546        }
547    }
548    Ok(())
549}
550
551/// Push a procedure cursor onto the execution stack.
552///
553/// Instead of expanding all procedure elements onto the e_stack, we push a
554/// single `ExecArray` cursor that the eval loop advances one element at a time.
555/// Each procedure occupies exactly one exec-stack item.
556fn exec_procedure(
557    ctx: &mut Context,
558    entity: EntityId,
559    start: u32,
560    len: u32,
561) -> Result<(), PsError> {
562    if len == 0 {
563        return Ok(());
564    }
565    ctx.e_stack.push(PsObject {
566        value: PsValue::ExecArray {
567            entity,
568            start,
569            len,
570            pos: 0,
571        },
572        flags: ObjFlags::executable_composite(),
573    })?;
574    Ok(())
575}
576
577/// Scan one token from a byte slice, handling procedures and immediate names.
578///
579/// Returns `(token_object, bytes_consumed, is_immediate, auto_exec)` or `None` at EOF.
580/// `auto_exec` is true for BOS sequences which should be pushed to e_stack.
581fn scan_token_from_bytes(
582    ctx: &mut Context,
583    bytes: &[u8],
584) -> Result<Option<(PsObject, usize, bool, bool)>, PsError> {
585    let mut tokenizer = Tokenizer::new(bytes);
586    match tokenizer.next_token()? {
587        Some(Token::BinaryTokenByte(tag)) => {
588            let pos = tokenizer.position();
589            let (result, consumed) =
590                stet_core::binary_token::parse_from_slice(ctx, tag, &bytes[pos..])?;
591            let total = pos + consumed;
592            match result {
593                stet_core::binary_token::BinaryTokenResult::Single(obj) => {
594                    Ok(Some((obj, total, false, false)))
595                }
596                stet_core::binary_token::BinaryTokenResult::Sequence(obj) => {
597                    Ok(Some((obj, total, false, true)))
598                }
599            }
600        }
601        Some(token) => {
602            let is_immediate = matches!(token, Token::ImmediateName(_));
603            // Numbers and executable names: consume one trailing whitespace (PLRM)
604            let eats_whitespace =
605                matches!(token, Token::Int(_) | Token::Real(_) | Token::Name(_, _));
606            let tok_obj = if matches!(token, Token::ProcBegin) {
607                parse_procedure(ctx, &mut tokenizer)?
608            } else {
609                token_to_object(ctx, token)?
610            };
611            let mut consumed = tokenizer.position();
612            if eats_whitespace && consumed < bytes.len() && is_ps_whitespace(bytes[consumed]) {
613                consumed += 1;
614            }
615            Ok(Some((tok_obj, consumed, is_immediate, false)))
616        }
617        None => Ok(None),
618    }
619}
620
621/// PostScript whitespace check for trailing-whitespace consumption.
622fn is_ps_whitespace(b: u8) -> bool {
623    matches!(b, b'\0' | b'\t' | b'\n' | 0x0C | b'\r' | b' ')
624}
625
626/// Dispatch a freshly-scanned token to the appropriate stack.
627///
628/// Executable names → e_stack (for dict lookup), everything else → o_stack.
629/// Immediate names (`//name`) skip execution since they were already resolved.
630fn dispatch_scanned_token(
631    ctx: &mut Context,
632    tok_obj: PsObject,
633    is_immediate: bool,
634) -> Result<(), PsError> {
635    if matches!(tok_obj.value, PsValue::Name(_)) && tok_obj.flags.is_executable() && !is_immediate {
636        ctx.e_stack.push(tok_obj)?;
637    } else {
638        ctx.o_stack.push(tok_obj)?;
639    }
640    Ok(())
641}
642
643/// Parse a `{ ... }` procedure body from a streaming file source.
644///
645/// Called when the streaming tokenizer returns `ProcBegin`. Reads tokens
646/// from the file byte-by-byte until the matching `}`.
647fn stream_parse_procedure(ctx: &mut Context, file_entity: EntityId) -> Result<PsObject, PsError> {
648    let mut elements = Vec::new();
649
650    loop {
651        ctx.pump_proc_sources(file_entity)?;
652        match stream_next_token(&mut ctx.files, file_entity)? {
653            None => return Err(PsError::SyntaxError), // unterminated
654            Some((Token::ProcEnd, _)) => break,
655            Some((Token::ProcBegin, _)) => {
656                let nested = stream_parse_procedure(ctx, file_entity)?;
657                elements.push(nested);
658            }
659            Some((Token::BinaryTokenByte(tag), _)) => {
660                let result = stet_core::binary_token::parse_from_stream(ctx, tag, file_entity)?;
661                let obj = match result {
662                    stet_core::binary_token::BinaryTokenResult::Single(o) => o,
663                    stet_core::binary_token::BinaryTokenResult::Sequence(o) => o,
664                };
665                elements.push(obj);
666            }
667            Some((token, _)) => {
668                let obj = token_to_object(ctx, token)?;
669                elements.push(obj);
670            }
671        }
672    }
673
674    let len = elements.len();
675    let save_level = ctx.save_stack.current_level();
676    let global = ctx.vm_alloc_mode;
677    let created = ctx.save_stack.last_save_id();
678    let entity = ctx.arrays.allocate_with(len, save_level, global, created);
679    let dest = ctx.arrays.get_mut(entity, 0, len as u32);
680    dest.copy_from_slice(&elements);
681
682    let mut obj = PsObject::procedure(entity, len as u32);
683    if global {
684        obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, true, true);
685    }
686    Ok(obj)
687}
688
689/// Count newline characters in a byte slice.
690/// Handles CR, LF, and CR-LF (treated as one newline).
691fn count_newlines(bytes: &[u8]) -> u32 {
692    let mut count = 0u32;
693    let mut i = 0;
694    while i < bytes.len() {
695        match bytes[i] {
696            b'\r' => {
697                count += 1;
698                // CR-LF counts as one newline
699                if i + 1 < bytes.len() && bytes[i + 1] == b'\n' {
700                    i += 1;
701                }
702            }
703            b'\n' | b'\x0c' => {
704                count += 1;
705            }
706            _ => {}
707        }
708        i += 1;
709    }
710    count
711}
712
713/// Advance a loop iteration (for, repeat, loop, forall).
714fn advance_loop(ctx: &mut Context, loop_entity: EntityId) -> Result<(), PsError> {
715    use stet_core::context::LoopType;
716
717    let loop_state = ctx.get_loop(loop_entity);
718    let loop_type = match loop_state.loop_type {
719        LoopType::For => 0,
720        LoopType::Repeat => 1,
721        LoopType::Loop => 2,
722        LoopType::Forall => 3,
723        LoopType::PathForall => 4,
724    };
725    let proc_entity = loop_state.proc_entity;
726    let proc_start = loop_state.proc_start;
727    let proc_len = loop_state.proc_len;
728
729    match loop_type {
730        0 => {
731            // For loop
732            let counter = ctx.get_loop(loop_entity).counter;
733            let increment = ctx.get_loop(loop_entity).increment;
734            let limit = ctx.get_loop(loop_entity).limit;
735            let use_int = ctx.get_loop(loop_entity).use_int;
736
737            let done = if increment > 0.0 {
738                counter > limit
739            } else {
740                counter < limit
741            };
742
743            if done {
744                return Ok(());
745            }
746
747            // Push counter value
748            if use_int {
749                ctx.o_stack.push(PsObject::int(counter as i32))?;
750            } else {
751                ctx.o_stack.push(PsObject::real(counter))?;
752            }
753
754            // Update counter for next iteration
755            let new_counter = counter + increment;
756            ctx.get_loop_mut(loop_entity).counter = new_counter;
757
758            // Push loop marker back, then procedure
759            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
760            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
761        }
762        1 => {
763            // Repeat loop
764            let counter = ctx.get_loop(loop_entity).counter;
765            if counter <= 0.0 {
766                return Ok(());
767            }
768            ctx.get_loop_mut(loop_entity).counter = counter - 1.0;
769            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
770            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
771        }
772        2 => {
773            // Infinite loop
774            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
775            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
776        }
777        3 => {
778            // Forall
779            advance_forall(ctx, loop_entity, proc_entity, proc_start, proc_len)?;
780        }
781        4 => {
782            // PathForall
783            advance_pathforall(ctx, loop_entity)?;
784        }
785        _ => unreachable!(),
786    }
787
788    Ok(())
789}
790
791/// Advance a forall iteration.
792fn advance_forall(
793    ctx: &mut Context,
794    loop_entity: EntityId,
795    proc_entity: EntityId,
796    proc_start: u32,
797    proc_len: u32,
798) -> Result<(), PsError> {
799    let source = ctx.get_loop(loop_entity).source;
800    let index = ctx.get_loop(loop_entity).index;
801
802    match source.value {
803        PsValue::Array { entity, start, len } | PsValue::PackedArray { entity, start, len } => {
804            if index >= len {
805                return Ok(());
806            }
807            let elem = ctx.arrays.get_element(entity, start + index);
808            ctx.o_stack.push(elem)?;
809            ctx.get_loop_mut(loop_entity).index = index + 1;
810            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
811            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
812        }
813        PsValue::String { entity, start, len } => {
814            if index >= len {
815                return Ok(());
816            }
817            let byte = ctx.strings.get_byte(entity, start + index);
818            ctx.o_stack.push(PsObject::int(byte as i32))?;
819            ctx.get_loop_mut(loop_entity).index = index + 1;
820            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
821            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
822        }
823        PsValue::Dict(dict_entity) => {
824            // Keys were snapshotted at forall creation time.
825            // SAFETY: dict_keys is always Some for dict forall loops.
826            let keys = ctx.get_loop(loop_entity).dict_keys.as_ref().unwrap();
827            if (index as usize) >= keys.len() {
828                return Ok(());
829            }
830            let key = keys[index as usize].clone();
831            let val = ctx.dicts.get(dict_entity, &key).unwrap_or(PsObject::null());
832
833            // Push key and value
834            let key_obj = dict_key_to_object(ctx, &key);
835            ctx.o_stack.push(key_obj)?;
836            ctx.o_stack.push(val)?;
837
838            ctx.get_loop_mut(loop_entity).index = index + 1;
839            ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
840            exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
841        }
842        _ => return Err(PsError::TypeCheck),
843    }
844
845    Ok(())
846}
847
848/// Advance a pathforall iteration: process one path segment per call.
849fn advance_pathforall(ctx: &mut Context, loop_entity: EntityId) -> Result<(), PsError> {
850    use stet_core::geometry::PathSegment;
851
852    let index = ctx.get_loop(loop_entity).index as usize;
853
854    // Check if we've exhausted all segments
855    let seg_len = ctx
856        .get_loop(loop_entity)
857        .path_segments
858        .as_ref()
859        .map_or(0, |s| s.len());
860    if index >= seg_len {
861        return Ok(());
862    }
863
864    // Extract segment data, ictm, and proc for this iteration
865    let loop_state = ctx.get_loop(loop_entity);
866    let seg = loop_state.path_segments.as_ref().unwrap()[index].clone();
867    let ictm = loop_state.path_ictm.unwrap();
868    let procs = loop_state.path_procs.unwrap();
869
870    // Determine which proc to call and push the arguments
871    let proc = match seg {
872        PathSegment::MoveTo(dx, dy) => {
873            let (ux, uy) = ictm.transform_point(dx, dy);
874            ctx.o_stack.push(PsObject::real(ux))?;
875            ctx.o_stack.push(PsObject::real(uy))?;
876            procs[0] // move_proc
877        }
878        PathSegment::LineTo(dx, dy) => {
879            let (ux, uy) = ictm.transform_point(dx, dy);
880            ctx.o_stack.push(PsObject::real(ux))?;
881            ctx.o_stack.push(PsObject::real(uy))?;
882            procs[1] // line_proc
883        }
884        PathSegment::CurveTo {
885            x1,
886            y1,
887            x2,
888            y2,
889            x3,
890            y3,
891        } => {
892            let (ux1, uy1) = ictm.transform_point(x1, y1);
893            let (ux2, uy2) = ictm.transform_point(x2, y2);
894            let (ux3, uy3) = ictm.transform_point(x3, y3);
895            ctx.o_stack.push(PsObject::real(ux1))?;
896            ctx.o_stack.push(PsObject::real(uy1))?;
897            ctx.o_stack.push(PsObject::real(ux2))?;
898            ctx.o_stack.push(PsObject::real(uy2))?;
899            ctx.o_stack.push(PsObject::real(ux3))?;
900            ctx.o_stack.push(PsObject::real(uy3))?;
901            procs[2] // curve_proc
902        }
903        PathSegment::ClosePath => {
904            procs[3] // close_proc
905        }
906    };
907
908    // Advance index for next iteration
909    ctx.get_loop_mut(loop_entity).index = (index + 1) as u32;
910
911    // Push loop marker back, then the callback procedure
912    ctx.e_stack.push(PsObject::loop_mark(loop_entity))?;
913    let (proc_entity, proc_start, proc_len) = match proc.value {
914        PsValue::Array { entity, start, len } => (entity, start, len),
915        _ => return Err(PsError::TypeCheck),
916    };
917    exec_procedure(ctx, proc_entity, proc_start, proc_len)?;
918
919    Ok(())
920}
921
922/// Convert a DictKey back to a PsObject for forall iteration.
923fn dict_key_to_object(ctx: &mut Context, key: &DictKey) -> PsObject {
924    match key {
925        DictKey::Name(id) => PsObject::name_lit(*id),
926        DictKey::Int(v) => PsObject::int(*v),
927        DictKey::Real(bits) => PsObject::real(f64::from_bits(*bits)),
928        DictKey::Bool(v) => PsObject::bool(*v),
929        DictKey::String(bytes) => {
930            let entity = stet_ops::vm_ops::alloc_string(ctx, bytes);
931            PsObject::string(entity, bytes.len() as u32)
932        }
933        DictKey::Operator(op) => {
934            use stet_core::object::OpCode;
935            PsObject::operator(OpCode(*op))
936        }
937        DictKey::Identity(eid, _start, len) => {
938            // Return as array (best approximation for identity keys)
939            PsObject::array(EntityId(*eid), *len)
940        }
941    }
942}
943
944/// Dispatch a PostScript error via errordict.
945///
946/// PLRM error dispatch:
947/// 1. Look up the error name in errordict → get handler procedure
948/// 2. Handler populates `$error` dict and calls `stop`
949/// 3. If no handler found, or if already in error handler, fall back to eprintln
950fn dispatch_error(ctx: &mut Context, error: &PsError) -> Result<(), PsError> {
951    // Guard against infinite recursion
952    if ctx.in_error_handler {
953        use std::io::Write;
954        let _ = writeln!(ctx.stdout, "Error (in handler): {}", error);
955        return Ok(());
956    }
957
958    let error_name = error.to_string();
959    let error_name_id = ctx.names.intern(error_name.as_bytes());
960    let error_key = DictKey::Name(error_name_id);
961
962    // Look up error handler in errordict
963    if let Some(handler) = ctx.dicts.get(ctx.errordict, &error_key)
964        && handler.flags.is_executable()
965    {
966        ctx.in_error_handler = true;
967
968        // Push offending command name on operand stack — the errordict handler
969        // pushes the error name, then `.error` expects `command errorname` on
970        // the stack. Use the current operator if available, otherwise the error
971        // name itself.
972        let cmd_name = ctx.current_operator.unwrap_or(error_name_id);
973        ctx.current_operator = None;
974        ctx.o_stack.push(PsObject::name_lit(cmd_name))?;
975
976        // Push handler on e_stack for natural execution by the eval loop.
977        // The handler (e.g. `{ /undefined //.error exec }`) will populate
978        // $error and call `stop`, which propagates naturally to be caught by
979        // any enclosing `stopped` context.
980        ctx.e_stack.push(handler)?;
981
982        ctx.in_error_handler = false;
983        return Ok(());
984    }
985
986    // Fallback: no handler found, print to stdout (like stderr)
987    use std::io::Write;
988    let _ = writeln!(ctx.stdout, "Error: {}", error);
989    Ok(())
990}
991
992/// Unwind execution stack to the nearest `Stopped` marker.
993/// DictEnd markers encountered during unwinding conditionally pop the dict
994/// stack (only if the expected dict is still on top).
995fn unwind_to_stopped(ctx: &mut Context) -> Result<(), PsError> {
996    while let Some(obj) = ctx.e_stack.try_pop() {
997        match obj.value {
998            PsValue::Stopped => return Ok(()),
999            PsValue::DictEnd(expected) => {
1000                pop_dict_end(ctx, expected);
1001            }
1002            _ => {}
1003        }
1004    }
1005    // No stopped marker found — propagate
1006    Err(PsError::Stop)
1007}
1008
1009/// Unwind execution stack to the nearest `Loop` marker.
1010fn unwind_to_loop(ctx: &mut Context) -> Result<(), PsError> {
1011    while let Some(obj) = ctx.e_stack.try_pop() {
1012        match obj.value {
1013            PsValue::Loop(_) => return Ok(()),
1014            PsValue::Stopped => {
1015                // Don't pop past stopped — push it back and dispatch error
1016                ctx.e_stack.push(obj)?;
1017                return Err(PsError::InvalidExit);
1018            }
1019            PsValue::DictEnd(expected) => {
1020                pop_dict_end(ctx, expected);
1021            }
1022            _ => {}
1023        }
1024    }
1025    Err(PsError::InvalidExit)
1026}
1027
1028/// Conditionally pop the dict stack for a DictEnd marker.
1029/// Only pops if the top of d_stack is still the expected entity — the PS
1030/// procedure may have already called `end` to clean up (e.g., on error paths).
1031fn pop_dict_end(ctx: &mut Context, expected: EntityId) {
1032    if ctx.d_stack.last() == Some(&expected) {
1033        ctx.d_stack.pop();
1034        ctx.invalidate_name_cache();
1035    }
1036}
1037
1038/// Convert a token to a PsObject. Delegates to `Context::token_to_object`.
1039pub fn token_to_object(ctx: &mut Context, token: Token) -> Result<PsObject, PsError> {
1040    ctx.token_to_object(token)
1041}
1042
1043/// Tokenize a byte stream and execute it.
1044///
1045/// Creates a file-backed source in the FileStore and pushes it as an
1046/// executable `File` on the exec stack. The eval loop tokenizes and
1047/// executes one token at a time, ensuring `//name` immediate lookups
1048/// see earlier definitions and ALL errors route through errordict.
1049///
1050/// Using a File (not an executable string) means `currentfile` can find
1051/// the source — this is correct for top-level execution of PS files.
1052pub fn parse_and_exec(ctx: &mut Context, source: &[u8]) -> Result<(), PsError> {
1053    let file_entity = ctx.files.create_string_source(source.to_vec());
1054    ctx.e_stack.push(PsObject {
1055        value: PsValue::File(file_entity),
1056        flags: ObjFlags::executable_composite(),
1057    })?;
1058    eval(ctx)
1059}
1060
1061/// Execute PostScript source loaded from a named file.
1062///
1063/// Like `parse_and_exec`, but records the file path on the StringSource
1064/// entity so that `resolve_filename` can find the file's parent directory
1065/// when resolving relative paths in nested `run`/`file` calls.
1066pub fn parse_and_exec_file(ctx: &mut Context, source: &[u8], path: &str) -> Result<(), PsError> {
1067    let file_entity = ctx.files.create_string_source(source.to_vec());
1068    // Record the canonical path so resolve_filename can extract its directory.
1069    let canonical = std::path::Path::new(path)
1070        .canonicalize()
1071        .unwrap_or_else(|_| std::path::PathBuf::from(path));
1072    ctx.files
1073        .set_name(file_entity, canonical.to_string_lossy().to_string());
1074    ctx.e_stack.push(PsObject {
1075        value: PsValue::File(file_entity),
1076        flags: ObjFlags::executable_composite(),
1077    })?;
1078    eval(ctx)
1079}
1080
1081/// Parse a `{ ... }` procedure body (recursive for nested procedures).
1082fn parse_procedure(ctx: &mut Context, tokenizer: &mut Tokenizer) -> Result<PsObject, PsError> {
1083    let mut elements = Vec::new();
1084
1085    loop {
1086        match tokenizer.next_token()? {
1087            Some(Token::ProcEnd) => break,
1088            Some(Token::ProcBegin) => {
1089                let nested = parse_procedure(ctx, tokenizer)?;
1090                elements.push(nested);
1091            }
1092            Some(Token::BinaryTokenByte(tag)) => {
1093                let pos = tokenizer.position();
1094                // SAFETY: tokenizer borrows from a stable slice; we access the
1095                // underlying input via position.
1096                let input_bytes = tokenizer.remaining_from(pos);
1097                let (result, consumed) =
1098                    stet_core::binary_token::parse_from_slice(ctx, tag, input_bytes)?;
1099                tokenizer.advance(consumed);
1100                let obj = match result {
1101                    stet_core::binary_token::BinaryTokenResult::Single(o) => o,
1102                    stet_core::binary_token::BinaryTokenResult::Sequence(o) => o,
1103                };
1104                elements.push(obj);
1105            }
1106            Some(token) => {
1107                let obj = token_to_object(ctx, token)?;
1108                elements.push(obj);
1109            }
1110            None => return Err(PsError::SyntaxError), // unterminated procedure
1111        }
1112    }
1113
1114    let len = elements.len();
1115    let save_level = ctx.save_stack.current_level();
1116    let global = ctx.vm_alloc_mode;
1117    let created = ctx.save_stack.last_save_id();
1118    let entity = ctx.arrays.allocate_with(len, save_level, global, created);
1119    let dest = ctx.arrays.get_mut(entity, 0, len as u32);
1120    dest.copy_from_slice(&elements);
1121
1122    let mut obj = PsObject::procedure(entity, len as u32);
1123    if global {
1124        obj.flags = ObjFlags::new(ObjFlags::ACCESS_UNLIMITED, true, true, true);
1125    }
1126    Ok(obj)
1127}
1128
1129#[cfg(test)]
1130mod tests {
1131    use super::*;
1132    use std::io::Write;
1133
1134    #[test]
1135    fn test_parse_procedure() {
1136        let mut ctx = Context::new();
1137        let mut tokenizer = Tokenizer::new(b"1 2 add }");
1138        let proc_obj = parse_procedure(&mut ctx, &mut tokenizer).unwrap();
1139        assert!(proc_obj.flags.is_executable());
1140        assert!(proc_obj.is_array_type());
1141        match proc_obj.value {
1142            PsValue::Array { len, .. } => assert_eq!(len, 3),
1143            _ => panic!("Expected array"),
1144        }
1145    }
1146
1147    #[test]
1148    fn test_nested_procedure() {
1149        let mut ctx = Context::new();
1150        let mut tokenizer = Tokenizer::new(b"{ 1 add } exec }");
1151        let proc_obj = parse_procedure(&mut ctx, &mut tokenizer).unwrap();
1152        match proc_obj.value {
1153            PsValue::Array { len, .. } => assert_eq!(len, 2), // { 1 add } and exec
1154            _ => panic!("Expected array"),
1155        }
1156    }
1157
1158    // --- Phase 2 integration tests (done-when criteria) ---
1159
1160    fn setup_ctx() -> (Context, std::sync::Arc<std::sync::Mutex<Vec<u8>>>) {
1161        let buf = std::sync::Arc::new(std::sync::Mutex::new(Vec::new()));
1162        let writer = buf.clone();
1163
1164        struct ArcWriter(std::sync::Arc<std::sync::Mutex<Vec<u8>>>);
1165        impl Write for ArcWriter {
1166            fn write(&mut self, data: &[u8]) -> std::io::Result<usize> {
1167                self.0.lock().unwrap().extend_from_slice(data);
1168                Ok(data.len())
1169            }
1170            fn flush(&mut self) -> std::io::Result<()> {
1171                Ok(())
1172            }
1173        }
1174
1175        let mut ctx = Context::new_with_output(Box::new(ArcWriter(writer)));
1176        stet_ops::build_system_dict(&mut ctx);
1177        (ctx, buf)
1178    }
1179
1180    fn run_ps(source: &[u8]) -> String {
1181        let (mut ctx, buf) = setup_ctx();
1182        parse_and_exec(&mut ctx, source).ok();
1183        String::from_utf8(buf.lock().unwrap().clone()).unwrap()
1184    }
1185
1186    /// Done-when #2: save/restore reverts definitions.
1187    /// `save /s exch def /x 1 def s restore x =` — x should be undefined after restore
1188    #[test]
1189    fn test_save_restore_reverts_def() {
1190        let (mut ctx, buf) = setup_ctx();
1191        // Define x before save for baseline
1192        let result = parse_and_exec(&mut ctx, b"save /s exch def /x 1 def s restore");
1193        assert!(result.is_ok());
1194        // x should be undefined now — trying to use it should trigger an error
1195        let output = String::from_utf8(buf.lock().unwrap().clone()).unwrap();
1196        // Verify x is not defined by checking that a lookup would fail
1197        let x_id = ctx.names.intern(b"x");
1198        let key = DictKey::Name(x_id);
1199        assert!(
1200            ctx.dict_load(&key).is_none(),
1201            "x should be undefined after restore"
1202        );
1203        drop(output);
1204    }
1205
1206    /// Done-when #3: save/restore reverts array mutations.
1207    /// Define array before save, mutate after save, restore reverts mutation.
1208    #[test]
1209    fn test_save_restore_reverts_array() {
1210        let output = run_ps(b"[1 2 3] /a exch def save /s exch def a 1 99 put s restore a 1 get =");
1211        assert_eq!(output.trim(), "2");
1212    }
1213
1214    /// Done-when #4: File round-trip
1215    #[test]
1216    fn test_file_round_trip() {
1217        // Use forward slashes even on Windows — PostScript string-literal
1218        // syntax treats `\` as an escape character, so a native Windows
1219        // path like `C:\Users\...` would be mangled inside `(...)`.
1220        // Rust's fs APIs on Windows accept `/` just fine.
1221        let path = std::env::temp_dir()
1222            .join("stet_phase2_file_test.txt")
1223            .to_string_lossy()
1224            .replace('\\', "/");
1225        let source = format!(
1226            "({}) (w) file /f exch def f (hello world) writestring f closefile \
1227             ({}) (r) file /f exch def f 11 string readstring pop print f closefile",
1228            path, path
1229        );
1230        let output = run_ps(source.as_bytes());
1231        assert_eq!(output, "hello world");
1232        std::fs::remove_file(&path).ok();
1233    }
1234
1235    /// Done-when #5: `{ 1 0 div } stopped { (caught\n) print } if` → prints "caught"
1236    #[test]
1237    fn test_stopped_catches_error() {
1238        let output = run_ps(b"{ 1 0 div } stopped { (caught\n) print } if");
1239        assert_eq!(output.trim(), "caught");
1240    }
1241
1242    /// Done-when #6: `true setglobal 3 array gcheck` → returns true
1243    #[test]
1244    fn test_setglobal_gcheck() {
1245        let output = run_ps(b"true setglobal 3 array gcheck =");
1246        assert_eq!(output.trim(), "true");
1247    }
1248
1249    /// Done-when #7: `vmstatus` returns three integers
1250    #[test]
1251    fn test_vmstatus() {
1252        let output = run_ps(b"vmstatus = = =");
1253        let lines: Vec<&str> = output.trim().lines().collect();
1254        assert_eq!(lines.len(), 3, "vmstatus should push 3 values");
1255        // All should be parseable as integers
1256        for line in &lines {
1257            assert!(
1258                line.trim().parse::<i32>().is_ok(),
1259                "Expected integer: {}",
1260                line
1261            );
1262        }
1263    }
1264
1265    /// Test that error dispatch populates $error and calls stop
1266    #[test]
1267    fn test_error_dispatch_stop() {
1268        let output = run_ps(b"{ 1 0 div } stopped { (error caught\n) print } if");
1269        assert!(output.contains("error caught"));
1270    }
1271
1272    /// Test nested save/restore
1273    #[test]
1274    fn test_nested_save_restore() {
1275        let output = run_ps(
1276            b"/x 10 def \
1277              save /s1 exch def \
1278              /x 20 def \
1279              save /s2 exch def \
1280              /x 30 def \
1281              x = \
1282              s2 restore \
1283              x = \
1284              s1 restore \
1285              x =",
1286        );
1287        let lines: Vec<&str> = output.trim().lines().collect();
1288        assert_eq!(lines, vec!["30", "20", "10"]);
1289    }
1290
1291    /// Test that global entities are not affected by restore.
1292    /// The array is allocated in global mode and defined before save,
1293    /// so the mutation survives restore.
1294    #[test]
1295    fn test_global_survives_restore() {
1296        let output = run_ps(
1297            b"true setglobal \
1298              3 array /ga exch def \
1299              false setglobal \
1300              save /s exch def \
1301              ga 0 42 put \
1302              s restore \
1303              ga 0 get =",
1304        );
1305        assert_eq!(output.trim(), "42");
1306    }
1307}