gbf_core/
opcode.rs

1#![deny(missing_docs)]
2
3use serde::{Deserialize, Serialize};
4use std::{fmt::Display, str::FromStr};
5use thiserror::Error;
6
7/// Error type for invalid opcodes.
8#[derive(Error, Debug, Serialize, Deserialize, Clone)]
9pub enum OpcodeError {
10    /// Error for when an invalid opcode is encountered.
11    #[error("Invalid opcode: {0}")]
12    InvalidOpcode(u8),
13
14    /// Error for when an invalid opcode string is encountered.
15    #[error("Invalid opcode: {0}")]
16    InvalidOpcodeString(String),
17}
18
19/// A macro to define opcodes as a `#[repr(u8)]` enum.
20///
21/// # Overview
22/// This macro simplifies defining opcodes by automatically generating:
23/// - An `Opcode` enum with associated `u8` values.
24/// - Utility methods like `from_byte` for safe conversion and `to_byte` for reverse mapping.
25macro_rules! define_opcodes {
26    (
27        $( $name:ident = $value:expr ),* $(,)?
28    ) => {
29        /// Enum representing opcodes for a bytecode system.
30        ///
31        /// Each variant corresponds to an opcode, with its numeric value
32        /// defined as a `u8`.
33        #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Hash)]
34        #[repr(u8)]
35        pub enum Opcode {
36            $(
37                #[doc = concat!("Represents the opcode ", stringify!($name))]
38                $name = $value,
39            )*
40        }
41
42        impl Opcode {
43            /// Converts a `u8` into an `Opcode`, if possible.
44            ///
45            /// # Arguments
46            /// - `byte`: The `u8` value to convert.
47            ///
48            /// # Returns
49            /// - `Some(Opcode)` if the value corresponds to a valid opcode.
50            /// - `None` if the value does not match any defined opcode.
51            ///
52            /// # Example
53            /// ```
54            /// use gbf_core::opcode::Opcode;
55            ///
56            /// let opcode = Opcode::from_byte(0x1).unwrap();
57            /// assert_eq!(opcode, Opcode::Jmp);
58            /// ```
59            pub fn from_byte(byte: u8) -> Result<Self, OpcodeError> {
60                match byte {
61                    $(
62                        $value => Ok(Opcode::$name),
63                    )*
64                    _ => Err(OpcodeError::InvalidOpcode(byte)),
65                }
66            }
67
68            /// Converts an `Opcode` to its `u8` value.
69            ///
70            /// # Returns
71            /// - The numeric value (`u8`) of the opcode.
72            ///
73            /// # Example
74            /// ```
75            /// use gbf_core::opcode::Opcode;
76            ///
77            /// let opcode = Opcode::Jmp;
78            /// assert_eq!(opcode.to_byte(), 0x1);
79            /// ```
80            pub fn to_byte(self) -> u8 {
81                self as u8
82            }
83
84            /// Get the number of defined opcodes.
85            ///
86            /// # Returns
87            /// - The number of opcodes.
88            ///
89            /// # Example
90            /// ```
91            /// use gbf_core::opcode::Opcode;
92            ///
93            /// let count = Opcode::count();
94            /// ```
95            pub fn count() -> usize {
96                0 $(+ { let _ = stringify!($name); 1 })*
97            }
98
99            /// Get a list of all defined opcodes.
100            ///
101            /// # Returns
102            /// - A vector containing all defined opcodes.
103            ///
104            /// # Example
105            /// ```
106            /// use gbf_core::opcode::Opcode;
107            ///
108            /// let opcodes = Opcode::all();
109            /// ```
110            pub fn all() -> &'static [Opcode] {
111                &[
112                    $(
113                        Opcode::$name,
114                    )*
115                ]
116            }
117
118            /// If the opcode is a conditional jump instruction.
119            ///
120            /// # Returns
121            /// - `true` if the opcode is a conditional jump instruction.
122            /// - `false` otherwise.
123            ///
124            /// # Example
125            /// ```
126            /// use gbf_core::opcode::Opcode;
127            ///
128            /// let opcode = Opcode::Jeq;
129            /// assert!(opcode.is_conditional_jump());
130            /// ```
131            pub fn is_conditional_jump(self) -> bool {
132                return match self {
133                    $(
134                        Opcode::$name => {
135                            matches!(self, Opcode::Jeq | Opcode::Jne)
136                        },
137                    )*
138                };
139            }
140
141            /// If the opcode is an unconditional jump instruction.
142            ///
143            /// # Returns
144            /// - `true` if the opcode is an unconditional jump instruction.
145            /// - `false` otherwise.
146            ///
147            /// # Example
148            /// ```
149            /// use gbf_core::opcode::Opcode;
150            ///
151            /// let opcode = Opcode::Jmp;
152            /// assert!(opcode.is_unconditional_jump());
153            /// ```
154            pub fn is_unconditional_jump(self) -> bool {
155                return match self {
156                    $(
157                        Opcode::$name => {
158                            matches!(self, Opcode::Jmp)
159                        },
160                    )*
161                };
162            }
163
164            /// If this CFG-related opcode contains a branch with a fall-through path to the next instruction.
165            ///
166            /// # Returns
167            /// - `true` if the opcode is a block-starting opcode.
168            /// - `false` otherwise.
169            ///
170            /// # Example
171            /// ```
172            /// use gbf_core::opcode::Opcode;
173            ///
174            /// let opcode = Opcode::Jmp;
175            /// assert!(!opcode.has_fall_through());
176            /// ```
177            pub fn has_fall_through(self) -> bool {
178                return self.is_conditional_jump() || match self {
179                    $(
180                        Opcode::$name => {
181                            matches!(self, Opcode::With | Opcode::ShortCircuitAnd | Opcode::ShortCircuitOr | Opcode::ForEach)
182                        },
183                    )*
184                };
185            }
186
187            /// If this CFG or non-CFG related opcode should connect to the next block if it is a terminal
188            /// opcode.
189            ///
190            /// # Returns
191            /// - `true` if the opcode has a fall-through path.
192            /// - `false` otherwise.
193            ///
194            /// # Example
195            /// ```
196            /// use gbf_core::opcode::Opcode;
197            ///
198            /// let opcode = Opcode::Jmp;
199            /// assert!(!opcode.connects_to_next_block());
200            ///
201            /// let opcode2 = Opcode::Ret;
202            /// assert!(!opcode2.connects_to_next_block());
203            /// ```
204            pub fn connects_to_next_block(self) -> bool {
205                !self.is_unconditional_jump() && !matches!(self, Opcode::Ret)
206            }
207
208            /// If this CFG-related opcode has a corresponding jump target as an operand.
209            ///
210            /// # Returns
211            /// - `true` if the opcode has a jump target.
212            /// - `false` otherwise.
213            ///
214            /// # Example
215            /// ```
216            /// use gbf_core::opcode::Opcode;
217            ///
218            /// let opcode = Opcode::Jmp;
219            /// assert!(opcode.has_jump_target());
220            /// ```
221            pub fn has_jump_target(self) -> bool {
222                return self.is_unconditional_jump() || self.has_fall_through();
223            }
224
225            /// If this CFG-related opcode should always be the last opcode in a block. If there is an
226            /// instruction that succeeds this opcode, it should be considered the start of a different block.
227            ///
228            /// # Returns
229            /// - `true` if the opcode is block-ending.
230            /// - `false` otherwise.
231            ///
232            /// # Example
233            /// ```
234            /// use gbf_core::opcode::Opcode;
235            ///
236            /// let opcode = Opcode::Ret;
237            /// assert!(opcode.is_block_end());
238            /// ```
239            pub fn is_block_end(self) -> bool {
240                return self.has_jump_target() || match self {
241                    $(
242                        Opcode::$name => {
243                            matches!(self, Opcode::Ret | Opcode::ShortCircuitEnd | Opcode::WithEnd)
244                        },
245                    )*
246                };
247            }
248        }
249
250        impl Display for Opcode {
251            /// Convert an `Opcode` to a human-readable string.
252            ///
253            /// # Returns
254            /// - A string representation of the opcode.
255            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
256                write!(f, "{}", match self {
257                    $(
258                        Opcode::$name => stringify!($name),
259                    )*
260                })
261            }
262        }
263
264        impl FromStr for Opcode {
265            type Err = OpcodeError;
266
267            /// Convert a string to an `Opcode`
268            ///
269            /// # Arguments
270            /// - `name`: The string to convert.
271            ///
272            /// # Returns
273            /// - `Ok(Opcode)` if the string corresponds to a valid opcode.
274            fn from_str(s: &str) -> Result<Self, Self::Err> {
275                match s {
276                    $(
277                        stringify!($name) => Ok(Opcode::$name),
278                    )*
279                    _ => Err(OpcodeError::InvalidOpcodeString(s.to_string())),
280                }
281            }
282        }
283    };
284}
285
286// Using the macro to define the opcodes
287define_opcodes! {
288    Jmp = 0x1,
289    Jeq = 0x2,
290    ShortCircuitOr = 0x3,
291    Jne = 0x4,
292    ShortCircuitAnd = 0x5,
293    Call = 0x6,
294    Ret = 0x7,
295    Sleep = 0x8,
296    IncreaseLoopCounter = 0x9,
297    FunctionStart = 0xa,
298    WaitFor = 0xb,
299    // Unknown values: 0xc - 0x13
300    PushNumber = 0x14,
301    PushString = 0x15,
302    PushVariable = 0x16,
303    PushArray = 0x17,
304    PushTrue = 0x18,
305    PushFalse = 0x19,
306    PushNull = 0x1a,
307    Pi = 0x1b,
308    // Unknown values: 0x1c  - 0x1d
309    Copy = 0x1e,
310    Swap = 0x1f,
311    Pop = 0x20,
312    ConvertToFloat = 0x21,
313    ConvertToString = 0x22,
314    AccessMember = 0x23,
315    ConvertToObject = 0x24,
316    EndArray = 0x25,
317    NewUninitializedArray = 0x26,
318    SetArray = 0x27,
319    New = 0x28,
320    MakeVar = 0x29,
321    NewObject = 0x2a,
322    ConvertToVariable = 0x2b,
323    ShortCircuitEnd = 0x2c,
324    SetRegister = 0x2d,
325    GetRegister = 0x2e,
326    MarkRegisterVariable = 0x2f,
327    // Unknown values: 0x30 - 0x31
328    Assign = 0x32,
329    EndParams = 0x33,
330    Inc = 0x34,
331    Dec = 0x35,
332    // Unknown values: 0x36 - 0x3b
333    Add = 0x3c,
334    Subtract = 0x3d,
335    Multiply = 0x3e,
336    Divide = 0x3f,
337    Modulo = 0x40,
338    Power = 0x41,
339    // Unknown values: 0x42 - 0x43
340    LogicalNot = 0x44,
341    UnarySubtract = 0x45,
342    Equal = 0x46,
343    NotEqual = 0x47,
344    LessThan = 0x48,
345    GreaterThan = 0x49,
346    LessThanOrEqual = 0x4a,
347    GreaterThanOrEqual = 0x4b,
348    BitwiseOr = 0x4c,
349    BitwiseAnd = 0x4d,
350    BitwiseXor = 0x4e,
351    BitwiseInvert = 0x4f,
352    InRange = 0x50,
353    In = 0x51,
354    ObjIndex = 0x52,
355    ObjType = 0x53,
356    Format = 0x54,
357    Int = 0x55,
358    Abs = 0x56,
359    Random = 0x57,
360    Sin = 0x58,
361    Cos = 0x59,
362    ArcTan = 0x5a,
363    Exp = 0x5b,
364    Log = 0x5c,
365    Min = 0x5d,
366    Max = 0x5e,
367    GetAngle = 0x5f,
368    GetDir = 0x60,
369    VecX = 0x61,
370    VecY = 0x62,
371    ObjIndices = 0x63,
372    ObjLink = 0x64,
373    ShiftLeft = 0x65,
374    ShiftRight = 0x66,
375    Char = 0x67,
376    // Unknown values: 0x68 - 0x6d
377    ObjTrim = 0x6e,
378    ObjLength = 0x6f,
379    ObjPos = 0x70,
380    Join = 0x71,
381    ObjCharAt = 0x72,
382    ObjSubstring = 0x73,
383    ObjStarts = 0x74,
384    ObjEnds = 0x75,
385    ObjTokenize = 0x76,
386    GetTranslation = 0x77,
387    ObjPositions = 0x78,
388    // Unknown values: 0x78 - 0x81
389    ObjSize = 0x82,
390    ArrayAccess = 0x83,
391    AssignArray = 0x84,
392    AssignMultiDimensionalArrayIndex = 0x85,
393    AssignMultiDimensionalArray = 0x86,
394    ObjSubArray = 0x87,
395    ObjAddString = 0x88,
396    ObjDeleteString = 0x89,
397    ObjRemoveString = 0x8a,
398    ObjReplaceString = 0x8b,
399    ObjInsertString = 0x8c,
400    ObjClear = 0x8d,
401    MultiDimenArray = 0x8e,
402    // Unknown values: 0x8f - 0x95
403    With = 0x96,
404    WithEnd = 0x97,
405    // Unknown values: 0x98 - 0xa2
406    ForEach = 0xa3,
407    // Unknown values: 0xa4 - 0xb3
408    This = 0xb4,
409    ThisO = 0xb5,
410    Player = 0xb6,
411    PlayerO = 0xb7,
412    Level = 0xb8,
413    // Unknown values: 0xb9 - 0xbc
414    Temp = 0xbd,
415    Params = 0xbe,
416    // Unknown values: 0xbf - 0xef
417    ImmStringByte = 0xf0,
418    ImmStringShort = 0xf1,
419    ImmStringInt = 0xf2,
420    ImmByte = 0xf3,
421    ImmShort = 0xf4,
422    ImmInt = 0xf5,
423    ImmFloat = 0xf6,
424}
425
426#[cfg(test)]
427mod tests {
428    use super::*;
429
430    #[test]
431    fn test_valid_conversion() {
432        assert_eq!(Opcode::from_byte(0x1).unwrap(), Opcode::Jmp);
433        assert_eq!(Opcode::from_byte(0x21).unwrap(), Opcode::ConvertToFloat);
434    }
435
436    #[test]
437    fn test_invalid_conversion() {
438        assert!(Opcode::from_byte(0xFF).is_err());
439        assert!(Opcode::from_byte(0x68).is_err());
440    }
441
442    #[test]
443    fn test_to_byte() {
444        assert_eq!(Opcode::Jmp.to_byte(), 0x1);
445        assert_eq!(Opcode::ConvertToString.to_byte(), 0x22);
446    }
447
448    #[test]
449    fn test_fmt() {
450        assert_eq!(Opcode::Jmp.to_string(), "Jmp");
451        assert_eq!(Opcode::ConvertToString.to_string(), "ConvertToString");
452
453        let opcode = Opcode::from_byte(0x1).unwrap();
454        assert_eq!(format!("{}", opcode), "Jmp");
455
456        let opcode = Opcode::from_byte(0x22).unwrap();
457        assert_eq!(format!("{}", opcode), "ConvertToString");
458
459        // Test all opcodes
460        for opcode in Opcode::all() {
461            let str_val = format!("{}", opcode);
462            let from_str = Opcode::from_str(&str_val).unwrap();
463            assert_eq!(*opcode, from_str);
464        }
465    }
466
467    #[test]
468    fn test_from_str() {
469        assert_eq!(Opcode::from_str("Jmp").unwrap(), Opcode::Jmp);
470        assert_eq!(
471            Opcode::from_str("ConvertToString").unwrap(),
472            Opcode::ConvertToString
473        );
474        assert!(Opcode::from_str("Invalid").is_err());
475    }
476
477    #[test]
478    fn test_count() {
479        assert_eq!(Opcode::all().len(), Opcode::count());
480    }
481
482    #[test]
483    fn test_is_conditional_jump() {
484        assert!(Opcode::Jeq.is_conditional_jump());
485        assert!(Opcode::Jne.is_conditional_jump());
486        assert!(!Opcode::Jmp.is_conditional_jump());
487    }
488
489    #[test]
490    fn test_is_unconditional_jump() {
491        assert!(Opcode::Jmp.is_unconditional_jump());
492        assert!(!Opcode::Jeq.is_unconditional_jump());
493        assert!(!Opcode::Jne.is_unconditional_jump());
494    }
495
496    #[test]
497    fn test_has_jump_target() {
498        assert!(Opcode::Jmp.has_jump_target());
499        assert!(Opcode::Jeq.has_jump_target());
500        assert!(Opcode::Jne.has_jump_target());
501        assert!(Opcode::With.has_jump_target());
502        assert!(Opcode::ShortCircuitAnd.has_jump_target());
503        assert!(Opcode::ShortCircuitOr.has_jump_target());
504        assert!(Opcode::Jmp.has_jump_target());
505    }
506
507    #[test]
508    fn test_is_block_end() {
509        assert!(Opcode::Jmp.is_block_end());
510        assert!(Opcode::Ret.is_block_end());
511        assert!(Opcode::ShortCircuitEnd.is_block_end());
512        assert!(Opcode::WithEnd.is_block_end());
513        assert!(Opcode::Jeq.is_block_end());
514        assert!(Opcode::Jne.is_block_end());
515        assert!(Opcode::ShortCircuitAnd.is_block_end());
516        assert!(Opcode::ShortCircuitOr.is_block_end());
517        assert!(Opcode::ForEach.is_block_end());
518        assert!(Opcode::ShortCircuitEnd.is_block_end());
519    }
520}