Skip to content

Commit

Permalink
Add bitcode/qir conversion functions to generator (#115)
Browse files Browse the repository at this point in the history
* Adding new functions
* fixing bitcode python return type.
  • Loading branch information
idavis authored May 9, 2022
1 parent 9ae0395 commit 584b406
Show file tree
Hide file tree
Showing 7 changed files with 243 additions and 5 deletions.
2 changes: 2 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

## [Unreleased]

- Add bitcode/qir conversion functions to generator by @idavis in https://github.com/qir-alliance/pyqir/pull/115

## [0.4.0a1] - 2022-04-18

- Adding static result allocation by @idavis in https://github.com/qir-alliance/pyqir/pull/103
Expand Down
2 changes: 2 additions & 0 deletions pyqir-generator/pyqir/generator/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
Qubit as Qubit,
ResultRef as ResultRef,
SimpleModule as SimpleModule,
ir_to_bitcode as ir_to_bitcode,
bitcode_to_ir as bitcode_to_ir,
)

from pyqir.generator._values import Value as Value
25 changes: 24 additions & 1 deletion pyqir-generator/pyqir/generator/_native.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,30 @@

from pyqir.generator import types
from pyqir.generator._values import Value
from typing import Callable, Sequence, Tuple
from typing import Callable, Optional, Sequence, Tuple


def ir_to_bitcode(ir: str, module_name: Optional[str], source_file_name: Optional[str]) -> bytes:
"""
Converts the supplied QIR string to its bitcode equivalent.
:param ir: The QIR string to convert
:param module_name: The name of the QIR module, default is "" if None
:param source_file_name: The source file name of the QIR module. Unchanged if None
:return: The equivalent bitcode as bytes.
"""
...

def bitcode_to_ir(bitcode: bytes, module_name: Optional[str], source_file_name: Optional[str]) -> str:
"""
Converts the supplied bitcode to its QIR string equivalent.
:param ir: The bitcode bytes to convert
:param module_name: The name of the QIR module, default is "" if None
:param source_file_name: The source file name of the QIR module. Unchanged if None
:return: The equivalent QIR string.
"""
...


class Qubit:
Expand Down
42 changes: 38 additions & 4 deletions pyqir-generator/src/python.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use pyo3::{
basic::CompareOp,
exceptions::{PyOSError, PyOverflowError, PyTypeError, PyValueError},
prelude::*,
types::PySequence,
types::{PyBytes, PySequence, PyString, PyUnicode},
PyObjectProtocol,
};
use qirlib::generation::{
Expand All @@ -21,6 +21,32 @@ use std::{
vec,
};

#[pyfunction]
#[allow(clippy::needless_pass_by_value)]
fn ir_to_bitcode<'a>(
py: Python<'a>,
value: &str,
module_name: Option<String>,
source_file_name: Option<String>,
) -> PyResult<&'a PyBytes> {
let bitcode = qirlib::generation::ir_to_bitcode(value, &module_name, &source_file_name)
.map_err(PyOSError::new_err)?;
Ok(PyBytes::new(py, &bitcode))
}

#[pyfunction]
#[allow(clippy::needless_pass_by_value)]
fn bitcode_to_ir<'a>(
py: Python<'a>,
value: &PyBytes,
module_name: Option<String>,
source_file_name: Option<String>,
) -> PyResult<&'a PyString> {
let ir = qirlib::generation::bitcode_to_ir(value.as_bytes(), &module_name, &source_file_name)
.map_err(PyOSError::new_err)?;
Ok(PyUnicode::new(py, ir.as_str()))
}

#[pymodule]
#[pyo3(name = "_native")]
fn native_module(_py: Python, m: &PyModule) -> PyResult<()> {
Expand All @@ -29,7 +55,12 @@ fn native_module(_py: Python, m: &PyModule) -> PyResult<()> {
m.add_class::<Function>()?;
m.add_class::<Builder>()?;
m.add_class::<SimpleModule>()?;
m.add_class::<BasicQisBuilder>()
m.add_class::<BasicQisBuilder>()?;

m.add_function(wrap_pyfunction!(ir_to_bitcode, m)?)?;
m.add_function(wrap_pyfunction!(bitcode_to_ir, m)?)?;

Ok(())
}

const TYPES_MODULE_NAME: &str = "pyqir.generator.types";
Expand Down Expand Up @@ -310,9 +341,12 @@ impl SimpleModule {
emit::ir(&model).map_err(PyOSError::new_err)
}

fn bitcode(&self, py: Python) -> PyResult<Vec<u8>> {
fn bitcode<'a>(&self, py: Python<'a>) -> PyResult<&'a PyBytes> {
let model = self.model_from_builder(py);
emit::bitcode(&model).map_err(PyOSError::new_err)
match emit::bitcode(&model) {
Ok(bitcode) => Ok(PyBytes::new(py, &bitcode[..])),
Err(err) => Err(PyOSError::new_err(err)),
}
}

