Skip to main content

majit_trace/
history.rs

1/// The Trace data structure — a completed sequence of IR operations.
2///
3/// A Trace is the output of the Trace and the input to the
4/// optimizer and backend. It represents a linear sequence of operations
5/// that forms a loop (ending with JUMP) or an exit (ending with FINISH).
6///
7/// Reference: rpython/jit/metainterp/history.py TreeLoop
8use majit_ir::{InputArg, Op, OpCode, OpRef};
9
10/// RPython `History` parity alias for `TreeLoop`.
11///
12/// The Rust implementation keeps the historical `TreeLoop` name for naming
13/// consistency with `rpython/jit/metainterp/pyjitpl.py` internals, while
14/// `History` is retained for direct RPython call-site parity.
15pub type History = TreeLoop;
16
17/// A completed trace ready for optimization and compilation.
18#[derive(Clone, Debug)]
19pub struct TreeLoop {
20    /// Input arguments to the trace (loop header variables).
21    pub inputargs: Vec<InputArg>,
22    /// The recorded operations, in execution order.
23    pub ops: Vec<Op>,
24    /// opencoder.py parity: per-guard snapshots captured during tracing.
25    /// Indexed by the guard op's `rd_resume_position`.
26    pub snapshots: Vec<crate::recorder::Snapshot>,
27}
28
29impl TreeLoop {
30    /// Create a new trace from input arguments and operations.
31    pub fn new(inputargs: Vec<InputArg>, ops: Vec<Op>) -> Self {
32        TreeLoop {
33            inputargs,
34            ops,
35            snapshots: Vec::new(),
36        }
37    }
38
39    /// Create a new trace with snapshots.
40    pub fn with_snapshots(
41        inputargs: Vec<InputArg>,
42        ops: Vec<Op>,
43        snapshots: Vec<crate::recorder::Snapshot>,
44    ) -> Self {
45        TreeLoop {
46            inputargs,
47            ops,
48            snapshots,
49        }
50    }
51
52    /// Number of operations in the trace.
53    pub fn num_ops(&self) -> usize {
54        self.ops.len()
55    }
56
57    /// Number of input arguments.
58    pub fn num_inputargs(&self) -> usize {
59        self.inputargs.len()
60    }
61
62    /// Whether this trace ends with a JUMP (i.e., is a loop).
63    pub fn is_loop(&self) -> bool {
64        self.ops.last().is_some_and(|op| op.opcode == OpCode::Jump)
65    }
66
67    /// Whether this trace ends with FINISH.
68    pub fn is_finished(&self) -> bool {
69        self.ops
70            .last()
71            .is_some_and(|op| op.opcode == OpCode::Finish)
72    }
73
74    /// Get a reference to the operation at the given OpRef index.
75    pub fn get_op(&self, opref: OpRef) -> Option<&Op> {
76        self.ops.get(opref.0 as usize)
77    }
78
79    /// Iterate over all operations.
80    pub fn iter_ops(&self) -> impl Iterator<Item = &Op> {
81        self.ops.iter()
82    }
83
84    /// Iterate over all guard operations.
85    pub fn iter_guards(&self) -> impl Iterator<Item = &Op> {
86        self.ops.iter().filter(|op| op.opcode.is_guard())
87    }
88
89    /// Number of guard operations.
90    pub fn num_guards(&self) -> usize {
91        self.ops.iter().filter(|op| op.opcode.is_guard()).count()
92    }
93
94    /// Get the final operation (Jump or Finish).
95    pub fn get_final_op(&self) -> Option<&Op> {
96        self.ops.last().filter(|op| op.opcode.is_final())
97    }
98
99    /// Get the Label position (if this is a peeled loop).
100    pub fn find_label(&self) -> Option<usize> {
101        self.ops.iter().position(|op| op.opcode == OpCode::Label)
102    }
103
104    /// Split at Label: returns (preamble_ops, body_ops).
105    /// If no Label, returns (all_ops, empty).
106    pub fn split_at_label(&self) -> (&[Op], &[Op]) {
107        match self.find_label() {
108            Some(pos) => (&self.ops[..pos], &self.ops[pos..]),
109            None => (&self.ops, &[]),
110        }
111    }
112
113    /// Get the input arg types.
114    pub fn inputarg_types(&self) -> Vec<majit_ir::Type> {
115        self.inputargs.iter().map(|ia| ia.tp).collect()
116    }
117
118    /// Create a trace iterator for this trace.
119    pub fn get_iter(&self) -> crate::opencoder::TraceIterator<'_> {
120        crate::opencoder::TraceIterator::new(&self.ops)
121    }
122
123    /// history.py: check_consistency()
124    /// Verify that the trace structure is valid.
125    pub fn check_consistency(&self) -> bool {
126        if self.ops.is_empty() {
127            return true;
128        }
129        // Last op must be Jump or Finish
130        let last = self.ops.last().unwrap();
131        if !last.opcode.is_final() {
132            return false;
133        }
134        // No duplicate positions
135        let mut seen = std::collections::HashSet::new();
136        for op in &self.ops {
137            if !op.pos.is_none() {
138                if !seen.insert(op.pos) {
139                    return false; // duplicate position
140                }
141            }
142        }
143        true
144    }
145
146    /// Get all OpRefs used as arguments but not defined by any op.
147    /// These are the "free variables" — inputs from outside the trace.
148    pub fn free_vars(&self) -> Vec<OpRef> {
149        let defined: std::collections::HashSet<OpRef> = self
150            .ops
151            .iter()
152            .filter(|op| !op.pos.is_none())
153            .map(|op| op.pos)
154            .collect();
155        let mut free = std::collections::HashSet::new();
156        for op in &self.ops {
157            for arg in &op.args {
158                if !arg.is_none() && !defined.contains(arg) {
159                    free.insert(*arg);
160                }
161            }
162        }
163        let mut result: Vec<OpRef> = free.into_iter().collect();
164        result.sort_by_key(|r| r.0);
165        result
166    }
167
168    /// Count operations by opcode category.
169    pub fn count_by_category(&self) -> (usize, usize, usize, usize) {
170        let mut guards = 0;
171        let mut pure = 0;
172        let mut calls = 0;
173        let mut other = 0;
174        for op in &self.ops {
175            if op.opcode.is_guard() {
176                guards += 1;
177            } else if op.opcode.is_always_pure() {
178                pure += 1;
179            } else if op.opcode.is_call() {
180                calls += 1;
181            } else {
182                other += 1;
183            }
184        }
185        (guards, pure, calls, other)
186    }
187
188    /// opencoder.py CutTrace parity — create a new trace by cutting at the
189    /// given position. `original_boxes` become the new inputargs; any OpRef
190    /// referenced after the cut but defined before it (and not in
191    /// `original_boxes`) is re-emitted as a prefix operation (transitive
192    /// closure of dependencies).
193    pub fn cut_trace_from(
194        &self,
195        start: crate::recorder::TracePosition,
196        original_boxes: &[OpRef],
197        original_box_types: &[majit_ir::Type],
198    ) -> TreeLoop {
199        use std::collections::{HashMap, HashSet, VecDeque};
200
201        let num_original_inputargs = self.inputargs.len() as u32;
202        let cut_ops = &self.ops[start.ops_len..];
203
204        // Phase 1: Build initial remap from original_boxes → new inputargs.
205        let mut remap: HashMap<OpRef, OpRef> = HashMap::new();
206        let original_set: HashSet<OpRef> = original_boxes.iter().copied().collect();
207        for (i, &old_ref) in original_boxes.iter().enumerate() {
208            remap.insert(old_ref, OpRef(i as u32));
209        }
210
211        // Collect all OpRefs defined by post-cut ops.
212        let defined_after_cut: HashSet<OpRef> = cut_ops
213            .iter()
214            .filter(|op| !op.pos.is_none())
215            .map(|op| op.pos)
216            .collect();
217
218        // Phase 2: Find escaped refs — referenced after cut, defined before
219        // cut, not in original_boxes. Use BFS for transitive closure: an
220        // escaped op's own args may also be escaped.
221        let is_pre_cut_ref = |r: &OpRef| -> bool {
222            !r.is_none()
223                && r.0 < 10_000
224                && !original_set.contains(r)
225                && !defined_after_cut.contains(r)
226        };
227
228        let mut escaped_set: HashSet<OpRef> = HashSet::new();
229        let mut queue: VecDeque<OpRef> = VecDeque::new();
230
231        // Seed with refs used by post-cut ops (args only, not fail_args).
232        // RPython CutTrace parity: pre-cut refs in fail_args map to
233        // OpRef::NONE (resume data handles materialization). Only regular
234        // op args seed escaped refs for prefix re-emission.
235        for op in cut_ops {
236            for arg in &op.args {
237                if is_pre_cut_ref(arg) && escaped_set.insert(*arg) {
238                    queue.push_back(*arg);
239                }
240            }
241        }
242
243        // BFS: transitively collect dependencies of escaped ops.
244        while let Some(esc_ref) = queue.pop_front() {
245            if esc_ref.0 < num_original_inputargs {
246                // Original inputarg of the full trace — must become a new
247                // inputarg (handled in phase 3 below).
248                continue;
249            }
250            let op_idx = (esc_ref.0 - num_original_inputargs) as usize;
251            if let Some(op) = self.ops.get(op_idx) {
252                for arg in &op.args {
253                    if is_pre_cut_ref(arg) && escaped_set.insert(*arg) {
254                        queue.push_back(*arg);
255                    }
256                }
257            }
258        }
259
260        // Phase 3: Partition escaped refs.
261        //  - "orig_inputarg_escaped": refs to the full trace's original inputargs
262        //    that weren't in original_boxes → must become new inputargs.
263        //  - "op_escaped": refs to pre-cut ops → re-emit as prefix operations.
264        let mut orig_inputarg_escaped: Vec<OpRef> = Vec::new();
265        let mut op_escaped: Vec<OpRef> = Vec::new();
266        for &r in &escaped_set {
267            if r.0 < num_original_inputargs {
268                orig_inputarg_escaped.push(r);
269            } else {
270                op_escaped.push(r);
271            }
272        }
273        orig_inputarg_escaped.sort_by_key(|r| r.0);
274        op_escaped.sort_by_key(|r| r.0); // preserve original order
275
276        // Phase 4: Build new inputargs = original_boxes + escaped inputargs.
277        let mut new_ia_boxes = original_boxes.to_vec();
278        let mut new_ia_types = original_box_types.to_vec();
279        for &r in &orig_inputarg_escaped {
280            remap.insert(r, OpRef(new_ia_boxes.len() as u32));
281            new_ia_boxes.push(r);
282            new_ia_types.push(self.inputargs[r.0 as usize].tp);
283        }
284        let new_inputargs_count = new_ia_boxes.len() as u32;
285
286        let new_inputargs: Vec<InputArg> = new_ia_types
287            .iter()
288            .enumerate()
289            .map(|(i, &tp)| InputArg {
290                index: i as u32,
291                tp,
292            })
293            .collect();
294
295        // Phase 5: Re-emit escaped ops as prefix, assigning fresh OpRefs.
296        let mut next_ref = new_inputargs_count;
297        for &r in &op_escaped {
298            remap.insert(r, OpRef(next_ref));
299            next_ref += 1;
300        }
301
302        // Also assign fresh refs for post-cut ops (shifted by prefix count).
303        let prefix_count = op_escaped.len() as u32;
304        for (i, op) in cut_ops.iter().enumerate() {
305            if !op.pos.is_none() {
306                remap.insert(op.pos, OpRef(new_inputargs_count + prefix_count + i as u32));
307            }
308        }
309
310        let remap_ref = |r: &OpRef| -> OpRef {
311            if r.is_none() || r.0 >= 10_000 {
312                *r
313            } else if let Some(&new_ref) = remap.get(r) {
314                new_ref
315            } else {
316                OpRef::NONE
317            }
318        };
319
320        // Build prefix ops (re-emitted escaped definitions).
321        let mut prefix_ops: Vec<Op> = Vec::with_capacity(op_escaped.len());
322        for (pi, &r) in op_escaped.iter().enumerate() {
323            let op_idx = (r.0 - num_original_inputargs) as usize;
324            let orig_op = &self.ops[op_idx];
325            let mut new_op = orig_op.clone();
326            new_op.pos = OpRef(new_inputargs_count + pi as u32);
327            for arg in new_op.args.iter_mut() {
328                *arg = remap_ref(arg);
329            }
330            // Prefix ops don't need fail_args (they're not guards).
331            new_op.fail_args = None;
332            prefix_ops.push(new_op);
333        }
334
335        // Phase 6: Remap post-cut ops.
336        let mut new_ops: Vec<Op> = Vec::with_capacity(prefix_ops.len() + cut_ops.len());
337        new_ops.extend(prefix_ops);
338        for (i, op) in cut_ops.iter().enumerate() {
339            let mut new_op = op.clone();
340            new_op.pos = OpRef(new_inputargs_count + prefix_count + i as u32);
341            for arg in new_op.args.iter_mut() {
342                *arg = remap_ref(arg);
343            }
344            if let Some(ref mut fa) = new_op.fail_args {
345                for arg in fa.iter_mut() {
346                    *arg = remap_ref(arg);
347                }
348            }
349            new_ops.push(new_op);
350        }
351
352        // opencoder.py parity: carry snapshots through cut_trace_from.
353        // RPython's CutTrace wraps the original trace and adjusts iteration
354        // start, preserving snapshot access for the optimizer's
355        // store_final_boxes_in_guard (resume.py:ResumeDataVirtualAdder.finish).
356        // Without snapshots, bridge/loop guards fall back to fail_args-only
357        // rd_numb (fewer slots than the full frame → SIGSEGV on recovery).
358        TreeLoop::with_snapshots(new_inputargs, new_ops, self.snapshots.clone())
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use majit_ir::Type;
366
367    #[test]
368    fn test_empty_trace() {
369        let trace = TreeLoop::new(vec![], vec![]);
370        assert_eq!(trace.num_ops(), 0);
371        assert_eq!(trace.num_inputargs(), 0);
372        assert!(!trace.is_loop());
373        assert!(!trace.is_finished());
374    }
375
376    #[test]
377    fn test_trace_with_jump() {
378        let inputargs = vec![InputArg::new_int(0)];
379        let ops = vec![
380            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
381            Op::new(OpCode::Jump, &[OpRef(1)]),
382        ];
383        let trace = TreeLoop::new(inputargs, ops);
384        assert!(trace.is_loop());
385        assert!(!trace.is_finished());
386        assert_eq!(trace.num_ops(), 2);
387        assert_eq!(trace.num_inputargs(), 1);
388    }
389
390    #[test]
391    fn test_trace_with_finish() {
392        let inputargs = vec![InputArg::new_int(0)];
393        let ops = vec![
394            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
395            Op::new(OpCode::Finish, &[OpRef(1)]),
396        ];
397        let trace = TreeLoop::new(inputargs, ops);
398        assert!(!trace.is_loop());
399        assert!(trace.is_finished());
400    }
401
402    #[test]
403    fn test_inputarg_types() {
404        let inputargs = vec![
405            InputArg::new_int(0),
406            InputArg::new_ref(1),
407            InputArg::new_float(2),
408        ];
409        let trace = TreeLoop::new(inputargs, vec![]);
410        assert_eq!(trace.inputargs[0].tp, Type::Int);
411        assert_eq!(trace.inputargs[1].tp, Type::Ref);
412        assert_eq!(trace.inputargs[2].tp, Type::Float);
413    }
414
415    // ══════════════════════════════════════════════════════════════════
416    // History / TreeLoop parity tests
417    // Ported from rpython/jit/metainterp/test/test_history.py
418    // ══════════════════════════════════════════════════════════════════
419
420    #[test]
421    fn test_trace_structure_inputargs_and_ops() {
422        // TreeLoop has inputargs and operations as primary fields.
423        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
424        let ops = vec![
425            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
426            Op::new(OpCode::IntSub, &[OpRef(2), OpRef(0)]),
427            Op::new(OpCode::Jump, &[OpRef(3), OpRef(1)]),
428        ];
429        let trace = TreeLoop::new(inputargs, ops);
430
431        assert_eq!(trace.num_inputargs(), 2);
432        assert_eq!(trace.num_ops(), 3);
433        assert!(trace.is_loop());
434    }
435
436    #[test]
437    fn test_trace_guards_can_have_fail_args() {
438        // Guards in a trace carry fail_args.
439        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
440        let mut guard = Op::new(OpCode::GuardTrue, &[OpRef(0)]);
441        guard.fail_args = Some(smallvec::smallvec![OpRef(0), OpRef(1)]);
442
443        let ops = vec![
444            guard,
445            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
446            Op::new(OpCode::Jump, &[OpRef(2), OpRef(1)]),
447        ];
448        let trace = TreeLoop::new(inputargs, ops);
449
450        let guards: Vec<_> = trace.iter_guards().collect();
451        assert_eq!(guards.len(), 1);
452        let fa = guards[0].fail_args.as_ref().unwrap();
453        assert_eq!(fa.len(), 2);
454        assert_eq!(fa[0], OpRef(0));
455        assert_eq!(fa[1], OpRef(1));
456    }
457
458    #[test]
459    fn test_trace_get_op() {
460        // get_op retrieves ops by index.
461        let inputargs = vec![InputArg::new_int(0)];
462        let ops = vec![
463            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
464            Op::new(OpCode::Jump, &[OpRef(1)]),
465        ];
466        let trace = TreeLoop::new(inputargs, ops);
467
468        let op0 = trace.get_op(OpRef(0)).unwrap();
469        assert_eq!(op0.opcode, OpCode::IntAdd);
470
471        let op1 = trace.get_op(OpRef(1)).unwrap();
472        assert_eq!(op1.opcode, OpCode::Jump);
473
474        assert!(trace.get_op(OpRef(99)).is_none());
475    }
476
477    #[test]
478    fn test_trace_iter_guards_filters_correctly() {
479        // iter_guards returns only guard ops.
480        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
481        let ops = vec![
482            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
483            Op::new(OpCode::GuardTrue, &[OpRef(2)]),
484            Op::new(OpCode::IntSub, &[OpRef(2), OpRef(0)]),
485            Op::new(OpCode::GuardFalse, &[OpRef(3)]),
486            Op::new(OpCode::Jump, &[OpRef(3), OpRef(1)]),
487        ];
488        let trace = TreeLoop::new(inputargs, ops);
489
490        let guards: Vec<_> = trace.iter_guards().collect();
491        assert_eq!(guards.len(), 2);
492        assert_eq!(guards[0].opcode, OpCode::GuardTrue);
493        assert_eq!(guards[1].opcode, OpCode::GuardFalse);
494    }
495
496    #[test]
497    fn test_trace_not_loop_not_finished() {
498        // A trace without Jump or Finish is neither loop nor finished.
499        let inputargs = vec![InputArg::new_int(0)];
500        let ops = vec![Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)])];
501        let trace = TreeLoop::new(inputargs, ops);
502        assert!(!trace.is_loop());
503        assert!(!trace.is_finished());
504    }
505
506    #[test]
507    fn test_trace_loop_vs_finish_exclusive() {
508        // A trace cannot be both a loop and finished.
509        let inputargs = vec![InputArg::new_int(0)];
510
511        let loop_trace = TreeLoop::new(
512            inputargs.clone(),
513            vec![
514                Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
515                Op::new(OpCode::Jump, &[OpRef(1)]),
516            ],
517        );
518        assert!(loop_trace.is_loop());
519        assert!(!loop_trace.is_finished());
520
521        let finish_trace = TreeLoop::new(
522            inputargs,
523            vec![
524                Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
525                Op::new(OpCode::Finish, &[OpRef(1)]),
526            ],
527        );
528        assert!(!finish_trace.is_loop());
529        assert!(finish_trace.is_finished());
530    }
531
532    #[test]
533    fn test_trace_mixed_type_inputargs() {
534        // Traces support mixed-type input arguments (int, ref, float).
535        let inputargs = vec![
536            InputArg::new_int(0),
537            InputArg::new_ref(1),
538            InputArg::new_float(2),
539        ];
540        let ops = vec![Op::new(OpCode::Jump, &[OpRef(0), OpRef(1), OpRef(2)])];
541        let trace = TreeLoop::new(inputargs, ops);
542
543        assert_eq!(trace.num_inputargs(), 3);
544        assert_eq!(trace.inputargs[0].tp, Type::Int);
545        assert_eq!(trace.inputargs[1].tp, Type::Ref);
546        assert_eq!(trace.inputargs[2].tp, Type::Float);
547        assert!(trace.is_loop());
548    }
549
550    #[test]
551    fn test_trace_multiple_guards_with_different_fail_args() {
552        // Multiple guards can have different fail_args.
553        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
554
555        let mut g0 = Op::new(OpCode::GuardTrue, &[OpRef(0)]);
556        g0.fail_args = Some(smallvec::smallvec![OpRef(0)]);
557
558        let mut g1 = Op::new(OpCode::GuardFalse, &[OpRef(1)]);
559        g1.fail_args = Some(smallvec::smallvec![OpRef(0), OpRef(1)]);
560
561        let ops = vec![
562            g0,
563            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
564            g1,
565            Op::new(OpCode::Jump, &[OpRef(2), OpRef(1)]),
566        ];
567        let trace = TreeLoop::new(inputargs, ops);
568
569        let guards: Vec<_> = trace.iter_guards().collect();
570        assert_eq!(guards.len(), 2);
571
572        assert_eq!(guards[0].fail_args.as_ref().unwrap().len(), 1);
573        assert_eq!(guards[1].fail_args.as_ref().unwrap().len(), 2);
574    }
575
576    #[test]
577    fn test_trace_guard_without_fail_args() {
578        // Guards without explicitly set fail_args have None.
579        let inputargs = vec![InputArg::new_int(0)];
580        let ops = vec![
581            Op::new(OpCode::GuardTrue, &[OpRef(0)]),
582            Op::new(OpCode::Jump, &[OpRef(0)]),
583        ];
584        let trace = TreeLoop::new(inputargs, ops);
585
586        let guards: Vec<_> = trace.iter_guards().collect();
587        assert_eq!(guards.len(), 1);
588        assert!(guards[0].fail_args.is_none());
589    }
590
591    #[test]
592    fn test_trace_ops_have_correct_opcodes() {
593        // iter_ops preserves op order and opcodes.
594        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
595        let ops = vec![
596            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
597            Op::new(OpCode::IntMul, &[OpRef(2), OpRef(0)]),
598            Op::new(OpCode::IntSub, &[OpRef(3), OpRef(1)]),
599            Op::new(OpCode::Jump, &[OpRef(4), OpRef(1)]),
600        ];
601        let trace = TreeLoop::new(inputargs, ops);
602
603        let opcodes: Vec<_> = trace.iter_ops().map(|op| op.opcode).collect();
604        assert_eq!(
605            opcodes,
606            vec![OpCode::IntAdd, OpCode::IntMul, OpCode::IntSub, OpCode::Jump]
607        );
608    }
609
610    // ══════════════════════════════════════════════════════════════════
611    // History breadth tests — deeper parity with test_history.py
612    // ══════════════════════════════════════════════════════════════════
613
614    #[test]
615    fn test_trace_ops_with_descrs() {
616        // Ops can carry descriptors (field descrs, call descrs).
617        use majit_ir::DescrRef;
618        use std::sync::Arc;
619
620        #[derive(Debug)]
621        struct TestDescr(u32);
622        impl majit_ir::Descr for TestDescr {
623            fn index(&self) -> u32 {
624                self.0
625            }
626        }
627
628        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
629        let descr: DescrRef = Arc::new(TestDescr(42));
630        let ops = vec![
631            Op::with_descr(OpCode::CallI, &[OpRef(0)], descr.clone()),
632            Op::with_descr(OpCode::GuardTrue, &[OpRef(0)], descr.clone()),
633            Op::new(OpCode::Jump, &[OpRef(0), OpRef(1)]),
634        ];
635        let trace = TreeLoop::new(inputargs, ops);
636
637        // Call op has descr
638        assert!(trace.ops[0].descr.is_some());
639        assert_eq!(trace.ops[0].descr.as_ref().unwrap().index(), 42);
640        // Guard op has descr
641        assert!(trace.ops[1].descr.is_some());
642        assert_eq!(trace.ops[1].descr.as_ref().unwrap().index(), 42);
643        // Jump op has no descr
644        assert!(trace.ops[2].descr.is_none());
645    }
646
647    #[test]
648    fn test_trace_iteration_order_matches_recording() {
649        // Iteration order must match the order in which ops were recorded.
650        let inputargs = vec![InputArg::new_int(0)];
651        let expected_opcodes = vec![
652            OpCode::IntAdd,
653            OpCode::IntSub,
654            OpCode::IntMul,
655            OpCode::IntNeg,
656            OpCode::IntLt,
657            OpCode::Jump,
658        ];
659        let ops = vec![
660            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
661            Op::new(OpCode::IntSub, &[OpRef(1), OpRef(0)]),
662            Op::new(OpCode::IntMul, &[OpRef(2), OpRef(0)]),
663            Op::new(OpCode::IntNeg, &[OpRef(3)]),
664            Op::new(OpCode::IntLt, &[OpRef(4), OpRef(0)]),
665            Op::new(OpCode::Jump, &[OpRef(4)]),
666        ];
667        let trace = TreeLoop::new(inputargs, ops);
668
669        let actual: Vec<_> = trace.iter_ops().map(|op| op.opcode).collect();
670        assert_eq!(actual, expected_opcodes);
671    }
672
673    #[test]
674    fn test_trace_is_immutable_snapshot() {
675        // After creation, Trace fields are only accessible as immutable references.
676        // Verify that cloning a trace produces an independent copy.
677        let inputargs = vec![InputArg::new_int(0)];
678        let ops = vec![
679            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
680            Op::new(OpCode::Jump, &[OpRef(1)]),
681        ];
682        let trace = TreeLoop::new(inputargs, ops);
683        let trace2 = trace.clone();
684
685        assert_eq!(trace.num_ops(), trace2.num_ops());
686        assert_eq!(trace.num_inputargs(), trace2.num_inputargs());
687        assert_eq!(trace.is_loop(), trace2.is_loop());
688    }
689
690    #[test]
691    fn test_trace_stress_100_ops() {
692        // Stress test: a trace with 100+ operations.
693        let inputargs = vec![InputArg::new_int(0)];
694        let mut ops = Vec::new();
695        let mut prev = OpRef(0);
696        for i in 0..100 {
697            let mut op = Op::new(OpCode::IntAdd, &[prev, OpRef(0)]);
698            op.pos = OpRef(i + 1);
699            ops.push(op);
700            prev = OpRef(i + 1);
701        }
702        ops.push(Op::new(OpCode::Jump, &[prev]));
703        let trace = TreeLoop::new(inputargs, ops);
704
705        assert_eq!(trace.num_ops(), 101); // 100 IntAdd + 1 Jump
706        assert!(trace.is_loop());
707
708        // Verify first and last ops
709        assert_eq!(trace.ops[0].opcode, OpCode::IntAdd);
710        assert_eq!(trace.ops[99].opcode, OpCode::IntAdd);
711        assert_eq!(trace.ops[100].opcode, OpCode::Jump);
712
713        // All intermediate ops should be IntAdd
714        for op in &trace.ops[..100] {
715            assert_eq!(op.opcode, OpCode::IntAdd);
716        }
717    }
718
719    #[test]
720    fn test_trace_guard_fail_args_reference_valid_refs() {
721        // fail_args must reference valid input or op refs.
722        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
723
724        let add_op = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]);
725        let mut guard_op = Op::new(OpCode::GuardTrue, &[OpRef(2)]);
726        // fail_args referencing input args (0, 1) and the add result (2)
727        guard_op.fail_args = Some(smallvec::smallvec![OpRef(0), OpRef(1), OpRef(2)]);
728
729        let ops = vec![
730            add_op,
731            guard_op,
732            Op::new(OpCode::Jump, &[OpRef(2), OpRef(1)]),
733        ];
734        let trace = TreeLoop::new(inputargs, ops);
735
736        let guard = trace.iter_guards().next().unwrap();
737        let fa = guard.fail_args.as_ref().unwrap();
738        // All referenced OpRefs are valid: 0, 1 are inputargs; 2 is the add op
739        assert!(fa.iter().all(|r| r.0 <= 2));
740        assert_eq!(fa.len(), 3);
741    }
742
743    #[test]
744    fn test_trace_many_guards_with_varying_fail_args() {
745        // Multiple guards with varying fail_args sizes.
746        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
747
748        let mut g0 = Op::new(OpCode::GuardTrue, &[OpRef(0)]);
749        g0.fail_args = Some(smallvec::smallvec![]);
750
751        let mut g1 = Op::new(OpCode::GuardFalse, &[OpRef(1)]);
752        g1.fail_args = Some(smallvec::smallvec![OpRef(0)]);
753
754        let add = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]);
755
756        let mut g2 = Op::new(OpCode::GuardTrue, &[OpRef(0)]);
757        g2.fail_args = Some(smallvec::smallvec![OpRef(0), OpRef(1), OpRef(2)]);
758
759        let ops = vec![
760            g0,
761            g1,
762            add,
763            g2,
764            Op::new(OpCode::Jump, &[OpRef(0), OpRef(1)]),
765        ];
766        let trace = TreeLoop::new(inputargs, ops);
767
768        let guards: Vec<_> = trace.iter_guards().collect();
769        assert_eq!(guards.len(), 3);
770        assert_eq!(guards[0].fail_args.as_ref().unwrap().len(), 0);
771        assert_eq!(guards[1].fail_args.as_ref().unwrap().len(), 1);
772        assert_eq!(guards[2].fail_args.as_ref().unwrap().len(), 3);
773    }
774
775    #[test]
776    fn test_trace_get_op_returns_correct_positions() {
777        // get_op with valid and invalid indices.
778        let inputargs = vec![InputArg::new_int(0)];
779        let ops = vec![
780            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
781            Op::new(OpCode::IntSub, &[OpRef(1), OpRef(0)]),
782            Op::new(OpCode::IntMul, &[OpRef(2), OpRef(1)]),
783            Op::new(OpCode::Jump, &[OpRef(3)]),
784        ];
785        let trace = TreeLoop::new(inputargs, ops);
786
787        assert_eq!(trace.get_op(OpRef(0)).unwrap().opcode, OpCode::IntAdd);
788        assert_eq!(trace.get_op(OpRef(1)).unwrap().opcode, OpCode::IntSub);
789        assert_eq!(trace.get_op(OpRef(2)).unwrap().opcode, OpCode::IntMul);
790        assert_eq!(trace.get_op(OpRef(3)).unwrap().opcode, OpCode::Jump);
791        assert!(trace.get_op(OpRef(4)).is_none());
792        assert!(trace.get_op(OpRef(100)).is_none());
793    }
794
795    #[test]
796    fn test_trace_clone_independence() {
797        // Modifications to a cloned trace do not affect the original.
798        let inputargs = vec![InputArg::new_int(0)];
799        let ops = vec![
800            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
801            Op::new(OpCode::Jump, &[OpRef(1)]),
802        ];
803        let trace = TreeLoop::new(inputargs, ops);
804        let mut trace2 = trace.clone();
805
806        trace2
807            .ops
808            .push(Op::new(OpCode::IntSub, &[OpRef(0), OpRef(0)]));
809        assert_eq!(trace.num_ops(), 2);
810        assert_eq!(trace2.num_ops(), 3);
811    }
812
813    #[test]
814    fn test_trace_only_guards_in_iter_guards() {
815        // iter_guards must skip all non-guard ops, even in a complex trace.
816        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
817        let ops = vec![
818            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
819            Op::new(OpCode::IntSub, &[OpRef(0), OpRef(1)]),
820            Op::new(OpCode::GuardTrue, &[OpRef(2)]),
821            Op::new(OpCode::IntMul, &[OpRef(2), OpRef(3)]),
822            Op::new(OpCode::IntNeg, &[OpRef(4)]),
823            Op::new(OpCode::GuardFalse, &[OpRef(5)]),
824            Op::new(OpCode::IntLt, &[OpRef(4), OpRef(5)]),
825            Op::new(OpCode::GuardNoException, &[]),
826            Op::new(OpCode::Jump, &[OpRef(4), OpRef(5)]),
827        ];
828        let trace = TreeLoop::new(inputargs, ops);
829
830        let guard_opcodes: Vec<_> = trace.iter_guards().map(|op| op.opcode).collect();
831        assert_eq!(
832            guard_opcodes,
833            vec![
834                OpCode::GuardTrue,
835                OpCode::GuardFalse,
836                OpCode::GuardNoException
837            ]
838        );
839    }
840
841    #[test]
842    fn test_check_consistency_valid() {
843        let ops = vec![
844            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
845            Op::new(OpCode::Jump, &[OpRef(0)]),
846        ];
847        let trace = TreeLoop::new(vec![InputArg::new_int(0)], ops);
848        assert!(trace.check_consistency());
849    }
850
851    #[test]
852    fn test_check_consistency_no_final() {
853        let ops = vec![Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)])];
854        let trace = TreeLoop::new(vec![], ops);
855        assert!(!trace.check_consistency());
856    }
857
858    #[test]
859    fn test_free_vars() {
860        let mut ops = vec![
861            Op::new(OpCode::IntAdd, &[OpRef(100), OpRef(101)]),
862            Op::new(OpCode::Finish, &[OpRef(0)]),
863        ];
864        ops[0].pos = OpRef(0);
865        ops[1].pos = OpRef(1);
866        let trace = TreeLoop::new(vec![], ops);
867        let free = trace.free_vars();
868        // OpRef(100) and OpRef(101) are free (not defined)
869        assert!(free.contains(&OpRef(100)));
870        assert!(free.contains(&OpRef(101)));
871        assert!(!free.contains(&OpRef(0))); // defined by op[0]
872    }
873
874    #[test]
875    fn test_count_by_category() {
876        let ops = vec![
877            Op::new(OpCode::GuardTrue, &[OpRef(0)]),
878            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
879            Op::new(OpCode::IntMul, &[OpRef(0), OpRef(1)]),
880            Op::new(OpCode::CallI, &[OpRef(0)]),
881            Op::new(OpCode::Finish, &[]),
882        ];
883        let trace = TreeLoop::new(vec![], ops);
884        let (guards, pure, calls, other) = trace.count_by_category();
885        assert_eq!(guards, 1);
886        assert_eq!(pure, 2);
887        assert_eq!(calls, 1);
888        assert_eq!(other, 1); // Finish
889    }
890
891    #[test]
892    fn test_split_at_label() {
893        let ops = vec![
894            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
895            Op::new(OpCode::Label, &[OpRef(0)]),
896            Op::new(OpCode::IntMul, &[OpRef(0), OpRef(1)]),
897            Op::new(OpCode::Jump, &[OpRef(0)]),
898        ];
899        let trace = TreeLoop::new(vec![], ops);
900        let (preamble, body) = trace.split_at_label();
901        assert_eq!(preamble.len(), 1);
902        assert_eq!(body.len(), 3); // Label + IntMul + Jump
903    }
904
905    #[test]
906    fn test_num_guards() {
907        let ops = vec![
908            Op::new(OpCode::GuardTrue, &[OpRef(0)]),
909            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
910            Op::new(OpCode::GuardNonnull, &[OpRef(0)]),
911            Op::new(OpCode::GuardClass, &[OpRef(0), OpRef(1)]),
912            Op::new(OpCode::Finish, &[]),
913        ];
914        let trace = TreeLoop::new(vec![], ops);
915        assert_eq!(trace.num_guards(), 3);
916    }
917
918    #[test]
919    fn test_get_final_op() {
920        let ops = vec![
921            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
922            Op::new(OpCode::Finish, &[OpRef(0)]),
923        ];
924        let trace = TreeLoop::new(vec![], ops);
925        let final_op = trace.get_final_op().unwrap();
926        assert_eq!(final_op.opcode, OpCode::Finish);
927    }
928
929    #[test]
930    fn test_get_iter() {
931        let ops = vec![
932            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]),
933            Op::new(OpCode::IntMul, &[OpRef(0), OpRef(1)]),
934            Op::new(OpCode::Jump, &[OpRef(0)]),
935        ];
936        let trace = TreeLoop::new(vec![], ops);
937        let mut iter = trace.get_iter();
938        assert_eq!(iter.num_ops(), 3);
939        assert!(!iter.done());
940        iter.next_op();
941        assert_eq!(iter.position(), 1);
942    }
943
944    #[test]
945    fn test_inputarg_types_all() {
946        let inputargs = vec![
947            InputArg {
948                index: 0,
949                tp: Type::Int,
950            },
951            InputArg {
952                index: 1,
953                tp: Type::Ref,
954            },
955            InputArg {
956                index: 2,
957                tp: Type::Float,
958            },
959        ];
960        let trace = TreeLoop::new(inputargs, vec![Op::new(OpCode::Finish, &[])]);
961        let types = trace.inputarg_types();
962        assert_eq!(types, vec![Type::Int, Type::Ref, Type::Float]);
963    }
964
965    // ══════════════════════════════════════════════════════════════════
966    // cut_trace_from tests — opencoder.py CutTrace parity
967    // ══════════════════════════════════════════════════════════════════
968
969    #[test]
970    fn test_cut_trace_from_no_escaped_refs() {
971        // Simple cut: all post-cut refs are either in original_boxes
972        // or defined after the cut.
973        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
974        let mut ops = Vec::new();
975        // Pre-cut ops (2 inputargs → first op is OpRef(2))
976        let mut op0 = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]);
977        op0.pos = OpRef(2);
978        ops.push(op0);
979        // Post-cut ops
980        let mut op1 = Op::new(OpCode::IntMul, &[OpRef(0), OpRef(1)]);
981        op1.pos = OpRef(3);
982        ops.push(op1);
983        let mut op2 = Op::new(OpCode::Jump, &[OpRef(3)]);
984        op2.pos = OpRef(4);
985        ops.push(op2);
986        let trace = TreeLoop::new(inputargs, ops);
987
988        let start = crate::recorder::TracePosition {
989            op_count: 3,
990            ops_len: 1, // cut after op0
991        };
992        let original_boxes = vec![OpRef(0), OpRef(1)];
993        let original_box_types = vec![Type::Int, Type::Int];
994
995        let cut = trace.cut_trace_from(start, &original_boxes, &original_box_types);
996        assert_eq!(cut.inputargs.len(), 2);
997        assert_eq!(cut.ops.len(), 2); // IntMul + Jump
998        assert_eq!(cut.ops[0].opcode, OpCode::IntMul);
999        assert_eq!(cut.ops[0].args[0], OpRef(0)); // remapped from OpRef(0)
1000        assert_eq!(cut.ops[0].args[1], OpRef(1)); // remapped from OpRef(1)
1001        assert_eq!(cut.ops[1].opcode, OpCode::Jump);
1002        assert_eq!(cut.ops[1].args[0], OpRef(2)); // remapped from OpRef(3) → new idx 2
1003    }
1004
1005    #[test]
1006    fn test_cut_trace_from_with_escaped_op() {
1007        // An op defined before the cut point is used after the cut.
1008        // It should be re-emitted as a prefix operation.
1009        let inputargs = vec![InputArg::new_int(0), InputArg::new_int(1)];
1010        let mut ops = Vec::new();
1011        // op0: v2 = int_add(v0, v1) — before cut
1012        let mut op0 = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(1)]);
1013        op0.pos = OpRef(2);
1014        ops.push(op0);
1015        // op1: v3 = int_mul(v2, v0) — after cut, references v2 (escaped!)
1016        let mut op1 = Op::new(OpCode::IntMul, &[OpRef(2), OpRef(0)]);
1017        op1.pos = OpRef(3);
1018        ops.push(op1);
1019        let mut op2 = Op::new(OpCode::Jump, &[OpRef(3)]);
1020        op2.pos = OpRef(4);
1021        ops.push(op2);
1022        let trace = TreeLoop::new(inputargs, ops);
1023
1024        let start = crate::recorder::TracePosition {
1025            op_count: 3,
1026            ops_len: 1, // cut after op0
1027        };
1028        // original_boxes only has v0 — v2 is escaped
1029        let original_boxes = vec![OpRef(0)];
1030        let original_box_types = vec![Type::Int];
1031
1032        let cut = trace.cut_trace_from(start, &original_boxes, &original_box_types);
1033        // v1 = OpRef(1) is an original trace inputarg NOT in original_boxes.
1034        // It's referenced by the escaped int_add op → added as extra inputarg.
1035        // Result: inputargs = [v0, v1], prefix = [int_add], post-cut = [int_mul, jump]
1036        assert_eq!(cut.inputargs.len(), 2); // v0 + escaped v1
1037        assert_eq!(cut.ops.len(), 3); // prefix(int_add) + int_mul + jump
1038    }
1039
1040    #[test]
1041    fn test_cut_trace_from_constants_preserved() {
1042        // Constants (OpRef >= 10_000) should not be remapped.
1043        let inputargs = vec![InputArg::new_int(0)];
1044        let mut ops = Vec::new();
1045        // pre-cut: noop
1046        let mut op0 = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]);
1047        op0.pos = OpRef(1);
1048        ops.push(op0);
1049        // post-cut: uses a constant
1050        let mut op1 = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(10_000)]);
1051        op1.pos = OpRef(2);
1052        ops.push(op1);
1053        let mut op2 = Op::new(OpCode::Jump, &[OpRef(2)]);
1054        op2.pos = OpRef(3);
1055        ops.push(op2);
1056        let trace = TreeLoop::new(inputargs, ops);
1057
1058        let start = crate::recorder::TracePosition {
1059            op_count: 2,
1060            ops_len: 1,
1061        };
1062        let original_boxes = vec![OpRef(0)];
1063        let original_box_types = vec![Type::Int];
1064
1065        let cut = trace.cut_trace_from(start, &original_boxes, &original_box_types);
1066        assert_eq!(cut.ops.len(), 2);
1067        // Constant ref should be preserved as-is
1068        assert_eq!(cut.ops[0].args[1], OpRef(10_000));
1069    }
1070
1071    #[test]
1072    fn test_cut_trace_from_transitive_escaped() {
1073        // Escaped op depends on another escaped op (transitive closure).
1074        let inputargs = vec![InputArg::new_int(0)];
1075        let mut ops = Vec::new();
1076        // v1 = int_add(v0, v0) — before cut
1077        let mut op0 = Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]);
1078        op0.pos = OpRef(1);
1079        ops.push(op0);
1080        // v2 = int_mul(v1, v0) — before cut
1081        let mut op1 = Op::new(OpCode::IntMul, &[OpRef(1), OpRef(0)]);
1082        op1.pos = OpRef(2);
1083        ops.push(op1);
1084        // v3 = int_sub(v2, v0) — after cut, references v2 (escaped, depends on v1)
1085        let mut op2 = Op::new(OpCode::IntSub, &[OpRef(2), OpRef(0)]);
1086        op2.pos = OpRef(3);
1087        ops.push(op2);
1088        let mut op3 = Op::new(OpCode::Jump, &[OpRef(3)]);
1089        op3.pos = OpRef(4);
1090        ops.push(op3);
1091        let trace = TreeLoop::new(inputargs, ops);
1092
1093        let start = crate::recorder::TracePosition {
1094            op_count: 3,
1095            ops_len: 2, // cut after op0 and op1
1096        };
1097        let original_boxes = vec![OpRef(0)];
1098        let original_box_types = vec![Type::Int];
1099
1100        let cut = trace.cut_trace_from(start, &original_boxes, &original_box_types);
1101        // 1 inputarg, 2 prefix ops (v1=int_add, v2=int_mul), 2 post-cut ops
1102        assert_eq!(cut.inputargs.len(), 1);
1103        assert_eq!(cut.ops.len(), 4);
1104        assert_eq!(cut.ops[0].opcode, OpCode::IntAdd); // re-emitted v1
1105        assert_eq!(cut.ops[1].opcode, OpCode::IntMul); // re-emitted v2
1106        assert_eq!(cut.ops[2].opcode, OpCode::IntSub);
1107        assert_eq!(cut.ops[3].opcode, OpCode::Jump);
1108        // Verify remapping chain: v2's arg should reference re-emitted v1
1109        assert_eq!(cut.ops[1].args[0], OpRef(1)); // v1 → prefix idx 0 → OpRef(1)
1110    }
1111}