fidget_bytecode/
lib.rs

1//! Tape bytecode format
2//!
3//! Fidget's bytecode is a packed representation of a
4//! [`RegTape`](fidget_core::compiler::RegTape).  It may be used as the
5//! evaluation tape for non-Rust VMs, e.g. an interpreter running on a GPU.
6//!
7//! The format is **not stable**; it may change without notice.  It would be
8//! wise to dynamically check any interpreter against [`iter_ops`], which
9//! associates opcode integers with their names.
10//!
11//! The bytecode format is a list of little-endian `u32` words, representing
12//! tape operations in forward-evaluation order. Each operation in the tape maps
13//! to two words, though the second word is not always used.  Having a
14//! fixed-length representation makes it easier to iterate both forwards (for
15//! evaluation) and backwards (for simplification).
16//!
17//! The first two words are always `0xFFFF_FFFF 0x0000_0000`, and the last two
18//! words are always `0xFFFF_FFFF 0xFFFF_FFFF`.  Note that this is equivalent to
19//! an operation with opcode `0xFF`; this special opcode may also be used with
20//! user-defined semantics, as long as the immediate is not either reserved
21//! value.
22//!
23//! ## Register-only operations
24//!
25//! Register-only operations (i.e. opcodes without an immediate `f32` or `u32`)
26//! are packed into a single `u32` as follows:
27//!
28//! | Byte | Value                                       |
29//! |------|---------------------------------------------|
30//! | 0    | opcode                                      |
31//! | 1    | output register                             |
32//! | 2    | first input register                        |
33//! | 3    | second input register                       |
34//!
35//! Depending on the opcode, the input register bytes may not be used.
36//!
37//! The second word is always `0xFF000000`
38//!
39//! ## Operations with an `f32` immediate
40//!
41//! Operations with an `f32` immediate are packed into two `u32` words.
42//! The first word is similar to before:
43//!
44//! | Byte | Value                                       |
45//! |------|---------------------------------------------|
46//! | 0    | opcode                                      |
47//! | 1    | output register                             |
48//! | 2    | first input register                        |
49//! | 3    | not used                                    |
50//!
51//! The second word is the `f32` reinterpreted as a `u32`.
52//!
53//! ## Operations with an `u32` immediate
54//!
55//! Operations with a `u32` immediate (e.g.
56//! [`Load`](RegOp::Load)) are also packed into two `u32`
57//! words.  The first word is what you'd expect:
58//!
59//! | Byte | Value                                       |
60//! |------|---------------------------------------------|
61//! | 0    | opcode                                      |
62//! | 1    | input or output register                    |
63//! | 2    | not used                                    |
64//! | 3    | not used                                    |
65//!
66//! The second word is the `u32` immediate.
67//!
68//! ## Opcode values
69//!
70//! Opcode values are generated automatically from [`BytecodeOp`]
71//! values, which are one-to-one with [`RegOp`] variants.
72#![warn(missing_docs)]
73
74use fidget_core::{compiler::RegOp, vm::VmData};
75use zerocopy::IntoBytes;
76
77pub use fidget_core::compiler::RegOpDiscriminants as BytecodeOp;
78
79/// Serialized bytecode for external evaluation
80pub struct Bytecode {
81    reg_count: u8,
82    mem_count: u32,
83    data: Vec<u32>,
84}
85
86impl Bytecode {
87    /// Returns the length of the bytecode data (in `u32` words)
88    #[allow(clippy::len_without_is_empty)]
89    pub fn len(&self) -> usize {
90        self.data.len()
91    }
92
93    /// Raw serialized operations
94    pub fn data(&self) -> &[u32] {
95        &self.data
96    }
97
98    /// Maximum register index used by the tape
99    pub fn reg_count(&self) -> u8 {
100        self.reg_count
101    }
102
103    /// Maximum memory slot used for `Load` / `Store` operations
104    pub fn mem_count(&self) -> u32 {
105        self.mem_count
106    }
107
108    /// Returns a view of the byte slice
109    pub fn as_bytes(&self) -> &[u8] {
110        self.data.as_bytes()
111    }
112
113    /// Builds a new bytecode object from VM data
114    pub fn new<const N: usize>(t: &VmData<N>) -> Self {
115        // The initial opcode is `OP_JUMP 0x0000_0000`
116        let mut data = vec![u32::MAX, 0u32];
117        let mut reg_count = 0u8;
118        let mut mem_count = 0u32;
119        for op in t.iter_asm() {
120            let r = BytecodeOp::from(op);
121            let mut word = [r as u8, 0xFF, 0xFF, 0xFF];
122            let mut imm = None;
123            let mut store_reg = |i, r| {
124                reg_count = reg_count.max(r); // update the max reg
125                word[i] = r;
126            };
127            match op {
128                RegOp::Input(reg, slot) | RegOp::Output(reg, slot) => {
129                    store_reg(1, reg);
130                    imm = Some(slot);
131                }
132
133                RegOp::Load(reg, slot) | RegOp::Store(reg, slot) => {
134                    store_reg(1, reg);
135                    mem_count = mem_count.max(slot);
136                    imm = Some(slot);
137                }
138
139                RegOp::CopyImm(out, imm_f32) => {
140                    store_reg(1, out);
141                    imm = Some(imm_f32.to_bits());
142                }
143                RegOp::NegReg(out, reg)
144                | RegOp::AbsReg(out, reg)
145                | RegOp::RecipReg(out, reg)
146                | RegOp::SqrtReg(out, reg)
147                | RegOp::SquareReg(out, reg)
148                | RegOp::FloorReg(out, reg)
149                | RegOp::CeilReg(out, reg)
150                | RegOp::RoundReg(out, reg)
151                | RegOp::CopyReg(out, reg)
152                | RegOp::SinReg(out, reg)
153                | RegOp::CosReg(out, reg)
154                | RegOp::TanReg(out, reg)
155                | RegOp::AsinReg(out, reg)
156                | RegOp::AcosReg(out, reg)
157                | RegOp::AtanReg(out, reg)
158                | RegOp::ExpReg(out, reg)
159                | RegOp::LnReg(out, reg)
160                | RegOp::NotReg(out, reg) => {
161                    store_reg(1, out);
162                    store_reg(2, reg);
163                }
164
165                RegOp::AddRegImm(out, reg, imm_f32)
166                | RegOp::MulRegImm(out, reg, imm_f32)
167                | RegOp::DivRegImm(out, reg, imm_f32)
168                | RegOp::DivImmReg(out, reg, imm_f32)
169                | RegOp::SubImmReg(out, reg, imm_f32)
170                | RegOp::SubRegImm(out, reg, imm_f32)
171                | RegOp::AtanRegImm(out, reg, imm_f32)
172                | RegOp::AtanImmReg(out, reg, imm_f32)
173                | RegOp::MinRegImm(out, reg, imm_f32)
174                | RegOp::MaxRegImm(out, reg, imm_f32)
175                | RegOp::CompareRegImm(out, reg, imm_f32)
176                | RegOp::CompareImmReg(out, reg, imm_f32)
177                | RegOp::ModRegImm(out, reg, imm_f32)
178                | RegOp::ModImmReg(out, reg, imm_f32)
179                | RegOp::AndRegImm(out, reg, imm_f32)
180                | RegOp::OrRegImm(out, reg, imm_f32) => {
181                    store_reg(1, out);
182                    store_reg(2, reg);
183                    imm = Some(imm_f32.to_bits());
184                }
185
186                RegOp::AddRegReg(out, lhs, rhs)
187                | RegOp::MulRegReg(out, lhs, rhs)
188                | RegOp::DivRegReg(out, lhs, rhs)
189                | RegOp::SubRegReg(out, lhs, rhs)
190                | RegOp::AtanRegReg(out, lhs, rhs)
191                | RegOp::MinRegReg(out, lhs, rhs)
192                | RegOp::MaxRegReg(out, lhs, rhs)
193                | RegOp::CompareRegReg(out, lhs, rhs)
194                | RegOp::ModRegReg(out, lhs, rhs)
195                | RegOp::AndRegReg(out, lhs, rhs)
196                | RegOp::OrRegReg(out, lhs, rhs) => {
197                    store_reg(1, out);
198                    store_reg(2, lhs);
199                    store_reg(3, rhs);
200                }
201            }
202            data.push(u32::from_le_bytes(word));
203            data.push(imm.unwrap_or(0xFF000000));
204        }
205        // Add the final `OP_JUMP 0xFFFF_FFFF`
206        data.extend([u32::MAX, u32::MAX]);
207
208        Bytecode {
209            data,
210            mem_count,
211            reg_count,
212        }
213    }
214}
215
216/// Iterates over opcode `(names, value)` tuples, with names in `CamelCase`
217///
218/// This is a helper function for defining constants in a VM interpreter
219pub fn iter_ops<'a>() -> impl Iterator<Item = (&'a str, u8)> {
220    use strum::IntoEnumIterator;
221
222    BytecodeOp::iter().enumerate().map(|(i, op)| {
223        let s: &'static str = op.into();
224        (s, i as u8)
225    })
226}
227
228#[cfg(test)]
229mod test {
230    use super::*;
231
232    #[test]
233    fn simple_bytecode() {
234        let mut ctx = fidget_core::Context::new();
235        let x = ctx.x();
236        let c = ctx.constant(1.0);
237        let out = ctx.add(x, c).unwrap();
238        let data = VmData::<255>::new(&ctx, &[out]).unwrap();
239        let bc = Bytecode::new(&data);
240        let mut iter = bc.data.iter();
241        let mut next = || *iter.next().unwrap();
242        assert_eq!(next(), 0xFFFFFFFF); // start marker
243        assert_eq!(next(), 0);
244        assert_eq!(
245            next().to_le_bytes(),
246            [BytecodeOp::Input as u8, 0, 0xFF, 0xFF]
247        );
248        assert_eq!(next(), 0); // input slot 0
249        assert_eq!(
250            next().to_le_bytes(),
251            [BytecodeOp::AddRegImm as u8, 0, 0, 0xFF]
252        );
253        assert_eq!(f32::from_bits(next()), 1.0);
254        assert_eq!(
255            next().to_le_bytes(),
256            [BytecodeOp::Output as u8, 0, 0xFF, 0xFF]
257        );
258        assert_eq!(next(), 0); // output slot 0
259        assert_eq!(next(), 0xFFFFFFFF); // end marker
260        assert_eq!(next(), 0xFFFFFFFF);
261        assert!(iter.next().is_none());
262    }
263}