Skip to main content

qcode/value/insn/
binop.rs

1use std::fmt::{Display, Formatter};
2
3use crate::value::LocalValueId;
4
5use super::mnemonic::{Args, MnemonicKind};
6use smallvec::smallvec;
7
8#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
9pub struct Binary {
10    pub op: Binop,
11    pub lhs: LocalValueId,
12    pub rhs: LocalValueId,
13}
14
15impl MnemonicKind for Binary {
16    fn opcode(&self) -> &'static str {
17        "binop"
18    }
19
20    fn args(&self) -> Args {
21        smallvec![self.lhs, self.rhs]
22    }
23}
24
25#[non_exhaustive]
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
27pub enum Binop {
28    Int(IntBinop),
29    Float(FloatBinop),
30}
31
32impl Display for Binop {
33    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
34        match self {
35            Binop::Int(op) => write!(f, "{}", op),
36            Binop::Float(op) => write!(f, "{}", op),
37        }
38    }
39}
40
41impl Binop {
42    pub fn is_comparison(self) -> bool {
43        match self {
44            Binop::Int(op) => op.is_comparison(),
45            Binop::Float(op) => op.is_comparison(),
46        }
47    }
48
49    pub fn is_shift(self) -> bool {
50        matches!(
51            self,
52            Binop::Int(IntBinop::ShiftLeft | IntBinop::ShiftRight | IntBinop::SShiftRight)
53        )
54    }
55}
56
57#[non_exhaustive]
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
59pub enum IntBinop {
60    Equal,
61    NotEqual,
62    Less,
63    SLess,
64    LessEqual,
65    SLessEqual,
66    Add,
67    Sub,
68    Xor,
69    And,
70    Or,
71    ShiftLeft,
72    ShiftRight,
73    SShiftRight,
74    Mul,
75    Div,
76    Rem,
77    Sdiv,
78    Srem,
79}
80
81impl IntBinop {
82    pub fn is_comparison(self) -> bool {
83        matches!(
84            self,
85            IntBinop::Equal
86                | IntBinop::NotEqual
87                | IntBinop::Less
88                | IntBinop::SLess
89                | IntBinop::LessEqual
90                | IntBinop::SLessEqual
91        )
92    }
93
94    /// Evaluates this operation on raw bit patterns.
95    ///
96    /// `size` is the operand width in bytes. Arithmetic results are masked to `size` bytes.
97    /// Comparison results are always `0` or `1`.
98    pub fn eval(&self, lhs: u128, rhs: u128, size: usize) -> u128 {
99        use super::bits::{mask_for_size, signed_value};
100        let mask = mask_for_size(size);
101        let a = lhs & mask;
102        let b = rhs & mask;
103        match self {
104            IntBinop::Equal => u128::from(a == b),
105            IntBinop::NotEqual => u128::from(a != b),
106            IntBinop::Less => u128::from(a < b),
107            IntBinop::SLess => u128::from(signed_value(a, size) < signed_value(b, size)),
108            IntBinop::LessEqual => u128::from(a <= b),
109            IntBinop::SLessEqual => u128::from(signed_value(a, size) <= signed_value(b, size)),
110            IntBinop::Add => a.wrapping_add(b) & mask,
111            IntBinop::Sub => a.wrapping_sub(b) & mask,
112            IntBinop::Mul => a.wrapping_mul(b) & mask,
113            IntBinop::Div => a.checked_div(b).unwrap_or(0),
114            IntBinop::Rem => a.checked_rem(b).unwrap_or(0),
115            IntBinop::Sdiv => {
116                let l = signed_value(a, size);
117                let r = signed_value(b, size);
118                if r == 0 {
119                    0
120                } else {
121                    l.overflowing_div(r).0 as u128 & mask
122                }
123            }
124            IntBinop::Srem => {
125                let l = signed_value(a, size);
126                let r = signed_value(b, size);
127                if r == 0 {
128                    0
129                } else {
130                    l.overflowing_rem(r).0 as u128 & mask
131                }
132            }
133            IntBinop::And => a & b,
134            IntBinop::Or => a | b,
135            IntBinop::Xor => a ^ b,
136            // The p-code shift operations take the shift amount as the full
137            // unsigned value of input1, which may be wider than input0 and is
138            // therefore not masked to `size`. A count at or beyond input0's
139            // width shifts every bit out rather than wrapping around.
140            IntBinop::ShiftLeft => shift_amount(rhs, size).map_or(0, |n| (a << n) & mask),
141            IntBinop::ShiftRight => shift_amount(rhs, size).map_or(0, |n| (a >> n) & mask),
142            IntBinop::SShiftRight => {
143                let signed = signed_value(a, size);
144                match shift_amount(rhs, size) {
145                    Some(n) => (signed >> n) as u128 & mask,
146                    // Shifting past the width leaves a copy of the sign bit.
147                    None if signed < 0 => mask,
148                    None => 0,
149                }
150            }
151        }
152    }
153}
154
155/// Returns the p-code shift amount for an `size`-byte input0, or `None` when
156/// the count is at or beyond input0's width and every bit is shifted out.
157fn shift_amount(rhs: u128, size: usize) -> Option<u32> {
158    let bits = u128::from(u32::try_from(size).ok()?.saturating_mul(8));
159    (rhs < bits).then_some(rhs as u32)
160}
161
162impl Display for IntBinop {
163    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
164        let s = match self {
165            IntBinop::Equal => "==",
166            IntBinop::NotEqual => "!=",
167            IntBinop::Less => "<",
168            IntBinop::SLess => "s<",
169            IntBinop::LessEqual => "<=",
170            IntBinop::SLessEqual => "s<=",
171            IntBinop::Add => "+",
172            IntBinop::Sub => "-",
173            IntBinop::Xor => "^",
174            IntBinop::And => "&",
175            IntBinop::Or => "|",
176            IntBinop::ShiftLeft => "<<",
177            IntBinop::ShiftRight => ">>",
178            IntBinop::SShiftRight => "s>>",
179            IntBinop::Mul => "*",
180            IntBinop::Div => "/",
181            IntBinop::Rem => "%",
182            IntBinop::Sdiv => "s/",
183            IntBinop::Srem => "s%",
184        };
185
186        write!(f, "{}", s)
187    }
188}
189
190#[non_exhaustive]
191#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
192pub enum FloatBinop {
193    Equal,
194    NotEqual,
195    Less,
196    LessEqual,
197    Add,
198    Sub,
199    Mul,
200    Div,
201}
202
203impl FloatBinop {
204    pub fn is_comparison(self) -> bool {
205        matches!(
206            self,
207            FloatBinop::Equal | FloatBinop::NotEqual | FloatBinop::Less | FloatBinop::LessEqual
208        )
209    }
210}
211
212impl Display for FloatBinop {
213    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
214        let s = match self {
215            FloatBinop::Equal => "f==",
216            FloatBinop::NotEqual => "f!=",
217            FloatBinop::Less => "f<",
218            FloatBinop::LessEqual => "f<=",
219            FloatBinop::Add => "f+",
220            FloatBinop::Sub => "f-",
221            FloatBinop::Mul => "f*",
222            FloatBinop::Div => "f/",
223        };
224
225        write!(f, "{}", s)
226    }
227}
228
229#[cfg(test)]
230mod tests {
231    use wazabin_qcode_macro::qcode;
232
233    use crate::context::Context;
234    use crate::value::insn::{Instruction, Mnemonic};
235
236    use super::*;
237
238    #[test]
239    fn test_add_display() {
240        let mut ctx = Context::new();
241
242        qcode!(
243            ctx,
244            "
245            <block>
246                local i32 V0;
247                %v0 = load(V0:4, V0);
248                %v = i32 %v0 + i32 0x2;
249                goto <0x1001>;
250            "
251        );
252
253        let v = Instruction::from_id(&ctx, v);
254
255        match v.mnemonic() {
256            Mnemonic::Binop(Binary {
257                op: Binop::Int(op), ..
258            }) => assert_eq!(*op, IntBinop::Add),
259            _ => panic!("expected i32 integer binop instruction, found {}", v),
260        }
261
262        assert_eq!(v.size(), 4);
263        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 + i32 0x2;");
264    }
265
266    #[test]
267    fn test_sub_display() {
268        let mut ctx = Context::new();
269
270        qcode!(
271            ctx,
272            "
273            <block>
274                local i32 V0;
275                %v0 = load(V0:4, V0);
276                %v = i32 %v0 - i32 0x2;
277                goto <0x1001>;
278            "
279        );
280
281        let v = Instruction::from_id(&ctx, v);
282
283        match v.mnemonic() {
284            Mnemonic::Binop(Binary {
285                op: Binop::Int(op), ..
286            }) => assert_eq!(*op, IntBinop::Sub),
287            _ => panic!("expected i32 integer binop instruction, found {}", v),
288        }
289
290        assert_eq!(v.size(), 4);
291        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 - i32 0x2;");
292    }
293
294    #[test]
295    fn test_mul_display() {
296        let mut ctx = Context::new();
297
298        qcode!(
299            ctx,
300            "
301            <block>
302               local i32 V0;
303                %v0 = load(V0:4, V0);
304                %v = i32 %v0 * i32 0x2;
305                goto <0x1001>;
306            "
307        );
308
309        let v = Instruction::from_id(&ctx, v);
310
311        match v.mnemonic() {
312            Mnemonic::Binop(Binary {
313                op: Binop::Int(op), ..
314            }) => assert_eq!(*op, IntBinop::Mul),
315            _ => panic!("expected i32 integer binop instruction, found {}", v),
316        }
317
318        assert_eq!(v.size(), 4);
319        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 * i32 0x2;");
320    }
321
322    #[test]
323    fn test_div_display() {
324        let mut ctx = Context::new();
325
326        qcode!(
327            ctx,
328            "
329            <block>
330                local i32 V0;
331                %v0 = load(V0:4, V0);
332                %v = i32 %v0 / i32 0x2;
333                goto <0x1001>;
334            "
335        );
336
337        let v = Instruction::from_id(&ctx, v);
338
339        match v.mnemonic() {
340            Mnemonic::Binop(Binary {
341                op: Binop::Int(op), ..
342            }) => assert_eq!(*op, IntBinop::Div),
343            _ => panic!("expected i32 integer binop instruction, found {}", v),
344        }
345
346        assert_eq!(v.size(), 4);
347        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 / i32 0x2;");
348    }
349
350    #[test]
351    fn test_bit_and_display() {
352        let mut ctx = Context::new();
353
354        qcode!(
355            ctx,
356            "
357            <block>
358                local i32 V0;
359                %v0 = load(V0:4, V0);
360                %v = i32 %v0 & i32 0x2;
361                goto <0x1001>;
362            "
363        );
364
365        let v = Instruction::from_id(&ctx, v);
366
367        match v.mnemonic() {
368            Mnemonic::Binop(Binary {
369                op: Binop::Int(op), ..
370            }) => assert_eq!(*op, IntBinop::And),
371            _ => panic!("expected i32 integer binop instruction, found {}", v),
372        }
373
374        assert_eq!(v.size(), 4);
375        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 & i32 0x2;");
376    }
377
378    #[test]
379    fn test_bit_or_display() {
380        let mut ctx = Context::new();
381
382        qcode!(
383            ctx,
384            "
385            <block>
386                local i32 V0;
387                %v0 = load(V0:4, V0);
388                %v = i32 %v0 | i32 0x2;
389                goto <0x1001>;
390            "
391        );
392
393        let v = Instruction::from_id(&ctx, v);
394
395        match v.mnemonic() {
396            Mnemonic::Binop(Binary {
397                op: Binop::Int(op), ..
398            }) => assert_eq!(*op, IntBinop::Or),
399            _ => panic!("expected i32 integer binop instruction, found {}", v),
400        }
401
402        assert_eq!(v.size(), 4);
403        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 | i32 0x2;");
404    }
405
406    #[test]
407    fn test_bit_xor_display() {
408        let mut ctx = Context::new();
409
410        qcode!(
411            ctx,
412            "
413            <block>
414                local i32 V0;
415                %v0 = load(V0:4, V0);
416                %v = i32 %v0 ^ i32 0x2;
417                goto <0x1001>;
418            "
419        );
420
421        let v = Instruction::from_id(&ctx, v);
422
423        match v.mnemonic() {
424            Mnemonic::Binop(Binary {
425                op: Binop::Int(op), ..
426            }) => assert_eq!(*op, IntBinop::Xor),
427            _ => panic!("expected i32 integer binop instruction, found {}", v),
428        }
429
430        assert_eq!(v.size(), 4);
431        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 ^ i32 0x2;");
432    }
433
434    /// p-code takes a shift amount as the full unsigned value of input1,
435    /// which may be wider than input0. A count at or beyond input0's width
436    /// shifts every bit out instead of wrapping, and an arithmetic right
437    /// shift leaves a copy of the sign bit.
438    #[test]
439    fn shift_amount_is_not_masked_to_the_operand_width() {
440        // A count that only looks small once truncated to input0's width.
441        // MMX shift-by-register relies on this: PSRLW's count is the whole
442        // 64-bit source, so 0xc000_0000_0000_0001 must empty each 16-bit lane
443        // rather than shift it by one.
444        let wide = 0xc000_0000_0000_0001u128;
445        assert_eq!(IntBinop::ShiftRight.eval(0x8000, wide, 2), 0);
446        assert_eq!(IntBinop::ShiftLeft.eval(0x8000, wide, 2), 0);
447        assert_eq!(IntBinop::SShiftRight.eval(0x8000, wide, 2), 0xffff);
448        assert_eq!(IntBinop::SShiftRight.eval(0x7fff, wide, 2), 0);
449
450        // Exactly at the width every bit is still shifted out.
451        assert_eq!(IntBinop::ShiftRight.eval(0xffff, 16, 2), 0);
452        assert_eq!(IntBinop::ShiftLeft.eval(0xffff, 16, 2), 0);
453        assert_eq!(IntBinop::SShiftRight.eval(0x8000, 16, 2), 0xffff);
454
455        // Ordinary in-range counts are unaffected.
456        assert_eq!(IntBinop::ShiftRight.eval(0x8000, 1, 2), 0x4000);
457        assert_eq!(IntBinop::ShiftLeft.eval(0x0001, 15, 2), 0x8000);
458        assert_eq!(IntBinop::SShiftRight.eval(0x8000, 1, 2), 0xc000);
459        assert_eq!(IntBinop::ShiftRight.eval(0x8000, 0, 2), 0x8000);
460
461        // A full-width 128-bit operand has no count that fits in u32 alone.
462        assert_eq!(IntBinop::ShiftLeft.eval(1, 127, 16), 1u128 << 127);
463        assert_eq!(IntBinop::ShiftLeft.eval(1, 128, 16), 0);
464    }
465
466    #[test]
467    fn test_shl_display() {
468        let mut ctx = Context::new();
469
470        qcode!(
471            ctx,
472            "
473            <block>
474                local i32 V0;
475                %v0 = load(V0:4, V0);
476                %v = i32 %v0 << i32 0x2;
477                goto <0x1001>;
478            "
479        );
480
481        let v = Instruction::from_id(&ctx, v);
482
483        match v.mnemonic() {
484            Mnemonic::Binop(Binary {
485                op: Binop::Int(op), ..
486            }) => assert_eq!(*op, IntBinop::ShiftLeft),
487            _ => panic!("expected i32 integer binop instruction, found {}", v),
488        }
489
490        assert_eq!(v.size(), 4);
491        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 << i32 0x2;");
492    }
493
494    #[test]
495    fn test_shr_display() {
496        let mut ctx = Context::new();
497
498        qcode!(
499            ctx,
500            "
501            <block>
502                local i32 V0;
503                %v0 = load(V0:4, V0);
504                %v = i32 %v0 >> i32 0x2;
505                goto <0x1001>;
506            "
507        );
508
509        let v = Instruction::from_id(&ctx, v);
510
511        match v.mnemonic() {
512            Mnemonic::Binop(Binary {
513                op: Binop::Int(op), ..
514            }) => assert_eq!(*op, IntBinop::ShiftRight),
515            _ => panic!("expected i32 integer binop instruction, found {}", v),
516        }
517
518        assert_eq!(v.size(), 4);
519        assert_eq!(v.as_statement().to_string(), "i32 %v = i32 %v0 >> i32 0x2;");
520    }
521
522    #[test]
523    fn test_eq_display() {
524        let mut ctx = Context::new();
525
526        qcode!(
527            ctx,
528            "
529            <block>
530                local i32 V0;
531                %v0 = load(V0:4, V0);
532                %v = i32 %v0 == i32 0x2;
533                goto <0x1001>;
534            "
535        );
536
537        let v = Instruction::from_id(&ctx, v);
538
539        match v.mnemonic() {
540            Mnemonic::Binop(Binary {
541                op: Binop::Int(op), ..
542            }) => assert_eq!(*op, IntBinop::Equal),
543            _ => panic!("expected i32 integer binop instruction, found {}", v),
544        }
545
546        assert_eq!(v.size(), 1);
547        assert_eq!(
548            v.as_statement().to_string(),
549            "bool %v = i32 %v0 == i32 0x2;"
550        );
551    }
552
553    #[test]
554    fn test_ne_display() {
555        let mut ctx = Context::new();
556
557        qcode!(
558            ctx,
559            "
560            <block>
561                local i32 V0;
562                %v0 = load(V0:4, V0);
563                %v = i32 %v0 != i32 0x2;
564                goto <0x1001>;
565            "
566        );
567
568        let v = Instruction::from_id(&ctx, v);
569
570        match v.mnemonic() {
571            Mnemonic::Binop(Binary {
572                op: Binop::Int(op), ..
573            }) => assert_eq!(*op, IntBinop::NotEqual),
574            _ => panic!("expected i32 integer binop instruction, found {}", v),
575        }
576
577        assert_eq!(v.size(), 1);
578        assert_eq!(
579            v.as_statement().to_string(),
580            "bool %v = i32 %v0 != i32 0x2;"
581        );
582    }
583
584    #[test]
585    fn test_lt_display() {
586        let mut ctx = Context::new();
587
588        qcode!(
589            ctx,
590            "
591            <block>
592                local i32 V0;
593                %v0 = load(V0:4, V0);
594                %v = i32 %v0 < i32 0x2;
595                goto <0x1001>;
596            "
597        );
598
599        let v = Instruction::from_id(&ctx, v);
600
601        match v.mnemonic() {
602            Mnemonic::Binop(Binary {
603                op: Binop::Int(op), ..
604            }) => assert_eq!(*op, IntBinop::Less),
605            _ => panic!("expected i32 integer binop instruction, found {}", v),
606        }
607
608        assert_eq!(v.size(), 1);
609        assert_eq!(v.as_statement().to_string(), "bool %v = i32 %v0 < i32 0x2;");
610    }
611
612    #[test]
613    fn test_le_display() {
614        let mut ctx = Context::new();
615
616        qcode!(
617            ctx,
618            "
619            <block>
620                local i32 V0;
621                %v0 = load(V0:4, V0);
622                %v = i32 %v0 <= i32 0x2;
623                goto <0x1001>;
624            "
625        );
626
627        let v = Instruction::from_id(&ctx, v);
628
629        match v.mnemonic() {
630            Mnemonic::Binop(Binary {
631                op: Binop::Int(op), ..
632            }) => assert_eq!(*op, IntBinop::LessEqual),
633            _ => panic!("expected i32 integer binop instruction, found {}", v),
634        }
635
636        assert_eq!(v.size(), 1);
637        assert_eq!(
638            v.as_statement().to_string(),
639            "bool %v = i32 %v0 <= i32 0x2;"
640        );
641    }
642
643    #[test]
644    fn test_gt_display() {
645        let mut ctx = Context::new();
646
647        qcode!(
648            ctx,
649            "
650            <block>
651                local i32 V0;
652                %v0 = load(V0:4, V0);
653                %v = i32 %v0 > i32 0x2;
654                goto <0x1001>;
655            "
656        );
657
658        let v = Instruction::from_id(&ctx, v);
659
660        match v.mnemonic() {
661            Mnemonic::Binop(Binary {
662                op: Binop::Int(op), ..
663            }) => assert_eq!(*op, IntBinop::Less),
664            _ => panic!("expected i32 integer binop instruction, found {}", v),
665        }
666
667        assert_eq!(v.size(), 1);
668        assert_eq!(v.as_statement().to_string(), "bool %v = i32 0x2 < i32 %v0;");
669    }
670
671    #[test]
672    fn test_ge_display() {
673        let mut ctx = Context::new();
674
675        qcode!(
676            ctx,
677            "
678            <block>
679                local i32 V0;
680                %v0 = load(V0:4, V0);
681                %v = i32 %v0 >= i32 0x2;
682                goto <0x1001>;
683            "
684        );
685
686        let v = Instruction::from_id(&ctx, v);
687
688        match v.mnemonic() {
689            Mnemonic::Binop(Binary {
690                op: Binop::Int(op), ..
691            }) => assert_eq!(*op, IntBinop::LessEqual),
692            _ => panic!("expected i32 integer binop instruction, found {}", v),
693        }
694
695        assert_eq!(v.size(), 1);
696        assert_eq!(
697            v.as_statement().to_string(),
698            "bool %v = i32 0x2 <= i32 %v0;"
699        );
700    }
701
702    #[test]
703    #[should_panic(expected = "qcode size mismatch")]
704    fn test_explicit_capture_size_mismatch_panics() {
705        let mut ctx = Context::new();
706
707        qcode!(
708            ctx,
709            "
710            <block>
711                local i32 V0;
712                %v0 = load(V0:4, V0);
713                %v = i16 %v0 + i16 0x2;
714                goto <0x1001>;
715            "
716        );
717    }
718}