Skip to main content

vyre_primitives/parsing/
bytecode_dispatch_table_pack.rs

1//! `bytecode_dispatch_table_pack`  -  pack an opcode-handler dispatch table
2//! into a constant-buffer for fast GPU-side bytecode interpretation.
3//!
4//! Op id: `vyre-primitives::parsing::bytecode_dispatch_table_pack`. Soundness:
5//! `Exact` over the opcode → handler-offset mapping. The canonical
6//! bytecode-on-GPU interpreter loop reads `dispatch_table[opcode]` to find
7//! which handler program to invoke, then executes it. Centralising the table
8//! layout and validation here lets every interpreter dialect (Lua-shape,
9//! JVM-shape, WASM-shape) share one well-typed packing format.
10//!
11//! ## Why it matters
12//!
13//! Bytecode interpreter loops on GPU lose on naive implementations because
14//! the dispatch-table fetch + indirect-branch pattern is the canonical
15//! "GPU loses to CPU" workload (CPU has branch predictor + huge L1; GPU
16//! has neither). The fix: pack the dispatch table into a constant-buffer
17//! that resides in shared memory + use uniform-control-flow patterns where
18//! every thread executes the same handler in the same warp (warp-specialized
19//! interpretation).
20//!
21//! This module ships the *packing* part. Interpreter loops read the packed
22//! table through this stable wire layout.
23//!
24//! ## Wire format
25//!
26//! Each table entry is one u32 packed as:
27//!
28//! ```text
29//!   bits 0..23   -  handler_offset (max 2^24 = 16M handlers  -  plenty)
30//!   bits 24..27  -  handler_arity  (number of operand bytes, 0..15)
31//!   bits 28..31  -  flags          (bit 28 = side_effecting, bit 29 = control_flow)
32//! ```
33//!
34//! The packed format means dispatch is one u32 load + one mask-and-shift
35//! per opcode. No pointer chasing.
36
37/// One opcode → handler entry, the host-side representation that
38/// `pack_dispatch_table` turns into a packed u32.
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
40pub struct OpcodeHandlerEntry {
41    /// Offset of the handler routine within the program-pool buffer.
42    /// Capped at 2^24 - 1 = 16777215.
43    pub handler_offset: u32,
44    /// Operand-byte count this handler reads after the opcode byte.
45    /// Capped at 15.
46    pub handler_arity: u8,
47    /// True if the handler has observable side effects (writes a buffer,
48    /// triggers a sync, etc). Codegen uses this to refuse fusion across.
49    pub side_effecting: bool,
50    /// True if the handler can change control flow (branch / call / return).
51    /// Interpreter optimizer uses this to disable speculation across.
52    pub control_flow: bool,
53}
54
55/// Pack errors. Returned when the host-side entry can't be encoded into the
56/// 1-u32 wire format.
57#[derive(Debug, Clone, PartialEq, Eq)]
58#[non_exhaustive]
59pub enum PackError {
60    /// Handler offset exceeded the 24-bit field budget.
61    OffsetTooLarge {
62        /// The opcode index whose entry overflowed.
63        opcode: usize,
64        /// The offset that exceeded `1 << 24`.
65        offset: u32,
66    },
67    /// Handler arity exceeded the 4-bit field budget.
68    ArityTooLarge {
69        /// The opcode index whose entry overflowed.
70        opcode: usize,
71        /// The arity that exceeded `15`.
72        arity: u8,
73    },
74    /// Caller-owned output could not reserve enough entries.
75    Allocation {
76        /// Requested packed entries.
77        requested: usize,
78        /// Allocator detail.
79        source: String,
80    },
81}
82
83/// Number of packed u32 words required for `entries`.
84#[must_use]
85#[inline]
86pub const fn packed_dispatch_table_len(entries_len: usize) -> usize {
87    entries_len
88}
89
90/// Pack a dispatch table of `OpcodeHandlerEntry` into one u32 per entry,
91/// suitable for upload as a constant buffer. The output index matches the
92/// input index; `output[opcode_byte]` is the packed entry.
93///
94/// # Errors
95///
96/// Returns the first encoding overflow encountered. Caller fixes by
97/// reducing the handler-offset (split into chunks) or refusing to register
98/// an arity > 15 handler.
99pub fn pack_dispatch_table(entries: &[OpcodeHandlerEntry]) -> Result<Vec<u32>, PackError> {
100    let mut out = Vec::new();
101    pack_dispatch_table_into(entries, &mut out)?;
102    Ok(out)
103}
104
105/// Pack a dispatch table into caller-owned storage.
106///
107/// This is the hot-path API for repeated interpreter construction: the
108/// caller owns `out`, and this function clears then reuses its capacity.
109///
110/// # Errors
111///
112/// Returns the first encoding overflow or output allocation failure
113/// encountered. On error, `out` is left unchanged.
114pub fn pack_dispatch_table_into(
115    entries: &[OpcodeHandlerEntry],
116    out: &mut Vec<u32>,
117) -> Result<(), PackError> {
118    for (idx, entry) in entries.iter().enumerate() {
119        if entry.handler_offset >= (1u32 << 24) {
120            return Err(PackError::OffsetTooLarge {
121                opcode: idx,
122                offset: entry.handler_offset,
123            });
124        }
125        if entry.handler_arity > 15 {
126            return Err(PackError::ArityTooLarge {
127                opcode: idx,
128                arity: entry.handler_arity,
129            });
130        }
131    }
132    let len = packed_dispatch_table_len(entries.len());
133    vyre_foundation::allocation::reserve_exact_cleared(out, len).map_err(|source| PackError::Allocation {
134        requested: len,
135        source: source.to_string(),
136    })?;
137    out.extend(entries.iter().map(|entry| {
138        let mut packed: u32 = entry.handler_offset & 0x00FF_FFFF;
139        packed |= (u32::from(entry.handler_arity) & 0xF) << 24;
140        if entry.side_effecting {
141            packed |= 1 << 28;
142        }
143        if entry.control_flow {
144            packed |= 1 << 29;
145        }
146        packed
147    }));
148    Ok(())
149}
150
151/// Pack one dispatch-table entry without validation.
152#[must_use]
153pub fn pack_entry(entry: OpcodeHandlerEntry) -> u32 {
154    let mut packed = entry.handler_offset & 0x00FF_FFFF;
155    packed |= (u32::from(entry.handler_arity) & 0xF) << 24;
156    if entry.side_effecting {
157        packed |= 1 << 28;
158    }
159    if entry.control_flow {
160        packed |= 1 << 29;
161    }
162    packed
163}
164
165/// Unpack one u32 entry back into the host-side representation. Used by
166/// interpreter Programs that read `dispatch_table[opcode]` and need to
167/// know which handler to invoke + how many operand bytes to consume.
168#[must_use]
169pub fn unpack_entry(packed: u32) -> OpcodeHandlerEntry {
170    OpcodeHandlerEntry {
171        handler_offset: packed & 0x00FF_FFFF,
172        handler_arity: ((packed >> 24) & 0xF) as u8,
173        side_effecting: (packed >> 28) & 0x1 == 1,
174        control_flow: (packed >> 29) & 0x1 == 1,
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::*;
181
182    #[test]
183    fn round_trip_preserves_entry_fields() {
184        let entry = OpcodeHandlerEntry {
185            handler_offset: 0x123456,
186            handler_arity: 7,
187            side_effecting: true,
188            control_flow: false,
189        };
190        let packed = pack_dispatch_table(&[entry]).expect("Fix: pack must succeed");
191        assert_eq!(packed.len(), 1);
192        let recovered = unpack_entry(packed[0]);
193        assert_eq!(recovered, entry, "round-trip must preserve every field");
194        assert_eq!(unpack_entry(pack_entry(entry)), entry);
195    }
196
197    #[test]
198    fn pack_into_reuses_output_and_is_transactional_on_invalid_entry() {
199        let entries = [
200            OpcodeHandlerEntry {
201                handler_offset: 1,
202                handler_arity: 2,
203                side_effecting: false,
204                control_flow: false,
205            },
206            OpcodeHandlerEntry {
207                handler_offset: 3,
208                handler_arity: 4,
209                side_effecting: true,
210                control_flow: true,
211            },
212        ];
213        let mut out = Vec::with_capacity(8);
214        out.extend_from_slice(&[u32::MAX; 8]);
215        let ptr = out.as_ptr();
216
217        pack_dispatch_table_into(&entries, &mut out).unwrap();
218
219        assert_eq!(
220            out,
221            entries.iter().copied().map(pack_entry).collect::<Vec<_>>()
222        );
223        assert_eq!(out.as_ptr(), ptr);
224        let before = out.clone();
225        let bad = [OpcodeHandlerEntry {
226            handler_offset: 1 << 24,
227            handler_arity: 0,
228            side_effecting: false,
229            control_flow: false,
230        }];
231        assert!(matches!(
232            pack_dispatch_table_into(&bad, &mut out),
233            Err(PackError::OffsetTooLarge { .. })
234        ));
235        assert_eq!(out, before);
236    }
237
238    #[test]
239    fn round_trip_handles_all_flag_combinations() {
240        for side_effecting in [false, true] {
241            for control_flow in [false, true] {
242                let entry = OpcodeHandlerEntry {
243                    handler_offset: 42,
244                    handler_arity: 3,
245                    side_effecting,
246                    control_flow,
247                };
248                let packed = pack_dispatch_table(&[entry]).unwrap();
249                assert_eq!(unpack_entry(packed[0]), entry);
250            }
251        }
252    }
253
254    #[test]
255    fn pack_rejects_offset_at_field_boundary() {
256        let entry = OpcodeHandlerEntry {
257            handler_offset: 1u32 << 24, // exactly the limit  -  must reject
258            handler_arity: 0,
259            side_effecting: false,
260            control_flow: false,
261        };
262        match pack_dispatch_table(&[entry]) {
263            Err(PackError::OffsetTooLarge { opcode: 0, offset }) => {
264                assert_eq!(offset, 1u32 << 24);
265            }
266            other => panic!("expected OffsetTooLarge at the 24-bit boundary; got {other:?}"),
267        }
268    }
269
270    #[test]
271    fn pack_rejects_arity_at_field_boundary() {
272        let entry = OpcodeHandlerEntry {
273            handler_offset: 0,
274            handler_arity: 16, // 4-bit field max is 15
275            side_effecting: false,
276            control_flow: false,
277        };
278        match pack_dispatch_table(&[entry]) {
279            Err(PackError::ArityTooLarge { opcode: 0, arity }) => {
280                assert_eq!(arity, 16);
281            }
282            other => panic!("expected ArityTooLarge at the 4-bit boundary; got {other:?}"),
283        }
284    }
285
286    #[test]
287    fn pack_preserves_per_entry_index_in_error() {
288        // Entry 7 is the bad one; error must report opcode = 7.
289        let mut entries = vec![
290            OpcodeHandlerEntry {
291                handler_offset: 0,
292                handler_arity: 0,
293                side_effecting: false,
294                control_flow: false,
295            };
296            10
297        ];
298        entries[7].handler_offset = 1u32 << 25; // overflow
299        match pack_dispatch_table(&entries) {
300            Err(PackError::OffsetTooLarge { opcode: 7, .. }) => {}
301            other => panic!("expected error at opcode 7; got {other:?}"),
302        }
303    }
304
305    #[test]
306    fn pack_empty_table_returns_empty_vec() {
307        let packed = pack_dispatch_table(&[]).expect("Fix: empty pack must succeed");
308        assert!(packed.is_empty());
309    }
310
311    #[test]
312    fn pack_into_reuses_existing_capacity() {
313        let entries = [
314            OpcodeHandlerEntry {
315                handler_offset: 8,
316                handler_arity: 2,
317                side_effecting: false,
318                control_flow: true,
319            },
320            OpcodeHandlerEntry {
321                handler_offset: 16,
322                handler_arity: 3,
323                side_effecting: true,
324                control_flow: false,
325            },
326        ];
327        let mut out = Vec::with_capacity(64);
328        let before = out.capacity();
329        pack_dispatch_table_into(&entries, &mut out).expect("Fix: pack_into must succeed");
330        assert_eq!(out.len(), entries.len());
331        assert_eq!(
332            out.capacity(),
333            before,
334            "pack_into must reuse caller-owned capacity"
335        );
336        assert_eq!(unpack_entry(out[0]), entries[0]);
337        assert_eq!(unpack_entry(out[1]), entries[1]);
338    }
339
340    #[test]
341    fn required_len_matches_entry_count() {
342        assert_eq!(packed_dispatch_table_len(0), 0);
343        assert_eq!(packed_dispatch_table_len(256), 256);
344    }
345
346    #[test]
347    fn pack_full_256_opcode_table_succeeds() {
348        // Realistic interpreter: 256 opcodes, each with a handler.
349        let entries: Vec<_> = (0..256u32)
350            .map(|opcode| OpcodeHandlerEntry {
351                handler_offset: opcode * 16,
352                handler_arity: (opcode % 4) as u8,
353                side_effecting: opcode % 2 == 0,
354                control_flow: opcode % 8 == 0,
355            })
356            .collect();
357        let packed = pack_dispatch_table(&entries).expect("Fix: full 256-table must pack");
358        assert_eq!(packed.len(), 256);
359        // Spot-check a few entries.
360        assert_eq!(unpack_entry(packed[0]), entries[0]);
361        assert_eq!(unpack_entry(packed[127]), entries[127]);
362        assert_eq!(unpack_entry(packed[255]), entries[255]);
363    }
364
365    #[test]
366    fn handler_arity_zero_packs_cleanly() {
367        let entry = OpcodeHandlerEntry {
368            handler_offset: 100,
369            handler_arity: 0,
370            side_effecting: false,
371            control_flow: false,
372        };
373        let packed = pack_dispatch_table(&[entry]).unwrap();
374        assert_eq!(packed[0], 100, "arity=0 + flags=0 packs as just the offset");
375    }
376}