diff --git a/setup.py b/setup.py index 0465345b..68aae185 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,8 @@ version="0.2.0", license="Apache License 2.0", python_requires=">= 3.10", - packages=find_namespace_packages(where="src", exclude=("test",)), + packages=find_namespace_packages(where="src", exclude=("test",)) + + ["qir2qasm_trans", "qir2qasm_trans.qir_trans"], package_dir={"": "src"}, install_requires=[ "amazon-braket-sdk>=1.89.1", @@ -36,6 +37,8 @@ "gast", "termcolor", "openqasm_pygments", + "llvmlite", + "pyqir", ], extras_require={ "test": [ diff --git a/src/autoqasm/__init__.py b/src/autoqasm/__init__.py index 882cb898..bdfbce96 100644 --- a/src/autoqasm/__init__.py +++ b/src/autoqasm/__init__.py @@ -44,6 +44,12 @@ def my_program(): result[1] = measure __qubits__[1]; """ +# Create an alias so it can be accessed as autoqasm.qir2qasm_trans +import sys + +# Import qir2qasm_trans module to make it available as autoqasm.qir2qasm_trans +import qir2qasm_trans # noqa: F401 + from . import errors, instructions, operators # noqa: F401 from .api import gate, gate_calibration, main, subroutine # noqa: F401 from .instructions import QubitIdentifierType as Qubit # noqa: F401 @@ -52,6 +58,8 @@ def my_program(): from .types import ArrayVar, BitVar, BoolVar, FloatVar, IntVar # noqa: F401 from .types import Range as range # noqa: F401 +sys.modules[__name__ + ".qir2qasm_trans"] = qir2qasm_trans + def __getattr__(name): if name == "qubits": diff --git a/src/qir2qasm_trans/QIR-example/CUDAQ_bell.ll b/src/qir2qasm_trans/QIR-example/CUDAQ_bell.ll new file mode 100644 index 00000000..9c08f763 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/CUDAQ_bell.ll @@ -0,0 +1,40 @@ +; ModuleID = 'LLVMDialectModule' +source_filename = "LLVMDialectModule" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128" +target triple = "x86_64-unknown-linux-gnu" + +%Qubit = type opaque +%Result = type opaque + +@cstr.72303030303000 = private constant [7 x i8] c"r00000\00" +@cstr.72303030303100 = private constant [7 x i8] c"r00001\00" + +define void @__nvqpp__mlirgen____nvqppBuilderKernel_R23EYF50OU() local_unnamed_addr #0 { +"0": + tail call void @__quantum__qis__h__body(%Qubit* null) + tail call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* nonnull inttoptr (i64 1 to %Qubit*)) + tail call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null) + tail call void @__quantum__qis__mz__body(%Qubit* nonnull inttoptr (i64 1 to %Qubit*), %Result* nonnull writeonly inttoptr (i64 1 to %Result*)) + tail call void @__quantum__rt__result_record_output(%Result* null, i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @cstr.72303030303000, i64 0, i64 0)) + tail call void @__quantum__rt__result_record_output(%Result* nonnull inttoptr (i64 1 to %Result*), i8* nonnull getelementptr inbounds ([7 x i8], [7 x i8]* @cstr.72303030303100, i64 0, i64 0)) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) local_unnamed_addr + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) local_unnamed_addr #1 + +declare void @__quantum__rt__result_record_output(%Result*, i8*) local_unnamed_addr + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) local_unnamed_addr + +attributes #0 = { "entry_point" "output_labeling_schema"="schema_id" "output_names"="[[[0,[0,\22r00000\22]],[1,[1,\22r00001\22]]]]" "qir_profiles"="base_profile" "requiredQubits"="2" "requiredResults"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} + +!0 = !{i32 2, !"Debug Info Version", i32 3} +!1 = !{i32 1, !"qir_major_version", i32 1} +!2 = !{i32 7, !"qir_minor_version", i32 0} +!3 = !{i32 1, !"dynamic_qubit_management", i1 false} +!4 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/src/qir2qasm_trans/QIR-example/arithmetic.ll b/src/qir2qasm_trans/QIR-example/arithmetic.ll new file mode 100644 index 00000000..df454ad8 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/arithmetic.ll @@ -0,0 +1,27 @@ +; ModuleID = 'arithmetic' +source_filename = "arithmetic" + +define void @main() #0 { +entry: + %0 = call i32 @get_int() + %1 = add i32 3, %0 + %2 = mul i32 2, %1 + %3 = call i32 @get_int() + %4 = sub i32 0, %3 + call void @take_int(i32 %2) + call void @take_int(i32 %4) + ret void +} + +declare i32 @get_int() + +declare void @take_int(i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/src/qir2qasm_trans/QIR-example/base_profile.ll b/src/qir2qasm_trans/QIR-example/base_profile.ll new file mode 100644 index 00000000..79dd9c42 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/base_profile.ll @@ -0,0 +1,69 @@ +; type definitions + +%Result = type opaque +%Qubit = type opaque + +; global constants (labels for output recording) + +@0 = internal constant [3 x i8] c"r1\00" +@1 = internal constant [3 x i8] c"r2\00" + +; entry point definition + +define i64 @Entry_Point_Name() #0 { +entry: + ; calls to initialize the execution environment + call void @__quantum__rt__initialize(i8* null) + br label %body + +body: ; preds = %entry + ; calls to QIS functions that are not irreversible + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) + br label %measurements + +measurements: ; preds = %body + ; calls to QIS functions that are irreversible + call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* writeonly inttoptr (i64 1 to %Result*)) + br label %output + +output: ; preds = %measurements + ; calls to record the program output + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([3 x i8], [3 x i8]* @0, i32 0, i32 0)) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* getelementptr inbounds ([3 x i8], [3 x i8]* @1, i32 0, i32 0)) + + ret i64 0 +} + +; declarations of QIS functions + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +; declarations of runtime functions for initialization and output recording + +declare void @__quantum__rt__initialize(i8*) + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +; attributes + +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="2" } + +attributes #1 = { "irreversible" } + +; module flags + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/src/qir2qasm_trans/QIR-example/bell_pair.bc b/src/qir2qasm_trans/QIR-example/bell_pair.bc new file mode 100644 index 00000000..a87f6bae Binary files /dev/null and b/src/qir2qasm_trans/QIR-example/bell_pair.bc differ diff --git a/src/qir2qasm_trans/QIR-example/bell_pair.ll b/src/qir2qasm_trans/QIR-example/bell_pair.ll new file mode 100644 index 00000000..dd4a0aeb --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/bell_pair.ll @@ -0,0 +1,30 @@ +; ModuleID = 'bell' +source_filename = "bell" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/src/qir2qasm_trans/QIR-example/bernstein_vazirani.ll b/src/qir2qasm_trans/QIR-example/bernstein_vazirani.ll new file mode 100644 index 00000000..bf9a13af --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/bernstein_vazirani.ll @@ -0,0 +1,51 @@ +; ModuleID = 'python2qir' +source_filename = "python2qir" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 4 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 5 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__z__body(%Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 5 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 5 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 4 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 2 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 3 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 4 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 5 to %Qubit*), %Result* inttoptr (i64 5 to %Result*)) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__z__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="7" "required_num_results"="6" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/src/qir2qasm_trans/QIR-example/dynamic_allocation.bc b/src/qir2qasm_trans/QIR-example/dynamic_allocation.bc new file mode 100644 index 00000000..8b669717 Binary files /dev/null and b/src/qir2qasm_trans/QIR-example/dynamic_allocation.bc differ diff --git a/src/qir2qasm_trans/QIR-example/dynamic_allocation.ll b/src/qir2qasm_trans/QIR-example/dynamic_allocation.ll new file mode 100644 index 00000000..199cc66f --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/dynamic_allocation.ll @@ -0,0 +1,49 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare %Result* @__quantum__rt__result_get_one() + +declare i1 @__quantum__rt__result_equal(%Result*, %Result*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) + +define void @main() #0 { +entry: + %0 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %0) + %1 = call %Result* @__quantum__qis__m__body(%Qubit* %0) + %2 = call %Result* @__quantum__rt__result_get_one() + %3 = call i1 @__quantum__rt__result_equal(%Result* %1, %Result* %2) + br i1 %3, label %then, label %else + +then: ; preds = %entry + call void @__quantum__qis__reset__body(%Qubit* %0) + br label %continue + +else: ; preds = %entry + br label %continue + +continue: ; preds = %else, %then + call void @__quantum__rt__qubit_release(%Qubit* %0) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__reset__body(%Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/QIR-example/if_then1.ll b/src/qir2qasm_trans/QIR-example/if_then1.ll new file mode 100644 index 00000000..c055454d --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/if_then1.ll @@ -0,0 +1,34 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A: + %0 = call i32 @get_int() + br i1 0, label %B, label %D + +B: ; preds = %entry + %1 = add i32 1, %0 + br i1 0, label %C, label %D + +C: ; preds = %entry + %2 = add i32 2, %0 + br label %D + +D: ; preds = %entry + %3 = add i32 3, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/QIR-example/if_then2.ll b/src/qir2qasm_trans/QIR-example/if_then2.ll new file mode 100644 index 00000000..e515d0b8 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/if_then2.ll @@ -0,0 +1,43 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %D3, label %E4 + +C2: + %3 = add i32 2, %0 + br label %E4 + +D3: + %4 = add i32 3, %0 + br label %F5 + +E4: + %5 = add i32 4, %0 + br label %F5 + +F5: + %6 = add i32 5, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/QIR-example/test.ll b/src/qir2qasm_trans/QIR-example/test.ll new file mode 100644 index 00000000..79864b46 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/test.ll @@ -0,0 +1,39 @@ +; ModuleID = 'while_test' +source_filename = "while_test" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %B1, label %E4 + +C2: + %3 = add i32 2, %0 + br i1 0, label %E4, label %D3 + +D3: + %4 = add i32 3, %0 + br i1 0, label %C2, label %D3 + +E4: + %5 = add i32 4, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/QIR-example/while1.ll b/src/qir2qasm_trans/QIR-example/while1.ll new file mode 100644 index 00000000..aa0f7f04 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/while1.ll @@ -0,0 +1,35 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br label %B1 + +B1: + %2 = add i32 1, %0 + br i1 0, label %C2, label %D3 + +C2: + %3 = add i32 2, %0 + br label %B1 + +D3: + %4 = add i32 3, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/QIR-example/while2.ll b/src/qir2qasm_trans/QIR-example/while2.ll new file mode 100644 index 00000000..73376c63 --- /dev/null +++ b/src/qir2qasm_trans/QIR-example/while2.ll @@ -0,0 +1,41 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + br label %B1 + +B1: + %1 = add i32 1, %0 + br label %C2 + +C2: + %2 = add i32 2, %0 + br label %D3 + +D3: + %3 = add i32 3, %0 + br i1 0, label %B1, label %E4 + +E4: + %4 = add i32 4, %0 + br i1 0, label %F5, label %C2 + +F5: + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/src/qir2qasm_trans/__init__.py b/src/qir2qasm_trans/__init__.py new file mode 100644 index 00000000..69aa421f --- /dev/null +++ b/src/qir2qasm_trans/__init__.py @@ -0,0 +1 @@ +# QIR to QASM translation module diff --git a/src/qir2qasm_trans/qir_trans/__init__.py b/src/qir2qasm_trans/qir_trans/__init__.py new file mode 100644 index 00000000..1e85c3f8 --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/__init__.py @@ -0,0 +1,73 @@ +import os + +# from pyqir import Context, Module +from llvmlite import binding +from llvmlite.binding.module import ModuleRef + +binding.initialize() +binding.initialize_native_target() +binding.initialize_native_asmprinter() + + +def load(filename: str) -> ModuleRef: + """Load a QIR program from the file ``filename``. + + Args: + filename: the filename to load the program from. + + Returns: + Module: -- + + Raises: + -- + """ + ext = os.path.splitext(filename)[1].lower() + + if ext == ".bc": + with open(filename, "rb") as f: + bitcode = f.read() + module_ref = loads_bc(bitcode) + elif ext == ".ll": + with open(filename, "r") as f: + ir_text = f.read() + module_ref = loads_ll(ir_text) + else: + raise ValueError(f"Unsupported file extension: {ext}") + + module_ref.verify() + + return module_ref + + +def loads_bc(bitcode: bytes) -> ModuleRef: + """Load a QIR program from the given string. + + Args: + program: the QIR program. + + Returns: + Module: -- + + Raises: + -- + """ + context = binding.create_context() + module = binding.parse_bitcode(bitcode, context) + return module + + +def loads_ll(ir_text: str) -> ModuleRef: + """Load a QIR program from the given string. + + Args: + program: the QIR program. + + Returns: + Module: -- + + Raises: + -- + """ + context = binding.create_context() + module = binding.parse_assembly(ir_text, context) + return module diff --git a/src/qir2qasm_trans/qir_trans/builder.py b/src/qir2qasm_trans/qir_trans/builder.py new file mode 100644 index 00000000..8308274e --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/builder.py @@ -0,0 +1,781 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +"""Builders for QIR → OpenQASM 3 translator""" + +import re +from dataclasses import dataclass +from typing import Callable, Dict, List, Optional, Tuple, Union + +import networkx as nx +from llvmlite.binding.module import ValueRef +from llvmlite.binding.typeref import TypeRef +from llvmlite.ir import FunctionType, IdentifiedStructType +from openqasm3 import ast + + +class FunctionBuilder: + """Building interface for the QIR function-call instruction (`call ...`) + + Subclasses should override `building` + """ + + def building( + self, symbols, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + """Build OpenQASM statements for a single QIR `call` instruction + + Args: + symbols (SymbolTable): Translation context. + ret_type (TypeRef): Return type of the QIR instruction. + operands (List[ValueRef]): Operands of the QIR instruction. + + Returns: + A tuple (ret_ident, statements) where: + - ret_ident: OpenQASM identifier for the result of the QIR instruction + (i.e., `%1 = call ...`). + - statements: List of OpenQASM statements for the QIR instruction. + """ + return None, [] + + +@dataclass +class FunctionInfo: + """Registry entry for a QIR function""" + + type: FunctionType # LLVM function type + def_statement: Optional[ast.QuantumGateDefinition] # QASM gate definitions (i.e. Rzz gate) + builder: FunctionBuilder # Calling builder for the function + + +class InstructionBuilder: + """Building interface for the LLVM IR instructions (i.e. `inttoptr`). A list of + LLVM instructions is in `https://github.com/qir-alliance/qir-spec/blob/main/specification + /under_development/profiles/Base_Profile.md#classical-instructions" + + Subclasses should override `building` + """ + + def building( + self, + **kwargs, + ) -> Optional[ast.IndexedIdentifier]: + return None + + +@dataclass +class InstructionInfo: + """Registry entry for a LLVM IR instruction""" + + builder: InstructionBuilder + + +class DeclBuilder: + """Building interface for the QIR struct declarations (i.e. `Qubit`, `Result`) + + Subclasses should override `building` + """ + + def building(self, name: str, size: int) -> ast.Statement: + return None + + +@dataclass +class StructInfo: + """Registry entry for a QIR struct""" + + type_qir: IdentifiedStructType # LLVM identified struct type + type_ast: Union[ast.ClassicalType, str] # Target OpenQASM AST representation + decl_builder: DeclBuilder # Declaration builder for the struct + static_name: str # Static register name for the struct + + +@dataclass +class BranchInfo: + """Branch metadata for a basic block.""" + + branch_condition: Optional[ast.Expression] # Conditional expression + branch_targets: List[str] # Target block labels (true/false) + + +class SymbolTable: + """Holds all translation-time symbols and contextual counters.""" + + def __init__(self): + # Struct / LLVM IR / QIR function registries + self.structs: Dict[str, StructInfo] = {} + self.instructions: Dict[str, InstructionInfo] = {} + self.functions: Dict[str, FunctionInfo] = {} + + # OpenQASM identifier for the SSA value (i.e. `%1 = ...`) + self.variables: Dict[str, Union[ast.IndexedIdentifier, ast.Identifier]] = {} + + # I/O Tags for the OpenQASM variables + self.io_variables: Dict[str, List[Tuple[ast.ClassicalType, ast.Identifier]]] = { + "input": [], + "output": [], + } + + # Register naming function + self.tmp_naming: Callable = lambda sname: f"{sname}_tmp" + self.io_naming: Callable = lambda sname, io_key, idx: f"{sname}_{io_key[0]}{idx}" + + # Counters for sizing OpenQASM temporaries and constants (i.e. `qubit[n]`) + self.structs_num: Dict[str, int] = {} + self.structs_tmp_num: Dict[str, int] = {} + self.classical_tmp: Dict[str, ast.ClassicalType] = {} + self.classical_tmp_num: Dict[str, int] = {} + + # Control-flow scaffolding + self.entry_block: str = None + self.block_statements: Dict[str, List[ast.Statement]] = {} + self.block_branchs: Dict[str, BranchInfo] = {} + self.cfg: nx.DiGraph = nx.DiGraph() + self.block_tmp_num: int = 0 + + def register_struct(self, name: str, info: StructInfo): + """Register a struct type and initialize counters.""" + self.structs[name] = info + self.structs_num[name] = 0 + self.structs_tmp_num[name] = 0 + + def record_variables(self, inst: ValueRef, ident: Union[ast.IndexedIdentifier, ast.Identifier]): + """Record the QASM identifier produced for the SSA value of an LLVM instruction""" + self.variables[str(inst)] = ident + + def get_variables( + self, inst_str: str + ) -> Optional[Union[ast.IndexedIdentifier, ast.Identifier]]: + """Lookup the QASM identifier previously bound to the given SSA string.""" + return self.variables.get(inst_str) + + def type_qir2qasm(self, tp: TypeRef): + """Translate a QIR (LLVM) TypeRef into a (kind_str, AST type) pair. + Args: + tp (TypeRef): LLVM type need to translated. + + Returns: + (type_str, type_ast) + - type_str: string name of the LLVM type kind (e.g., "integer", "struct", "pointer"). + - type_ast: OpenQASM AST type to use, or a struct name (str), or None for void. + """ + type_kind = tp.type_kind + type_str = type_kind.name + + _FLOAT_BIT_WIDTHS = { + "half": 16, + "float": 32, + "double": 64, + "x86_fp80": 80, + "fp128": 128, + "ppc_fp128": 128, + } + + if type_str == "void": + type_ast = None + elif type_str in _FLOAT_BIT_WIDTHS.keys(): + width = _FLOAT_BIT_WIDTHS[type_str] + type_ast = ast.FloatType(size=ast.IntegerLiteral(value=width)) + elif type_str == "integer": + if tp.type_width == 1: + type_ast = ast.BoolType() + else: + type_ast = ast.IntType(size=ast.IntegerLiteral(value=tp.type_width)) + elif type_str == "struct": + type_ast = tp.name + # elif type_str == "array": + # TODO: Support array types if needed. + elif type_str == "pointer": + type_qasm = self.type_qir2qasm(tp.element_type) + if type_qasm[0] != "pointer": + # return the `ast_type` of the element + type_ast = type_qasm[1] + else: + # Reject pointer-to-pointer + raise Exception("Unsupported ptr to ptr!") + # elif type_str == "vector": + # TODO: Support vector types if needed. + else: + raise Exception(f"{type_str} Undefined!") + return type_str, type_ast + + def value_qir2qasm(self, value_qir: ValueRef) -> Union[ast.IndexedIdentifier, ast.Expression]: + """Translate a QIR value into an OpenQASM expression or identifier. + + Args: + value_qir (ValueRef): LLVM/QIR value to translate. + + Returns: + Union[ast.IndexedIdentifier, ast.Expression]: + - If the value is a constant integer or float, returns the + corresponding OpenQASM literal (`IntegerLiteral` or `FloatLiteral`). + - If the value encodes a supported LLVM IR (e.g., `inttoptr`), + dispatches to the appropriate `InstructionBuilder` and returns the + resulting identifier. + - Otherwise, returns the identifier previously recorded in the + symbol table via `record_variables()`. + """ + if value_qir.is_constant: + value = value_qir.get_constant_value() + value_ast = None + + if isinstance(value, int): + value_ast = ast.IntegerLiteral(value) + elif isinstance(value, float): + value_ast = ast.FloatLiteral(value) + else: + # Many QIR "constant strings" are actually instruction encodings (e.g., inttoptr). + assert isinstance(value, str) + for inst_builder_info in self.instructions.values(): + inst_builder = inst_builder_info.builder + value_ast = inst_builder.building(self, value_qir) + if value_ast: + break + else: + raise Exception("Undefined llvm insturction!") + else: + # Non-constant: expect it was recorded earlier via `record_variables()`. + value_ast = self.get_variables(str(value_qir)) + return value_ast + + def alloc_tmp_var(self, type_ast: Union[ast.ClassicalType, str]) -> ast.IndexedIdentifier: + """Allocate a temporary variable for either a classical type or a struct. + + Args: + type_ast (Union[ast.ClassicalType, str]): The type of the temporary. + - If a string, it is interpreted as the name of a registered + struct (e.g., "Qubit"). + - If an OpenQASM classical type, a temporary of that type is allocated. + + Returns: + ast.IndexedIdentifier: An indexed identifier referencing the allocated + temporary. The identifier name follows: + - `_tmp[]` for struct temporaries. + - `_tmp[]` for classical temporaries. + """ + if isinstance(type_ast, str): + # Struct temporary buffer. + name = type_ast + idx = self.structs_tmp_num[name] + self.structs_tmp_num[name] += 1 + ident_name = self.tmp_naming(self.structs[name].static_name) + var_ast = ast.IndexedIdentifier( + name=ast.Identifier(name=ident_name), indices=[[ast.IntegerLiteral(value=idx)]] + ) + else: + # Classical temporary buffer. + assert isinstance(type_ast, ast.ClassicalType) + name = type_ast.__class__.__name__ + idx = self.classical_tmp_num.get(name) + if idx is None: + idx = 0 + self.classical_tmp_num[name] = 1 + self.classical_tmp[name] = type_ast + else: + self.classical_tmp_num[name] += 1 + ident_name = self.tmp_naming(name) + var_ast = ast.IndexedIdentifier( + name=ast.Identifier(name=ident_name), indices=[[ast.IntegerLiteral(value=idx)]] + ) + return var_ast + + def alloc_io_var(self, type_ast: ast.ClassicalType, io_key: str) -> ast.Identifier: + """Allocate a fresh I/O identifier for inputs or outputs. + + Args: + type_ast (ast.ClassicalType): The OpenQASM classical type of the variable + (e.g., IntType, FloatType). + io_key (str): Either `"input"` or `"output"`, indicating the I/O role. + + Returns: + ast.Identifier: A new identifier with the naming convention + `_`, for example: + - `IntType_i0` for the first integer input. + - `FloatType_o1` for the second float output. + """ + name = type_ast.__class__.__name__ + idx = len(self.io_variables[io_key]) + ident_name = self.io_naming(name, io_key, idx) + var_ast = ast.Identifier(name=ident_name) + self.io_variables[io_key].append((type_ast, var_ast)) + return var_ast + + def new_tmp_block_name(self) -> str: + """Gnerate the name for a new temporary block in control flow.""" + block_name = f"_tmp_block_{self.block_tmp_num}" + self.block_tmp_num += 1 + return block_name + + +## StructBuilder implementations + + +class QubitDeclarationBuilder(DeclBuilder): + """Declare `qubit name[size]`.""" + + def building(self, name: str, size: int) -> ast.Statement: + ident = ast.Identifier(name=name) + size_exp = ast.IntegerLiteral(value=size) + return ast.QubitDeclaration(qubit=ident, size=size_exp) + + +class ResultDeclarationBuilder(DeclBuilder): + """Declare a classical bit array for measurement results: `bit[size] name;`.""" + + def building(self, name: str, size: int) -> ast.Statement: + ident = ast.Identifier(name=name) + size_exp = ast.IntegerLiteral(value=size) + decl_type = ast.BitType(size=size_exp) + return ast.ClassicalDeclaration(type=decl_type, identifier=ident) + + +class ClassicalDeclarationBuilder(DeclBuilder): + """Declare an array of a given classical base type: `[size] name;`.""" + + def __init__(self, base_type: ast.ClassicalType): + self.base_type = base_type + + def building(self, name: str, size: int) -> ast.Statement: + ident = ast.Identifier(name=name) + size_exp = ast.IntegerLiteral(value=size) + decl_type = ast.ArrayType(base_type=self.base_type, dimensions=[size_exp]) + return ast.ClassicalDeclaration(type=decl_type, identifier=ident) + + +## InstructionBuilder implementations + + +class InttoptrBuilder(InstructionBuilder): + """Translate an `inttoptr`-encoded constant into an OpenQASM indexed identifier. + + Interprets strings like: + - `%StructName* null` + - `inttoptr (i64 to %StructName*)` + + Produces: + `s[]` (pluralized buffer of structs) + """ + + def __init__( + self, + ): + # Pattern for `%Struct* null` + self.null_pattern = r"(?P%\w+\*)\s+null" + + # Pattern for `inttoptr (i64 to %Struct*)` + self.pattern = ( + r"(?P%\w+\*)\s+" + r"inttoptr\s+\(i64\s+(?P\d+)\s+to\s+" + r"(?P=ret_type)" + r"\)" + ) + + def building( + self, + symbols: SymbolTable, + op: ValueRef, + ) -> Optional[ast.IndexedIdentifier]: + op_type = op.type + op_value = op.get_constant_value() + op_name = op_type.element_type.name + + escaped_type = re.escape(str(op_type)) + m1 = re.match(self.null_pattern.format(ret_type=escaped_type), op_value.strip()) + m2 = re.match(self.pattern.format(ret_type=escaped_type), op_value.strip()) + if m1: + idx = 0 + elif m2: + idx = int(m2.group("index")) + else: + return None + + # Track the maximum used index for this struct to size declarations later. + symbols.structs_num[op_name] = max(symbols.structs_num[op_name], idx + 1) + + # Return `s[idx]` + ident_name = symbols.structs[op_name].static_name + return ast.IndexedIdentifier( + name=ast.Identifier(name=ident_name), indices=[[ast.IntegerLiteral(value=idx)]] + ) + + +def identifier2expression( + ident: Union[ast.IndexedIdentifier, ast.Identifier, ast.Expression], +) -> ast.Expression: + """Convert an identifier to an expression node in OpenQASM.""" + if isinstance(ident, ast.Identifier) or isinstance(ident, ast.Expression): + return ident + else: + return ast.IndexExpression(collection=ident.name, index=ident.indices[0]) + + +def preprocess_params( + symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] +) -> Tuple[ + Optional[Union[ast.IndexedIdentifier, ast.Identifier]], + List[ast.Expression], + List[Union[ast.IndexedIdentifier, ast.Identifier]], + Optional[TypeRef], + Optional[Union[ast.IndexedIdentifier, ast.Identifier]], +]: + """Preprocess parameters for lowering a QIR call to an unknown OpenQASM function. + + This helper analyzes the return type and operands to split them into + classical arguments, qubit operands, and potential assignment targets. + + Args: + symbols (SymbolTable): Translation context. + ret_type (TypeRef): Return type of the QIR instruction. + operands (List[ValueRef]): Operands of the QIR instruction. + + Returns: + A tuple (ret_ident, arguments, qubits, assign_ident) where: + - ret_ident: Optional identifier for the return SSA value + (`None` for void-returning calls). + - arguments: Classical expressions to be passed as arguments. + - qubits: List of qubit identifiers collected from operands. + - assign_ident: Identifier representing the assignment resulting + from a function return or a pointer argument + (e.g., `typeA func(...)` or `void func(typeA* &arg_A, ...)`). + """ + ret_ident = None + assign_ident = None + arguments = [] + qubits = [] + + # Return Type + ret_type_str, ret_type_ast = symbols.type_qir2qasm(ret_type) + if ret_type_str == "void": + ret_ident = None + else: + # Allocate a temp to hold the return value. + struct_name = ret_type_ast + ret_ident = symbols.alloc_tmp_var(struct_name) + if struct_name == "Qubit": + # Pattern: `Qubit* func(...)` becomes `void func(..., Qubit qubit)` in QASM. + qubits.append(ret_ident) + else: + assign_ident = ret_ident + + # Qubits & Arguments + for op in operands: + op_type_str, op_type_ast = symbols.type_qir2qasm(op.type) + + # Value + op_ident = symbols.value_qir2qasm(op) + if op_type_ast == "Qubit": + qubits.append(op_ident) + else: + # Classical argument + op_ident = identifier2expression(op_ident) + arguments.append(op_ident) + + # Return & Assignment + if op_type_str == "pointer": + # Pattern: `void func(typeA* &arg_A, ...)` becomes `typeA func(type_A arg_A, ...)` + assert assign_ident is None + assign_ident = symbols.value_qir2qasm(op) + + return ret_ident, arguments, qubits, assign_ident + + +class GateBuilder(FunctionBuilder): + """Translate a simple quantum gate call. + + Example: + GateBuilder("rx").building(...) ⇒ `rx(theta) q;` + GateBuilder("u3", adjoint=True) ⇒ `u3dg(...) q;` # using "dg" suffix + """ + + def __init__(self, gate: str, adjoint: bool = False): + if adjoint: + self.ident = ast.Identifier(name=gate + "dg") + else: + self.ident = ast.Identifier(name=gate) + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + assert ret_type.type_kind.name == "void" + + arguments = [] + qubits = [] + for op in operands: + _, type_ast = symbols.type_qir2qasm(op.type) + op_ast = symbols.value_qir2qasm(op) + if type_ast == "Qubit": + qubits.append(op_ast) + else: + op_ast = identifier2expression(op_ast) + arguments.append(op_ast) + + return None, [ + ast.QuantumGate(modifiers=[], name=self.ident, arguments=arguments, qubits=qubits) + ] + + +class ResetBuilder(FunctionBuilder): + """Translate a single-qubit reset operation `reset q;`.""" + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + return None, [ast.QuantumReset(qubits=symbols.value_qir2qasm(operands[0]))] + + +class MeasurementBuilder(FunctionBuilder): + """Translate measurement operations: `m` or `mresetz`. + + - For `m`, allocate a temporary result and assign `measure q -> result`. + - For `mresetz`, measure and then reset the same qubit to |0⟩. + """ + + def __init__(self, name: str): + self.name = name + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + if self.name == "m": + ret_ident = symbols.alloc_tmp_var("Result") + tgt_ident = ret_ident + else: + ret_ident = None + tgt_ident = symbols.value_qir2qasm(operands[1]) + + qubit_ast = symbols.value_qir2qasm(operands[0]) + measure = ast.QuantumMeasurement(qubit=qubit_ast) + statements = [ast.QuantumMeasurementStatement(measure=measure, target=tgt_ident)] + + if self.name == "mresetz": + statements.append(ast.QuantumReset(qubits=qubit_ast)) + + return ret_ident, statements + + +class DefCalBuilder(FunctionBuilder): + """Translate a `defcal` function call.""" + + def __init__(self, name: str): + self.ident = ast.Identifier(name=name) + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + ret_ident, arguments, qubits, assign_ident = preprocess_params(symbols, ret_type, operands) + + # Treat call as a classical function with (args + qubits) as parameters. + expression = ast.FunctionCall( + name=self.ident, arguments=arguments + [identifier2expression(q) for q in qubits] + ) + + # Assignment vs. pure call + if assign_ident is not None: + statement = ast.ClassicalAssignment( + lvalue=assign_ident, op=ast.AssignmentOperator["="], rvalue=expression + ) + else: + statement = ast.ExpressionStatement(expression) + + return ret_ident, [statement] + + +class LoadBuilder(FunctionBuilder): + """Translate a simple load-like operation that aliases the operand as the result.""" + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + assert len(operands) == 1 + ret_ident = symbols.value_qir2qasm(operands[0]) + + return ret_ident, [] + + +class ConstantBuilder(FunctionBuilder): + """Emit an assignment from a constant expression into a fresh temporary.""" + + def __init__(self, constant: ast.Expression): + self.constant = constant + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + _, type_ast = symbols.type_qir2qasm(ret_type) + ret_ident = symbols.alloc_tmp_var(type_ast) + + statement = ast.ClassicalAssignment( + lvalue=ret_ident, op=ast.AssignmentOperator["="], rvalue=self.constant + ) + return ret_ident, [statement] + + +class BinaryExpressionBuilder(FunctionBuilder): + """Emit `tmp = (lhs rhs)` for a classical binary expression.""" + + def __init__(self, op: str): + self.op = ast.BinaryOperator[op] + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + _, type_ast = symbols.type_qir2qasm(ret_type) + ret_ident = symbols.alloc_tmp_var(type_ast) + + lhs = identifier2expression(symbols.value_qir2qasm(operands[0])) + rhs = identifier2expression(symbols.value_qir2qasm(operands[1])) + statement = ast.ClassicalAssignment( + lvalue=ret_ident, + op=ast.AssignmentOperator["="], + rvalue=ast.BinaryExpression(op=self.op, lhs=lhs, rhs=rhs), + ) + + return ret_ident, [statement] + + +class RecordBuilder(FunctionBuilder): + """Do nothing, as OpenQASM does not need to load from memory to register""" + + pass + + +class InputBuilder(FunctionBuilder): + """Emit a fresh input identifier of the given classical type.""" + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + _, type_ast = symbols.type_qir2qasm(ret_type) + ret_ident = symbols.alloc_io_var(type_ast, "input") + return ret_ident, [] + + +class OutputBuilder(FunctionBuilder): + """Write a classical value to a synthesized output identifier.""" + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + _, op_type = symbols.type_qir2qasm(operands[0].type) + op_value = identifier2expression(symbols.value_qir2qasm(operands[0])) + + assign_ident = symbols.alloc_io_var(op_type, "output") + statement = ast.ClassicalAssignment( + lvalue=assign_ident, op=ast.AssignmentOperator["="], rvalue=op_value + ) + return None, [statement] + + +class ReturnBuilder(FunctionBuilder): + """Translate a `return` statement.""" + + def building( + self, symbols: SymbolTable, ret_type: TypeRef, operands: List[ValueRef] + ) -> Tuple[Optional[Union[ast.IndexedIdentifier, ast.Identifier]], List[ast.Statement]]: + ## TODO: Implement for the OpenQASM subroutine. Main function does not have return. + ## For now, return builder outputs None and empty list. Once subroutine is supported, + ## we need to implement the return builder for subroutine. + # if len(operands) == 1: + # op_value = identifier2expression(symbols.value_qir2qasm(operands[0])) + # else: + # assert len(operands) == 0 + # op_value = None + # statement = ast.ReturnStatement(expression=op_value) + # return None, [statement] + return None, [] + + +# ---------------------- +# Gate definition helpers +# ---------------------- + + +# Declaration Builder +def build_rotation_2Q_definition(name: str) -> ast.QuantumGateDefinition: + """Synthesize a standard two-qubit rotation definition (rxx/ryy) via CX-Rz-CX. + + Constructs a gate `name(theta) q0, q1` with the following template: + + (optional pre-rotations) + cx q0, q1; + rz(theta) q1; + cx q0, q1; + (optional post-rotations) + + The pre/post rotations provide basis changes to realize RXX/RYY variants. + + Args: + name: One of {"rxx", "ryy", ...}. Unknown names fall back to plain RZZ-like body. + + Returns: + ast.QuantumGateDefinition with parameter `theta` and qubits q0, q1. + """ + ident = ast.Identifier(name=name) + + # Define the parameter `_theta` (negated if adjoint) + theta = ast.Identifier(name="_theta") + + # Qubit identifiers + q0 = ast.Identifier(name="q0") + q1 = ast.Identifier(name="q1") + + # Base Rzz-like body: CX - Rz(_theta) - CX + body = [ + ast.QuantumGate( + modifiers=[], name=ast.Identifier(name="cx"), arguments=[], qubits=[q0, q1] + ), + ast.QuantumGate( + modifiers=[], name=ast.Identifier(name="rz"), arguments=[theta], qubits=[q1] + ), + ast.QuantumGate( + modifiers=[], name=ast.Identifier(name="cx"), arguments=[], qubits=[q0, q1] + ), + ] + + # Construct π/2 and −π/2 literals + pi_over_2 = ast.BinaryExpression( + op="/", lhs=ast.Identifier("pi"), rhs=ast.IntegerLiteral(value=2) + ) + neg_pi_over_2 = ast.UnaryExpression(op="-", expression=pi_over_2) + + # Gate-specific pre/post rotations + if name == "rxx": + before = [ + ast.QuantumGate(modifiers=[], name=ast.Identifier(name="h"), arguments=[], qubits=[q]) + for q in [q0, q1] + ] + after = [ + ast.QuantumGate(modifiers=[], name=ast.Identifier(name="h"), arguments=[], qubits=[q]) + for q in [q0, q1] + ] + elif name == "ryy": + before = [ + ast.QuantumGate( + modifiers=[], name=ast.Identifier(name="rx"), arguments=[pi_over_2], qubits=[q] + ) + for q in [q0, q1] + ] + after = [ + ast.QuantumGate( + modifiers=[], name=ast.Identifier(name="rx"), arguments=[neg_pi_over_2], qubits=[q] + ) + for q in [q0, q1] + ] + else: + before = [] + after = [] + + # Combine and return the full definition + return ast.QuantumGateDefinition( + name=ident, arguments=[theta], qubits=[q0, q1], body=before + body + after + ) diff --git a/src/qir2qasm_trans/qir_trans/cfg_pattern.py b/src/qir2qasm_trans/qir_trans/cfg_pattern.py new file mode 100644 index 00000000..d76bd74a --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/cfg_pattern.py @@ -0,0 +1,237 @@ +from typing import Dict, Optional, Tuple + +import networkx as nx +from networkx.algorithms import isomorphism +from openqasm3 import ast + +from .builder import BranchInfo, SymbolTable + + +class CFGPattern: + def __init__(self): + self.pattern: nx.DiGraph = nx.DiGraph() + self.degrees: Dict[str, Tuple[Optional[int], Optional[int]]] = {} + + def matching(self, cfg: nx.DiGraph) -> Optional[Dict[str, str]]: + for match in isomorphism.DiGraphMatcher(cfg, self.pattern).subgraph_isomorphisms_iter(): + for g_node, p_node in match.items(): + in_degree, out_degree = self.degrees[p_node] + if (in_degree is not None) and (in_degree != cfg.in_degree(g_node)): + break + if (out_degree is not None) and (out_degree != cfg.out_degree(g_node)): + break + else: + return {p_node: g_node for g_node, p_node in match.items()} + return None + + def building(self, symbols: SymbolTable) -> bool: + return False + + +class SeqPattern(CFGPattern): + def __init__(self): + super().__init__() + self.pattern.add_edges_from([("A", "B")]) + self.degrees["A"] = (None, 1) + self.degrees["B"] = (1, None) + + def building(self, symbols: SymbolTable): + is_updated = False + blocks = self.matching(symbols.cfg) + + while blocks: + is_updated = True + symbols.block_statements[blocks["A"]].extend(symbols.block_statements.pop(blocks["B"])) + symbols.block_branchs[blocks["A"]] = symbols.block_branchs.pop(blocks["B"]) + symbols.cfg = nx.contracted_nodes( + symbols.cfg, blocks["A"], blocks["B"], self_loops=False + ) + + blocks = self.matching(symbols.cfg) + + return is_updated + + +class IfPattern1(CFGPattern): + def __init__(self): + super().__init__() + self.pattern.add_edges_from([("A", "B"), ("A", "C"), ("B", "D"), ("C", "D")]) + self.degrees["A"] = (None, 2) + self.degrees["B"] = (1, 1) + self.degrees["C"] = (1, 1) + self.degrees["D"] = (None, None) + + def building(self, symbols: SymbolTable): + is_updated = False + blocks = self.matching(symbols.cfg) + while blocks: + is_updated = True + br_info_A = symbols.block_branchs[blocks["A"]] + br_cond_A = br_info_A.branch_condition + br_tgts_A = br_info_A.branch_targets + + if br_tgts_A[0] == blocks["B"]: + # A -> B is the branch of True + if_block = symbols.block_statements[blocks["B"]] + else_block = symbols.block_statements[blocks["C"]] + else: + # A -> B is the branch of False + if_block = symbols.block_statements[blocks["C"]] + else_block = symbols.block_statements[blocks["B"]] + + if_statement = ast.BranchingStatement( + condition=br_cond_A, if_block=if_block, else_block=else_block + ) + symbols.block_statements[blocks["A"]].append(if_statement) + symbols.block_branchs[blocks["A"]] = BranchInfo(None, [blocks["D"]]) + symbols.cfg = nx.contracted_nodes( + symbols.cfg, blocks["A"], blocks["B"], self_loops=False + ) + symbols.cfg = nx.contracted_nodes( + symbols.cfg, blocks["A"], blocks["C"], self_loops=False + ) + + blocks = self.matching(symbols.cfg) + + return is_updated + + +class IfPattern2(CFGPattern): + def __init__(self): + super().__init__() + self.pattern.add_edges_from([("A", "B"), ("A", "D"), ("B", "D")]) + self.degrees["A"] = (None, 2) + self.degrees["B"] = (1, 1) + self.degrees["D"] = (None, None) + + def building(self, symbols: SymbolTable): + is_updated = False + blocks = self.matching(symbols.cfg) + while blocks: + is_updated = True + br_info_A = symbols.block_branchs[blocks["A"]] + br_cond_A = br_info_A.branch_condition + br_tgts_A = br_info_A.branch_targets + + if br_tgts_A[0] == blocks["B"]: + # A -> B is the branch of True + if_block = symbols.block_statements[blocks["B"]] + else_block = [] + else: + # A -> B is the branch of False + if_block = [] + else_block = symbols.block_statements[blocks["B"]] + + if_statement = ast.BranchingStatement( + condition=br_cond_A, if_block=if_block, else_block=else_block + ) + symbols.block_statements[blocks["A"]].append(if_statement) + symbols.block_branchs[blocks["A"]] = BranchInfo(None, [blocks["D"]]) + symbols.cfg = nx.contracted_nodes( + symbols.cfg, blocks["A"], blocks["B"], self_loops=False + ) + + blocks = self.matching(symbols.cfg) + + return is_updated + + +class IfPattern(CFGPattern): + def building(self, symbols: SymbolTable): + is_updated = IfPattern1().building(symbols) or IfPattern2().building(symbols) + while IfPattern1().building(symbols) or IfPattern2().building(symbols): + pass + return is_updated + + +class WhilePattern1(CFGPattern): + def __init__(self): + super().__init__() + self.pattern.add_edges_from([("A", "B"), ("B", "A")]) + self.degrees["A"] = (None, 2) # A is the exit block of the while loop. + self.degrees["B"] = (1, 1) + + def building(self, symbols: SymbolTable): + is_updated = False + blocks = self.matching(symbols.cfg) + while blocks: + is_updated = True + br_info_A = symbols.block_branchs[blocks["A"]] + br_cond_A = br_info_A.branch_condition + br_tgts_A = br_info_A.branch_targets + + assert br_cond_A is not None + if br_tgts_A[0] == blocks["B"]: + # A -> B is the branch of True + while_condition = br_cond_A + out_bt_tgt = br_tgts_A[1] + else: + # A -> B is the branch of False + while_condition = ast.UnaryExpression( + op=ast.UnaryOperator["!"], expression=br_cond_A + ) + out_bt_tgt = br_tgts_A[0] + + while_body = ( + symbols.block_statements[blocks["B"]] + symbols.block_statements[blocks["A"]] + ) + while_statement = ast.WhileLoop(while_condition=while_condition, block=while_body) + symbols.block_statements[blocks["A"]].append(while_statement) + symbols.block_branchs[blocks["A"]] = BranchInfo(None, [out_bt_tgt]) + + symbols.block_statements.pop(blocks["B"]) + symbols.block_branchs.pop(blocks["B"]) + + symbols.cfg.remove_edge(blocks["A"], blocks["B"]) + symbols.cfg.remove_node(blocks["B"]) + + blocks = self.matching(symbols.cfg) + + return is_updated + + +class WhilePattern2(CFGPattern): + def __init__(self): + super().__init__() + self.pattern.add_edges_from([("A", "A")]) + self.degrees["A"] = (None, 2) + + def building(self, symbols: SymbolTable): + is_updated = False + blocks = self.matching(symbols.cfg) + while blocks: + is_updated = True + br_info_A = symbols.block_branchs[blocks["A"]] + br_cond_A = br_info_A.branch_condition + br_tgts_A = br_info_A.branch_targets + + assert br_cond_A is not None + if br_tgts_A[0] == blocks["A"]: + # A -> A is the branch of True + while_condition = br_cond_A + out_bt_tgt = br_tgts_A[1] + else: + # A -> A is the branch of False + while_condition = ast.UnaryExpression( + op=ast.UnaryOperator["!"], expression=br_cond_A + ) + out_bt_tgt = br_tgts_A[0] + + while_body = symbols.block_statements[blocks["A"]].copy() + while_statement = ast.WhileLoop(while_condition=while_condition, block=while_body) + symbols.block_statements[blocks["A"]].append(while_statement) + symbols.block_branchs[blocks["A"]] = BranchInfo(None, [out_bt_tgt]) + + symbols.cfg.remove_edge(blocks["A"], blocks["A"]) + + blocks = self.matching(symbols.cfg) + + return is_updated + + +class WhilePattern(CFGPattern): + def building(self, symbols: SymbolTable): + is_updated = WhilePattern1().building(symbols) or WhilePattern2().building(symbols) + while WhilePattern1().building(symbols) or WhilePattern2().building(symbols): + pass + return is_updated diff --git a/src/qir2qasm_trans/qir_trans/control_flow.py b/src/qir2qasm_trans/qir_trans/control_flow.py new file mode 100644 index 00000000..d49f2e4c --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/control_flow.py @@ -0,0 +1,156 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from collections import deque +from typing import Dict, List, Optional, Tuple + +import networkx as nx + +from .builder import BranchInfo, SymbolTable + + +def isolate_incoming_edge(edge: Tuple[str, str], symbols: SymbolTable): + "Copy block B to isolate the incoming edge A->B for block B" + blockA, blockB = edge + blockB_copy = symbols.new_tmp_block_name() + + # 1) Redirect A's branch target from B -> B_copy + br_info_A = symbols.block_branchs[blockA] + br_tgts_A_new = [ + blockB_copy if block == blockB else block for block in br_info_A.branch_targets + ] + symbols.block_branchs[blockA] = BranchInfo(br_info_A.branch_condition, br_tgts_A_new) + + # 2) Duplicate B's statements and branch info to B_copy + symbols.block_statements[blockB_copy] = symbols.block_statements[blockB].copy() + br_info_B = symbols.block_branchs[blockB] + symbols.block_branchs[blockB_copy] = BranchInfo( + br_info_B.branch_condition, br_info_B.branch_targets + ) + + # 3) Rewire CFG: remove A->B, add A->B_copy, and B_copy -> successors(B) + symbols.cfg.remove_edge(blockA, blockB) + symbols.cfg.add_edge(blockA, blockB_copy) + for u in symbols.cfg.successors(blockB): + symbols.cfg.add_edge(blockB_copy, u) + + +def find_if_pattern(G: nx.DiGraph) -> Optional[Tuple[str, List[List[str]]]]: + """Find an 'if' statement candidate.""" + edges_deg1 = [(u, v) for u, v in G.edges if G.out_degree(u) == 1] + + H = nx.DiGraph() + H.add_nodes_from(G.nodes) + H.add_edges_from(edges_deg1) + + # Partition nodes by out-degree + deg0_H: set = {u for u in H.nodes if H.out_degree(u) == 0} + deg1_H: set = {u for u in H.nodes if H.out_degree(u) == 1} + + # chains[u] = path from block u to out-degree-0 block, using only out-degree-1 nodes + chains: Dict[str, List[str]] = {u: None for u in G.nodes} + + # BFS on the reverse graph, restricted to predecessors with out-degree == 1. + q: deque[Tuple[str, List[str]]] = deque([(u, [u]) for u in deg0_H]) + + while q: + block, path = q.popleft() + chains[block] = path + for pred_block in G.predecessors(block): + if pred_block in deg1_H: + assert chains[pred_block] is None + q.append((pred_block, [pred_block] + path)) + + # Scan out-degree-2 nodes for an 'if' statement candidate + deg2_G: set = {u for u in G.nodes if G.out_degree(u) == 2} + for block in deg2_G: + u, v = list(G.successors(block)) + path_u = chains[u] + path_v = chains[v] + if path_u[-1] == path_v[-1]: + while path_u and path_v and path_u[-1] == path_v[-1]: + path_u.pop(-1) + path_v.pop(-1) + return block, [path_u, path_v] + + return None + + +def isolate_if_pattern(symbols: SymbolTable) -> bool: + is_updated = False + result = find_if_pattern(symbols.cfg) + if result is None: + return is_updated + + # For each path, walk edges and split any node that has in-degree > 1. + if_block, paths = result + for path in paths: + for edge in zip([if_block] + path[:-1], path): + if symbols.cfg.in_degree(edge[1]) > 1: + is_updated = True + isolate_incoming_edge(edge, symbols) + + return is_updated + + +def find_while_pattern(G: nx.DiGraph) -> Optional[Tuple[str, List[List[str]]]]: + """Find an 'while' statement candidate.""" + edges_deg1 = [(u, v) for u, v in G.edges if G.out_degree(u) == 1] + + H = nx.DiGraph() + H.add_nodes_from(G.nodes) + H.add_edges_from(edges_deg1) + + # Partition nodes by out-degree + deg0_H: set = {u for u in H.nodes if H.out_degree(u) == 0} + deg1_H: set = {u for u in H.nodes if H.out_degree(u) == 1} + + # chains[u] = path from block u to out-degree-0 block, using only out-degree-1 nodes + chains: Dict[str, List[str]] = {u: None for u in G.nodes} + + # BFS on the reverse graph, restricted to predecessors with out-degree == 1. + q: deque[Tuple[str, List[str]]] = deque([(u, [u]) for u in deg0_H]) + + while q: + block, path = q.popleft() + chains[block] = path + for pred_block in G.predecessors(block): + if pred_block in deg1_H: + assert chains[pred_block] is None + q.append((pred_block, [pred_block] + path)) + + # Scan out-degree-2 nodes for an 'while' statement candidate + deg2_G: set = {u for u in G.nodes if G.out_degree(u) == 2} + for block in deg2_G: + for succ in G.successors(block): + path = chains[succ] + if block in path: + return block, path[: path.index(block)] + + return None + + +def isolate_while_pattern(symbols: SymbolTable) -> bool: + is_updated = False + result = find_while_pattern(symbols.cfg) + if result is None: + return is_updated + + # For each path, walk edges and split any node that has in-degree > 1. + while_block, path = result + for edge in zip([while_block] + path[:-1], path): + if symbols.cfg.in_degree(edge[1]) > 1: + is_updated = True + isolate_incoming_edge(edge, symbols) + + return is_updated diff --git a/src/qir2qasm_trans/qir_trans/qir_profile.py b/src/qir2qasm_trans/qir_trans/qir_profile.py new file mode 100644 index 00000000..17583935 --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/qir_profile.py @@ -0,0 +1,247 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +from typing import Dict, List, Union + +from llvmlite.ir import ( + Context, + DoubleType, + FunctionType, + IdentifiedStructType, + IntType, + Type, + VoidType, +) +from openqasm3 import ast + +from .builder import ( + BinaryExpressionBuilder, + ConstantBuilder, + DeclBuilder, + FunctionBuilder, + FunctionInfo, + GateBuilder, + InputBuilder, + InstructionBuilder, + InstructionInfo, + InttoptrBuilder, + LoadBuilder, + MeasurementBuilder, + OutputBuilder, + QubitDeclarationBuilder, + RecordBuilder, + ResetBuilder, + ResultDeclarationBuilder, + StructInfo, + build_rotation_2Q_definition, +) + + +class Profile: + """Base profile for mapping QIR constructs to OpenQASM 3. + + A `Profile` bundles: + - Structure/type mappings + - Classical instruction builders + - Function builders + + Attributes: + name (str): Profile name (e.g., "base_profile"). + structs (Dict[str, StructInfo]): Registered structure information by name. + classical_instruction (Dict[str, InstructionInfo]): Builders info for + LLVM *instructions* (e.g., ``inttoptr``). + standard_functions (Dict[str, FunctionInfo]): Builders info for QIR + function calls (e.g., QIS intrinsics). + context (Context): An LLVM IR context for creating/looking up types. + """ + + def __init__(self, name: str): + self.name = name + self.structs: Dict[str, StructInfo] = {} + self.classical_instruction: Dict[str, InstructionInfo] = {} + self.standard_functions: Dict[str, FunctionInfo] = {} + self.context = Context() + + self._define_structs() + self._define_functions() + self._define_classical_instructions() + + def register_function( + self, + func_name: str, + ret_type: Type, + arg_types: List[Type], + def_statement: ast.QuantumGateDefinition, + func_builder: FunctionBuilder, + ): + fn_type = FunctionType(ret_type, arg_types) + self.standard_functions[func_name] = FunctionInfo(fn_type, def_statement, func_builder) + + def register_classical_instruction(self, name: str, inst_builder: InstructionBuilder): + self.classical_instruction[name] = InstructionInfo(builder=inst_builder) + + def register_struct( + self, + name: str, + type_qir: IdentifiedStructType, + type_ast: Union[ast.ClassicalType, str], + decl_builder: DeclBuilder, + ): + """Register a structure type and its OpenQASM declaration strategy. + + Args: + name (str): Structure name (e.g., ``"Qubit"``). + type_qir (IdentifiedStructType): LLVM identified struct type. + type_ast (Union[ast.ClassicalType, str]): Target OpenQASM AST representation. + decl_builder (DeclBuilder): Declaration builder for the struct. + """ + # self.structs[name] = StructInfo(type_qir, None, decl_builder) + self.structs[name] = StructInfo(type_qir, type_ast, decl_builder, f"{name}s") + + def _define_structs(self): + """Hook for subclasses to register structure types.""" + pass + + def _define_functions(self): + """Hook for subclasses to register QIR functions and their builders.""" + pass + + def _define_classical_instructions(self): + """Hook for subclasses to register LLVM classical instruction lowerings.""" + pass + + +class BaseProfile(Profile): + """A minimal profile that wires common QIS intrinsics to OpenQASM gates.""" + + def __init__(self): + super().__init__("base_profile") + + def _define_structs(self): + """Register core QIR structures: Qubit and Result.""" + self.register_struct( + "Qubit", self.context.get_identified_type("Qubit"), "Qubit", QubitDeclarationBuilder() + ) + self.register_struct( + "Result", + self.context.get_identified_type("Result"), + ast.BitType(), + ResultDeclarationBuilder(), + ) + + def _define_functions(self): + qubit_ptr = self.structs["Qubit"].type_qir.as_pointer() + result_ptr = self.structs["Result"].type_qir.as_pointer() + void_type = VoidType() + double_type = DoubleType() + + def register_qis_std_gate_functions( + gates: List[str], ret_type: Type, arg_types: List[Type], adjoint: bool = False + ): + """Convenience helper to bulk-register quantum gates.""" + for gate in gates: + suffix = "adj" if adjoint else "body" + func_name = f"__quantum__qis__{gate}__{suffix}" + func_builder = GateBuilder(gate, adjoint=adjoint) + self.register_function(func_name, ret_type, arg_types, None, func_builder) + + # 1-qubit Clifford/T-like gates and their adjoints + register_qis_std_gate_functions(["h", "s", "t", "x", "y", "z"], void_type, [qubit_ptr]) + register_qis_std_gate_functions(["s", "t"], void_type, [qubit_ptr], adjoint=True) + + # 2-qubit and 3-qubit gates + self.register_function( + "__quantum__qis__cnot__body", + void_type, + [qubit_ptr, qubit_ptr], + None, + GateBuilder("cx"), + ) + register_qis_std_gate_functions(["cy", "cz", "swap"], void_type, [qubit_ptr, qubit_ptr]) + + # Parameterized 1-qubit rotations + register_qis_std_gate_functions(["rx", "ry", "rz"], void_type, [double_type, qubit_ptr]) + + # 3-qubit Toffoli + register_qis_std_gate_functions(["ccx"], void_type, [qubit_ptr, qubit_ptr, qubit_ptr]) + + # Synthesized 2-qubit XX/YY/ZZ rotations with explicit definitions + for gate in ["rxx", "ryy", "rzz"]: + func_name = f"__quantum__qis__{gate}__body" + self.register_function( + func_name, + void_type, + [double_type, qubit_ptr, qubit_ptr], + build_rotation_2Q_definition(gate), + GateBuilder(gate), + ) + + # register_qis_std_gate_functions(["barrier"], void_type, []) + + # Reset + self.register_function( + "__quantum__qis__reset__body", void_type, [qubit_ptr], None, ResetBuilder() + ) + + # Measurements + self.register_function( + "__quantum__qis__m__body", result_ptr, [qubit_ptr], None, MeasurementBuilder("m") + ) + for op in ["mz", "mresetz"]: + func_name = f"__quantum__qis__{op}__body" + self.register_function( + func_name, void_type, [qubit_ptr, result_ptr], None, MeasurementBuilder(op) + ) + + # Read result as i1 + self.register_function( + "__quantum__qis__read_result__body", IntType(1), [result_ptr], None, LoadBuilder() + ) + + # def register_rt_functions(ops: List[str], ret_type: Type, arg_types: List[Type]): + # for op in ops: + # func_name = f"__quantum__rt__{op}" + # self.register_function(func_name, ret_type, arg_types, None, FunctionBuilder()) + + # register_rt_functions(["initialize"], void_type, [IntType(8).as_pointer()]) + # register_rt_functions([f"{x}_record_output" for x in ["tuple", "arry"]], void_type, [IntType(64), IntType(8).as_pointer()]) + + # Runtime helpers + self.register_function( + "__quantum__rt__result_record_output", + void_type, + [result_ptr, IntType(8).as_pointer()], + None, + RecordBuilder(), + ) + + # Others + self.register_function( + "__quantum__rt__result_get_one", + result_ptr, + [], + None, + ConstantBuilder(constant=ast.BooleanLiteral(value=True)), + ) + self.register_function( + "__quantum__rt__result_equal", + IntType(1), + [result_ptr, result_ptr], + None, + BinaryExpressionBuilder("=="), + ) + self.register_function("get_int", IntType(32), [], None, InputBuilder()) + self.register_function("take_int", void_type, [IntType(32)], None, OutputBuilder()) + + def _define_classical_instructions(self): + self.register_classical_instruction("inttoptr", InttoptrBuilder()) diff --git a/src/qir2qasm_trans/qir_trans/translator.py b/src/qir2qasm_trans/qir_trans/translator.py new file mode 100644 index 00000000..20d23d8d --- /dev/null +++ b/src/qir2qasm_trans/qir_trans/translator.py @@ -0,0 +1,528 @@ +# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). You +# may not use this file except in compliance with the License. A copy of +# the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is +# distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF +# ANY KIND, either express or implied. See the License for the specific +# language governing permissions and limitations under the License. + +import io +import re +from typing import List, Optional, Sequence, Tuple, Type + +import networkx as nx +from llvmlite.binding.module import ModuleRef, ValueRef +from openqasm3 import ast +from openqasm3.printer import Printer, QASMVisitor + +from .builder import ( + BinaryExpressionBuilder, + BranchInfo, + ClassicalDeclarationBuilder, + DeclBuilder, + DefCalBuilder, + FunctionInfo, + ReturnBuilder, + SymbolTable, +) +from .cfg_pattern import IfPattern, SeqPattern, WhilePattern +from .control_flow import isolate_if_pattern, isolate_while_pattern +from .qir_profile import BaseProfile, Profile + +# Reserved OpenQASM 3 keywords that must not be used as identifiers. +_RESERVED_KEYWORDS = frozenset( + { + "OPENQASM", + "angle", + "array", + "barrier", + "bit", + "bool", + "box", + "break", + "cal", + "complex", + "const", + "continue", + "creg", + "ctrl", + "def", + "defcal", + "defcalgrammar", + "delay", + "duration", + "durationof", + "else", + "end", + "extern", + "float", + "for", + "gate", + "gphase", + "if", + "in", + "include", + "input", + "int", + "inv", + "let", + "measure", + "mutable", + "negctrl", + "output", + "pow", + "qreg", + "qubit", + "reset", + "return", + "sizeof", + "stretch", + "uint", + "while", + } +) + +_END_KEYWORD = "_function_end_" + +# Mapping from LLVM arithmetic opcodes to classical expression builders. +_LLVM_INSTRUCTIONS = { + "add": BinaryExpressionBuilder("+"), + "sub": BinaryExpressionBuilder("-"), + "mul": BinaryExpressionBuilder("*"), +} + + +class Exporter: + """QASM3 exporter main class.""" + + def __init__( + self, + include_files: Sequence[str] = (), + profile: Profile = BaseProfile(), + printer_class: Type[QASMVisitor] = Printer, + ): + """Configure exporter components. + + Args: + include_files (Sequence[str]): Standard gate library. Example: ["stdgates.inc"] to expose + standard gate declarations. + profile (Profile): Target QIR profile for translation. + printer_class (Type[QASMVisitor]): Visitor class that prints an AST to a QASM stream. + """ + self.include_files = list(include_files) + self.profile = profile + self.printer_class = printer_class + + def dumps(self, module): + """Convert the module to OpenQASM 3, returning the program as a string. + + Args: + module (ModuleRef): Source QIR module to export. + + Returns: + str: OpenQASM 3 program text. + """ + with io.StringIO() as stream: + self.dump(module, stream) + return stream.getvalue() + + def dump(self, module, stream): + """Convert the module to OpenQASM 3 and write to a file-like stream. + + Args: + module (ModuleRef): Source QIR module to export. + stream: Text IO stream (e.g., file handle or StringIO). + """ + builder = QASM3Builder(module, include_files=self.include_files, profile=self.profile) + printer = self.printer_class(stream) + printer.visit(builder.build_program()) + + +class QASM3Builder: + """QASM3 builder constructs an AST from a Module.""" + + def __init__(self, module: ModuleRef, include_files: List[str], profile: Profile): + """Initialize builder state and symbol table. + + Args: + module (ModuleRef): Source QIR module to translate. + include_files (List[str]): Target QIR profile for translation. + profile (Profile): Profile containing lowering rules and type mappings. + """ + self.module = module + self.symbols = SymbolTable() + self.include_files = include_files + self.profile = profile + + def build_program(self) -> ast.Program: + """Assemble the full OpenQASM 3 program AST. + + Returns: + ast.Program: Top-level program node including version and statements. + """ + include_statements = [] + for filename in self.include_files: + include_statements.append(ast.Include(filename)) + + func_decl_statements = self.build_func_declarations() + main_statements = self.build_main() + var_decl_statements = self.build_var_declarations() + + statements = ( + include_statements + var_decl_statements + func_decl_statements + main_statements + ) + return ast.Program(version="3.0", statements=statements) + + def build_var_declarations(self) -> List[ast.Statement]: + """Emit declarations for structures, temporaries, and I/O identifiers. + + Returns: + List[ast.Statement]: Declarations of OpenQASM variables. + """ + building_tasks: List[Tuple[DeclBuilder, str, int]] = [] + + # Structures → declare arrays for values and temporaries. + for name, struct_info in self.symbols.structs.items(): + builder = struct_info.decl_builder + building_tasks.append((builder, f"{name}s", self.symbols.structs_num[name])) + building_tasks.append((builder, f"{name}s_tmp", self.symbols.structs_tmp_num[name])) + + # Classical temporaries → declare arrays like `_tmp[n]`. + for name, type_ast in self.symbols.classical_tmp.items(): + builder = ClassicalDeclarationBuilder(type_ast) + building_tasks.append((builder, f"{name}_tmp", self.symbols.classical_tmp_num[name])) + + # Materialize structure/classical temp declarations. + statements: List[ast.Statement] = [] + for builder, name, size in building_tasks: + if size > 0: # Skip empty arrays + statements.append(builder.building(name, size)) + + # I/O declarations (`input` / `output`). + for io_key in ["input", "output"]: + for type_ast, ident_ast in self.symbols.io_variables[io_key]: + statements.append( + ast.IODeclaration( + io_identifier=ast.IOKeyword[io_key], type=type_ast, identifier=ident_ast + ) + ) + + return statements + + def build_func_declarations(self) -> List[ast.Statement]: + """Register known functions and lower unknown declarations into `defcal`. + + Returns: + List[ast.Statement]: Gate definitions and calibration definitions. + """ + module = self.module + + declaration_statements: List[ast.CalibrationDefinition] = [] + definition_statements: List[ast.QuantumGateDefinition] = [] + + # Register profile-provided structures and classical instruction lowering strategies. + for name, struct_info in self.profile.structs.items(): + self.symbols.register_struct(name, struct_info) + for name, inst_info in self.profile.classical_instruction.items(): + self.symbols.instructions[name] = inst_info + + # Iterate over the function declarations. + for func in module.functions: + if not func.is_declaration: + continue + + func_info = self.profile.standard_functions.get(func.name) + if func_info: + # Known function: record function info and collect definition if present. + self.symbols.functions[func.name] = func_info + def_statement = func_info.def_statement + if def_statement: + definition_statements.append(def_statement) + else: + # Unknown function: synthesize an empty-body `defcal` signature. + func_name = func.name + func_type = func.type.element_type + + # Build the `CalibrationDefinition` AST node in OpenQASM3 + arguments = [] + qubits = [] + num_qubits = 0 + + arg_types = [arg_type for arg_type in func_type.elements] + + # Process the return type for the "CalibrationDefinition" AST node. + # Per QIR convention, return is always at the first element. If there + # are multiple returns, the return object is a list. + ret_type = arg_types[0] + _, ret_type_ast = self.symbols.type_qir2qasm(ret_type) + if isinstance(ret_type_ast, str): + ret_type_ast = self.symbols.structs[ret_type_ast].type_ast + if ret_type_ast == "Qubit": + # In QIR, a function may allocate and return a new qubit. + # In OpenQASM, qubit allocation via operations is not allowed. + # All qubits must be declared at the beginning of the program. + + # In the translation, we assume the returned qubit is already declared, + # and calling this allocation function is treated as applying + # a virtual calibration gate to that qubit. + + # Transform: Qubit* func(...) => void func(..., Qubit) + qubits.append(ast.Identifier(name="q_ret")) + ret_type_ast = None + + # Process the arguments for the "CalibrationDefinition" AST node + for op_type in arg_types[1:]: + op_type_str, op_type_ast = self.symbols.type_qir2qasm(op_type) + if isinstance(op_type_ast, str): + op_type_ast = self.symbols.structs[op_type_ast].type_ast + if op_type_ast == "Qubit": + num_qubits += 1 + else: + arguments.append(op_type_ast) + + if (op_type_str == "pointer") and isinstance(op_type_ast, ast.ClassicalType): + if ret_type_ast is None: + # Transform: void func(typeA* *a, ...) => a = typeA func(type_A a, ...) + ret_type_ast = op_type_ast + else: + raise Exception("Too much return value!") + + # Process the qubits for the "CalibrationDefinition" AST node + if num_qubits == 1: + qubits.append(ast.Identifier(name="q")) + else: + for k in range(num_qubits): + qubits.append(ast.Identifier(name=f"q{k}")) + + # Register the new function defined by "defcal" in symbol table + func_info = FunctionInfo( + type=func_type.as_ir(self.profile.context), + def_statement=None, + builder=DefCalBuilder(func_name), + ) + self.symbols.functions[func.name] = func_info + + ## TODO: QIR function declaration can be classical (subroutine) or quantum (defcal). + ## For now, we assume only quantum declaration, and skip the subroutines. + # declaration = ast.SubroutineDefinition( + # name=ast.Identifier(name=func_name), + # arguments=arguments + qubits, + # body=[], + # return_type=ret_type_ast + # ) + + # Use gate Calibration for the opague function + declaration = ast.CalibrationDefinition( + name=ast.Identifier(name=func_name), + arguments=arguments, + qubits=qubits, + return_type=ret_type_ast, + body="", + ) + + declaration_statements.append(declaration) + + return declaration_statements + definition_statements + + def build_main(self) -> List[ast.Statement]: + """Translate the `entry_point` function body into OpenQASM statements. + + Returns: + List[ast.Statement]: OpenQASM Statements. + """ + main_func = None + for func in self.module.functions: + # Collect function attributes into a dictionary. + attr_dict = {} + for attr in func.attributes: + # Convert string expressions of "key"="value" to a dict of + # key-value pairs. + matches = re.findall(r'"(\w+)"(?:="([^"]*)")?', attr.decode()) + for k, v in matches: + if v != "": + attr_dict[k] = v + else: + attr_dict[k] = True + + if "entry_point" in attr_dict.keys(): + main_func = func + break + else: + raise Exception("No main function defined!") + + # Track entry block name for control-flow assembly. + self.entry_block = list(main_func.blocks)[0].name + + # + self.symbols.block_statements[_END_KEYWORD] = [] + self.symbols.block_branchs[_END_KEYWORD] = BranchInfo(None, []) + + # Trabslate each basic block in order. + for block in main_func.blocks: + self.build_block(block) + + # TODO: Assemble control flow + self.build_control() + assert self.symbols.cfg.number_of_nodes() == 1 + return self.symbols.block_statements[self.entry_block] + + def build_block(self, block: ValueRef) -> List[ast.Statement]: + """Translate a basic block into a list of QASM statements. + + Args: + block (ValueRef): LLVM basic block. + + Returns: + List[ast.Statement]: OpenQASM Statements. + """ + statements = [] + block_name = block.name + for inst in block.instructions: + inst_statements, br_info = self.build_instruction(inst) + statements.extend(inst_statements) + assert br_info is not None + self.symbols.block_statements[block_name] = statements + self.symbols.block_branchs[block_name] = br_info + return statements + + def build_control(self): + """Construct control-flow artifacts. + + Note: LLVM requires branch statement to be the last statment of a block. + # TODO: Currently builds a CFG only + """ + # Create a directed CFG from collected branch information. + cfg = nx.DiGraph() + for block, block_info in self.symbols.block_branchs.items(): + cfg.add_node(block) + for block, block_info in self.symbols.block_branchs.items(): + for tgt_block in block_info.branch_targets: + cfg.add_edge(block, tgt_block) + self.symbols.cfg = cfg + + is_updated = True + while is_updated: + # If a CFG pattern matching failed, it returns `is_updated=False`. This is a + # mechanism to prevent an infinte loop. + is_updated = SeqPattern().building(self.symbols) + is_updated = IfPattern().building(self.symbols) or is_updated + is_updated = WhilePattern().building(self.symbols) or is_updated + is_updated = is_updated or isolate_if_pattern(self.symbols) + is_updated = is_updated or isolate_while_pattern(self.symbols) + + def build_instruction( + self, instruction: ValueRef + ) -> Tuple[List[ast.Statement], Optional[BranchInfo]]: + """Translate a single LLVM instruction. + + Args: + instruction (ValueRef): LLVM instruction. + block_name (str): Name of the containing basic block. + + Returns: + List[ast.Statement]: Emitted statements + """ + statements: List[ast.Statement] = [] + br_info: Optional[BranchInfo] = None + + if instruction.opcode in _LLVM_INSTRUCTIONS.keys(): + statements = self.build_llvm_inst(instruction) + elif instruction.opcode == "call": + statements = self.build_func_call(instruction) + elif instruction.opcode == "br": + br_info = self.build_branch(instruction) + elif instruction.opcode == "ret": + statements = self.build_return(instruction) + br_info = BranchInfo(None, [_END_KEYWORD]) + else: + raise Exception(f"Undefined llvm instruction: {instruction.opcode}") + + return statements, br_info + + def build_llvm_inst(self, inst: ValueRef) -> List[ast.Statement]: + """Translate a supported LLVM arithmetic instruction to QASM statements. + + Args: + inst (ValueRef): LLVM instruction (e.g., `add`, `sub`, `mul`). + + Returns: + List[ast.Statement]: Emitted statements and records the SSA result. + """ + operands = list(inst.operands) + func_builder = _LLVM_INSTRUCTIONS[inst.opcode] + ret_type = inst.type + + ret_ident, statements = func_builder.building(self.symbols, ret_type, operands) + assert ret_ident is not None + # if ret_ident: + # self.symbols.record_variables(inst, ret_ident) + self.symbols.record_variables(inst, ret_ident) + return statements + + def build_func_call(self, inst: ValueRef) -> List[ast.Statement]: + """Translate a QIR `call` instruction into OpenQASM statements. + + Args: + inst (ValueRef): LLVM call instruction. + + Returns: + List[ast.Statement]: Emitted statements implementing the call. + """ + operands = list(inst.operands) + func = operands.pop(-1) # Callee is last in llvmlite's operand list. + func_name = func.name + + # Lookup builder for the callee. + func_info = self.symbols.functions[func_name] + func_builder = func_info.builder + ret_type = inst.type + + # Type check: ensure the callee's type matches the registered signature. + assert func.type.element_type.as_ir(self.profile.context) == func_info.type + + # Delegate to the builder to produce statements and result identifier. + ret_ident, statements = func_builder.building(self.symbols, ret_type, operands) + if ret_ident: + self.symbols.record_variables(inst, ret_ident) + return statements + + def build_branch(self, inst: ValueRef) -> BranchInfo: + """Extract branch information (targets and optional condition). + + Args: + inst (ValueRef): LLVM `br` instruction (conditional or unconditional). + + Returns: + BranchInfo: Condition expression (if any) and target block names. + """ + operands = list(inst.operands) + if len(operands) == 1: + # Unconditional branch: br label %target + br_info = BranchInfo(None, [operands[0].name]) + else: + # Conditional branch: br i1 %cond, label %true, label %false + # llvmlite load operands as (%cond, %false, %true) + assert len(operands) == 3 + br_info = BranchInfo( + self.symbols.value_qir2qasm(operands[0]), [operands[2].name, operands[1].name] + ) + return br_info + + def build_return(self, inst: ValueRef) -> List[ast.Statement]: + """Extract branch information (targets and optional condition). + + Args: + inst (ValueRef): LLVM `ret` instruction. + + Returns: + List[ast.Statement]: Emitted statements implementing the return. + """ + operands = list(inst.operands) + ret_type = inst.type + _, statements = ReturnBuilder().building(self.symbols, ret_type, operands) + return statements diff --git a/test/resources/qir_test_file/arguments.ll b/test/resources/qir_test_file/arguments.ll new file mode 100644 index 00000000..9bd5cafb --- /dev/null +++ b/test/resources/qir_test_file/arguments.ll @@ -0,0 +1,21 @@ +; ModuleID = 'arguments' +source_filename = "arguments.ll" + +%Qubit = type opaque + +declare void @my_test(%Qubit*, %Qubit*) + +define void @main() #0 { +entry: +call void @my_test(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/arguments.qasm b/test/resources/qir_test_file/arguments.qasm new file mode 100644 index 00000000..fcd64a0f --- /dev/null +++ b/test/resources/qir_test_file/arguments.qasm @@ -0,0 +1,4 @@ +OPENQASM 3.0; +qubit[2] Qubits; +defcal my_test() q0, q1 {} +my_test(Qubits[0], Qubits[1]); \ No newline at end of file diff --git a/test/resources/qir_test_file/arithmetic.ll b/test/resources/qir_test_file/arithmetic.ll new file mode 100644 index 00000000..017658d5 --- /dev/null +++ b/test/resources/qir_test_file/arithmetic.ll @@ -0,0 +1,27 @@ +; ModuleID = 'arithmetic' +source_filename = "arithmetic" + +define void @main() #0 { +entry: + %0 = call i32 @get_int() + %1 = add i32 3, %0 + %2 = mul i32 2, %1 + %3 = call i32 @get_int() + %4 = sub i32 0, %3 + call void @take_int(i32 %2) + call void @take_int(i32 %4) + ret void +} + +declare i32 @get_int() + +declare void @take_int(i32) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/test/resources/qir_test_file/arithmetic.qasm b/test/resources/qir_test_file/arithmetic.qasm new file mode 100644 index 00000000..9ac43eaf --- /dev/null +++ b/test/resources/qir_test_file/arithmetic.qasm @@ -0,0 +1,11 @@ +OPENQASM 3.0; +array[int[32], 3] IntType_tmp; +input int[32] IntType_i0; +input int[32] IntType_i1; +output int[32] IntType_o0; +output int[32] IntType_o1; +IntType_tmp[0] = 3 + IntType_i0; +IntType_tmp[1] = 2 * IntType_tmp[0]; +IntType_tmp[2] = 0 - IntType_i1; +IntType_o0 = IntType_tmp[1]; +IntType_o1 = IntType_tmp[2]; \ No newline at end of file diff --git a/test/resources/qir_test_file/bell.ll b/test/resources/qir_test_file/bell.ll new file mode 100644 index 00000000..dd4a0aeb --- /dev/null +++ b/test/resources/qir_test_file/bell.ll @@ -0,0 +1,30 @@ +; ModuleID = 'bell' +source_filename = "bell" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/test/resources/qir_test_file/bell.qasm b/test/resources/qir_test_file/bell.qasm new file mode 100644 index 00000000..81d2f890 --- /dev/null +++ b/test/resources/qir_test_file/bell.qasm @@ -0,0 +1,7 @@ +OPENQASM 3.0; +qubit[2] Qubits; +bit[2] Results; +h Qubits[0]; +cx Qubits[0], Qubits[1]; +Results[0] = measure Qubits[0]; +Results[1] = measure Qubits[1]; \ No newline at end of file diff --git a/test/resources/qir_test_file/bernstein_vazirani.ll b/test/resources/qir_test_file/bernstein_vazirani.ll new file mode 100644 index 00000000..bf9a13af --- /dev/null +++ b/test/resources/qir_test_file/bernstein_vazirani.ll @@ -0,0 +1,51 @@ +; ModuleID = 'python2qir' +source_filename = "python2qir" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 4 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 5 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__z__body(%Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__cnot__body(%Qubit* inttoptr (i64 5 to %Qubit*), %Qubit* inttoptr (i64 6 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 5 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 4 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 3 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 2 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 3 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 4 to %Result*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 5 to %Qubit*), %Result* inttoptr (i64 5 to %Result*)) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__z__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="7" "required_num_results"="6" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/test/resources/qir_test_file/bernstein_vazirani.qasm b/test/resources/qir_test_file/bernstein_vazirani.qasm new file mode 100644 index 00000000..6d25da21 --- /dev/null +++ b/test/resources/qir_test_file/bernstein_vazirani.qasm @@ -0,0 +1,26 @@ +OPENQASM 3.0; +qubit[7] Qubits; +bit[6] Results; +h Qubits[0]; +h Qubits[1]; +h Qubits[2]; +h Qubits[3]; +h Qubits[4]; +h Qubits[5]; +h Qubits[6]; +z Qubits[6]; +cx Qubits[1], Qubits[6]; +cx Qubits[3], Qubits[6]; +cx Qubits[5], Qubits[6]; +h Qubits[5]; +h Qubits[4]; +h Qubits[3]; +h Qubits[2]; +h Qubits[1]; +h Qubits[0]; +Results[0] = measure Qubits[0]; +Results[1] = measure Qubits[1]; +Results[2] = measure Qubits[2]; +Results[3] = measure Qubits[3]; +Results[4] = measure Qubits[4]; +Results[5] = measure Qubits[5]; diff --git a/test/resources/qir_test_file/bitcode_load.bc b/test/resources/qir_test_file/bitcode_load.bc new file mode 100644 index 00000000..a87f6bae Binary files /dev/null and b/test/resources/qir_test_file/bitcode_load.bc differ diff --git a/test/resources/qir_test_file/builder_array_error.ll b/test/resources/qir_test_file/builder_array_error.ll new file mode 100644 index 00000000..684b8944 --- /dev/null +++ b/test/resources/qir_test_file/builder_array_error.ll @@ -0,0 +1,26 @@ +; ModuleID = 'builder_array_error' +source_filename = "builder_array_error" + +%Qubit = type opaque +%Result = type opaque + +@v1 = constant <8 x i32> + +declare <8 x i32> @add_int_vectors(<8 x i32>) + +define void @main() #0 { +entry: +%p1 = load <8 x i32>, <8 x i32>* @v1 +%res = call <8 x i32> @add_int_vectors(<8 x i32> %p1) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/builder_defcal.ll b/test/resources/qir_test_file/builder_defcal.ll new file mode 100644 index 00000000..220c2886 --- /dev/null +++ b/test/resources/qir_test_file/builder_defcal.ll @@ -0,0 +1,21 @@ +; ModuleID = 'instruction_error' +source_filename = "instruction_error.ll" + +%Result = type opaque + +declare void @my_test(%Result*) + +define void @main() #0 { +entry: +call void @my_test(%Result* null) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/builder_defcal.qasm b/test/resources/qir_test_file/builder_defcal.qasm new file mode 100644 index 00000000..da6717e9 --- /dev/null +++ b/test/resources/qir_test_file/builder_defcal.qasm @@ -0,0 +1,4 @@ +OPENQASM 3.0; +bit[1] Results; +defcal my_test(bit) -> bit {} +Results[0] = my_test(Results[0]); \ No newline at end of file diff --git a/test/resources/qir_test_file/builder_instruction_error.ll b/test/resources/qir_test_file/builder_instruction_error.ll new file mode 100644 index 00000000..332bf04b --- /dev/null +++ b/test/resources/qir_test_file/builder_instruction_error.ll @@ -0,0 +1,21 @@ +; ModuleID = 'instruction_error' +source_filename = "instruction_error.ll" + +@str_r1 = internal constant [3 x i8] c"r1\00" + +declare void @my__result_record_output(i8*) + +define void @main() #0 { +entry: +call void @my__result_record_output(i8* getelementptr inbounds ([3 x i8], [3 x i8]* @str_r1, i32 0, i32 0)) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/builder_ptr2ptr_error.ll b/test/resources/qir_test_file/builder_ptr2ptr_error.ll new file mode 100644 index 00000000..4fe7d013 --- /dev/null +++ b/test/resources/qir_test_file/builder_ptr2ptr_error.ll @@ -0,0 +1,23 @@ +; ModuleID = 'ptr_to_ptr_example' +source_filename = "ptr_to_ptr_example.ll" + +declare i32* @my_load(i32**) + +@value = global i32 42 +@p1 = global i32* @value + +define void @main() #0 { +entry: +%ptr = call i32* @my_load(i32** @p1) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/dynamic_allocation.ll b/test/resources/qir_test_file/dynamic_allocation.ll new file mode 100644 index 00000000..199cc66f --- /dev/null +++ b/test/resources/qir_test_file/dynamic_allocation.ll @@ -0,0 +1,49 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +declare %Qubit* @__quantum__rt__qubit_allocate() + +declare void @__quantum__rt__qubit_release(%Qubit*) + +declare %Result* @__quantum__rt__result_get_one() + +declare i1 @__quantum__rt__result_equal(%Result*, %Result*) + +declare %Result* @__quantum__qis__m__body(%Qubit*) + +define void @main() #0 { +entry: + %0 = call %Qubit* @__quantum__rt__qubit_allocate() + call void @__quantum__qis__h__body(%Qubit* %0) + %1 = call %Result* @__quantum__qis__m__body(%Qubit* %0) + %2 = call %Result* @__quantum__rt__result_get_one() + %3 = call i1 @__quantum__rt__result_equal(%Result* %1, %Result* %2) + br i1 %3, label %then, label %else + +then: ; preds = %entry + call void @__quantum__qis__reset__body(%Qubit* %0) + br label %continue + +else: ; preds = %entry + br label %continue + +continue: ; preds = %else, %then + call void @__quantum__rt__qubit_release(%Qubit* %0) + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__reset__body(%Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/dynamic_allocation.qasm b/test/resources/qir_test_file/dynamic_allocation.qasm new file mode 100644 index 00000000..06cca270 --- /dev/null +++ b/test/resources/qir_test_file/dynamic_allocation.qasm @@ -0,0 +1,15 @@ +OPENQASM 3.0; +qubit[1] Qubits_tmp; +bit[2] Results_tmp; +array[bool, 1] BoolType_tmp; +defcal __quantum__rt__qubit_allocate() q_ret {} +defcal __quantum__rt__qubit_release() q {} +__quantum__rt__qubit_allocate(Qubits_tmp[0]); +h Qubits_tmp[0]; +Results_tmp[0] = measure Qubits_tmp[0]; +Results_tmp[1] = true; +BoolType_tmp[0] = Results_tmp[0] == Results_tmp[1]; +if (BoolType_tmp[0]) { + reset Qubits_tmp[0]; +} +__quantum__rt__qubit_release(Qubits_tmp[0]); diff --git a/test/resources/qir_test_file/external_functions.ll b/test/resources/qir_test_file/external_functions.ll new file mode 100644 index 00000000..98790034 --- /dev/null +++ b/test/resources/qir_test_file/external_functions.ll @@ -0,0 +1,30 @@ +; ModuleID = 'external_functions' +source_filename = "external_functions" + +%Qubit = type opaque + +define void @main() #0 { +entry: + call void @my_function() + call void @my_gate(i64 123, %Qubit* null) + %0 = call double @get_angle() + call void @__quantum__qis__rz__body(double %0, %Qubit* null) + ret void +} + +declare void @my_function() + +declare void @my_gate(i64, %Qubit*) + +declare double @get_angle() + +declare void @__quantum__qis__rz__body(double, %Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="1" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/external_functions.qasm b/test/resources/qir_test_file/external_functions.qasm new file mode 100644 index 00000000..c5268ed7 --- /dev/null +++ b/test/resources/qir_test_file/external_functions.qasm @@ -0,0 +1,10 @@ +OPENQASM 3.0; +qubit[1] Qubits; +array[float[64], 1] FloatType_tmp; +defcal my_function() {} +defcal my_gate(int[64]) q {} +defcal get_angle() -> float[64] {} +my_function(); +my_gate(123, Qubits[0]); +FloatType_tmp[0] = get_angle(); +rz(FloatType_tmp[0]) Qubits[0]; \ No newline at end of file diff --git a/test/resources/qir_test_file/if_result.ll b/test/resources/qir_test_file/if_result.ll new file mode 100644 index 00000000..ba5d3136 --- /dev/null +++ b/test/resources/qir_test_file/if_result.ll @@ -0,0 +1,94 @@ +; ModuleID = 'if_result' +source_filename = "if_result" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + %0 = call i1 @__quantum__qis__read_result__body(%Result* null) + br i1 %0, label %then, label %else + +then: ; preds = %entry + call void @__quantum__qis__x__body(%Qubit* null) + br label %continue + +else: ; preds = %entry + br label %continue + +continue: ; preds = %else, %then + call void @__quantum__qis__h__body(%Qubit* null) + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + %1 = call i1 @__quantum__qis__read_result__body(%Result* null) + br i1 %1, label %then1, label %else2 + +then1: ; preds = %continue + %2 = call i1 @__quantum__qis__read_result__body(%Result* inttoptr (i64 1 to %Result*)) + br i1 %2, label %then4, label %else5 + +else2: ; preds = %continue + br label %continue3 + +continue3: ; preds = %else2, %continue6 + %3 = call i1 @__quantum__qis__read_result__body(%Result* null) + br i1 %3, label %then7, label %else8 + +then4: ; preds = %then1 + call void @__quantum__qis__x__body(%Qubit* null) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + br label %continue6 + +else5: ; preds = %then1 + br label %continue6 + +continue6: ; preds = %else5, %then4 + br label %continue3 + +then7: ; preds = %continue3 + br label %continue9 + +else8: ; preds = %continue3 + call void @__quantum__qis__x__body(%Qubit* null) + br label %continue9 + +continue9: ; preds = %else8, %then7 + %4 = call i1 @__quantum__qis__read_result__body(%Result* null) + br i1 %4, label %then10, label %else11 + +then10: ; preds = %continue9 + call void @__quantum__qis__z__body(%Qubit* null) + br label %continue12 + +else11: ; preds = %continue9 + call void @__quantum__qis__y__body(%Qubit* null) + br label %continue12 + +continue12: ; preds = %else11, %then10 + ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +declare i1 @__quantum__qis__read_result__body(%Result*) + +declare void @__quantum__qis__x__body(%Qubit*) + +declare void @__quantum__qis__z__body(%Qubit*) + +declare void @__quantum__qis__y__body(%Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/if_result.qasm b/test/resources/qir_test_file/if_result.qasm new file mode 100644 index 00000000..2dd323fc --- /dev/null +++ b/test/resources/qir_test_file/if_result.qasm @@ -0,0 +1,27 @@ +OPENQASM 3.0; +qubit[2] Qubits; +bit[2] Results; +h Qubits[0]; +Results[0] = measure Qubits[0]; +if (Results[0]) { + x Qubits[0]; +} +h Qubits[0]; +Results[0] = measure Qubits[0]; +h Qubits[1]; +Results[1] = measure Qubits[1]; +if (Results[0]) { + if (Results[1]) { + x Qubits[0]; + x Qubits[1]; + } +} +if (Results[0]) { +} else { + x Qubits[0]; +} +if (Results[0]) { + z Qubits[0]; +} else { + y Qubits[0]; +} \ No newline at end of file diff --git a/test/resources/qir_test_file/if_then.ll b/test/resources/qir_test_file/if_then.ll new file mode 100644 index 00000000..d5ec428e --- /dev/null +++ b/test/resources/qir_test_file/if_then.ll @@ -0,0 +1,30 @@ +; ModuleID = 'if_then' +source_filename = "if_then" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { + call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) + %1 = call i1 @__quantum__qis__read_result__body(%Result* null) + br i1 %1, label %then, label %else + +then: ; preds = %0 + call void @__quantum__qis__x__body(%Qubit* null) + br label %continue + +else: ; preds = %0 + br label %continue + +continue: ; preds = %else, %then + ret void +} + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +declare i1 @__quantum__qis__read_result__body(%Result*) + +declare void @__quantum__qis__x__body(%Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="1" "required_num_results"="1" } +attributes #1 = { "irreversible" } \ No newline at end of file diff --git a/test/resources/qir_test_file/if_then.qasm b/test/resources/qir_test_file/if_then.qasm new file mode 100644 index 00000000..c8b36a7d --- /dev/null +++ b/test/resources/qir_test_file/if_then.qasm @@ -0,0 +1,7 @@ +OPENQASM 3.0; +qubit[1] Qubits; +bit[1] Results; +Results[0] = measure Qubits[0]; +if (Results[0]) { + x Qubits[0]; +} \ No newline at end of file diff --git a/test/resources/qir_test_file/if_then_1.ll b/test/resources/qir_test_file/if_then_1.ll new file mode 100644 index 00000000..c055454d --- /dev/null +++ b/test/resources/qir_test_file/if_then_1.ll @@ -0,0 +1,34 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A: + %0 = call i32 @get_int() + br i1 0, label %B, label %D + +B: ; preds = %entry + %1 = add i32 1, %0 + br i1 0, label %C, label %D + +C: ; preds = %entry + %2 = add i32 2, %0 + br label %D + +D: ; preds = %entry + %3 = add i32 3, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/if_then_1.qasm b/test/resources/qir_test_file/if_then_1.qasm new file mode 100644 index 00000000..a409befb --- /dev/null +++ b/test/resources/qir_test_file/if_then_1.qasm @@ -0,0 +1,10 @@ +OPENQASM 3.0; +array[int[32], 3] IntType_tmp; +input int[32] IntType_i0; +if (0) { + IntType_tmp[0] = 1 + IntType_i0; + if (0) { + IntType_tmp[1] = 2 + IntType_i0; + } +} +IntType_tmp[2] = 3 + IntType_i0; \ No newline at end of file diff --git a/test/resources/qir_test_file/if_then_2.ll b/test/resources/qir_test_file/if_then_2.ll new file mode 100644 index 00000000..e515d0b8 --- /dev/null +++ b/test/resources/qir_test_file/if_then_2.ll @@ -0,0 +1,43 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %D3, label %E4 + +C2: + %3 = add i32 2, %0 + br label %E4 + +D3: + %4 = add i32 3, %0 + br label %F5 + +E4: + %5 = add i32 4, %0 + br label %F5 + +F5: + %6 = add i32 5, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/if_then_2.qasm b/test/resources/qir_test_file/if_then_2.qasm new file mode 100644 index 00000000..7142eb95 --- /dev/null +++ b/test/resources/qir_test_file/if_then_2.qasm @@ -0,0 +1,16 @@ +OPENQASM 3.0; +array[int[32], 6] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 0 + IntType_i0; +if (0) { + IntType_tmp[1] = 1 + IntType_i0; + if (0) { + IntType_tmp[3] = 3 + IntType_i0; + } else { + IntType_tmp[4] = 4 + IntType_i0; + } +} else { + IntType_tmp[2] = 2 + IntType_i0; + IntType_tmp[4] = 4 + IntType_i0; +} +IntType_tmp[5] = 5 + IntType_i0; diff --git a/test/resources/qir_test_file/include.ll b/test/resources/qir_test_file/include.ll new file mode 100644 index 00000000..6b367558 --- /dev/null +++ b/test/resources/qir_test_file/include.ll @@ -0,0 +1,30 @@ +; ModuleID = 'bell' +source_filename = "bell" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: +call void @__quantum__qis__h__body(%Qubit* null) +call void @__quantum__qis__cnot__body(%Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) +call void @__quantum__qis__mz__body(%Qubit* null, %Result* null) +call void @__quantum__qis__mz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) +ret void +} + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cnot__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly) #1 + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/include.qasm b/test/resources/qir_test_file/include.qasm new file mode 100644 index 00000000..bd1c5c41 --- /dev/null +++ b/test/resources/qir_test_file/include.qasm @@ -0,0 +1,8 @@ +OPENQASM 3.0; +include "stdgates.inc"; +qubit[2] Qubits; +bit[2] Results; +h Qubits[0]; +cx Qubits[0], Qubits[1]; +Results[0] = measure Qubits[0]; +Results[1] = measure Qubits[1]; \ No newline at end of file diff --git a/test/resources/qir_test_file/llvm_inst_error.ll b/test/resources/qir_test_file/llvm_inst_error.ll new file mode 100644 index 00000000..36d74797 --- /dev/null +++ b/test/resources/qir_test_file/llvm_inst_error.ll @@ -0,0 +1,15 @@ +; ModuleID = 'llvm_inst_error' +source_filename = "llvm_inst_error.ll" +%MyStruct = type { i32, float } + +@g_i32 = global i32 42, align 4 +@g_struct = global %MyStruct { i32 1, float 1.000000e+00 }, align 8 +@g_array = global [4 x i8] zeroinitializer, align 1 + +define void @main() #0 { +entry: +%0 = load i32, i32* @g_i32, align 4 +ret void +} + +attributes #0 = {"entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } diff --git a/test/resources/qir_test_file/load_error.qir b/test/resources/qir_test_file/load_error.qir new file mode 100644 index 00000000..a87f6bae Binary files /dev/null and b/test/resources/qir_test_file/load_error.qir differ diff --git a/test/resources/qir_test_file/load_error_qir b/test/resources/qir_test_file/load_error_qir new file mode 100644 index 00000000..e69de29b diff --git a/test/resources/qir_test_file/main_func_error.ll b/test/resources/qir_test_file/main_func_error.ll new file mode 100644 index 00000000..7000083d --- /dev/null +++ b/test/resources/qir_test_file/main_func_error.ll @@ -0,0 +1,16 @@ +; ModuleID = 'main_func_error' +source_filename = "main_func_error.ll" + +define void @main() #0 { +entry: +ret void +} + +attributes #0 = {"output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="0" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/mresetz.ll b/test/resources/qir_test_file/mresetz.ll new file mode 100644 index 00000000..a4bf1ea7 --- /dev/null +++ b/test/resources/qir_test_file/mresetz.ll @@ -0,0 +1,23 @@ +; ModuleID = 'mresetz' +source_filename = "mresetz" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__mresetz__body(%Qubit* null, %Result* null) + ret void +} + +declare void @__quantum__qis__mresetz__body(%Qubit*, %Result*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="1" "required_num_results"="1" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/test/resources/qir_test_file/mresetz.qasm b/test/resources/qir_test_file/mresetz.qasm new file mode 100644 index 00000000..ddd092a1 --- /dev/null +++ b/test/resources/qir_test_file/mresetz.qasm @@ -0,0 +1,5 @@ +OPENQASM 3.0; +qubit[1] Qubits; +bit[1] Results; +Results[0] = measure Qubits[0]; +reset Qubits[0]; \ No newline at end of file diff --git a/test/resources/qir_test_file/return_value_error.ll b/test/resources/qir_test_file/return_value_error.ll new file mode 100644 index 00000000..c59c5a0a --- /dev/null +++ b/test/resources/qir_test_file/return_value_error.ll @@ -0,0 +1,21 @@ +; ModuleID = 'return_value_error' +source_filename = "return_value_error.ll" + +%Result = type opaque + +declare void @my_test(%Result*, %Result*) + +define void @main() #0 { +entry: +call void @my_test(%Result* null, %Result* null) +ret void +} + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="0" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} \ No newline at end of file diff --git a/test/resources/qir_test_file/rxx.ll b/test/resources/qir_test_file/rxx.ll new file mode 100644 index 00000000..7e4292ef --- /dev/null +++ b/test/resources/qir_test_file/rxx.ll @@ -0,0 +1,23 @@ +; ModuleID = 'rxx' +source_filename = "rxx" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +entry: + call void @__quantum__qis__rxx__body(double 1.000000e+00, %Qubit* null, %Qubit* inttoptr (i64 1 to %Qubit*)) + ret void +} + +declare void @__quantum__qis__rxx__body(double, %Qubit*, %Qubit*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom" "required_num_qubits"="2" "required_num_results"="0" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} diff --git a/test/resources/qir_test_file/rxx.qasm b/test/resources/qir_test_file/rxx.qasm new file mode 100644 index 00000000..78132e62 --- /dev/null +++ b/test/resources/qir_test_file/rxx.qasm @@ -0,0 +1,12 @@ +OPENQASM 3.0; +qubit[2] Qubits; +gate rxx(_theta) q0, q1 { + h q0; + h q1; + cx q0, q1; + rz(_theta) q1; + cx q0, q1; + h q0; + h q1; +} +rxx(1.0) Qubits[0], Qubits[1]; \ No newline at end of file diff --git a/test/resources/qir_test_file/while_1.ll b/test/resources/qir_test_file/while_1.ll new file mode 100644 index 00000000..1e9dccc5 --- /dev/null +++ b/test/resources/qir_test_file/while_1.ll @@ -0,0 +1,35 @@ +; ModuleID = 'while_1' +source_filename = "while_1" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br label %B1 + +B1: + %2 = add i32 1, %0 + br i1 0, label %C2, label %D3 + +C2: + %3 = add i32 2, %0 + br label %B1 + +D3: + %4 = add i32 3, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/while_1.qasm b/test/resources/qir_test_file/while_1.qasm new file mode 100644 index 00000000..7306cf2f --- /dev/null +++ b/test/resources/qir_test_file/while_1.qasm @@ -0,0 +1,10 @@ +OPENQASM 3.0; +array[int[32], 4] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 0 + IntType_i0; +IntType_tmp[1] = 1 + IntType_i0; +while (0) { + IntType_tmp[2] = 2 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; +} +IntType_tmp[3] = 3 + IntType_i0; diff --git a/test/resources/qir_test_file/while_2.ll b/test/resources/qir_test_file/while_2.ll new file mode 100644 index 00000000..efeb8aef --- /dev/null +++ b/test/resources/qir_test_file/while_2.ll @@ -0,0 +1,41 @@ +; ModuleID = 'while_2' +source_filename = "while_2" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + br label %B1 + +B1: + %1 = add i32 1, %0 + br label %C2 + +C2: + %2 = add i32 2, %0 + br label %D3 + +D3: + %3 = add i32 3, %0 + br i1 0, label %B1, label %E4 + +E4: + %4 = add i32 4, %0 + br i1 0, label %F5, label %C2 + +F5: + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/while_2.qasm b/test/resources/qir_test_file/while_2.qasm new file mode 100644 index 00000000..d5786d0e --- /dev/null +++ b/test/resources/qir_test_file/while_2.qasm @@ -0,0 +1,22 @@ +OPENQASM 3.0; +array[int[32], 4] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 1 + IntType_i0; +IntType_tmp[1] = 2 + IntType_i0; +IntType_tmp[2] = 3 + IntType_i0; +while (0) { + IntType_tmp[0] = 1 + IntType_i0; + IntType_tmp[1] = 2 + IntType_i0; + IntType_tmp[2] = 3 + IntType_i0; +} +IntType_tmp[3] = 4 + IntType_i0; +while (!0) { + IntType_tmp[1] = 2 + IntType_i0; + IntType_tmp[2] = 3 + IntType_i0; + while (0) { + IntType_tmp[0] = 1 + IntType_i0; + IntType_tmp[1] = 2 + IntType_i0; + IntType_tmp[2] = 3 + IntType_i0; + } + IntType_tmp[3] = 4 + IntType_i0; +} \ No newline at end of file diff --git a/test/resources/qir_test_file/while_3.ll b/test/resources/qir_test_file/while_3.ll new file mode 100644 index 00000000..ff0d29d2 --- /dev/null +++ b/test/resources/qir_test_file/while_3.ll @@ -0,0 +1,39 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %E4, label %D3 + +C2: + %3 = add i32 2, %0 + br i1 0, label %D3, label %E4 + +D3: + %4 = add i32 3, %0 + br label %B1 + +E4: + %5 = add i32 4, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/while_3.qasm b/test/resources/qir_test_file/while_3.qasm new file mode 100644 index 00000000..25e0cda3 --- /dev/null +++ b/test/resources/qir_test_file/while_3.qasm @@ -0,0 +1,22 @@ +OPENQASM 3.0; +array[int[32], 5] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 0 + IntType_i0; +if (0) { + IntType_tmp[1] = 1 + IntType_i0; + while (!0) { + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + } +} else { + IntType_tmp[2] = 2 + IntType_i0; + if (0) { + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + while (!0) { + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + } + } +} +IntType_tmp[4] = 4 + IntType_i0; diff --git a/test/resources/qir_test_file/while_4.ll b/test/resources/qir_test_file/while_4.ll new file mode 100644 index 00000000..b9f5c4be --- /dev/null +++ b/test/resources/qir_test_file/while_4.ll @@ -0,0 +1,43 @@ +; ModuleID = 'dynamic_allocation' +source_filename = "dynamic_allocation" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %E4, label %F5 + +C2: + %3 = add i32 2, %0 + br i1 0, label %E4, label %D3 + +D3: + %4 = add i32 3, %0 + br label %B1 + +E4: + %5 = add i32 4, %0 + ret void + +F5: + %6 = add i32 5, %0 + br label %D3 +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/while_4.qasm b/test/resources/qir_test_file/while_4.qasm new file mode 100644 index 00000000..5ab69103 --- /dev/null +++ b/test/resources/qir_test_file/while_4.qasm @@ -0,0 +1,25 @@ +OPENQASM 3.0; +array[int[32], 6] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 0 + IntType_i0; +if (0) { + IntType_tmp[1] = 1 + IntType_i0; + while (!0) { + IntType_tmp[5] = 5 + IntType_i0; + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + } +} else { + IntType_tmp[2] = 2 + IntType_i0; + if (0) { + } else { + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + while (!0) { + IntType_tmp[5] = 5 + IntType_i0; + IntType_tmp[3] = 3 + IntType_i0; + IntType_tmp[1] = 1 + IntType_i0; + } + } +} +IntType_tmp[4] = 4 + IntType_i0; diff --git a/test/resources/qir_test_file/while_5.ll b/test/resources/qir_test_file/while_5.ll new file mode 100644 index 00000000..79864b46 --- /dev/null +++ b/test/resources/qir_test_file/while_5.ll @@ -0,0 +1,39 @@ +; ModuleID = 'while_test' +source_filename = "while_test" + +%Qubit = type opaque +%Result = type opaque + +define void @main() #0 { +A0: + %0 = call i32 @get_int() + %1 = add i32 0, %0 + br i1 0, label %B1, label %C2 + +B1: + %2 = add i32 1, %0 + br i1 0, label %B1, label %E4 + +C2: + %3 = add i32 2, %0 + br i1 0, label %E4, label %D3 + +D3: + %4 = add i32 3, %0 + br i1 0, label %C2, label %D3 + +E4: + %5 = add i32 4, %0 + ret void +} + +declare i32 @get_int() + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="custom"} + +!llvm.module.flags = !{!0, !1, !2, !3} + +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 true} diff --git a/test/resources/qir_test_file/while_5.qasm b/test/resources/qir_test_file/while_5.qasm new file mode 100644 index 00000000..a3439532 --- /dev/null +++ b/test/resources/qir_test_file/while_5.qasm @@ -0,0 +1,20 @@ +OPENQASM 3.0; +array[int[32], 5] IntType_tmp; +input int[32] IntType_i0; +IntType_tmp[0] = 0 + IntType_i0; +if (0) { + IntType_tmp[1] = 1 + IntType_i0; + while (0) { + IntType_tmp[1] = 1 + IntType_i0; + } +} else { + IntType_tmp[2] = 2 + IntType_i0; + while (!0) { + IntType_tmp[3] = 3 + IntType_i0; + while (!0) { + IntType_tmp[3] = 3 + IntType_i0; + } + IntType_tmp[2] = 2 + IntType_i0; + } +} +IntType_tmp[4] = 4 + IntType_i0; diff --git a/test/unit_tests/autoqasm/test_parameters.py b/test/unit_tests/autoqasm/test_parameters.py index 9b214fd0..c020030a 100644 --- a/test/unit_tests/autoqasm/test_parameters.py +++ b/test/unit_tests/autoqasm/test_parameters.py @@ -306,15 +306,15 @@ def rx_theta(q: aq.Qubit, theta: float): @aq.main(num_qubits=3) def parametric(): - rx_theta(2, FreeParameter("θ")) + rx_theta(2, FreeParameter("_theta")) expected = """OPENQASM 3.0; gate rx_theta(theta) q { rx(theta) q; } -input float θ; +input float _theta; qubit[3] __qubits__; -rx_theta(θ) __qubits__[2];""" +rx_theta(_theta) __qubits__[2];""" assert parametric.build().to_ir() == expected diff --git a/test/unit_tests/qir2qasm_trans/test_builder.py b/test/unit_tests/qir2qasm_trans/test_builder.py new file mode 100644 index 00000000..d042cfc5 --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_builder.py @@ -0,0 +1,124 @@ +import os +import re +import textwrap + +import pytest +from autoqasm.qir2qasm_trans.qir_trans import load +from autoqasm.qir2qasm_trans.qir_trans.builder import ( + DeclBuilder, + FunctionBuilder, + InstructionBuilder, + SymbolTable, +) +from autoqasm.qir2qasm_trans.qir_trans.translator import Exporter + + +def test_mresetz_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "mresetz" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_FunctionBuilder_building(): + file_path = "test/resources/qir_test_file" + benchmark_name = "mresetz" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load and convert + mod = load(qir_file_path) + + for func in mod.functions: + # check main function definition + attr_dict = {} + for attr in func.attributes: + matches = re.findall(r'"(\w+)"(?:="([^"]*)")?', attr.decode()) + for k, v in matches: + if v != "": + attr_dict[k] = v + else: + attr_dict[k] = True + + if "entry_point" in attr_dict.keys(): + # if not func.is_declaration: + main = func + symbols = SymbolTable() + func_builder = FunctionBuilder() + inst_builder = InstructionBuilder() + decl_builder = DeclBuilder() + for block in main.blocks: + for inst in block.instructions: + func_builder.building(symbols, inst.type, list(inst.operands)) + inst_builder.building() + decl_builder.building("Qubit", 10) + + +def test_builder_array_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "builder_array_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load and convert + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"vector Undefined!"): + exporter.dumps(module) + + +def test_builder_ptr2ptr_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "builder_ptr2ptr_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load and convert + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"Unsupported ptr to ptr!"): + exporter.dumps(module) + + +def test_builder_instruction_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "builder_instruction_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load and convert + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"Undefined llvm insturction!"): + exporter.dumps(module) + + +def test_builder_defcal(): + file_path = "test/resources/qir_test_file" + benchmark_name = "builder_defcal" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() diff --git a/test/unit_tests/qir2qasm_trans/test_cfg.py b/test/unit_tests/qir2qasm_trans/test_cfg.py new file mode 100644 index 00000000..bfbe2b15 --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_cfg.py @@ -0,0 +1,158 @@ +import os +import textwrap + +import pyqir +import pytest +from autoqasm.qir2qasm_trans.qir_trans import load +from autoqasm.qir2qasm_trans.qir_trans.builder import SymbolTable +from autoqasm.qir2qasm_trans.qir_trans.cfg_pattern import CFGPattern +from autoqasm.qir2qasm_trans.qir_trans.translator import Exporter + + +def test_cfg_pattern_qir_to_qasm(): + cfg_pattern = CFGPattern() + assert cfg_pattern.building(SymbolTable()) == False + + +def test_if_result_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "if_result" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_if_then_1_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "if_then_1" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_if_then_2_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "if_then_2" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_while_1_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "while_1" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_while_2_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "while_2" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_while_3_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "while_3" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_while_4_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "while_4" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_while_5_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "while_5" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() diff --git a/test/unit_tests/qir2qasm_trans/test_end_to_end.py b/test/unit_tests/qir2qasm_trans/test_end_to_end.py new file mode 100644 index 00000000..45821bcd --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_end_to_end.py @@ -0,0 +1,159 @@ +import os +import textwrap + +import pyqir +from autoqasm.qir2qasm_trans.qir_trans import load +from autoqasm.qir2qasm_trans.qir_trans.translator import Exporter + + +def test_bell_pyqir_to_qasm(): + """Test end-to-end conversion of a Bell pair pyqir program to QASM.""" + bell = pyqir.SimpleModule("bell", num_qubits=2, num_results=2) + qis = pyqir.BasicQisBuilder(bell.builder) + + # Add instructions to the module to create a Bell pair and measure both qubits. + qis.h(bell.qubits[0]) + qis.cx(bell.qubits[0], bell.qubits[1]) + qis.mz(bell.qubits[0], bell.results[0]) + qis.mz(bell.qubits[1], bell.results[1]) + + file_path = "test/resources/qir_test_file" + benchmark_name = "bell" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + with open(qir_file_path, "w") as f: + f.write(bell.ir()) + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_arithmetic_pyqir_to_qasm(): + """Test end-to-end conversion of an arithmetic pyqir program to QASM.""" + + mod = pyqir.SimpleModule("arithmetic", num_qubits=0, num_results=0) + # Declare functions that can produce and consume integers at runtime. + i32 = pyqir.IntType(mod.context, 32) + get_int = mod.add_external_function("get_int", pyqir.FunctionType(i32, [])) + take_int = mod.add_external_function( + "take_int", pyqir.FunctionType(pyqir.Type.void(mod.context), [i32]) + ) + + # Add 3 to a number and multiply the result by 2. + a = mod.builder.call(get_int, []) + assert a is not None + # Python numbers need to be converted into QIR constant values. Since it's being + # added to a 32-bit integer returned by get_int, its type needs to be the same. + three = pyqir.const(i32, 3) + b = mod.builder.add(three, a) + c = mod.builder.mul(pyqir.const(i32, 2), b) + + # Negation can be done by subtracting an integer from zero. + x = mod.builder.call(get_int, []) + assert x is not None + negative_x = mod.builder.sub(pyqir.const(i32, 0), x) + + # Consume the results. + mod.builder.call(take_int, [c]) + mod.builder.call(take_int, [negative_x]) + + # Sample QIR content for the arithmetic circuit + file_path = "test/resources/qir_test_file" + benchmark_name = "arithmetic" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + with open(qir_file_path, "w") as f: + f.write(mod.ir()) + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_bernstein_vazirani_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "bernstein_vazirani" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_external_functions_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "external_functions" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_dynamic_allocation_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "dynamic_allocation" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_dynamic_if_then_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "if_then" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() diff --git a/test/unit_tests/qir2qasm_trans/test_init.py b/test/unit_tests/qir2qasm_trans/test_init.py new file mode 100644 index 00000000..ce9bf818 --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_init.py @@ -0,0 +1,71 @@ +import os +import textwrap + +import pyqir +import pytest +from autoqasm.qir2qasm_trans.qir_trans import load +from autoqasm.qir2qasm_trans.qir_trans.translator import Exporter + + +def test_bitcode_load(): + """Test end-to-end conversion of a Bell pair QIR program to QASM.""" + bell = pyqir.SimpleModule("bell", num_qubits=2, num_results=2) + qis = pyqir.BasicQisBuilder(bell.builder) + + # Add instructions to the module to create a Bell pair and measure both qubits. + qis.h(bell.qubits[0]) + qis.cx(bell.qubits[0], bell.qubits[1]) + qis.mz(bell.qubits[0], bell.results[0]) + qis.mz(bell.qubits[1], bell.results[1]) + + # Sample QIR content for a Bell pair circuit + # Expected QASM output + expected_qasm = """ + OPENQASM 3.0; + qubit[2] Qubits; + bit[2] Results; + h Qubits[0]; + cx Qubits[0], Qubits[1]; + Results[0] = measure Qubits[0]; + Results[1] = measure Qubits[1]; + """ + + file_path = "test/resources/qir_test_file" + benchmark_name = "bitcode_load" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.bc") + + with open(qir_file_path, "wb") as f: + f.write(bell.bitcode()) + + # Load the QIR module + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + qasm_output = exporter.dumps(module) + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_load_error(): + """Test end-to-end conversion of a Bell pair QIR program to QASM.""" + bell = pyqir.SimpleModule("bell", num_qubits=2, num_results=2) + qis = pyqir.BasicQisBuilder(bell.builder) + + # Add instructions to the module to create a Bell pair and measure both qubits. + qis.h(bell.qubits[0]) + qis.cx(bell.qubits[0], bell.qubits[1]) + qis.mz(bell.qubits[0], bell.results[0]) + qis.mz(bell.qubits[1], bell.results[1]) + + file_path = "test/resources/qir_test_file" + benchmark_name = "load_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.qir") + + with open(qir_file_path, "wb") as f: + f.write(bell.bitcode()) + + # Convert to QASM using the Exporter + with pytest.raises(ValueError, match=r"Unsupported file extension: .qir"): + load(str(qir_file_path)) diff --git a/test/unit_tests/qir2qasm_trans/test_qir_profile.py b/test/unit_tests/qir2qasm_trans/test_qir_profile.py new file mode 100644 index 00000000..979f248d --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_qir_profile.py @@ -0,0 +1,5 @@ +from autoqasm.qir2qasm_trans.qir_trans.qir_profile import Profile + + +def test_qir_profile(): + Profile(name="test") diff --git a/test/unit_tests/qir2qasm_trans/test_translator.py b/test/unit_tests/qir2qasm_trans/test_translator.py new file mode 100644 index 00000000..0e470c6b --- /dev/null +++ b/test/unit_tests/qir2qasm_trans/test_translator.py @@ -0,0 +1,102 @@ +import os +import textwrap + +import pytest +from autoqasm.qir2qasm_trans.qir_trans import load +from autoqasm.qir2qasm_trans.qir_trans.translator import Exporter + + +def test_rxx_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "rxx" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_arguments_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "arguments" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter() + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_include_qir_to_qasm(): + file_path = "test/resources/qir_test_file" + benchmark_name = "include" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + qasm_file_path = os.path.join(file_path, f"{benchmark_name}.qasm") + + # Load and convert + module = load(qir_file_path) + exporter = Exporter(include_files=["stdgates.inc"]) + qasm_output = exporter.dumps(module) + + with open(qasm_file_path, "r") as f: + expected_qasm = f.read() + + # Validate the complete output + assert qasm_output.strip() == textwrap.dedent(expected_qasm).strip() + + +def test_return_value_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "return_value_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load the QIR module + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"Too much return value!"): + exporter.dumps(module) + + +def test_main_func_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "main_func_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load the QIR module + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"No main function defined!"): + exporter.dumps(module) + + +def test_llvm_inst_error(): + file_path = "test/resources/qir_test_file" + benchmark_name = "llvm_inst_error" + qir_file_path = os.path.join(file_path, f"{benchmark_name}.ll") + + # Load the QIR module + module = load(str(qir_file_path)) + + # Convert to QASM using the Exporter + exporter = Exporter() + with pytest.raises(Exception, match=r"Undefined llvm instruction: load"): + exporter.dumps(module)