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

Issue:
Parser should allow construction from in-memory bitcode
Currently, the pyqir_parser only supports initializing the parser from a string that contains the path to an on-disk bitcode file. It would be helpful to support constructing a parser instance from an in-memory bitcode file, as that would open up more scenarios where code is already actively operating on bitcode, such as receiving it as a network stream.

Note that this may require updates to the underlying `llvm_ir` module that the parser depends on.
Unify parsing and code generation
PyQIR's parsing functionality and code generation functionality are currently totally independent; parsed QIR is read-only and can't be edited and re-emitted, and generated QIR is write-only. This precludes use cases like doing simple transforms or optimizations with PyQIR on QIR that came from another source. It's also more complicated to maintain since there is overlap between the two, and we're using a different Rust LLVM wrapper for each.

We want to combine Parser and Generator into one cohesive thing that can do both. The first step was refactoring Generator to use LLVM objects directly in #157. Next we will add parsing functionality to the LLVM-backed Generator types in #174. Finally, the pyqir-parser package will be retired. We will transition to a single pyqir package that has the same functionality as pyqir-parser, pyqir-generator, and pyqir-evaluator (with the first step in #170).

For users of pyqir-generator and pyqir-evaluator, the impact will be minimal. The main breaking change is to how types for external functions are created, which came with #157. pyqir-parser users will need to adapt to a completely rewritten API, although it will support the same functionality.

A sketch of the proposed API for the combined parsing and generating functionality is here: https://gist.github.com/samarsha/abdfd9fa7b6aee133904935c7b4511a5

The changes to external functions look like this. Before:

```python
from pyqir.generator import SimpleModule, types

m = SimpleModule("foo", 1, 1)
f = m.add_external_function(
    "f",
    types.Function([types.Int(8), types.QUBIT], types.VOID)
)
```

After:

```python
from pyqir.generator import SimpleModule

m = SimpleModule("foo", 1, 1)
types = m.types
f = m.add_external_function(
    "f",
    types.function(types.void, [types.int(8), types.qubit])
)
```


