Skip to main content

majit_trace/
opencoder.rs

1/// Compact binary encoding for traces.
2///
3/// Translates the concept from rpython/jit/metainterp/opencoder.py —
4/// a compact binary format for serializing and deserializing traces.
5///
6/// Uses LEB128 variable-length integer encoding for compactness.
7use std::collections::HashMap;
8
9use majit_ir::{InputArg, OPCODE_COUNT, Op, OpCode, OpRef, Type};
10
11use crate::history::TreeLoop;
12
13/// Encode a u64 as a variable-length integer (LEB128).
14pub fn encode_varint(buf: &mut Vec<u8>, mut value: u64) {
15    loop {
16        let byte = (value & 0x7F) as u8;
17        value >>= 7;
18        if value == 0 {
19            buf.push(byte);
20            return;
21        }
22        buf.push(byte | 0x80);
23    }
24}
25
26/// Decode a varint from a byte slice. Returns (value, bytes_consumed).
27///
28/// # Panics
29///
30/// Panics if the buffer is truncated (no terminating byte with high bit clear).
31pub fn decode_varint(buf: &[u8]) -> (u64, usize) {
32    let mut value: u64 = 0;
33    let mut shift = 0;
34    for (i, &byte) in buf.iter().enumerate() {
35        value |= ((byte & 0x7F) as u64) << shift;
36        if byte & 0x80 == 0 {
37            return (value, i + 1);
38        }
39        shift += 7;
40    }
41    panic!("truncated varint");
42}
43
44fn type_to_u8(tp: Type) -> u8 {
45    match tp {
46        Type::Int => 0,
47        Type::Ref => 1,
48        Type::Float => 2,
49        Type::Void => 3,
50    }
51}
52
53fn u8_to_type(v: u8) -> Type {
54    match v {
55        0 => Type::Int,
56        1 => Type::Ref,
57        2 => Type::Float,
58        3 => Type::Void,
59        _ => panic!("invalid type byte: {v}"),
60    }
61}
62
63fn u16_to_opcode(v: u16) -> OpCode {
64    assert!(
65        (v as usize) < OPCODE_COUNT,
66        "invalid opcode discriminant: {v}"
67    );
68    // SAFETY: OpCode is #[repr(u16)] and we checked the discriminant is in range.
69    unsafe { std::mem::transmute(v) }
70}
71
72/// Encode a Trace into a compact byte buffer.
73pub fn encode_trace(trace: &TreeLoop) -> Vec<u8> {
74    let mut buf = Vec::new();
75
76    // Encode input args count and types
77    encode_varint(&mut buf, trace.inputargs.len() as u64);
78    for arg in &trace.inputargs {
79        buf.push(type_to_u8(arg.tp));
80    }
81
82    // Encode ops count
83    encode_varint(&mut buf, trace.ops.len() as u64);
84
85    // Encode each op
86    for op in &trace.ops {
87        encode_varint(&mut buf, op.opcode as u16 as u64);
88        encode_varint(&mut buf, op.args.len() as u64);
89        for arg in &op.args {
90            encode_varint(&mut buf, arg.0 as u64);
91        }
92        // Encode descriptor: 0 = none, else descr_index + 1.
93        if let Some(ref descr) = op.descr {
94            let idx = descr.index();
95            if idx != u32::MAX {
96                encode_varint(&mut buf, (idx as u64) + 1);
97            } else {
98                buf.push(1); // has descriptor but no index
99            }
100        } else {
101            buf.push(0);
102        }
103        // Encode fail_args presence and count.
104        if let Some(ref fa) = op.fail_args {
105            encode_varint(&mut buf, fa.len() as u64 + 1); // +1 to distinguish from 0=none
106            for arg in fa.iter() {
107                encode_varint(&mut buf, arg.0 as u64);
108            }
109        } else {
110            buf.push(0);
111        }
112    }
113
114    buf
115}
116
117/// Decode a Trace from a compact byte buffer.
118///
119/// Note: descriptor references are not preserved — ops with descriptors
120/// in the original trace will have `descr: None` after decoding, but
121/// the has-descriptor flag is still decoded (and could be used to
122/// reconstruct descriptors from a separate table).
123pub fn decode_trace(buf: &[u8]) -> TreeLoop {
124    let mut pos = 0;
125
126    // Decode input args
127    let (num_inputargs, n) = decode_varint(&buf[pos..]);
128    pos += n;
129    let num_inputargs = num_inputargs as usize;
130
131    let mut inputargs = Vec::with_capacity(num_inputargs);
132    for i in 0..num_inputargs {
133        let tp = u8_to_type(buf[pos]);
134        pos += 1;
135        inputargs.push(InputArg::from_type(tp, i as u32));
136    }
137
138    // Decode ops
139    let (num_ops, n) = decode_varint(&buf[pos..]);
140    pos += n;
141    let num_ops = num_ops as usize;
142
143    let mut ops = Vec::with_capacity(num_ops);
144    for _ in 0..num_ops {
145        let (opcode_raw, n) = decode_varint(&buf[pos..]);
146        pos += n;
147        let opcode = u16_to_opcode(opcode_raw as u16);
148
149        let (num_args, n) = decode_varint(&buf[pos..]);
150        pos += n;
151        let num_args = num_args as usize;
152
153        let mut args = Vec::with_capacity(num_args);
154        for _ in 0..num_args {
155            let (arg_ref, n) = decode_varint(&buf[pos..]);
156            pos += n;
157            args.push(OpRef(arg_ref as u32));
158        }
159
160        // Decode descriptor index
161        let (descr_marker, n) = decode_varint(&buf[pos..]);
162        pos += n;
163        let _descr_index = if descr_marker > 0 {
164            Some((descr_marker - 1) as u32)
165        } else {
166            None
167        };
168
169        // Decode fail_args
170        let (fa_marker, n) = decode_varint(&buf[pos..]);
171        pos += n;
172        let fail_args = if fa_marker > 0 {
173            let fa_len = (fa_marker - 1) as usize;
174            let mut fa = Vec::with_capacity(fa_len);
175            for _ in 0..fa_len {
176                let (arg_ref, n) = decode_varint(&buf[pos..]);
177                pos += n;
178                fa.push(OpRef(arg_ref as u32));
179            }
180            Some(fa)
181        } else {
182            None
183        };
184
185        let mut op = Op::new(opcode, &args);
186        if let Some(fa) = fail_args {
187            op.fail_args = Some(fa.into());
188        }
189        ops.push(op);
190    }
191
192    TreeLoop::new(inputargs, ops)
193}
194
195// ── Signed varint encoding (opencoder.py) ──
196
197/// Encode a signed i64 as a varint (zigzag encoding).
198/// opencoder.py: encode_varint_signed
199pub fn encode_varint_signed(buf: &mut Vec<u8>, value: i64) {
200    // Zigzag: map negative to odd, positive to even
201    let zigzag = ((value << 1) ^ (value >> 63)) as u64;
202    encode_varint(buf, zigzag);
203}
204
205/// Decode a signed varint (zigzag encoding). Returns (value, bytes_consumed).
206/// opencoder.py: decode_varint_signed
207pub fn decode_varint_signed(buf: &[u8]) -> (i64, usize) {
208    let (zigzag, consumed) = decode_varint(buf);
209    let value = ((zigzag >> 1) as i64) ^ -((zigzag & 1) as i64);
210    (value, consumed)
211}
212
213// ── Snapshot management (opencoder.py) ──
214
215/// Tag constants for snapshot encoding.
216/// opencoder.py: TAGINT, TAGCONSTPTR, TAGCONSTOTHER, TAGBOX
217pub const TAGINT: u8 = 0; // small integer constant
218pub const TAGCONSTPTR: u8 = 1; // GC pointer constant
219pub const TAGCONSTOTHER: u8 = 2; // big int or float constant
220pub const TAGBOX: u8 = 3; // reference to a traced value (OpRef)
221
222/// Number of tag bits.
223pub const TAG_SHIFT: u32 = 2;
224/// Mask for tag extraction.
225pub const TAG_MASK: u8 = (1 << TAG_SHIFT) - 1;
226/// RPython compatibility constants.
227pub const TAGMASK: u8 = TAG_MASK;
228pub const TAGSHIFT: u32 = TAG_SHIFT;
229
230/// Initial trace buffer size.
231pub const INIT_SIZE: usize = 4096;
232
233/// RPython compatibility numeric limits for signed varints in this model.
234pub const MIN_VALUE: i64 = -(1 << 30);
235pub const MAX_VALUE: i64 = (1 << 30) - 1;
236
237/// RPython compatibility bounds for small int tagging.
238pub const SMALL_INT_START: i64 = -(1 << 28);
239pub const SMALL_INT_STOP: i64 = 1 << 28;
240
241/// RPython compatibility sentinel values for snapshot linked lists.
242pub const SNAPSHOT_PREV_NEEDS_PATCHING: i32 = -3;
243pub const SNAPSHOT_PREV_NONE: i32 = -2;
244pub const SNAPSHOT_PREV_COMES_NEXT: i32 = -1;
245
246#[inline]
247pub fn skip_varint_signed(buf: &[u8], mut index: usize, skip: usize) -> usize {
248    assert!(skip > 0);
249    for _ in 0..skip {
250        let byte = buf[index];
251        if byte & 0x80 != 0 {
252            index += 2;
253        } else {
254            index += 1;
255        }
256    }
257    index
258}
259
260#[inline]
261pub fn varint_only_decode(buf: &[u8], index: usize, skip: usize) -> i64 {
262    let start = if skip > 0 {
263        skip_varint_signed(buf, index, skip)
264    } else {
265        index
266    };
267    decode_varint_signed(&buf[start..]).0
268}
269
270#[inline]
271pub fn combine_uint(index1: u32, index2: u32) -> u32 {
272    (index1 << 16) | index2
273}
274
275#[inline]
276pub fn unpack_uint(packed: u32) -> (u32, u32) {
277    ((packed >> 16) & 0xffff, packed & 0xffff)
278}
279
280/// Encode a tagged value for snapshot storage.
281/// opencoder.py: tag(kind, value)
282pub fn tag(kind: u8, value: u32) -> u32 {
283    (value << TAG_SHIFT) | kind as u32
284}
285
286/// Decode a tagged value from snapshot storage.
287/// opencoder.py: untag(tagged) -> (kind, value)
288pub fn untag(tagged: u32) -> (u8, u32) {
289    let kind = (tagged & TAG_MASK as u32) as u8;
290    let value = tagged >> TAG_SHIFT;
291    (kind, value)
292}
293
294/// A snapshot captures the interpreter state at a guard/exit point.
295/// opencoder.py: Snapshot
296///
297/// When a guard fails, the JIT needs to reconstruct the interpreter
298/// state (frames, local variables) from the snapshot to resume
299/// execution in the interpreter.
300#[derive(Clone, Debug)]
301pub struct Snapshot {
302    /// Encoded values (tagged): local variables and virtual object fields.
303    pub values: Vec<u32>,
304    /// Index of the previous snapshot in the chain (for call stack).
305    pub prev: Option<usize>,
306    /// Jitcode index identifying which interpreter function this frame belongs to.
307    pub jitcode_index: u32,
308    /// Program counter (bytecode offset) within the jitcode.
309    pub pc: u32,
310}
311
312/// Top-level snapshot (includes vable/vref tracking).
313/// opencoder.py: TopSnapshot
314#[derive(Clone, Debug)]
315pub struct TopSnapshot {
316    /// The snapshot for the topmost frame.
317    pub snapshot: Snapshot,
318    /// Index into snapshot data for virtualizable array.
319    pub vable_array_index: Option<usize>,
320    /// Index into snapshot data for virtual ref array.
321    pub vref_array_index: Option<usize>,
322}
323
324/// opencoder.py: SnapshotIterator — iterates snapshot values
325/// in bottom-up frame order (inner frame first).
326pub struct SnapshotIterator<'a> {
327    storage: &'a SnapshotStorage,
328    current_snapshot_idx: Option<usize>,
329}
330
331impl<'a> SnapshotIterator<'a> {
332    pub fn new(storage: &'a SnapshotStorage, top_snapshot_idx: usize) -> Self {
333        let start = if top_snapshot_idx < storage.top_snapshots.len() {
334            let _top = &storage.top_snapshots[top_snapshot_idx];
335            Some(storage.snapshots.len() - 1) // start from innermost
336        } else {
337            None
338        };
339        SnapshotIterator {
340            storage,
341            current_snapshot_idx: start,
342        }
343    }
344
345    /// Get the current snapshot frame.
346    pub fn current(&self) -> Option<&'a Snapshot> {
347        self.current_snapshot_idx
348            .and_then(|idx| self.storage.snapshots.get(idx))
349    }
350
351    /// Move to the outer (caller) frame.
352    pub fn next_frame(&mut self) -> Option<&'a Snapshot> {
353        let idx = self.current_snapshot_idx?;
354        let snap = self.storage.snapshots.get(idx)?;
355        self.current_snapshot_idx = snap.prev;
356        self.current()
357    }
358
359    /// Decode a tagged value from the snapshot.
360    pub fn decode_value(&self, tagged: u32) -> DecodedSnapshotValue {
361        let (kind, value) = untag(tagged);
362        match kind {
363            TAGINT => DecodedSnapshotValue::SmallInt(value as i64),
364            TAGCONSTPTR => DecodedSnapshotValue::ConstPtr(
365                self.storage
366                    .const_refs
367                    .get(value as usize)
368                    .copied()
369                    .unwrap_or(0),
370            ),
371            TAGCONSTOTHER => DecodedSnapshotValue::ConstOther(value),
372            TAGBOX => DecodedSnapshotValue::Box(majit_ir::OpRef(value)),
373            _ => DecodedSnapshotValue::SmallInt(0),
374        }
375    }
376}
377
378/// Decoded value from a snapshot.
379#[derive(Clone, Debug)]
380pub enum DecodedSnapshotValue {
381    /// Small integer constant (fits in tag bits).
382    SmallInt(i64),
383    /// GC pointer constant (index into const_refs pool).
384    ConstPtr(u64),
385    /// Other constant (big int or float, index into pool).
386    ConstOther(u32),
387    /// Reference to a traced value (OpRef).
388    Box(majit_ir::OpRef),
389}
390
391/// opencoder.py: TopDownSnapshotIterator — iterates snapshots
392/// from outermost to innermost frame (top-down order).
393/// Used by resume data construction which processes frames from
394/// the outermost caller to the innermost callee.
395pub struct TopDownSnapshotIterator<'a> {
396    storage: &'a SnapshotStorage,
397    snapshot_index: usize,
398    vable_array_index: Option<usize>,
399    vref_array_index: Option<usize>,
400    /// Frames collected in bottom-up order, then reversed.
401    frames: Vec<usize>,
402    pos: usize,
403}
404
405impl<'a> TopDownSnapshotIterator<'a> {
406    /// Create a top-down iterator starting from a top snapshot.
407    pub fn new(storage: &'a SnapshotStorage, top_idx: usize) -> Self {
408        let mut frames = Vec::new();
409        let mut vable_array_index = None;
410        let mut vref_array_index = None;
411        if top_idx < storage.top_snapshots.len() {
412            vable_array_index = storage.top_snapshots[top_idx].vable_array_index;
413            vref_array_index = storage.top_snapshots[top_idx].vref_array_index;
414            // Walk the prev chain to collect all frames bottom-up
415            let first_snap_idx = storage
416                .top_snapshots
417                .get(top_idx)
418                .and_then(|top| top.snapshot.prev)
419                .unwrap_or(0);
420            let mut idx = Some(first_snap_idx);
421            while let Some(i) = idx {
422                if i < storage.snapshots.len() {
423                    frames.push(i);
424                    idx = storage.snapshots[i].prev;
425                } else {
426                    break;
427                }
428            }
429            // Reverse for top-down order (outermost first)
430            frames.reverse();
431        }
432        TopDownSnapshotIterator {
433            storage,
434            snapshot_index: top_idx,
435            vable_array_index,
436            vref_array_index,
437            frames,
438            pos: 0,
439        }
440    }
441
442    fn snapshot_values(&self, index: usize) -> &'a [u32] {
443        self.storage
444            .snapshots
445            .get(index)
446            .map(|snapshot| snapshot.values.as_slice())
447            .unwrap_or(&[])
448    }
449
450    /// opencoder.py: iter_vable_array()
451    pub fn iter_vable_array(&self) -> BoxArrayIter<'a> {
452        match self.vable_array_index {
453            Some(index) => BoxArrayIter::new(self.snapshot_values(index)),
454            None => BoxArrayIter::empty(),
455        }
456    }
457
458    /// opencoder.py: iter_vref_array()
459    pub fn iter_vref_array(&self) -> BoxArrayIter<'a> {
460        match self.vref_array_index {
461            Some(index) => BoxArrayIter::new(self.snapshot_values(index)),
462            None => BoxArrayIter::empty(),
463        }
464    }
465
466    /// opencoder.py: iter_array(snapshot_index)
467    pub fn iter_array(&self, snapshot_index: usize) -> BoxArrayIter<'a> {
468        BoxArrayIter::new(self.snapshot_values(snapshot_index))
469    }
470
471    /// opencoder.py: length(snapshot_index)
472    pub fn length(&self, snapshot_index: usize) -> usize {
473        self.snapshot_values(snapshot_index).len()
474    }
475
476    /// opencoder.py: prev(snapshot_index)
477    pub fn prev(&mut self, snapshot_index: usize) -> i32 {
478        self.storage
479            .snapshots
480            .get(snapshot_index)
481            .and_then(|snapshot| snapshot.prev)
482            .map_or(SNAPSHOT_PREV_NONE, |prev| prev as i32)
483    }
484
485    /// opencoder.py: unpack_jitcode_pc(snapshot_index)
486    pub fn unpack_jitcode_pc(&self, snapshot_index: usize) -> (u32, u32) {
487        self.storage
488            .snapshots
489            .get(snapshot_index)
490            .map(|snapshot| (snapshot.jitcode_index, snapshot.pc))
491            .unwrap_or((u32::MAX, u32::MAX))
492    }
493
494    /// opencoder.py: is_empty_snapshot(snapshot_index)
495    pub fn is_empty_snapshot(&self, snapshot_index: usize) -> bool {
496        self.storage
497            .snapshots
498            .get(snapshot_index)
499            .is_none_or(|snapshot| snapshot.values.is_empty())
500    }
501
502    /// opencoder.py: decode_snapshot_int()
503    pub fn decode_snapshot_int(&mut self) -> i64 {
504        self.snapshot_index as i64
505    }
506
507    /// Get the next frame (outermost to innermost).
508    pub fn next_frame(&mut self) -> Option<&'a Snapshot> {
509        if self.pos < self.frames.len() {
510            let idx = self.frames[self.pos];
511            self.pos += 1;
512            self.storage.snapshots.get(idx)
513        } else {
514            None
515        }
516    }
517
518    /// Number of frames.
519    pub fn num_frames(&self) -> usize {
520        self.frames.len()
521    }
522
523    /// Whether all frames have been consumed.
524    pub fn done(&self) -> bool {
525        self.pos >= self.frames.len()
526    }
527}
528
529impl<'a> Iterator for TopDownSnapshotIterator<'a> {
530    type Item = usize;
531
532    fn next(&mut self) -> Option<Self::Item> {
533        if self.pos < self.frames.len() {
534            let idx = self.frames[self.pos];
535            self.pos += 1;
536            Some(idx)
537        } else {
538            None
539        }
540    }
541}
542
543/// Snapshot storage for a trace.
544/// opencoder.py: Trace._snapshot_data, _snapshot_array_data
545///
546/// gcreftracer.py parity: const_refs entries are GC references stored as
547/// raw u64. They are rooted on the shadow stack so GC can update them
548/// when objects move. refresh_from_gc() copies updated values back.
549#[derive(Debug)]
550pub struct SnapshotStorage {
551    /// All snapshots in this trace, indexed by position.
552    pub snapshots: Vec<Snapshot>,
553    /// Top snapshots corresponding to guard operations.
554    pub top_snapshots: Vec<TopSnapshot>,
555    /// Constant pool: GC references (pointers).
556    /// opencoder.py: Trace._refs
557    pub const_refs: Vec<u64>,
558    /// Constant pool: big integers (>28-bit).
559    pub const_bigints: Vec<i64>,
560    /// Constant pool: float values.
561    pub const_floats: Vec<f64>,
562    /// (const_refs index, shadow stack index) for each rooted entry.
563    /// gcreftracer.py parity: GC's walk_roots updates shadow stack;
564    /// refresh_from_gc copies values back to const_refs.
565    rooted_ref_indices: Vec<(usize, usize)>,
566    /// Shadow stack depth at creation. release_roots pops to here.
567    shadow_stack_base: usize,
568}
569
570impl Default for SnapshotStorage {
571    fn default() -> Self {
572        SnapshotStorage {
573            snapshots: Vec::new(),
574            top_snapshots: Vec::new(),
575            const_refs: Vec::new(),
576            const_bigints: Vec::new(),
577            const_floats: Vec::new(),
578            rooted_ref_indices: Vec::new(),
579            shadow_stack_base: majit_gc::shadow_stack::depth(),
580        }
581    }
582}
583
584impl SnapshotStorage {
585    pub fn new() -> Self {
586        Self::default()
587    }
588
589    /// Add a snapshot and return its index.
590    pub fn add_snapshot(&mut self, snapshot: Snapshot) -> usize {
591        let idx = self.snapshots.len();
592        self.snapshots.push(snapshot);
593        idx
594    }
595
596    /// Add a top snapshot (for a guard).
597    pub fn add_top_snapshot(&mut self, top: TopSnapshot) -> usize {
598        let idx = self.top_snapshots.len();
599        self.top_snapshots.push(top);
600        idx
601    }
602
603    /// Add a GC reference constant and return its pool index.
604    /// gcreftracer.py parity: non-null refs are rooted on shadow stack.
605    pub fn add_const_ref(&mut self, ptr: u64) -> u32 {
606        let cr_idx = self.const_refs.len();
607        self.const_refs.push(ptr);
608        if ptr != 0 {
609            let ss_idx = majit_gc::shadow_stack::push(majit_ir::GcRef(ptr as usize));
610            self.rooted_ref_indices.push((cr_idx, ss_idx));
611        }
612        cr_idx as u32
613    }
614
615    /// Add a big integer constant and return its pool index.
616    pub fn add_const_bigint(&mut self, value: i64) -> u32 {
617        let idx = self.const_bigints.len() as u32;
618        self.const_bigints.push(value);
619        idx
620    }
621
622    /// Add a float constant and return its pool index.
623    pub fn add_const_float(&mut self, value: f64) -> u32 {
624        let idx = self.const_floats.len() as u32;
625        self.const_floats.push(value);
626        idx
627    }
628
629    /// Total number of snapshots.
630    pub fn num_snapshots(&self) -> usize {
631        self.snapshots.len()
632    }
633
634    /// Update const_refs from shadow stack — GC may have moved objects.
635    /// gcreftracer.py:gcrefs_trace parity.
636    pub fn refresh_from_gc(&mut self) {
637        for &(cr_idx, ss_idx) in &self.rooted_ref_indices {
638            self.const_refs[cr_idx] = majit_gc::shadow_stack::get(ss_idx).0 as u64;
639        }
640    }
641
642    /// Release shadow stack roots.
643    fn release_roots(&mut self) {
644        if !self.rooted_ref_indices.is_empty() {
645            majit_gc::shadow_stack::pop_to(self.shadow_stack_base);
646            self.rooted_ref_indices.clear();
647        }
648    }
649}
650
651impl Drop for SnapshotStorage {
652    fn drop(&mut self) {
653        self.release_roots();
654    }
655}
656
657// ── Trace Iterator (opencoder.py: TraceIterator) ──
658
659/// opencoder.py: TraceIterator — iterates operations in a trace
660/// with dead range tracking.
661///
662/// The iterator walks through encoded trace operations and maintains
663/// live/dead range information for each value (OpRef). This enables
664/// the optimizer to know which values are still in use at each point.
665pub struct TraceIterator<'a> {
666    /// The operations being iterated.
667    ops: &'a [majit_ir::Op],
668    /// Current position in the operation list.
669    pos: usize,
670    /// Live range end for each OpRef: maps OpRef → last use position.
671    /// opencoder.py: _deadranges
672    live_range_end: Vec<usize>,
673    /// Whether live ranges have been computed.
674    ranges_computed: bool,
675}
676
677impl<'a> TraceIterator<'a> {
678    /// Create a new TraceIterator over the given operations.
679    pub fn new(ops: &'a [majit_ir::Op]) -> Self {
680        TraceIterator {
681            ops,
682            pos: 0,
683            live_range_end: Vec::new(),
684            ranges_computed: false,
685        }
686    }
687
688    /// Get the next operation, or None if exhausted.
689    pub fn next_op(&mut self) -> Option<&'a majit_ir::Op> {
690        if self.pos < self.ops.len() {
691            let op = &self.ops[self.pos];
692            self.pos += 1;
693            Some(op)
694        } else {
695            None
696        }
697    }
698
699    /// Current position in the operation list.
700    pub fn position(&self) -> usize {
701        self.pos
702    }
703
704    /// Whether all operations have been consumed.
705    pub fn done(&self) -> bool {
706        self.pos >= self.ops.len()
707    }
708
709    /// Total number of operations.
710    pub fn num_ops(&self) -> usize {
711        self.ops.len()
712    }
713
714    /// Reset to the beginning.
715    pub fn reset(&mut self) {
716        self.pos = 0;
717    }
718
719    /// opencoder.py: get_dead_ranges() — compute live ranges for all values.
720    /// Returns a reference to the live_range_end map.
721    pub fn compute_dead_ranges(&mut self) -> &[usize] {
722        if self.ranges_computed {
723            return &self.live_range_end;
724        }
725
726        // Find the maximum OpRef used
727        let max_ref = self
728            .ops
729            .iter()
730            .flat_map(|op| {
731                op.args
732                    .iter()
733                    .chain(op.fail_args.iter().flat_map(|fa| fa.iter()))
734                    .map(|r| r.0 as usize)
735            })
736            .max()
737            .unwrap_or(0);
738
739        self.live_range_end = vec![0; max_ref + 1];
740
741        // Walk backwards: last use of each OpRef is its dead range end.
742        for (i, op) in self.ops.iter().enumerate().rev() {
743            for arg in &op.args {
744                let idx = arg.0 as usize;
745                if idx < self.live_range_end.len() && self.live_range_end[idx] == 0 {
746                    self.live_range_end[idx] = i;
747                }
748            }
749            if let Some(ref fa) = op.fail_args {
750                for arg in fa.iter() {
751                    let idx = arg.0 as usize;
752                    if idx < self.live_range_end.len() && self.live_range_end[idx] == 0 {
753                        self.live_range_end[idx] = i;
754                    }
755                }
756            }
757        }
758
759        self.ranges_computed = true;
760        &self.live_range_end
761    }
762
763    /// Check if a value is dead at the given position.
764    /// opencoder.py: is_dead(opref, pos)
765    pub fn is_dead_at(&self, opref: majit_ir::OpRef, pos: usize) -> bool {
766        let idx = opref.0 as usize;
767        if idx >= self.live_range_end.len() {
768            return true; // never used
769        }
770        self.live_range_end[idx] < pos
771    }
772}
773
774/// opencoder.py: CutTrace — supports cutting traces at breakpoints
775/// for bridge compilation.
776#[derive(Clone, Debug)]
777pub struct CutTrace {
778    /// Position at which the trace was cut.
779    pub cut_at: usize,
780    /// Number of input args at the cut point.
781    pub num_inputargs: usize,
782}
783
784impl CutTrace {
785    pub fn new(cut_at: usize, num_inputargs: usize) -> Self {
786        CutTrace {
787            cut_at,
788            num_inputargs,
789        }
790    }
791}
792
793/// opencoder.py: BoxArrayIter — iterates encoded box arrays
794/// in snapshot data, decoding tagged values.
795pub struct BoxArrayIter<'a> {
796    values: &'a [u32],
797    pos: usize,
798}
799
800impl<'a> BoxArrayIter<'a> {
801    pub fn empty() -> Self {
802        Self::new(&[])
803    }
804
805    pub fn new(values: &'a [u32]) -> Self {
806        BoxArrayIter { values, pos: 0 }
807    }
808
809    /// Get the next tagged value, or None if exhausted.
810    pub fn next_value(&mut self) -> Option<u32> {
811        if self.pos < self.values.len() {
812            let val = self.values[self.pos];
813            self.pos += 1;
814            Some(val)
815        } else {
816            None
817        }
818    }
819
820    /// Decode the next tagged value.
821    pub fn next_decoded(&mut self) -> Option<(u8, u32)> {
822        self.next_value().map(untag)
823    }
824
825    /// Remaining values.
826    pub fn remaining(&self) -> usize {
827        self.values.len() - self.pos
828    }
829
830    /// Whether exhausted.
831    pub fn done(&self) -> bool {
832        self.pos >= self.values.len()
833    }
834}
835
836impl<'a> Iterator for BoxArrayIter<'a> {
837    type Item = u32;
838
839    fn next(&mut self) -> Option<Self::Item> {
840        self.next_value()
841    }
842}
843
844// ── Trace Recording API (opencoder.py: Trace class) ──
845
846/// opencoder.py: Trace — compact trace recording buffer.
847///
848/// Records operations and snapshots during tracing. The recorded data
849/// can be iterated via `TraceIterator` or serialized via `encode_trace`.
850pub type Trace = TraceRecordBuffer;
851pub struct TraceRecordBuffer {
852    /// Encoded operation stream (binary).
853    data: Vec<u8>,
854    /// Number of input arguments.
855    num_inputargs: usize,
856    /// Number of recorded operations.
857    num_ops: usize,
858    /// Snapshot storage.
859    snapshots: SnapshotStorage,
860    /// Cut points for bridge compilation.
861    cut_points: Vec<usize>,
862    /// Whether the trace has overflowed (too long).
863    overflow: bool,
864    /// Maximum allowed data size.
865    max_size: usize,
866    /// Memoized integer constants used by `_cached_const_int()`.
867    const_int_cache: HashMap<i64, u32>,
868    /// Memoized pointer constants used by `_cached_const_ptr()`.
869    const_ptr_cache: HashMap<u64, u32>,
870}
871
872impl TraceRecordBuffer {
873    /// Create a new trace recording buffer.
874    pub fn new(num_inputargs: usize) -> Self {
875        TraceRecordBuffer {
876            data: Vec::with_capacity(4096),
877            num_inputargs,
878            num_ops: 0,
879            snapshots: SnapshotStorage::new(),
880            cut_points: Vec::new(),
881            overflow: false,
882            max_size: 1 << 20, // 1MB default
883            const_int_cache: HashMap::new(),
884            const_ptr_cache: HashMap::new(),
885        }
886    }
887
888    /// opencoder.py: tag_overflow_imminent()
889    /// Check if the recording buffer is nearly full.
890    pub fn tag_overflow_imminent(&self) -> bool {
891        self.data.len() > self.max_size * 9 / 10
892    }
893
894    /// opencoder.py: tracing_done()
895    /// Finalize the trace and check for overflow.
896    pub fn tracing_done(&mut self) -> bool {
897        if self.data.len() > self.max_size {
898            self.overflow = true;
899        }
900        !self.overflow
901    }
902
903    /// Record an operation.
904    pub fn record_op(&mut self, opcode: u16, num_args: u8) {
905        encode_varint(&mut self.data, opcode as u64);
906        self.data.push(num_args);
907        self.num_ops += 1;
908    }
909
910    /// opencoder.py: _op_start() — mark start offset for current opcode.
911    pub fn _op_start(&self) -> usize {
912        self.data.len()
913    }
914
915    /// opencoder.py: _op_end() — mark end offset for current opcode.
916    pub fn _op_end(&self) -> usize {
917        self.data.len()
918    }
919
920    /// RPython-compatible: `Trace._encode(op, arg)`.
921    ///
922    /// Returns a tagged representation for values stored in snapshots and
923    /// arrays.
924    #[inline]
925    pub fn _encode(&self, kind: u8, value: u32) -> u32 {
926        tag(kind, value)
927    }
928
929    /// RPython-compatible: `Trace._cached_const_int(value)`.
930    /// Reuse pooled small constants to keep indexes stable.
931    pub fn _cached_const_int(&mut self, value: i64) -> u32 {
932        if let Some(idx) = self.const_int_cache.get(&value) {
933            return *idx;
934        }
935        let idx = self.snapshots.add_const_bigint(value);
936        self.const_int_cache.insert(value, idx);
937        idx
938    }
939
940    /// RPython-compatible: `Trace._cached_const_ptr(ptr)`.
941    /// Reuse pooled pointer constants to keep indexes stable.
942    pub fn _cached_const_ptr(&mut self, ptr: u64) -> u32 {
943        if let Some(idx) = self.const_ptr_cache.get(&ptr) {
944            return *idx;
945        }
946        let idx = self.snapshots.add_const_ref(ptr);
947        self.const_ptr_cache.insert(ptr, idx);
948        idx
949    }
950
951    /// opencoder.py: _encode_descr(descr) — encode descriptor index.
952    pub fn _encode_descr(&self, descr: u32) -> u32 {
953        descr
954    }
955
956    /// opencoder.py: _add_box_to_storage(value).
957    ///
958    /// Store box value into a dedicated pool; return index for compatibility.
959    pub fn _add_box_to_storage(&mut self, value: u32) -> usize {
960        self.snapshots.const_refs.push(value as u64);
961        self.snapshots.const_refs.len() - 1
962    }
963
964    /// opencoder.py: append_byte(byte) — append raw byte to trace data.
965    pub fn append_byte(&mut self, byte: u8) {
966        self.data.push(byte);
967    }
968
969    /// opencoder.py: append_int(value) — append LEB128 value to trace data.
970    pub fn append_int(&mut self, value: u32) {
971        self.record_arg(value);
972    }
973
974    /// opencoder.py: append_snapshot_array_data_int(data, value).
975    pub fn append_snapshot_array_data_int(data: &mut Vec<u32>, value: i32) {
976        data.push(value as u32);
977    }
978
979    /// opencoder.py: append_snapshot_data_int(data, value).
980    pub fn append_snapshot_data_int(data: &mut Vec<u32>, value: i32) {
981        data.push(value as u32);
982    }
983
984    /// opencoder.py: _encode_snapshot(snapshot) → encoded snapshot payload.
985    pub fn _encode_snapshot(snapshot: &Snapshot) -> Vec<u32> {
986        snapshot.values.clone()
987    }
988
989    /// opencoder.py: create_snapshot(values) — helper for building a snapshot.
990    pub fn create_snapshot(&self, values: Vec<u32>) -> Snapshot {
991        Snapshot {
992            values,
993            prev: None,
994            jitcode_index: 0,
995            pc: 0,
996        }
997    }
998
999    /// opencoder.py: snapshot_add_prev(snapshot, prev).
1000    pub fn snapshot_add_prev(snapshot: &mut Snapshot, prev: Option<usize>) {
1001        snapshot.prev = prev;
1002    }
1003
1004    /// opencoder.py compatibility: record fixed-arity opcodes with explicit
1005    /// argument slots.
1006    pub fn record_op0(&mut self, opcode: u16) {
1007        self.record_op(opcode, 0);
1008    }
1009
1010    /// opencoder.py compatibility: record fixed-arity opcodes with explicit
1011    /// argument slots.
1012    pub fn record_op1(&mut self, opcode: u16, arg0: u32) {
1013        self.record_op(opcode, 1);
1014        self.record_arg(arg0);
1015    }
1016
1017    /// opencoder.py compatibility: record fixed-arity opcodes with explicit
1018    /// argument slots.
1019    pub fn record_op2(&mut self, opcode: u16, arg0: u32, arg1: u32) {
1020        self.record_op(opcode, 2);
1021        self.record_arg(arg0);
1022        self.record_arg(arg1);
1023    }
1024
1025    /// opencoder.py compatibility: record fixed-arity opcodes with explicit
1026    /// argument slots.
1027    pub fn record_op3(&mut self, opcode: u16, arg0: u32, arg1: u32, arg2: u32) {
1028        self.record_op(opcode, 3);
1029        self.record_arg(arg0);
1030        self.record_arg(arg1);
1031        self.record_arg(arg2);
1032    }
1033
1034    /// RPython-compatible: encode a boxed value array.
1035    pub fn _list_of_boxes(&self, boxes: &[u32]) -> Vec<u32> {
1036        boxes.iter().map(|&b| self._encode(TAGBOX, b)).collect()
1037    }
1038
1039    /// RPython-compatible: encode boxes for virtualizable state.
1040    pub fn _list_of_boxes_virtualizable(&self, boxes: &[u32]) -> Vec<u32> {
1041        boxes.iter().map(|&b| self._encode(TAGBOX, b)).collect()
1042    }
1043
1044    /// RPython-compatible helper: return a copied encoded array payload.
1045    pub fn new_array(&self, items: &[u32]) -> Vec<u32> {
1046        items.to_vec()
1047    }
1048
1049    /// Record an argument value.
1050    pub fn record_arg(&mut self, value: u32) {
1051        encode_varint(&mut self.data, value as u64);
1052    }
1053
1054    /// opencoder.py: cut_point()
1055    /// Mark the current position as a potential bridge entry.
1056    pub fn cut_point(&mut self) {
1057        self.cut_points.push(self.data.len());
1058    }
1059
1060    /// Number of recorded operations.
1061    pub fn num_ops(&self) -> usize {
1062        self.num_ops
1063    }
1064
1065    /// Whether the trace overflowed.
1066    pub fn overflowed(&self) -> bool {
1067        self.overflow
1068    }
1069
1070    /// Get the snapshot storage.
1071    pub fn snapshots(&self) -> &SnapshotStorage {
1072        &self.snapshots
1073    }
1074
1075    /// Get mutable snapshot storage (for adding snapshots during tracing).
1076    pub fn snapshots_mut(&mut self) -> &mut SnapshotStorage {
1077        &mut self.snapshots
1078    }
1079
1080    /// opencoder.py: capture_resumedata(snapshot)
1081    /// Record a snapshot at the current position (for guard resume data).
1082    pub fn capture_resumedata(&mut self, snapshot: Snapshot) -> usize {
1083        self.snapshots.add_snapshot(snapshot)
1084    }
1085
1086    /// opencoder.py: create_top_snapshot(frame, vable_boxes, vref_boxes)
1087    /// Create a top-level snapshot with virtualizable and virtual ref arrays.
1088    pub fn create_top_snapshot(
1089        &mut self,
1090        snapshot: Snapshot,
1091        vable_array_index: Option<usize>,
1092        vref_array_index: Option<usize>,
1093    ) -> usize {
1094        let _snap_idx = self.snapshots.add_snapshot(snapshot.clone());
1095        self.snapshots.add_top_snapshot(TopSnapshot {
1096            snapshot,
1097            vable_array_index,
1098            vref_array_index,
1099        })
1100    }
1101
1102    /// opencoder.py: create_empty_top_snapshot(vable_boxes, vref_boxes)
1103    /// Create a top snapshot with no frame data (for bridge entry).
1104    pub fn create_empty_top_snapshot(
1105        &mut self,
1106        vable_array_index: Option<usize>,
1107        vref_array_index: Option<usize>,
1108    ) -> usize {
1109        let empty_snap = Snapshot {
1110            values: Vec::new(),
1111            prev: None,
1112            jitcode_index: 0,
1113            pc: 0,
1114        };
1115        self.create_top_snapshot(empty_snap, vable_array_index, vref_array_index)
1116    }
1117
1118    /// opencoder.py: get_live_ranges()
1119    /// Compute live ranges for all recorded values.
1120    /// Returns a vector where index i contains the last position
1121    /// where value i is used.
1122    pub fn get_live_ranges(&self, ops: &[majit_ir::Op]) -> Vec<usize> {
1123        let mut iter = TraceIterator::new(ops);
1124        iter.compute_dead_ranges().to_vec()
1125    }
1126
1127    /// opencoder.py: unpack()
1128    /// Decode the recorded trace into (inputargs, ops).
1129    /// Convenience method for testing and debugging.
1130    pub fn data_len(&self) -> usize {
1131        self.data.len()
1132    }
1133
1134    /// Maximum allowed data size.
1135    pub fn max_size(&self) -> usize {
1136        self.max_size
1137    }
1138
1139    /// Set the maximum allowed data size.
1140    pub fn set_max_size(&mut self, size: usize) {
1141        self.max_size = size;
1142    }
1143
1144    /// opencoder.py: capture_resumedata(framestack, vable_boxes, vref_boxes)
1145    ///
1146    /// Multi-frame version: creates a chain of snapshots for the full
1147    /// frame stack, with the topmost frame as a TopSnapshot.
1148    pub fn capture_resumedata_framestack(
1149        &mut self,
1150        frame_pcs: &[u64],
1151        frame_slots: &[Vec<u32>],
1152        _virtualizable_boxes: &[u32],
1153        _virtualref_boxes: &[u32],
1154    ) -> usize {
1155        if frame_pcs.is_empty() {
1156            let empty_snap = Snapshot {
1157                values: vec![],
1158                prev: None,
1159                jitcode_index: 0,
1160                pc: 0,
1161            };
1162            let top = TopSnapshot {
1163                snapshot: empty_snap,
1164                vable_array_index: None,
1165                vref_array_index: None,
1166            };
1167            return self.snapshots.add_top_snapshot(top);
1168        }
1169
1170        let n = frame_pcs.len() - 1;
1171
1172        // Create snapshots bottom-up (outermost first)
1173        let mut parent_idx: Option<usize> = None;
1174        for i in 0..n {
1175            let snapshot = Snapshot {
1176                values: if i < frame_slots.len() {
1177                    frame_slots[i].clone()
1178                } else {
1179                    vec![]
1180                },
1181                prev: parent_idx,
1182                jitcode_index: 0,
1183                pc: frame_pcs[i] as u32,
1184            };
1185            parent_idx = Some(self.snapshots.add_snapshot(snapshot));
1186        }
1187
1188        // Top snapshot for innermost frame
1189        let top_snap = Snapshot {
1190            values: if n < frame_slots.len() {
1191                frame_slots[n].clone()
1192            } else {
1193                vec![]
1194            },
1195            prev: parent_idx,
1196            jitcode_index: 0,
1197            pc: frame_pcs[n] as u32,
1198        };
1199        let top = TopSnapshot {
1200            snapshot: top_snap,
1201            vable_array_index: None,
1202            vref_array_index: None,
1203        };
1204        self.snapshots.add_top_snapshot(top)
1205    }
1206
1207    /// opencoder.py: get_dead_ranges()
1208    ///
1209    /// Compute dead ranges: for each op index x, the values that are
1210    /// known to be dead before x. Used by the register allocator to
1211    /// know when to free registers.
1212    pub fn get_dead_ranges(&self, ops: &[majit_ir::Op]) -> Vec<usize> {
1213        let live_ranges = self.get_live_ranges(ops);
1214        let mut dead_ranges = vec![0usize; live_ranges.len() + 2];
1215        for (i, &last_use) in live_ranges.iter().enumerate() {
1216            if last_use > 0 && last_use + 1 < dead_ranges.len() {
1217                // Value i dies after position last_use.
1218                // Record it in dead_ranges[last_use + 1].
1219                let mut pos = last_use + 1;
1220                while pos < dead_ranges.len() && dead_ranges[pos] != 0 {
1221                    pos += 1;
1222                }
1223                if pos < dead_ranges.len() {
1224                    dead_ranges[pos] = i;
1225                }
1226            }
1227        }
1228        dead_ranges
1229    }
1230
1231    /// RPython-compatible: unpack tagged box stream into raw pairs.
1232    pub fn unpack(&self, encoded: &[u32]) -> Vec<(u8, u32)> {
1233        encoded.iter().copied().map(untag).collect()
1234    }
1235}
1236
1237#[cfg(test)]
1238mod tests {
1239    use super::*;
1240
1241    #[test]
1242    fn test_varint_roundtrip() {
1243        let values = [0u64, 1, 127, 128, 255, 256, 65535, 0xFFFF_FFFF, u64::MAX];
1244        for &val in &values {
1245            let mut buf = Vec::new();
1246            encode_varint(&mut buf, val);
1247            let (decoded, consumed) = decode_varint(&buf);
1248            assert_eq!(decoded, val, "roundtrip failed for {val}");
1249            assert_eq!(consumed, buf.len());
1250        }
1251    }
1252
1253    #[test]
1254    fn test_varint_small_values() {
1255        // Values 0..=127 should encode to a single byte.
1256        for val in 0..=127u64 {
1257            let mut buf = Vec::new();
1258            encode_varint(&mut buf, val);
1259            assert_eq!(buf.len(), 1, "value {val} should be 1 byte");
1260            assert_eq!(buf[0], val as u8);
1261        }
1262    }
1263
1264    #[test]
1265    fn test_varint_128_is_two_bytes() {
1266        let mut buf = Vec::new();
1267        encode_varint(&mut buf, 128);
1268        assert_eq!(buf.len(), 2);
1269        let (decoded, consumed) = decode_varint(&buf);
1270        assert_eq!(decoded, 128);
1271        assert_eq!(consumed, 2);
1272    }
1273
1274    #[test]
1275    fn test_empty_trace_roundtrip() {
1276        let trace = TreeLoop::new(vec![], vec![]);
1277        let encoded = encode_trace(&trace);
1278        let decoded = decode_trace(&encoded);
1279        assert_eq!(decoded.num_inputargs(), 0);
1280        assert_eq!(decoded.num_ops(), 0);
1281    }
1282
1283    #[test]
1284    fn test_trace_roundtrip() {
1285        let inputargs = vec![
1286            InputArg::new_int(0),
1287            InputArg::new_ref(1),
1288            InputArg::new_float(2),
1289        ];
1290        let ops = vec![
1291            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
1292            Op::new(OpCode::GuardTrue, &[OpRef(1)]),
1293            Op::new(OpCode::Jump, &[OpRef(2)]),
1294        ];
1295        let trace = TreeLoop::new(inputargs, ops);
1296
1297        let encoded = encode_trace(&trace);
1298        let decoded = decode_trace(&encoded);
1299
1300        assert_eq!(decoded.num_inputargs(), 3);
1301        assert_eq!(decoded.inputargs[0].tp, Type::Int);
1302        assert_eq!(decoded.inputargs[0].index, 0);
1303        assert_eq!(decoded.inputargs[1].tp, Type::Ref);
1304        assert_eq!(decoded.inputargs[1].index, 1);
1305        assert_eq!(decoded.inputargs[2].tp, Type::Float);
1306        assert_eq!(decoded.inputargs[2].index, 2);
1307
1308        assert_eq!(decoded.num_ops(), 3);
1309        assert_eq!(decoded.ops[0].opcode, OpCode::IntAdd);
1310        assert_eq!(decoded.ops[0].args.as_slice(), &[OpRef(0), OpRef(0)]);
1311        assert_eq!(decoded.ops[1].opcode, OpCode::GuardTrue);
1312        assert_eq!(decoded.ops[1].args.as_slice(), &[OpRef(1)]);
1313        assert_eq!(decoded.ops[2].opcode, OpCode::Jump);
1314        assert_eq!(decoded.ops[2].args.as_slice(), &[OpRef(2)]);
1315
1316        assert!(decoded.is_loop());
1317    }
1318
1319    #[test]
1320    fn test_trace_with_many_ops() {
1321        let inputargs = vec![InputArg::new_int(0)];
1322        let mut ops = Vec::new();
1323
1324        // Build a chain of IntAdd ops
1325        for i in 0..100u32 {
1326            ops.push(Op::new(OpCode::IntAdd, &[OpRef(i), OpRef(0)]));
1327        }
1328        ops.push(Op::new(OpCode::Jump, &[OpRef(100)]));
1329
1330        let trace = TreeLoop::new(inputargs, ops);
1331        let encoded = encode_trace(&trace);
1332        let decoded = decode_trace(&encoded);
1333
1334        assert_eq!(decoded.num_ops(), 101);
1335        for i in 0..100 {
1336            assert_eq!(decoded.ops[i].opcode, OpCode::IntAdd);
1337            assert_eq!(decoded.ops[i].args[0], OpRef(i as u32));
1338            assert_eq!(decoded.ops[i].args[1], OpRef(0));
1339        }
1340        assert_eq!(decoded.ops[100].opcode, OpCode::Jump);
1341        assert!(decoded.is_loop());
1342    }
1343
1344    #[test]
1345    fn test_trace_with_finish() {
1346        let inputargs = vec![InputArg::new_int(0)];
1347        let ops = vec![
1348            Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)]),
1349            Op::new(OpCode::Finish, &[OpRef(1)]),
1350        ];
1351        let trace = TreeLoop::new(inputargs, ops);
1352        let encoded = encode_trace(&trace);
1353        let decoded = decode_trace(&encoded);
1354
1355        assert!(decoded.is_finished());
1356        assert!(!decoded.is_loop());
1357    }
1358
1359    #[test]
1360    fn test_trace_preserves_descr_flag() {
1361        // Verify that the has-descr byte is encoded (even though we don't
1362        // reconstruct the descriptor on decode).
1363        let ops = vec![Op::new(OpCode::IntAdd, &[OpRef(0), OpRef(0)])];
1364        let trace = TreeLoop::new(vec![InputArg::new_int(0)], ops);
1365
1366        let encoded = encode_trace(&trace);
1367        // The last byte of the encoded op should be the descr flag (0 = no descr)
1368        assert_eq!(*encoded.last().unwrap(), 0);
1369    }
1370
1371    #[test]
1372    fn test_varint_multiple_in_buffer() {
1373        let mut buf = Vec::new();
1374        encode_varint(&mut buf, 42);
1375        encode_varint(&mut buf, 12345);
1376        encode_varint(&mut buf, 0);
1377
1378        let (v1, n1) = decode_varint(&buf);
1379        assert_eq!(v1, 42);
1380        let (v2, n2) = decode_varint(&buf[n1..]);
1381        assert_eq!(v2, 12345);
1382        let (v3, _n3) = decode_varint(&buf[n1 + n2..]);
1383        assert_eq!(v3, 0);
1384    }
1385
1386    #[test]
1387    fn test_signed_varint_roundtrip() {
1388        let values = [0i64, 1, -1, 127, -128, 1000, -1000, i64::MAX, i64::MIN];
1389        for &val in &values {
1390            let mut buf = Vec::new();
1391            encode_varint_signed(&mut buf, val);
1392            let (decoded, _consumed) = decode_varint_signed(&buf);
1393            assert_eq!(decoded, val, "signed varint roundtrip failed for {val}");
1394        }
1395    }
1396
1397    #[test]
1398    fn test_tag_untag_roundtrip() {
1399        for kind in [TAGINT, TAGCONSTPTR, TAGCONSTOTHER, TAGBOX] {
1400            for value in [0u32, 1, 42, 255, 1000] {
1401                let tagged = tag(kind, value);
1402                let (k, v) = untag(tagged);
1403                assert_eq!(k, kind, "tag kind mismatch");
1404                assert_eq!(v, value, "tag value mismatch");
1405            }
1406        }
1407    }
1408
1409    #[test]
1410    fn test_snapshot_storage() {
1411        let mut storage = SnapshotStorage::new();
1412        let snap = Snapshot {
1413            values: vec![tag(TAGINT, 42), tag(TAGBOX, 0)],
1414            prev: None,
1415            jitcode_index: 0,
1416            pc: 10,
1417        };
1418        let idx = storage.add_snapshot(snap);
1419        assert_eq!(idx, 0);
1420        assert_eq!(storage.num_snapshots(), 1);
1421
1422        let const_idx = storage.add_const_ref(0x1000);
1423        assert_eq!(const_idx, 0);
1424        let float_idx = storage.add_const_float(3.14);
1425        assert_eq!(float_idx, 0);
1426    }
1427
1428    #[test]
1429    fn test_trace_iterator_dead_ranges() {
1430        let ops = vec![
1431            majit_ir::Op::new(majit_ir::OpCode::IntAdd, &[OpRef(100), OpRef(101)]),
1432            majit_ir::Op::new(majit_ir::OpCode::IntAdd, &[OpRef(0), OpRef(101)]),
1433            majit_ir::Op::new(majit_ir::OpCode::Finish, &[OpRef(1)]),
1434        ];
1435        let mut iter = TraceIterator::new(&ops);
1436        assert_eq!(iter.num_ops(), 3);
1437        assert!(!iter.done());
1438
1439        let ranges = iter.compute_dead_ranges();
1440        // OpRef(100) is used at position 0 only → dead after 0
1441        // OpRef(101) is used at positions 0 and 1 → dead after 1
1442        assert!(ranges.len() > 100);
1443
1444        // Check iterator works
1445        assert!(iter.next_op().is_some());
1446        assert_eq!(iter.position(), 1);
1447    }
1448
1449    #[test]
1450    fn test_box_array_iter() {
1451        let values = vec![tag(TAGINT, 42), tag(TAGBOX, 10), tag(TAGCONSTPTR, 0)];
1452        let mut iter = BoxArrayIter::new(&values);
1453        assert_eq!(iter.remaining(), 3);
1454
1455        let (k, v) = iter.next_decoded().unwrap();
1456        assert_eq!(k, TAGINT);
1457        assert_eq!(v, 42);
1458
1459        let (k, v) = iter.next_decoded().unwrap();
1460        assert_eq!(k, TAGBOX);
1461        assert_eq!(v, 10);
1462
1463        assert_eq!(iter.remaining(), 1);
1464        assert!(!iter.done());
1465        iter.next_value();
1466        assert!(iter.done());
1467    }
1468
1469    #[test]
1470    fn test_trace_record_buffer() {
1471        let mut buf = TraceRecordBuffer::new(2);
1472        assert_eq!(buf.num_ops(), 0);
1473        assert!(!buf.overflowed());
1474        assert!(!buf.tag_overflow_imminent());
1475
1476        buf.record_op(OpCode::IntAdd as u16, 2);
1477        buf.record_arg(100);
1478        buf.record_arg(101);
1479        assert_eq!(buf.num_ops(), 1);
1480
1481        buf.cut_point();
1482        assert!(buf.tracing_done());
1483    }
1484
1485    #[test]
1486    fn test_trace_record_snapshot() {
1487        let mut buf = TraceRecordBuffer::new(1);
1488        let snap = Snapshot {
1489            values: vec![tag(TAGINT, 42)],
1490            prev: None,
1491            jitcode_index: 0,
1492            pc: 5,
1493        };
1494        let idx = buf.capture_resumedata(snap);
1495        assert_eq!(idx, 0);
1496        assert_eq!(buf.snapshots().num_snapshots(), 1);
1497    }
1498}