Repository: qir-alliance/pyqir (language: python)

Issue:
Support functions with return values
This PR adds support for calling external functions that have a return value, and using that return value as the argument to other external functions or rotation gates. It adds a variable case to PyQIR values. Variables are untyped (type errors will be caught only by LLVM).


Discussion / hints:

Fix patch:
diff --git a/eng/psakefile.ps1 b/eng/psakefile.ps1
index 69ce69c7..2668eca0 100644
--- a/eng/psakefile.ps1
+++ b/eng/psakefile.ps1
@@ -481,6 +481,9 @@ task run-examples {
 
         $ef_first_line = & $python "external_functions.py" | Select-Object -First 1
         Assert ($ef_first_line -eq "; ModuleID = 'external_functions'") "external_functions.py"
+
+        $arithmetic_first_line = & $python "arithmetic.py" | Select-Object -First 1
+        Assert ($arithmetic_first_line -eq "; ModuleID = 'arithmetic'") "arithmetic.py"
     }
 
     exec -workingDirectory $pyqir.evaluator.examples_dir {
diff --git a/examples/generator/arithmetic.py b/examples/generator/arithmetic.py
new file mode 100644
index 00000000..42222a1f
--- /dev/null
+++ b/examples/generator/arithmetic.py
@@ -0,0 +1,33 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+from pyqir.generator import BasicQisBuilder, SimpleModule, const, types
+
+mod = SimpleModule("arithmetic", num_qubits=0, num_results=0)
+qis = BasicQisBuilder(mod.builder)
+
+# Declare functions that can produce and consume integers at runtime. See
+# external_functions.py.
+i32 = types.Int(32)
+get_int = mod.add_external_function("get_int", types.Function([], i32))
+take_int = mod.add_external_function(
+    "take_int", types.Function([i32], types.VOID))
+
+# Add 3 to a number and multiply the result by 2.
+a = mod.builder.call(get_int, [])
+# Python numbers need to be converted into QIR constant values. Since it's being
+# added to a 32-bit integer returned by get_int, its type needs to be the same.
+three = const(i32, 3)
+b = mod.builder.add(three, a)
+c = mod.builder.mul(const(i32, 2), b)
+
+# Negation can be done by subtracting an integer from zero.
+x = mod.builder.call(get_int, [])
+negative_x = mod.builder.sub(const(i32, 0), x)
+
+# Consume the results.
+mod.builder.call(take_int, [c])
+mod.builder.call(take_int, [negative_x])
+
+if __name__ == "__main__":
+    print(mod.ir())
diff --git a/pyqir-evaluator/src/lib.rs b/pyqir-evaluator/src/lib.rs
index 24b68fd7..b27d21f2 100644
--- a/pyqir-evaluator/src/lib.rs
+++ b/pyqir-evaluator/src/lib.rs
@@ -2,13 +2,6 @@
 // Licensed under the MIT License.
 
 #![warn(clippy::all, clippy::pedantic)]
-// pyo3 generates errors with _obj and _tmp values
-#![allow(clippy::used_underscore_binding)]
-// NB: Some arguments get turned into Deref by PyO3, which is meaningless
-//     for Option of a type that's already Deref. We ignore the warning
-//     here since it's introduced by an upstream macro and not something
-//     we can directly control in our code.
-#![allow(clippy::needless_option_as_deref)]
 
 #[cfg(feature = "python-bindings")]
 pub mod python;
diff --git a/pyqir-evaluator/src/python.rs b/pyqir-evaluator/src/python.rs
index ae0f3fa7..9cdc352c 100644
--- a/pyqir-evaluator/src/python.rs
+++ b/pyqir-evaluator/src/python.rs
@@ -1,6 +1,11 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+// pyo3 generates errors with _obj and _tmp values
+#![allow(clippy::used_underscore_binding)]
+// Some arguments get turned into Deref by PyO3 macros, which we can't control.
+#![allow(clippy::borrow_deref_ref, clippy::needless_option_as_deref)]
+
 use pyo3::{
     exceptions::PyOSError,
     prelude::*,
diff --git a/pyqir-generator/pyqir/generator/__init__.py b/pyqir-generator/pyqir/generator/__init__.py
index cbd9f395..e5ee02b0 100644
--- a/pyqir-generator/pyqir/generator/__init__.py
+++ b/pyqir-generator/pyqir/generator/__init__.py
@@ -23,4 +23,5 @@
     ir_to_bitcode as ir_to_bitcode,
 )
 
+from pyqir.generator._builder import IntPredicate as IntPredicate
 from pyqir.generator.types import Type as Type
diff --git a/pyqir-generator/pyqir/generator/_builder.py b/pyqir-generator/pyqir/generator/_builder.py
new file mode 100644
index 00000000..bdac4ff1
--- /dev/null
+++ b/pyqir-generator/pyqir/generator/_builder.py
@@ -0,0 +1,38 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+from enum import Enum, auto
+
+
+class IntPredicate(Enum):
+    """An integer comparison predicate."""
+
+    EQ = auto()
+    """Equal."""
+
+    NE = auto()
+    """Not equal."""
+
+    UGT = auto()
+    """Unsigned greater than."""
+
+    UGE = auto()
+    """Unsigned greater or equal."""
+
+    ULT = auto()
+    """Unsigned less than."""
+
+    ULE = auto()
+    """Unsigned less or equal."""
+
+    SGT = auto()
+    """Signed greater than."""
+
+    SGE = auto()
+    """Signed greater or equal."""
+
+    SLT = auto()
+    """Signed less than."""
+
+    SLE = auto()
+    """Signed less or equal."""
diff --git a/pyqir-generator/pyqir/generator/_native.pyi b/pyqir-generator/pyqir/generator/_native.pyi
index 5bce5658..0f146de6 100644
--- a/pyqir-generator/pyqir/generator/_native.pyi
+++ b/pyqir-generator/pyqir/generator/_native.pyi
@@ -1,6 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
+from pyqir.generator._builder import IntPredicate
 from pyqir.generator.types import Type
 from typing import Callable, Optional, Sequence, Tuple, Union
 
@@ -58,13 +59,104 @@ def const(ty: Type, value: Union[int, float]) -> Value:
 class Builder:
     """An instruction builder."""
 
+    def and_(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a bitwise logical and instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The result.
+        """
+        ...
+
+    def or_(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a bitwise logical or instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The result.
+        """
+        ...
+
+    def xor(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a bitwise logical exclusive or instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The result.
+        """
+        ...
+
+    def add(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts an addition instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The sum.
+        """
+        ...
+
+    def sub(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a subtraction instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The difference.
+        """
+        ...
+
+    def mul(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a multiplication instruction.
+
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :returns: The product.
+        """
+        ...
+
+    def shl(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a shift left instruction.
+
+        :param lhs: The value to shift.
+        :param rhs: The number of bits to shift by.
+        :returns: The result.
+        """
+        ...
+
+    def lshr(self, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts a logical (zero fill) shift right instruction.
+
+        :param lhs: The value to shift.
+        :param rhs: The number of bits to shift by.
+        :returns: The result.
+        """
+        ...
+
+    def icmp(self, pred: IntPredicate, lhs: Value, rhs: Value) -> Value:
+        """
+        Inserts an integer comparison instruction.
+
+        :param pred: The predicate to compare by.
+        :param lhs: The left-hand side.
+        :param rhs: The right-hand side.
+        :return: The boolean result.
+        """
+        ...
+
     def call(
         self,
         function: Function,
         args: Sequence[Union[Value, ResultRef, bool, int, float]]
     ) -> Optional[Value]:
         """
-        Builds a call instruction.
+        Inserts a call instruction.
 
         :param function: The function to call.
         :param args: The arguments to the function.
@@ -172,7 +264,7 @@ class BasicQisBuilder:
 
     def cx(self, control: Value, target: Value) -> None:
         """
-        Builds a controlled Pauli :math:`X` gate.
+        Inserts a controlled Pauli :math:`X` gate.
 
         :param control: The control qubit.
         :param target: The target qubit.
@@ -181,7 +273,7 @@ class BasicQisBuilder:
 
     def cz(self, control: Value, target: Value) -> None:
         """
-        Builds a controlled Pauli :math:`Z` gate.
+        Inserts a controlled Pauli :math:`Z` gate.
 
         :param control: The control qubit.
         :param target: The target qubit.
@@ -190,7 +282,7 @@ class BasicQisBuilder:
 
     def h(self, qubit: Value) -> None:
         """
-        Builds a Hadamard gate.
+        Inserts a Hadamard gate.
 
         :param qubit: The target qubit.
         """
@@ -198,7 +290,7 @@ class BasicQisBuilder:
 
     def m(self, qubit: Value, result: ResultRef) -> None:
         """
-        Builds a measurement operation.
+        Inserts a measurement operation.
 
         :param qubit: The qubit to measure.
         :param result: A result reference where the measurement result will be
@@ -208,7 +300,7 @@ class BasicQisBuilder:
 
     def reset(self, qubit: Value) -> None:
         """
-        Builds a reset operation.
+        Inserts a reset operation.
 
         :param qubit: The qubit to reset.
         """
@@ -216,7 +308,7 @@ class BasicQisBuilder:
 
     def rx(self, theta: Union[Value, float], qubit: Value) -> None:
         """
-        Builds a rotation gate about the :math:`x` axis.
+        Inserts a rotation gate about the :math:`x` axis.
 
         :param theta: The angle to rotate by.
         :param qubit: The qubit to rotate.
@@ -225,7 +317,7 @@ class BasicQisBuilder:
 
     def ry(self, theta: Union[Value, float], qubit: Value) -> None:
         """
-        Builds a rotation gate about the :math:`y` axis.
+        Inserts a rotation gate about the :math:`y` axis.
 
         :param theta: The angle to rotate by.
         :param qubit: The qubit to rotate.
@@ -234,7 +326,7 @@ class BasicQisBuilder:
 
     def rz(self, theta: Union[Value, float], qubit: Value) -> None:
         """
-        Builds a rotation gate about the :math:`z` axis.
+        Inserts a rotation gate about the :math:`z` axis.
 
         :param theta: The angle to rotate by.
         :param qubit: The qubit to rotate.
@@ -243,7 +335,7 @@ class BasicQisBuilder:
 
     def s(self, qubit: Value) -> None:
         """
-        Builds an :math:`S` gate.
+        Inserts an :math:`S` gate.
 
         :param qubit: The target qubit.
         """
@@ -251,7 +343,7 @@ class BasicQisBuilder:
 
     def s_adj(self, qubit: Value) -> None:
         """
-        Builds an adjoint :math:`S` gate.
+        Inserts an adjoint :math:`S` gate.
 
         :param qubit: The target qubit.
         """
@@ -259,7 +351,7 @@ class BasicQisBuilder:
 
     def t(self, qubit: Value) -> None:
         """
-        Builds a :math:`T` gate.
+        Inserts a :math:`T` gate.
 
         :param qubit: The target qubit.
         """
@@ -267,7 +359,7 @@ class BasicQisBuilder:
 
     def t_adj(self, qubit: Value) -> None:
         """
-        Builds an adjoint :math:`T` gate.
+        Inserts an adjoint :math:`T` gate.
 
         :param qubit: The target qubit.
         """
@@ -275,7 +367,7 @@ class BasicQisBuilder:
 
     def x(self, qubit: Value) -> None:
         """
-        Builds a Pauli :math:`X` gate.
+        Inserts a Pauli :math:`X` gate.
 
         :param qubit: The target qubit.
         """
@@ -283,7 +375,7 @@ class BasicQisBuilder:
 
     def y(self, qubit: Value) -> None:
         """
-        Builds a Pauli :math:`Y` gate.
+        Inserts a Pauli :math:`Y` gate.
 
         :param qubit: The target qubit.
         """
@@ -291,7 +383,7 @@ class BasicQisBuilder:
 
     def z(self, qubit: Value) -> None:
         """
-        Builds a Pauli :math:`Z` gate.
+        Inserts a Pauli :math:`Z` gate.
 
         :param qubit: The target qubit.
         """
@@ -304,7 +396,7 @@ class BasicQisBuilder:
         zero: Callable[[], None] = ...,
     ) -> None:
         """
-        Builds a conditional branch on the result of a measurement.
+        Inserts a branch conditioned on the result of a measurement.
 
         Dereferences the result reference, then evaluates the instructions
         built by ``one`` if the result is one, or the instructions built by
@@ -312,9 +404,9 @@ class BasicQisBuilder:
         use this builder to build instructions.
 
         :param result: The result to branch on.
-        :param one: A callable that builds instructions for the branch where
+        :param one: A callable that inserts instructions for the branch where
                     the result is one.
-        :param zero: A callable that builds instructions for the branch where
+        :param zero: A callable that inserts instructions for the branch where
                      the result is zero.
         """
         ...
diff --git a/pyqir-generator/src/lib.rs b/pyqir-generator/src/lib.rs
index a10a97bc..b27d21f2 100644
--- a/pyqir-generator/src/lib.rs
+++ b/pyqir-generator/src/lib.rs
@@ -2,16 +2,6 @@
 // Licensed under the MIT License.
 
 #![warn(clippy::all, clippy::pedantic)]
-// pyo3 generates errors with _obj and _tmp values
-#![allow(clippy::used_underscore_binding)]
-// NB: Some arguments get turned into Deref by PyO3, which is meaningless
-//     for Option of a type that's already Deref. We ignore the warning
-//     here since it's introduced by an upstream macro and not something
-//     we can directly control in our code.
-#![allow(clippy::needless_option_as_deref)]
-// This was introduced in 1.62, but we can't update the dependency to
-// to resolve it until we move to a newer version of python.
-#![allow(clippy::format_push_string)]
 
 #[cfg(feature = "python-bindings")]
 pub mod python;
diff --git a/pyqir-generator/src/python.rs b/pyqir-generator/src/python.rs
index 672ad469..7d48e6bf 100644
--- a/pyqir-generator/src/python.rs
+++ b/pyqir-generator/src/python.rs
@@ -1,6 +1,14 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
+// pyo3 generates errors with _obj and _tmp values
+#![allow(clippy::used_underscore_binding)]
+// Some arguments get turned into Deref by PyO3 macros, which we can't control.
+#![allow(clippy::borrow_deref_ref, clippy::needless_option_as_deref)]
+// This was introduced in 1.62, but we can't update the dependency to
+// to resolve it until we move to a newer version of python.
+#![allow(clippy::format_push_string)]
+
 use pyo3::{
     basic::CompareOp,
     exceptions::{PyOSError, PyOverflowError, PyTypeError, PyValueError},
@@ -11,8 +19,8 @@ use pyo3::{
 use qirlib::generation::{
     emit,
     interop::{
-        self, Call, ClassicalRegister, Controlled, If, Instruction, Int, Measured, QuantumRegister,
-        Rotated, SemanticModel, Single, Type, Variable,
+        self, BinaryKind, BinaryOp, Call, ClassicalRegister, Controlled, If, Instruction, Int,
+        IntPredicate, Measured, QuantumRegister, Rotated, SemanticModel, Single, Type, Variable,
     },
 };
 use std::{
@@ -141,6 +149,27 @@ impl From<PyType> for Type {
     }
 }
 
+struct PyIntPredicate(IntPredicate);
+
+impl<'source> FromPyObject<'source> for PyIntPredicate {
+    fn extract(ob: &'source PyAny) -> PyResult<Self> {
+        match ob.getattr("name")?.extract()? {
+            "EQ" => Ok(IntPredicate::EQ),
+            "NE" => Ok(IntPredicate::NE),
+            "UGT" => Ok(IntPredicate::UGT),
+            "UGE" => Ok(IntPredicate::UGE),
+            "ULT" => Ok(IntPredicate::ULT),
+            "ULE" => Ok(IntPredicate::ULE),
+            "SGT" => Ok(IntPredicate::SGT),
+            "SGE" => Ok(IntPredicate::SGE),
+            "SLT" => Ok(IntPredicate::SLT),
+            "SLE" => Ok(IntPredicate::SLE),
+            _ => Err(PyValueError::new_err("Invalid predicate.")),
+        }
+        .map(Self)
+    }
+}
+
 #[derive(Clone, Eq, Hash, PartialEq)]
 #[pyclass]
 struct ResultRef {
@@ -227,10 +256,49 @@ impl Builder {
         Builder {
             frames: vec![vec![]],
             external_functions: vec![],
-            next_variable: Variable::default(),
+            next_variable: Variable::new(),
         }
     }
 
+    #[pyo3(name = "and_")]
+    fn and(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::And, lhs.0, rhs.0)
+    }
+
+    #[pyo3(name = "or_")]
+    fn or(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Or, lhs.0, rhs.0)
+    }
+
+    fn xor(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Xor, lhs.0, rhs.0)
+    }
+
+    fn add(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Add, lhs.0, rhs.0)
+    }
+
+    fn sub(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Sub, lhs.0, rhs.0)
+    }
+
+    fn mul(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Mul, lhs.0, rhs.0)
+    }
+
+    fn shl(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::Shl, lhs.0, rhs.0)
+    }
+
+    fn lshr(&mut self, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::LShr, lhs.0, rhs.0)
+    }
+
+    #[allow(clippy::needless_pass_by_value)]
+    fn icmp(&mut self, pred: PyIntPredicate, lhs: Value, rhs: Value) -> Value {
+        self.push_binary_op(BinaryKind::ICmp(pred.0), lhs.0, rhs.0)
+    }
+
     fn call(&mut self, function: Function, args: &PySequence) -> PyResult<Option<Value>> {
         let (param_types, return_type) = match function.ty {
             Type::Function { params, result } => (params, result),
@@ -283,6 +351,22 @@ impl Builder {
         self.next_variable = v.next();
         v
     }
+
+    fn push_binary_op(
+        &mut self,
+        kind: BinaryKind,
+        lhs: interop::Value,
+        rhs: interop::Value,
+    ) -> Value {
+        let result = self.fresh_variable();
+        self.push_inst(Instruction::BinaryOp(BinaryOp {
+            kind,
+            lhs,
+            rhs,
+            result,
+        }));
+        Value(interop::Value::Variable(result))
+    }
 }
 
 #[pyclass]
diff --git a/pyqir-parser/src/lib.rs b/pyqir-parser/src/lib.rs
index 1f264c5b..0ebae0d9 100644
--- a/pyqir-parser/src/lib.rs
+++ b/pyqir-parser/src/lib.rs
@@ -1,9 +1,7 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
 
-#![deny(clippy::all, clippy::pedantic)]
-// pyo3 generates errors with _obj and _tmp values
-#![allow(clippy::used_underscore_binding)]
+#![warn(clippy::all, clippy::pedantic)]
 
 pub mod parse;
 
diff --git a/pyqir-parser/src/python.rs b/pyqir-parser/src/python.rs
index aa812bee..b626d7c1 100644
--- a/pyqir-parser/src/python.rs
+++ b/pyqir-parser/src/python.rs
@@ -9,6 +9,11 @@
 // from within rust, and wrappers for each class and function will be added to __init__.py so that the
 // parser API can have full python doc comments for usability.
 
+// pyo3 generates errors with _obj and _tmp values
+#![allow(clippy::used_underscore_binding)]
+// Some arguments get turned into Deref by PyO3 macros, which we can't control.
+#![allow(clippy::borrow_deref_ref, clippy::needless_option_as_deref)]
+
 use crate::parse::verify_module_can_be_loaded;
 
 use super::parse::{
diff --git a/qirlib/src/generation/emit.rs b/qirlib/src/generation/emit.rs
index 891a2553..86e0c5b6 100644
--- a/qirlib/src/generation/emit.rs
+++ b/qirlib/src/generation/emit.rs
@@ -374,8 +374,8 @@ mod result_alloc_tests {
 mod if_tests {
     use super::test_utils::check_or_save_reference_ir;
     use crate::generation::interop::{
-        Call, ClassicalRegister, If, Instruction, Measured, QuantumRegister, SemanticModel, Single,
-        Type, Value, Variable,
+        BinaryKind, BinaryOp, Call, ClassicalRegister, If, Instruction, IntPredicate, Measured,
+        QuantumRegister, SemanticModel, Single, Type, Value, Variable,
     };
 
     #[test]
@@ -667,20 +667,23 @@ mod if_tests {
 
     #[test]
     fn test_call_variable() -> Result<(), String> {
+        let i64 = Type::Int { width: 64 };
+        let x = Variable::new();
+
         check_or_save_reference_ir(&SemanticModel {
             name: "test_call_variable".to_string(),
             registers: vec![],
             qubits: vec![],
             instructions: vec![
                 Instruction::Call(Call {
-                    result: Some(Variable::default()),
                     name: "foo".to_string(),
                     args: vec![],
+                    result: Some(x),
                 }),
                 Instruction::Call(Call {
-                    result: None,
                     name: "bar".to_string(),
-                    args: vec![Value::Variable(Variable::default())],
+                    args: vec![Value::Variable(x)],
+                    result: None,
                 }),
             ],
             use_static_qubit_alloc: true,
@@ -690,13 +693,105 @@ mod if_tests {
                     "foo".to_string(),
                     Type::Function {
                         params: vec![],
-                        result: Box::new(Type::Int { width: 64 }),
+                        result: Box::new(i64.clone()),
                     },
                 ),
                 (
                     "bar".to_string(),
                     Type::Function {
-                        params: vec![Type::Int { width: 64 }],
+                        params: vec![i64],
+                        result: Box::new(Type::Void),
+                    },
+                ),
+            ],
+        })
+    }
+
+    #[test]
+    fn test_int_binary_operators() -> Result<(), String> {
+        let mut instructions = vec![];
+        let lhs = Variable::new();
+        let rhs = lhs.next();
+
+        for result in [lhs, rhs] {
+            instructions.push(Instruction::Call(Call {
+                name: "source".to_string(),
+                args: vec![],
+                result: Some(result),
+            }));
+        }
+
+        let kinds = [
+            BinaryKind::And,
+            BinaryKind::Or,
+            BinaryKind::Xor,
+            BinaryKind::Add,
+            BinaryKind::Sub,
+            BinaryKind::Mul,
+            BinaryKind::Shl,
+            BinaryKind::LShr,
+            BinaryKind::ICmp(IntPredicate::EQ),
+            BinaryKind::ICmp(IntPredicate::NE),
+            BinaryKind::ICmp(IntPredicate::UGT),
+            BinaryKind::ICmp(IntPredicate::UGE),
+            BinaryKind::ICmp(IntPredicate::ULT),
+            BinaryKind::ICmp(IntPredicate::ULE),
+            BinaryKind::ICmp(IntPredicate::SGT),
+            BinaryKind::ICmp(IntPredicate::SGE),
+            BinaryKind::ICmp(IntPredicate::SLT),
+            BinaryKind::ICmp(IntPredicate::SLE),
+        ];
+
+        let mut result = rhs;
+        for kind in kinds {
+            let sink = if matches!(kind, BinaryKind::ICmp(_)) {
+                result = result.next();
+                "sink_i1".to_string()
+            } else {
+                result = result.next();
+                "sink_i32".to_string()
+            };
+
+            instructions.push(Instruction::BinaryOp(BinaryOp {
+                kind,
+                lhs: Value::Variable(lhs),
+                rhs: Value::Variable(rhs),
+                result,
+            }));
+
+            instructions.push(Instruction::Call(Call {
+                name: sink,
+                args: vec![Value::Variable(result)],
+                result: None,
+            }));
+        }
+
+        check_or_save_reference_ir(&SemanticModel {
+            name: "test_int_binary_operators".to_string(),
+            registers: vec![],
+            qubits: vec![],
+            instructions,
+            use_static_qubit_alloc: false,
+            use_static_result_alloc: false,
+            external_functions: vec![
+                (
+                    "source".to_string(),
+                    Type::Function {
+                        params: vec![],
+                        result: Box::new(Type::Int { width: 32 }),
+                    },
+                ),
+                (
+                    "sink_i1".to_string(),
+                    Type::Function {
+                        params: vec![Type::Int { width: 1 }],
+                        result: Box::new(Type::Void),
+                    },
+                ),
+                (
+                    "sink_i32".to_string(),
+                    Type::Function {
+                        params: vec![Type::Int { width: 32 }],
                         result: Box::new(Type::Void),
                     },
                 ),
diff --git a/qirlib/src/generation/interop.rs b/qirlib/src/generation/interop.rs
index bf742c26..6a77b811 100644
--- a/qirlib/src/generation/interop.rs
+++ b/qirlib/src/generation/interop.rs
@@ -119,10 +119,57 @@ pub enum Instruction {
     Rz(Rotated),
     Reset(Single),
     M(Measured),
+    BinaryOp(BinaryOp),
     Call(Call),
     If(If),
 }
 
+#[derive(Clone, Debug, PartialEq)]
+pub struct BinaryOp {
+    pub kind: BinaryKind,
+    pub lhs: Value,
+    pub rhs: Value,
+    pub result: Variable,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum BinaryKind {
+    And,
+    Or,
+    Xor,
+    Add,
+    Sub,
+    Mul,
+    Shl,
+    LShr,
+    ICmp(IntPredicate),
+}
+
+/// An integer comparison predicate.
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+pub enum IntPredicate {
+    /// Equal.
+    EQ,
+    /// Not equal.
+    NE,
+    /// Unsigned greater than.
+    UGT,
+    /// Unsigned greater than or equal.
+    UGE,
+    /// Unsigned less than.
+    ULT,
+    /// Unsigned less than or equal.
+    ULE,
+    /// Signed greater than.
+    SGT,
+    /// Signed greater than or equal.
+    SGE,
+    /// Signed less than.
+    SLT,
+    /// Signed less than or equal.
+    SLE,
+}
+
 #[derive(Clone, Debug, PartialEq)]
 pub struct Call {
     pub name: String,
@@ -130,7 +177,7 @@ pub struct Call {
     pub result: Option<Variable>,
 }
 
-#[derive(Clone)]
+#[derive(Clone, Debug, Eq, Hash, PartialEq)]
 pub enum Type {
     Void,
     Int {
@@ -160,13 +207,18 @@ pub struct Variable {
 }
 
 impl Variable {
+    #[must_use]
+    pub fn new() -> Self {
+        Self::default()
+    }
+
     #[must_use]
     pub fn next(&self) -> Self {
         Self { id: self.id + 1 }
     }
 }
 
-#[derive(Clone, Debug, PartialEq)]
+#[derive(Clone, Debug, Eq, PartialEq)]
 pub struct Int {
     width: u32,
     value: u64,
diff --git a/qirlib/src/generation/qir/instructions.rs b/qirlib/src/generation/qir/instructions.rs
index 89bc7139..fd14bed0 100644
--- a/qirlib/src/generation/qir/instructions.rs
+++ b/qirlib/src/generation/qir/instructions.rs
@@ -5,7 +5,7 @@ use crate::{
     codegen::CodeGenerator,
     generation::{
         env::{Environment, ResultState},
-        interop::{Call, If, Instruction, Value},
+        interop::{BinaryKind, BinaryOp, Call, If, Instruction, IntPredicate, Value},
         qir::result,
     },
 };
@@ -157,11 +157,52 @@ pub(crate) fn emit<'ctx>(
             let qubit = get_value(generator, env, &inst.qubit);
             generator.emit_void_call(generator.qis_z_body(), &[qubit]);
         }
+        Instruction::BinaryOp(op) => emit_binary_op(generator, env, op),
         Instruction::Call(call) => emit_call(generator, env, call),
         Instruction::If(if_) => emit_if(generator, env, entry_point, if_),
     }
 }
 
+fn emit_binary_op<'ctx>(
+    generator: &CodeGenerator<'ctx>,
+    env: &mut Environment<'ctx>,
+    op: &BinaryOp,
+) {
+    let lhs = get_value(generator, env, &op.lhs).into_int_value();
+    let rhs = get_value(generator, env, &op.rhs).into_int_value();
+    let result = match op.kind {
+        BinaryKind::And => generator.builder.build_and(lhs, rhs, ""),
+        BinaryKind::Or => generator.builder.build_or(lhs, rhs, ""),
+        BinaryKind::Xor => generator.builder.build_xor(lhs, rhs, ""),
+        BinaryKind::Add => generator.builder.build_int_add(lhs, rhs, ""),
+        BinaryKind::Sub => generator.builder.build_int_sub(lhs, rhs, ""),
+        BinaryKind::Mul => generator.builder.build_int_mul(lhs, rhs, ""),
+        BinaryKind::Shl => generator.builder.build_left_shift(lhs, rhs, ""),
+        BinaryKind::LShr => generator.builder.build_right_shift(lhs, rhs, false, ""),
+        BinaryKind::ICmp(pred) => {
+            generator
+                .builder
+                .build_int_compare(to_inkwell_predicate(pred), lhs, rhs, "")
+        }
+    };
+    env.set_variable(op.result, result.into()).unwrap();
+}
+
+fn to_inkwell_predicate(pred: IntPredicate) -> inkwell::IntPredicate {
+    match pred {
+        IntPredicate::EQ => inkwell::IntPredicate::EQ,
+        IntPredicate::NE => inkwell::IntPredicate::NE,
+        IntPredicate::UGT => inkwell::IntPredicate::UGT,
+        IntPredicate::UGE => inkwell::IntPredicate::UGE,
+        IntPredicate::ULT => inkwell::IntPredicate::ULT,
+        IntPredicate::ULE => inkwell::IntPredicate::ULE,
+        IntPredicate::SGT => inkwell::IntPredicate::SGT,
+        IntPredicate::SGE => inkwell::IntPredicate::SGE,
+        IntPredicate::SLT => inkwell::IntPredicate::SLT,
+        IntPredicate::SLE => inkwell::IntPredicate::SLE,
+    }
+}
+
 fn emit_call<'ctx>(generator: &CodeGenerator<'ctx>, env: &mut Environment<'ctx>, call: &Call) {
     let args: Vec<_> = call
         .args


Test patch:
diff --git a/pyqir-generator/tests/test_int_operators.py b/pyqir-generator/tests/test_int_operators.py
new file mode 100644
index 00000000..d4f4606f
--- /dev/null
+++ b/pyqir-generator/tests/test_int_operators.py
@@ -0,0 +1,77 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+from functools import partial
+from pyqir.generator import Builder, IntPredicate, SimpleModule, Value, const, types
+from typing import Callable, List, Tuple
+import unittest
+
+_OPERATORS: List[Tuple[str, Callable[[Builder], Callable[[Value, Value], Value]]]] = [
+    ("and", lambda b: b.and_),
+    ("or", lambda b: b.or_),
+    ("xor", lambda b: b.xor),
+    ("add", lambda b: b.add),
+    ("sub", lambda b: b.sub),
+    ("mul", lambda b: b.mul),
+    ("shl", lambda b: b.shl),
+    ("lshr", lambda b: b.lshr),
+    ("icmp eq", lambda b: partial(b.icmp, IntPredicate.EQ)),
+    ("icmp ne", lambda b: partial(b.icmp, IntPredicate.NE)),
+    ("icmp ugt", lambda b: partial(b.icmp, IntPredicate.UGT)),
+    ("icmp uge", lambda b: partial(b.icmp, IntPredicate.UGE)),
+    ("icmp ult", lambda b: partial(b.icmp, IntPredicate.ULT)),
+    ("icmp ule", lambda b: partial(b.icmp, IntPredicate.ULE)),
+    ("icmp sgt", lambda b: partial(b.icmp, IntPredicate.SGT)),
+    ("icmp sge", lambda b: partial(b.icmp, IntPredicate.SGE)),
+    ("icmp slt", lambda b: partial(b.icmp, IntPredicate.SLT)),
+    ("icmp sle", lambda b: partial(b.icmp, IntPredicate.SLE))
+]
+
+
+class IntOperatorsTest(unittest.TestCase):
+    def test_variable_variable(self) -> None:
+        for (name, build) in _OPERATORS:
+            with self.subTest(name):
+                mod = SimpleModule("test " + name, 0, 0)
+                source = mod.add_external_function(
+                    "source", types.Function([], types.Int(64)))
+                ty = types.BOOL if name.startswith("icmp") else types.Int(64)
+                sink = mod.add_external_function(
+                    "sink", types.Function([ty], types.VOID))
+
+                x = mod.builder.call(source, [])
+                y = mod.builder.call(source, [])
+                z = build(mod.builder)(x, y)
+                mod.builder.call(sink, [z])
+
+                self.assertIn(f"%2 = {name} i64 %0, %1", mod.ir())
+
+    def test_constant_variable(self) -> None:
+        for (name, build) in _OPERATORS:
+            with self.subTest(name):
+                mod = SimpleModule("test " + name, 0, 0)
+                source = mod.add_external_function(
+                    "source", types.Function([], types.Int(64)))
+                ty = types.BOOL if name.startswith("icmp") else types.Int(64)
+                sink = mod.add_external_function(
+                    "sink", types.Function([ty], types.VOID))
+
+                x = mod.builder.call(source, [])
+                y = build(mod.builder)(const(types.Int(64), 1), x)
+                mod.builder.call(sink, [y])
+
+                self.assertIn(f"%1 = {name} i64 1, %0", mod.ir())
+
+    def test_type_mismatch(self) -> None:
+        mod = SimpleModule("test_type_mismatch", 0, 0)
+        source = mod.add_external_function(
+            "source", types.Function([], types.Int(16)))
+        sink = mod.add_external_function(
+            "sink", types.Function([types.Int(16)], types.VOID))
+
+        x = mod.builder.call(source, [])
+        y = mod.builder.add(x, const(types.Int(18), 2))
+        mod.builder.call(sink, [y])
+
+        with self.assertRaises(OSError):
+            mod.ir()
diff --git a/qirlib/resources/tests/test_int_binary_operators.ll b/qirlib/resources/tests/test_int_binary_operators.ll
new file mode 100644
index 00000000..16db51fa
--- /dev/null
+++ b/qirlib/resources/tests/test_int_binary_operators.ll
@@ -0,0 +1,53 @@
+; ModuleID = 'test_int_binary_operators'
+source_filename = "test_int_binary_operators"
+
+declare i32 @source()
+
+declare void @sink_i1(i1)
+
+declare void @sink_i32(i32)
+
+define void @main() #0 {
+entry:
+  %0 = call i32 @source()
+  %1 = call i32 @source()
+  %2 = and i32 %0, %1
+  call void @sink_i32(i32 %2)
+  %3 = or i32 %0, %1
+  call void @sink_i32(i32 %3)
+  %4 = xor i32 %0, %1
+  call void @sink_i32(i32 %4)
+  %5 = add i32 %0, %1
+  call void @sink_i32(i32 %5)
+  %6 = sub i32 %0, %1
+  call void @sink_i32(i32 %6)
+  %7 = mul i32 %0, %1
+  call void @sink_i32(i32 %7)
+  %8 = shl i32 %0, %1
+  call void @sink_i32(i32 %8)
+  %9 = lshr i32 %0, %1
+  call void @sink_i32(i32 %9)
+  %10 = icmp eq i32 %0, %1
+  call void @sink_i1(i1 %10)
+  %11 = icmp ne i32 %0, %1
+  call void @sink_i1(i1 %11)
+  %12 = icmp ugt i32 %0, %1
+  call void @sink_i1(i1 %12)
+  %13 = icmp uge i32 %0, %1
+  call void @sink_i1(i1 %13)
+  %14 = icmp ult i32 %0, %1
+  call void @sink_i1(i1 %14)
+  %15 = icmp ule i32 %0, %1
+  call void @sink_i1(i1 %15)
+  %16 = icmp sgt i32 %0, %1
+  call void @sink_i1(i1 %16)
+  %17 = icmp sge i32 %0, %1
+  call void @sink_i1(i1 %17)
+  %18 = icmp slt i32 %0, %1
+  call void @sink_i1(i1 %18)
+  %19 = icmp sle i32 %0, %1
+  call void @sink_i1(i1 %19)
+  ret void
+}
+
+attributes #0 = { "EntryPoint" }