Discussion / hints:
Indeed, `llvm_ir` [only provides](https://docs.rs/llvm-ir/0.8.0/llvm_ir/module/struct.Module.html) a `from_bc_path(path)` function. ☹️

The [source code](https://github.com/cdisselkoen/llvm-ir/blob/main/src/module.rs) relies on `LLVMCreateMemoryBufferWithContentsOfFile()` then `LLVMParseBitcodeInContext2()` (both from the [`llvm-sys`](https://crates.io/crates/llvm-sys) crate) before calling `Self::from_llvm_ref()`.

In order not to update the `llvm_ir` code, could we somehow (Rust definitely isn't my universe...) inherit / compose / embed / extend / encapsulate the `Module` struct and add a `from_bc(buffer)` function, with calls to only `LLVMParseBitcodeInContext2()` and `Self::from_llvm_ref()`?


@LaurentAjdnik Given which traits and implementations are `pub` vs `pub(crate)` (Rust's rough equivalent to "internal"), this isn't something we can easily extend from the outside. The best bet would be contributing this as new functionality to `llvm_ir` directly. I don't think that code would be difficult to write, but we'd have to see if they are willing to take it and publish an update the crate.
> but we'd have to see if they are willing to take it and publish an update the crate.

In the most extreme case, may be able to specify a [`[patch]` section](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html#the-patch-section) in Cargo.toml. It sounds like we're in precisely one of the [intended usecases](https://doc.rust-lang.org/cargo/reference/overriding-dependencies.html):

> • You've submitted a fix to an upstream crate for a bug you found, but you'd like to immediately have your application start depending on the fixed version of the crate to avoid blocking on the bug fix getting merged.



Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 3df6b07f..fae461c5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -19,9 +19,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
 
 [[package]]
 name = "base64"
-version = "0.13.0"
+version = "0.13.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd"
+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
 
 [[package]]
 name = "bitflags"
@@ -43,9 +43,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.0.73"
+version = "1.0.74"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11"
+checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574"
 
 [[package]]
 name = "cfg-if"
@@ -55,9 +55,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
 
 [[package]]
 name = "cmake"
-version = "0.1.48"
+version = "0.1.49"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8ad8cef104ac57b68b89df3208164d228503abbdce70f6880ffa3d970e7443a"
+checksum = "db34956e100b30725f2eb215f90d4871051239535632f84fea3bc92722c66b7c"
 dependencies = [
  "cc",
 ]
@@ -97,9 +97,9 @@ checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c"
 
 [[package]]
 name = "getrandom"
-version = "0.2.7"
+version = "0.2.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
+checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31"
 dependencies = [
  "cfg-if",
  "libc",
@@ -115,7 +115,7 @@ checksum = "adab1eaa3408fb7f0c777a73e7465fd5656136fc93b670eb6df3c88c2c1344e3"
 [[package]]
 name = "inkwell"
 version = "0.1.0"
-source = "git+https://github.com/TheDan64/inkwell?branch=master#27b8ab5623696709c8948fabfc62ce47d5a32edd"
+source = "git+https://github.com/TheDan64/inkwell?branch=master#4a0025d3ca3d7bb489228f274f203139739241b7"
 dependencies = [
  "either",
  "inkwell_internals",
@@ -131,7 +131,7 @@ dependencies = [
 [[package]]
 name = "inkwell_internals"
 version = "0.5.0"
-source = "git+https://github.com/TheDan64/inkwell?branch=master#27b8ab5623696709c8948fabfc62ce47d5a32edd"
+source = "git+https://github.com/TheDan64/inkwell?branch=master#4a0025d3ca3d7bb489228f274f203139739241b7"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -155,9 +155,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
 [[package]]
 name = "libc"
-version = "0.2.135"
+version = "0.2.137"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c"
+checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89"
 
 [[package]]
 name = "llvm-ir"
@@ -275,9 +275,9 @@ checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be"
 
 [[package]]
 name = "once_cell"
-version = "1.15.0"
+version = "1.16.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1"
+checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860"
 
 [[package]]
 name = "parking_lot"
@@ -297,7 +297,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
 dependencies = [
  "lock_api",
- "parking_lot_core 0.9.3",
+ "parking_lot_core 0.9.4",
 ]
 
 [[package]]
@@ -316,9 +316,9 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.3"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929"
+checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0"
 dependencies = [
  "cfg-if",
  "libc",
@@ -440,7 +440,13 @@ dependencies = [
 name = "pyqir"
 version = "0.7.0-alpha.1"
 dependencies = [
+ "either",
  "inkwell",
+ "libc",
+ "llvm-sys 110.0.4",
+ "llvm-sys 120.2.5",
+ "llvm-sys 130.0.5",
+ "llvm-sys 140.0.3",
  "pyo3",
  "qirlib",
 ]
@@ -738,46 +744,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-sys"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2"
+checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7"
 dependencies = [
+ "windows_aarch64_gnullvm",
  "windows_aarch64_msvc",
  "windows_i686_gnu",
  "windows_i686_msvc",
  "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
  "windows_x86_64_msvc",
 ]
 
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e"
+
 [[package]]
 name = "windows_aarch64_msvc"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47"
+checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4"
 
 [[package]]
 name = "windows_i686_gnu"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6"
+checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7"
 
 [[package]]
 name = "windows_i686_msvc"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024"
+checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246"
 
 [[package]]
 name = "windows_x86_64_gnu"
-version = "0.36.1"
+version = "0.42.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1"
+checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028"
 
 [[package]]
 name = "windows_x86_64_msvc"
-version = "0.36.1"
+version = "0.42.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680"
+checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
 [[package]]
 name = "wyz"
diff --git a/docs/api-reference/evaluator.md b/docs/api-reference/evaluator.md
deleted file mode 100644
index d06b9d6f..00000000
--- a/docs/api-reference/evaluator.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# Evaluator
-
-```{eval-rst}
-.. autoclass:: pyqir.evaluator.NonadaptiveEvaluator
-   :members:
-
-.. autoclass:: pyqir.evaluator.GateSet
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.evaluator.GateLogger
-   :show-inheritance:
-   :members:
-   :undoc-members:
-```
diff --git a/docs/api-reference/generator.md b/docs/api-reference/generator.md
deleted file mode 100644
index d9b99c8b..00000000
--- a/docs/api-reference/generator.md
+++ /dev/null
@@ -1,45 +0,0 @@
-# Generator
-
-## Modules
-
-```{eval-rst}
-.. autoclass:: pyqir.generator.SimpleModule
-   :members:
-```
-
-## Builders
-
-```{eval-rst}
-.. autoclass:: pyqir.generator.Builder
-   :members:
-
-.. autoclass:: pyqir.generator.BasicQisBuilder
-   :members:
-
-.. autoenum:: pyqir.generator.IntPredicate
-```
-
-## Types
-
-```{eval-rst}
-.. autoclass:: pyqir.generator.Type
-
-.. autoclass:: pyqir.generator.TypeFactory
-   :members:
-```
-
-## Values
-
-```{eval-rst}
-.. autoclass:: pyqir.generator.Value
-   :members:
-
-.. autofunction:: pyqir.generator.const
-```
-
-## Parsing
-
-```{eval-rst}
-.. autofunction:: pyqir.generator.ir_to_bitcode
-.. autofunction:: pyqir.generator.bitcode_to_ir
-```
diff --git a/docs/api-reference/index.md b/docs/api-reference/index.md
deleted file mode 100644
index ed466f17..00000000
--- a/docs/api-reference/index.md
+++ /dev/null
@@ -1,11 +0,0 @@
-# API Reference
-
-```{toctree}
----
-maxdepth: 2
----
-
-parser
-generator
-evaluator
-```
diff --git a/docs/api-reference/parser.md b/docs/api-reference/parser.md
deleted file mode 100644
index bca0b3ec..00000000
--- a/docs/api-reference/parser.md
+++ /dev/null
@@ -1,262 +0,0 @@
-# Parser
-
-## Classes representing QIR types
-
-```{eval-rst}
-.. autoclass:: pyqir.parser.QirType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirVoidType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirIntegerType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirPointerType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirDoubleType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirArrayType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirStructType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirNamedStructType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirResultType
-   :show-inheritance:
-   :members:
-   :undoc-members:
-```
-
-## Classes representing QIR operands
-
-```{eval-rst}
-.. autoclass:: pyqir.parser.QirOperand
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirLocalOperand
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirIntConstant
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirDoubleConstant
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirNullConstant
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirQubitConstant
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirResultConstant
-   :show-inheritance:
-   :members:
-   :undoc-members:
-```
-
-## Classes representing basic block terminators
-
-```{eval-rst}
-.. autoclass:: pyqir.parser.QirTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirRetTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirBrTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirCondBrTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirSwitchTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirUnreachableTerminator
-   :show-inheritance:
-   :members:
-   :undoc-members:
-```
-
-## Classes representing QIR instructions
-
-```{eval-rst}
-.. autoclass:: pyqir.parser.QirInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirQisCallInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirRtCallInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirQirCallInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirCallInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirAddInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirSubInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirMulInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirUDivInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirSDivInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirURemInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirSRemInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirAndInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirOrInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirXorInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirShlInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirLShrInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirAShrInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFAddInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFSubInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFMulInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFDivInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFRemInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFNegInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirICmpInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirFCmpInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-
-.. autoclass:: pyqir.parser.QirPhiInstr
-   :show-inheritance:
-   :members:
-   :undoc-members:
-```
diff --git a/docs/api.md b/docs/api.md
new file mode 100644
index 00000000..c3c992d6
--- /dev/null
+++ b/docs/api.md
@@ -0,0 +1,19 @@
+# API Reference
+
+## PyQIR
+
+```{eval-rst}
+.. automodule:: pyqir
+   :members:
+   :undoc-members:
+   :show-inheritance:
+```
+
+## Evaluation
+
+```{eval-rst}
+.. automodule:: pyqir.evaluator
+   :members:
+   :undoc-members:
+   :show-inheritance:
+```
diff --git a/docs/conf.py b/docs/conf.py
index 23e7b89e..1c87c3de 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -40,9 +40,6 @@
     "Type": "Type",
 }
 
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ["_templates"]
-
 # List of patterns, relative to source directory, that match files and
 # directories to ignore when looking for source files.
 # This pattern also affects html_static_path and html_extra_path.
@@ -59,8 +56,3 @@
 # a list of builtin themes.
 #
 html_theme = "alabaster"
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ["_static"]
diff --git a/docs/index.md b/docs/index.md
index 85abe540..8c3086df 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,12 +1,4 @@
-# Welcome to PyQIR's Documentation
-
-## Introduction
-
-### Installing PyQIR
-
-## Contents
-
-% TODO
+# PyQIR
 
 ```{toctree}
 ---
@@ -14,11 +6,11 @@ maxdepth: 2
 ---
 
 getting-started/index
-api-reference/index
+api
 development-guide/index
 ```
 
-## Indices and tables
+## Indices
 
 - {ref}`genindex`
 - {ref}`modindex`
diff --git a/examples/arithmetic.py b/examples/arithmetic.py
index 47158199..3f5a0986 100644
--- a/examples/arithmetic.py
+++ b/examples/arithmetic.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule, const
+from pyqir import BasicQisBuilder, SimpleModule, const
 
 mod = SimpleModule("arithmetic", num_qubits=0, num_results=0)
 qis = BasicQisBuilder(mod.builder)
diff --git a/examples/bell_pair.py b/examples/bell_pair.py
index 1211d862..bc90674e 100644
--- a/examples/bell_pair.py
+++ b/examples/bell_pair.py
@@ -3,7 +3,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule
+from pyqir import BasicQisBuilder, SimpleModule
 
 # Create the module with two qubits and two results.
 bell = SimpleModule("bell", num_qubits=2, num_results=2)
diff --git a/examples/dynamic_allocation.py b/examples/dynamic_allocation.py
index cf0b1d56..1d4e738b 100644
--- a/examples/dynamic_allocation.py
+++ b/examples/dynamic_allocation.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule
+from pyqir import BasicQisBuilder, SimpleModule
 
 # PyQIR Generator assumes you want to use static allocation for qubits and
 # results, but you can still use dynamic allocation by manually calling the
diff --git a/examples/external_functions.py b/examples/external_functions.py
index 431ac305..67242042 100644
--- a/examples/external_functions.py
+++ b/examples/external_functions.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule, const
+from pyqir import BasicQisBuilder, SimpleModule, const
 
 mod = SimpleModule("external_functions", num_qubits=1, num_results=0)
 qis = BasicQisBuilder(mod.builder)
diff --git a/examples/if_bool.py b/examples/if_bool.py
index 392be86b..4e15b908 100644
--- a/examples/if_bool.py
+++ b/examples/if_bool.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, IntPredicate, SimpleModule, const
+from pyqir import BasicQisBuilder, IntPredicate, SimpleModule, const
 
 mod = SimpleModule("if_bool", num_qubits=2, num_results=2)
 qis = BasicQisBuilder(mod.builder)
diff --git a/examples/if_result.py b/examples/if_result.py
index 8f0ce172..13b30fda 100644
--- a/examples/if_result.py
+++ b/examples/if_result.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule
+from pyqir import BasicQisBuilder, SimpleModule
 
 mod = SimpleModule("if_result", num_qubits=2, num_results=2)
 qis = BasicQisBuilder(mod.builder)
diff --git a/examples/mock_to_qir.py b/examples/mock_to_qir.py
index 64840371..84649c51 100644
--- a/examples/mock_to_qir.py
+++ b/examples/mock_to_qir.py
@@ -9,7 +9,7 @@
 from mock_language.MockLanguageParser import MockLanguageParser
 from mock_language.MockLanguageListener import MockLanguageListener
 from pathlib import Path
-from pyqir.generator import BasicQisBuilder, SimpleModule, Value
+from pyqir import BasicQisBuilder, SimpleModule, Value
 
 
 class QirGenerator(MockLanguageListener):  # type: ignore[misc]
diff --git a/examples/python2qir.py b/examples/python2qir.py
index e1e55c9f..159107d3 100644
--- a/examples/python2qir.py
+++ b/examples/python2qir.py
@@ -6,8 +6,7 @@
 # Here, we transform a Qiskit circuit without using the Qiskit package
 
 import ast
-
-from pyqir.generator import BasicQisBuilder, SimpleModule
+from pyqir import BasicQisBuilder, SimpleModule
 
 
 def main() -> None:
diff --git a/examples/teleport.py b/examples/teleport.py
index a49776bc..7140af04 100644
--- a/examples/teleport.py
+++ b/examples/teleport.py
@@ -1,15 +1,13 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.evaluator import GateLogger, GateSet, NonadaptiveEvaluator
-from pyqir.generator import BasicQisBuilder, SimpleModule, Value
+from pyqir import BasicQisBuilder, SimpleModule, Value
+from pyqir.evaluator import GateLogger, NonadaptiveEvaluator
 import tempfile
-from typing import List, Tuple
+from typing import List
 
 
-def teleport(
-    qis: BasicQisBuilder, qubits: Tuple[Value, ...], results: Tuple[Value, ...]
-) -> None:
+def teleport(qis: BasicQisBuilder, qubits: List[Value], results: List[Value]) -> None:
     msg = qubits[0]
     target = qubits[1]
     register = qubits[2]
diff --git a/pyqir-parser/Cargo.toml b/pyqir-parser/Cargo.toml
index 001dbd57..1d722707 100644
--- a/pyqir-parser/Cargo.toml
+++ b/pyqir-parser/Cargo.toml
@@ -27,4 +27,4 @@ name = "pyqir_parser"
 crate-type = ["cdylib", "lib"]
 
 [package.metadata.maturin]
-name = "pyqir.parser._native"
+name = "pyqir_parser._native"
diff --git a/pyqir-parser/pyqir/__init__.py b/pyqir-parser/pyqir/__init__.py
deleted file mode 100644
index 896d3903..00000000
--- a/pyqir-parser/pyqir/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-# pkgutil-style namespace package is required by Maturin.
-# See: https://github.com/PyO3/maturin/issues/811
-__path__ = __import__("pkgutil").extend_path(__path__, __name__)
diff --git a/pyqir-parser/pyqir/parser/__init__.py b/pyqir-parser/pyqir_parser/__init__.py
similarity index 67%
rename from pyqir-parser/pyqir/parser/__init__.py
rename to pyqir-parser/pyqir_parser/__init__.py
index 338b29a9..91c9806b 100644
--- a/pyqir-parser/pyqir/parser/__init__.py
+++ b/pyqir-parser/pyqir_parser/__init__.py
@@ -1,4 +1,4 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.parser._parser import *
+from pyqir_parser._parser import *
diff --git a/pyqir-parser/pyqir/parser/_native.pyi b/pyqir-parser/pyqir_parser/_native.pyi
similarity index 100%
rename from pyqir-parser/pyqir/parser/_native.pyi
rename to pyqir-parser/pyqir_parser/_native.pyi
diff --git a/pyqir-parser/pyqir/parser/_parser.py b/pyqir-parser/pyqir_parser/_parser.py
similarity index 99%
rename from pyqir-parser/pyqir/parser/_parser.py
rename to pyqir-parser/pyqir_parser/_parser.py
index 37987e11..5192ace7 100644
--- a/pyqir-parser/pyqir/parser/_parser.py
+++ b/pyqir-parser/pyqir_parser/_parser.py
@@ -1,8 +1,8 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-import pyqir.parser._native as native
-from pyqir.parser._native import module_from_bitcode
+import pyqir_parser._native as native
+from pyqir_parser._native import module_from_bitcode
 from typing import List, Optional, Tuple
 
 __all__ = [
diff --git a/pyqir-parser/pyqir/py.typed b/pyqir-parser/pyqir_parser/py.typed
similarity index 100%
rename from pyqir-parser/pyqir/py.typed
rename to pyqir-parser/pyqir_parser/py.typed
diff --git a/pyqir/Cargo.toml b/pyqir/Cargo.toml
index 39ff8b6b..d194e809 100644
--- a/pyqir/Cargo.toml
+++ b/pyqir/Cargo.toml
@@ -4,21 +4,27 @@ name = "pyqir"
 version = "0.7.0-alpha.1"
 edition = "2018"
 license = "MIT"
-description = "PyQIR generates and evaluates the Quantum Intermediate Representation."
+description = "PyQIR parses, generates and evaluates the Quantum Intermediate Representation."
 readme = "README.md"
 homepage = "https://github.com/qir-alliance/pyqir"
 repository = "https://github.com/qir-alliance/pyqir"
 
 [dependencies]
+either = "1.8.0"
 inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["target-x86"] }
+libc = "0.2.137"
+llvm-sys-110 = { package = "llvm-sys", version = "110.0.3", optional = true }
+llvm-sys-120 = { package = "llvm-sys", version = "120.2.2", optional = true }
+llvm-sys-130 = { package = "llvm-sys", version = "130.0.2", optional = true }
+llvm-sys-140 = { package = "llvm-sys", version = "140.0.2", optional = true }
 pyo3 = { version = "0.17.2", features = ["abi3-py37", "extension-module"] }
 qirlib = { path = "../qirlib" }
 
 [features]
-llvm11-0 = ["qirlib/llvm11-0"]
-llvm12-0 = ["qirlib/llvm12-0"]
-llvm13-0 = ["qirlib/llvm13-0"]
-llvm14-0 = ["qirlib/llvm14-0"]
+llvm11-0 = ["llvm-sys-110", "qirlib/llvm11-0"]
+llvm12-0 = ["llvm-sys-120", "qirlib/llvm12-0"]
+llvm13-0 = ["llvm-sys-130", "qirlib/llvm13-0"]
+llvm14-0 = ["llvm-sys-140", "qirlib/llvm14-0"]
 
 [lib]
 crate-type = ["cdylib"]
diff --git a/pyqir/pyqir/__init__.py b/pyqir/pyqir/__init__.py
index 896d3903..912f7c3d 100644
--- a/pyqir/pyqir/__init__.py
+++ b/pyqir/pyqir/__init__.py
@@ -1,6 +1,82 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-# pkgutil-style namespace package is required by Maturin.
-# See: https://github.com/PyO3/maturin/issues/811
-__path__ = __import__("pkgutil").extend_path(__path__, __name__)
+from pyqir._native import (
+    ArrayType,
+    Attribute,
+    BasicBlock,
+    BasicQisBuilder,
+    Builder,
+    Call,
+    Constant,
+    FCmp,
+    FloatConstant,
+    FloatPredicate,
+    Function,
+    FunctionType,
+    ICmp,
+    Instruction,
+    IntConstant,
+    IntPredicate,
+    IntType,
+    Module,
+    Opcode,
+    Phi,
+    PointerType,
+    SimpleModule,
+    StructType,
+    Switch,
+    Type,
+    TypeFactory,
+    Value,
+    const,
+    constant_bytes,
+    is_entry_point,
+    is_interop_friendly,
+    is_qubit,
+    is_result,
+    qubit_id,
+    required_num_qubits,
+    required_num_results,
+    result_id,
+)
+
+__all__ = [
+    "ArrayType",
+    "Attribute",
+    "BasicBlock",
+    "BasicQisBuilder",
+    "Builder",
+    "Call",
+    "Constant",
+    "FCmp",
+    "FloatConstant",
+    "FloatPredicate",
+    "Function",
+    "FunctionType",
+    "ICmp",
+    "Instruction",
+    "IntConstant",
+    "IntPredicate",
+    "IntType",
+    "Module",
+    "Opcode",
+    "Phi",
+    "PointerType",
+    "SimpleModule",
+    "StructType",
+    "Switch",
+    "Type",
+    "TypeFactory",
+    "Value",
+    "const",
+    "constant_bytes",
+    "is_entry_point",
+    "is_interop_friendly",
+    "is_qubit",
+    "is_result",
+    "qubit_id",
+    "required_num_qubits",
+    "required_num_results",
+    "result_id",
+]
diff --git a/pyqir/pyqir/_native.pyi b/pyqir/pyqir/_native.pyi
index 123cc071..1b0fa4a4 100644
--- a/pyqir/pyqir/_native.pyi
+++ b/pyqir/pyqir/_native.pyi
@@ -1,12 +1,10 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
+from enum import Enum
 from pyqir.evaluator import GateSet
-from pyqir.generator import IntPredicate
 from typing import Callable, List, Optional, Sequence, Tuple, Union
 
-class Type: ...
-
 class TypeFactory:
     @property
     def void(self) -> Type: ...
@@ -20,11 +18,7 @@ class TypeFactory:
     @property
     def result(self) -> Type: ...
     @staticmethod
-    def function(return_: Type, params: List[Type]) -> Type: ...
-
-class Value: ...
-
-def const(ty: Type, value: Union[int, float]) -> Value: ...
+    def function(ret: Type, params: Sequence[Type]) -> Type: ...
 
 class Builder:
     def and_(self, lhs: Value, rhs: Value) -> Value: ...
@@ -38,7 +32,7 @@ class Builder:
     def icmp(self, pred: IntPredicate, lhs: Value, rhs: Value) -> Value: ...
     def call(
         self,
-        function: Value,
+        callee: Value,
         args: Sequence[Union[Value, bool, int, float]],
     ) -> Optional[Value]: ...
     def if_(
@@ -77,23 +71,247 @@ class SimpleModule:
     @property
     def types(self) -> TypeFactory: ...
     @property
-    def qubits(self) -> Tuple[Value, ...]: ...
+    def qubits(self) -> List[Value]: ...
     @property
-    def results(self) -> Tuple[Value, ...]: ...
+    def results(self) -> List[Value]: ...
     @property
     def builder(self) -> Builder: ...
     def ir(self) -> str: ...
     def bitcode(self) -> bytes: ...
-    def add_external_function(self, name: str, ty: Type) -> Value: ...
-
-def ir_to_bitcode(
-    ir: str, module_name: Optional[str] = ..., source_file_name: Optional[str] = ...
-) -> bytes: ...
-def bitcode_to_ir(
-    bitcode: bytes,
-    module_name: Optional[str] = ...,
-    source_file_name: Optional[str] = ...,
-) -> str: ...
+    def add_external_function(self, name: str, ty: Type) -> Function: ...
+
+class Type:
+    @property
+    def is_void(self) -> bool: ...
+    @property
+    def is_double(self) -> bool: ...
+
+class IntType(Type):
+    @property
+    def width(self) -> int: ...
+
+class FunctionType(Type):
+    @property
+    def ret(self) -> Type: ...
+    @property
+    def params(self) -> List[Type]: ...
+
+class StructType(Type):
+    @property
+    def name(self) -> Optional[str]: ...
+    @property
+    def fields(self) -> List[Type]: ...
+
+class ArrayType(Type):
+    @property
+    def element(self) -> Type: ...
+    @property
+    def count(self) -> int: ...
+
+class PointerType(Type):
+    @property
+    def pointee(self) -> Type: ...
+    @property
+    def address_space(self) -> int: ...
+
+def is_qubit(ty: Type) -> bool: ...
+def is_result(ty: Type) -> bool: ...
+
+class Value:
+    @property
+    def type(self) -> Type: ...
+    @property
+    def name(self) -> Optional[str]: ...
+
+class BasicBlock(Value):
+    @property
+    def instructions(self) -> List[Instruction]: ...
+    @property
+    def terminator(self) -> Optional[Instruction]: ...
+
+class Constant(Value):
+    @property
+    def is_null(self) -> bool: ...
+
+class IntConstant(Constant):
+    @property
+    def type(self) -> IntType: ...
+    @property
+    def value(self) -> int: ...
+
+class FloatConstant(Constant):
+    @property
+    def value(self) -> float: ...
+
+class Function(Constant):
+    @property
+    def type(self) -> FunctionType: ...
+    @property
+    def params(self) -> List[Value]: ...
+    @property
+    def basic_blocks(self) -> List[BasicBlock]: ...
+    def attribute(self, name: str) -> Optional[Attribute]: ...
+
+class Attribute:
+    @property
+    def value(self) -> str: ...
+
+def const(ty: Type, value: Union[int, float]) -> Value: ...
+def qubit_id(value: Value) -> Optional[int]: ...
+def result_id(value: Value) -> Optional[int]: ...
+def is_entry_point(function: Function) -> bool: ...
+def is_interop_friendly(function: Function) -> bool: ...
+def required_num_qubits(function: Function) -> Optional[int]: ...
+def required_num_results(function: Function) -> Optional[int]: ...
+def constant_bytes(value: Value) -> Optional[bytes]: ...
+
+class Opcode(Enum):
+    ADD: Opcode
+    ADDR_SPACE_CAST: Opcode
+    ALLOCA: Opcode
+    AND: Opcode
+    ASHR: Opcode
+    ATOMIC_CMP_XCHG: Opcode
+    ATOMIC_RMW: Opcode
+    BIT_CAST: Opcode
+    BR: Opcode
+    CALL_BR: Opcode
+    CALL: Opcode
+    CATCH_PAD: Opcode
+    CATCH_RET: Opcode
+    CATCH_SWITCH: Opcode
+    CLEANUP_PAD: Opcode
+    CLEANUP_RET: Opcode
+    EXTRACT_ELEMENT: Opcode
+    EXTRACT_VALUE: Opcode
+    FADD: Opcode
+    FCMP: Opcode
+    FDIV: Opcode
+    FENCE: Opcode
+    FMUL: Opcode
+    FNEG: Opcode
+    FP_EXT: Opcode
+    FP_TO_SI: Opcode
+    FP_TO_UI: Opcode
+    FP_TRUNC: Opcode
+    FREEZE: Opcode
+    FREM: Opcode
+    FSUB: Opcode
+    GET_ELEMENT_PTR: Opcode
+    ICMP: Opcode
+    INDIRECT_BR: Opcode
+    INSERT_ELEMENT: Opcode
+    INSERT_VALUE: Opcode
+    INT_TO_PTR: Opcode
+    INVOKE: Opcode
+    LANDING_PAD: Opcode
+    LOAD: Opcode
+    LSHR: Opcode
+    MUL: Opcode
+    OR: Opcode
+    PHI: Opcode
+    PTR_TO_INT: Opcode
+    RESUME: Opcode
+    RET: Opcode
+    SDIV: Opcode
+    SELECT: Opcode
+    SEXT: Opcode
+    SHL: Opcode
+    SHUFFLE_VECTOR: Opcode
+    SI_TO_FP: Opcode
+    SREM: Opcode
+    STORE: Opcode
+    SUB: Opcode
+    SWITCH: Opcode
+    TRUNC: Opcode
+    UDIV: Opcode
+    UI_TO_FP: Opcode
+    UNREACHABLE: Opcode
+    UREM: Opcode
+    USER_OP_1: Opcode
+    USER_OP_2: Opcode
+    VA_ARG: Opcode
+    XOR: Opcode
+    ZEXT: Opcode
+
+class IntPredicate(Enum):
+    EQ: IntPredicate
+    NE: IntPredicate
+    UGT: IntPredicate
+    UGE: IntPredicate
+    ULT: IntPredicate
+    ULE: IntPredicate
+    SGT: IntPredicate
+    SGE: IntPredicate
+    SLT: IntPredicate
+    SLE: IntPredicate
+
+class FloatPredicate(Enum):
+    FALSE: FloatPredicate
+    OEQ: FloatPredicate
+    OGT: FloatPredicate
+    OGE: FloatPredicate
+    OLT: FloatPredicate
+    OLE: FloatPredicate
+    ONE: FloatPredicate
+    ORD: FloatPredicate
+    UNO: FloatPredicate
+    UEQ: FloatPredicate
+    UGT: FloatPredicate
+    UGE: FloatPredicate
+    ULT: FloatPredicate
+    ULE: FloatPredicate
+    UNE: FloatPredicate
+    TRUE: FloatPredicate
+
+class Instruction(Value):
+    @property
+    def opcode(self) -> Opcode: ...
+    @property
+    def operands(self) -> List[Value]: ...
+    @property
+    def successors(self) -> List[BasicBlock]: ...
+
+class Switch(Instruction):
+    @property
+    def cond(self) -> Value: ...
+    @property
+    def default(self) -> BasicBlock: ...
+    @property
+    def cases(self) -> List[Tuple[IntConstant, BasicBlock]]: ...
+
+class ICmp(Instruction):
+    @property
+    def predicate(self) -> IntPredicate: ...
+
+class FCmp(Instruction):
+    @property
+    def predicate(self) -> FloatPredicate: ...
+
+class Call(Instruction):
+    @property
+    def callee(self) -> Value: ...
+    @property
+    def args(self) -> List[Value]: ...
+
+class Phi(Instruction):
+    @property
+    def incoming(self) -> List[Tuple[Value, BasicBlock]]: ...
+
+class Module:
+    @staticmethod
+    def from_ir(ir: str, name: str = "") -> Module: ...
+    @staticmethod
+    def from_bitcode(bitcode: bytes, name: str = "") -> Module: ...
+    @property
+    def source_filename(self) -> str: ...
+    @source_filename.setter
+    def source_filename(self, value: str) -> None: ...
+    @property
+    def functions(self) -> List[Function]: ...
+    @property
+    def bitcode(self) -> bytes: ...
+    def __str__(self) -> str: ...
 
 class PyNonadaptiveJit:
     def eval(
diff --git a/pyqir/pyqir/evaluator/_gates.py b/pyqir/pyqir/evaluator.py
similarity index 71%
rename from pyqir/pyqir/evaluator/_gates.py
rename to pyqir/pyqir/evaluator.py
index 2b3dfe08..b850e317 100644
--- a/pyqir/pyqir/evaluator/_gates.py
+++ b/pyqir/pyqir/evaluator.py
@@ -1,7 +1,8 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from typing import Any, Dict, List
+from typing import Any, Dict, List, Optional
+from pyqir._native import PyNonadaptiveJit
 
 
 class GateSet:
@@ -138,3 +139,39 @@ def print(self) -> None:
 
         for instruction in self.instructions:
             print(instruction)
+
+
+class NonadaptiveEvaluator:
+    """
+    The non-adaptive JIT evaluates QIR programs without simulating the quantum
+    state. Measurement results are pre-determined before the program starts.
+    """
+
+    def __init__(self) -> None:
+        self._jit = PyNonadaptiveJit()
+
+    def eval(
+        self,
+        file_path: str,
+        gateset: GateSet,
+        entry_point: Optional[str] = None,
+        result_stream: Optional[List[bool]] = None,
+    ) -> None:
+        """
+        JIT compiles and evaluates the QIR program, delegating quantum
+        operations to the supplied gate set.
+
+        The result stream will be read in order by the measurement instruction.
+        Each measurement will pop a result from the beginning of the stream. If
+        the stream runs out of results, measurement returns zero.
+
+        Right now the evaluator does not have a full runtime environment and can
+        JIT QIR produced by the pyqir-generator, but cannot use any external function
+        calls.
+
+        :param file_path: file path of existing QIR in a ll or bc file
+        :param gateset: python GateSet based object defining the operations
+        :param entry_point: entry point name; required if QIR contains multiple entry points
+        :param result_stream: list of boolean result values representing the QIS measure results
+        """
+        self._jit.eval(file_path, gateset, entry_point, result_stream)
diff --git a/pyqir/pyqir/evaluator/__init__.py b/pyqir/pyqir/evaluator/__init__.py
deleted file mode 100644
index a3fb78ec..00000000
--- a/pyqir/pyqir/evaluator/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-from typing import List, Optional
-from pyqir.evaluator._gates import GateLogger as GateLogger, GateSet as GateSet
-from pyqir._native import PyNonadaptiveJit
-
-
-class NonadaptiveEvaluator:
-    """
-    The non-adaptive JIT evaluates QIR programs without simulating the quantum
-    state. Measurement results are pre-determined before the program starts.
-    """
-
-    def __init__(self) -> None:
-        self._jit = PyNonadaptiveJit()
-
-    def eval(
-        self,
-        file_path: str,
-        gateset: GateSet,
-        entry_point: Optional[str] = None,
-        result_stream: Optional[List[bool]] = None,
-    ) -> None:
-        """
-        JIT compiles and evaluates the QIR program, delegating quantum
-        operations to the supplied gate set.
-
-        The result stream will be read in order by the measurement instruction.
-        Each measurement will pop a result from the beginning of the stream. If
-        the stream runs out of results, measurement returns zero.
-
-        Right now the evaluator does not have a full runtime environment and can
-        JIT QIR produced by the pyqir-generator, but cannot use any external function
-        calls.
-
-        :param file_path: file path of existing QIR in a ll or bc file
-        :param gateset: python GateSet based object defining the operations
-        :param entry_point: entry point name; required if QIR contains multiple entry points
-        :param result_stream: list of boolean result values representing the QIS measure results
-        """
-        self._jit.eval(file_path, gateset, entry_point, result_stream)
diff --git a/pyqir/pyqir/generator/__init__.py b/pyqir/pyqir/generator/__init__.py
deleted file mode 100644
index 995e1f86..00000000
--- a/pyqir/pyqir/generator/__init__.py
+++ /dev/null
@@ -1,59 +0,0 @@
-# Copyright (c) Microsoft Corporation.
-# Licensed under the MIT License.
-
-"""
-Provides the ability to generate QIR using a Python API.
-
-This package is intended to be used by code automating translation processes
-enabling the conversion in some format to QIR via Python; i.e., this is a low
-level API intended to be used as a bridge to existing Python frameworks enabling
-the generation of QIR rather than directly consumed by an end-user. It is not
-intended to be used as a framework for algorithm and application development.
-"""
-
-from enum import Enum, auto
-from pyqir._native import (
-    BasicQisBuilder as BasicQisBuilder,
-    Builder as Builder,
-    SimpleModule as SimpleModule,
-    Type as Type,
-    TypeFactory as TypeFactory,
-    Value as Value,
-    bitcode_to_ir as bitcode_to_ir,
-    const as const,
-    ir_to_bitcode as ir_to_bitcode,
-)
-
-
-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/src/builder.rs b/pyqir/src/builder.rs
new file mode 100644
index 00000000..f77351a6
--- /dev/null
+++ b/pyqir/src/builder.rs
@@ -0,0 +1,319 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::{
+    context::{self, Context},
+    instructions::IntPredicate,
+    module::Module,
+    values::{self, AnyValue, Value},
+};
+use inkwell::{
+    types::{AnyTypeEnum, FunctionType},
+    values::{AnyValueEnum, CallableValue, IntValue},
+};
+use pyo3::{exceptions::PyValueError, prelude::*, types::PySequence};
+use std::{
+    convert::{Into, TryFrom, TryInto},
+    mem::transmute,
+    result::Result,
+};
+
+/// An instruction builder.
+#[pyclass(unsendable)]
+pub(crate) struct Builder {
+    builder: inkwell::builder::Builder<'static>,
+    context: Py<Context>,
+    // TODO: In principle, the module could be extracted from the builder.
+    // See https://github.com/TheDan64/inkwell/issues/347.
+    module: Py<Module>,
+}
+
+#[pymethods]
+impl Builder {
+    /// Inserts a bitwise logical and instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn and_(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_and::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a bitwise logical or instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn or_(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_or::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a bitwise logical exclusive or instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn xor(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_xor::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts an addition instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The sum.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn add(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_int_add::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a subtraction instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The difference.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn sub(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_int_sub::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a multiplication instruction.
+    ///
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :returns: The product.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn mul(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_int_mul::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a shift left instruction.
+    ///
+    /// :param Value lhs: The value to shift.
+    /// :param Value rhs: The number of bits to shift by.
+    /// :returns: The result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn shl(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_left_shift::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a logical (zero fill) shift right instruction.
+    ///
+    /// :param Value lhs: The value to shift.
+    /// :param Value rhs: The number of bits to shift by.
+    /// :returns: The result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, lhs, rhs)")]
+    fn lshr(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_right_shift::<IntValue>(
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            false,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts an integer comparison instruction.
+    ///
+    /// :param IntPredicate pred: The predicate to compare by.
+    /// :param Value lhs: The left-hand side.
+    /// :param Value rhs: The right-hand side.
+    /// :return: The boolean result.
+    /// :rtype: Value
+    #[pyo3(text_signature = "(self, pred, lhs, rhs)")]
+    #[allow(clippy::needless_pass_by_value)]
+    fn icmp(&self, py: Python, pred: IntPredicate, lhs: Value, rhs: Value) -> PyResult<PyObject> {
+        context::require_same(py, [&self.context, lhs.context(), rhs.context()])?;
+        let value = self.builder.build_int_compare::<IntValue>(
+            pred.into(),
+            unsafe { lhs.get() }.try_into()?,
+            unsafe { rhs.get() }.try_into()?,
+            "",
+        );
+        unsafe { Value::from_any(py, self.context.clone(), value) }
+    }
+
+    /// Inserts a call instruction.
+    ///
+    /// :param Value value: The value to call.
+    /// :param Sequence[Union[Value, bool, int, float]] args: The arguments to the function.
+    /// :returns: The return value, or None if the function has a void return type.
+    /// :rtype: Optional[Value]
+    #[pyo3(text_signature = "(self, callee, args)")]
+    fn call(&self, py: Python, callee: &Value, args: &PySequence) -> PyResult<Option<PyObject>> {
+        context::require_same(
+            py,
+            values::extract_contexts(args.iter()?.filter_map(Result::ok))
+                .chain([self.context.clone(), callee.context().clone()]),
+        )?;
+
+        let callable: Callable = unsafe { callee.get() }.try_into()?;
+        let param_types = callable.ty.get_param_types();
+        if param_types.len() != args.len()? {
+            return Err(PyValueError::new_err(format!(
+                "Expected {} arguments, got {}.",
+                param_types.len(),
+                args.len()?
+            )));
+        }
+
+        let args = args
+            .iter()?
+            .zip(param_types)
+            .map(|(v, t)| {
+                unsafe { values::extract_any(&t, v?) }?
+                    .try_into()
+                    .map_err(Into::into)
+            })
+            .collect::<PyResult<Vec<_>>>()?;
+
+        let call = self.builder.build_call(callable.value, &args, "");
+        let value = call.try_as_basic_value().left();
+        value
+            .map(|v| unsafe { Value::from_any(py, callee.context().clone(), v) })
+            .transpose()
+    }
+
+    /// Inserts a branch conditioned on a boolean.
+    ///
+    /// Instructions inserted when ``true`` is called will be inserted into the true branch.
+    /// Instructions inserted when ``false`` is called will be inserted into the false branch. The
+    /// true and false callables should use this module's builder to build instructions.
+    ///
+    /// :param Value cond: The boolean condition to branch on.
+    /// :param Callable[[], None] true:
+    ///     A callable that inserts instructions for the branch where the condition is true.
+    /// :param Callable[[], None] false:
+    ///     A callable that inserts instructions for the branch where the condition is false.
+    #[pyo3(text_signature = "(self, cond, true, false)")]
+    fn if_(
+        &self,
+        py: Python,
+        cond: &Value,
+        r#true: Option<&PyAny>,
+        r#false: Option<&PyAny>,
+    ) -> PyResult<()> {
+        context::require_same(py, [&self.context, cond.context()])?;
+        let module = self.module.borrow(py);
+        let builder = qirlib::Builder::from(&self.builder, unsafe { module.get() });
+        builder.try_build_if(
+            unsafe { cond.get() }.try_into()?,
+            |_| r#true.iter().try_for_each(|f| f.call0().map(|_| ())),
+            |_| r#false.iter().try_for_each(|f| f.call0().map(|_| ())),
+        )
+    }
+}
+
+impl Builder {
+    pub(crate) fn new(py: Python, module: Py<Module>) -> Self {
+        let context = module.borrow(py).context().clone();
+        let builder = {
+            let context = context.borrow(py);
+            let builder = context.create_builder();
+            unsafe {
+                transmute::<inkwell::builder::Builder<'_>, inkwell::builder::Builder<'static>>(
+                    builder,
+                )
+            }
+        };
+
+        Self {
+            builder,
+            context,
+            module,
+        }
+    }
+
+    pub(crate) unsafe fn get(&self) -> &inkwell::builder::Builder<'static> {
+        &self.builder
+    }
+
+    pub(crate) fn context(&self) -> &Py<Context> {
+        &self.context
+    }
+
+    pub(crate) fn module(&self) -> &Py<Module> {
+        &self.module
+    }
+}
+
+struct Callable<'ctx> {
+    value: CallableValue<'ctx>,
+    ty: FunctionType<'ctx>,
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for Callable<'ctx> {
+    type Error = PyErr;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::FunctionValue(f)) => Some(Self {
+                value: CallableValue::from(f),
+                ty: f.get_type(),
+            }),
+            AnyValue::Any(AnyValueEnum::PointerValue(p)) => match p.get_type().get_element_type() {
+                AnyTypeEnum::FunctionType(ty) => Some(Self {
+                    value: CallableValue::try_from(p).unwrap(),
+                    ty,
+                }),
+                _ => None,
+            },
+            _ => None,
+        }
+        .ok_or_else(|| PyValueError::new_err("Value is not callable."))
+    }
+}
diff --git a/pyqir/src/context.rs b/pyqir/src/context.rs
new file mode 100644
index 00000000..dd305d6e
--- /dev/null
+++ b/pyqir/src/context.rs
@@ -0,0 +1,50 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use pyo3::{exceptions::PyValueError, prelude::*, PyClass};
+use std::{borrow::Borrow, ops::Deref};
+
+#[pyclass]
+#[derive(Eq, PartialEq)]
+pub(crate) struct Context(inkwell::context::Context);
+
+impl Context {
+    pub(crate) fn new() -> Self {
+        Self(inkwell::context::Context::create())
+    }
+}
+
+impl Deref for Context {
+    type Target = inkwell::context::Context;
+
+    fn deref(&self) -> &Self::Target {
+        &self.0
+    }
+}
+
+pub(crate) fn require_same(
+    py: Python,
+    contexts: impl IntoIterator<Item = impl Borrow<Py<Context>>>,
+) -> PyResult<()> {
+    // then_some is stabilized in Rust 1.62.
+    #[allow(clippy::unnecessary_lazy_evaluations)]
+    is_all_same(py, contexts)
+        .then(|| ())
+        .ok_or_else(|| PyValueError::new_err("Some objects come from a different context."))
+}
+
+fn is_all_same<T>(py: Python, items: impl IntoIterator<Item = impl Borrow<Py<T>>>) -> bool
+where
+    T: Eq + PyClass,
+{
+    let mut items = items.into_iter();
+    if let Some(mut prev) = items.next() {
+        for item in items {
+            if *item.borrow().borrow(py) != *prev.borrow().borrow(py) {
+                return false;
+            }
+            prev = item;
+        }
+    }
+    true
+}
diff --git a/pyqir/src/generator.rs b/pyqir/src/generator.rs
deleted file mode 100644
index 6a361745..00000000
--- a/pyqir/src/generator.rs
+++ /dev/null
@@ -1,949 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-//
-// Safety
-// ------
-//
-// To store Inkwell/LLVM objects in Python classes, we transmute the 'ctx lifetime to static. You
-// need to be careful when using Inkwell types with unsafely extended lifetimes. Follow these rules:
-//
-// 1. When storing in a data type, always include a Py<Context> field containing the context
-//    originally referred to by 'ctx.
-// 2. Before calling Inkwell methods that use 'ctx, call Context::require_same to assert that all
-//    contexts being used are the same.
-
-use crate::utils::{
-    any_to_meta, call_if_some, clone_module, extract_constant, function_type, is_all_same,
-    try_callable_value,
-};
-use inkwell::{
-    builder::Builder as InkwellBuilder,
-    context::Context as InkwellContext,
-    module::Module as InkwellModule,
-    types::{AnyType, AnyTypeEnum},
-    values::{AnyValue, AnyValueEnum},
-    IntPredicate,
-};
-use pyo3::{
-    exceptions::{PyOSError, PyValueError},
-    prelude::*,
-    types::{PyBytes, PySequence, PyString, PyUnicode},
-};
-use qirlib::{module, types, BuilderBasicQisExt};
-use std::{borrow::Borrow, convert::Into, mem::transmute, result::Result};
-
-struct PyIntPredicate(IntPredicate);
-
-impl<'source> FromPyObject<'source> for PyIntPredicate {
-    fn extract(ob: &'source PyAny) -> PyResult<Self> {
-        match ob.getattr("name")?.extract()? {
-            "EQ" => Ok(Self(IntPredicate::EQ)),
-            "NE" => Ok(Self(IntPredicate::NE)),
-            "UGT" => Ok(Self(IntPredicate::UGT)),
-            "UGE" => Ok(Self(IntPredicate::UGE)),
-            "ULT" => Ok(Self(IntPredicate::ULT)),
-            "ULE" => Ok(Self(IntPredicate::ULE)),
-            "SGT" => Ok(Self(IntPredicate::SGT)),
-            "SGE" => Ok(Self(IntPredicate::SGE)),
-            "SLT" => Ok(Self(IntPredicate::SLT)),
-            "SLE" => Ok(Self(IntPredicate::SLE)),
-            _ => Err(PyValueError::new_err("Invalid integer predicate.")),
-        }
-    }
-}
-
-#[pyclass]
-#[derive(Eq, PartialEq)]
-struct Context(InkwellContext);
-
-impl Context {
-    fn from_values<'a>(
-        values: impl IntoIterator<Item = &'a PyAny> + 'a,
-    ) -> impl Iterator<Item = Py<Self>> + 'a {
-        values
-            .into_iter()
-            .filter_map(|v| Some(v.extract::<Value>().ok()?.context))
-    }
-
-    fn require_same(
-        py: Python,
-        contexts: impl IntoIterator<Item = impl Borrow<Py<Self>>>,
-    ) -> PyResult<()> {
-        // then_some is stabilized in Rust 1.62.
-        #[allow(clippy::unnecessary_lazy_evaluations)]
-        is_all_same(py, contexts)
-            .then(|| ())
-            .ok_or_else(|| PyValueError::new_err("Some objects come from a different context."))
-    }
-}
-
-/// A type.
-#[pyclass(unsendable)]
-pub(crate) struct Type {
-    ty: AnyTypeEnum<'static>,
-    context: Py<Context>,
-}
-
-#[pyclass(unsendable)]
-struct Module {
-    module: InkwellModule<'static>,
-    context: Py<Context>,
-}
-
-impl Module {
-    fn new(py: Python, context: Py<Context>, name: &str) -> Module {
-        let module = {
-            let context = context.borrow(py);
-            let module = context.0.create_module(name);
-            unsafe { transmute::<InkwellModule<'_>, InkwellModule<'static>>(module) }
-        };
-        Module { module, context }
-    }
-}
-
-/// Provides access to all supported types.
-#[pyclass]
-pub(crate) struct TypeFactory {
-    module: Py<Module>,
-}
-
-#[pymethods]
-impl TypeFactory {
-    /// The void type.
-    ///
-    /// :type: Type
-    #[getter]
-    fn void(&self, py: Python) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| m.get_context().void_type().into())
-    }
-
-    /// The boolean type.
-    ///
-    /// :type: Type
-    #[getter]
-    fn bool(&self, py: Python) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| m.get_context().bool_type().into())
-    }
-
-    /// An integer type.
-    ///
-    /// :param int width: The number of bits in the integers.
-    /// :returns: The integer type.
-    /// :rtype: Type
-    #[pyo3(text_signature = "(width)")]
-    fn int(&self, py: Python, width: u32) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| m.get_context().custom_width_int_type(width).into())
-    }
-
-    /// The double type.
-    ///
-    /// :type: Type
-    #[getter]
-    fn double(&self, py: Python) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| m.get_context().f64_type().into())
-    }
-
-    /// The qubit type.
-    ///
-    /// :type: Type
-    #[getter]
-    fn qubit(&self, py: Python) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| types::qubit(m).into())
-    }
-
-    /// The measurement result type.
-    ///
-    /// :type: Type
-    #[getter]
-    fn result(&self, py: Python) -> PyResult<Py<Type>> {
-        self.create_type(py, |m| types::result(m).into())
-    }
-
-    /// A function type.
-    ///
-    /// :param Type ret: The return type.
-    /// :param List[Type] params: The parameter types.
-    /// :returns: The function type.
-    /// :rtype: Type
-    #[staticmethod]
-    #[pyo3(text_signature = "(ret, params)")]
-    #[allow(clippy::needless_pass_by_value)]
-    fn function(py: Python, ret: &Type, params: Vec<Py<Type>>) -> PyResult<Py<Type>> {
-        Context::require_same(
-            py,
-            params
-                .iter()
-                .map(|t| t.borrow(py).context.clone())
-                .chain([ret.context.clone()]),
-        )?;
-
-        let ty = function_type(&ret.ty, params.iter().map(|t| t.borrow(py).ty))
-            .ok_or_else(|| PyValueError::new_err("Invalid return or parameter type."))?
-            .into();
-
-        let ty = unsafe { transmute::<AnyTypeEnum<'_>, AnyTypeEnum<'static>>(ty) };
-        let context = ret.context.clone();
-        Py::new(py, Type { ty, context })
-    }
-}
-
-impl TypeFactory {
-    fn create_type(
-        &self,
-        py: Python,
-        f: impl for<'ctx> Fn(&InkwellModule<'ctx>) -> AnyTypeEnum<'ctx>,
-    ) -> PyResult<Py<Type>> {
-        let module = self.module.borrow(py);
-        let context = module.context.clone();
-        let ty = {
-            let ty = f(&module.module);
-            unsafe { transmute::<AnyTypeEnum<'_>, AnyTypeEnum<'static>>(ty) }
-        };
-        Py::new(py, Type { ty, context })
-    }
-}
-
-/// A value.
-#[pyclass(unsendable)]
-#[derive(Clone)]
-pub(crate) struct Value {
-    value: AnyValueEnum<'static>,
-    context: Py<Context>,
-}
-
-impl Value {
-    unsafe fn new<'ctx>(context: Py<Context>, value: &impl AnyValue<'ctx>) -> Self {
-        let value = value.as_any_value_enum();
-        let value = transmute::<AnyValueEnum<'_>, AnyValueEnum<'static>>(value);
-        Self { value, context }
-    }
-
-    unsafe fn extract<'ctx>(ty: &impl AnyType<'ctx>, ob: &PyAny) -> PyResult<AnyValueEnum<'ctx>> {
-        ob.extract()
-            .map(|v: Self| v.value)
-            .or_else(|_| extract_constant(ty, ob))
-    }
-}
-
-/// An instruction builder.
-#[pyclass(unsendable)]
-pub(crate) struct Builder {
-    builder: InkwellBuilder<'static>,
-    context: Py<Context>,
-    // TODO: In principle, the module could be extracted from the builder.
-    // See https://github.com/TheDan64/inkwell/issues/347.
-    module: Py<Module>,
-}
-
-impl Builder {
-    fn new(py: Python, module: Py<Module>) -> Self {
-        let context = module.borrow(py).context.clone();
-        let builder = {
-            let context = context.borrow(py);
-            let builder = context.0.create_builder();
-            unsafe { transmute::<InkwellBuilder<'_>, InkwellBuilder<'static>>(builder) }
-        };
-
-        Self {
-            builder,
-            context,
-            module,
-        }
-    }
-}
-
-#[pymethods]
-impl Builder {
-    /// Inserts a bitwise logical and instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn and_(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_and(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a bitwise logical or instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn or_(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_or(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a bitwise logical exclusive or instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn xor(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_xor(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts an addition instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The sum.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn add(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_int_add(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a subtraction instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The difference.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn sub(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_int_sub(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a multiplication instruction.
-    ///
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :returns: The product.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn mul(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value =
-            self.builder
-                .build_int_mul(lhs.value.into_int_value(), rhs.value.into_int_value(), "");
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a shift left instruction.
-    ///
-    /// :param Value lhs: The value to shift.
-    /// :param Value rhs: The number of bits to shift by.
-    /// :returns: The result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn shl(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value = self.builder.build_left_shift(
-            lhs.value.into_int_value(),
-            rhs.value.into_int_value(),
-            "",
-        );
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a logical (zero fill) shift right instruction.
-    ///
-    /// :param Value lhs: The value to shift.
-    /// :param Value rhs: The number of bits to shift by.
-    /// :returns: The result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, lhs, rhs)")]
-    fn lshr(&self, py: Python, lhs: &Value, rhs: &Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value = self.builder.build_right_shift(
-            lhs.value.into_int_value(),
-            rhs.value.into_int_value(),
-            false,
-            "",
-        );
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts an integer comparison instruction.
-    ///
-    /// :param IntPredicate pred: The predicate to compare by.
-    /// :param Value lhs: The left-hand side.
-    /// :param Value rhs: The right-hand side.
-    /// :return: The boolean result.
-    /// :rtype: Value
-    #[pyo3(text_signature = "(self, pred, lhs, rhs)")]
-    #[allow(clippy::needless_pass_by_value)]
-    fn icmp(&self, py: Python, pred: PyIntPredicate, lhs: Value, rhs: Value) -> PyResult<Value> {
-        Context::require_same(py, [&self.context, &lhs.context, &rhs.context])?;
-        let value = self.builder.build_int_compare(
-            pred.0,
-            lhs.value.into_int_value(),
-            rhs.value.into_int_value(),
-            "",
-        );
-        Ok(unsafe { Value::new(self.context.clone(), &value) })
-    }
-
-    /// Inserts a call instruction.
-    ///
-    /// :param Function function: The function to call.
-    /// :param Sequence[Union[Value, bool, int, float]] args: The arguments to the function.
-    /// :returns: The return value, or None if the function has a void return type.
-    /// :rtype: Optional[Value]
-    #[pyo3(text_signature = "(self, function, args)")]
-    fn call(&self, py: Python, function: &Value, args: &PySequence) -> PyResult<Option<Value>> {
-        Context::require_same(
-            py,
-            Context::from_values(args.iter()?.filter_map(Result::ok))
-                .chain([self.context.clone(), function.context.clone()]),
-        )?;
-
-        let (callable, param_types) = try_callable_value(function.value)
-            .ok_or_else(|| PyValueError::new_err("Value is not callable."))?;
-
-        if param_types.len() != args.len()? {
-            return Err(PyValueError::new_err(format!(
-                "Expected {} arguments, got {}.",
-                param_types.len(),
-                args.len()?
-            )));
-        }
-
-        let args = args
-            .iter()?
-            .zip(param_types)
-            .map(|(v, t)| {
-                let value = unsafe { Value::extract(&t, v?) }?;
-                any_to_meta(value).ok_or_else(|| PyValueError::new_err("Invalid argument."))
-            })
-            .collect::<PyResult<Vec<_>>>()?;
-
-        let call = self.builder.build_call(callable, &args, "");
-        let value = call.try_as_basic_value().left();
-        Ok(value.map(|v| unsafe { Value::new(function.context.clone(), &v) }))
-    }
-
-    /// Inserts a branch conditioned on a boolean.
-    ///
-    /// Instructions inserted when ``true`` is called will be inserted into the true branch.
-    /// Instructions inserted when ``false`` is called will be inserted into the false branch. The
-    /// true and false callables should use this module's builder to build instructions.
-    ///
-    /// :param Value cond: The boolean condition to branch on.
-    /// :param Callable[[], None] true:
-    ///     A callable that inserts instructions for the branch where the condition is true.
-    /// :param Callable[[], None] false:
-    ///     A callable that inserts instructions for the branch where the condition is false.
-    #[pyo3(text_signature = "(self, cond, true, false)")]
-    fn if_(
-        &self,
-        py: Python,
-        cond: &Value,
-        r#true: Option<&PyAny>,
-        r#false: Option<&PyAny>,
-    ) -> PyResult<()> {
-        Context::require_same(py, [&self.context, &cond.context])?;
-        let module = self.module.borrow(py);
-        let builder = qirlib::Builder::from(&self.builder, &module.module);
-        builder.try_build_if(
-            cond.value.into_int_value(),
-            |_| call_if_some(r#true),
-            |_| call_if_some(r#false),
-        )
-    }
-}
-
-/// A simple module represents an executable program with these restrictions:
-///
-/// - There is one global qubit register and one global result register. Both are statically
-///   allocated with a fixed size.
-/// - There is only a single function that runs as the entry point.
-///
-/// :param str name: The name of the module.
-/// :param int num_qubits: The number of statically allocated qubits.
-/// :param int num_results: The number of statically allocated results.
-#[pyclass(unsendable)]
-#[pyo3(text_signature = "(name, num_qubits, num_results)")]
-pub(crate) struct SimpleModule {
-    module: Py<Module>,
-    builder: Py<Builder>,
-    types: Py<TypeFactory>,
-    num_qubits: u64,
-    num_results: u64,
-}
-
-#[pymethods]
-impl SimpleModule {
-    #[new]
-    fn new(py: Python, name: &str, num_qubits: u64, num_results: u64) -> PyResult<SimpleModule> {
-        let context = Py::new(py, Context(InkwellContext::create()))?;
-        let module = Py::new(py, Module::new(py, context, name))?;
-        let builder = Py::new(py, Builder::new(py, module.clone()))?;
-
-        {
-            let builder = builder.borrow(py);
-            let module = module.borrow(py);
-            module::simple_init(&module.module, &builder.builder, num_qubits, num_results);
-        }
-
-        let types = Py::new(
-            py,
-            TypeFactory {
-                module: module.clone(),
-            },
-        )?;
-
-        Ok(SimpleModule {
-            module,
-            builder,
-            types,
-            num_qubits,
-            num_results,
-        })
-    }
-
-    #[getter]
-    fn types(&self) -> Py<TypeFactory> {
-        self.types.clone()
-    }
-
-    /// The global qubit register.
-    ///
-    /// :type: Tuple[Value, ...]
-    #[getter]
-    fn qubits(&self, py: Python) -> Vec<Value> {
-        let builder = self.builder.borrow(py);
-        let module = self.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        (0..self.num_qubits)
-            .map(|id| unsafe { Value::new(module.context.clone(), &builder.build_qubit(id)) })
-            .collect()
-    }
-
-    /// The global result register.
-    ///
-    /// :type: Tuple[Value, ...]
-    #[getter]
-    fn results(&self, py: Python) -> Vec<Value> {
-        let builder = self.builder.borrow(py);
-        let module = self.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        (0..self.num_results)
-            .map(|id| unsafe { Value::new(module.context.clone(), &builder.build_result(id)) })
-            .collect()
-    }
-
-    /// The instruction builder.
-    ///
-    /// :type: Builder
-    #[getter]
-    fn builder(&self) -> Py<Builder> {
-        self.builder.clone()
-    }
-
-    /// Emits the LLVM IR for the module as plain text.
-    ///
-    /// :rtype: str
-    fn ir(&self, py: Python) -> PyResult<String> {
-        self.emit(py, |m| m.print_to_string().to_string())
-    }
-
-    /// Emits the LLVM bitcode for the module as a sequence of bytes.
-    ///
-    /// :rtype: bytes
-    fn bitcode<'py>(&self, py: Python<'py>) -> PyResult<&'py PyBytes> {
-        self.emit(py, |m| {
-            PyBytes::new(py, m.write_bitcode_to_memory().as_slice())
-        })
-    }
-
-    /// Adds a declaration for an externally linked function to the module.
-    ///
-    /// :param str name: The name of the function.
-    /// :param Type ty: The type of the function.
-    /// :return: The function value.
-    /// :rtype: Function
-    #[pyo3(text_signature = "(self, name, ty)")]
-    fn add_external_function(&mut self, py: Python, name: &str, ty: &Type) -> PyResult<Value> {
-        let module = self.module.borrow(py);
-        Context::require_same(py, [&module.context, &ty.context])?;
-
-        let context = ty.context.clone();
-        let ty = ty.ty.into_function_type();
-        let function = module.module.add_function(name, ty, None);
-        Ok(unsafe { Value::new(context, &function) })
-    }
-}
-
-impl SimpleModule {
-    fn emit<T>(&self, py: Python, f: impl Fn(&InkwellModule) -> T) -> PyResult<T> {
-        let module = self.module.borrow(py);
-        let builder = self.builder.borrow(py);
-        let ret = builder.builder.build_return(None);
-        let new_context = InkwellContext::create();
-        let new_module = clone_module(&module.module, &new_context)?;
-        ret.erase_from_basic_block();
-        module::simple_finalize(&new_module).map_err(PyOSError::new_err)?;
-        Ok(f(&new_module))
-    }
-}
-
-/// An instruction builder that generates instructions from the basic quantum instruction set.
-///
-/// :param Builder builder: The underlying builder used to build QIS instructions.
-#[pyclass]
-#[pyo3(text_signature = "(builder)")]
-pub(crate) struct BasicQisBuilder {
-    builder: Py<Builder>,
-}
-
-#[pymethods]
-impl BasicQisBuilder {
-    #[new]
-    fn new(builder: Py<Builder>) -> Self {
-        BasicQisBuilder { builder }
-    }
-
-    /// Inserts a controlled Pauli :math:`X` gate.
-    ///
-    /// :param Value control: The control qubit.
-    /// :param Value target: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, control, target)")]
-    fn cx(&self, py: Python, control: &Value, target: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &control.context, &target.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_cx(
-            control.value.into_pointer_value(),
-            target.value.into_pointer_value(),
-        );
-        Ok(())
-    }
-
-    /// Inserts a controlled Pauli :math:`Z` gate.
-    ///
-    /// :param Value control: The control qubit.
-    /// :param Value target: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, control, target)")]
-    fn cz(&self, py: Python, control: &Value, target: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &control.context, &target.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_cz(
-            control.value.into_pointer_value(),
-            target.value.into_pointer_value(),
-        );
-        Ok(())
-    }
-
-    /// Inserts a Hadamard gate.
-    ///
-    /// :param qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn h(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_h(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a Z-basis measurement operation.
-    ///
-    /// :param Value qubit: The qubit to measure.
-    /// :param Value result: A result where the measurement result will be written to.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit, result)")]
-    fn mz(&self, py: Python, qubit: &Value, result: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context, &result.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_mz(
-            qubit.value.into_pointer_value(),
-            result.value.into_pointer_value(),
-        );
-        Ok(())
-    }
-
-    /// Inserts a reset operation.
-    ///
-    /// :param Value qubit: The qubit to reset.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn reset(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_reset(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a rotation gate about the :math:`x` axis.
-    ///
-    /// :param Union[Value, float] theta: The angle to rotate by.
-    /// :param Value qubit: The qubit to rotate.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, theta, qubit)")]
-    fn rx(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(
-            py,
-            Context::from_values([theta]).chain([builder.context.clone(), qubit.context.clone()]),
-        )?;
-
-        let context = builder.context.borrow(py);
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        let theta = unsafe { Value::extract(&context.0.f64_type(), theta)? };
-        builder.build_rx(theta.into_float_value(), qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a rotation gate about the :math:`y` axis.
-    ///
-    /// :param Union[Value, float] theta: The angle to rotate by.
-    /// :param Value qubit: The qubit to rotate.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, theta, qubit)")]
-    fn ry(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(
-            py,
-            Context::from_values([theta]).chain([builder.context.clone(), qubit.context.clone()]),
-        )?;
-
-        let context = builder.context.borrow(py);
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        let theta = unsafe { Value::extract(&context.0.f64_type(), theta)? };
-        builder.build_ry(theta.into_float_value(), qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a rotation gate about the :math:`z` axis.
-    ///
-    /// :param Union[Value, float] theta: The angle to rotate by.
-    /// :param Value qubit: The qubit to rotate.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, theta, qubit)")]
-    fn rz(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(
-            py,
-            Context::from_values([theta]).chain([builder.context.clone(), qubit.context.clone()]),
-        )?;
-
-        let context = builder.context.borrow(py);
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        let theta = unsafe { Value::extract(&context.0.f64_type(), theta)? };
-        builder.build_rz(theta.into_float_value(), qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts an :math:`S` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn s(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_s(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts an adjoint :math:`S` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn s_adj(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_s_adj(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a :math:`T` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn t(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_t(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts an adjoint :math:`T` gate.
-    ///
-    /// :param qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn t_adj(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_t_adj(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a Pauli :math:`X` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn x(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_x(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a Pauli :math:`Y` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn y(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_y(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a Pauli :math:`Z` gate.
-    ///
-    /// :param Value qubit: The target qubit.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, qubit)")]
-    fn z(&self, py: Python, qubit: &Value) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &qubit.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.build_z(qubit.value.into_pointer_value());
-        Ok(())
-    }
-
-    /// Inserts a branch conditioned on a measurement result.
-    ///
-    /// Instructions inserted when ``one`` is called will be inserted into the one branch.
-    /// Instructions inserted when ``zero`` is called will be inserted into the zero branch. The one
-    /// and zero callables should use this module's builder to build instructions.
-    ///
-    /// :param Value cond: The result condition to branch on.
-    /// :param Callable[[], None] one:
-    ///     A callable that inserts instructions for the branch where the result is one.
-    /// :param Callable[[], None] zero:
-    ///     A callable that inserts instructions for the branch where the result is zero.
-    /// :rtype: None
-    #[pyo3(text_signature = "(self, cond, one, zero)")]
-    fn if_result(
-        &self,
-        py: Python,
-        cond: &Value,
-        one: Option<&PyAny>,
-        zero: Option<&PyAny>,
-    ) -> PyResult<()> {
-        let builder = self.builder.borrow(py);
-        Context::require_same(py, [&builder.context, &cond.context])?;
-        let module = builder.module.borrow(py);
-        let builder = qirlib::Builder::from(&builder.builder, &module.module);
-        builder.try_build_if_result(
-            cond.value.into_pointer_value(),
-            |_| call_if_some(one),
-            |_| call_if_some(zero),
-        )
-    }
-}
-
-/// Creates a constant value.
-///
-/// :param Type ty: The type of the value.
-/// :param Union[int, float] value: The value of the constant.
-/// :returns: The constant value.
-/// :rtype: Value
-#[pyfunction]
-#[pyo3(text_signature = "(ty, value)")]
-pub(crate) fn r#const(ty: &Type, value: &PyAny) -> PyResult<Value> {
-    let context = ty.context.clone();
-    let value = extract_constant(&ty.ty, value)?;
-    Ok(unsafe { Value::new(context, &value) })
-}
-
-/// Converts the supplied QIR string to its bitcode equivalent.
-///
-/// :param str ir: The QIR string to convert
-/// :param Optional[str] module_name: The name of the QIR module, default is "" if None
-/// :param Optional[str] source_file_name: The source file name of the QIR module. Unchanged if None
-/// :return: The equivalent bitcode as bytes.
-/// :rtype: bytes
-#[pyfunction]
-#[pyo3(text_signature = "(ir, module_name=None, source_file_name=None)")]
-pub(crate) fn ir_to_bitcode<'a>(
-    py: Python<'a>,
-    ir: &str,
-    module_name: Option<&str>,
-    source_file_name: Option<&str>,
-) -> PyResult<&'a PyBytes> {
-    let bitcode =
-        module::ir_to_bitcode(ir, module_name, source_file_name).map_err(PyOSError::new_err)?;
-    Ok(PyBytes::new(py, &bitcode))
-}
-
-/// Converts the supplied bitcode to its QIR string equivalent.
-///
-/// :param bytes ir: The bitcode bytes to convert
-/// :param Optional[str] module_name: The name of the QIR module, default is "" if None
-/// :param Optional[str] source_file_name: The source file name of the QIR module. Unchanged if None
-/// :return: The equivalent QIR string.
-/// :rtype: str
-#[pyfunction]
-#[pyo3(text_signature = "(bitcode, module_name=None, source_file_name=None)")]
-pub(crate) fn bitcode_to_ir<'a>(
-    py: Python<'a>,
-    bitcode: &PyBytes,
-    module_name: Option<&str>,
-    source_file_name: Option<&str>,
-) -> PyResult<&'a PyString> {
-    let ir = module::bitcode_to_ir(bitcode.as_bytes(), module_name, source_file_name)
-        .map_err(PyOSError::new_err)?;
-    Ok(PyUnicode::new(py, &ir))
-}
diff --git a/pyqir/src/instructions.rs b/pyqir/src/instructions.rs
new file mode 100644
index 00000000..39bde8ee
--- /dev/null
+++ b/pyqir/src/instructions.rs
@@ -0,0 +1,568 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::used_underscore_binding)]
+
+use crate::{
+    context::Context,
+    values::{BasicBlock, Value},
+};
+use either::Either::{Left, Right};
+use inkwell::{
+    values::InstructionOpcode,
+    values::{InstructionValue, PhiValue},
+    LLVMReference,
+};
+use llvm_sys::core::LLVMIsATerminatorInst;
+use pyo3::{conversion::ToPyObject, prelude::*};
+use std::{
+    convert::{Into, TryInto},
+    mem::transmute,
+};
+
+/// An instruction.
+#[pyclass(extends = Value, subclass, unsendable)]
+pub(crate) struct Instruction(InstructionValue<'static>);
+
+#[pymethods]
+impl Instruction {
+    /// The instruction opcode.
+    ///
+    /// :type: Opcode
+    #[getter]
+    fn opcode(&self) -> Opcode {
+        self.0.get_opcode().into()
+    }
+
+    /// The operands to the instruction.
+    ///
+    /// :type: List[Value]
+    #[getter]
+    fn operands(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let inst = slf.0;
+        let value = slf.into_super();
+        let context = value.context();
+        (0..inst.get_num_operands())
+            .map(|i| match inst.get_operand(i).unwrap() {
+                Left(value) => unsafe { Value::from_any(py, context.clone(), value) },
+                Right(block) => unsafe { Value::from_any(py, context.clone(), block) },
+            })
+            .collect()
+    }
+
+    /// The basic blocks that are successors to this instruction. If this is not a terminator, the
+    /// list is empty.
+    ///
+    /// :type: List[BasicBlock]
+    #[getter]
+    fn successors(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        if unsafe { LLVMIsATerminatorInst(slf.0.get_ref()) }.is_null() {
+            Ok(Vec::new())
+        } else {
+            // then_some is stabilized in Rust 1.62.
+            #[allow(clippy::unnecessary_lazy_evaluations)]
+            Self::operands(slf, py)?
+                .into_iter()
+                .filter_map(|o| {
+                    o.as_ref(py)
+                        .is_instance_of::<BasicBlock>()
+                        .map(|b| b.then(|| o))
+                        .transpose()
+                })
+                .collect()
+        }
+    }
+}
+
+impl Instruction {
+    pub(crate) unsafe fn from_inst(
+        py: Python,
+        context: Py<Context>,
+        inst: InstructionValue,
+    ) -> PyResult<PyObject> {
+        let inst = transmute::<InstructionValue<'_>, InstructionValue<'static>>(inst);
+        let base = Value::init(context, inst.into()).add_subclass(Self(inst));
+        match inst.get_opcode() {
+            InstructionOpcode::Switch => Ok(Py::new(py, base.add_subclass(Switch))?.to_object(py)),
+            InstructionOpcode::ICmp => Ok(Py::new(py, base.add_subclass(ICmp))?.to_object(py)),
+            InstructionOpcode::FCmp => Ok(Py::new(py, base.add_subclass(FCmp))?.to_object(py)),
+            InstructionOpcode::Call => Ok(Py::new(py, base.add_subclass(Call))?.to_object(py)),
+            InstructionOpcode::Phi => {
+                let phi = Phi(inst.try_into().unwrap());
+                Ok(Py::new(py, base.add_subclass(phi))?.to_object(py))
+            }
+            _ => Ok(Py::new(py, base)?.to_object(py)),
+        }
+    }
+}
+
+/// An instruction opcode.
+#[pyclass]
+pub(crate) enum Opcode {
+    #[pyo3(name = "ADD")]
+    Add,
+    #[pyo3(name = "ADDR_SPACE_CAST")]
+    AddrSpaceCast,
+    #[pyo3(name = "ALLOCA")]
+    Alloca,
+    #[pyo3(name = "AND")]
+    And,
+    #[pyo3(name = "ASHR")]
+    AShr,
+    #[pyo3(name = "ATOMIC_CMP_XCHG")]
+    AtomicCmpXchg,
+    #[pyo3(name = "ATOMIC_RMW")]
+    AtomicRmw,
+    #[pyo3(name = "BIT_CAST")]
+    BitCast,
+    #[pyo3(name = "BR")]
+    Br,
+    #[pyo3(name = "CALL")]
+    Call,
+    #[pyo3(name = "CALL_BR")]
+    CallBr,
+    #[pyo3(name = "CATCH_RET")]
+    CatchRet,
+    #[pyo3(name = "CATCH_PAD")]
+    CatchPad,
+    #[pyo3(name = "CATCH_SWITCH")]
+    CatchSwitch,
+    #[pyo3(name = "CLEANUP_PAD")]
+    CleanupPad,
+    #[pyo3(name = "CLEANUP_RET")]
+    CleanupRet,
+    #[pyo3(name = "EXTRACT_ELEMENT")]
+    ExtractElement,
+    #[pyo3(name = "EXTRACT_VALUE")]
+    ExtractValue,
+    #[pyo3(name = "FADD")]
+    FAdd,
+    #[pyo3(name = "FCMP")]
+    FCmp,
+    #[pyo3(name = "FDIV")]
+    FDiv,
+    #[pyo3(name = "FENCE")]
+    Fence,
+    #[pyo3(name = "FMUL")]
+    FMul,
+    #[pyo3(name = "FNEG")]
+    FNeg,
+    #[pyo3(name = "FP_EXT")]
+    FPExt,
+    #[pyo3(name = "FP_TO_SI")]
+    FPToSI,
+    #[pyo3(name = "FP_TO_UI")]
+    FPToUI,
+    #[pyo3(name = "FP_TRUNC")]
+    FPTrunc,
+    #[pyo3(name = "FREEZE")]
+    Freeze,
+    #[pyo3(name = "FREM")]
+    FRem,
+    #[pyo3(name = "FSUB")]
+    FSub,
+    #[pyo3(name = "GET_ELEMENT_PTR")]
+    GetElementPtr,
+    #[pyo3(name = "ICMP")]
+    ICmp,
+    #[pyo3(name = "INDIRECT_BR")]
+    IndirectBr,
+    #[pyo3(name = "INSERT_ELEMENT")]
+    InsertElement,
+    #[pyo3(name = "INSERT_VALUE")]
+    InsertValue,
+    #[pyo3(name = "INT_TO_PTR")]
+    IntToPtr,
+    #[pyo3(name = "INVOKE")]
+    Invoke,
+    #[pyo3(name = "LANDING_PAD")]
+    LandingPad,
+    #[pyo3(name = "LOAD")]
+    Load,
+    #[pyo3(name = "LSHR")]
+    LShr,
+    #[pyo3(name = "MUL")]
+    Mul,
+    #[pyo3(name = "OR")]
+    Or,
+    #[pyo3(name = "PHI")]
+    Phi,
+    #[pyo3(name = "PTR_TO_INT")]
+    PtrToInt,
+    #[pyo3(name = "RESUME")]
+    Resume,
+    #[pyo3(name = "RET")]
+    Ret,
+    #[pyo3(name = "SDIV")]
+    SDiv,
+    #[pyo3(name = "SELECT")]
+    Select,
+    #[pyo3(name = "SEXT")]
+    SExt,
+    #[pyo3(name = "SHL")]
+    Shl,
+    #[pyo3(name = "SHUFFLE_VECTOR")]
+    ShuffleVector,
+    #[pyo3(name = "SI_TO_FP")]
+    SIToFP,
+    #[pyo3(name = "SREM")]
+    SRem,
+    #[pyo3(name = "STORE")]
+    Store,
+    #[pyo3(name = "SUB")]
+    Sub,
+    #[pyo3(name = "SWITCH")]
+    Switch,
+    #[pyo3(name = "TRUNC")]
+    Trunc,
+    #[pyo3(name = "UDIV")]
+    UDiv,
+    #[pyo3(name = "UI_TO_FP")]
+    UIToFP,
+    #[pyo3(name = "UNREACHABLE")]
+    Unreachable,
+    #[pyo3(name = "UREM")]
+    URem,
+    #[pyo3(name = "USER_OP_1")]
+    UserOp1,
+    #[pyo3(name = "USER_OP_2")]
+    UserOp2,
+    #[pyo3(name = "VA_ARG")]
+    VaArg,
+    #[pyo3(name = "XOR")]
+    Xor,
+    #[pyo3(name = "ZEXT")]
+    ZExt,
+}
+
+impl From<InstructionOpcode> for Opcode {
+    fn from(opcode: InstructionOpcode) -> Self {
+        match opcode {
+            InstructionOpcode::Add => Self::Add,
+            InstructionOpcode::AddrSpaceCast => Self::AddrSpaceCast,
+            InstructionOpcode::Alloca => Self::Alloca,
+            InstructionOpcode::And => Self::And,
+            InstructionOpcode::AShr => Self::AShr,
+            InstructionOpcode::AtomicCmpXchg => Self::AtomicCmpXchg,
+            InstructionOpcode::AtomicRMW => Self::AtomicRmw,
+            InstructionOpcode::BitCast => Self::BitCast,
+            InstructionOpcode::Br => Self::Br,
+            InstructionOpcode::Call => Self::Call,
+            InstructionOpcode::CallBr => Self::CallBr,
+            InstructionOpcode::CatchPad => Self::CatchPad,
+            InstructionOpcode::CatchRet => Self::CatchRet,
+            InstructionOpcode::CatchSwitch => Self::CatchSwitch,
+            InstructionOpcode::CleanupPad => Self::CleanupPad,
+            InstructionOpcode::CleanupRet => Self::CleanupRet,
+            InstructionOpcode::ExtractElement => Self::ExtractElement,
+            InstructionOpcode::ExtractValue => Self::ExtractValue,
+            InstructionOpcode::FNeg => Self::FNeg,
+            InstructionOpcode::FAdd => Self::FAdd,
+            InstructionOpcode::FCmp => Self::FCmp,
+            InstructionOpcode::FDiv => Self::FDiv,
+            InstructionOpcode::Fence => Self::Fence,
+            InstructionOpcode::FMul => Self::FMul,
+            InstructionOpcode::FPExt => Self::FPExt,
+            InstructionOpcode::FPToSI => Self::FPToSI,
+            InstructionOpcode::FPToUI => Self::FPToUI,
+            InstructionOpcode::FPTrunc => Self::FPTrunc,
+            InstructionOpcode::Freeze => Self::Freeze,
+            InstructionOpcode::FRem => Self::FRem,
+            InstructionOpcode::FSub => Self::FSub,
+            InstructionOpcode::GetElementPtr => Self::GetElementPtr,
+            InstructionOpcode::ICmp => Self::ICmp,
+            InstructionOpcode::IndirectBr => Self::IndirectBr,
+            InstructionOpcode::InsertElement => Self::InsertElement,
+            InstructionOpcode::InsertValue => Self::InsertValue,
+            InstructionOpcode::IntToPtr => Self::IntToPtr,
+            InstructionOpcode::Invoke => Self::Invoke,
+            InstructionOpcode::LandingPad => Self::LandingPad,
+            InstructionOpcode::Load => Self::Load,
+            InstructionOpcode::LShr => Self::LShr,
+            InstructionOpcode::Mul => Self::Mul,
+            InstructionOpcode::Or => Self::Or,
+            InstructionOpcode::Phi => Self::Phi,
+            InstructionOpcode::PtrToInt => Self::PtrToInt,
+            InstructionOpcode::Resume => Self::Resume,
+            InstructionOpcode::Return => Self::Ret,
+            InstructionOpcode::SDiv => Self::SDiv,
+            InstructionOpcode::Select => Self::Select,
+            InstructionOpcode::SExt => Self::SExt,
+            InstructionOpcode::Shl => Self::Shl,
+            InstructionOpcode::ShuffleVector => Self::ShuffleVector,
+            InstructionOpcode::SIToFP => Self::SIToFP,
+            InstructionOpcode::SRem => Self::SRem,
+            InstructionOpcode::Store => Self::Store,
+            InstructionOpcode::Sub => Self::Sub,
+            InstructionOpcode::Switch => Self::Switch,
+            InstructionOpcode::Trunc => Self::Trunc,
+            InstructionOpcode::UDiv => Self::UDiv,
+            InstructionOpcode::UIToFP => Self::UIToFP,
+            InstructionOpcode::Unreachable => Self::Unreachable,
+            InstructionOpcode::URem => Self::URem,
+            InstructionOpcode::UserOp1 => Self::UserOp1,
+            InstructionOpcode::UserOp2 => Self::UserOp2,
+            InstructionOpcode::VAArg => Self::VaArg,
+            InstructionOpcode::Xor => Self::Xor,
+            InstructionOpcode::ZExt => Self::ZExt,
+        }
+    }
+}
+
+/// A switch instruction.
+#[pyclass(extends = Instruction)]
+pub(crate) struct Switch;
+
+#[pymethods]
+impl Switch {
+    /// The condition of the switch.
+    ///
+    /// :type: Value
+    #[getter]
+    fn cond(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let inst = slf.into_super();
+        let cond = inst.0.get_operand(0).unwrap().left().unwrap();
+        let context = inst.into_super().context().clone();
+        unsafe { Value::from_any(py, context, cond) }
+    }
+
+    /// The default successor block if none of the cases match.
+    ///
+    /// :type: BasicBlock
+    #[getter]
+    fn default(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let inst = slf.into_super();
+        let block = inst.0.get_operand(1).unwrap().right().unwrap();
+        let context = inst.into_super().context().clone();
+        unsafe { Value::from_any(py, context, block) }
+    }
+
+    /// The switch cases.
+    ///
+    /// :type: List[Tuple[Value, BasicBlock]]
+    #[getter]
+    fn cases(slf: PyRef<Self>, py: Python) -> PyResult<Vec<(PyObject, PyObject)>> {
+        let inst_ref = slf.into_super();
+        let inst = inst_ref.0;
+        let value = inst_ref.into_super();
+        let context = value.context();
+
+        (2..inst.get_num_operands())
+            .step_by(2)
+            .map(|i| {
+                let cond = inst.get_operand(i).unwrap().left().unwrap();
+                let cond = unsafe { Value::from_any(py, context.clone(), cond)? };
+                let succ = inst.get_operand(i + 1).unwrap().right().unwrap();
+                let succ = unsafe { Value::from_any(py, context.clone(), succ)? };
+                Ok((cond, succ))
+            })
+            .collect()
+    }
+}
+
+/// An integer comparison instruction.
+#[pyclass(extends = Instruction)]
+pub(crate) struct ICmp;
+
+#[pymethods]
+impl ICmp {
+    /// The comparison predicate.
+    ///
+    /// :type: IntPredicate
+    #[getter]
+    fn predicate(slf: PyRef<Self>) -> IntPredicate {
+        slf.into_super().0.get_icmp_predicate().unwrap().into()
+    }
+}
+
+/// An integer comparison predicate.
+#[pyclass]
+#[derive(Clone)]
+pub(crate) enum IntPredicate {
+    #[pyo3(name = "EQ")]
+    Eq,
+    #[pyo3(name = "NE")]
+    Ne,
+    #[pyo3(name = "UGT")]
+    Ugt,
+    #[pyo3(name = "UGE")]
+    Uge,
+    #[pyo3(name = "ULT")]
+    Ult,
+    #[pyo3(name = "ULE")]
+    Ule,
+    #[pyo3(name = "SGT")]
+    Sgt,
+    #[pyo3(name = "SGE")]
+    Sge,
+    #[pyo3(name = "SLT")]
+    Slt,
+    #[pyo3(name = "SLE")]
+    Sle,
+}
+
+impl From<inkwell::IntPredicate> for IntPredicate {
+    fn from(pred: inkwell::IntPredicate) -> Self {
+        match pred {
+            inkwell::IntPredicate::EQ => Self::Eq,
+            inkwell::IntPredicate::NE => Self::Ne,
+            inkwell::IntPredicate::UGT => Self::Ugt,
+            inkwell::IntPredicate::UGE => Self::Uge,
+            inkwell::IntPredicate::ULT => Self::Ult,
+            inkwell::IntPredicate::ULE => Self::Ule,
+            inkwell::IntPredicate::SGT => Self::Sgt,
+            inkwell::IntPredicate::SGE => Self::Sge,
+            inkwell::IntPredicate::SLT => Self::Slt,
+            inkwell::IntPredicate::SLE => Self::Sle,
+        }
+    }
+}
+
+impl From<IntPredicate> for inkwell::IntPredicate {
+    fn from(pred: IntPredicate) -> Self {
+        match pred {
+            IntPredicate::Eq => Self::EQ,
+            IntPredicate::Ne => Self::NE,
+            IntPredicate::Ugt => Self::UGT,
+            IntPredicate::Uge => Self::UGE,
+            IntPredicate::Ult => Self::ULT,
+            IntPredicate::Ule => Self::ULE,
+            IntPredicate::Sgt => Self::SGT,
+            IntPredicate::Sge => Self::SGE,
+            IntPredicate::Slt => Self::SLT,
+            IntPredicate::Sle => Self::SLE,
+        }
+    }
+}
+
+/// A floating-point comparison instruction.
+#[pyclass(extends = Instruction)]
+pub(crate) struct FCmp;
+
+#[pymethods]
+impl FCmp {
+    /// The comparison predicate.
+    ///
+    /// :type: FloatPredicate
+    #[getter]
+    fn predicate(slf: PyRef<Self>) -> FloatPredicate {
+        slf.into_super().0.get_fcmp_predicate().unwrap().into()
+    }
+}
+
+/// A floating-point comparison predicate.
+#[pyclass]
+#[derive(Clone)]
+pub(crate) enum FloatPredicate {
+    #[pyo3(name = "FALSE")]
+    False,
+    #[pyo3(name = "OEQ")]
+    Oeq,
+    #[pyo3(name = "OGT")]
+    Ogt,
+    #[pyo3(name = "OGE")]
+    Oge,
+    #[pyo3(name = "OLT")]
+    Olt,
+    #[pyo3(name = "OLE")]
+    Ole,
+    #[pyo3(name = "ONE")]
+    One,
+    #[pyo3(name = "ORD")]
+    Ord,
+    #[pyo3(name = "UNO")]
+    Uno,
+    #[pyo3(name = "UEQ")]
+    Ueq,
+    #[pyo3(name = "UGT")]
+    Ugt,
+    #[pyo3(name = "UGE")]
+    Uge,
+    #[pyo3(name = "ULT")]
+    Ult,
+    #[pyo3(name = "ULE")]
+    Ule,
+    #[pyo3(name = "UNE")]
+    Une,
+    #[pyo3(name = "TRUE")]
+    True,
+}
+
+impl From<inkwell::FloatPredicate> for FloatPredicate {
+    fn from(pred: inkwell::FloatPredicate) -> Self {
+        match pred {
+            inkwell::FloatPredicate::OEQ => Self::Oeq,
+            inkwell::FloatPredicate::OGE => Self::Oge,
+            inkwell::FloatPredicate::OGT => Self::Ogt,
+            inkwell::FloatPredicate::OLE => Self::Ole,
+            inkwell::FloatPredicate::OLT => Self::Olt,
+            inkwell::FloatPredicate::ONE => Self::One,
+            inkwell::FloatPredicate::ORD => Self::Ord,
+            inkwell::FloatPredicate::PredicateFalse => Self::False,
+            inkwell::FloatPredicate::PredicateTrue => Self::True,
+            inkwell::FloatPredicate::UEQ => Self::Ueq,
+            inkwell::FloatPredicate::UGE => Self::Uge,
+            inkwell::FloatPredicate::UGT => Self::Ugt,
+            inkwell::FloatPredicate::ULE => Self::Ule,
+            inkwell::FloatPredicate::ULT => Self::Ult,
+            inkwell::FloatPredicate::UNE => Self::Une,
+            inkwell::FloatPredicate::UNO => Self::Uno,
+        }
+    }
+}
+
+/// A call instruction.
+#[pyclass(extends = Instruction, unsendable)]
+pub(crate) struct Call;
+
+#[pymethods]
+impl Call {
+    /// The value being called.
+    ///
+    /// :type: Value
+    #[getter]
+    fn callee(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let inst = slf.into_super();
+        let last = inst.0.get_operand(inst.0.get_num_operands() - 1);
+        let callee = last.unwrap().left().unwrap();
+        let context = inst.into_super().context().clone();
+        unsafe { Value::from_any(py, context, callee) }
+    }
+
+    /// The arguments to the call.
+    ///
+    /// :type: List[Value]
+    #[getter]
+    fn args(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let inst = slf.into_super();
+        let mut args = Instruction::operands(inst, py)?;
+        args.pop().unwrap();
+        Ok(args)
+    }
+}
+
+/// A phi node instruction.
+#[pyclass(extends = Instruction, unsendable)]
+pub(crate) struct Phi(PhiValue<'static>);
+
+#[pymethods]
+impl Phi {
+    /// The incoming values and their preceding basic blocks.
+    ///
+    /// :type: List[Tuple[Value, BasicBlock]]
+    #[getter]
+    fn incoming(slf: PyRef<Self>, py: Python) -> PyResult<Vec<(PyObject, PyObject)>> {
+        let phi = slf.0;
+        let value = slf.into_super().into_super();
+        let context = value.context();
+
+        (0..phi.count_incoming())
+            .map(|i| {
+                let (value, block) = phi.get_incoming(i).unwrap();
+                let value = unsafe { Value::from_any(py, context.clone(), value)? };
+                let block = unsafe { Value::from_any(py, context.clone(), block)? };
+                Ok((value, block))
+            })
+            .collect()
+    }
+}
diff --git a/pyqir/src/lib.rs b/pyqir/src/lib.rs
index 847f6ee7..512fb837 100644
--- a/pyqir/src/lib.rs
+++ b/pyqir/src/lib.rs
@@ -1,9 +1,32 @@
 // Copyright (c) Microsoft Corporation.
 // Licensed under the MIT License.
+//
+// To store Inkwell objects in Python classes, we transmute the `'ctx` lifetime to static. You need
+// to be careful when using Inkwell types with unsafely extended lifetimes. Follow these rules:
+//
+// 1. When storing in a data type, always include a `Py<Context>` field containing the context
+//    originally referred to by `'ctx`.
+// 2. Before calling Inkwell methods that use `'ctx`, call `context::require_same` to assert that
+//    all contexts being used are the same.
 
 #![warn(clippy::all, clippy::pedantic)]
 
+#[cfg(feature = "llvm11-0")]
+extern crate llvm_sys_110 as llvm_sys;
+#[cfg(feature = "llvm12-0")]
+extern crate llvm_sys_120 as llvm_sys;
+#[cfg(feature = "llvm13-0")]
+extern crate llvm_sys_130 as llvm_sys;
+#[cfg(feature = "llvm14-0")]
+extern crate llvm_sys_140 as llvm_sys;
+
+mod builder;
+mod context;
 mod evaluator;
-mod generator;
+mod instructions;
+mod module;
 mod python;
-mod utils;
+mod qis;
+mod simple;
+mod types;
+mod values;
diff --git a/pyqir/src/module.rs b/pyqir/src/module.rs
new file mode 100644
index 00000000..369b2f48
--- /dev/null
+++ b/pyqir/src/module.rs
@@ -0,0 +1,137 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::{context::Context, values::Value};
+use inkwell::memory_buffer::MemoryBuffer;
+use pyo3::{exceptions::PyValueError, prelude::*, types::PyBytes};
+use std::mem::transmute;
+
+/// A module.
+#[pyclass(unsendable)]
+pub(crate) struct Module {
+    module: inkwell::module::Module<'static>,
+    context: Py<Context>,
+}
+
+#[pymethods]
+impl Module {
+    /// Creates a module from LLVM IR.
+    ///
+    /// :param str ir: The LLVM IR for a module.
+    /// :param Optional[str] name: The name of the module.
+    /// :rtype: Module
+    /// :returns: The module.
+    #[staticmethod]
+    #[pyo3(text_signature = "(ir, name=\"\")")]
+    fn from_ir(py: Python, ir: &str, name: Option<&str>) -> PyResult<Self> {
+        let context = Context::new();
+        let buffer =
+            MemoryBuffer::create_from_memory_range(ir.as_bytes(), name.unwrap_or_default());
+        let module = context
+            .create_module_from_ir(buffer)
+            .map_err(|e| PyValueError::new_err(e.to_string()))?;
+        Ok(Self {
+            module: unsafe {
+                transmute::<inkwell::module::Module<'_>, inkwell::module::Module<'static>>(module)
+            },
+            context: Py::new(py, context)?,
+        })
+    }
+
+    /// Creates a module from LLVM bitcode.
+    ///
+    /// :param bytes bitcode: The LLVM bitcode for a module.
+    /// :param Optional[str] name: The name of the module.
+    /// :rtype: Module
+    /// :returns: The module.
+    #[staticmethod]
+    #[pyo3(text_signature = "(bitcode, name=\"\")")]
+    fn from_bitcode(py: Python, bitcode: &[u8], name: Option<&str>) -> PyResult<Self> {
+        let context = Context::new();
+        let buffer = MemoryBuffer::create_from_memory_range(bitcode, name.unwrap_or_default());
+        let module = inkwell::module::Module::parse_bitcode_from_buffer(&buffer, &context)
+            .map_err(|e| PyValueError::new_err(e.to_string()))?;
+        Ok(Self {
+            module: unsafe {
+                transmute::<inkwell::module::Module<'_>, inkwell::module::Module<'static>>(module)
+            },
+            context: Py::new(py, context)?,
+        })
+    }
+
+    /// The name of the original source file that this module was compiled from.
+    ///
+    /// :type: str
+    #[getter]
+    fn source_filename(&self) -> &str {
+        self.module
+            .get_source_file_name()
+            .to_str()
+            .expect("Name is not valid UTF-8.")
+    }
+
+    #[setter]
+    fn set_source_filename(&self, value: &str) {
+        self.module.set_source_file_name(value);
+    }
+
+    /// The functions declared in this module.
+    ///
+    /// :type: List[Function]
+    #[getter]
+    fn functions(&self, py: Python) -> PyResult<Vec<PyObject>> {
+        self.module
+            .get_functions()
+            .map(|f| unsafe { Value::from_any(py, self.context.clone(), f) })
+            .collect()
+    }
+
+    /// The LLVM bitcode for this module.
+    ///
+    /// :type: bytes
+    #[getter]
+    fn bitcode<'py>(&self, py: Python<'py>) -> &'py PyBytes {
+        PyBytes::new(py, self.module.write_bitcode_to_memory().as_slice())
+    }
+
+    fn __str__(&self) -> String {
+        self.module.to_string()
+    }
+}
+
+impl Module {
+    pub(crate) fn new(py: Python, context: Py<Context>, name: &str) -> Self {
+        let module = {
+            let context = context.borrow(py);
+            let module = context.create_module(name);
+            unsafe {
+                transmute::<inkwell::module::Module<'_>, inkwell::module::Module<'static>>(module)
+            }
+        };
+        Self { module, context }
+    }
+
+    pub(crate) unsafe fn get(&self) -> &inkwell::module::Module<'static> {
+        &self.module
+    }
+
+    pub(crate) fn context(&self) -> &Py<Context> {
+        &self.context
+    }
+}
+
+/// An attribute.
+#[pyclass(unsendable)]
+pub(crate) struct Attribute(pub(crate) inkwell::attributes::Attribute);
+
+#[pymethods]
+impl Attribute {
+    /// The value of the attribute as a string.
+    #[getter]
+    fn value(&self) -> &str {
+        self.0
+            .get_string_value()
+            .to_str()
+            .expect("Value is not valid UTF-8.")
+    }
+}
diff --git a/pyqir/src/python.rs b/pyqir/src/python.rs
index 028c1a95..c8730b6f 100644
--- a/pyqir/src/python.rs
+++ b/pyqir/src/python.rs
@@ -2,25 +2,62 @@
 // Licensed under the MIT License.
 
 use crate::{
+    builder::Builder,
     evaluator::PyNonadaptiveJit,
-    generator::{
-        bitcode_to_ir, ir_to_bitcode, r#const, BasicQisBuilder, Builder, SimpleModule, Type,
-        TypeFactory, Value,
+    instructions::{
+        Call, FCmp, FloatPredicate, ICmp, Instruction, IntPredicate, Opcode, Phi, Switch,
+    },
+    module::{Attribute, Module},
+    qis::BasicQisBuilder,
+    simple::{SimpleModule, TypeFactory},
+    types::{is_qubit, is_result, ArrayType, FunctionType, IntType, PointerType, StructType, Type},
+    values::{
+        constant_bytes, is_entry_point, is_interop_friendly, qubit_id, r#const,
+        required_num_qubits, required_num_results, result_id, BasicBlock, Constant, FloatConstant,
+        Function, IntConstant, Value,
     },
 };
 use pyo3::prelude::*;
 
 #[pymodule]
 fn _native(_py: Python, m: &PyModule) -> PyResult<()> {
+    m.add_class::<ArrayType>()?;
+    m.add_class::<Attribute>()?;
+    m.add_class::<BasicBlock>()?;
     m.add_class::<BasicQisBuilder>()?;
     m.add_class::<Builder>()?;
+    m.add_class::<Call>()?;
+    m.add_class::<Constant>()?;
+    m.add_class::<FCmp>()?;
+    m.add_class::<FloatConstant>()?;
+    m.add_class::<FloatPredicate>()?;
+    m.add_class::<Function>()?;
+    m.add_class::<FunctionType>()?;
+    m.add_class::<ICmp>()?;
+    m.add_class::<Instruction>()?;
+    m.add_class::<IntConstant>()?;
+    m.add_class::<IntPredicate>()?;
+    m.add_class::<IntType>()?;
+    m.add_class::<Module>()?;
+    m.add_class::<Opcode>()?;
+    m.add_class::<Phi>()?;
+    m.add_class::<PointerType>()?;
     m.add_class::<PyNonadaptiveJit>()?;
     m.add_class::<SimpleModule>()?;
+    m.add_class::<StructType>()?;
+    m.add_class::<Switch>()?;
     m.add_class::<Type>()?;
     m.add_class::<TypeFactory>()?;
     m.add_class::<Value>()?;
-    m.add_function(wrap_pyfunction!(bitcode_to_ir, m)?)?;
-    m.add_function(wrap_pyfunction!(ir_to_bitcode, m)?)?;
+    m.add_function(wrap_pyfunction!(constant_bytes, m)?)?;
+    m.add_function(wrap_pyfunction!(is_entry_point, m)?)?;
+    m.add_function(wrap_pyfunction!(is_interop_friendly, m)?)?;
+    m.add_function(wrap_pyfunction!(is_qubit, m)?)?;
+    m.add_function(wrap_pyfunction!(is_result, m)?)?;
+    m.add_function(wrap_pyfunction!(qubit_id, m)?)?;
     m.add_function(wrap_pyfunction!(r#const, m)?)?;
+    m.add_function(wrap_pyfunction!(required_num_qubits, m)?)?;
+    m.add_function(wrap_pyfunction!(required_num_results, m)?)?;
+    m.add_function(wrap_pyfunction!(result_id, m)?)?;
     Ok(())
 }
diff --git a/pyqir/src/qis.rs b/pyqir/src/qis.rs
new file mode 100644
index 00000000..5e8452e5
--- /dev/null
+++ b/pyqir/src/qis.rs
@@ -0,0 +1,305 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::{
+    builder::Builder,
+    context,
+    values::{self, Value},
+};
+use pyo3::prelude::*;
+use qirlib::BuilderBasicQisExt;
+use std::convert::TryInto;
+
+/// An instruction builder that generates instructions from the basic quantum instruction set.
+///
+/// :param Builder builder: The underlying builder used to build QIS instructions.
+#[pyclass]
+#[pyo3(text_signature = "(builder)")]
+pub(crate) struct BasicQisBuilder {
+    builder: Py<Builder>,
+}
+
+#[pymethods]
+impl BasicQisBuilder {
+    #[new]
+    fn new(builder: Py<Builder>) -> Self {
+        BasicQisBuilder { builder }
+    }
+
+    /// Inserts a controlled Pauli :math:`X` gate.
+    ///
+    /// :param Value control: The control qubit.
+    /// :param Value target: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, control, target)")]
+    fn cx(&self, py: Python, control: &Value, target: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), control.context(), target.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_cx(
+            unsafe { control.get() }.try_into()?,
+            unsafe { target.get() }.try_into()?,
+        );
+        Ok(())
+    }
+
+    /// Inserts a controlled Pauli :math:`Z` gate.
+    ///
+    /// :param Value control: The control qubit.
+    /// :param Value target: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, control, target)")]
+    fn cz(&self, py: Python, control: &Value, target: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), control.context(), target.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_cz(
+            unsafe { control.get() }.try_into()?,
+            unsafe { target.get() }.try_into()?,
+        );
+        Ok(())
+    }
+
+    /// Inserts a Hadamard gate.
+    ///
+    /// :param qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn h(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_h(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a Z-basis measurement operation.
+    ///
+    /// :param Value qubit: The qubit to measure.
+    /// :param Value result: A result where the measurement result will be written to.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit, result)")]
+    fn mz(&self, py: Python, qubit: &Value, result: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context(), result.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_mz(
+            unsafe { qubit.get() }.try_into()?,
+            unsafe { result.get() }.try_into()?,
+        );
+        Ok(())
+    }
+
+    /// Inserts a reset operation.
+    ///
+    /// :param Value qubit: The qubit to reset.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn reset(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_reset(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a rotation gate about the :math:`x` axis.
+    ///
+    /// :param Union[Value, float] theta: The angle to rotate by.
+    /// :param Value qubit: The qubit to rotate.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, theta, qubit)")]
+    fn rx(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(
+            py,
+            values::extract_contexts([theta])
+                .chain([builder.context().clone(), qubit.context().clone()]),
+        )?;
+
+        let context = builder.context().borrow(py);
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        let theta = unsafe { values::extract_any(&context.f64_type(), theta)? };
+        builder.build_rx(theta.try_into()?, unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a rotation gate about the :math:`y` axis.
+    ///
+    /// :param Union[Value, float] theta: The angle to rotate by.
+    /// :param Value qubit: The qubit to rotate.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, theta, qubit)")]
+    fn ry(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(
+            py,
+            values::extract_contexts([theta])
+                .chain([builder.context().clone(), qubit.context().clone()]),
+        )?;
+
+        let context = builder.context().borrow(py);
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        let theta = unsafe { values::extract_any(&context.f64_type(), theta)? };
+        builder.build_ry(theta.try_into()?, unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a rotation gate about the :math:`z` axis.
+    ///
+    /// :param Union[Value, float] theta: The angle to rotate by.
+    /// :param Value qubit: The qubit to rotate.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, theta, qubit)")]
+    fn rz(&self, py: Python, theta: &PyAny, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(
+            py,
+            values::extract_contexts([theta])
+                .chain([builder.context().clone(), qubit.context().clone()]),
+        )?;
+
+        let context = builder.context().borrow(py);
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        let theta = unsafe { values::extract_any(&context.f64_type(), theta)? };
+        builder.build_rz(theta.try_into()?, unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts an :math:`S` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn s(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_s(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts an adjoint :math:`S` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn s_adj(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_s_adj(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a :math:`T` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn t(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_t(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts an adjoint :math:`T` gate.
+    ///
+    /// :param qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn t_adj(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_t_adj(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a Pauli :math:`X` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn x(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_x(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a Pauli :math:`Y` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn y(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_y(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a Pauli :math:`Z` gate.
+    ///
+    /// :param Value qubit: The target qubit.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, qubit)")]
+    fn z(&self, py: Python, qubit: &Value) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), qubit.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.build_z(unsafe { qubit.get() }.try_into()?);
+        Ok(())
+    }
+
+    /// Inserts a branch conditioned on a measurement result.
+    ///
+    /// Instructions inserted when ``one`` is called will be inserted into the one branch.
+    /// Instructions inserted when ``zero`` is called will be inserted into the zero branch. The one
+    /// and zero callables should use this module's builder to build instructions.
+    ///
+    /// :param Value cond: The result condition to branch on.
+    /// :param Callable[[], None] one:
+    ///     A callable that inserts instructions for the branch where the result is one.
+    /// :param Callable[[], None] zero:
+    ///     A callable that inserts instructions for the branch where the result is zero.
+    /// :rtype: None
+    #[pyo3(text_signature = "(self, cond, one, zero)")]
+    fn if_result(
+        &self,
+        py: Python,
+        cond: &Value,
+        one: Option<&PyAny>,
+        zero: Option<&PyAny>,
+    ) -> PyResult<()> {
+        let builder = self.builder.borrow(py);
+        context::require_same(py, [builder.context(), cond.context()])?;
+        let module = builder.module().borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        builder.try_build_if_result(
+            unsafe { cond.get() }.try_into()?,
+            |_| one.iter().try_for_each(|f| f.call0().map(|_| ())),
+            |_| zero.iter().try_for_each(|f| f.call0().map(|_| ())),
+        )
+    }
+}
diff --git a/pyqir/src/simple.rs b/pyqir/src/simple.rs
new file mode 100644
index 00000000..4ea2ca88
--- /dev/null
+++ b/pyqir/src/simple.rs
@@ -0,0 +1,299 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::{
+    builder::Builder,
+    context::{self, Context},
+    module::Module,
+    types::Type,
+    values::Value,
+};
+use inkwell::types::{AnyType, AnyTypeEnum, BasicType, BasicTypeEnum, FunctionType};
+use pyo3::{
+    exceptions::{PyOSError, PyUnicodeDecodeError, PyValueError},
+    prelude::*,
+    types::PyBytes,
+};
+use qirlib::{module, types};
+use std::{
+    convert::{Into, TryFrom},
+    mem::transmute,
+};
+
+/// A simple module represents an executable program with these restrictions:
+///
+/// - There is one global qubit register and one global result register. Both are statically
+///   allocated with a fixed size.
+/// - There is only a single function that runs as the entry point.
+///
+/// :param str name: The name of the module.
+/// :param int num_qubits: The number of statically allocated qubits.
+/// :param int num_results: The number of statically allocated results.
+#[pyclass(unsendable)]
+#[pyo3(text_signature = "(name, num_qubits, num_results)")]
+pub(crate) struct SimpleModule {
+    module: Py<Module>,
+    builder: Py<Builder>,
+    types: Py<TypeFactory>,
+    num_qubits: u64,
+    num_results: u64,
+}
+
+#[pymethods]
+impl SimpleModule {
+    #[new]
+    fn new(py: Python, name: &str, num_qubits: u64, num_results: u64) -> PyResult<SimpleModule> {
+        let context = Py::new(py, Context::new())?;
+        let module = Py::new(py, Module::new(py, context, name))?;
+        let builder = Py::new(py, Builder::new(py, module.clone()))?;
+
+        {
+            let builder = builder.borrow(py);
+            let module = module.borrow(py);
+            unsafe { module::simple_init(module.get(), builder.get(), num_qubits, num_results) };
+        }
+
+        let types = Py::new(
+            py,
+            TypeFactory {
+                module: module.clone(),
+            },
+        )?;
+
+        Ok(SimpleModule {
+            module,
+            builder,
+            types,
+            num_qubits,
+            num_results,
+        })
+    }
+
+    #[getter]
+    fn types(&self) -> Py<TypeFactory> {
+        self.types.clone()
+    }
+
+    /// The global qubit register.
+    ///
+    /// :type: Tuple[Value, ...]
+    #[getter]
+    fn qubits(&self, py: Python) -> PyResult<Vec<PyObject>> {
+        let builder = self.builder.borrow(py);
+        let module = self.module.borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        (0..self.num_qubits)
+            .map(|id| unsafe {
+                Value::from_any(py, module.context().clone(), builder.build_qubit(id))
+            })
+            .collect()
+    }
+
+    /// The global result register.
+    ///
+    /// :type: Tuple[Value, ...]
+    #[getter]
+    fn results(&self, py: Python) -> PyResult<Vec<PyObject>> {
+        let builder = self.builder.borrow(py);
+        let module = self.module.borrow(py);
+        let builder = unsafe { qirlib::Builder::from(builder.get(), module.get()) };
+        (0..self.num_results)
+            .map(|id| unsafe {
+                Value::from_any(py, module.context().clone(), builder.build_result(id))
+            })
+            .collect()
+    }
+
+    /// The instruction builder.
+    ///
+    /// :type: Builder
+    #[getter]
+    fn builder(&self) -> Py<Builder> {
+        self.builder.clone()
+    }
+
+    /// Emits the LLVM IR for the module as plain text.
+    ///
+    /// :rtype: str
+    fn ir(&self, py: Python) -> PyResult<String> {
+        self.emit(py, |m| m.print_to_string().to_string())
+    }
+
+    /// Emits the LLVM bitcode for the module as a sequence of bytes.
+    ///
+    /// :rtype: bytes
+    fn bitcode<'py>(&self, py: Python<'py>) -> PyResult<&'py PyBytes> {
+        self.emit(py, |m| {
+            PyBytes::new(py, m.write_bitcode_to_memory().as_slice())
+        })
+    }
+
+    /// Adds a declaration for an externally linked function to the module.
+    ///
+    /// :param str name: The name of the function.
+    /// :param Type ty: The type of the function.
+    /// :return: The function value.
+    /// :rtype: Function
+    #[pyo3(text_signature = "(self, name, ty)")]
+    fn add_external_function(&mut self, py: Python, name: &str, ty: &Type) -> PyResult<PyObject> {
+        let module = self.module.borrow(py);
+        context::require_same(py, [module.context(), ty.context()])?;
+
+        let context = ty.context().clone();
+        let ty = unsafe { transmute::<AnyTypeEnum<'_>, AnyTypeEnum<'static>>(ty.get()) }
+            .into_function_type();
+        let function = unsafe { module.get() }.add_function(name, ty, None);
+        unsafe { Value::from_any(py, context, function) }
+    }
+}
+
+impl SimpleModule {
+    fn emit<T>(&self, py: Python, f: impl Fn(&inkwell::module::Module) -> T) -> PyResult<T> {
+        let module = self.module.borrow(py);
+        let builder = self.builder.borrow(py);
+        let ret = unsafe { builder.get() }.build_return(None);
+        let new_context = inkwell::context::Context::create();
+        let new_module = clone_module(unsafe { module.get() }, &new_context)?;
+        ret.erase_from_basic_block();
+        module::simple_finalize(&new_module).map_err(PyOSError::new_err)?;
+        Ok(f(&new_module))
+    }
+}
+
+/// Provides access to all supported types.
+#[pyclass]
+pub(crate) struct TypeFactory {
+    module: Py<Module>,
+}
+
+#[pymethods]
+impl TypeFactory {
+    /// The void type.
+    ///
+    /// :type: Type
+    #[getter]
+    fn void(&self, py: Python) -> PyResult<PyObject> {
+        self.new_type(py, |m| m.get_context().void_type().into())
+    }
+
+    /// The boolean type.
+    ///
+    /// :type: Type
+    #[getter]
+    fn bool(&self, py: Python) -> PyResult<PyObject> {
+        self.new_type(py, |m| m.get_context().bool_type().into())
+    }
+
+    /// An integer type.
+    ///
+    /// :param int width: The number of bits in the integers.
+    /// :returns: The integer type.
+    /// :rtype: Type
+    #[pyo3(text_signature = "(width)")]
+    fn int(&self, py: Python, width: u32) -> PyResult<PyObject> {
+        self.new_type(py, |m| m.get_context().custom_width_int_type(width).into())
+    }
+
+    /// The double type.
+    ///
+    /// :type: Type
+    #[getter]
+    fn double(&self, py: Python) -> PyResult<PyObject> {
+        self.new_type(py, |m| m.get_context().f64_type().into())
+    }
+
+    /// The qubit type.
+    ///
+    /// :type: Type
+    #[getter]
+    fn qubit(&self, py: Python) -> PyResult<PyObject> {
+        self.new_type(py, |m| types::qubit(m).into())
+    }
+
+    /// The measurement result type.
+    ///
+    /// :type: Type
+    #[getter]
+    fn result(&self, py: Python) -> PyResult<PyObject> {
+        self.new_type(py, |m| types::result(m).into())
+    }
+
+    /// A function type.
+    ///
+    /// :param Type ret: The return type.
+    /// :param List[Type] params: The parameter types.
+    /// :returns: The function type.
+    /// :rtype: Type
+    #[staticmethod]
+    #[pyo3(text_signature = "(ret, params)")]
+    #[allow(clippy::needless_pass_by_value)]
+    fn function(py: Python, ret: &Type, params: Vec<Py<Type>>) -> PyResult<PyObject> {
+        context::require_same(
+            py,
+            params
+                .iter()
+                .map(|t| t.borrow(py).context().clone())
+                .chain([ret.context().clone()]),
+        )?;
+
+        let ty = function_type(
+            unsafe { &ret.get() },
+            params.iter().map(|t| unsafe {
+                transmute::<AnyTypeEnum<'_>, AnyTypeEnum<'static>>(t.borrow(py).get())
+            }),
+        )
+        .ok_or_else(|| PyValueError::new_err("Invalid return or parameter type."))?;
+
+        unsafe { Type::from_any(py, ret.context().clone(), ty.into()) }
+    }
+}
+
+impl TypeFactory {
+    fn new_type(
+        &self,
+        py: Python,
+        f: impl for<'ctx> Fn(&inkwell::module::Module<'ctx>) -> AnyTypeEnum<'ctx>,
+    ) -> PyResult<PyObject> {
+        let module = self.module.borrow(py);
+        let context = module.context().clone();
+        let ty = f(unsafe { module.get() });
+        unsafe { Type::from_any(py, context, ty) }
+    }
+}
+
+fn function_type<'ctx>(
+    return_type: &impl AnyType<'ctx>,
+    params: impl IntoIterator<Item = AnyTypeEnum<'ctx>>,
+) -> Option<FunctionType<'ctx>> {
+    let params = params
+        .into_iter()
+        .map(|ty| BasicTypeEnum::try_from(ty).map(Into::into).ok())
+        .collect::<Option<Vec<_>>>()?;
+
+    match return_type.as_any_type_enum() {
+        AnyTypeEnum::VoidType(void) => Some(void.fn_type(&params, false)),
+        any => BasicTypeEnum::try_from(any)
+            .map(|basic| basic.fn_type(&params, false))
+            .ok(),
+    }
+}
+
+fn clone_module<'ctx>(
+    module: &inkwell::module::Module,
+    context: &'ctx inkwell::context::Context,
+) -> PyResult<inkwell::module::Module<'ctx>> {
+    let name = module
+        .get_name()
+        .to_str()
+        .map_err(PyUnicodeDecodeError::new_err)?;
+    let bitcode = module.write_bitcode_to_memory();
+    let new_module = inkwell::module::Module::parse_bitcode_from_buffer(&bitcode, context)
+        .map_err(|e| {
+            module.verify().err().map_or_else(
+                || PyOSError::new_err(e.to_string()),
+                |e| PyOSError::new_err(e.to_string()),
+            )
+        })?;
+    new_module.set_name(name);
+    Ok(new_module)
+}
diff --git a/pyqir/src/types.rs b/pyqir/src/types.rs
new file mode 100644
index 00000000..80a764ba
--- /dev/null
+++ b/pyqir/src/types.rs
@@ -0,0 +1,234 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::used_underscore_binding)]
+
+use crate::context::Context;
+use inkwell::types::{AnyTypeEnum, BasicTypeEnum};
+use pyo3::{conversion::ToPyObject, prelude::*};
+use qirlib::types;
+use std::mem::transmute;
+
+/// A type.
+#[pyclass(subclass, unsendable)]
+pub(crate) struct Type {
+    ty: AnyTypeEnum<'static>,
+    context: Py<Context>,
+}
+
+#[pymethods]
+impl Type {
+    /// Whether this type is the void type.
+    ///
+    /// :type: bool
+    #[getter]
+    fn is_void(&self) -> bool {
+        self.ty.is_void_type()
+    }
+
+    /// Whether this type is the bool type.
+    ///
+    /// :type: bool
+    #[getter]
+    fn is_double(&self) -> bool {
+        match self.ty {
+            AnyTypeEnum::FloatType(float) => {
+                float.size_of().get_zero_extended_constant() == Some(64)
+            }
+            _ => false,
+        }
+    }
+}
+
+impl Type {
+    pub(crate) unsafe fn from_any(
+        py: Python,
+        context: Py<Context>,
+        ty: AnyTypeEnum,
+    ) -> PyResult<PyObject> {
+        let ty = transmute::<AnyTypeEnum<'_>, AnyTypeEnum<'static>>(ty);
+        let base = PyClassInitializer::from(Self { ty, context });
+        Ok(match ty {
+            AnyTypeEnum::ArrayType(a) => {
+                Py::new(py, base.add_subclass(ArrayType(a)))?.to_object(py)
+            }
+            AnyTypeEnum::FunctionType(f) => {
+                Py::new(py, base.add_subclass(FunctionType(f)))?.to_object(py)
+            }
+            AnyTypeEnum::IntType(i) => Py::new(py, base.add_subclass(IntType(i)))?.to_object(py),
+            AnyTypeEnum::PointerType(p) => {
+                Py::new(py, base.add_subclass(PointerType(p)))?.to_object(py)
+            }
+            AnyTypeEnum::StructType(s) => {
+                Py::new(py, base.add_subclass(StructType(s)))?.to_object(py)
+            }
+            AnyTypeEnum::FloatType(_) | AnyTypeEnum::VectorType(_) | AnyTypeEnum::VoidType(_) => {
+                Py::new(py, base)?.to_object(py)
+            }
+        })
+    }
+
+    pub(crate) unsafe fn get(&self) -> AnyTypeEnum<'static> {
+        self.ty
+    }
+
+    pub(crate) fn context(&self) -> &Py<Context> {
+        &self.context
+    }
+}
+
+/// An integer type.
+#[pyclass(extends = Type, unsendable)]
+pub(crate) struct IntType(inkwell::types::IntType<'static>);
+
+#[pymethods]
+impl IntType {
+    /// The number of bits in the integer.
+    ///
+    /// :type: int
+    #[getter]
+    fn width(&self) -> u32 {
+        self.0.get_bit_width()
+    }
+}
+
+/// A function type.
+#[pyclass(extends = Type, unsendable)]
+pub(crate) struct FunctionType(inkwell::types::FunctionType<'static>);
+
+#[pymethods]
+impl FunctionType {
+    /// The return type of the function.
+    ///
+    /// :type: Type
+    #[getter]
+    fn ret(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let ty = basic_to_any(slf.0.get_return_type().unwrap());
+        let context = slf.into_super().context.clone();
+        unsafe { Type::from_any(py, context, ty) }
+    }
+
+    /// The types of the function parameters.
+    ///
+    /// :type: List[Type]
+    #[getter]
+    fn params(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let params = slf.0.get_param_types();
+        let context = &slf.into_super().context;
+        params
+            .into_iter()
+            .map(|ty| unsafe { Type::from_any(py, context.clone(), basic_to_any(ty)) })
+            .collect()
+    }
+}
+
+/// A structure type.
+#[pyclass(extends = Type, unsendable)]
+pub(crate) struct StructType(inkwell::types::StructType<'static>);
+
+#[pymethods]
+impl StructType {
+    /// The name of the structure or the empty string if the structure is anonymous.
+    #[getter]
+    fn name(&self) -> Option<&str> {
+        self.0
+            .get_name()
+            .map(|n| n.to_str().expect("Name is not valid UTF-8."))
+    }
+
+    /// The types of the structure fields.
+    ///
+    /// :type: List[Type]
+    #[getter]
+    fn fields(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let fields = slf.0.get_field_types();
+        let context = &slf.into_super().context;
+        fields
+            .into_iter()
+            .map(|ty| unsafe { Type::from_any(py, context.clone(), basic_to_any(ty)) })
+            .collect()
+    }
+}
+
+/// An array type.
+#[pyclass(extends = Type, unsendable)]
+pub(crate) struct ArrayType(inkwell::types::ArrayType<'static>);
+
+#[pymethods]
+impl ArrayType {
+    /// The type of the array elements.
+    ///
+    /// :type: Type
+    #[getter]
+    fn element(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let ty = basic_to_any(slf.0.get_element_type());
+        let context = slf.into_super().context.clone();
+        unsafe { Type::from_any(py, context, ty) }
+    }
+
+    /// The number of elements in the a rray.
+    ///
+    /// :type: int
+    #[getter]
+    fn count(&self) -> u32 {
+        self.0.len()
+    }
+}
+
+/// A pointer type.
+#[pyclass(extends = Type, unsendable)]
+pub(crate) struct PointerType(inkwell::types::PointerType<'static>);
+
+#[pymethods]
+impl PointerType {
+    /// The type being pointed to.
+    ///
+    /// :type: Type
+    #[getter]
+    fn pointee(slf: PyRef<Self>, py: Python) -> PyResult<PyObject> {
+        let ty = slf.0.get_element_type();
+        let context = slf.into_super().context.clone();
+        unsafe { Type::from_any(py, context, ty) }
+    }
+
+    /// The pointer address space.
+    ///
+    /// :type: int
+    #[getter]
+    fn address_space(&self) -> u32 {
+        self.0.get_address_space() as u32
+    }
+}
+
+/// Whether the type is the QIR qubit type.
+///
+/// :param Type ty: The type.
+/// :rtype: bool
+/// :returns: True if the type is the QIR qubit type.
+#[pyfunction]
+#[pyo3(text_signature = "(ty)")]
+pub(crate) fn is_qubit(ty: &Type) -> bool {
+    types::is_qubit(ty.ty)
+}
+
+/// Whether the type is the QIR result type.
+///
+/// :param Type ty: The type.
+/// :rtype: bool
+/// :returns: True if the type is the QIR result type.
+#[pyfunction]
+#[pyo3(text_signature = "(ty)")]
+pub(crate) fn is_result(ty: &Type) -> bool {
+    types::is_result(ty.ty)
+}
+
+fn basic_to_any(ty: BasicTypeEnum) -> AnyTypeEnum {
+    match ty {
+        BasicTypeEnum::ArrayType(a) => a.into(),
+        BasicTypeEnum::FloatType(f) => f.into(),
+        BasicTypeEnum::IntType(i) => i.into(),
+        BasicTypeEnum::PointerType(p) => p.into(),
+        BasicTypeEnum::StructType(s) => s.into(),
+        BasicTypeEnum::VectorType(v) => v.into(),
+    }
+}
diff --git a/pyqir/src/utils.rs b/pyqir/src/utils.rs
deleted file mode 100644
index 3e08d46d..00000000
--- a/pyqir/src/utils.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-use inkwell::{
-    context::Context,
-    module::Module,
-    types::{AnyType, AnyTypeEnum, BasicType, BasicTypeEnum, FunctionType},
-    values::{AnyValueEnum, BasicMetadataValueEnum, CallableValue},
-};
-use pyo3::{
-    exceptions::{PyOSError, PyTypeError, PyUnicodeDecodeError},
-    prelude::*,
-    PyClass,
-};
-use std::{
-    borrow::Borrow,
-    convert::{Into, TryFrom},
-};
-
-pub(crate) fn extract_constant<'ctx>(
-    ty: &impl AnyType<'ctx>,
-    ob: &PyAny,
-) -> PyResult<AnyValueEnum<'ctx>> {
-    match ty.as_any_type_enum() {
-        AnyTypeEnum::IntType(int) => Ok(int.const_int(ob.extract()?, true).into()),
-        AnyTypeEnum::FloatType(float) => Ok(float.const_float(ob.extract()?).into()),
-        _ => Err(PyTypeError::new_err(
-            "Can't convert Python value into this type.",
-        )),
-    }
-}
-
-pub(crate) fn function_type<'ctx>(
-    return_type: &impl AnyType<'ctx>,
-    params: impl IntoIterator<Item = AnyTypeEnum<'ctx>>,
-) -> Option<FunctionType<'ctx>> {
-    let params = params
-        .into_iter()
-        .map(|ty| BasicTypeEnum::try_from(ty).map(Into::into).ok())
-        .collect::<Option<Vec<_>>>()?;
-
-    match return_type.as_any_type_enum() {
-        AnyTypeEnum::VoidType(void) => Some(void.fn_type(&params, false)),
-        any => BasicTypeEnum::try_from(any)
-            .map(|basic| basic.fn_type(&params, false))
-            .ok(),
-    }
-}
-
-pub(crate) fn try_callable_value(
-    value: AnyValueEnum,
-) -> Option<(CallableValue, Vec<BasicTypeEnum>)> {
-    match value {
-        AnyValueEnum::FunctionValue(f) => {
-            Some((CallableValue::from(f), f.get_type().get_param_types()))
-        }
-        AnyValueEnum::PointerValue(p) => match p.get_type().get_element_type() {
-            AnyTypeEnum::FunctionType(ty) => {
-                Some((CallableValue::try_from(p).unwrap(), ty.get_param_types()))
-            }
-            _ => None,
-        },
-        _ => None,
-    }
-}
-
-pub(crate) fn any_to_meta(value: AnyValueEnum) -> Option<BasicMetadataValueEnum> {
-    match value {
-        AnyValueEnum::ArrayValue(a) => Some(BasicMetadataValueEnum::ArrayValue(a)),
-        AnyValueEnum::IntValue(i) => Some(BasicMetadataValueEnum::IntValue(i)),
-        AnyValueEnum::FloatValue(f) => Some(BasicMetadataValueEnum::FloatValue(f)),
-        AnyValueEnum::PointerValue(p) => Some(BasicMetadataValueEnum::PointerValue(p)),
-        AnyValueEnum::StructValue(s) => Some(BasicMetadataValueEnum::StructValue(s)),
-        AnyValueEnum::VectorValue(v) => Some(BasicMetadataValueEnum::VectorValue(v)),
-        AnyValueEnum::MetadataValue(m) => Some(BasicMetadataValueEnum::MetadataValue(m)),
-        AnyValueEnum::PhiValue(_)
-        | AnyValueEnum::FunctionValue(_)
-        | AnyValueEnum::InstructionValue(_) => None,
-    }
-}
-
-pub(crate) fn call_if_some(f: Option<&PyAny>) -> PyResult<()> {
-    match f {
-        Some(f) => f.call0().map(|_| ()),
-        None => Ok(()),
-    }
-}
-
-pub(crate) fn is_all_same<T>(
-    py: Python,
-    items: impl IntoIterator<Item = impl Borrow<Py<T>>>,
-) -> bool
-where
-    T: Eq + PyClass,
-{
-    let mut items = items.into_iter();
-    if let Some(mut prev) = items.next() {
-        for item in items {
-            if *item.borrow().borrow(py) != *prev.borrow().borrow(py) {
-                return false;
-            }
-            prev = item;
-        }
-    };
-    true
-}
-
-pub(crate) fn clone_module<'ctx>(
-    module: &Module,
-    context: &'ctx Context,
-) -> PyResult<Module<'ctx>> {
-    let bitcode = module.write_bitcode_to_memory();
-    let new_module = Module::parse_bitcode_from_buffer(&bitcode, context).map_err(|e| {
-        module.verify().err().map_or_else(
-            || PyOSError::new_err(e.to_string()),
-            |e| PyOSError::new_err(e.to_string()),
-        )
-    })?;
-    let name = module
-        .get_name()
-        .to_str()
-        .map_err(PyUnicodeDecodeError::new_err)?;
-    new_module.set_name(name);
-    Ok(new_module)
-}
diff --git a/pyqir/src/values.rs b/pyqir/src/values.rs
new file mode 100644
index 00000000..93954e4a
--- /dev/null
+++ b/pyqir/src/values.rs
@@ -0,0 +1,633 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#![allow(clippy::used_underscore_binding)]
+
+use crate::{context::Context, instructions::Instruction, module::Attribute, types::Type};
+use inkwell::{
+    attributes::AttributeLoc,
+    types::{AnyType, AnyTypeEnum},
+    values::{
+        AnyValueEnum, BasicMetadataValueEnum, BasicValueEnum, FloatValue, FunctionValue,
+        InstructionValue, IntValue, PointerValue,
+    },
+    LLVMReference,
+};
+use libc::c_char;
+use llvm_sys::{
+    core::{
+        LLVMBasicBlockAsValue, LLVMDisposeMessage, LLVMGetValueName2, LLVMIsConstant,
+        LLVMPrintValueToString,
+    },
+    prelude::*,
+};
+use pyo3::{
+    conversion::ToPyObject,
+    exceptions::{PyTypeError, PyValueError},
+    prelude::*,
+    types::PyBytes,
+};
+use qirlib::values;
+use std::{
+    convert::{Into, TryFrom, TryInto},
+    ffi::CStr,
+    fmt::{self, Display, Formatter},
+    mem::transmute,
+    ops::Deref,
+    slice,
+};
+
+/// A value.
+#[pyclass(subclass, unsendable)]
+#[derive(Clone)]
+pub(crate) struct Value {
+    value: AnyValue<'static>,
+    context: Py<Context>,
+}
+
+#[pymethods]
+impl Value {
+    /// The type of this value.
+    ///
+    /// :type: Type
+    #[getter]
+    fn r#type(&self, py: Python) -> PyResult<PyObject> {
+        unsafe { Type::from_any(py, self.context.clone(), self.value.ty()) }
+    }
+
+    /// The name of this value or the empty string if this value is anonymous.
+    #[getter]
+    fn name(&self) -> &str {
+        self.value
+            .name()
+            .to_str()
+            .expect("Name is not valid UTF-8.")
+    }
+
+    fn __str__(&self) -> String {
+        self.value.to_string()
+    }
+}
+
+impl Value {
+    pub(crate) unsafe fn from_any<'ctx>(
+        py: Python,
+        context: Py<Context>,
+        value: impl Into<AnyValue<'ctx>>,
+    ) -> PyResult<PyObject> {
+        let value = transmute::<AnyValue<'_>, AnyValue<'static>>(value.into());
+        #[allow(clippy::same_functions_in_if_condition)]
+        if let Ok(inst) = value.try_into() {
+            Instruction::from_inst(py, context, inst)
+        } else if let Ok(block) = value.try_into() {
+            let base = PyClassInitializer::from(Self { value, context });
+            let block = base.add_subclass(BasicBlock(block));
+            Ok(Py::new(py, block)?.to_object(py))
+        } else if value.is_const() {
+            Constant::from_any(py, context, value)
+        } else {
+            Ok(Py::new(py, Self { value, context })?.to_object(py))
+        }
+    }
+
+    pub(crate) unsafe fn init(context: Py<Context>, value: AnyValue) -> PyClassInitializer<Self> {
+        let value = transmute::<AnyValue<'_>, AnyValue<'static>>(value);
+        PyClassInitializer::from(Self { value, context })
+    }
+
+    pub(crate) unsafe fn get(&self) -> AnyValue<'static> {
+        self.value
+    }
+
+    pub(crate) fn context(&self) -> &Py<Context> {
+        &self.context
+    }
+}
+
+/// A basic block.
+#[pyclass(extends = Value, unsendable)]
+pub(crate) struct BasicBlock(inkwell::basic_block::BasicBlock<'static>);
+
+#[pymethods]
+impl BasicBlock {
+    /// The instructions in this basic block.
+    ///
+    /// :type: List[Instruction]
+    #[getter]
+    fn instructions(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let block = slf.0;
+        let context = &slf.into_super().context;
+        let mut insts = Vec::new();
+        let mut inst = block.get_first_instruction();
+
+        while let Some(i) = inst {
+            insts.push(unsafe { Instruction::from_inst(py, context.clone(), i) }?);
+            inst = i.get_next_instruction();
+        }
+
+        Ok(insts)
+    }
+
+    /// The terminating instruction of this basic block if there is one.
+    ///
+    /// :type: Optional[Instruction]
+    #[getter]
+    fn terminator(slf: PyRef<Self>, py: Python) -> PyResult<Option<PyObject>> {
+        match slf.0.get_terminator() {
+            Some(terminator) => {
+                let context = slf.into_super().context.clone();
+                unsafe { Instruction::from_inst(py, context, terminator) }.map(Some)
+            }
+            None => Ok(None),
+        }
+    }
+}
+
+/// A constant value.
+#[pyclass(extends = Value, subclass)]
+pub(crate) struct Constant;
+
+#[pymethods]
+impl Constant {
+    /// Whether this value is the null pointer.
+    ///
+    /// :type: bool
+    #[getter]
+    fn is_null(slf: PyRef<Self>) -> bool {
+        slf.into_super().value.is_null()
+    }
+}
+
+impl Constant {
+    unsafe fn from_any(py: Python, context: Py<Context>, value: AnyValue) -> PyResult<PyObject> {
+        if value.is_const() {
+            let value = transmute::<AnyValue<'_>, AnyValue<'static>>(value);
+            let base = PyClassInitializer::from(Value { value, context }).add_subclass(Constant);
+            match value.try_into() {
+                Ok(AnyValueEnum::IntValue(_)) => {
+                    Ok(Py::new(py, base.add_subclass(IntConstant))?.to_object(py))
+                }
+                Ok(AnyValueEnum::FloatValue(_)) => {
+                    Ok(Py::new(py, base.add_subclass(FloatConstant))?.to_object(py))
+                }
+                Ok(AnyValueEnum::FunctionValue(f)) => {
+                    Ok(Py::new(py, base.add_subclass(Function(f)))?.to_object(py))
+                }
+                _ => Ok(Py::new(py, base)?.to_object(py)),
+            }
+        } else {
+            Err(PyValueError::new_err("Value is not constant."))
+        }
+    }
+}
+
+/// A constant integer value.
+#[pyclass(extends = Constant)]
+pub(crate) struct IntConstant;
+
+#[pymethods]
+impl IntConstant {
+    /// The value.
+    ///
+    /// :type: int
+    #[getter]
+    fn value(slf: PyRef<Self>) -> u64 {
+        let int: IntValue = slf.into_super().into_super().value.try_into().unwrap();
+        int.get_zero_extended_constant().unwrap()
+    }
+}
+
+/// A constant floating-point value.
+#[pyclass(extends = Constant)]
+pub(crate) struct FloatConstant;
+
+#[pymethods]
+impl FloatConstant {
+    /// The value.
+    ///
+    /// :type: float
+    #[getter]
+    fn value(slf: PyRef<Self>) -> f64 {
+        let float: FloatValue = slf.into_super().into_super().value.try_into().unwrap();
+        float.get_constant().unwrap().0
+    }
+}
+
+/// A function value.
+#[pyclass(extends = Constant, unsendable)]
+pub(crate) struct Function(FunctionValue<'static>);
+
+#[pymethods]
+impl Function {
+    /// The parameters to this function.
+    ///
+    /// :type: List[Value]
+    #[getter]
+    fn params(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let params = slf.0.get_params();
+        let context = &slf.into_super().into_super().context;
+        params
+            .into_iter()
+            .map(|p| unsafe { Value::from_any(py, context.clone(), p) })
+            .collect()
+    }
+
+    /// The basic blocks in this function.
+    ///
+    /// :type: List[BasicBlock]
+    #[getter]
+    fn basic_blocks(slf: PyRef<Self>, py: Python) -> PyResult<Vec<PyObject>> {
+        let function = slf.0;
+        let context = &slf.into_super().into_super().context;
+        function
+            .get_basic_blocks()
+            .into_iter()
+            .map(|b| unsafe { Value::from_any(py, context.clone(), b) })
+            .collect()
+    }
+
+    /// Gets an attribute of this function with the given name if it has one.
+    ///
+    /// :param str name: The name of the attribute.
+    /// :rtype: Optional[Attribute]
+    /// :returns: The attribute.
+    #[pyo3(text_signature = "(name)")]
+    fn attribute(&self, name: &str) -> Option<Attribute> {
+        Some(Attribute(
+            self.0.get_string_attribute(AttributeLoc::Function, name)?,
+        ))
+    }
+}
+
+impl Function {
+    pub(crate) unsafe fn get(&self) -> FunctionValue<'static> {
+        self.0
+    }
+}
+
+#[derive(Clone, Copy)]
+pub(crate) enum AnyValue<'ctx> {
+    Any(AnyValueEnum<'ctx>),
+    BasicBlock(inkwell::basic_block::BasicBlock<'ctx>),
+}
+
+impl<'ctx> AnyValue<'ctx> {
+    fn ty(&self) -> AnyTypeEnum<'ctx> {
+        match self {
+            Self::Any(AnyValueEnum::ArrayValue(a)) => a.get_type().into(),
+            Self::Any(AnyValueEnum::IntValue(i)) => i.get_type().into(),
+            Self::Any(AnyValueEnum::FloatValue(f)) => f.get_type().into(),
+            Self::Any(AnyValueEnum::PhiValue(p)) => p.as_instruction().get_type(),
+            Self::Any(AnyValueEnum::FunctionValue(f)) => f.get_type().into(),
+            Self::Any(AnyValueEnum::PointerValue(p)) => p.get_type().into(),
+            Self::Any(AnyValueEnum::StructValue(s)) => s.get_type().into(),
+            Self::Any(AnyValueEnum::VectorValue(v)) => v.get_type().into(),
+            Self::Any(AnyValueEnum::InstructionValue(i)) => i.get_type(),
+            Self::Any(AnyValueEnum::MetadataValue(m)) => {
+                inkwell::values::AnyValue::as_any_value_enum(m).get_type()
+            }
+            Self::BasicBlock(b) => b.get_context().void_type().into(),
+        }
+    }
+
+    fn name(&self) -> &CStr {
+        let mut len = 0;
+        let name = unsafe { LLVMGetValueName2(self.get_ref(), &mut len) };
+        let name = unsafe { slice::from_raw_parts(name.cast(), len + 1) };
+        CStr::from_bytes_with_nul(name).expect("Name is not a valid C string.")
+    }
+
+    fn is_const(&self) -> bool {
+        unsafe { LLVMIsConstant(self.get_ref()) != 0 }
+    }
+
+    fn is_null(&self) -> bool {
+        match self {
+            Self::Any(AnyValueEnum::PointerValue(p)) => p.is_null(),
+            Self::Any(_) | Self::BasicBlock(_) => false,
+        }
+    }
+}
+
+impl<'ctx> Display for AnyValue<'ctx> {
+    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
+        match self {
+            Self::Any(any) => {
+                let message = inkwell::values::AnyValue::print_to_string(any);
+                f.write_str(message.to_str().map_err(|_| fmt::Error)?)
+            }
+            Self::BasicBlock(block) => {
+                let value = unsafe { LLVMBasicBlockAsValue(block.get_ref()) };
+                let message = unsafe { Message(LLVMPrintValueToString(value)) };
+                f.write_str(message.to_str().map_err(|_| fmt::Error)?)
+            }
+        }
+    }
+}
+
+impl LLVMReference<LLVMValueRef> for AnyValue<'_> {
+    unsafe fn get_ref(&self) -> LLVMValueRef {
+        match self {
+            Self::Any(any) => any.get_ref(),
+            Self::BasicBlock(block) => LLVMBasicBlockAsValue(block.get_ref()),
+        }
+    }
+}
+
+impl<'ctx> From<AnyValueEnum<'ctx>> for AnyValue<'ctx> {
+    fn from(any: AnyValueEnum<'ctx>) -> Self {
+        Self::Any(any)
+    }
+}
+
+impl<'ctx> From<BasicValueEnum<'ctx>> for AnyValue<'ctx> {
+    fn from(basic: BasicValueEnum<'ctx>) -> Self {
+        Self::Any(basic.into())
+    }
+}
+
+impl<'ctx> From<IntValue<'ctx>> for AnyValue<'ctx> {
+    fn from(int: IntValue<'ctx>) -> Self {
+        Self::Any(int.into())
+    }
+}
+
+impl<'ctx> From<FloatValue<'ctx>> for AnyValue<'ctx> {
+    fn from(float: FloatValue<'ctx>) -> Self {
+        Self::Any(float.into())
+    }
+}
+
+impl<'ctx> From<FunctionValue<'ctx>> for AnyValue<'ctx> {
+    fn from(function: FunctionValue<'ctx>) -> Self {
+        Self::Any(function.into())
+    }
+}
+
+impl<'ctx> From<PointerValue<'ctx>> for AnyValue<'ctx> {
+    fn from(pointer: PointerValue<'ctx>) -> Self {
+        Self::Any(pointer.into())
+    }
+}
+
+impl<'ctx> From<InstructionValue<'ctx>> for AnyValue<'ctx> {
+    fn from(instruction: InstructionValue<'ctx>) -> Self {
+        Self::Any(instruction.into())
+    }
+}
+
+impl<'ctx> From<inkwell::basic_block::BasicBlock<'ctx>> for AnyValue<'ctx> {
+    fn from(block: inkwell::basic_block::BasicBlock<'ctx>) -> Self {
+        Self::BasicBlock(block)
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for AnyValueEnum<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(a) => Ok(a),
+            AnyValue::BasicBlock(_) => Err(ConvertError("value excluding basic blocks")),
+        }
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for BasicMetadataValueEnum<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::ArrayValue(a)) => Some(a.into()),
+            AnyValue::Any(AnyValueEnum::IntValue(i)) => Some(i.into()),
+            AnyValue::Any(AnyValueEnum::FloatValue(f)) => Some(f.into()),
+            AnyValue::Any(AnyValueEnum::PointerValue(p)) => Some(p.into()),
+            AnyValue::Any(AnyValueEnum::StructValue(s)) => Some(s.into()),
+            AnyValue::Any(AnyValueEnum::VectorValue(v)) => Some(v.into()),
+            AnyValue::Any(AnyValueEnum::InstructionValue(i)) => i
+                .try_into()
+                .map(BasicMetadataValueEnum::IntValue)
+                .or_else(|()| i.try_into().map(BasicMetadataValueEnum::FloatValue))
+                .or_else(|()| i.try_into().map(BasicMetadataValueEnum::PointerValue))
+                .ok(),
+            AnyValue::Any(AnyValueEnum::MetadataValue(m)) => Some(m.into()),
+            AnyValue::Any(AnyValueEnum::PhiValue(_) | AnyValueEnum::FunctionValue(_))
+            | AnyValue::BasicBlock(_) => None,
+        }
+        .ok_or(ConvertError("argument value"))
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for IntValue<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::IntValue(i)) => Some(i),
+            AnyValue::Any(AnyValueEnum::InstructionValue(i)) => i.try_into().ok(),
+            _ => None,
+        }
+        .ok_or(ConvertError("integer value"))
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for FloatValue<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::FloatValue(f)) => Some(f),
+            AnyValue::Any(AnyValueEnum::InstructionValue(i)) => i.try_into().ok(),
+            _ => None,
+        }
+        .ok_or(ConvertError("float value"))
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for PointerValue<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::PointerValue(p)) => Some(p),
+            AnyValue::Any(AnyValueEnum::InstructionValue(i)) => i.try_into().ok(),
+            _ => None,
+        }
+        .ok_or(ConvertError("pointer value"))
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for InstructionValue<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(AnyValueEnum::ArrayValue(a)) => a.as_instruction(),
+            AnyValue::Any(AnyValueEnum::IntValue(i)) => i.as_instruction(),
+            AnyValue::Any(AnyValueEnum::FloatValue(f)) => f.as_instruction(),
+            AnyValue::Any(AnyValueEnum::PhiValue(p)) => Some(p.as_instruction()),
+            AnyValue::Any(AnyValueEnum::PointerValue(p)) => p.as_instruction(),
+            AnyValue::Any(AnyValueEnum::StructValue(s)) => s.as_instruction(),
+            AnyValue::Any(AnyValueEnum::VectorValue(v)) => v.as_instruction(),
+            AnyValue::Any(AnyValueEnum::InstructionValue(i)) => Some(i),
+            AnyValue::Any(AnyValueEnum::FunctionValue(_) | AnyValueEnum::MetadataValue(_))
+            | AnyValue::BasicBlock(_) => None,
+        }
+        .ok_or(ConvertError("instruction value"))
+    }
+}
+
+impl<'ctx> TryFrom<AnyValue<'ctx>> for inkwell::basic_block::BasicBlock<'ctx> {
+    type Error = ConvertError;
+
+    fn try_from(value: AnyValue<'ctx>) -> Result<Self, Self::Error> {
+        match value {
+            AnyValue::Any(_) => Err(ConvertError("basic block")),
+            AnyValue::BasicBlock(b) => Ok(b),
+        }
+    }
+}
+
+#[derive(Debug)]
+pub(crate) struct ConvertError(&'static str);
+
+impl From<ConvertError> for PyErr {
+    fn from(error: ConvertError) -> Self {
+        PyValueError::new_err(format!("Couldn't convert value to {}.", error.0))
+    }
+}
+
+struct Message(*mut c_char);
+
+impl Deref for Message {
+    type Target = CStr;
+
+    fn deref(&self) -> &Self::Target {
+        unsafe { CStr::from_ptr(self.0) }
+    }
+}
+
+impl Drop for Message {
+    fn drop(&mut self) {
+        unsafe { LLVMDisposeMessage(self.0) }
+    }
+}
+
+/// Creates a constant value.
+///
+/// :param Type ty: The type of the value.
+/// :param Union[int, float] value: The value of the constant.
+/// :returns: The constant value.
+/// :rtype: Value
+#[pyfunction]
+#[pyo3(text_signature = "(ty, value)")]
+pub(crate) fn r#const(py: Python, ty: &Type, value: &PyAny) -> PyResult<PyObject> {
+    let context = ty.context().clone();
+    let value = extract_constant(unsafe { &ty.get() }, value)?;
+    unsafe { Value::from_any(py, context, value) }
+}
+
+/// If the value is a static qubit ID, extracts it.
+///
+/// :param Value value: The value.
+/// :rtype: Optional[int]
+/// :returns: The static qubit ID.
+#[pyfunction]
+#[pyo3(text_signature = "(value)")]
+pub(crate) fn qubit_id(value: &Value) -> Option<u64> {
+    values::qubit_id(unsafe { value.get() }.try_into().ok()?)
+}
+
+/// If the value is a static result ID, extracts it.
+///
+/// :param Value value: The value.
+/// :rtype: Optional[int]
+/// :returns: The static result ID.
+#[pyfunction]
+#[pyo3(text_signature = "(value)")]
+pub(crate) fn result_id(value: &Value) -> Option<u64> {
+    values::result_id(unsafe { value.get() }.try_into().ok()?)
+}
+
+/// Whether the function is an entry point.
+///
+/// :param Function function: The function.
+/// :rtype: bool
+/// :returns: True if the function is an entry point.
+#[pyfunction]
+#[pyo3(text_signature = "(function)")]
+pub(crate) fn is_entry_point(function: &Function) -> bool {
+    values::is_entry_point(unsafe { function.get() })
+}
+
+/// Whether the function is interop-friendly.
+///
+/// :param Function function: The function.
+/// :rtype: bool
+/// :returns: True if the function is interop-friendly.
+#[pyfunction]
+#[pyo3(text_signature = "(function)")]
+pub(crate) fn is_interop_friendly(function: &Function) -> bool {
+    values::is_interop_friendly(unsafe { function.get() })
+}
+
+/// If the function declares a required number of qubits, extracts it.
+///
+/// :param Function function: The function.
+/// :rtype: Optional[int]
+/// :returns: The required number of qubits.
+#[pyfunction]
+#[pyo3(text_signature = "(function)")]
+pub(crate) fn required_num_qubits(function: &Function) -> Option<u64> {
+    values::required_num_qubits(unsafe { function.get() })
+}
+
+/// If the function declares a required number of results, extracts it.
+///
+/// :param Function function: The function.
+/// :rtype: Optional[int]
+/// :returns: The required number of results.
+#[pyfunction]
+#[pyo3(text_signature = "(function)")]
+pub(crate) fn required_num_results(function: &Function) -> Option<u64> {
+    values::required_num_results(unsafe { function.get() })
+}
+
+/// If the value is a pointer to a constant byte array, extracts it.
+///
+/// :param Value value: The value.
+/// :rtype: Optional[bytes]
+/// :returns: The constant byte array.
+#[pyfunction]
+#[pyo3(text_signature = "(value)")]
+pub(crate) fn constant_bytes<'p>(py: Python<'p>, value: &Value) -> Option<&'p PyBytes> {
+    let bytes = values::constant_bytes(unsafe { value.get() }.try_into().ok()?)?;
+    Some(PyBytes::new(py, bytes))
+}
+
+pub(crate) unsafe fn extract_any<'ctx>(
+    ty: &impl AnyType<'ctx>,
+    ob: &PyAny,
+) -> PyResult<AnyValue<'ctx>> {
+    ob.extract()
+        .map(|v: Value| v.value)
+        .or_else(|_| extract_constant(ty, ob))
+}
+
+fn extract_constant<'ctx>(ty: &impl AnyType<'ctx>, ob: &PyAny) -> PyResult<AnyValue<'ctx>> {
+    match ty.as_any_type_enum() {
+        AnyTypeEnum::IntType(int) => Ok(int.const_int(ob.extract()?, true).into()),
+        AnyTypeEnum::FloatType(float) => Ok(float.const_float(ob.extract()?).into()),
+        _ => Err(PyTypeError::new_err(
+            "Can't convert Python value into this type.",
+        )),
+    }
+}
+
+pub(crate) fn extract_contexts<'a>(
+    values: impl IntoIterator<Item = &'a PyAny> + 'a,
+) -> impl Iterator<Item = Py<Context>> + 'a {
+    values
+        .into_iter()
+        .filter_map(|v| Some(v.extract::<Value>().ok()?.context))
+}
diff --git a/qirlib/Cargo.toml b/qirlib/Cargo.toml
index 4bb55e2c..06c5b528 100644
--- a/qirlib/Cargo.toml
+++ b/qirlib/Cargo.toml
@@ -19,7 +19,7 @@ llvm-sys-130 = { package = "llvm-sys", version = "130.0.2", optional = true }
 llvm-sys-140 = { package = "llvm-sys", version = "140.0.2", optional = true }
 
 # The llvm version of inkwell is controlled through this crates features.
-inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["target-x86"] }
+inkwell = { git = "https://github.com/TheDan64/inkwell", branch = "master", default-features = false, features = ["internal-getters", "target-x86"] }
 log = "0.4.14"
 cty = "0.2.1"
 rand = "0.8.4"
@@ -48,10 +48,10 @@ semver = "1.0.6"
 name = "qirlib"
 
 [features]
-llvm11-0 = ["inkwell/llvm11-0"]
-llvm12-0 = ["inkwell/llvm12-0"]
-llvm13-0 = ["inkwell/llvm13-0"]
-llvm14-0 = ["inkwell/llvm14-0"]
+llvm11-0 = ["inkwell/llvm11-0", "llvm-sys-110"]
+llvm12-0 = ["inkwell/llvm12-0", "llvm-sys-120"]
+llvm13-0 = ["inkwell/llvm13-0", "llvm-sys-130"]
+llvm14-0 = ["inkwell/llvm14-0", "llvm-sys-140"]
 
 # default to use llvm-sys/inkwell for llvm linking
 default = ["external-llvm-linking"]
diff --git a/qirlib/src/builder.rs b/qirlib/src/builder.rs
index ba77097d..1acdeb65 100644
--- a/qirlib/src/builder.rs
+++ b/qirlib/src/builder.rs
@@ -1,3 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
 use crate::types;
 use inkwell::{
     builder::Builder as BuilderBase,
diff --git a/qirlib/src/evaluation/jit.rs b/qirlib/src/evaluation/jit.rs
index 213fbfb0..25c7bb06 100644
--- a/qirlib/src/evaluation/jit.rs
+++ b/qirlib/src/evaluation/jit.rs
@@ -160,8 +160,7 @@ fn load_file(path: impl AsRef<Path>, context: &Context) -> Result<Module, String
 mod tests {
     use super::{run_module, run_module_file};
     use crate::evaluation::interop::{Instruction, SemanticModel, Single};
-    use crate::module;
-    use inkwell::context::Context;
+    use inkwell::{context::Context, memory_buffer::MemoryBuffer, module::Module};
     use serial_test::serial;
     use std::path::PathBuf;
 
@@ -298,7 +297,9 @@ mod tests {
 
     fn run_test_module(bytes: &[u8], entry_point: Option<&str>) -> Result<SemanticModel, String> {
         let context = Context::create();
-        let module = module::load_memory(bytes, "test", &context)?;
+        let buffer = MemoryBuffer::create_from_memory_range_copy(bytes, "test");
+        let module =
+            Module::parse_bitcode_from_buffer(&buffer, &context).map_err(|e| e.to_string())?;
         run_module(&module, entry_point, None)
     }
 }
diff --git a/qirlib/src/evaluation/mod.rs b/qirlib/src/evaluation/mod.rs
index 2ea92593..d6697c69 100644
--- a/qirlib/src/evaluation/mod.rs
+++ b/qirlib/src/evaluation/mod.rs
@@ -1,5 +1,8 @@
-pub mod gates;
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+mod gates;
 pub mod interop;
-pub mod intrinsics;
+mod intrinsics;
 pub mod jit;
-pub mod runtime;
+mod runtime;
diff --git a/qirlib/src/lib.rs b/qirlib/src/lib.rs
index 4f16e8ea..013da4ed 100644
--- a/qirlib/src/lib.rs
+++ b/qirlib/src/lib.rs
@@ -3,6 +3,15 @@
 
 #![warn(clippy::all, clippy::pedantic)]
 
+#[cfg(feature = "llvm11-0")]
+extern crate llvm_sys_110 as llvm_sys;
+#[cfg(feature = "llvm12-0")]
+extern crate llvm_sys_120 as llvm_sys;
+#[cfg(feature = "llvm13-0")]
+extern crate llvm_sys_130 as llvm_sys;
+#[cfg(feature = "llvm14-0")]
+extern crate llvm_sys_140 as llvm_sys;
+
 #[cfg(not(feature = "no-llvm-linking"))]
 pub use builder::Builder;
 #[cfg(not(feature = "no-llvm-linking"))]
@@ -20,3 +29,5 @@ mod qis;
 mod tests;
 #[cfg(not(feature = "no-llvm-linking"))]
 pub mod types;
+#[cfg(not(feature = "no-llvm-linking"))]
+pub mod values;
diff --git a/qirlib/src/module.rs b/qirlib/src/module.rs
index a48d3881..e48f6423 100644
--- a/qirlib/src/module.rs
+++ b/qirlib/src/module.rs
@@ -4,52 +4,12 @@
 use inkwell::{
     attributes::AttributeLoc,
     builder::Builder,
-    context::Context,
-    memory_buffer::MemoryBuffer,
     module::Module,
     passes::{PassManager, PassManagerBuilder},
     values::FunctionValue,
     OptimizationLevel,
 };
 
-/// # Errors
-///
-/// Will return `Err` if a module cannot be created from the supplied IR
-pub fn ir_to_bitcode(
-    value: &str,
-    module_name: Option<&str>,
-    source_file_name: Option<&str>,
-) -> Result<Vec<u8>, String> {
-    let context = Context::create();
-    let buffer = MemoryBuffer::create_from_memory_range_copy(
-        value.as_bytes(),
-        module_name.unwrap_or_default(),
-    );
-    let module = context
-        .create_module_from_ir(buffer)
-        .map_err(|e| e.to_string())?;
-    if let Some(name) = source_file_name {
-        module.set_source_file_name(name);
-    }
-    Ok(module.write_bitcode_to_memory().as_slice().to_owned())
-}
-
-/// # Errors
-///
-/// Will return `Err` if a module cannot be created from the supplied bitcode
-pub fn bitcode_to_ir(
-    value: &[u8],
-    module_name: Option<&str>,
-    source_file_name: Option<&str>,
-) -> Result<String, String> {
-    let context = Context::create();
-    let module = load_memory(value, module_name.unwrap_or_default(), &context)?;
-    if let Some(name) = source_file_name {
-        module.set_source_file_name(name);
-    }
-    Ok(module.print_to_string().to_string())
-}
-
 pub fn simple_init(
     module: &Module,
     builder: &Builder,
@@ -70,18 +30,6 @@ pub fn simple_finalize(module: &Module) -> Result<(), String> {
     module.verify().map_err(|e| e.to_string())
 }
 
-/// # Errors
-///
-/// - Module fails to load.
-pub(crate) fn load_memory<'a>(
-    bytes: &[u8],
-    name: &str,
-    context: &'a Context,
-) -> Result<Module<'a>, String> {
-    let buffer = MemoryBuffer::create_from_memory_range_copy(bytes, name);
-    Module::parse_bitcode_from_buffer(&buffer, context).map_err(|e| e.to_string())
-}
-
 // This method returns true if any of the passes modified the function or module and false otherwise.
 pub(crate) fn run_basic_passes(module: &Module) -> bool {
     let pass_manager_builder = PassManagerBuilder::create();
@@ -111,12 +59,7 @@ fn add_num_attribute(function: FunctionValue, key: &str, value: u64) {
 
 #[cfg(test)]
 mod tests {
-    use crate::{
-        builder::Builder,
-        module::{self, create_entry_point},
-        qis::BuilderBasicQisExt,
-        tests::assert_reference_ir,
-    };
+    use crate::{module::create_entry_point, tests::assert_reference_ir};
     use inkwell::context::Context;
     use std::{fs::File, io::Read, path::Path};
     use tempfile::tempdir;
@@ -136,25 +79,6 @@ mod tests {
         assert_eq!(expected, ir_string);
     }
 
-    fn example_ir() -> String {
-        let context = Context::create();
-        let module = context.create_module("test");
-        let builder = Builder::new(&module);
-        super::simple_init(&module, &builder, 1, 1);
-        builder.build_mz(builder.build_qubit(0), builder.build_result(0));
-        builder.build_return(None);
-        module.print_to_string().to_string()
-    }
-
-    #[test]
-    fn ir_round_trip_is_identical() -> Result<(), String> {
-        let actual_ir = example_ir();
-        let bitcode = module::ir_to_bitcode(actual_ir.as_str(), None, None)?;
-        let converted_ir = module::bitcode_to_ir(bitcode.as_slice(), Some("test"), Some("test"))?;
-        assert_eq!(actual_ir, converted_ir);
-        Ok(())
-    }
-
     #[test]
     fn emitted_bitcode_files_are_identical_to_base64_encoded() {
         let dir = tempdir().expect("");
diff --git a/qirlib/src/types.rs b/qirlib/src/types.rs
index 3f4bdbf4..157cf201 100644
--- a/qirlib/src/types.rs
+++ b/qirlib/src/types.rs
@@ -3,7 +3,7 @@
 
 use inkwell::{
     module::Module,
-    types::{PointerType, StructType},
+    types::{AnyTypeEnum, PointerType, StructType},
     AddressSpace,
 };
 
@@ -11,10 +11,20 @@ pub fn qubit<'ctx>(module: &Module<'ctx>) -> PointerType<'ctx> {
     get_or_create_struct(module, "Qubit").ptr_type(AddressSpace::Generic)
 }
 
+#[must_use]
+pub fn is_qubit(ty: AnyTypeEnum) -> bool {
+    is_opaque_pointer_to(ty, "Qubit")
+}
+
 pub fn result<'ctx>(module: &Module<'ctx>) -> PointerType<'ctx> {
     get_or_create_struct(module, "Result").ptr_type(AddressSpace::Generic)
 }
 
+#[must_use]
+pub fn is_result(ty: AnyTypeEnum) -> bool {
+    is_opaque_pointer_to(ty, "Result")
+}
+
 fn get_or_create_struct<'ctx>(module: &Module<'ctx>, name: &str) -> StructType<'ctx> {
     module.get_struct_type(name).unwrap_or_else(|| {
         log::debug!("{} was not defined in the module", name);
@@ -22,6 +32,19 @@ fn get_or_create_struct<'ctx>(module: &Module<'ctx>, name: &str) -> StructType<'
     })
 }
 
