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}