Skip to main content

yulang_native/
cps_eval.rs

1use std::cell::RefCell;
2use std::collections::{BTreeMap, HashSet};
3use std::fmt;
4use std::rc::Rc;
5
6use yulang_runtime as runtime;
7use yulang_typed_ir as typed_ir;
8
9use crate::cps_frame_trace::{
10    CpsFrameTraceEvent, CpsFrameTraceLayer, CpsFrameTraceSlot, push_cps_frame_trace_event,
11};
12use crate::cps_ir::{
13    CpsContinuation, CpsContinuationId, CpsFunction, CpsHandlerArm, CpsHandlerEnv, CpsHandlerId,
14    CpsLiteral, CpsModule, CpsStmt, CpsTerminator, CpsValueId,
15};
16
17pub type CpsEvalResult<T> = Result<T, CpsEvalError>;
18
19thread_local! {
20    static LATEST_HANDLER_ENVS: RefCell<Vec<CpsLatestHandlerEnv>> = const { RefCell::new(Vec::new()) };
21}
22
23fn trace_enabled() -> bool {
24    std::env::var_os("YULANG_CPS_TRACE_FRAMES").is_some()
25}
26
27fn trace_cps(event: &str, msg: impl std::fmt::Display) {
28    if trace_enabled() {
29        eprintln!("[cps-trace] {event}: {msg}");
30    }
31}
32
33/// Compact summary of a CpsRuntimeValue for trace output. Avoids the
34/// recursive Debug bomb that thunks/closures/resumptions produce.
35fn summarize_cps_value(value: &CpsRuntimeValue) -> String {
36    match value {
37        CpsRuntimeValue::Plain(v) => format!("Plain({v:?})"),
38        CpsRuntimeValue::Thunk(thunk) => {
39            format!(
40                "Thunk(owner={}, entry={:?})",
41                thunk.owner_function, thunk.entry
42            )
43        }
44        CpsRuntimeValue::Closure(closure) => format!(
45            "Closure(owner={}, entry={:?}, recursive_self={:?})",
46            closure.owner_function, closure.entry, closure.recursive_self,
47        ),
48        CpsRuntimeValue::Resumption(resumption) => format!(
49            "Resumption(owner={}, target={:?}, frames={}, handlers={}, guards={})",
50            resumption.owner_function,
51            resumption.target,
52            resumption.return_frames.len(),
53            resumption.handlers.len(),
54            resumption.guard_stack.len(),
55        ),
56        CpsRuntimeValue::List(items) => {
57            let preview = items
58                .iter()
59                .take(3)
60                .map(summarize_cps_value)
61                .collect::<Vec<_>>()
62                .join(", ");
63            format!("List(len={}, [{}])", items.len(), preview)
64        }
65        CpsRuntimeValue::Tuple(items) => {
66            let preview = items
67                .iter()
68                .map(summarize_cps_value)
69                .collect::<Vec<_>>()
70                .join(", ");
71            format!("Tuple([{preview}])")
72        }
73        CpsRuntimeValue::Record(fields) => {
74            let preview = fields
75                .iter()
76                .map(|(name, value)| format!("{}: {}", name.0, summarize_cps_value(value)))
77                .collect::<Vec<_>>()
78                .join(", ");
79            format!("Record({{{preview}}})")
80        }
81        CpsRuntimeValue::Variant { tag, value } => match value {
82            Some(value) => format!("Variant({}, {})", tag.0, summarize_cps_value(value)),
83            None => format!("Variant({})", tag.0),
84        },
85        CpsRuntimeValue::ScopeReturn {
86            prompt,
87            target,
88            value,
89        } => format!(
90            "ScopeReturn(prompt={}, target={:?}, value={})",
91            prompt.0,
92            target,
93            summarize_cps_value(value),
94        ),
95        CpsRuntimeValue::RoutedJump(jump) => format!(
96            "RoutedJump(owner={}, target={:?}, value={}, threshold={})",
97            jump.owner_function,
98            jump.target,
99            summarize_cps_value(&jump.value),
100            jump.return_frame_threshold,
101        ),
102        CpsRuntimeValue::Aborted(value) => format!("Aborted({})", summarize_cps_value(value)),
103    }
104}
105
106#[derive(Debug, Clone, PartialEq)]
107pub enum CpsEvalError {
108    MissingFunction {
109        name: String,
110    },
111    MissingContinuation {
112        function: String,
113        id: CpsContinuationId,
114    },
115    MissingHandler {
116        function: String,
117        id: CpsHandlerId,
118    },
119    ContinuationArgumentMismatch {
120        function: String,
121        id: CpsContinuationId,
122        expected: usize,
123        actual: usize,
124    },
125    FunctionArgumentMismatch {
126        function: String,
127        expected: usize,
128        actual: usize,
129    },
130    MissingValue {
131        function: String,
132        id: CpsValueId,
133    },
134    ExpectedPlainValue {
135        function: String,
136        id: CpsValueId,
137    },
138    ExpectedResumption {
139        function: String,
140        id: CpsValueId,
141    },
142    UnsupportedPrimitive {
143        op: typed_ir::PrimitiveOp,
144    },
145    PrimitiveTypeMismatch {
146        op: typed_ir::PrimitiveOp,
147        value: runtime::VmValue,
148    },
149    InvalidPrimitiveArity {
150        op: typed_ir::PrimitiveOp,
151        expected: usize,
152        actual: usize,
153    },
154    ExpectedRecord {
155        function: String,
156        value: runtime::VmValue,
157    },
158    MissingRecordField {
159        function: String,
160        field: typed_ir::Name,
161    },
162    MissingGuard,
163    ExpectedGuard {
164        function: String,
165        id: CpsValueId,
166        value: runtime::VmValue,
167    },
168    /// A `ScopeReturn` reached the root without ever finding its prompt
169    /// in `active_handlers`. This is a lowering bug — every `Perform`
170    /// must lead to an arm whose enclosing handler scope is still on
171    /// the stack.
172    EscapedScopeReturn {
173        function: String,
174        prompt: u64,
175        target: CpsContinuationId,
176    },
177}
178
179impl fmt::Display for CpsEvalError {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        match self {
182            CpsEvalError::MissingFunction { name } => {
183                write!(f, "CPS function {name} is missing")
184            }
185            CpsEvalError::MissingContinuation { function, id } => {
186                write!(f, "CPS function {function} is missing continuation {id:?}")
187            }
188            CpsEvalError::MissingHandler { function, id } => {
189                write!(f, "CPS function {function} is missing handler {id:?}")
190            }
191            CpsEvalError::ContinuationArgumentMismatch {
192                function,
193                id,
194                expected,
195                actual,
196            } => write!(
197                f,
198                "CPS continuation {function}::{id:?} expected {expected} arguments, got {actual}"
199            ),
200            CpsEvalError::FunctionArgumentMismatch {
201                function,
202                expected,
203                actual,
204            } => write!(
205                f,
206                "CPS function {function} expected {expected} arguments, got {actual}"
207            ),
208            CpsEvalError::MissingValue { function, id } => {
209                write!(f, "CPS function {function} references missing value {id:?}")
210            }
211            CpsEvalError::ExpectedPlainValue { function, id } => {
212                write!(f, "CPS function {function} expected plain value {id:?}")
213            }
214            CpsEvalError::ExpectedResumption { function, id } => {
215                write!(
216                    f,
217                    "CPS function {function} expected resumption value {id:?}"
218                )
219            }
220            CpsEvalError::UnsupportedPrimitive { op } => {
221                write!(f, "CPS evaluator does not support primitive {op:?} yet")
222            }
223            CpsEvalError::PrimitiveTypeMismatch { op, value } => {
224                write!(f, "CPS primitive {op:?} cannot accept value {value:?}")
225            }
226            CpsEvalError::InvalidPrimitiveArity {
227                op,
228                expected,
229                actual,
230            } => write!(
231                f,
232                "CPS primitive {op:?} expected {expected} arguments, got {actual}"
233            ),
234            CpsEvalError::ExpectedRecord { function, value } => {
235                write!(
236                    f,
237                    "CPS function {function} expected record value, got {value:?}"
238                )
239            }
240            CpsEvalError::MissingRecordField { function, field } => {
241                write!(
242                    f,
243                    "CPS function {function} selected missing record field {field:?}"
244                )
245            }
246            CpsEvalError::MissingGuard => write!(f, "CPS evaluator has no active guard id"),
247            CpsEvalError::ExpectedGuard {
248                function,
249                id,
250                value,
251            } => write!(
252                f,
253                "CPS function {function} expected guard value {id:?}, got {value:?}"
254            ),
255            CpsEvalError::EscapedScopeReturn {
256                function,
257                prompt,
258                target,
259            } => write!(
260                f,
261                "ScopeReturn (prompt {prompt}, target {target:?}) escaped \
262                 from CPS function {function} without a matching handler"
263            ),
264        }
265    }
266}
267
268impl std::error::Error for CpsEvalError {}
269
270pub fn eval_cps_module(module: &CpsModule) -> CpsEvalResult<Vec<runtime::VmValue>> {
271    module
272        .roots
273        .iter()
274        .map(|root| {
275            let value = with_fresh_handler_env_overlay(|| eval_function(module, root, Vec::new()))?;
276            let value = resolve_routed_jump(module, value, &[])?;
277            // ScopeReturn must be matched against an InstallHandler frame
278            // somewhere on the way up. If one reaches the root, that's a
279            // lowering bug — there was no handler to catch it.
280            if let CpsRuntimeValue::ScopeReturn { prompt, target, .. } = &value {
281                return Err(CpsEvalError::EscapedScopeReturn {
282                    function: root.name.clone(),
283                    prompt: prompt.0,
284                    target: *target,
285                });
286            }
287            into_plain_value(root, CpsValueId(usize::MAX), unwrap_aborted(value))
288        })
289        .collect()
290}
291
292fn with_fresh_handler_env_overlay<T>(f: impl FnOnce() -> T) -> T {
293    let previous = LATEST_HANDLER_ENVS.with(|envs| envs.replace(Vec::new()));
294    let result = f();
295    LATEST_HANDLER_ENVS.with(|envs| {
296        envs.replace(previous);
297    });
298    result
299}
300
301fn unwrap_aborted(value: CpsRuntimeValue) -> CpsRuntimeValue {
302    match value {
303        CpsRuntimeValue::Aborted(inner) => unwrap_aborted(*inner),
304        other => other,
305    }
306}
307
308/// Outcome of inspecting a value returned by an internal call (DirectCall,
309/// ApplyClosure, ForceThunk, Resume, ResumeWithHandler) for a `ScopeReturn`
310/// that should be routed by the current eval frame.
311enum ScopeReturnAction {
312    /// Plain value — write to the call site's `dest` and continue.
313    Value(CpsRuntimeValue),
314    /// `ScopeReturn`'s prompt matched a frame in `active_handlers`. The
315    /// matched frame and any inner ones have already been popped. The
316    /// caller should jump to `target` (with `value` as the single arg) if
317    /// `target != EXIT_RWH_TARGET`, or return `value` from the current
318    /// eval frame if `target == EXIT_RWH_TARGET`. `return_frame_threshold`
319    /// is how many return_frames existed when the matched handler was
320    /// installed; frames pushed after that count are inside the handler
321    /// scope and should be discarded (delimited-continuation extent).
322    JumpOrExit {
323        target: CpsContinuationId,
324        value: CpsRuntimeValue,
325        return_frame_threshold: usize,
326    },
327    /// `ScopeReturn` did not match — propagate the original value up.
328    Propagate(CpsRuntimeValue),
329}
330
331fn handle_scope_return(
332    result: CpsRuntimeValue,
333    active_handlers: &mut Vec<CpsHandlerFrame>,
334    _return_frames: &[CpsReturnFrame],
335    current_function: &str,
336    current_eval_id: CpsEvalId,
337) -> ScopeReturnAction {
338    match result {
339        CpsRuntimeValue::ScopeReturn {
340            prompt,
341            target,
342            value,
343        } => {
344            // A ScopeReturn is resolved only by the eval frame that installed
345            // the matching handler prompt. Function identity is not enough:
346            // a multi-shot resumption can run the same CPS function in a
347            // fresh eval frame (e.g. `each__mono1` replayed inside an outer
348            // `once`'s recursive arm), and that fresh frame must not catch
349            // a handler installed by the original eval. Return frames
350            // preserve `owner_eval_id` so the original eval identity is
351            // restored when the captured continuation is resumed via
352            // `continue_return_frames` (write22).
353            if let Some(index) = active_handlers.iter().rposition(|frame| {
354                frame.prompt == prompt && frame.install_eval_id == current_eval_id
355            }) {
356                let frame = &active_handlers[index];
357                // owner check: a function-local jump target only makes
358                // sense in the same function. EXIT_RWH_TARGET is the
359                // sentinel for ResumeWithHandler so it crosses functions.
360                let frame_owner_match =
361                    target == EXIT_RWH_TARGET || frame.escape_owner_function == current_function;
362                let frame_owner = frame.escape_owner_function.clone();
363                let frame_install_eval = frame.install_eval_id;
364                let truncate_at = frame.return_frame_threshold;
365                if !frame_owner_match {
366                    trace_cps(
367                        "ScopeReturnDispatch",
368                        format!(
369                            "fn={} eval={} prompt={} target={:?} matched=yes install_eval={} owner={} owner_match=no action=Propagate",
370                            current_function,
371                            current_eval_id.0,
372                            prompt.0,
373                            target,
374                            frame_install_eval.0,
375                            frame_owner,
376                        ),
377                    );
378                    return ScopeReturnAction::Propagate(CpsRuntimeValue::ScopeReturn {
379                        prompt,
380                        target,
381                        value,
382                    });
383                }
384                trace_cps(
385                    "ScopeReturnDispatch",
386                    format!(
387                        "fn={} eval={} prompt={} target={:?} matched=yes install_eval={} owner={} owner_match=yes truncate_at={} action=JumpOrExit",
388                        current_function,
389                        current_eval_id.0,
390                        prompt.0,
391                        target,
392                        frame_install_eval.0,
393                        frame_owner,
394                        truncate_at,
395                    ),
396                );
397                active_handlers.truncate(index);
398                ScopeReturnAction::JumpOrExit {
399                    target,
400                    value: *value,
401                    return_frame_threshold: truncate_at,
402                }
403            } else {
404                trace_cps(
405                    "ScopeReturnDispatch",
406                    format!(
407                        "fn={} eval={} prompt={} target={:?} matched=no action=Propagate",
408                        current_function, current_eval_id.0, prompt.0, target,
409                    ),
410                );
411                ScopeReturnAction::Propagate(CpsRuntimeValue::ScopeReturn {
412                    prompt,
413                    target,
414                    value,
415                })
416            }
417        }
418        other => ScopeReturnAction::Value(other),
419    }
420}
421
422/// Mark every handler frame as inherited so a fresh `eval_continuations`
423/// frame doesn't try to resolve a `ScopeReturn` against handlers whose
424/// real install site lives in a parent eval.
425fn into_inherited(mut handlers: Vec<CpsHandlerFrame>) -> Vec<CpsHandlerFrame> {
426    for frame in &mut handlers {
427        frame.inherited = true;
428    }
429    handlers
430}
431
432fn cps_value_from_vm(value: runtime::VmValue) -> CpsRuntimeValue {
433    match value {
434        runtime::VmValue::Tuple(items) => {
435            CpsRuntimeValue::Tuple(items.into_iter().map(cps_value_from_vm).collect())
436        }
437        runtime::VmValue::Variant { tag, value } => CpsRuntimeValue::Variant {
438            tag,
439            value: value.map(|v| Box::new(cps_value_from_vm(*v))),
440        },
441        runtime::VmValue::List(list) => {
442            let items = list
443                .to_vec()
444                .into_iter()
445                .map(|item| cps_value_from_vm((*item).clone()))
446                .collect::<Vec<_>>();
447            CpsRuntimeValue::List(Rc::new(items))
448        }
449        other => CpsRuntimeValue::Plain(other),
450    }
451}
452
453fn cps_value_to_vm(value: CpsRuntimeValue) -> Option<runtime::VmValue> {
454    match value {
455        CpsRuntimeValue::Plain(value) => Some(value),
456        CpsRuntimeValue::Aborted(inner) => cps_value_to_vm(*inner),
457        CpsRuntimeValue::RoutedJump(_) => None,
458        // ScopeReturn must never appear here — callers either resolve it via
459        // `handle_scope_return` at every internal call boundary, or fail at
460        // root with EscapedScopeReturn. Returning None lets `into_plain_value`
461        // surface ExpectedPlainValue if it does sneak through.
462        CpsRuntimeValue::ScopeReturn { .. } => None,
463        CpsRuntimeValue::Tuple(items) => Some(runtime::VmValue::Tuple(
464            items
465                .into_iter()
466                .map(cps_value_to_vm)
467                .collect::<Option<Vec<_>>>()?,
468        )),
469        CpsRuntimeValue::Record(fields) => Some(runtime::VmValue::Record(
470            fields
471                .into_iter()
472                .map(|(name, value)| Some((name, cps_value_to_vm(value)?)))
473                .collect::<Option<BTreeMap<_, _>>>()?,
474        )),
475        CpsRuntimeValue::Variant { tag, value } => Some(runtime::VmValue::Variant {
476            tag,
477            value: match value {
478                Some(value) => Some(Box::new(cps_value_to_vm(*value)?)),
479                None => None,
480            },
481        }),
482        CpsRuntimeValue::List(items) => {
483            let vm_items = items
484                .iter()
485                .cloned()
486                .map(cps_value_to_vm)
487                .collect::<Option<Vec<_>>>()?;
488            let mut tree = runtime::runtime::list_tree::ListTree::empty();
489            for item in vm_items {
490                tree = runtime::runtime::list_tree::ListTree::concat(
491                    tree,
492                    runtime::runtime::list_tree::ListTree::singleton(Rc::new(item)),
493                );
494            }
495            Some(runtime::VmValue::List(tree))
496        }
497        CpsRuntimeValue::Resumption(_)
498        | CpsRuntimeValue::Thunk(_)
499        | CpsRuntimeValue::Closure(_) => None,
500    }
501}
502
503fn eval_function(
504    module: &CpsModule,
505    function: &CpsFunction,
506    args: Vec<runtime::VmValue>,
507) -> CpsEvalResult<CpsRuntimeValue> {
508    eval_function_with_context(
509        module,
510        function,
511        args.into_iter().map(CpsRuntimeValue::Plain).collect(),
512        Vec::new(),
513        Vec::new(),
514        Vec::new(),
515        Vec::new(),
516        0,
517    )
518}
519
520fn eval_function_with_context(
521    module: &CpsModule,
522    function: &CpsFunction,
523    args: Vec<CpsRuntimeValue>,
524    active_handlers: Vec<CpsHandlerFrame>,
525    guard_stack: Vec<CpsGuardEntry>,
526    return_frames: Vec<CpsReturnFrame>,
527    active_blocked: Vec<CpsBlockedEffect>,
528    initial_frame_count: usize,
529) -> CpsEvalResult<CpsRuntimeValue> {
530    if function.params.len() != args.len() {
531        return Err(CpsEvalError::FunctionArgumentMismatch {
532            function: function.name.clone(),
533            expected: function.params.len(),
534            actual: args.len(),
535        });
536    }
537    // Inherit all handler frames from the caller so this fresh eval does not
538    // resolve ScopeReturns that belong to parent scopes.
539    eval_continuations(
540        module,
541        function,
542        function.entry,
543        args,
544        Vec::new(),
545        active_handlers,
546        guard_stack,
547        return_frames,
548        active_blocked,
549        initial_frame_count,
550    )
551}
552
553/// Entry point that inherits caller handlers before entering the loop.
554/// Use this for cross-function calls (direct calls, closure applies, thunk forces).
555/// Issues a fresh `CpsEvalId` — this eval is a new dynamic frame, so any
556/// handler installed inside it gets a fresh `install_eval_id` and any
557/// ScopeReturn raised inside it can only resolve handlers installed here.
558fn eval_continuations(
559    module: &CpsModule,
560    function: &CpsFunction,
561    entry: CpsContinuationId,
562    initial_args: Vec<CpsRuntimeValue>,
563    initial_values: Vec<Option<CpsRuntimeValue>>,
564    active_handlers: Vec<CpsHandlerFrame>,
565    guard_stack: Vec<CpsGuardEntry>,
566    return_frames: Vec<CpsReturnFrame>,
567    active_blocked: Vec<CpsBlockedEffect>,
568    initial_frame_count: usize,
569) -> CpsEvalResult<CpsRuntimeValue> {
570    let current_eval_id = fresh_eval_id();
571    resume_continuation(
572        module,
573        function,
574        entry,
575        initial_args,
576        initial_values,
577        into_inherited(active_handlers),
578        guard_stack,
579        return_frames,
580        active_blocked,
581        initial_frame_count,
582        current_eval_id,
583    )
584}
585
586/// Core evaluation loop. Unlike `eval_continuations`, does NOT call
587/// `into_inherited` and does NOT issue a fresh eval id — the caller is
588/// responsible for handler state and for choosing the eval identity. Used
589/// by `continue_return_frames` so restored frames see their handlers with
590/// the original inherited/non-inherited state preserved AND with the
591/// original owner eval id restored as `current_eval_id` (write22).
592fn resume_continuation(
593    module: &CpsModule,
594    function: &CpsFunction,
595    entry: CpsContinuationId,
596    initial_args: Vec<CpsRuntimeValue>,
597    initial_values: Vec<Option<CpsRuntimeValue>>,
598    active_handlers: Vec<CpsHandlerFrame>,
599    guard_stack: Vec<CpsGuardEntry>,
600    return_frames: Vec<CpsReturnFrame>,
601    active_blocked: Vec<CpsBlockedEffect>,
602    initial_frame_count: usize,
603    current_eval_id: CpsEvalId,
604) -> CpsEvalResult<CpsRuntimeValue> {
605    let mut values = initial_values;
606    let mut current = entry;
607    let mut args = initial_args;
608    let mut guard_stack = guard_stack;
609    let mut active_handlers = active_handlers;
610    let mut return_frames = return_frames;
611    let active_blocked = active_blocked;
612    let initial_frame_count = initial_frame_count;
613    let current_eval_id = current_eval_id;
614    let mut next_guard_id = guard_stack
615        .iter()
616        .map(|entry| entry.id)
617        .max()
618        .map_or(0, |id| id + 1);
619    // Loop labels are hygienic across macros; pass the label explicitly.
620    macro_rules! dispatch_scope_return {
621        ($cont:lifetime, $result:expr, $dest:expr) => {{
622            let result = resolve_routed_jump(module, $result, &return_frames)?;
623            if matches!(
624                result,
625                CpsRuntimeValue::Aborted(_) | CpsRuntimeValue::RoutedJump(_)
626            ) {
627                return Ok(result);
628            }
629            match handle_scope_return(
630                result,
631                &mut active_handlers,
632                &return_frames,
633                &function.name,
634                current_eval_id,
635            ) {
636                // Plain value or EXIT-target match: the value belongs to
637                // the call site's `dest` slot, and execution of the rest
638                // of the current continuation continues normally.
639                ScopeReturnAction::Value(v) => write_value(&mut values, *$dest, v),
640                ScopeReturnAction::JumpOrExit { target, value, return_frame_threshold }
641                    if target == EXIT_RWH_TARGET =>
642                {
643                    // Cut frames installed inside the matched handler's scope.
644                    if return_frames.len() > return_frame_threshold {
645                        return_frames.truncate(return_frame_threshold);
646                    }
647                    write_value(&mut values, *$dest, value);
648                }
649                ScopeReturnAction::JumpOrExit { target, value, return_frame_threshold } => {
650                    if return_frames.len() > return_frame_threshold {
651                        return_frames.truncate(return_frame_threshold);
652                    }
653                    current = target;
654                    args = vec![value];
655                    continue $cont;
656                }
657                ScopeReturnAction::Propagate(v) => {
658                    // write25 Step 5: before bubbling the SR via the
659                    // call stack, see if the current eval's own
660                    // return_frames hold the install scope for this
661                    // handler (e.g. a Resume eval's adjusted captured
662                    // chain). If so, route there directly with the
663                    // modified active_handlers snapshot in scope.
664                    if let Some(routed) =
665                        try_route_scope_return_through_return_frames(
666                            module,
667                            &v,
668                            &return_frames,
669                            initial_frame_count,
670                        )?
671                    {
672                        return Ok(routed);
673                    }
674                    return Ok(v);
675                }
676            }
677        }};
678    }
679    'cont: loop {
680        let continuation = continuation_by_id(function, current)?;
681        assign_continuation_args(&mut values, function, continuation, args)?;
682        args = Vec::new();
683
684        for stmt in &continuation.stmts {
685            match stmt {
686                CpsStmt::Literal { dest, literal } => {
687                    write_value(
688                        &mut values,
689                        *dest,
690                        CpsRuntimeValue::Plain(eval_literal(literal)),
691                    );
692                }
693                CpsStmt::FreshGuard { dest, var } => {
694                    let id = next_guard_id;
695                    next_guard_id += 1;
696                    guard_stack.push(CpsGuardEntry { var: *var, id });
697                    write_value(
698                        &mut values,
699                        *dest,
700                        CpsRuntimeValue::Plain(runtime::VmValue::EffectId(id)),
701                    );
702                }
703                CpsStmt::PeekGuard { dest } => {
704                    let id = guard_stack
705                        .last()
706                        .map(|entry| entry.id)
707                        .ok_or(CpsEvalError::MissingGuard)?;
708                    write_value(
709                        &mut values,
710                        *dest,
711                        CpsRuntimeValue::Plain(runtime::VmValue::EffectId(id)),
712                    );
713                }
714                CpsStmt::FindGuard { dest, guard } => {
715                    let guard = read_effect_id(function, &values, *guard)?;
716                    write_value(
717                        &mut values,
718                        *dest,
719                        CpsRuntimeValue::Plain(runtime::VmValue::Bool(
720                            guard_stack.iter().any(|entry| entry.id == guard),
721                        )),
722                    );
723                }
724                CpsStmt::MakeThunk { dest, entry } => {
725                    let thunk_values = values.clone();
726                    write_value(
727                        &mut values,
728                        *dest,
729                        CpsRuntimeValue::Thunk(Rc::new(CpsThunk {
730                            owner_function: function.name.clone(),
731                            entry: *entry,
732                            values: Rc::new(thunk_values),
733                            handlers: Rc::new(active_handlers.clone()),
734                            guard_stack: Rc::new(guard_stack.clone()),
735                            blocked: Rc::new(Vec::new()),
736                        })),
737                    );
738                }
739                CpsStmt::AddThunkBoundary {
740                    dest,
741                    thunk,
742                    guard,
743                    allowed,
744                    active,
745                } => {
746                    let guard = read_effect_id(function, &values, *guard)?;
747                    let value = add_thunk_boundary(
748                        read_value(function, &values, *thunk)?,
749                        guard,
750                        allowed.clone(),
751                        *active,
752                    );
753                    write_value(&mut values, *dest, value);
754                }
755                CpsStmt::MakeClosure { dest, entry } => {
756                    let closure_values = values.clone();
757                    write_value(
758                        &mut values,
759                        *dest,
760                        CpsRuntimeValue::Closure(Rc::new(CpsClosure {
761                            owner_function: function.name.clone(),
762                            entry: *entry,
763                            values: Rc::new(closure_values),
764                            recursive_self: None,
765                        })),
766                    );
767                }
768                CpsStmt::MakeRecursiveClosure { dest, entry } => {
769                    let closure_values = values.clone();
770                    let closure = CpsRuntimeValue::Closure(Rc::new(CpsClosure {
771                        owner_function: function.name.clone(),
772                        entry: *entry,
773                        values: Rc::new(closure_values),
774                        recursive_self: Some(*dest),
775                    }));
776                    write_value(&mut values, *dest, closure);
777                }
778                CpsStmt::ForceThunk { dest, thunk } => {
779                    // Force iteratively: a function whose body builds a
780                    // `MakeThunk` (e.g. `my work(): int = { ... }` with an
781                    // effect-typed return) returns a Thunk wrapping its
782                    // body, and the surrounding lowering may have wrapped
783                    // the call again to defer evaluation past the catch
784                    // boundary. The user-level demand here is "produce the
785                    // catch scope's value", which means peeling Thunks
786                    // until we land on a non-Thunk (or a ScopeReturn that
787                    // dispatches us elsewhere).
788                    let mut result = read_value(function, &values, *thunk)?;
789                    loop {
790                        match result {
791                            CpsRuntimeValue::Thunk(thunk) => {
792                                let handlers = if !active_handlers.is_empty() {
793                                    active_handlers.clone()
794                                } else {
795                                    thunk.handlers.as_ref().clone()
796                                };
797                                let guards = if !guard_stack.is_empty() {
798                                    guard_stack.clone()
799                                } else {
800                                    thunk.guard_stack.as_ref().clone()
801                                };
802                                let owner = function_by_name(module, &thunk.owner_function)?;
803                                // Synchronous force: inherit parent's frames
804                                // so any Perform inside the thunk body
805                                // captures them, but don't consume them on
806                                // the thunk's plain Return (parent's eval is
807                                // still alive).
808                                let inherited = return_frames.len();
809                                result = eval_continuations(
810                                    module,
811                                    owner,
812                                    thunk.entry,
813                                    Vec::new(),
814                                    thunk.values.as_ref().clone(),
815                                    handlers,
816                                    guards,
817                                    return_frames.clone(),
818                                    active_blocked_for_thunk(&active_blocked, &thunk),
819                                    inherited,
820                                )?;
821                                if matches!(result, CpsRuntimeValue::ScopeReturn { .. }) {
822                                    break;
823                                }
824                            }
825                            _ => break,
826                        }
827                    }
828                    dispatch_scope_return!('cont, result, dest);
829                }
830                CpsStmt::Tuple { dest, items } => {
831                    let items = items
832                        .iter()
833                        .map(|id| read_value(function, &values, *id))
834                        .collect::<CpsEvalResult<Vec<_>>>()?;
835                    write_value(&mut values, *dest, CpsRuntimeValue::Tuple(items));
836                }
837                CpsStmt::Record { dest, base, fields } => {
838                    let mut record = match base {
839                        Some(base) => match read_value(function, &values, *base)? {
840                            CpsRuntimeValue::Record(fields) => fields,
841                            CpsRuntimeValue::Plain(runtime::VmValue::Record(fields)) => fields
842                                .into_iter()
843                                .map(|(name, value)| (name, CpsRuntimeValue::Plain(value)))
844                                .collect(),
845                            value => {
846                                return Err(CpsEvalError::ExpectedRecord {
847                                    function: function.name.clone(),
848                                    value: into_plain_value(function, *base, value)?,
849                                });
850                            }
851                        },
852                        None => BTreeMap::new(),
853                    };
854                    for field in fields {
855                        record.insert(
856                            field.name.clone(),
857                            read_value(function, &values, field.value)?,
858                        );
859                    }
860                    write_value(&mut values, *dest, CpsRuntimeValue::Record(record));
861                }
862                CpsStmt::RecordWithoutFields { dest, base, fields } => {
863                    let mut record = match read_value(function, &values, *base)? {
864                        CpsRuntimeValue::Record(fields) => fields,
865                        CpsRuntimeValue::Plain(runtime::VmValue::Record(fields)) => fields
866                            .into_iter()
867                            .map(|(name, value)| (name, CpsRuntimeValue::Plain(value)))
868                            .collect(),
869                        value => {
870                            return Err(CpsEvalError::ExpectedRecord {
871                                function: function.name.clone(),
872                                value: into_plain_value(function, *base, value)?,
873                            });
874                        }
875                    };
876                    for field in fields {
877                        record.remove(field);
878                    }
879                    write_value(&mut values, *dest, CpsRuntimeValue::Record(record));
880                }
881                CpsStmt::Variant { dest, tag, value } => {
882                    let value = value
883                        .map(|id| read_value(function, &values, id))
884                        .transpose()?
885                        .map(Box::new);
886                    write_value(
887                        &mut values,
888                        *dest,
889                        CpsRuntimeValue::Variant {
890                            tag: tag.clone(),
891                            value,
892                        },
893                    );
894                }
895                CpsStmt::Select { dest, base, field } => {
896                    let value = match read_value(function, &values, *base)? {
897                        CpsRuntimeValue::Record(fields) => {
898                            fields.get(field).cloned().ok_or_else(|| {
899                                CpsEvalError::MissingRecordField {
900                                    function: function.name.clone(),
901                                    field: field.clone(),
902                                }
903                            })?
904                        }
905                        CpsRuntimeValue::Plain(runtime::VmValue::Record(fields)) => fields
906                            .get(field)
907                            .cloned()
908                            .map(CpsRuntimeValue::Plain)
909                            .ok_or_else(|| CpsEvalError::MissingRecordField {
910                                function: function.name.clone(),
911                                field: field.clone(),
912                            })?,
913                        value => {
914                            return Err(CpsEvalError::ExpectedRecord {
915                                function: function.name.clone(),
916                                value: into_plain_value(function, *base, value)?,
917                            });
918                        }
919                    };
920                    write_value(&mut values, *dest, value);
921                }
922                CpsStmt::TupleGet { dest, tuple, index } => {
923                    let value = match read_value(function, &values, *tuple)? {
924                        CpsRuntimeValue::Tuple(items) => {
925                            items.get(*index).cloned().ok_or_else(|| {
926                                CpsEvalError::MissingRecordField {
927                                    function: function.name.clone(),
928                                    field: typed_ir::Name(index.to_string()),
929                                }
930                            })?
931                        }
932                        CpsRuntimeValue::Plain(runtime::VmValue::Tuple(items)) => {
933                            cps_value_from_vm(items.get(*index).cloned().ok_or_else(|| {
934                                CpsEvalError::MissingRecordField {
935                                    function: function.name.clone(),
936                                    field: typed_ir::Name(index.to_string()),
937                                }
938                            })?)
939                        }
940                        other => other,
941                    };
942                    write_value(&mut values, *dest, value);
943                }
944                CpsStmt::SelectWithDefault {
945                    dest,
946                    base,
947                    field,
948                    default,
949                } => {
950                    let default = read_value(function, &values, *default)?;
951                    let value = match read_value(function, &values, *base)? {
952                        CpsRuntimeValue::Record(fields) => fields.get(field).cloned(),
953                        CpsRuntimeValue::Plain(runtime::VmValue::Record(fields)) => {
954                            fields.get(field).cloned().map(CpsRuntimeValue::Plain)
955                        }
956                        value => {
957                            return Err(CpsEvalError::ExpectedRecord {
958                                function: function.name.clone(),
959                                value: into_plain_value(function, *base, value)?,
960                            });
961                        }
962                    }
963                    .unwrap_or(default);
964                    write_value(&mut values, *dest, value);
965                }
966                CpsStmt::RecordHasField { dest, base, field } => {
967                    let has_field = match read_value(function, &values, *base)? {
968                        CpsRuntimeValue::Record(fields) => fields.contains_key(field),
969                        CpsRuntimeValue::Plain(runtime::VmValue::Record(fields)) => {
970                            fields.contains_key(field)
971                        }
972                        value => {
973                            return Err(CpsEvalError::ExpectedRecord {
974                                function: function.name.clone(),
975                                value: into_plain_value(function, *base, value)?,
976                            });
977                        }
978                    };
979                    write_value(
980                        &mut values,
981                        *dest,
982                        CpsRuntimeValue::Plain(runtime::VmValue::Bool(has_field)),
983                    );
984                }
985                CpsStmt::VariantTagEq { dest, variant, tag } => {
986                    let matches = match read_value(function, &values, *variant)? {
987                        CpsRuntimeValue::Variant { tag: actual, .. } => actual == *tag,
988                        CpsRuntimeValue::Plain(runtime::VmValue::Variant {
989                            tag: actual, ..
990                        }) => actual == *tag,
991                        _ => false,
992                    };
993                    write_value(
994                        &mut values,
995                        *dest,
996                        CpsRuntimeValue::Plain(runtime::VmValue::Bool(matches)),
997                    );
998                }
999                CpsStmt::VariantPayload { dest, variant } => {
1000                    let value = match read_value(function, &values, *variant)? {
1001                        CpsRuntimeValue::Variant {
1002                            value: Some(value), ..
1003                        } => *value,
1004                        CpsRuntimeValue::Variant { value: None, .. } => {
1005                            CpsRuntimeValue::Plain(runtime::VmValue::Unit)
1006                        }
1007                        CpsRuntimeValue::Plain(runtime::VmValue::Variant {
1008                            value: Some(value),
1009                            ..
1010                        }) => cps_value_from_vm(*value),
1011                        CpsRuntimeValue::Plain(runtime::VmValue::Variant {
1012                            value: None, ..
1013                        }) => CpsRuntimeValue::Plain(runtime::VmValue::Unit),
1014                        other => other,
1015                    };
1016                    write_value(&mut values, *dest, value);
1017                }
1018                CpsStmt::Primitive { dest, op, args } => {
1019                    let arg_values = args
1020                        .iter()
1021                        .map(|id| read_value(function, &values, *id))
1022                        .collect::<CpsEvalResult<Vec<_>>>()?;
1023                    let result = eval_cps_primitive(*op, arg_values.clone())?;
1024                    // Trace list-family primitives: queue manipulation is
1025                    // the central concern for std::undet.once's branch arm.
1026                    if matches!(
1027                        op,
1028                        typed_ir::PrimitiveOp::ListEmpty
1029                            | typed_ir::PrimitiveOp::ListSingleton
1030                            | typed_ir::PrimitiveOp::ListMerge
1031                            | typed_ir::PrimitiveOp::ListLen
1032                            | typed_ir::PrimitiveOp::ListIndex
1033                            | typed_ir::PrimitiveOp::ListIndexRangeRaw
1034                    ) {
1035                        trace_cps(
1036                            "PrimitiveList",
1037                            format!(
1038                                "fn={} cont={:?} op={:?} args=[{}] result={}",
1039                                function.name,
1040                                current,
1041                                op,
1042                                arg_values
1043                                    .iter()
1044                                    .map(summarize_cps_value)
1045                                    .collect::<Vec<_>>()
1046                                    .join(", "),
1047                                summarize_cps_value(&result),
1048                            ),
1049                        );
1050                    }
1051                    write_value(&mut values, *dest, result);
1052                }
1053                CpsStmt::DirectCall {
1054                    dest,
1055                    target,
1056                    args: arg_ids,
1057                } => {
1058                    let target_function = function_by_name(module, target)?;
1059                    let call_args = arg_ids
1060                        .iter()
1061                        .map(|id| read_value(function, &values, *id))
1062                        .collect::<CpsEvalResult<Vec<_>>>()?;
1063                    // Synchronous call: pass parent's return_frames so a
1064                    // Perform inside the callee captures them in the
1065                    // resumption. The callee's initial_frame_count = current
1066                    // return_frames.len() so its Return doesn't consume any
1067                    // (the parent eval is still alive).
1068                    let inherited = return_frames.len();
1069                    let result = eval_function_with_context(
1070                        module,
1071                        target_function,
1072                        call_args,
1073                        active_handlers.clone(),
1074                        guard_stack.clone(),
1075                        return_frames.clone(),
1076                        active_blocked.clone(),
1077                        inherited,
1078                    )?;
1079                    dispatch_scope_return!('cont, result, dest);
1080                }
1081                CpsStmt::ApplyClosure { dest, closure, arg } => {
1082                    // ApplyClosure can target either a Closure or a Resumption.
1083                    // The latter happens when a resumption was stored inside a
1084                    // first-class value (e.g. queue<resumption> from std::undet.once)
1085                    // and later extracted via TupleGet/ListIndex; the surface
1086                    // type system cannot distinguish them so we dispatch here.
1087                    let callable = read_value(function, &values, *closure)?;
1088                    let arg_preview = read_value(function, &values, *arg)
1089                        .ok()
1090                        .as_ref()
1091                        .map(summarize_cps_value)
1092                        .unwrap_or_else(|| "?".to_string());
1093                    trace_cps(
1094                        "ApplyClosure",
1095                        format!(
1096                            "fn={} cont={:?} callable={} arg={} return_frames.len={} initial={}",
1097                            function.name,
1098                            current,
1099                            summarize_cps_value(&callable),
1100                            arg_preview,
1101                            return_frames.len(),
1102                            initial_frame_count,
1103                        ),
1104                    );
1105                    let result = match callable {
1106                        CpsRuntimeValue::Closure(closure) => {
1107                            let arg = read_value(function, &values, *arg)?;
1108                            let owner = function_by_name(module, &closure.owner_function)?;
1109                            let mut closure_values = closure.values.as_ref().clone();
1110                            if let Some(self_id) = closure.recursive_self {
1111                                write_value(
1112                                    &mut closure_values,
1113                                    self_id,
1114                                    CpsRuntimeValue::Closure(closure.clone()),
1115                                );
1116                            }
1117                            // Sync apply: inherit parent's frames so a Perform
1118                            // inside the closure body captures them, but
1119                            // initial_frame_count = current len so the
1120                            // closure's Return doesn't consume them.
1121                            let inherited = return_frames.len();
1122                            eval_continuations(
1123                                module,
1124                                owner,
1125                                closure.entry,
1126                                vec![arg],
1127                                closure_values,
1128                                active_handlers.clone(),
1129                                guard_stack.clone(),
1130                                return_frames.clone(),
1131                                active_blocked.clone(),
1132                                inherited,
1133                            )?
1134                        }
1135                        CpsRuntimeValue::Resumption(resumption) => {
1136                            // Treat as Resume: the surface saw an opaque
1137                            // callable, but the runtime value is a captured
1138                            // continuation. Resume needs a plain payload.
1139                            let arg = read_plain_value(function, &values, *arg)?;
1140                            let owner = function_by_name(module, &resumption.owner_function)?;
1141                            let anchor = resumption.handled_anchor;
1142                            let resumed_handlers = merge_resumption_handlers(
1143                                resumption.handlers.as_ref(),
1144                                &active_handlers,
1145                                anchor,
1146                            );
1147                            let adjusted_frames = merge_extras_into_frames(
1148                                resumption.return_frames.as_ref(),
1149                                &active_handlers,
1150                                anchor,
1151                            );
1152                            trace_cps(
1153                                "ResumeHandlerMerge",
1154                                format!(
1155                                    "site=ApplyClosure(Resumption) fn={} eval={} anchor={:?} captured={} current={} merged={}",
1156                                    function.name,
1157                                    current_eval_id.0,
1158                                    anchor.map(|a| (a.prompt.0, a.install_eval_id.0)),
1159                                    summarize_handler_stack(resumption.handlers.as_ref()),
1160                                    summarize_handler_stack(&active_handlers),
1161                                    summarize_handler_stack(&resumed_handlers),
1162                                ),
1163                            );
1164                            // Resume replays a captured continuation, so the
1165                            // captured frames are ours to consume — initial=0.
1166                            eval_continuations(
1167                                module,
1168                                owner,
1169                                resumption.target,
1170                                vec![CpsRuntimeValue::Plain(arg)],
1171                                resumption.values.as_ref().clone(),
1172                                resumed_handlers,
1173                                resumption.guard_stack.as_ref().clone(),
1174                                adjusted_frames,
1175                                resumption.active_blocked.as_ref().clone(),
1176                                0,
1177                            )?
1178                        }
1179                        _ => {
1180                            return Err(CpsEvalError::ExpectedPlainValue {
1181                                function: function.name.clone(),
1182                                id: *closure,
1183                            });
1184                        }
1185                    };
1186                    dispatch_scope_return!('cont, result, dest);
1187                }
1188                CpsStmt::CloneContinuation { dest, source } => {
1189                    let value = read_value(function, &values, *source)?;
1190                    write_value(&mut values, *dest, value);
1191                }
1192                CpsStmt::Resume {
1193                    dest,
1194                    resumption,
1195                    arg,
1196                } => {
1197                    let resumption = read_resumption(function, &values, *resumption)?;
1198                    let arg = read_plain_value(function, &values, *arg)?;
1199                    let owner = function_by_name(module, &resumption.owner_function)?;
1200                    let anchor = resumption.handled_anchor;
1201                    let resumed_handlers = merge_resumption_handlers(
1202                        resumption.handlers.as_ref(),
1203                        &active_handlers,
1204                        anchor,
1205                    );
1206                    let adjusted_frames = merge_extras_into_frames(
1207                        resumption.return_frames.as_ref(),
1208                        &active_handlers,
1209                        anchor,
1210                    );
1211                    trace_cps(
1212                        "ResumeHandlerMerge",
1213                        format!(
1214                            "site=Resume fn={} eval={} anchor={:?} captured={} current={} merged={}",
1215                            function.name,
1216                            current_eval_id.0,
1217                            anchor.map(|a| (a.prompt.0, a.install_eval_id.0)),
1218                            summarize_handler_stack(resumption.handlers.as_ref()),
1219                            summarize_handler_stack(&active_handlers),
1220                            summarize_handler_stack(&resumed_handlers),
1221                        ),
1222                    );
1223                    // Resume replays captured continuation; captured frames
1224                    // are ours to consume.
1225                    let result = eval_continuations(
1226                        module,
1227                        owner,
1228                        resumption.target,
1229                        vec![CpsRuntimeValue::Plain(arg)],
1230                        resumption.values.as_ref().clone(),
1231                        resumed_handlers,
1232                        resumption.guard_stack.as_ref().clone(),
1233                        adjusted_frames,
1234                        resumption.active_blocked.as_ref().clone(),
1235                        0,
1236                    )?;
1237                    dispatch_scope_return!('cont, result, dest);
1238                }
1239                CpsStmt::InstallHandler {
1240                    handler,
1241                    envs,
1242                    value,
1243                    escape,
1244                } => {
1245                    let envs = capture_handler_envs(function, &values, envs)?;
1246                    let pushed_prompt = fresh_prompt();
1247                    let threshold = return_frames.len();
1248                    active_handlers.push(CpsHandlerFrame {
1249                        prompt: pushed_prompt,
1250                        handler: *handler,
1251                        guard_stack: guard_stack.clone(),
1252                        envs: envs.clone(),
1253                        escape: *escape,
1254                        escape_owner_function: function.name.clone(),
1255                        inherited: false,
1256                        return_frame_threshold: threshold,
1257                        install_eval_id: current_eval_id,
1258                    });
1259                    return_frames.push(CpsReturnFrame {
1260                        prompt_exit: Some(CpsPromptExitFrame {
1261                            prompt: pushed_prompt,
1262                        }),
1263                        owner_function: function.name.clone(),
1264                        continuation: *value,
1265                        values: Rc::new(values.clone()),
1266                        active_handlers: active_handlers.clone(),
1267                        guard_stack: guard_stack.clone(),
1268                        active_blocked: active_blocked.clone(),
1269                        owner_initial_frame_count: initial_frame_count,
1270                        owner_eval_id: current_eval_id,
1271                    });
1272                    trace_cps(
1273                        "InstallHandler",
1274                        format!(
1275                            "fn={} eval={} cont={:?} handler={:?} prompt={} value={:?} escape={:?} threshold={} handlers.now={}",
1276                            function.name,
1277                            current_eval_id.0,
1278                            current,
1279                            handler,
1280                            pushed_prompt.0,
1281                            value,
1282                            escape,
1283                            threshold,
1284                            active_handlers.len(),
1285                        ),
1286                    );
1287                }
1288                CpsStmt::UninstallHandler { handler } => {
1289                    if let Some(pos) = active_handlers
1290                        .iter()
1291                        .rposition(|frame| frame.handler == *handler)
1292                    {
1293                        active_handlers.remove(pos);
1294                    }
1295                }
1296                CpsStmt::ResumeWithHandler {
1297                    dest,
1298                    resumption,
1299                    arg,
1300                    handler,
1301                    envs,
1302                } => {
1303                    let resumption = read_resumption(function, &values, *resumption)?;
1304                    let arg = read_plain_value(function, &values, *arg)?;
1305                    let updates_existing_handler_env =
1306                        envs.iter().any(|env| !env.targets.is_empty());
1307                    let envs = capture_handler_envs(function, &values, envs)?;
1308                    let owner = function_by_name(module, &resumption.owner_function)?;
1309                    let rebase_existing_handler_env = updates_existing_handler_env
1310                        && resumption
1311                            .handlers
1312                            .iter()
1313                            .any(|frame| frame.handler == *handler);
1314                    overlay_handler_envs_in_stack(
1315                        &function.name,
1316                        &mut active_handlers,
1317                        *handler,
1318                        &envs,
1319                        true,
1320                    );
1321                    // Push the RWH-installed frame onto our own active_handlers
1322                    // (non-inherited, sentinel-target) so a ScopeReturn that
1323                    // matches the freshly-installed handler resolves at this
1324                    // very call site rather than escaping the eval frame.
1325                    let pushed_prompt = fresh_prompt();
1326                    if !rebase_existing_handler_env {
1327                        active_handlers.push(CpsHandlerFrame {
1328                            prompt: pushed_prompt,
1329                            handler: *handler,
1330                            guard_stack: guard_stack.clone(),
1331                            envs: envs.clone(),
1332                            escape: EXIT_RWH_TARGET,
1333                            escape_owner_function: function.name.clone(),
1334                            inherited: false,
1335                            return_frame_threshold: return_frames.len(),
1336                            install_eval_id: current_eval_id,
1337                        });
1338                    }
1339                    // RWH uses REBASED semantics: the just-installed handler
1340                    // SHADOWS the captured innermost handler. So inner_handlers
1341                    // = captured ++ [RWH-pushed], with RWH innermost. This is
1342                    // different from regular Resume merge (which preserves
1343                    // captured innermost). Tested by
1344                    // `evaluates_resumption_under_fresh_handler_stack`.
1345                    let inner_handlers = {
1346                        let mut stack = resumption.handlers.as_ref().clone();
1347                        overlay_handler_envs_in_stack(
1348                            &function.name,
1349                            &mut stack,
1350                            *handler,
1351                            &envs,
1352                            false,
1353                        );
1354                        if !rebase_existing_handler_env {
1355                            let mut owned = active_handlers
1356                                .last()
1357                                .cloned()
1358                                .expect("just pushed RWH frame");
1359                            owned.inherited = true;
1360                            stack.push(owned);
1361                        }
1362                        stack
1363                    };
1364                    let pushed_extra = if rebase_existing_handler_env {
1365                        Vec::new()
1366                    } else {
1367                        active_handlers
1368                            .iter()
1369                            .filter(|f| f.prompt == pushed_prompt)
1370                            .cloned()
1371                            .collect::<Vec<_>>()
1372                    };
1373                    // Frame continuations use the same rebased stack: append
1374                    // the RWH frame as the innermost handler for every captured
1375                    // return frame.
1376                    let mut captured_frames = resumption.return_frames.as_ref().clone();
1377                    overlay_handler_envs_in_frames(
1378                        &function.name,
1379                        &mut captured_frames,
1380                        *handler,
1381                        &envs,
1382                        false,
1383                    );
1384                    let adjusted_frames =
1385                        append_resume_with_handler_frames(&captured_frames, &pushed_extra);
1386                    let adjusted_frames = if rebase_existing_handler_env {
1387                        own_captured_return_frames(adjusted_frames)
1388                    } else {
1389                        adjusted_frames
1390                    };
1391                    trace_cps(
1392                        "ResumeHandlerMerge",
1393                        format!(
1394                            "site=ResumeWithHandler(rebased) fn={} eval={} pushed_prompt={} captured={} pushed_extra={} inner={}",
1395                            function.name,
1396                            current_eval_id.0,
1397                            pushed_prompt.0,
1398                            summarize_handler_stack(resumption.handlers.as_ref()),
1399                            summarize_handler_stack(&pushed_extra),
1400                            summarize_handler_stack(&inner_handlers),
1401                        ),
1402                    );
1403                    let result = eval_continuations(
1404                        module,
1405                        owner,
1406                        resumption.target,
1407                        vec![CpsRuntimeValue::Plain(arg)],
1408                        resumption.values.as_ref().clone(),
1409                        inner_handlers,
1410                        resumption.guard_stack.as_ref().clone(),
1411                        adjusted_frames,
1412                        resumption.active_blocked.as_ref().clone(),
1413                        0,
1414                    )?;
1415                    dispatch_scope_return!('cont, result, dest);
1416                    // Pop the RWH frame in the value-flow path. JumpOrExit /
1417                    // Propagate paths do not return to here, so they don't
1418                    // need it; an `EXIT_RWH_TARGET` match will already have
1419                    // truncated past this frame in `handle_scope_return`.
1420                    if let Some(pos) = active_handlers
1421                        .iter()
1422                        .rposition(|f| f.prompt == pushed_prompt)
1423                    {
1424                        active_handlers.truncate(pos);
1425                    }
1426                }
1427            }
1428        }
1429
1430        match &continuation.terminator {
1431            CpsTerminator::Return(value) => {
1432                // Only consume frames pushed during THIS eval. The prefix up to
1433                // `initial_frame_count` was inherited from a sync caller and
1434                // belongs to that caller's eval loop, which is still alive.
1435                let v = read_value(function, &values, *value)?;
1436                trace_cps(
1437                    "Return",
1438                    format!(
1439                        "fn={} cont={:?} value={} return_frames.len={} initial={}",
1440                        function.name,
1441                        current,
1442                        summarize_cps_value(&v),
1443                        return_frames.len(),
1444                        initial_frame_count,
1445                    ),
1446                );
1447                if return_frames.len() <= initial_frame_count {
1448                    return Ok(v);
1449                }
1450                // write25 Step 5+: re-enable write17-style pre-force.
1451                // If the Returned value is a Thunk and the top own-frame's
1452                // continuation immediately ForceThunks its param, run that
1453                // continuation in the top frame's owner context with the
1454                // top frame STILL in `return_frames`. This keeps frames
1455                // like `F_each_post` in scope while the deferred body
1456                // runs, so a deep `Perform` captures them in the
1457                // resumption. Combined with write22's `install_eval_id`
1458                // and write25's walk-based routing, the captured
1459                // `F_each_post` lets `try_route_scope_return_through_return_frames`
1460                // resolve `H_sub` at its install eval directly from the
1461                // captured chain, instead of dropping it via call-stack
1462                // propagation.
1463                //
1464                // Owner check (write17's reason for being disabled):
1465                // running the top frame's continuation under the frame's
1466                // owner function avoids the previous mismatch where the
1467                // resumed body used `current_function=fold_impl` while
1468                // looking for `H_sub.escape_owner=each`. Now we run in
1469                // `frame.owner_function=each`, so the strict eval-id
1470                // check (write22) succeeds.
1471                if let CpsRuntimeValue::Thunk(thunk) = &v {
1472                    let top_index = return_frames.len() - 1;
1473                    let top_frame = &return_frames[top_index];
1474                    if return_frame_immediately_forces_param(module, top_frame)?
1475                        && top_index >= initial_frame_count
1476                    {
1477                        let top_frame = top_frame.clone();
1478                        trace_cps(
1479                            "PreForceResumeTopFrame",
1480                            format!(
1481                                "top_owner={} top_owner_eval={} top_cont={:?} retained_frames_len={}",
1482                                top_frame.owner_function,
1483                                top_frame.owner_eval_id.0,
1484                                top_frame.continuation,
1485                                return_frames.len(),
1486                            ),
1487                        );
1488                        let forced = force_returned_thunk_before_frame_consumption(
1489                            module,
1490                            thunk.clone(),
1491                            &top_frame,
1492                            return_frames.clone(),
1493                            initial_frame_count,
1494                        )?;
1495                        if matches!(forced, CpsRuntimeValue::ScopeReturn { .. }) {
1496                            return Ok(forced);
1497                        }
1498                        return continue_return_frames(module, forced, &return_frames, &[]);
1499                    }
1500                }
1501                // write23: pass the FULL return_frames (not just own_frames)
1502                // so continue_return_frames can preserve the inherited prefix
1503                // for the resumed eval. Previously, splitting off own_frames
1504                // dropped the inherited part, which silently lost the
1505                // parent's `F_each_post`-style frames in tail-call chains
1506                // (where the parent has already terminated via EffectfulCall
1507                // and its frames live only in the callee's return_frames).
1508                return continue_return_frames(module, v, &return_frames, &[]);
1509            }
1510            CpsTerminator::Continue { target, args: next } => {
1511                args = next
1512                    .iter()
1513                    .map(|id| read_value(function, &values, *id))
1514                    .collect::<CpsEvalResult<Vec<_>>>()?;
1515                current = *target;
1516            }
1517            CpsTerminator::Branch {
1518                cond,
1519                then_cont,
1520                else_cont,
1521            } => {
1522                let cond = read_plain_value(function, &values, *cond)?;
1523                current = if bool_value(typed_ir::PrimitiveOp::BoolNot, &cond)? {
1524                    *then_cont
1525                } else {
1526                    *else_cont
1527                };
1528            }
1529            CpsTerminator::Perform {
1530                effect,
1531                payload,
1532                resume,
1533                handler,
1534                blocked,
1535            } => {
1536                trace_cps(
1537                    "Perform",
1538                    format!(
1539                        "fn={} eval={} cont={:?} effect={:?} return_frames.len={} initial={} active_handlers={}",
1540                        function.name,
1541                        current_eval_id.0,
1542                        current,
1543                        effect,
1544                        return_frames.len(),
1545                        initial_frame_count,
1546                        summarize_handler_stack(&active_handlers),
1547                    ),
1548                );
1549                let payload = read_plain_value(function, &values, *payload)?;
1550                let blocked = blocked
1551                    .map(|blocked| read_effect_id(function, &values, blocked))
1552                    .transpose()?
1553                    .or_else(|| active_blocked_id(effect, &active_blocked));
1554                let handler_stack =
1555                    handler_stack_with_static(&active_handlers, *handler, &guard_stack);
1556                let (handler_arm, frame, handler_body_stack, handler_owner) =
1557                    handler_arm_for_stack(module, function, &handler_stack, effect, blocked)?;
1558                let handler_values = values_with_handler_env(
1559                    &handler_owner.name,
1560                    Vec::new(),
1561                    frame,
1562                    handler_arm.entry,
1563                );
1564                let frame_prompt = frame.prompt;
1565                let frame_escape = frame.escape;
1566                let frame_in_active = active_handlers.iter().any(|f| f.prompt == frame_prompt);
1567                // write24: record which handler frame's arm produced this
1568                // resumption. When the resumption is later resumed, merge
1569                // uses this as the anchor to place resume-site siblings
1570                // immediately after the captured handler.
1571                let handled_anchor = if frame_in_active {
1572                    Some(CpsHandlerAnchor {
1573                        prompt: frame.prompt,
1574                        install_eval_id: frame.install_eval_id,
1575                    })
1576                } else {
1577                    None
1578                };
1579                if trace_enabled() {
1580                    let matched_index = handler_stack.iter().position(|f| f.prompt == frame_prompt);
1581                    trace_cps(
1582                        "PerformHandlerSearch",
1583                        format!(
1584                            "fn={} eval={} effect={:?} stack={} matched_index={:?} matched_prompt={} matched_install_eval={} matched_owner={} in_active={}",
1585                            function.name,
1586                            current_eval_id.0,
1587                            effect,
1588                            summarize_handler_stack(&handler_stack),
1589                            matched_index,
1590                            frame.prompt.0,
1591                            frame.install_eval_id.0,
1592                            if frame.escape_owner_function.is_empty() {
1593                                "<synth>"
1594                            } else {
1595                                frame.escape_owner_function.as_str()
1596                            },
1597                            frame_in_active,
1598                        ),
1599                    );
1600                }
1601                let (resumption_handlers, resumption_return_frames) = if frame_in_active {
1602                    let captured =
1603                        capture_continuation_inside_prompt(&handler_stack, &return_frames, frame);
1604                    (captured.handlers, captured.return_frames)
1605                } else {
1606                    (handler_stack.clone(), return_frames.clone())
1607                };
1608                let resumption = CpsRuntimeValue::Resumption(Rc::new(CpsResumption {
1609                    owner_function: function.name.clone(),
1610                    target: *resume,
1611                    values: Rc::new(values.clone()),
1612                    handlers: Rc::new(resumption_handlers),
1613                    guard_stack: Rc::new(guard_stack.clone()),
1614                    active_blocked: Rc::new(active_blocked.clone()),
1615                    return_frames: Rc::new(resumption_return_frames),
1616                    handled_anchor,
1617                }));
1618                // Detect whether the chosen handler frame is in our local
1619                // `active_handlers` (so its prompt will match on dispatch)
1620                // or whether it was synthesized by `handler_stack_with_static`
1621                // because we had no installed handlers at all. In the
1622                // synthetic case the arm's result must just become the
1623                // perform-frame's return value, with no ScopeReturn wrapping.
1624                let result = eval_continuations(
1625                    module,
1626                    handler_owner,
1627                    handler_arm.entry,
1628                    vec![CpsRuntimeValue::Plain(payload), resumption],
1629                    handler_values,
1630                    handler_body_stack,
1631                    guard_stack.clone(),
1632                    Vec::new(),
1633                    active_blocked.clone(),
1634                    0,
1635                )?;
1636                if !frame_in_active {
1637                    // Synthetic fallback frame: the perform's effect had no
1638                    // installed handler in this eval, so the arm's result is
1639                    // the value of *this* eval frame.
1640                    return Ok(result);
1641                }
1642                let arm_already_reached_escape = handler_arm_continues_to_installed_escape(
1643                    handler_owner,
1644                    frame.handler,
1645                    handler_arm.entry,
1646                    frame_escape,
1647                );
1648                if arm_already_reached_escape
1649                    && !matches!(result, CpsRuntimeValue::ScopeReturn { .. })
1650                    && frame.install_eval_id == current_eval_id
1651                {
1652                    let mut frames = return_frames.clone();
1653                    if frames.len() > frame.return_frame_threshold {
1654                        frames.truncate(frame.return_frame_threshold);
1655                    }
1656                    return continue_return_frames(module, result, &frames, &[]);
1657                }
1658                if !matches!(result, CpsRuntimeValue::ScopeReturn { .. })
1659                    && frame.install_eval_id != current_eval_id
1660                    && handler_arm_uses_resume_with_handler(
1661                        handler_owner,
1662                        frame.handler,
1663                        handler_arm.entry,
1664                    )
1665                {
1666                    return Ok(result);
1667                }
1668                // The arm body's natural Return becomes a non-local jump to
1669                // the matching handler scope. Don't re-wrap if the arm itself
1670                // emitted a ScopeReturn (it might be targeting an outer scope).
1671                let scope_return = match result {
1672                    CpsRuntimeValue::ScopeReturn { .. } => result,
1673                    other => CpsRuntimeValue::ScopeReturn {
1674                        prompt: frame_prompt,
1675                        target: frame_escape,
1676                        value: Box::new(other),
1677                    },
1678                };
1679                // The InstallHandler that matches this perform might be in
1680                // the *current* function (e.g. `catch x: branch -> ...` and
1681                // the perform of `branch()` is in the same function). In
1682                // that case the prompt is in our `active_handlers` and we
1683                // must resolve here rather than bubble out — otherwise the
1684                // ScopeReturn escapes to the root.
1685                match handle_scope_return(
1686                    scope_return,
1687                    &mut active_handlers,
1688                    &return_frames,
1689                    &function.name,
1690                    current_eval_id,
1691                ) {
1692                    ScopeReturnAction::Value(v) => {
1693                        // Shouldn't happen — we just constructed a ScopeReturn.
1694                        return Ok(v);
1695                    }
1696                    ScopeReturnAction::Propagate(v) => {
1697                        // write25 Step 5: walk own return_frames for an
1698                        // install-scope match before bubbling up.
1699                        if let Some(routed) = try_route_scope_return_through_return_frames(
1700                            module,
1701                            &v,
1702                            &return_frames,
1703                            initial_frame_count,
1704                        )? {
1705                            return Ok(routed);
1706                        }
1707                        return Ok(v);
1708                    }
1709                    ScopeReturnAction::JumpOrExit {
1710                        target,
1711                        value,
1712                        return_frame_threshold,
1713                    } => {
1714                        if return_frames.len() > return_frame_threshold {
1715                            return_frames.truncate(return_frame_threshold);
1716                        }
1717                        if target == EXIT_RWH_TARGET {
1718                            return Ok(value);
1719                        }
1720                        current = target;
1721                        args = vec![value];
1722                        continue 'cont;
1723                    }
1724                }
1725            }
1726            CpsTerminator::EffectfulCall {
1727                target,
1728                args: arg_ids,
1729                resume,
1730            } => {
1731                // Effectful direct call: this eval frame terminates here.
1732                // Push a return frame so the callee's Perform (or any further
1733                // effectful call) captures this function's post-call cont in
1734                // its resumption. The callee's `initial_frame_count` is the
1735                // pre-push length so it can consume the frame we just pushed
1736                // (and any further frames it pushes itself), but NOT the
1737                // frames we inherited from above (those remain ours-to-keep
1738                // until we ourselves are restored via continue_return_frames).
1739                let target_function = function_by_name(module, target)?;
1740                let call_args = arg_ids
1741                    .iter()
1742                    .map(|id| read_value(function, &values, *id))
1743                    .collect::<CpsEvalResult<Vec<_>>>()?;
1744                let pre_push_count = return_frames.len();
1745                let frame = CpsReturnFrame {
1746                    prompt_exit: None,
1747                    owner_function: function.name.clone(),
1748                    continuation: *resume,
1749                    values: Rc::new(values.clone()),
1750                    active_handlers: active_handlers.clone(),
1751                    guard_stack: guard_stack.clone(),
1752                    active_blocked: active_blocked.clone(),
1753                    owner_initial_frame_count: initial_frame_count,
1754                    owner_eval_id: current_eval_id,
1755                };
1756                let mut new_frames = return_frames.clone();
1757                new_frames.push(frame);
1758                return eval_function_with_context(
1759                    module,
1760                    target_function,
1761                    call_args,
1762                    active_handlers.clone(),
1763                    guard_stack.clone(),
1764                    new_frames,
1765                    active_blocked.clone(),
1766                    pre_push_count,
1767                );
1768            }
1769            CpsTerminator::EffectfulForce { thunk, resume } => {
1770                // Effectful thunk force: this eval frame terminates here.
1771                // The thunk's body runs as if called via EffectfulCall, with
1772                // the post-force cont captured as a return frame.
1773                let value = read_value(function, &values, *thunk)?;
1774                match value {
1775                    CpsRuntimeValue::Thunk(thunk_rc) => {
1776                        let pre_push_count = return_frames.len();
1777                        let frame = CpsReturnFrame {
1778                            prompt_exit: None,
1779                            owner_function: function.name.clone(),
1780                            continuation: *resume,
1781                            values: Rc::new(values.clone()),
1782                            active_handlers: active_handlers.clone(),
1783                            guard_stack: guard_stack.clone(),
1784                            active_blocked: active_blocked.clone(),
1785                            owner_initial_frame_count: initial_frame_count,
1786                            owner_eval_id: current_eval_id,
1787                        };
1788                        let mut new_frames = return_frames.clone();
1789                        new_frames.push(frame);
1790                        let owner = function_by_name(module, &thunk_rc.owner_function)?;
1791                        let handlers = if !active_handlers.is_empty() {
1792                            active_handlers.clone()
1793                        } else {
1794                            thunk_rc.handlers.as_ref().clone()
1795                        };
1796                        let guards = if !guard_stack.is_empty() {
1797                            guard_stack.clone()
1798                        } else {
1799                            thunk_rc.guard_stack.as_ref().clone()
1800                        };
1801                        return eval_continuations(
1802                            module,
1803                            owner,
1804                            thunk_rc.entry,
1805                            Vec::new(),
1806                            thunk_rc.values.as_ref().clone(),
1807                            handlers,
1808                            guards,
1809                            new_frames,
1810                            active_blocked_for_thunk(&active_blocked, &thunk_rc),
1811                            pre_push_count,
1812                        );
1813                    }
1814                    other => {
1815                        // Non-thunk value: no force needed. Pass directly to
1816                        // the resume continuation as if EffectfulForce was a
1817                        // no-op.
1818                        current = *resume;
1819                        args = vec![other];
1820                        continue 'cont;
1821                    }
1822                }
1823            }
1824            CpsTerminator::EffectfulApply {
1825                closure,
1826                arg,
1827                resume,
1828            } => {
1829                // Effectful closure application: same shape as EffectfulCall
1830                // but dispatches on Closure or Resumption value.
1831                let callable = read_value(function, &values, *closure)?;
1832                let arg_preview = read_value(function, &values, *arg)
1833                    .ok()
1834                    .as_ref()
1835                    .map(summarize_cps_value)
1836                    .unwrap_or_else(|| "?".to_string());
1837                trace_cps(
1838                    "EffectfulApply",
1839                    format!(
1840                        "fn={} cont={:?} callable={} arg={} resume={:?} return_frames.len={} initial={}",
1841                        function.name,
1842                        current,
1843                        summarize_cps_value(&callable),
1844                        arg_preview,
1845                        resume,
1846                        return_frames.len(),
1847                        initial_frame_count,
1848                    ),
1849                );
1850                let pre_push_count = return_frames.len();
1851                let frame = CpsReturnFrame {
1852                    prompt_exit: None,
1853                    owner_function: function.name.clone(),
1854                    continuation: *resume,
1855                    values: Rc::new(values.clone()),
1856                    active_handlers: active_handlers.clone(),
1857                    guard_stack: guard_stack.clone(),
1858                    active_blocked: active_blocked.clone(),
1859                    owner_initial_frame_count: initial_frame_count,
1860                    owner_eval_id: current_eval_id,
1861                };
1862                let mut new_frames = return_frames.clone();
1863                new_frames.push(frame);
1864                match callable {
1865                    CpsRuntimeValue::Closure(closure) => {
1866                        let arg = read_value(function, &values, *arg)?;
1867                        let owner = function_by_name(module, &closure.owner_function)?;
1868                        let mut closure_values = closure.values.as_ref().clone();
1869                        if let Some(self_id) = closure.recursive_self {
1870                            write_value(
1871                                &mut closure_values,
1872                                self_id,
1873                                CpsRuntimeValue::Closure(closure.clone()),
1874                            );
1875                        }
1876                        return eval_continuations(
1877                            module,
1878                            owner,
1879                            closure.entry,
1880                            vec![arg],
1881                            closure_values,
1882                            active_handlers.clone(),
1883                            guard_stack.clone(),
1884                            new_frames,
1885                            active_blocked.clone(),
1886                            pre_push_count,
1887                        );
1888                    }
1889                    CpsRuntimeValue::Resumption(resumption) => {
1890                        // EffectfulApply to a Resumption — replay the
1891                        // captured continuation, then run the post-apply
1892                        // cont after it returns. write16 §5: frame order
1893                        // matters. continue_return_frames pops from end
1894                        // (innermost first), so:
1895                        //   [parent_frames..., F_post, res_frames...]
1896                        // ensures consumption is:
1897                        //   innermost res frame → outermost res frame →
1898                        //   F_post → innermost parent → outermost parent.
1899                        let arg = read_plain_value(function, &values, *arg)?;
1900                        let owner = function_by_name(module, &resumption.owner_function)?;
1901                        let anchor = resumption.handled_anchor;
1902                        let resumed_handlers = merge_resumption_handlers(
1903                            resumption.handlers.as_ref(),
1904                            &active_handlers,
1905                            anchor,
1906                        );
1907                        let adjusted_res = merge_extras_into_frames(
1908                            resumption.return_frames.as_ref(),
1909                            &active_handlers,
1910                            anchor,
1911                        );
1912                        trace_cps(
1913                            "ResumeHandlerMerge",
1914                            format!(
1915                                "site=EffectfulApply(Resumption) fn={} eval={} anchor={:?} captured={} current={} merged={}",
1916                                function.name,
1917                                current_eval_id.0,
1918                                anchor.map(|a| (a.prompt.0, a.install_eval_id.0)),
1919                                summarize_handler_stack(resumption.handlers.as_ref()),
1920                                summarize_handler_stack(&active_handlers),
1921                                summarize_handler_stack(&resumed_handlers),
1922                            ),
1923                        );
1924                        // new_frames = parent_frames + [F_post]. Append
1925                        // adjusted_res AFTER so it pops first.
1926                        let mut combined_frames = new_frames;
1927                        combined_frames.extend(adjusted_res);
1928                        // Resume-style replay: captured frames are ours to
1929                        // consume — initial=0.
1930                        return eval_continuations(
1931                            module,
1932                            owner,
1933                            resumption.target,
1934                            vec![CpsRuntimeValue::Plain(arg)],
1935                            resumption.values.as_ref().clone(),
1936                            resumed_handlers,
1937                            resumption.guard_stack.as_ref().clone(),
1938                            combined_frames,
1939                            resumption.active_blocked.as_ref().clone(),
1940                            0,
1941                        );
1942                    }
1943                    _ => {
1944                        return Err(CpsEvalError::ExpectedPlainValue {
1945                            function: function.name.clone(),
1946                            id: *closure,
1947                        });
1948                    }
1949                }
1950            }
1951        }
1952    }
1953}
1954
1955/// write17: returns true when the frame's continuation immediately
1956/// ForceThunks its received parameter — i.e. its first stmt is
1957/// `ForceThunk { thunk: <params[0]>, .. }`. Used by `Return(Thunk)` to
1958/// detect the case where popping the frame would lose the post-call cont
1959/// from a thunk body's resumption.
1960fn return_frame_immediately_forces_param(
1961    module: &CpsModule,
1962    frame: &CpsReturnFrame,
1963) -> CpsEvalResult<bool> {
1964    let function = function_by_name(module, &frame.owner_function)?;
1965    let Some(continuation) = function
1966        .continuations
1967        .iter()
1968        .find(|c| c.id == frame.continuation)
1969    else {
1970        return Ok(false);
1971    };
1972    let Some(&first_param) = continuation.params.first() else {
1973        return Ok(false);
1974    };
1975    Ok(matches!(
1976        continuation.stmts.first(),
1977        Some(CpsStmt::ForceThunk { thunk, .. }) if *thunk == first_param
1978    ))
1979}
1980
1981/// write17: Evaluate a Thunk body BEFORE popping the top return frame,
1982/// so the body's effects can capture the frame in their resumptions.
1983///
1984/// Critical invariants:
1985/// - `return_frames` is passed AS-IS (no pop). The top frame stays available
1986///   so the thunk body's Perform sees it as captureable.
1987/// - `initial_frame_count` is the CURRENT eval's initial — NOT
1988///   `return_frames.len()`. Setting it to `len()` would make the top frame
1989///   inherited from the thunk body's POV and prevent it from ever being
1990///   consumed.
1991/// - handlers / guards come from `top_frame` (the would-be ForceThunk
1992///   context), since we're pre-empting that context.
1993/// - If the thunk body itself returns a Thunk, peel via loop (mirrors the
1994///   sync stmt-level ForceThunk's behavior).
1995#[allow(dead_code)]
1996fn force_returned_thunk_before_frame_consumption(
1997    module: &CpsModule,
1998    mut thunk: Rc<CpsThunk>,
1999    top_frame: &CpsReturnFrame,
2000    return_frames: Vec<CpsReturnFrame>,
2001    initial_frame_count: usize,
2002) -> CpsEvalResult<CpsRuntimeValue> {
2003    loop {
2004        let owner = function_by_name(module, &thunk.owner_function)?;
2005        // Use top_frame's saved handlers (preserving non-inherited state) and
2006        // resume_continuation (no into_inherited). This way an effect handler
2007        // installed at the EffectfulCall site (e.g. H_sub in each) remains
2008        // non-inherited in the body's eval, so its ScopeReturn can resolve
2009        // when propagating back here instead of escaping to root.
2010        let result = resume_continuation(
2011            module,
2012            owner,
2013            thunk.entry,
2014            Vec::new(),
2015            thunk.values.as_ref().clone(),
2016            top_frame.active_handlers.clone(),
2017            top_frame.guard_stack.clone(),
2018            return_frames.clone(),
2019            active_blocked_for_thunk(&top_frame.active_blocked, &thunk),
2020            initial_frame_count,
2021            // Restore the eval id of the would-be ForceThunk context so the
2022            // body's ScopeReturn resolves at the installer's eval.
2023            top_frame.owner_eval_id,
2024        )?;
2025        match result {
2026            CpsRuntimeValue::Thunk(next) => {
2027                thunk = next;
2028                continue;
2029            }
2030            other => return Ok(other),
2031        }
2032    }
2033}
2034
2035/// Compact trace summary of a handler stack. Shows the key fields used by
2036/// ScopeReturn resolution so the trace audit can confirm merge order.
2037fn summarize_handler_stack(stack: &[CpsHandlerFrame]) -> String {
2038    let parts: Vec<String> = stack
2039        .iter()
2040        .map(|h| {
2041            format!(
2042                "(p={},inh={},eval={},owner={},thr={})",
2043                h.prompt.0,
2044                if h.inherited { "T" } else { "F" },
2045                h.install_eval_id.0,
2046                if h.escape_owner_function.is_empty() {
2047                    "<synth>"
2048                } else {
2049                    h.escape_owner_function.as_str()
2050                },
2051                h.return_frame_threshold,
2052            )
2053        })
2054        .collect();
2055    format!("[{}]", parts.join(","))
2056}
2057
2058/// Are two handler frames the "same" handler — i.e. same install instance?
2059fn same_handler_frame(a: &CpsHandlerFrame, b: &CpsHandlerFrame) -> bool {
2060    a.prompt == b.prompt && a.install_eval_id == b.install_eval_id
2061}
2062
2063fn capture_continuation_inside_prompt(
2064    handlers: &[CpsHandlerFrame],
2065    return_frames: &[CpsReturnFrame],
2066    handled: &CpsHandlerFrame,
2067) -> CapturedPromptContinuation {
2068    let start = captured_prompt_frame_start(return_frames, handled);
2069    let return_frames = return_frames[start..]
2070        .iter()
2071        .cloned()
2072        .map(|frame| rebase_captured_return_frame(frame, start, handled))
2073        .collect();
2074    CapturedPromptContinuation {
2075        handlers: handlers
2076            .iter()
2077            .cloned()
2078            .map(|handler| rebase_captured_handler_frame(handler, start, handled))
2079            .collect(),
2080        return_frames: own_captured_return_frames(return_frames),
2081    }
2082}
2083
2084fn captured_prompt_frame_start(
2085    return_frames: &[CpsReturnFrame],
2086    handled: &CpsHandlerFrame,
2087) -> usize {
2088    let start = return_frames
2089        .iter()
2090        .rposition(|frame| {
2091            frame
2092                .prompt_exit
2093                .as_ref()
2094                .is_some_and(|exit| exit.prompt == handled.prompt)
2095        })
2096        .map(|index| index + 1)
2097        // If the marker is absent, this captured slice is already running
2098        // under an inherited prompt. Keep the whole slice; the handler
2099        // threshold may point at a replay-time post frame that still belongs
2100        // to this continuation.
2101        .unwrap_or(0)
2102        .min(return_frames.len());
2103    start
2104}
2105
2106fn rebase_captured_return_frame(
2107    mut frame: CpsReturnFrame,
2108    dropped_frames: usize,
2109    handled: &CpsHandlerFrame,
2110) -> CpsReturnFrame {
2111    frame.owner_initial_frame_count = frame
2112        .owner_initial_frame_count
2113        .saturating_sub(dropped_frames);
2114    frame.active_handlers = frame
2115        .active_handlers
2116        .into_iter()
2117        .map(|handler| rebase_captured_handler_frame(handler, dropped_frames, handled))
2118        .collect();
2119    frame
2120}
2121
2122fn rebase_captured_handler_frame(
2123    mut handler: CpsHandlerFrame,
2124    dropped_frames: usize,
2125    handled: &CpsHandlerFrame,
2126) -> CpsHandlerFrame {
2127    if handler.install_eval_id.0 >= handled.install_eval_id.0 {
2128        handler.return_frame_threshold = handler
2129            .return_frame_threshold
2130            .saturating_sub(dropped_frames);
2131    }
2132    handler
2133}
2134
2135/// Merge a captured continuation's handler stack with the resume site's
2136/// current handler stack.
2137///
2138/// Semantics (write24): handlers installed at the resume site (in the
2139/// arm body) are siblings to the captured continuation's outer scope —
2140/// they belong AFTER the handler whose arm produced the resumption (the
2141/// "anchor") but BEFORE the captured continuation's inner handler tail.
2142/// Concretely:
2143///
2144/// ```text
2145/// captured = [outer, H_sub]              (e.g. branch perform capture)
2146/// current  = [H_inner]                   (recursive once installed H_inner)
2147/// anchor   = outer                       (= the captured handler that matched)
2148/// merged   = [outer, H_inner, H_sub]
2149/// ```
2150///
2151/// `handle_scope_return` uses `rposition` for handler search, so the
2152/// merged stack reads outer→inner (innermost = last). Putting `H_inner`
2153/// AFTER `outer` and BEFORE `H_sub` gives the expected semantics: a
2154/// new `reject` performed in the resumed continuation finds `H_inner`
2155/// before `outer`, and `sub::return` still aborts to `H_sub` first.
2156///
2157/// Without anchor, falls back to a shared-prefix merge: handlers in
2158/// `current` that aren't already in `captured` are placed before the
2159/// captured tail.
2160fn merge_resumption_handlers(
2161    captured: &[CpsHandlerFrame],
2162    current: &[CpsHandlerFrame],
2163    anchor: Option<CpsHandlerAnchor>,
2164) -> Vec<CpsHandlerFrame> {
2165    let is_anchor = |frame: &CpsHandlerFrame, anchor: CpsHandlerAnchor| {
2166        frame.prompt == anchor.prompt && frame.install_eval_id == anchor.install_eval_id
2167    };
2168
2169    if let Some(anchor) = anchor {
2170        if let Some(anchor_index) = captured.iter().position(|f| is_anchor(f, anchor)) {
2171            let mut merged = Vec::with_capacity(captured.len() + current.len());
2172
2173            // Captured prefix up to and including the anchor (the handler
2174            // whose arm body produced this resumption).
2175            merged.extend(captured[..=anchor_index].iter().cloned());
2176
2177            // Resume-site siblings: handlers in `current` that aren't
2178            // already in the merged prefix or the captured tail. These
2179            // were installed in the arm body and belong between the
2180            // anchor and the captured tail.
2181            for frame in current {
2182                let in_prefix = merged.iter().any(|m| same_handler_frame(m, frame));
2183                let in_tail = captured[anchor_index + 1..]
2184                    .iter()
2185                    .any(|c| same_handler_frame(c, frame));
2186                if !in_prefix && !in_tail {
2187                    merged.push(frame.clone());
2188                }
2189            }
2190
2191            // Captured continuation's inner handler tail.
2192            merged.extend(captured[anchor_index + 1..].iter().cloned());
2193            return merged;
2194        }
2195    }
2196
2197    // Fallback: shared-prefix merge. Used when anchor is None
2198    // (synthetic-handler perform) or when the anchor handler can't be
2199    // located in `captured` (defensive — shouldn't happen).
2200    let mut shared = 0;
2201    while shared < captured.len()
2202        && shared < current.len()
2203        && same_handler_frame(&captured[shared], &current[shared])
2204    {
2205        shared += 1;
2206    }
2207
2208    let mut merged = Vec::with_capacity(captured.len() + current.len());
2209    merged.extend(captured[..shared].iter().cloned());
2210
2211    for frame in &current[shared..] {
2212        if !captured.iter().any(|c| same_handler_frame(c, frame)) {
2213            merged.push(frame.clone());
2214        }
2215    }
2216
2217    merged.extend(captured[shared..].iter().cloned());
2218    merged
2219}
2220
2221/// Apply `ResumeWithHandler`'s rebased handler stack to captured return frames.
2222///
2223/// The just-installed RWH frame is appended to each captured frame's handler
2224/// stack as the innermost handler. This intentionally differs from
2225/// `merge_extras_into_frames`, which preserves the captured innermost handler.
2226fn append_resume_with_handler_frames(
2227    frames: &[CpsReturnFrame],
2228    extra: &[CpsHandlerFrame],
2229) -> Vec<CpsReturnFrame> {
2230    if extra.is_empty() {
2231        return frames.to_vec();
2232    }
2233    frames
2234        .iter()
2235        .map(|frame| {
2236            let mut adjusted = frame.clone();
2237            for handler in extra {
2238                if !adjusted
2239                    .active_handlers
2240                    .iter()
2241                    .any(|h| h.prompt == handler.prompt)
2242                {
2243                    adjusted.active_handlers.push(handler.clone());
2244                }
2245            }
2246            adjusted
2247        })
2248        .collect()
2249}
2250
2251/// Apply `merge_resumption_handlers` per frame: each captured return
2252/// frame's saved `active_handlers` is the "captured" side; `current` is
2253/// the resume site's live handler stack. The `anchor` (the matched
2254/// handler whose arm produced the resumption) is used to position the
2255/// resume-site siblings consistently across every captured frame.
2256fn merge_extras_into_frames(
2257    frames: &[CpsReturnFrame],
2258    current: &[CpsHandlerFrame],
2259    anchor: Option<CpsHandlerAnchor>,
2260) -> Vec<CpsReturnFrame> {
2261    frames
2262        .iter()
2263        .map(|frame| {
2264            let merged =
2265                merge_resumption_handlers(&frame.active_handlers, current, anchor);
2266            if trace_enabled() && merged != frame.active_handlers {
2267                trace_cps(
2268                    "InjectExtraHandlers",
2269                    format!(
2270                        "frame_owner={} frame_owner_eval={} before={} current={} anchor={:?} after={}",
2271                        frame.owner_function,
2272                        frame.owner_eval_id.0,
2273                        summarize_handler_stack(&frame.active_handlers),
2274                        summarize_handler_stack(current),
2275                        anchor.map(|a| (a.prompt.0, a.install_eval_id.0)),
2276                        summarize_handler_stack(&merged),
2277                    ),
2278                );
2279            }
2280            let mut adjusted = frame.clone();
2281            adjusted.active_handlers = merged;
2282            adjusted
2283        })
2284        .collect()
2285}
2286
2287/// Return frames stored in a resumption are no longer owned by their
2288/// original live caller stack. Replaying `k` must consume the whole captured
2289/// chain; otherwise a frame restored from inside a library helper can stop at
2290/// its old `owner_initial_frame_count` and skip the user's post-loop code.
2291fn own_captured_return_frames(mut frames: Vec<CpsReturnFrame>) -> Vec<CpsReturnFrame> {
2292    for frame in &mut frames {
2293        frame.owner_initial_frame_count = 0;
2294    }
2295    frames
2296}
2297
2298/// Pop and resume return frames innermost-first. Each frame's continuation
2299/// runs with its saved handler/guard state plus `extra_handlers` (handlers
2300/// installed in the calling eval AFTER the original Perform that created
2301/// the resumption — see `Resume` stmt for the typical case).
2302///
2303/// The Return terminator of each frame's eval will call this helper again
2304/// for the remaining frames, so this function only handles one step.
2305fn continue_return_frames(
2306    module: &CpsModule,
2307    value: CpsRuntimeValue,
2308    frames: &[CpsReturnFrame],
2309    extra_handlers: &[CpsHandlerFrame],
2310) -> CpsEvalResult<CpsRuntimeValue> {
2311    if frames.is_empty() {
2312        return Ok(value);
2313    }
2314    if matches!(value, CpsRuntimeValue::ScopeReturn { .. })
2315        || matches!(value, CpsRuntimeValue::Aborted(_))
2316        || matches!(value, CpsRuntimeValue::RoutedJump(_))
2317    {
2318        // A ScopeReturn from the inner eval should not have additional
2319        // frame continuations applied — propagate it untouched so the
2320        // caller's `dispatch_scope_return` can match the right prompt.
2321        return Ok(value);
2322    }
2323    let (frame, rest) = frames.split_last().expect("non-empty");
2324    trace_cps(
2325        "ContinueReturnFrames",
2326        format!(
2327            "pop owner={} cont={:?} owner_eval={} rest.len={} owner_initial={}",
2328            frame.owner_function,
2329            frame.continuation,
2330            frame.owner_eval_id.0,
2331            rest.len(),
2332            frame.owner_initial_frame_count,
2333        ),
2334    );
2335    let function = function_by_name(module, &frame.owner_function)?;
2336    let mut combined = frame.active_handlers.clone();
2337    for extra in extra_handlers {
2338        if !combined.iter().any(|f| f.prompt == extra.prompt) {
2339            combined.push(extra.clone());
2340        }
2341    }
2342    // Use resume_continuation (no into_inherited) so the saved handlers'
2343    // non-inherited state is preserved — that's how a ScopeReturn destined
2344    // for the handler scope in (e.g.) once_dfs_int can still resolve when
2345    // we're running work's post-call cont here. Also restore owner_eval_id
2346    // as current_eval_id so the install scope identity is preserved
2347    // (write22): the saved handlers' install_eval_id matches the eval that
2348    // installed them, and that's exactly the eval we're resuming.
2349    // owner_initial_frame_count may exceed rest.len() if a ScopeReturn
2350    // truncated frames after this one was pushed. Clamp so the resumed
2351    // owner eval can still proceed.
2352    let owner_initial = frame.owner_initial_frame_count.min(rest.len());
2353    resume_continuation(
2354        module,
2355        function,
2356        frame.continuation,
2357        vec![value],
2358        frame.values.as_ref().clone(),
2359        combined,
2360        frame.guard_stack.clone(),
2361        rest.to_vec(),
2362        frame.active_blocked.clone(),
2363        owner_initial,
2364        frame.owner_eval_id,
2365    )
2366}
2367
2368/// write25 Step 5: walk-based ScopeReturn routing.
2369///
2370/// When `handle_scope_return` returns Propagate, the dispatching eval's
2371/// LOCAL `return_frames` may still hold the install scope for the SR's
2372/// handler. Specifically, the resumption's adjusted captured chain
2373/// (after `merge_extras_into_frames`) lives in the Resume eval's local
2374/// `return_frames` — and the modified copies there can hold handler
2375/// frames whose `install_eval_id == frame.owner_eval_id`, i.e. the
2376/// install scope of the handler is exactly that captured frame's owner
2377/// eval.
2378///
2379/// Walk innermost-first across the whole return-frame snapshot. For each frame, check
2380/// whether its saved `active_handlers` has a handler matching
2381/// `(prompt, install_eval_id == frame.owner_eval_id)` AND
2382/// `escape_owner_function == frame.owner_function`. If found, restore
2383/// that frame's eval at `handler.escape` with the in-scope handlers
2384/// truncated below the matched handler (so siblings above it survive),
2385/// and return the routed result.
2386///
2387/// Returns `None` when:
2388/// - the value is not a ScopeReturn,
2389/// - the target is `EXIT_RWH_TARGET` (RWH-escape — keep using
2390///   call-stack propagation for now),
2391/// - no own frame holds the install scope.
2392fn try_route_scope_return_through_return_frames(
2393    module: &CpsModule,
2394    scope_return: &CpsRuntimeValue,
2395    return_frames: &[CpsReturnFrame],
2396    initial_frame_count: usize,
2397) -> CpsEvalResult<Option<CpsRuntimeValue>> {
2398    let CpsRuntimeValue::ScopeReturn {
2399        prompt,
2400        target,
2401        value,
2402    } = scope_return
2403    else {
2404        return Ok(None);
2405    };
2406    let prompt = *prompt;
2407    let target = *target;
2408    if target == EXIT_RWH_TARGET {
2409        // write25 explicitly defers EXIT_RWH_TARGET routing through the
2410        // own-frame walk; existing RWH paths (which use the same sentinel
2411        // target) rely on call-stack propagation.
2412        return Ok(None);
2413    }
2414    if return_frames.is_empty() {
2415        return Ok(None);
2416    }
2417    trace_cps(
2418        "ScopeReturnFrameWalk",
2419        format!(
2420            "prompt={} target={:?} frames.len={} initial={}",
2421            prompt.0,
2422            target,
2423            return_frames.len(),
2424            initial_frame_count,
2425        ),
2426    );
2427    for frame_index in (0..return_frames.len()).rev() {
2428        let frame = &return_frames[frame_index];
2429        let frame_eval_id = frame.owner_eval_id;
2430        let frame_owner = &frame.owner_function;
2431        if trace_enabled() {
2432            trace_cps(
2433                "ScopeReturnFrameWalk",
2434                format!(
2435                    "  check frame_index={} owner={} owner_eval={} handlers={}",
2436                    frame_index,
2437                    frame.owner_function,
2438                    frame.owner_eval_id.0,
2439                    summarize_handler_stack(&frame.active_handlers),
2440                ),
2441            );
2442        }
2443        // Search this frame's snapshot for a handler that was installed
2444        // in this very frame's owner eval.
2445        let Some(handler_index) = frame.active_handlers.iter().rposition(|handler| {
2446            handler.prompt == prompt && handler.install_eval_id == frame_eval_id
2447        }) else {
2448            continue;
2449        };
2450        let matched_handler = frame.active_handlers[handler_index].clone();
2451        // owner check: the matched handler's escape must live in the
2452        // frame's owner function (so we can jump to it within that
2453        // eval). For non-EXIT targets this is required.
2454        if matched_handler.escape_owner_function != *frame_owner {
2455            continue;
2456        }
2457        debug_assert_eq!(
2458            matched_handler.install_eval_id, frame.owner_eval_id,
2459            "matched handler's install eval must equal frame.owner_eval_id"
2460        );
2461        // Build the post-jump handler stack: everything below the matched
2462        // handler stays in scope. Handlers above it (resume-site siblings
2463        // injected via `merge_extras_into_frames` between the anchor and
2464        // the captured inner tail) — write25 says truncate at the matched
2465        // index so they survive.
2466        // NOTE: For our anchor-based merge, the merged stack reads
2467        // `[outer, inner, H_sub]` with the matched H_sub at the end, so
2468        // truncating at `handler_index` keeps `[outer, inner]`. That's
2469        // exactly what we want.
2470        let mut post_handlers = frame.active_handlers.clone();
2471        post_handlers.truncate(handler_index);
2472        // Compute the rest of the frame chain. The matched frame is
2473        // consumed (we're jumping to its handler's escape). Frames after
2474        // it were already popped during the walk. Frames before it stay
2475        // as the resumed eval's inherited chain, but truncated to the
2476        // handler's install threshold (anything pushed in the handler's
2477        // scope is discarded).
2478        let mut rest_frames: Vec<CpsReturnFrame> = return_frames[..frame_index].to_vec();
2479        let truncate_at = matched_handler.return_frame_threshold;
2480        let rest_before = rest_frames.len();
2481        if rest_frames.len() > truncate_at {
2482            rest_frames.truncate(truncate_at);
2483        }
2484        let rest_after = rest_frames.len();
2485        trace_cps(
2486            "FrameWalkMatch",
2487            format!(
2488                "frame_owner={} frame_owner_eval={} handler_prompt={} handler_install_eval={} handlers_before={} handlers_after={} rest_before={} truncate_at={} rest_after={}",
2489                frame.owner_function,
2490                frame.owner_eval_id.0,
2491                matched_handler.prompt.0,
2492                matched_handler.install_eval_id.0,
2493                summarize_handler_stack(&frame.active_handlers),
2494                summarize_handler_stack(&post_handlers),
2495                rest_before,
2496                truncate_at,
2497                rest_after,
2498            ),
2499        );
2500        let owner_initial = frame.owner_initial_frame_count.min(rest_frames.len());
2501        let routed_jump = CpsRoutedJump {
2502            value: value.clone(),
2503            return_frame_threshold: truncate_at,
2504            owner_function: frame.owner_function.clone(),
2505            target: matched_handler.escape,
2506            values: frame.values.clone(),
2507            active_handlers: post_handlers,
2508            guard_stack: frame.guard_stack.clone(),
2509            return_frames: rest_frames,
2510            active_blocked: frame.active_blocked.clone(),
2511            initial_frame_count: owner_initial,
2512            eval_id: frame.owner_eval_id,
2513        };
2514        // The target lives in an inherited caller activation. Keep this as a
2515        // jump command even when outer handlers remain; collapsing it to a
2516        // plain value would feed the handler result into the skipped caller's
2517        // normal post-call continuation.
2518        if frame_index < initial_frame_count {
2519            return Ok(Some(CpsRuntimeValue::RoutedJump(Box::new(routed_jump))));
2520        }
2521        let owner = function_by_name(module, &routed_jump.owner_function)?;
2522        let result = resume_continuation(
2523            module,
2524            owner,
2525            routed_jump.target,
2526            vec![*value.clone()],
2527            routed_jump.values.as_ref().clone(),
2528            routed_jump.active_handlers,
2529            routed_jump.guard_stack,
2530            routed_jump.return_frames,
2531            routed_jump.active_blocked,
2532            routed_jump.initial_frame_count,
2533            routed_jump.eval_id,
2534        )?;
2535        return Ok(Some(result));
2536    }
2537    Ok(None)
2538}
2539
2540fn resolve_routed_jump(
2541    module: &CpsModule,
2542    value: CpsRuntimeValue,
2543    current_return_frames: &[CpsReturnFrame],
2544) -> CpsEvalResult<CpsRuntimeValue> {
2545    let CpsRuntimeValue::RoutedJump(jump) = value else {
2546        return Ok(value);
2547    };
2548    if current_return_frames.len() > jump.return_frame_threshold {
2549        return Ok(CpsRuntimeValue::RoutedJump(jump));
2550    }
2551    let owner = function_by_name(module, &jump.owner_function)?;
2552    resume_continuation(
2553        module,
2554        owner,
2555        jump.target,
2556        vec![*jump.value],
2557        jump.values.as_ref().clone(),
2558        jump.active_handlers,
2559        jump.guard_stack,
2560        jump.return_frames,
2561        jump.active_blocked,
2562        jump.initial_frame_count,
2563        jump.eval_id,
2564    )
2565}
2566
2567fn function_by_name<'a>(module: &'a CpsModule, name: &str) -> CpsEvalResult<&'a CpsFunction> {
2568    module
2569        .functions
2570        .iter()
2571        .chain(module.roots.iter())
2572        .find(|function| function.name == name)
2573        .ok_or_else(|| CpsEvalError::MissingFunction {
2574            name: name.to_string(),
2575        })
2576}
2577
2578fn continuation_by_id(
2579    function: &CpsFunction,
2580    id: CpsContinuationId,
2581) -> CpsEvalResult<&CpsContinuation> {
2582    function
2583        .continuations
2584        .iter()
2585        .find(|continuation| continuation.id == id)
2586        .ok_or_else(|| CpsEvalError::MissingContinuation {
2587            function: function.name.clone(),
2588            id,
2589        })
2590}
2591
2592fn handler_arm_continues_to_installed_escape(
2593    function: &CpsFunction,
2594    handler: CpsHandlerId,
2595    entry: CpsContinuationId,
2596    escape: CpsContinuationId,
2597) -> bool {
2598    let escape_is_installed_for_handler = function.continuations.iter().any(|continuation| {
2599        continuation.stmts.iter().any(|stmt| {
2600            matches!(
2601                stmt,
2602                CpsStmt::InstallHandler {
2603                    handler: id,
2604                    escape: installed_escape,
2605                    ..
2606                } if *id == handler && *installed_escape == escape
2607            )
2608        })
2609    });
2610    if !escape_is_installed_for_handler {
2611        return false;
2612    }
2613    handler_arm_continue_chain_reaches_escape(function, handler, entry, escape)
2614}
2615
2616fn handler_arm_continue_chain_reaches_escape(
2617    function: &CpsFunction,
2618    handler: CpsHandlerId,
2619    entry: CpsContinuationId,
2620    escape: CpsContinuationId,
2621) -> bool {
2622    let mut current = entry;
2623    let mut saw_uninstall = false;
2624    let mut visited = HashSet::new();
2625    while visited.insert(current) {
2626        let Some(continuation) = function
2627            .continuations
2628            .iter()
2629            .find(|continuation| continuation.id == current)
2630        else {
2631            return false;
2632        };
2633        saw_uninstall |= continuation.stmts.iter().any(
2634            |stmt| matches!(stmt, CpsStmt::UninstallHandler { handler: id } if *id == handler),
2635        );
2636        let CpsTerminator::Continue { target, .. } = &continuation.terminator else {
2637            return saw_uninstall && current == escape;
2638        };
2639        if *target == escape {
2640            return saw_uninstall;
2641        }
2642        current = *target;
2643    }
2644    false
2645}
2646
2647fn handler_arm_uses_resume_with_handler(
2648    function: &CpsFunction,
2649    handler: CpsHandlerId,
2650    entry: CpsContinuationId,
2651) -> bool {
2652    let mut current = entry;
2653    let mut visited = HashSet::new();
2654    while visited.insert(current) {
2655        let Some(continuation) = function
2656            .continuations
2657            .iter()
2658            .find(|continuation| continuation.id == current)
2659        else {
2660            return false;
2661        };
2662        if continuation.stmts.iter().any(
2663            |stmt| matches!(stmt, CpsStmt::ResumeWithHandler { handler: id, .. } if *id == handler),
2664        ) {
2665            return true;
2666        }
2667        let CpsTerminator::Continue { target, .. } = &continuation.terminator else {
2668            return false;
2669        };
2670        current = *target;
2671    }
2672    false
2673}
2674
2675fn handler_arm_for_stack<'a>(
2676    module: &'a CpsModule,
2677    current_function: &'a CpsFunction,
2678    stack: &'a [CpsHandlerFrame],
2679    effect: &typed_ir::Path,
2680    blocked: Option<u64>,
2681) -> CpsEvalResult<(
2682    &'a CpsHandlerArm,
2683    &'a CpsHandlerFrame,
2684    Vec<CpsHandlerFrame>,
2685    &'a CpsFunction,
2686)> {
2687    for (index, frame) in stack.iter().enumerate().rev() {
2688        if let Some(blocked) = blocked
2689            && frame.guard_stack.iter().any(|entry| entry.id == blocked)
2690        {
2691            trace_cps(
2692                "PerformHandlerSkip",
2693                format!(
2694                    "fn={} effect={:?} index={} prompt={} blocked={}",
2695                    current_function.name, effect, index, frame.prompt.0, blocked
2696                ),
2697            );
2698            continue;
2699        }
2700        for owner in module.functions.iter().chain(module.roots.iter()) {
2701            if let Some(arm) = owner
2702                .handlers
2703                .iter()
2704                .find(|handler| handler.id == frame.handler)
2705                .and_then(|handler| {
2706                    handler
2707                        .arms
2708                        .iter()
2709                        .find(|arm| effect_matches(&arm.effect, effect))
2710                })
2711            {
2712                return Ok((arm, frame, stack[..index].to_vec(), owner));
2713            }
2714        }
2715    }
2716    Err(CpsEvalError::MissingHandler {
2717        function: current_function.name.clone(),
2718        id: stack.last().expect("handler stack is non-empty").handler,
2719    })
2720}
2721
2722fn handler_stack_with_static(
2723    active_handlers: &[CpsHandlerFrame],
2724    fallback: CpsHandlerId,
2725    guard_stack: &[CpsGuardEntry],
2726) -> Vec<CpsHandlerFrame> {
2727    if active_handlers.is_empty() {
2728        // Synthetic frame for the static-fallback path. Real escape target
2729        // is unknown at this point, so we use EXIT_RWH_TARGET so an abort
2730        // at this synthetic frame surfaces back through the call boundary.
2731        // install_eval_id = SYNTHETIC_EVAL_ID: this frame must never act
2732        // as a ScopeReturn resolver — the perform's arm body, not a
2733        // matching install scope, decides the value here.
2734        vec![CpsHandlerFrame {
2735            prompt: fresh_prompt(),
2736            handler: fallback,
2737            guard_stack: guard_stack.to_vec(),
2738            envs: Vec::new(),
2739            escape: EXIT_RWH_TARGET,
2740            escape_owner_function: String::new(),
2741            inherited: false,
2742            return_frame_threshold: 0,
2743            install_eval_id: SYNTHETIC_EVAL_ID,
2744        }]
2745    } else {
2746        active_handlers.to_vec()
2747    }
2748}
2749
2750#[allow(dead_code)]
2751fn handler_stack_with_pushed(
2752    active_handlers: &[CpsHandlerFrame],
2753    handler: CpsHandlerId,
2754    guard_stack: &[CpsGuardEntry],
2755    envs: Vec<CpsEvaluatedHandlerEnv>,
2756) -> Vec<CpsHandlerFrame> {
2757    let mut stack = active_handlers.to_vec();
2758    stack.push(CpsHandlerFrame {
2759        prompt: fresh_prompt(),
2760        handler,
2761        guard_stack: guard_stack.to_vec(),
2762        envs,
2763        // ResumeWithHandler-installed frame: arm result returns to the
2764        // outer eval frame (the call site that ran this stmt).
2765        escape: EXIT_RWH_TARGET,
2766        escape_owner_function: String::new(),
2767        inherited: false,
2768        return_frame_threshold: 0,
2769        // Dead code currently — but if revived, the caller must pass a
2770        // real eval id. SYNTHETIC keeps it safe.
2771        install_eval_id: SYNTHETIC_EVAL_ID,
2772    });
2773    stack
2774}
2775
2776fn capture_handler_envs(
2777    function: &CpsFunction,
2778    values: &[Option<CpsRuntimeValue>],
2779    envs: &[CpsHandlerEnv],
2780) -> CpsEvalResult<Vec<CpsEvaluatedHandlerEnv>> {
2781    envs.iter()
2782        .map(|env| {
2783            let mut values_by_id = Vec::new();
2784            for (index, value) in env.values.iter().enumerate() {
2785                let target = env.targets.get(index).copied().unwrap_or(*value);
2786                values_by_id.push((target, read_value(function, values, *value)?));
2787            }
2788            Ok(CpsEvaluatedHandlerEnv {
2789                entry: env.entry,
2790                values: values_by_id,
2791            })
2792        })
2793        .collect()
2794}
2795
2796fn overlay_handler_envs_in_frames(
2797    function: &str,
2798    frames: &mut [CpsReturnFrame],
2799    handler: CpsHandlerId,
2800    updates: &[CpsEvaluatedHandlerEnv],
2801    remember_latest: bool,
2802) {
2803    for frame in frames {
2804        overlay_handler_envs_in_stack(
2805            function,
2806            &mut frame.active_handlers,
2807            handler,
2808            updates,
2809            remember_latest,
2810        );
2811    }
2812}
2813
2814fn overlay_handler_envs_in_stack(
2815    function: &str,
2816    stack: &mut [CpsHandlerFrame],
2817    handler: CpsHandlerId,
2818    updates: &[CpsEvaluatedHandlerEnv],
2819    remember_latest: bool,
2820) {
2821    if updates.is_empty() {
2822        return;
2823    }
2824    for frame in stack.iter_mut().filter(|frame| frame.handler == handler) {
2825        overlay_handler_envs(&mut frame.envs, updates);
2826    }
2827    if remember_latest {
2828        remember_latest_handler_envs(handler, updates);
2829    }
2830    push_cps_frame_trace_event(CpsFrameTraceEvent::HandlerEnvOverlay {
2831        layer: CpsFrameTraceLayer::CpsEval,
2832        function: function.to_string(),
2833        handler: handler.0,
2834        entries: updates.iter().map(|env| env.entry.0).collect(),
2835        values: trace_cps_handler_env_slots(updates),
2836    });
2837}
2838
2839fn overlay_handler_envs(
2840    envs: &mut Vec<CpsEvaluatedHandlerEnv>,
2841    updates: &[CpsEvaluatedHandlerEnv],
2842) {
2843    for update in updates {
2844        let Some(existing) = envs.iter_mut().find(|env| env.entry == update.entry) else {
2845            envs.push(update.clone());
2846            continue;
2847        };
2848        for (target, value) in &update.values {
2849            if let Some((_, existing_value)) = existing
2850                .values
2851                .iter_mut()
2852                .find(|(existing_target, _)| existing_target == target)
2853            {
2854                *existing_value = value.clone();
2855            } else {
2856                existing.values.push((*target, value.clone()));
2857            }
2858        }
2859    }
2860}
2861
2862fn remember_latest_handler_envs(handler: CpsHandlerId, updates: &[CpsEvaluatedHandlerEnv]) {
2863    LATEST_HANDLER_ENVS.with(|latest| {
2864        let mut latest = latest.borrow_mut();
2865        for update in updates {
2866            for (target, value) in &update.values {
2867                if let Some(existing) = latest.iter_mut().find(|existing| {
2868                    existing.handler == handler
2869                        && existing.entry == update.entry
2870                        && existing.target == *target
2871                }) {
2872                    existing.value = value.clone();
2873                } else {
2874                    latest.push(CpsLatestHandlerEnv {
2875                        handler,
2876                        entry: update.entry,
2877                        target: *target,
2878                        value: value.clone(),
2879                    });
2880                }
2881            }
2882        }
2883    });
2884}
2885
2886fn latest_handler_env_value(
2887    handler: CpsHandlerId,
2888    entry: CpsContinuationId,
2889    target: CpsValueId,
2890) -> Option<CpsRuntimeValue> {
2891    LATEST_HANDLER_ENVS.with(|latest| {
2892        latest
2893            .borrow()
2894            .iter()
2895            .rev()
2896            .find(|latest| {
2897                latest.handler == handler && latest.entry == entry && latest.target == target
2898            })
2899            .map(|latest| latest.value.clone())
2900    })
2901}
2902
2903fn values_with_handler_env(
2904    function: &str,
2905    mut values: Vec<Option<CpsRuntimeValue>>,
2906    frame: &CpsHandlerFrame,
2907    entry: CpsContinuationId,
2908) -> Vec<Option<CpsRuntimeValue>> {
2909    let Some(env) = frame.envs.iter().find(|env| env.entry == entry) else {
2910        return values;
2911    };
2912    let effective_values = env
2913        .values
2914        .iter()
2915        .map(|(id, value)| {
2916            (
2917                *id,
2918                latest_handler_env_value(frame.handler, entry, *id)
2919                    .unwrap_or_else(|| value.clone()),
2920            )
2921        })
2922        .collect::<Vec<_>>();
2923    push_cps_frame_trace_event(CpsFrameTraceEvent::HandlerEnvRead {
2924        layer: CpsFrameTraceLayer::CpsEval,
2925        function: function.to_string(),
2926        handler: frame.handler.0,
2927        entry: entry.0,
2928        values: trace_cps_handler_env_value_slots(&effective_values),
2929    });
2930    for (id, value) in effective_values {
2931        write_value(&mut values, id, value);
2932    }
2933    values
2934}
2935
2936fn trace_cps_handler_env_slots(envs: &[CpsEvaluatedHandlerEnv]) -> Vec<CpsFrameTraceSlot> {
2937    envs.iter()
2938        .flat_map(|env| {
2939            env.values.iter().map(|(target, value)| CpsFrameTraceSlot {
2940                target: target.0,
2941                value: summarize_cps_value(value),
2942            })
2943        })
2944        .collect()
2945}
2946
2947fn trace_cps_handler_env_value_slots(
2948    values: &[(CpsValueId, CpsRuntimeValue)],
2949) -> Vec<CpsFrameTraceSlot> {
2950    values
2951        .iter()
2952        .map(|(target, value)| CpsFrameTraceSlot {
2953            target: target.0,
2954            value: summarize_cps_value(value),
2955        })
2956        .collect()
2957}
2958
2959fn add_thunk_boundary(
2960    value: CpsRuntimeValue,
2961    guard_id: u64,
2962    allowed: typed_ir::Type,
2963    active: bool,
2964) -> CpsRuntimeValue {
2965    let CpsRuntimeValue::Thunk(thunk) = value else {
2966        return value;
2967    };
2968    let mut blocked = thunk.blocked.as_ref().clone();
2969    blocked.push(CpsBlockedEffect {
2970        guard_id,
2971        allowed,
2972        active,
2973    });
2974    CpsRuntimeValue::Thunk(Rc::new(CpsThunk {
2975        owner_function: thunk.owner_function.clone(),
2976        entry: thunk.entry,
2977        values: thunk.values.clone(),
2978        handlers: thunk.handlers.clone(),
2979        guard_stack: thunk.guard_stack.clone(),
2980        blocked: Rc::new(blocked),
2981    }))
2982}
2983
2984fn active_blocked_for_thunk(
2985    current: &[CpsBlockedEffect],
2986    thunk: &CpsThunk,
2987) -> Vec<CpsBlockedEffect> {
2988    let mut active = current.to_vec();
2989    active.extend(
2990        thunk
2991            .blocked
2992            .iter()
2993            .filter(|blocked| blocked.active)
2994            .cloned(),
2995    );
2996    active
2997}
2998
2999fn active_blocked_id(effect: &typed_ir::Path, active: &[CpsBlockedEffect]) -> Option<u64> {
3000    active
3001        .iter()
3002        .rev()
3003        .find(|blocked| !effect_allowed_by_type(&blocked.allowed, effect))
3004        .map(|blocked| blocked.guard_id)
3005}
3006
3007fn effect_allowed_by_type(allowed: &typed_ir::Type, effect: &typed_ir::Path) -> bool {
3008    match allowed {
3009        typed_ir::Type::Any => true,
3010        typed_ir::Type::Never => false,
3011        typed_ir::Type::Named { path, .. } => effect_path_matches_allowed(path, effect),
3012        typed_ir::Type::Row { items, tail } => {
3013            items
3014                .iter()
3015                .any(|item| effect_allowed_by_type(item, effect))
3016                || matches!(tail.as_ref(), typed_ir::Type::Any)
3017        }
3018        _ => false,
3019    }
3020}
3021
3022fn effect_path_matches_allowed(allowed: &typed_ir::Path, effect: &typed_ir::Path) -> bool {
3023    if effect.segments.starts_with(&allowed.segments) {
3024        return true;
3025    }
3026    if allowed.segments.len() > 1
3027        && effect.segments.len() == allowed.segments.len()
3028        && effect.segments[..effect.segments.len() - 1]
3029            == allowed.segments[..allowed.segments.len() - 1]
3030        && effect_segment_matches_allowed(
3031            &allowed.segments[allowed.segments.len() - 1],
3032            &effect.segments[effect.segments.len() - 1],
3033        )
3034    {
3035        return true;
3036    }
3037    effect
3038        .segments
3039        .iter()
3040        .enumerate()
3041        .skip(1)
3042        .any(|(index, _)| effect.segments[index..].starts_with(&allowed.segments))
3043}
3044
3045fn effect_segment_matches_allowed(allowed: &typed_ir::Name, effect: &typed_ir::Name) -> bool {
3046    allowed == effect
3047        || effect
3048            .0
3049            .strip_suffix("#effect")
3050            .is_some_and(|base| base == allowed.0)
3051}
3052
3053fn effect_matches(expected: &typed_ir::Path, actual: &typed_ir::Path) -> bool {
3054    effect_path_matches_allowed(expected, actual)
3055}
3056
3057fn assign_continuation_args(
3058    values: &mut Vec<Option<CpsRuntimeValue>>,
3059    function: &CpsFunction,
3060    continuation: &CpsContinuation,
3061    args: Vec<CpsRuntimeValue>,
3062) -> CpsEvalResult<()> {
3063    if continuation.params.len() != args.len() {
3064        return Err(CpsEvalError::ContinuationArgumentMismatch {
3065            function: function.name.clone(),
3066            id: continuation.id,
3067            expected: continuation.params.len(),
3068            actual: args.len(),
3069        });
3070    }
3071    for (param, value) in continuation.params.iter().copied().zip(args) {
3072        write_value(values, param, value);
3073    }
3074    Ok(())
3075}
3076
3077fn write_value(values: &mut Vec<Option<CpsRuntimeValue>>, id: CpsValueId, value: CpsRuntimeValue) {
3078    if values.len() <= id.0 {
3079        values.resize_with(id.0 + 1, || None);
3080    }
3081    values[id.0] = Some(value);
3082}
3083
3084fn read_value(
3085    function: &CpsFunction,
3086    values: &[Option<CpsRuntimeValue>],
3087    id: CpsValueId,
3088) -> CpsEvalResult<CpsRuntimeValue> {
3089    values
3090        .get(id.0)
3091        .and_then(Clone::clone)
3092        .ok_or_else(|| CpsEvalError::MissingValue {
3093            function: function.name.clone(),
3094            id,
3095        })
3096}
3097
3098fn read_plain_value(
3099    function: &CpsFunction,
3100    values: &[Option<CpsRuntimeValue>],
3101    id: CpsValueId,
3102) -> CpsEvalResult<runtime::VmValue> {
3103    into_plain_value(function, id, read_value(function, values, id)?)
3104}
3105
3106fn read_effect_id(
3107    function: &CpsFunction,
3108    values: &[Option<CpsRuntimeValue>],
3109    id: CpsValueId,
3110) -> CpsEvalResult<u64> {
3111    match read_plain_value(function, values, id)? {
3112        runtime::VmValue::EffectId(value_id) => Ok(value_id),
3113        value => Err(CpsEvalError::ExpectedGuard {
3114            function: function.name.clone(),
3115            id,
3116            value,
3117        }),
3118    }
3119}
3120
3121fn into_plain_value(
3122    function: &CpsFunction,
3123    id: CpsValueId,
3124    value: CpsRuntimeValue,
3125) -> CpsEvalResult<runtime::VmValue> {
3126    cps_value_to_vm(value).ok_or_else(|| CpsEvalError::ExpectedPlainValue {
3127        function: function.name.clone(),
3128        id,
3129    })
3130}
3131
3132fn read_resumption(
3133    function: &CpsFunction,
3134    values: &[Option<CpsRuntimeValue>],
3135    id: CpsValueId,
3136) -> CpsEvalResult<Rc<CpsResumption>> {
3137    match read_value(function, values, id)? {
3138        CpsRuntimeValue::Resumption(resumption) => Ok(resumption),
3139        _ => Err(CpsEvalError::ExpectedResumption {
3140            function: function.name.clone(),
3141            id,
3142        }),
3143    }
3144}
3145
3146#[allow(dead_code)]
3147fn read_closure(
3148    function: &CpsFunction,
3149    values: &[Option<CpsRuntimeValue>],
3150    id: CpsValueId,
3151) -> CpsEvalResult<Rc<CpsClosure>> {
3152    match read_value(function, values, id)? {
3153        CpsRuntimeValue::Closure(closure) => Ok(closure),
3154        _ => Err(CpsEvalError::ExpectedPlainValue {
3155            function: function.name.clone(),
3156            id,
3157        }),
3158    }
3159}
3160
3161#[derive(Debug, Clone, PartialEq)]
3162enum CpsRuntimeValue {
3163    Plain(runtime::VmValue),
3164    Resumption(Rc<CpsResumption>),
3165    Thunk(Rc<CpsThunk>),
3166    Closure(Rc<CpsClosure>),
3167    /// First-class CPS containers whose elements may themselves be
3168    /// resumptions, thunks, closures, or other CPS values. Used by
3169    /// `std::undet.once`'s `list<resumption>` queue and `(k, queue)` tuple
3170    /// pattern, which a `VmValue::List`/`Tuple` cannot represent.
3171    List(Rc<Vec<CpsRuntimeValue>>),
3172    Tuple(Vec<CpsRuntimeValue>),
3173    Record(BTreeMap<typed_ir::Name, CpsRuntimeValue>),
3174    Variant {
3175        tag: typed_ir::Name,
3176        value: Option<Box<CpsRuntimeValue>>,
3177    },
3178    /// A frame-walk ScopeReturn resolved to a handler escape owned by an
3179    /// outer live eval frame. Carry the jump until that eval frontier is
3180    /// reached, then execute the escape as ordinary value flow.
3181    RoutedJump(Box<CpsRoutedJump>),
3182    /// A routed non-local result that must keep bubbling through live host
3183    /// calls without executing their remaining continuation code.
3184    #[allow(dead_code)]
3185    Aborted(Box<CpsRuntimeValue>),
3186    /// Non-local return carrying a value to a specific handler-installed scope.
3187    /// Generated when a `Perform`'s arm body completes; propagates through call
3188    /// sites until the matching prompt is found in `active_handlers`. A frame
3189    /// matches by `prompt` (a dynamic id, fresh per `InstallHandler` /
3190    /// `ResumeWithHandler` execution), so recursive handler scopes don't get
3191    /// confused with each other. When matched, the prompt's owning frame and
3192    /// any inner frames are popped, and:
3193    ///   - if `target != EXIT_RWH_TARGET`, control jumps to that continuation
3194    ///     with `value` as its single argument;
3195    ///   - if `target == EXIT_RWH_TARGET`, the eval frame returns `value` as
3196    ///     its result so the matching `ResumeWithHandler` call site sees a
3197    ///     plain return.
3198    ScopeReturn {
3199        prompt: CpsPromptId,
3200        target: CpsContinuationId,
3201        value: Box<CpsRuntimeValue>,
3202    },
3203}
3204
3205#[derive(Debug, Clone, PartialEq)]
3206struct CpsRoutedJump {
3207    value: Box<CpsRuntimeValue>,
3208    return_frame_threshold: usize,
3209    owner_function: String,
3210    target: CpsContinuationId,
3211    values: Rc<Vec<Option<CpsRuntimeValue>>>,
3212    active_handlers: Vec<CpsHandlerFrame>,
3213    guard_stack: Vec<CpsGuardEntry>,
3214    return_frames: Vec<CpsReturnFrame>,
3215    active_blocked: Vec<CpsBlockedEffect>,
3216    initial_frame_count: usize,
3217    eval_id: CpsEvalId,
3218}
3219
3220/// Sentinel `target` value used by `ResumeWithHandler`-installed handler
3221/// frames to mean "no continuation target — just return the value to the
3222/// outer eval frame." Picked as `usize::MAX` since real continuation ids
3223/// are dense small integers.
3224const EXIT_RWH_TARGET: CpsContinuationId = CpsContinuationId(usize::MAX);
3225
3226#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3227struct CpsPromptId(u64);
3228
3229thread_local! {
3230    static PROMPT_COUNTER: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
3231}
3232
3233fn fresh_prompt() -> CpsPromptId {
3234    PROMPT_COUNTER.with(|cell| {
3235        let id = cell.get();
3236        cell.set(id + 1);
3237        CpsPromptId(id)
3238    })
3239}
3240
3241/// Identity of an eval frame instance. Distinct from function name and
3242/// continuation id: a recursive resumption can run the same function in a
3243/// fresh eval frame, and we must not let a ScopeReturn from the fresh
3244/// frame resolve a handler installed by the original frame. See write22.
3245#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
3246struct CpsEvalId(u64);
3247
3248thread_local! {
3249    static EVAL_ID_COUNTER: std::cell::Cell<u64> = const { std::cell::Cell::new(0) };
3250}
3251
3252fn fresh_eval_id() -> CpsEvalId {
3253    EVAL_ID_COUNTER.with(|cell| {
3254        let id = cell.get();
3255        cell.set(id + 1);
3256        CpsEvalId(id)
3257    })
3258}
3259
3260/// Sentinel eval id used for synthetic handler frames (Perform fallback
3261/// without an installed handler) that should never participate in
3262/// ScopeReturn resolution. Picking a fresh id at synth time would work
3263/// too, but using a sentinel makes traces clearer.
3264const SYNTHETIC_EVAL_ID: CpsEvalId = CpsEvalId(u64::MAX);
3265
3266/// Identifier of the handler frame whose arm produced a resumption. Used
3267/// to anchor handler-stack merging at Resume time: the resume-site's
3268/// sibling handlers (installed in the arm body, e.g. `loop`'s inner
3269/// recursive handler in `std::undet::once`) belong immediately AFTER
3270/// this anchor in the merged stack so they sit between the captured
3271/// outer handlers and the captured inner tail (write24 Step 1).
3272#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3273struct CpsHandlerAnchor {
3274    prompt: CpsPromptId,
3275    install_eval_id: CpsEvalId,
3276}
3277
3278#[derive(Debug, Clone, PartialEq)]
3279struct CpsResumption {
3280    owner_function: String,
3281    target: CpsContinuationId,
3282    values: Rc<Vec<Option<CpsRuntimeValue>>>,
3283    handlers: Rc<Vec<CpsHandlerFrame>>,
3284    guard_stack: Rc<Vec<CpsGuardEntry>>,
3285    active_blocked: Rc<Vec<CpsBlockedEffect>>,
3286    /// Stack of return frames representing caller continuations that were
3287    /// suspended waiting for this resumption. When the resumption is resumed,
3288    /// its local continuation runs first; if it returns normally the frames
3289    /// are continued in order (innermost first).
3290    return_frames: Rc<Vec<CpsReturnFrame>>,
3291    /// The handler frame whose arm body created this resumption. `None`
3292    /// when the perform fell through to a synthetic static-fallback frame
3293    /// (no installed handler in active_handlers at perform time). Used by
3294    /// `merge_resumption_handlers` to place resume-site siblings at the
3295    /// correct stack depth.
3296    handled_anchor: Option<CpsHandlerAnchor>,
3297}
3298
3299struct CapturedPromptContinuation {
3300    handlers: Vec<CpsHandlerFrame>,
3301    return_frames: Vec<CpsReturnFrame>,
3302}
3303
3304/// A suspended caller continuation. Pushed by effectful terminators
3305/// (EffectfulCall / EffectfulApply / EffectfulForce) when a potentially-
3306/// effectful call is made inside a handler scope. Stored in
3307/// `CpsResumption.return_frames` so that resuming k also resumes the
3308/// caller's computation after the call.
3309#[derive(Debug, Clone, PartialEq)]
3310struct CpsReturnFrame {
3311    prompt_exit: Option<CpsPromptExitFrame>,
3312    owner_function: String,
3313    continuation: CpsContinuationId,
3314    values: Rc<Vec<Option<CpsRuntimeValue>>>,
3315    /// Handler snapshot at the effectful-call site, BEFORE the callee's
3316    /// `into_inherited` pass. Restored as-is (non-inherited frames remain
3317    /// non-inherited) when the frame is re-entered via `continue_return_frames`.
3318    active_handlers: Vec<CpsHandlerFrame>,
3319    guard_stack: Vec<CpsGuardEntry>,
3320    active_blocked: Vec<CpsBlockedEffect>,
3321    /// Threshold for the owner eval's `initial_frame_count` — i.e. how
3322    /// many of *its* return_frames were inherited from above when this
3323    /// frame was pushed. Restored when the owner is re-entered via
3324    /// `continue_return_frames` so the owner's Return only consumes its
3325    /// own pushed frames, not the ones it inherited.
3326    owner_initial_frame_count: usize,
3327    /// Identity of the eval frame that pushed this return frame. When
3328    /// `continue_return_frames` resumes the owner continuation, this id is
3329    /// restored as `current_eval_id` so `ScopeReturn` resolution targets
3330    /// the same eval frame that originally installed the matching handler
3331    /// (write22).
3332    owner_eval_id: CpsEvalId,
3333}
3334
3335#[derive(Debug, Clone, PartialEq)]
3336struct CpsPromptExitFrame {
3337    prompt: CpsPromptId,
3338}
3339
3340#[derive(Debug, Clone, PartialEq)]
3341struct CpsThunk {
3342    owner_function: String,
3343    entry: CpsContinuationId,
3344    values: Rc<Vec<Option<CpsRuntimeValue>>>,
3345    handlers: Rc<Vec<CpsHandlerFrame>>,
3346    guard_stack: Rc<Vec<CpsGuardEntry>>,
3347    blocked: Rc<Vec<CpsBlockedEffect>>,
3348}
3349
3350#[derive(Debug, Clone, PartialEq)]
3351struct CpsBlockedEffect {
3352    guard_id: u64,
3353    allowed: typed_ir::Type,
3354    active: bool,
3355}
3356
3357#[derive(Debug, Clone, PartialEq)]
3358struct CpsClosure {
3359    owner_function: String,
3360    entry: CpsContinuationId,
3361    values: Rc<Vec<Option<CpsRuntimeValue>>>,
3362    recursive_self: Option<CpsValueId>,
3363}
3364
3365#[derive(Debug, Clone, PartialEq)]
3366struct CpsHandlerFrame {
3367    /// Dynamic prompt id, fresh per scope install. Matched by `ScopeReturn`
3368    /// so recursive handler installs (e.g. `std::undet.once`'s `loop`) don't
3369    /// alias each other on the way up.
3370    prompt: CpsPromptId,
3371    handler: CpsHandlerId,
3372    guard_stack: Vec<CpsGuardEntry>,
3373    envs: Vec<CpsEvaluatedHandlerEnv>,
3374    /// Continuation in `escape_owner_function` reached when the handler scope
3375    /// completes. Set to `EXIT_RWH_TARGET` for frames pushed by
3376    /// `ResumeWithHandler` — they bottom out at the call site.
3377    escape: CpsContinuationId,
3378    /// Name of the function whose CPS body holds the `escape` continuation.
3379    /// `ScopeReturn` only resolves into a jump in that function's eval frame;
3380    /// elsewhere it must propagate up.
3381    escape_owner_function: String,
3382    /// `return_frames.len()` at the time this handler was installed. When a
3383    /// `ScopeReturn` jumps via this handler's escape, return_frames pushed
3384    /// after install are inside the handler's scope and must be discarded
3385    /// (delimited continuation semantics: non-local exit cuts the slice
3386    /// inside the handler). Without this, an early `sub::return` would
3387    /// still trigger trailing reject() in the saved frames.
3388    return_frame_threshold: usize,
3389    /// True iff this frame is part of a resumption snapshot (i.e. the
3390    /// `resumption.handlers` of a `Resume` / `ResumeWithHandler`). Kept as
3391    /// auxiliary info — `Perform`-time handler search still uses it. The
3392    /// primary check for `ScopeReturn` resolution is `install_eval_id`
3393    /// below, which is more precise.
3394    inherited: bool,
3395    /// Identity of the eval frame that ran `InstallHandler` to create this
3396    /// frame. A `ScopeReturn` only resolves at a handler whose
3397    /// `install_eval_id == current_eval_id`. Function identity is not
3398    /// enough: a multi-shot resumption can run the same CPS function in a
3399    /// fresh eval frame, and that fresh frame must not catch a handler
3400    /// installed by the original eval (write22).
3401    install_eval_id: CpsEvalId,
3402}
3403
3404#[derive(Debug, Clone, PartialEq)]
3405struct CpsEvaluatedHandlerEnv {
3406    entry: CpsContinuationId,
3407    values: Vec<(CpsValueId, CpsRuntimeValue)>,
3408}
3409
3410#[derive(Debug, Clone, PartialEq)]
3411struct CpsLatestHandlerEnv {
3412    handler: CpsHandlerId,
3413    entry: CpsContinuationId,
3414    target: CpsValueId,
3415    value: CpsRuntimeValue,
3416}
3417
3418#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3419struct CpsGuardEntry {
3420    var: runtime::EffectIdVar,
3421    id: u64,
3422}
3423
3424fn eval_literal(lit: &CpsLiteral) -> runtime::VmValue {
3425    match lit {
3426        CpsLiteral::Int(value) => runtime::VmValue::Int(value.clone()),
3427        CpsLiteral::Float(value) => runtime::VmValue::Float(value.clone()),
3428        CpsLiteral::String(value) => {
3429            runtime::VmValue::String(runtime::runtime::string_tree::StringTree::from_str(value))
3430        }
3431        CpsLiteral::Bool(value) => runtime::VmValue::Bool(*value),
3432        CpsLiteral::Unit => runtime::VmValue::Unit,
3433    }
3434}
3435
3436/// Evaluate a primitive op over CPS runtime values. List ops can carry
3437/// resumptions / closures / thunks, so the list/append/uncons family stays
3438/// inside the CpsRuntimeValue domain. Everything else falls back to the VM
3439/// primitive after converting args and result through `cps_value_to_vm` /
3440/// `cps_value_from_vm`.
3441fn eval_cps_primitive(
3442    op: typed_ir::PrimitiveOp,
3443    args: Vec<CpsRuntimeValue>,
3444) -> CpsEvalResult<CpsRuntimeValue> {
3445    use typed_ir::PrimitiveOp;
3446    match op {
3447        PrimitiveOp::ListEmpty => {
3448            // ListEmpty's runtime arity is 1 (a unit witness). Accept and
3449            // ignore the argument so list<resumption> empties land in the
3450            // CPS value domain.
3451            if args.len() > 1 {
3452                return Err(CpsEvalError::InvalidPrimitiveArity {
3453                    op,
3454                    expected: 1,
3455                    actual: args.len(),
3456                });
3457            }
3458            Ok(CpsRuntimeValue::List(Rc::new(Vec::new())))
3459        }
3460        PrimitiveOp::ListSingleton => {
3461            if args.len() != 1 {
3462                return Err(CpsEvalError::InvalidPrimitiveArity {
3463                    op,
3464                    expected: 1,
3465                    actual: args.len(),
3466                });
3467            }
3468            Ok(CpsRuntimeValue::List(Rc::new(vec![
3469                args.into_iter().next().unwrap(),
3470            ])))
3471        }
3472        PrimitiveOp::ListMerge => {
3473            if args.len() != 2 {
3474                return Err(CpsEvalError::InvalidPrimitiveArity {
3475                    op,
3476                    expected: 2,
3477                    actual: args.len(),
3478                });
3479            }
3480            let mut iter = args.into_iter();
3481            let left = iter.next().unwrap();
3482            let right = iter.next().unwrap();
3483            let mut merged = control_list_items(op, left)?;
3484            merged.extend(control_list_items(op, right)?);
3485            Ok(CpsRuntimeValue::List(Rc::new(merged)))
3486        }
3487        PrimitiveOp::ListLen => {
3488            if args.len() != 1 {
3489                return Err(CpsEvalError::InvalidPrimitiveArity {
3490                    op,
3491                    expected: 1,
3492                    actual: args.len(),
3493                });
3494            }
3495            let items = control_list_items(op, args.into_iter().next().unwrap())?;
3496            Ok(CpsRuntimeValue::Plain(runtime::VmValue::Int(
3497                items.len().to_string(),
3498            )))
3499        }
3500        PrimitiveOp::ListIndex => {
3501            if args.len() != 2 {
3502                return Err(CpsEvalError::InvalidPrimitiveArity {
3503                    op,
3504                    expected: 2,
3505                    actual: args.len(),
3506                });
3507            }
3508            let mut iter = args.into_iter();
3509            let list = iter.next().unwrap();
3510            let idx_val = iter.next().unwrap();
3511            let items = control_list_items(op, list)?;
3512            let idx = cps_value_to_usize(op, idx_val)?;
3513            items
3514                .into_iter()
3515                .nth(idx)
3516                .ok_or(CpsEvalError::UnsupportedPrimitive { op })
3517        }
3518        PrimitiveOp::ListIndexRangeRaw => {
3519            if args.len() != 3 {
3520                return Err(CpsEvalError::InvalidPrimitiveArity {
3521                    op,
3522                    expected: 3,
3523                    actual: args.len(),
3524                });
3525            }
3526            let mut iter = args.into_iter();
3527            let list = iter.next().unwrap();
3528            let start_val = iter.next().unwrap();
3529            let end_val = iter.next().unwrap();
3530            let items = control_list_items(op, list)?;
3531            let start = cps_value_to_usize(op, start_val)?;
3532            let end = cps_value_to_usize(op, end_val)?;
3533            Ok(CpsRuntimeValue::List(Rc::new(
3534                items.into_iter().skip(start).take(end - start).collect(),
3535            )))
3536        }
3537        _ => {
3538            let plain_args = args
3539                .into_iter()
3540                .map(cps_value_to_vm)
3541                .collect::<Option<Vec<_>>>()
3542                .ok_or(CpsEvalError::UnsupportedPrimitive { op })?;
3543            eval_primitive(op, &plain_args).map(cps_value_from_vm)
3544        }
3545    }
3546}
3547
3548fn cps_value_to_usize(op: typed_ir::PrimitiveOp, value: CpsRuntimeValue) -> CpsEvalResult<usize> {
3549    match value {
3550        CpsRuntimeValue::Plain(runtime::VmValue::Int(s)) => s
3551            .parse::<usize>()
3552            .map_err(|_| CpsEvalError::UnsupportedPrimitive { op }),
3553        _ => Err(CpsEvalError::UnsupportedPrimitive { op }),
3554    }
3555}
3556
3557fn control_list_items(
3558    op: typed_ir::PrimitiveOp,
3559    value: CpsRuntimeValue,
3560) -> CpsEvalResult<Vec<CpsRuntimeValue>> {
3561    match value {
3562        CpsRuntimeValue::List(items) => Ok(items.as_ref().clone()),
3563        CpsRuntimeValue::Plain(plain) => match plain {
3564            runtime::VmValue::List(list) => Ok(list
3565                .to_vec()
3566                .into_iter()
3567                .map(|item| cps_value_from_vm((*item).clone()))
3568                .collect()),
3569            other => Err(CpsEvalError::PrimitiveTypeMismatch { op, value: other }),
3570        },
3571        _ => Err(CpsEvalError::UnsupportedPrimitive { op }),
3572    }
3573}
3574
3575fn eval_primitive(
3576    op: typed_ir::PrimitiveOp,
3577    args: &[runtime::VmValue],
3578) -> CpsEvalResult<runtime::VmValue> {
3579    crate::eval::eval_primitive_for_abi(op, args).map_err(cps_eval_primitive_error)
3580}
3581
3582fn bool_value(op: typed_ir::PrimitiveOp, value: &runtime::VmValue) -> CpsEvalResult<bool> {
3583    match value {
3584        runtime::VmValue::Bool(value) => Ok(*value),
3585        value => Err(CpsEvalError::PrimitiveTypeMismatch {
3586            op,
3587            value: value.clone(),
3588        }),
3589    }
3590}
3591
3592fn cps_eval_primitive_error(error: crate::eval::NativeEvalError) -> CpsEvalError {
3593    match error {
3594        crate::eval::NativeEvalError::InvalidPrimitiveArity {
3595            op,
3596            expected,
3597            actual,
3598        } => CpsEvalError::InvalidPrimitiveArity {
3599            op,
3600            expected,
3601            actual,
3602        },
3603        crate::eval::NativeEvalError::PrimitiveTypeMismatch { op, value } => {
3604            CpsEvalError::PrimitiveTypeMismatch { op, value }
3605        }
3606        crate::eval::NativeEvalError::UnsupportedPrimitive { op } => {
3607            CpsEvalError::UnsupportedPrimitive { op }
3608        }
3609        other => unreachable!("native primitive evaluator returned non-primitive error: {other}"),
3610    }
3611}
3612
3613#[cfg(test)]
3614mod tests {
3615    use std::rc::Rc;
3616
3617    use crate::cps_ir::{
3618        CpsContinuation, CpsContinuationId, CpsFunction, CpsHandler, CpsHandlerArm, CpsHandlerId,
3619        CpsLiteral, CpsModule, CpsShotKind, CpsStmt, CpsTerminator, CpsValueId,
3620    };
3621    use crate::cps_validate::validate_cps_module;
3622
3623    use super::*;
3624
3625    #[test]
3626    fn evaluates_plain_value_primitives_through_native_evaluator() {
3627        let string_root = CpsFunction {
3628            name: "string-root".to_string(),
3629            params: Vec::new(),
3630            entry: CpsContinuationId(0),
3631            handlers: Vec::new(),
3632            continuations: vec![CpsContinuation {
3633                id: CpsContinuationId(0),
3634                params: Vec::new(),
3635                captures: Vec::new(),
3636                shot_kind: CpsShotKind::OneShot,
3637                stmts: vec![
3638                    CpsStmt::Literal {
3639                        dest: CpsValueId(0),
3640                        literal: CpsLiteral::String("aあ🙂z".to_string()),
3641                    },
3642                    CpsStmt::Literal {
3643                        dest: CpsValueId(1),
3644                        literal: CpsLiteral::Int("1".to_string()),
3645                    },
3646                    CpsStmt::Literal {
3647                        dest: CpsValueId(2),
3648                        literal: CpsLiteral::Int("3".to_string()),
3649                    },
3650                    CpsStmt::Literal {
3651                        dest: CpsValueId(3),
3652                        literal: CpsLiteral::String("bc".to_string()),
3653                    },
3654                    CpsStmt::Primitive {
3655                        dest: CpsValueId(4),
3656                        op: typed_ir::PrimitiveOp::StringSpliceRaw,
3657                        args: vec![CpsValueId(0), CpsValueId(1), CpsValueId(2), CpsValueId(3)],
3658                    },
3659                ],
3660                terminator: CpsTerminator::Return(CpsValueId(4)),
3661            }],
3662        };
3663        let list_root = CpsFunction {
3664            name: "list-root".to_string(),
3665            params: Vec::new(),
3666            entry: CpsContinuationId(0),
3667            handlers: Vec::new(),
3668            continuations: vec![CpsContinuation {
3669                id: CpsContinuationId(0),
3670                params: Vec::new(),
3671                captures: Vec::new(),
3672                shot_kind: CpsShotKind::OneShot,
3673                stmts: vec![
3674                    CpsStmt::Literal {
3675                        dest: CpsValueId(0),
3676                        literal: CpsLiteral::Int("1".to_string()),
3677                    },
3678                    CpsStmt::Literal {
3679                        dest: CpsValueId(1),
3680                        literal: CpsLiteral::Int("2".to_string()),
3681                    },
3682                    CpsStmt::Literal {
3683                        dest: CpsValueId(2),
3684                        literal: CpsLiteral::Int("3".to_string()),
3685                    },
3686                    CpsStmt::Literal {
3687                        dest: CpsValueId(3),
3688                        literal: CpsLiteral::Int("4".to_string()),
3689                    },
3690                    CpsStmt::Primitive {
3691                        dest: CpsValueId(4),
3692                        op: typed_ir::PrimitiveOp::ListSingleton,
3693                        args: vec![CpsValueId(0)],
3694                    },
3695                    CpsStmt::Primitive {
3696                        dest: CpsValueId(5),
3697                        op: typed_ir::PrimitiveOp::ListSingleton,
3698                        args: vec![CpsValueId(1)],
3699                    },
3700                    CpsStmt::Primitive {
3701                        dest: CpsValueId(6),
3702                        op: typed_ir::PrimitiveOp::ListMerge,
3703                        args: vec![CpsValueId(4), CpsValueId(5)],
3704                    },
3705                    CpsStmt::Primitive {
3706                        dest: CpsValueId(7),
3707                        op: typed_ir::PrimitiveOp::ListSingleton,
3708                        args: vec![CpsValueId(2)],
3709                    },
3710                    CpsStmt::Primitive {
3711                        dest: CpsValueId(8),
3712                        op: typed_ir::PrimitiveOp::ListMerge,
3713                        args: vec![CpsValueId(6), CpsValueId(7)],
3714                    },
3715                    CpsStmt::Primitive {
3716                        dest: CpsValueId(9),
3717                        op: typed_ir::PrimitiveOp::ListSingleton,
3718                        args: vec![CpsValueId(3)],
3719                    },
3720                    CpsStmt::Primitive {
3721                        dest: CpsValueId(10),
3722                        op: typed_ir::PrimitiveOp::ListMerge,
3723                        args: vec![CpsValueId(8), CpsValueId(9)],
3724                    },
3725                    CpsStmt::Literal {
3726                        dest: CpsValueId(11),
3727                        literal: CpsLiteral::Int("8".to_string()),
3728                    },
3729                    CpsStmt::Literal {
3730                        dest: CpsValueId(12),
3731                        literal: CpsLiteral::Int("9".to_string()),
3732                    },
3733                    CpsStmt::Primitive {
3734                        dest: CpsValueId(13),
3735                        op: typed_ir::PrimitiveOp::ListSingleton,
3736                        args: vec![CpsValueId(11)],
3737                    },
3738                    CpsStmt::Primitive {
3739                        dest: CpsValueId(14),
3740                        op: typed_ir::PrimitiveOp::ListSingleton,
3741                        args: vec![CpsValueId(12)],
3742                    },
3743                    CpsStmt::Primitive {
3744                        dest: CpsValueId(15),
3745                        op: typed_ir::PrimitiveOp::ListMerge,
3746                        args: vec![CpsValueId(13), CpsValueId(14)],
3747                    },
3748                    CpsStmt::Literal {
3749                        dest: CpsValueId(16),
3750                        literal: CpsLiteral::Int("1".to_string()),
3751                    },
3752                    CpsStmt::Literal {
3753                        dest: CpsValueId(17),
3754                        literal: CpsLiteral::Int("3".to_string()),
3755                    },
3756                    CpsStmt::Primitive {
3757                        dest: CpsValueId(18),
3758                        op: typed_ir::PrimitiveOp::ListSpliceRaw,
3759                        args: vec![
3760                            CpsValueId(10),
3761                            CpsValueId(16),
3762                            CpsValueId(17),
3763                            CpsValueId(15),
3764                        ],
3765                    },
3766                ],
3767                terminator: CpsTerminator::Return(CpsValueId(18)),
3768            }],
3769        };
3770        let module = CpsModule {
3771            functions: Vec::new(),
3772            roots: vec![string_root, list_root],
3773        };
3774
3775        validate_cps_module(&module).expect("valid CPS");
3776        assert_eq!(
3777            eval_cps_module(&module).expect("evaluated"),
3778            vec![
3779                runtime::VmValue::String(runtime::runtime::string_tree::StringTree::from_str(
3780                    "abcz",
3781                )),
3782                runtime::VmValue::List(runtime::runtime::list_tree::ListTree::from_items([
3783                    Rc::new(runtime::VmValue::Int("1".to_string())),
3784                    Rc::new(runtime::VmValue::Int("8".to_string())),
3785                    Rc::new(runtime::VmValue::Int("9".to_string())),
3786                    Rc::new(runtime::VmValue::Int("4".to_string())),
3787                ])),
3788            ]
3789        );
3790    }
3791
3792    #[test]
3793    fn evaluates_multishot_resumption_with_shared_snapshot() {
3794        let effect = typed_ir::Path::from_name(typed_ir::Name("choose".to_string()));
3795        let module = CpsModule {
3796            functions: Vec::new(),
3797            roots: vec![CpsFunction {
3798                name: "root".to_string(),
3799                params: Vec::new(),
3800                entry: CpsContinuationId(0),
3801                handlers: vec![CpsHandler {
3802                    id: CpsHandlerId(0),
3803                    arms: vec![CpsHandlerArm {
3804                        effect: effect.clone(),
3805                        entry: CpsContinuationId(2),
3806                    }],
3807                }],
3808                continuations: vec![
3809                    CpsContinuation {
3810                        id: CpsContinuationId(0),
3811                        params: Vec::new(),
3812                        captures: Vec::new(),
3813                        shot_kind: CpsShotKind::MultiShot,
3814                        stmts: vec![CpsStmt::Literal {
3815                            dest: CpsValueId(0),
3816                            literal: CpsLiteral::Int("1".to_string()),
3817                        }],
3818                        terminator: CpsTerminator::Perform {
3819                            effect,
3820                            payload: CpsValueId(0),
3821                            resume: CpsContinuationId(1),
3822                            handler: CpsHandlerId(0),
3823                            blocked: None,
3824                        },
3825                    },
3826                    CpsContinuation {
3827                        id: CpsContinuationId(1),
3828                        params: vec![CpsValueId(1)],
3829                        captures: Vec::new(),
3830                        shot_kind: CpsShotKind::MultiShot,
3831                        stmts: vec![
3832                            CpsStmt::Literal {
3833                                dest: CpsValueId(2),
3834                                literal: CpsLiteral::Int("10".to_string()),
3835                            },
3836                            CpsStmt::Primitive {
3837                                dest: CpsValueId(3),
3838                                op: typed_ir::PrimitiveOp::IntAdd,
3839                                args: vec![CpsValueId(1), CpsValueId(2)],
3840                            },
3841                        ],
3842                        terminator: CpsTerminator::Return(CpsValueId(3)),
3843                    },
3844                    CpsContinuation {
3845                        id: CpsContinuationId(2),
3846                        params: vec![CpsValueId(4), CpsValueId(5)],
3847                        captures: Vec::new(),
3848                        shot_kind: CpsShotKind::MultiShot,
3849                        stmts: vec![
3850                            CpsStmt::Literal {
3851                                dest: CpsValueId(6),
3852                                literal: CpsLiteral::Int("2".to_string()),
3853                            },
3854                            CpsStmt::Resume {
3855                                dest: CpsValueId(7),
3856                                resumption: CpsValueId(5),
3857                                arg: CpsValueId(4),
3858                            },
3859                            CpsStmt::Resume {
3860                                dest: CpsValueId(8),
3861                                resumption: CpsValueId(5),
3862                                arg: CpsValueId(6),
3863                            },
3864                            CpsStmt::Primitive {
3865                                dest: CpsValueId(9),
3866                                op: typed_ir::PrimitiveOp::IntAdd,
3867                                args: vec![CpsValueId(7), CpsValueId(8)],
3868                            },
3869                        ],
3870                        terminator: CpsTerminator::Return(CpsValueId(9)),
3871                    },
3872                ],
3873            }],
3874        };
3875
3876        validate_cps_module(&module).expect("valid CPS");
3877        assert_eq!(
3878            eval_cps_module(&module).expect("evaluated"),
3879            vec![runtime::VmValue::Int("23".to_string())]
3880        );
3881    }
3882
3883    #[test]
3884    fn evaluates_resumption_under_fresh_handler_stack() {
3885        let module = rebased_resumption_module();
3886
3887        validate_cps_module(&module).expect("valid CPS");
3888        assert_eq!(
3889            eval_cps_module(&module).expect("evaluated"),
3890            vec![runtime::VmValue::Int("13".to_string())]
3891        );
3892    }
3893
3894    fn rebased_resumption_module() -> CpsModule {
3895        let effect = typed_ir::Path::from_name(typed_ir::Name("choose".to_string()));
3896        CpsModule {
3897            functions: Vec::new(),
3898            roots: vec![CpsFunction {
3899                name: "root".to_string(),
3900                params: Vec::new(),
3901                entry: CpsContinuationId(0),
3902                handlers: vec![
3903                    CpsHandler {
3904                        id: CpsHandlerId(0),
3905                        arms: vec![CpsHandlerArm {
3906                            effect: effect.clone(),
3907                            entry: CpsContinuationId(2),
3908                        }],
3909                    },
3910                    CpsHandler {
3911                        id: CpsHandlerId(1),
3912                        arms: vec![CpsHandlerArm {
3913                            effect: effect.clone(),
3914                            entry: CpsContinuationId(4),
3915                        }],
3916                    },
3917                ],
3918                continuations: vec![
3919                    CpsContinuation {
3920                        id: CpsContinuationId(0),
3921                        params: Vec::new(),
3922                        captures: Vec::new(),
3923                        shot_kind: CpsShotKind::MultiShot,
3924                        stmts: vec![CpsStmt::Literal {
3925                            dest: CpsValueId(0),
3926                            literal: CpsLiteral::Int("1".to_string()),
3927                        }],
3928                        terminator: CpsTerminator::Perform {
3929                            effect: effect.clone(),
3930                            payload: CpsValueId(0),
3931                            resume: CpsContinuationId(1),
3932                            handler: CpsHandlerId(0),
3933                            blocked: None,
3934                        },
3935                    },
3936                    CpsContinuation {
3937                        id: CpsContinuationId(1),
3938                        params: vec![CpsValueId(1)],
3939                        captures: Vec::new(),
3940                        shot_kind: CpsShotKind::MultiShot,
3941                        stmts: vec![CpsStmt::Literal {
3942                            dest: CpsValueId(2),
3943                            literal: CpsLiteral::Int("2".to_string()),
3944                        }],
3945                        terminator: CpsTerminator::Perform {
3946                            effect: effect.clone(),
3947                            payload: CpsValueId(2),
3948                            resume: CpsContinuationId(3),
3949                            handler: CpsHandlerId(0),
3950                            blocked: None,
3951                        },
3952                    },
3953                    CpsContinuation {
3954                        id: CpsContinuationId(2),
3955                        params: vec![CpsValueId(4), CpsValueId(5)],
3956                        captures: Vec::new(),
3957                        shot_kind: CpsShotKind::MultiShot,
3958                        stmts: vec![CpsStmt::ResumeWithHandler {
3959                            dest: CpsValueId(6),
3960                            resumption: CpsValueId(5),
3961                            arg: CpsValueId(4),
3962                            handler: CpsHandlerId(1),
3963                            envs: Vec::new(),
3964                        }],
3965                        terminator: CpsTerminator::Return(CpsValueId(6)),
3966                    },
3967                    CpsContinuation {
3968                        id: CpsContinuationId(3),
3969                        params: vec![CpsValueId(9)],
3970                        captures: vec![CpsValueId(1)],
3971                        shot_kind: CpsShotKind::MultiShot,
3972                        stmts: vec![CpsStmt::Primitive {
3973                            dest: CpsValueId(13),
3974                            op: typed_ir::PrimitiveOp::IntAdd,
3975                            args: vec![CpsValueId(1), CpsValueId(9)],
3976                        }],
3977                        terminator: CpsTerminator::Return(CpsValueId(13)),
3978                    },
3979                    CpsContinuation {
3980                        id: CpsContinuationId(4),
3981                        params: vec![CpsValueId(7), CpsValueId(8)],
3982                        captures: Vec::new(),
3983                        shot_kind: CpsShotKind::MultiShot,
3984                        stmts: vec![
3985                            CpsStmt::Literal {
3986                                dest: CpsValueId(10),
3987                                literal: CpsLiteral::Int("10".to_string()),
3988                            },
3989                            CpsStmt::Primitive {
3990                                dest: CpsValueId(11),
3991                                op: typed_ir::PrimitiveOp::IntAdd,
3992                                args: vec![CpsValueId(7), CpsValueId(10)],
3993                            },
3994                            CpsStmt::Resume {
3995                                dest: CpsValueId(12),
3996                                resumption: CpsValueId(8),
3997                                arg: CpsValueId(11),
3998                            },
3999                        ],
4000                        terminator: CpsTerminator::Return(CpsValueId(12)),
4001                    },
4002                ],
4003            }],
4004        }
4005    }
4006}