+fn is_opaque_pointer_to(ty: AnyTypeEnum, name: &str) -> bool {
+    match ty {
+        AnyTypeEnum::PointerType(p) => match p.get_element_type() {
+            AnyTypeEnum::StructType(s) => {
+                let struct_name = s.get_name().and_then(|n| n.to_str().ok());
+                struct_name == Some(name)
+            }
+            _ => false,
+        },
+        _ => false,
+    }
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/qirlib/src/values.rs b/qirlib/src/values.rs
new file mode 100644
index 00000000..df2911d3
--- /dev/null
+++ b/qirlib/src/values.rs
@@ -0,0 +1,109 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+use crate::types;
+use core::slice;
+use inkwell::{
+    attributes::AttributeLoc,
+    types::{AnyTypeEnum, PointerType},
+    values::{AnyValueEnum, FunctionValue},
+    LLVMReference,
+};
+use llvm_sys::{
+    core::{
+        LLVMConstIntGetZExtValue, LLVMGetAsString, LLVMGetConstOpcode, LLVMGetInitializer,
+        LLVMGetOperand, LLVMIsAConstantDataSequential, LLVMIsAConstantExpr,
+    },
+    LLVMOpcode,
+};
+use std::convert::TryFrom;
+
+#[must_use]
+pub fn qubit_id(value: AnyValueEnum) -> Option<u64> {
+    if types::is_qubit(value.get_type()) {
+        pointer_to_int(value)
+    } else {
+        None
+    }
+}
+
+#[must_use]
+pub fn result_id(value: AnyValueEnum) -> Option<u64> {
+    if types::is_result(value.get_type()) {
+        pointer_to_int(value)
+    } else {
+        None
+    }
+}
+
+#[must_use]
+pub fn is_entry_point(function: FunctionValue) -> bool {
+    function
+        .get_string_attribute(AttributeLoc::Function, "EntryPoint")
+        .is_some()
+}
+
+#[must_use]
+pub fn is_interop_friendly(function: FunctionValue) -> bool {
+    function
+        .get_string_attribute(AttributeLoc::Function, "InteropFriendly")
+        .is_some()
+}
+
+#[must_use]
+pub fn required_num_qubits(function: FunctionValue) -> Option<u64> {
+    let attribute = function.get_string_attribute(AttributeLoc::Function, "requiredQubits")?;
+    attribute.get_string_value().to_str().ok()?.parse().ok()
+}
+
+#[must_use]
+pub fn required_num_results(function: FunctionValue) -> Option<u64> {
+    let attribute = function.get_string_attribute(AttributeLoc::Function, "requiredResults")?;
+    attribute.get_string_value().to_str().ok()?.parse().ok()
+}
+
+#[must_use]
+#[allow(clippy::missing_panics_doc)]
+pub fn constant_bytes(value: AnyValueEnum) -> Option<&[u8]> {
+    let pointer = match value {
+        AnyValueEnum::PointerValue(p) if is_byte_string(p.get_type()) => Some(p),
+        _ => None,
+    }?;
+
+    let expr = unsafe { LLVMIsAConstantExpr(pointer.get_ref()) };
+    let opcode = unsafe { LLVMGetConstOpcode(expr) };
+    if opcode != LLVMOpcode::LLVMGetElementPtr {
+        return None;
+    }
+
+    let element = unsafe { LLVMGetOperand(expr, 0) };
+    let offset = unsafe { LLVMConstIntGetZExtValue(LLVMGetOperand(expr, 1)) };
+    let offset = usize::try_from(offset).expect("Pointer offset larger than usize.");
+    let init = unsafe { LLVMIsAConstantDataSequential(LLVMGetInitializer(element)) };
+    if init.is_null() {
+        return None;
+    }
+
+    let mut len = 0;
+    let data = unsafe { LLVMGetAsString(init, &mut len) };
+    let data = unsafe { slice::from_raw_parts(data.cast(), len) };
+    Some(&data[offset..])
+}
+
+fn pointer_to_int(value: AnyValueEnum) -> Option<u64> {
+    match value {
+        AnyValueEnum::PointerValue(p) => {
+            let context = p.get_type().get_context();
+            p.const_to_int(context.i64_type())
+                .get_zero_extended_constant()
+        }
+        _ => None,
+    }
+}
+
+fn is_byte_string(ty: PointerType) -> bool {
+    match ty.get_element_type() {
+        AnyTypeEnum::IntType(i) => i.get_bit_width() == 8,
+        _ => false,
+    }
+}


Test patch:
diff --git a/pyqir-parser/tests/test_parser_api.py b/pyqir-parser/tests/test_parser_api.py
index cdac2023..804a36c8 100644
--- a/pyqir-parser/tests/test_parser_api.py
+++ b/pyqir-parser/tests/test_parser_api.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.parser import *
+from pyqir_parser import *
 import pytest
 
 
diff --git a/pyqir/tests/test_conversions.py b/pyqir/tests/test_conversions.py
index 5a8b09d0..a94ac72f 100644
--- a/pyqir/tests/test_conversions.py
+++ b/pyqir/tests/test_conversions.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule, ir_to_bitcode, bitcode_to_ir
+from pyqir import BasicQisBuilder, SimpleModule, Module
 
 
 def get_module() -> SimpleModule:
@@ -13,44 +13,47 @@ def get_module() -> SimpleModule:
 
 def test_ir_round_trip_is_identical() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir, "test")