fn add_external_function(&mut self, py: Python, name: String, ty: PyFunctionType) -> Function {
Expand Down
56 changes: 56 additions & 0 deletions pyqir-generator/tests/test_conversions.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

from pyqir.generator import BasicQisBuilder, SimpleModule, ir_to_bitcode, bitcode_to_ir


def get_module() -> SimpleModule:
mod = SimpleModule("test", 1, 1)
qis = BasicQisBuilder(mod.builder)
qis.m(mod.qubits[0], mod.results[0])
return mod

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")
assert expected_ir == converted_ir

def test_ir_round_trip_is_not_identical_when_module_name_isnot_supplied() -> None:
expected_ir = get_module().ir()
bitcode = ir_to_bitcode(expected_ir)
converted_ir = bitcode_to_ir(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")
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")
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")
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")
assert isinstance(converted_ir, str)

def test_bitcode_returns_bytes_type() -> None:
bitcode = get_module().bitcode()
assert isinstance(bitcode, bytes)

def test_ir_returns_str_type() -> None:
expected_ir = get_module().ir()
assert isinstance(expected_ir, str)
118 changes: 118 additions & 0 deletions qirlib/src/generation/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,121 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use inkwell::{context::Context, memory_buffer::MemoryBuffer};

use crate::module;

pub mod emit;
pub mod interop;
pub mod qir;

/// # Errors
///
/// Will return `Err` if a module cannot be created from the supplied IR
pub fn ir_to_bitcode(
value: &str,
module_name: &Option<String>,
source_file_name: &Option<String>,
) -> Result<Vec<u8>, String> {
let context = Context::create();
let bytes = value.as_bytes();
let buffer_name = match module_name {
Some(name) => name.as_str(),
None => "",
};
let memory_buffer = MemoryBuffer::create_from_memory_range_copy(bytes, buffer_name);
let module = context
.create_module_from_ir(memory_buffer)
.map_err(|err| err.to_string())?;

if let Some(source_name) = source_file_name {
module.set_source_file_name(source_name.as_str());
}

let bitcode = module.write_bitcode_to_memory().as_slice().to_owned();
Ok(bitcode)
}

/// # Errors
///
/// Will return `Err` if a module cannot be created from the supplied bitcode
pub fn bitcode_to_ir(
value: &[u8],
module_name: &Option<String>,
source_file_name: &Option<String>,
) -> Result<String, String> {
let context = Context::create();
let buffer_name = match module_name.as_ref() {
Some(name) => name.as_str(),
None => "",
};
let module = module::load_memory(value, buffer_name, &context)?;

if let Some(source_name) = source_file_name.as_ref() {
module.set_source_file_name(source_name.as_str());
}

let ir = module.print_to_string().to_string();

Ok(ir)
}

#[cfg(test)]
mod module_conversion_tests {
use std::collections::HashMap;

use crate::generation::emit;

use super::interop::{
ClassicalRegister, Instruction, Measured, QuantumRegister, SemanticModel,
};

use super::*;

fn get_model(
name: String,
use_static_qubit_alloc: bool,
use_static_result_alloc: bool,
) -> SemanticModel {
SemanticModel {
name,
registers: vec![ClassicalRegister::new("r".to_string(), 1)],
qubits: vec![QuantumRegister::new("q".to_string(), 0)],
instructions: vec![Instruction::M(Measured::new(
"q0".to_string(),
"r0".to_string(),
))],
use_static_qubit_alloc,
use_static_result_alloc,
external_functions: HashMap::new(),
}
}

#[test]
fn ir_round_trip_is_identical() -> Result<(), String> {
let model = get_model("test".to_owned(), false, false);
let actual_ir: String = emit::ir(&model)?;
let bitcode = ir_to_bitcode(actual_ir.as_str(), &None, &None)?;
let converted_ir = bitcode_to_ir(
bitcode.as_slice(),
&Some("test".to_owned()),
&Some("test".to_owned()),
)?;
assert_eq!(actual_ir, converted_ir);
Ok(())
}

#[test]
fn module_name_is_normalized() -> Result<(), String> {
let model = get_model("tests".to_owned(), false, false);
let actual_ir: String = emit::ir(&model)?;
let bitcode = ir_to_bitcode(actual_ir.as_str(), &None, &None)?;
let converted_ir = bitcode_to_ir(
bitcode.as_slice(),
&Some("tests".to_owned()),
&Some("tests".to_owned()),
)?;
assert_eq!(actual_ir, converted_ir);
Ok(())
}
}
3 changes: 3 additions & 0 deletions qirlib/src/generation/qir/result.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use crate::codegen::CodeGenerator;
use inkwell::values::{BasicMetadataValueEnum, IntValue, PointerValue};

Expand Down

0 comments on commit 584b406

Please sign in to comment.