-    converted_ir = bitcode_to_ir(bitcode, "test")
+    bitcode = Module.from_ir(expected_ir, "test").bitcode
+    converted_ir = str(Module.from_bitcode(bitcode, "test"))
     assert expected_ir == converted_ir
 
 
-def test_ir_round_trip_is_not_identical_when_module_name_isnot_supplied() -> None:
+def test_ir_round_trip_is_not_identical_when_module_name_is_not_supplied() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir)
-    converted_ir = bitcode_to_ir(bitcode)
+    bitcode = Module.from_ir(expected_ir).bitcode
+    converted_ir = str(Module.from_bitcode(bitcode))
     assert expected_ir != converted_ir
 
 
 def test_module_name_persists_in_conversion() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir, "test")
-    converted_ir = bitcode_to_ir(bitcode, "test2")
+    bitcode = Module.from_ir(expected_ir, "test").bitcode
+    converted_ir = str(Module.from_bitcode(bitcode, "test2"))
     assert expected_ir != converted_ir
     assert "; ModuleID = 'test2'" in converted_ir
 
 
 def test_file_name_persists_in_conversion() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir, "test", "some file")
-    converted_ir = bitcode_to_ir(bitcode, "test", "some other file")
+    module1 = Module.from_ir(expected_ir, "test")
+    module1.source_filename = "some file"
+    module2 = Module.from_bitcode(module1.bitcode, "test")
+    module2.source_filename = "some other file"
+    converted_ir = str(module2)
     assert expected_ir != converted_ir
     assert 'source_filename = "some other file"' in converted_ir
 
 
 def test_ir_to_bitcode_returns_bytes_type() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir, "test")
+    bitcode = Module.from_ir(expected_ir, "test").bitcode
     assert isinstance(bitcode, bytes)
 
 
 def test_bitcode_to_ir_returns_str_type() -> None:
     expected_ir = get_module().ir()
-    bitcode = ir_to_bitcode(expected_ir, "test")
-    converted_ir = bitcode_to_ir(bitcode, "test")
+    bitcode = Module.from_ir(expected_ir, "test").bitcode
+    converted_ir = str(Module.from_bitcode(bitcode, "test"))
     assert isinstance(converted_ir, str)
 
 
diff --git a/pyqir/tests/test_evaluator_api.py b/pyqir/tests/test_evaluator.py
similarity index 100%
rename from pyqir/tests/test_evaluator_api.py
rename to pyqir/tests/test_evaluator.py
diff --git a/pyqir/tests/test_external_functions.py b/pyqir/tests/test_external_functions.py
index cd7b75ab..2e9e0315 100644
--- a/pyqir/tests/test_external_functions.py
+++ b/pyqir/tests/test_external_functions.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import (
+from pyqir import (
     BasicQisBuilder,
     SimpleModule,
     Type,
@@ -316,7 +316,9 @@ def test_variable_wrong_angle_type(self) -> None:
 
         x = mod.builder.call(foo, [])
         assert x is not None
-        with self.assertRaisesRegex(BaseException, "^Found IntValue"):
+        with self.assertRaisesRegex(
+            ValueError, r"^Couldn't convert value to float value\.$"
+        ):
             qis.rz(x, mod.qubits[0])
 
     def test_two_variables(self) -> None:
diff --git a/pyqir/tests/test_generator_api.py b/pyqir/tests/test_generator.py
similarity index 98%
rename from pyqir/tests/test_generator_api.py
rename to pyqir/tests/test_generator.py
index 92234e88..aa68a967 100644
--- a/pyqir/tests/test_generator_api.py
+++ b/pyqir/tests/test_generator.py
@@ -7,7 +7,7 @@
 about the generated IR.
 """
 
-from pyqir.generator import BasicQisBuilder, SimpleModule
+from pyqir import BasicQisBuilder, SimpleModule
 import pytest
 
 
diff --git a/pyqir/tests/test_if.py b/pyqir/tests/test_if.py
index 6b20badf..ec8d712e 100644
--- a/pyqir/tests/test_if.py
+++ b/pyqir/tests/test_if.py
@@ -2,7 +2,7 @@
 # Licensed under the MIT License.
 
 from abc import ABCMeta, abstractmethod
-from pyqir.generator import BasicQisBuilder, IntPredicate, SimpleModule, const
+from pyqir import BasicQisBuilder, IntPredicate, SimpleModule, const
 from pyqir.evaluator import GateLogger, GateSet, NonadaptiveEvaluator
 import pytest
 import tempfile
diff --git a/pyqir/tests/test_int_operators.py b/pyqir/tests/test_int_operators.py
index 5fa963a0..0790afdf 100644
--- a/pyqir/tests/test_int_operators.py
+++ b/pyqir/tests/test_int_operators.py
@@ -2,7 +2,7 @@
 # Licensed under the MIT License.
 
 from functools import partial
-from pyqir.generator import Builder, IntPredicate, SimpleModule, Value, const
+from pyqir import Builder, IntPredicate, SimpleModule, Value, const
 from typing import Callable, List, Tuple
 import unittest
 
diff --git a/pyqir/tests/test_parser.py b/pyqir/tests/test_parser.py
new file mode 100644
index 00000000..b3b43a06
--- /dev/null
+++ b/pyqir/tests/test_parser.py
@@ -0,0 +1,258 @@
+# Copyright (c) Microsoft Corporation.
+# Licensed under the MIT License.
+
+from pathlib import Path
+from pyqir import (
+    BasicBlock,
+    Call,
+    Constant,
+    Function,
+    IntConstant,
+    IntType,
+    Module,
+    Opcode,
+    constant_bytes,
+    is_entry_point,
+    is_interop_friendly,
+    is_qubit,
+    qubit_id,
+    required_num_qubits,
+    result_id,
+)
+import pytest
+
+
+def test_parser() -> None:
+    bitcode = Path("../pyqir-parser/tests/teleportchain.baseprofile.bc").read_bytes()
+    mod = Module.from_bitcode(bitcode)
+    assert len(mod.functions) == 21
+
+    func_name = (
+        "TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__Interop"
+    )
+    func = next(filter(lambda f: f.name == func_name, mod.functions))
+    interop_funcs = list(filter(is_interop_friendly, mod.functions))
+    assert len(interop_funcs) == 1
+
+    blocks = func.basic_blocks
+    assert len(blocks) == 9
+    assert blocks[0].name == "entry"
+
+    term = blocks[0].terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+    assert term.operands[0].name == ""
+    assert term.successors[0].name == "continue__1.i.i.i"
+    assert term.successors[1].name == "then0__1.i.i.i"
+
+    term = blocks[1].terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+    assert len(term.operands) == 1
+    assert term.successors[0].name == "continue__1.i.i.i"
+
+    term = blocks[8].terminator
+    assert term is not None
+    assert term.opcode == Opcode.RET
+
+    term_type = term.operands[0].type
+    assert isinstance(term_type, IntType)
+    assert term_type.width == 8
+
+    block = next(filter(lambda b: b.name == "then0__2.i.i3.i", func.basic_blocks))
+    assert isinstance(block.instructions[0], Call)
+    assert qubit_id(block.instructions[0].operands[0]) == 5
+
+    block = next(filter(lambda b: b.name == "continue__1.i.i2.i", func.basic_blocks))
+    term = block.terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+    assert len(term.operands) == 3
+
+    inst = term.operands[0]
+    assert isinstance(inst, Call)
+    assert inst.callee.name == "__quantum__qir__read_result"
+    assert result_id(inst.args[0]) == 3
+
+    inst_type = inst.type
+    assert isinstance(inst_type, IntType)
+    assert inst_type.width == 1
+
+    exit = func.basic_blocks[-1]
+    call_mz = exit.instructions[0]
+    assert isinstance(call_mz, Call)
+    assert call_mz.callee.name == "__quantum__qis__mz__body"
+    assert call_mz.type.is_void
+    assert call_mz.name == ""
+
+
+def test_parser_select_support() -> None:
+    bitcode = Path("../pyqir-parser/tests/select.bc").read_bytes()
+    mod = Module.from_bitcode(bitcode)
+    func = next(filter(is_entry_point, mod.functions))
+    block = func.basic_blocks[0]
+    select = block.instructions[5]
+    assert select.opcode == Opcode.SELECT
+    assert select.name == "spec.select"
+
+    cond = select.operands[0]
+    assert isinstance(cond, Call)
+    assert cond.callee.name == "__quantum__qis__read_result__body"
+    assert result_id(cond.args[0]) == 0
+
+    true = select.operands[1]
+    assert isinstance(true, IntConstant)
+    assert true.value == 2
+    assert true.type.width == 64
+
+    false = select.operands[2]
+    assert isinstance(false, IntConstant)
+    assert false.value == 0
+    assert false.type.width == 64
+
+    select2 = block.instructions[9]
+    assert select2.opcode == Opcode.SELECT
+    assert select2.name == "val.i.2"
+    assert select2.operands[1].name == "spec.select"
+    assert select2.operands[2].name == "val.i.1"
+
+
+def test_global_string() -> None:
+    bitcode = Path("../pyqir-parser/tests/hello.bc").read_bytes()
+    mod = Module.from_bitcode(bitcode)
+    func_name = "program__main__body"
+    func = next(filter(lambda f: f.name == func_name, mod.functions))
+    assert isinstance(func, Function)
+    assert isinstance(func.basic_blocks[0], BasicBlock)
+    assert func.basic_blocks[0].name == "entry"
+
+    call = func.basic_blocks[0].instructions[0]
+    assert isinstance(call, Call)
+    assert call.callee.name == "__quantum__rt__string_create"
+
+    value = constant_bytes(call.args[0])
+    assert value is not None
+    assert value.decode("utf-8") == "Hello World!\0"
+
+
+def test_parser_zext_support() -> None:
+    bitcode = Path("../pyqir-parser/tests/select.bc").read_bytes()
+    mod = Module.from_bitcode(bitcode)
+    func = next(filter(is_entry_point, mod.functions))
+    block = func.basic_blocks[0]
+    inst = block.instructions[7]
+    assert inst.opcode == Opcode.ZEXT
+
+    ty = inst.type
+    assert isinstance(ty, IntType)
+    assert ty.width == 64
+    assert inst.name == ""
+    assert len(inst.operands) == 1
+
+    operand = inst.operands[0]
+    assert operand.name == ""
+
+    operand_ty = operand.type
+    assert isinstance(operand_ty, IntType)
+    assert operand_ty.width == 1
+
+
+def test_loading_invalid_bitcode() -> None:
+    bitcode = Path("../pyqir-parser/tests/teleportchain.ll.reference").read_bytes()
+    with pytest.raises(ValueError) as e:
+        Module.from_bitcode(bitcode)
+    assert e.value.args[0] == "Invalid bitcode signature"
+
+
+def test_parser_internals() -> None:
+    bitcode = Path("../pyqir-parser/tests/teleportchain.baseprofile.bc").read_bytes()
+    mod = Module.from_bitcode(bitcode)
+    func_name = (
+        "TeleportChain__DemonstrateTeleportationUsingPresharedEntanglement__Interop"
+    )
+    func = next(filter(lambda f: f.name == func_name, mod.functions))
+    assert len(func.params) == 0
+    assert isinstance(func.type.ret, IntType)
+
+    func_list = mod.functions
+    assert len(func_list) == 21
+    assert func_list[0].name == func_name
+
+    interop_funcs = list(filter(is_interop_friendly, mod.functions))
+    assert len(interop_funcs) == 1
+    assert interop_funcs[0].name == func_name
+    assert required_num_qubits(interop_funcs[0]) == 6
+
+    attribute = interop_funcs[0].attribute("requiredQubits")
+    assert attribute is not None
+    assert attribute.value == "6"
+
+    blocks = func.basic_blocks
+    assert len(blocks) == 9
+
+    entry_block = blocks[0]
+    assert entry_block.name == "entry"
+    assert len(entry_block.instructions) == 12
+
+    term = entry_block.terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+    assert term.operands[2].name == "then0__1.i.i.i"
+    assert term.operands[1].name == "continue__1.i.i.i"
+
+    term = blocks[1].terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+    assert isinstance(term.operands[0], BasicBlock)
+    assert term.operands[0].name == "continue__1.i.i.i"
+
+    term = blocks[8].terminator
+    assert term is not None
+    assert term.opcode == Opcode.RET
+
+    call = entry_block.instructions[0]
+    assert isinstance(call, Call)
+    assert call.callee.name == "__quantum__qis__h__body"
+
+    args = call.args
+    assert len(args) == 1
+
+    arg = args[0]
+    assert isinstance(arg, Constant)
+    assert is_qubit(arg.type)
+    assert qubit_id(arg) == 0
+
+    call = entry_block.instructions[8]
+    assert isinstance(call, Call)
+    assert call.callee.name == "__quantum__qis__mz__body"
+
+    arg = call.args[0]
+    assert isinstance(arg, Constant)
+    assert qubit_id(arg) == 1
+
+    arg = call.args[1]
+    assert isinstance(arg, Constant)
+    assert result_id(arg) == 0
+
+    term = entry_block.terminator
+    assert term is not None
+    assert term.opcode == Opcode.BR
+
+    branch_cond = term.operands[0]
+    assert isinstance(branch_cond, Call)
+
+    arg = branch_cond.args[0]
+    assert isinstance(arg, Constant)
+    assert result_id(arg) == 0
+    assert branch_cond.name == ""
+
+    call = entry_block.instructions[10]
+    assert isinstance(call, Call)
+    assert call.callee.name == "__quantum__qir__read_result"
+
+    arg = call.args[0]
+    assert isinstance(arg, Constant)
+    assert result_id(arg) == 0
+
+    assert not entry_block.instructions[10].type.is_void
+    assert entry_block.instructions[10].name == ""
diff --git a/pyqir/tests/test_qis.py b/pyqir/tests/test_qis.py
index b3d1196e..2b66bad8 100644
--- a/pyqir/tests/test_qis.py
+++ b/pyqir/tests/test_qis.py
@@ -1,7 +1,7 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
-from pyqir.generator import BasicQisBuilder, SimpleModule, TypeFactory, Value, const
+from pyqir import BasicQisBuilder, SimpleModule, TypeFactory, Value, const
 from typing import Callable, List, Tuple, Union
 import unittest
 
diff --git a/pyqir/tests/test_teleport.py b/pyqir/tests/test_teleport.py
index b615707d..4f0eb873 100644
--- a/pyqir/tests/test_teleport.py
+++ b/pyqir/tests/test_teleport.py
@@ -1,14 +1,14 @@
 # Copyright (c) Microsoft Corporation.
 # Licensed under the MIT License.
 
+from pyqir import BasicQisBuilder, SimpleModule, Value
 from pyqir.evaluator import GateLogger, GateSet, NonadaptiveEvaluator
-from pyqir.generator import BasicQisBuilder, SimpleModule, Value
 import tempfile
-from typing import List, Tuple
+from typing import List, Sequence
 
 
 def _teleport(
-    qis: BasicQisBuilder, qubits: Tuple[Value, ...], results: Tuple[Value, ...]
+    qis: BasicQisBuilder, qubits: Sequence[Value], results: Sequence[Value]
 ) -> None:
     msg = qubits[0]
     target = qubits[1]