From 24b7a72596c57fe9144a97e1ef315561723cd572 Mon Sep 17 00:00:00 2001 From: Ian Davis Date: Wed, 24 Apr 2024 14:43:11 -0700 Subject: [PATCH 01/46] Setting up initial features --- .gitignore | 1 + .prettierignore | 1 + Cargo.lock | 705 ++- Cargo.toml | 1 + build.py | 8 +- compiler/qsc/Cargo.toml | 1 + compiler/qsc/src/codegen.rs | 207 +- compiler/qsc/src/codegen/tests.rs | 8 +- compiler/qsc/src/incremental.rs | 13 +- compiler/qsc/src/interpret.rs | 62 +- compiler/qsc/src/interpret/tests.rs | 49 +- compiler/qsc/src/lib.rs | 8 + compiler/qsc_ast/src/ast.rs | 10 + compiler/qsc_codegen/src/qsharp.rs | 16 +- compiler/qsc_frontend/src/compile.rs | 2 +- compiler/qsc_qasm3/Cargo.toml | 35 + compiler/qsc_qasm3/README.md | 38 + compiler/qsc_qasm3/src/angle.rs | 434 ++ compiler/qsc_qasm3/src/ast_builder.rs | 1562 ++++++ compiler/qsc_qasm3/src/compile.rs | 4364 +++++++++++++++++ compiler/qsc_qasm3/src/compile/tests.rs | 53 + compiler/qsc_qasm3/src/io.rs | 89 + compiler/qsc_qasm3/src/lib.rs | 595 +++ compiler/qsc_qasm3/src/oqasm_helpers.rs | 183 + compiler/qsc_qasm3/src/oqasm_types.rs | 184 + compiler/qsc_qasm3/src/parse.rs | 265 + compiler/qsc_qasm3/src/parse/tests.rs | 61 + compiler/qsc_qasm3/src/runtime.rs | 265 + compiler/qsc_qasm3/src/symbols.rs | 458 ++ compiler/qsc_qasm3/src/tests.rs | 311 ++ compiler/qsc_qasm3/src/tests/assignment.rs | 77 + .../qsc_qasm3/src/tests/assignment/alias.rs | 79 + .../qsc_qasm3/src/tests/assignment/slices.rs | 2 + compiler/qsc_qasm3/src/tests/declaration.rs | 115 + .../qsc_qasm3/src/tests/declaration/array.rs | 36 + .../src/tests/declaration/array/bit.rs | 70 + .../src/tests/declaration/array/qubit.rs | 41 + .../qsc_qasm3/src/tests/declaration/bit.rs | 88 + .../qsc_qasm3/src/tests/declaration/bool.rs | 105 + .../src/tests/declaration/complex.rs | 374 ++ .../qsc_qasm3/src/tests/declaration/float.rs | 348 ++ .../qsc_qasm3/src/tests/declaration/gate.rs | 96 + .../src/tests/declaration/integer.rs | 393 ++ .../qsc_qasm3/src/tests/declaration/io.rs | 6 + .../tests/declaration/io/explicit_input.rs | 225 + .../tests/declaration/io/explicit_output.rs | 279 ++ .../tests/declaration/io/implicit_output.rs | 264 + .../qsc_qasm3/src/tests/declaration/qubit.rs | 59 + .../src/tests/declaration/unsigned_integer.rs | 269 + compiler/qsc_qasm3/src/tests/expression.rs | 14 + .../qsc_qasm3/src/tests/expression/binary.rs | 53 + .../binary/arithmetic_conversions.rs | 132 + .../src/tests/expression/binary/comparison.rs | 276 ++ .../src/tests/expression/binary/ident.rs | 70 + .../src/tests/expression/binary/literal.rs | 4 + .../binary/literal/multiplication.rs | 73 + .../qsc_qasm3/src/tests/expression/bits.rs | 103 + .../qsc_qasm3/src/tests/expression/ident.rs | 152 + .../expression/implicit_cast_from_bit.rs | 206 + .../expression/implicit_cast_from_bitarray.rs | 159 + .../expression/implicit_cast_from_bool.rs | 241 + .../expression/implicit_cast_from_complex.rs | 2 + .../expression/implicit_cast_from_float.rs | 223 + .../expression/implicit_cast_from_int.rs | 215 + .../qsc_qasm3/src/tests/expression/indexed.rs | 117 + .../qsc_qasm3/src/tests/expression/unary.rs | 134 + compiler/qsc_qasm3/src/tests/output.rs | 344 ++ .../qsc_qasm3/src/tests/sample_circuits.rs | 5 + .../src/tests/sample_circuits/bell_pair.rs | 44 + .../tests/sample_circuits/rgqft_multiplier.rs | 127 + compiler/qsc_qasm3/src/tests/scopes.rs | 57 + compiler/qsc_qasm3/src/tests/statement.rs | 15 + .../src/tests/statement/annotation.rs | 75 + compiler/qsc_qasm3/src/tests/statement/end.rs | 41 + .../qsc_qasm3/src/tests/statement/for_loop.rs | 171 + .../src/tests/statement/gate_call.rs | 154 + .../qsc_qasm3/src/tests/statement/if_stmt.rs | 164 + .../statement/implicit_modified_gate_call.rs | 122 + .../qsc_qasm3/src/tests/statement/include.rs | 66 + .../qsc_qasm3/src/tests/statement/measure.rs | 129 + .../src/tests/statement/modified_gate_call.rs | 257 + .../qsc_qasm3/src/tests/statement/reset.rs | 180 + .../qsc_qasm3/src/tests/statement/switch.rs | 302 ++ .../src/tests/statement/while_loop.rs | 64 + compiler/qsc_qasm3/src/types.rs | 316 ++ pip/Cargo.toml | 1 + pip/pyproject.toml | 1 + pip/qsharp/_fs.py | 69 +- pip/qsharp/_http.py | 24 + pip/qsharp/_ipython.py | 8 + pip/qsharp/_native.pyi | 223 +- pip/qsharp/_qsharp.py | 104 +- pip/qsharp/interop/__init__.py | 33 + pip/qsharp/interop/qiskit/__init__.py | 7 + .../interop/qiskit/backends/__init__.py | 8 + .../interop/qiskit/backends/backend_base.py | 534 ++ .../interop/qiskit/backends/compilation.py | 36 + pip/qsharp/interop/qiskit/backends/errors.py | 29 + .../interop/qiskit/backends/qirtarget.py | 123 + .../interop/qiskit/backends/qsharp_backend.py | 268 + .../interop/qiskit/backends/re_backend.py | 190 + .../interop/qiskit/execution/__init__.py | 4 + .../interop/qiskit/execution/default.py | 10 + pip/qsharp/interop/qiskit/jobs/__init__.py | 5 + pip/qsharp/interop/qiskit/jobs/qsjob.py | 117 + pip/qsharp/interop/qiskit/jobs/qsjobset.py | 123 + pip/qsharp/interop/qiskit/passes/__init__.py | 4 + .../interop/qiskit/passes/remove_delay.py | 22 + pip/src/interop.rs | 620 +++ pip/src/interpreter.rs | 201 +- pip/src/lib.rs | 1 + pip/tests-integration/__init__.py | 4 + pip/tests-integration/conftest.py | 13 + .../interop_qiskit/__init__.py | 26 + .../resources/custom_intrinsics.inc | 5 + .../resources/custom_intrinsics.ll | 38 + .../interop_qiskit/test_circuits/__init__.py | 47 + .../interop_qiskit/test_circuits/random.py | 61 + .../test_circuits/test_circuits.py | 683 +++ .../interop_qiskit/test_gate_correctness.py | 292 ++ .../interop_qiskit/test_gateset_qasm.py | 169 + .../interop_qiskit/test_qir.py | 228 + .../interop_qiskit/test_re.py | 97 + .../interop_qiskit/test_run_sim.py | 157 + pip/tests-integration/test_requirements.txt | 5 +- resource_estimator/src/lib.rs | 3 +- samples/estimation/estimation-qiskit.ipynb | 623 +++ samples/python_interop/qiskit.ipynb | 482 ++ wasm/src/lib.rs | 2 +- wasm/src/tests.rs | 2 +- 130 files changed, 23161 insertions(+), 263 deletions(-) create mode 100644 compiler/qsc_qasm3/Cargo.toml create mode 100644 compiler/qsc_qasm3/README.md create mode 100644 compiler/qsc_qasm3/src/angle.rs create mode 100644 compiler/qsc_qasm3/src/ast_builder.rs create mode 100644 compiler/qsc_qasm3/src/compile.rs create mode 100644 compiler/qsc_qasm3/src/compile/tests.rs create mode 100644 compiler/qsc_qasm3/src/io.rs create mode 100644 compiler/qsc_qasm3/src/lib.rs create mode 100644 compiler/qsc_qasm3/src/oqasm_helpers.rs create mode 100644 compiler/qsc_qasm3/src/oqasm_types.rs create mode 100644 compiler/qsc_qasm3/src/parse.rs create mode 100644 compiler/qsc_qasm3/src/parse/tests.rs create mode 100644 compiler/qsc_qasm3/src/runtime.rs create mode 100644 compiler/qsc_qasm3/src/symbols.rs create mode 100644 compiler/qsc_qasm3/src/tests.rs create mode 100644 compiler/qsc_qasm3/src/tests/assignment.rs create mode 100644 compiler/qsc_qasm3/src/tests/assignment/alias.rs create mode 100644 compiler/qsc_qasm3/src/tests/assignment/slices.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/array.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/array/bit.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/array/qubit.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/bit.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/bool.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/complex.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/float.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/gate.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/integer.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/io.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/io/explicit_input.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/io/explicit_output.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/io/implicit_output.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/qubit.rs create mode 100644 compiler/qsc_qasm3/src/tests/declaration/unsigned_integer.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary/arithmetic_conversions.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary/comparison.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary/ident.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary/literal.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/binary/literal/multiplication.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/bits.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/ident.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bit.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bitarray.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bool.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_complex.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_float.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_int.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/indexed.rs create mode 100644 compiler/qsc_qasm3/src/tests/expression/unary.rs create mode 100644 compiler/qsc_qasm3/src/tests/output.rs create mode 100644 compiler/qsc_qasm3/src/tests/sample_circuits.rs create mode 100644 compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs create mode 100644 compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs create mode 100644 compiler/qsc_qasm3/src/tests/scopes.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/annotation.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/end.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/for_loop.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/gate_call.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/if_stmt.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/implicit_modified_gate_call.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/include.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/measure.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/modified_gate_call.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/reset.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/switch.rs create mode 100644 compiler/qsc_qasm3/src/tests/statement/while_loop.rs create mode 100644 compiler/qsc_qasm3/src/types.rs create mode 100644 pip/qsharp/interop/__init__.py create mode 100644 pip/qsharp/interop/qiskit/__init__.py create mode 100644 pip/qsharp/interop/qiskit/backends/__init__.py create mode 100644 pip/qsharp/interop/qiskit/backends/backend_base.py create mode 100644 pip/qsharp/interop/qiskit/backends/compilation.py create mode 100644 pip/qsharp/interop/qiskit/backends/errors.py create mode 100644 pip/qsharp/interop/qiskit/backends/qirtarget.py create mode 100644 pip/qsharp/interop/qiskit/backends/qsharp_backend.py create mode 100644 pip/qsharp/interop/qiskit/backends/re_backend.py create mode 100644 pip/qsharp/interop/qiskit/execution/__init__.py create mode 100644 pip/qsharp/interop/qiskit/execution/default.py create mode 100644 pip/qsharp/interop/qiskit/jobs/__init__.py create mode 100644 pip/qsharp/interop/qiskit/jobs/qsjob.py create mode 100644 pip/qsharp/interop/qiskit/jobs/qsjobset.py create mode 100644 pip/qsharp/interop/qiskit/passes/__init__.py create mode 100644 pip/qsharp/interop/qiskit/passes/remove_delay.py create mode 100644 pip/src/interop.rs create mode 100644 pip/tests-integration/__init__.py create mode 100644 pip/tests-integration/conftest.py create mode 100644 pip/tests-integration/interop_qiskit/__init__.py create mode 100644 pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc create mode 100644 pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll create mode 100644 pip/tests-integration/interop_qiskit/test_circuits/__init__.py create mode 100644 pip/tests-integration/interop_qiskit/test_circuits/random.py create mode 100644 pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py create mode 100644 pip/tests-integration/interop_qiskit/test_gate_correctness.py create mode 100644 pip/tests-integration/interop_qiskit/test_gateset_qasm.py create mode 100644 pip/tests-integration/interop_qiskit/test_qir.py create mode 100644 pip/tests-integration/interop_qiskit/test_re.py create mode 100644 pip/tests-integration/interop_qiskit/test_run_sim.py create mode 100644 samples/estimation/estimation-qiskit.ipynb create mode 100644 samples/python_interop/qiskit.ipynb diff --git a/.gitignore b/.gitignore index 7e98fc144d..8587b8fcf9 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,4 @@ __pycache__/ .mypy_cache/ .pytest_cache/ .idea/ +*.so diff --git a/.prettierignore b/.prettierignore index 07ab6d39f1..8cc94b7af4 100644 --- a/.prettierignore +++ b/.prettierignore @@ -26,3 +26,4 @@ __pycache__/ /vscode/test/out/ /vscode/test/**/test-workspace/ /wasm/ +/pip/ diff --git a/Cargo.lock b/Cargo.lock index f77fa50a7b..fd5c8a7e23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -33,6 +44,15 @@ dependencies = [ "mimalloc-sys", ] +[[package]] +name = "always-assert" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4436e0292ab1bb631b42973c61205e704475fe8126af845c8d923c0996328127" +dependencies = [ + "log", +] + [[package]] name = "anes" version = "0.1.6" @@ -103,6 +123,17 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +[[package]] +name = "ariadne" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd" +dependencies = [ + "concolor", + "unicode-width", + "yansi", +] + [[package]] name = "async-trait" version = "0.1.81" @@ -111,7 +142,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -135,12 +166,38 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "backtrace-ext" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "537beee3be4a18fb023b570f80e3ae28003db9167a751266b259926e25539d50" +dependencies = [ + "backtrace", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" +[[package]] +name = "boolenum" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6c8abd585d7026df20a9ae12982127ba5e81cc7a09397b957e71659da8c5de8" +dependencies = [ + "proc-macro-error", + "quote", + "syn 1.0.109", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -149,9 +206,9 @@ checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" -version = "1.17.0" +version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fd4c6dcc3b0aea2f5c0b4b82c2b15fe39ddbc76041a310848f4706edf76bb31" +checksum = "773d90827bc3feecfb67fab12e24de0749aad83c74b9504ecde46237b5cd24e2" [[package]] name = "byteorder" @@ -167,9 +224,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.1.13" +version = "1.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72db2f7947ecee9b03b510377e8bb9077afa27176fdbff55c51027e976fdcc48" +checksum = "57b6a275aa2903740dc87da01c62040406b8812552e97129a63ea8850a17c6e6" dependencies = [ "jobserver", "libc", @@ -240,7 +297,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -255,6 +312,38 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fd119d74b830634cea2a0f58bbd0d54540518a14397557951e79340abc28c0" +[[package]] +name = "concolor" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b946244a988c390a94667ae0e3958411fa40cc46ea496a929b263d883f5f9c3" +dependencies = [ + "bitflags 1.3.2", + "concolor-query", + "is-terminal", +] + +[[package]] +name = "concolor-query" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d11d52c3d7ca2e6d0040212be9e4dbbcd78b6447f535b6b561f449427944cf" +dependencies = [ + "windows-sys 0.45.0", +] + +[[package]] +name = "countme" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636" + +[[package]] +name = "cov-mark" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0570650661aa447e7335f1d5e4f499d8e58796e617bedc9267d971e51c8b49d4" + [[package]] name = "criterion" version = "0.5.1" @@ -289,6 +378,21 @@ dependencies = [ "itertools", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22ec99545bb0ed0ea7bb9b8e1e9122ea386ff8a48c0922e43f36d45ab09e0e80" + [[package]] name = "crunchy" version = "0.2.2" @@ -307,6 +411,12 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d" +[[package]] +name = "drop_bomb" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bda8e21c04aca2ae33ffc2fd8c23134f3cac46db123ba97bd9d3f3b8a4a85e1" + [[package]] name = "either" version = "1.13.0" @@ -330,7 +440,7 @@ checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -356,6 +466,22 @@ dependencies = [ "log", ] +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + [[package]] name = "expect-test" version = "1.5.0" @@ -428,7 +554,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -499,6 +625,21 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" + [[package]] name = "heck" version = "0.5.0" @@ -523,6 +664,16 @@ version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" +[[package]] +name = "indexmap" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93ead53efc7ea8ed3cfb0c79fc8023fbb782a5432b52830b6518941cebe6505c" +dependencies = [ + "equivalent", + "hashbrown 0.14.5", +] + [[package]] name = "indoc" version = "2.0.5" @@ -540,6 +691,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.1" @@ -570,6 +727,12 @@ dependencies = [ "libc", ] +[[package]] +name = "jod-thread" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b23360e99b8717f20aaa4598f5a6541efbe30630039fbc7706cf954a87947ae" + [[package]] name = "js-sys" version = "0.3.70" @@ -619,6 +782,12 @@ dependencies = [ "qsc", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" + [[package]] name = "log" version = "0.4.22" @@ -656,9 +825,15 @@ version = "7.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1" dependencies = [ + "backtrace", + "backtrace-ext", "cfg-if", "miette-derive", "owo-colors", + "supports-color", + "supports-hyperlinks", + "supports-unicode", + "terminal_size", "textwrap", "thiserror", "unicode-width", @@ -672,7 +847,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -691,6 +866,15 @@ dependencies = [ "adler", ] +[[package]] +name = "miow" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ffbca2f655e33c08be35d87278e5b18b89550a37dbd598c20db92f6a471123" +dependencies = [ + "windows-sys 0.42.0", +] + [[package]] name = "nalgebra" version = "0.33.0" @@ -715,7 +899,7 @@ checksum = "254a5372af8fc138e36684761d3c0cdb758a4410e938babcff1c860ce14ddbfc" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -792,9 +976,9 @@ dependencies = [ [[package]] name = "object" -version = "0.36.3" +version = "0.36.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b64972346851a39438c60b341ebc01bba47464ae329e55cf343eb93964efd9" +checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a" dependencies = [ "memchr", ] @@ -811,6 +995,72 @@ version = "11.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b410bbe7e14ab526a0e86877eb47c6996a2bd7746f027ba551028c925390e4e9" +[[package]] +name = "oq3_lexer" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0de2f0f9d48042c12f82b2550808378718627e108fc3f6adf63e02e5293541a3" +dependencies = [ + "unicode-properties", + "unicode-xid", +] + +[[package]] +name = "oq3_parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e69b215426a4a2a023fd62cca4436c633ba0ab39ee260aca875ac60321b3704b" +dependencies = [ + "drop_bomb", + "oq3_lexer", + "ra_ap_limit", +] + +[[package]] +name = "oq3_semantics" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e15e9cee54e92fb1b3aaa42556b0bd76c8c1c10912a7d6798f43dfc3afdcb0d" +dependencies = [ + "boolenum", + "hashbrown 0.12.3", + "oq3_source_file", + "oq3_syntax", + "rowan", +] + +[[package]] +name = "oq3_source_file" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f65243cc4807c600c544a21db6c17544c53aa2bc034b3eccf388251cc6530e7" +dependencies = [ + "ariadne", + "oq3_syntax", +] + +[[package]] +name = "oq3_syntax" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c3d637a7db9ddb3811719db8a466bd4960ea668df4b2d14043a1b0038465b0" +dependencies = [ + "cov-mark", + "either", + "indexmap", + "itertools", + "once_cell", + "oq3_lexer", + "oq3_parser", + "ra_ap_stdx", + "rowan", + "rustc-hash", + "rustversion", + "smol_str", + "triomphe", + "xshell", +] + [[package]] name = "owo-colors" version = "4.0.0" @@ -860,6 +1110,30 @@ dependencies = [ "special", ] +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + [[package]] name = "proc-macro2" version = "1.0.86" @@ -919,7 +1193,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -932,7 +1206,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -970,6 +1244,7 @@ dependencies = [ "qsc_hir", "qsc_linter", "qsc_lowerer", + "qsc_parse", "qsc_partial_eval", "qsc_passes", "qsc_project", @@ -1039,7 +1314,7 @@ dependencies = [ name = "qsc_data_structures" version = "0.0.0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "expect-test", "miette", "rustc-hash", @@ -1231,11 +1506,32 @@ dependencies = [ "thiserror", ] +[[package]] +name = "qsc_qasm3" +version = "0.0.0" +dependencies = [ + "bitflags 2.6.0", + "difference", + "expect-test", + "indoc", + "miette", + "num-bigint", + "oq3_lexer", + "oq3_parser", + "oq3_semantics", + "oq3_source_file", + "oq3_syntax", + "qsc", + "qsc_qasm3", + "rustc-hash", + "thiserror", +] + [[package]] name = "qsc_rca" version = "0.0.0" dependencies = [ - "bitflags", + "bitflags 2.6.0", "expect-test", "indenter", "miette", @@ -1299,6 +1595,7 @@ dependencies = [ "num-complex", "pyo3", "qsc", + "qsc_qasm3", "resource_estimator", "rustc-hash", "serde_json", @@ -1340,13 +1637,33 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.36" +version = "1.0.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" dependencies = [ "proc-macro2", ] +[[package]] +name = "ra_ap_limit" +version = "0.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d33758724f997689f84146e5401e28d875a061804f861f113696f44f5232aa" + +[[package]] +name = "ra_ap_stdx" +version = "0.0.188" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e80fb2ff88b31fa35cde89ae13ea7c9ada97c7a2c778dcafef530a267658000" +dependencies = [ + "always-assert", + "crossbeam-channel", + "jod-thread", + "libc", + "miow", + "winapi", +] + [[package]] name = "rand" version = "0.8.5" @@ -1444,6 +1761,18 @@ dependencies = [ "thiserror", ] +[[package]] +name = "rowan" +version = "0.15.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d" +dependencies = [ + "countme", + "hashbrown 0.14.5", + "rustc-hash", + "text-size", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -1456,6 +1785,25 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" +[[package]] +name = "rustix" +version = "0.38.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a85d50532239da68e9addb745ba38ff4612a242c1c7ceea689c4bc7c2f43c36f" +dependencies = [ + "bitflags 2.6.0", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -1491,9 +1839,9 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff085d2cb684faa248efb494c39b68e522822ac0de72ccf08109abde717cfb2" +checksum = "99fce0ffe7310761ca6bf9faf5115afbc19688edd00171d81b1bb1b116c63e09" dependencies = [ "serde_derive", ] @@ -1511,20 +1859,20 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.208" +version = "1.0.209" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24008e81ff7613ed8e5ba0cfaf24e2c2f1e5b8a0495711e44fcd4882fca62bcf" +checksum = "a5831b979fd7b5439637af1752d535ff49f4860c0f341d1baeb6faf0f4242170" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] name = "serde_json" -version = "1.0.125" +version = "1.0.127" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83c8e735a073ccf5be70aa8066aa984eaf2fa000db6c8d0100ae605b366d31ed" +checksum = "8043c06d9f82bd7271361ed64f415fe5e12a77fdb52e573e7f06a516dea329ad" dependencies = [ "itoa", "memchr", @@ -1566,6 +1914,15 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" +[[package]] +name = "smol_str" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd538fb6910ac1099850255cf94a94df6551fbdd602454387d0adb2d1ca6dead" +dependencies = [ + "serde", +] + [[package]] name = "special" version = "0.10.3" @@ -1581,11 +1938,43 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "supports-color" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9829b314621dfc575df4e409e79f9d6a66a3bd707ab73f23cb4aa3a854ac854f" +dependencies = [ + "is_ci", +] + +[[package]] +name = "supports-hyperlinks" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c0a1e5168041f5f3ff68ff7d95dcb9c8749df29f6e7e89ada40dd4c9de404ee" + +[[package]] +name = "supports-unicode" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7401a30af6cb5818bb64852270bb722533397edcfc7344954a38f420819ece2" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "syn" -version = "2.0.75" +version = "2.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6af063034fc1935ede7be0122941bafa9bacb949334d090b77ca98b5817c7d9" +checksum = "578e081a14e0cefc3279b0472138c513f37b41a08d5a3cca9b6e4e8ceb6cd525" dependencies = [ "proc-macro2", "quote", @@ -1598,6 +1987,22 @@ version = "0.12.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" +[[package]] +name = "terminal_size" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" +dependencies = [ + "rustix", + "windows-sys 0.48.0", +] + +[[package]] +name = "text-size" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233" + [[package]] name = "textwrap" version = "0.16.1" @@ -1626,7 +2031,7 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] [[package]] @@ -1641,9 +2046,9 @@ dependencies = [ [[package]] name = "tokio" -version = "1.39.3" +version = "1.40.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9babc99b9923bfa4804bd74722ff02c0381021eafa4db9949217e3be8e84fff5" +checksum = "e2b070231665d27ad9ec9b8df639893f46727666c6767db40317fbe920a5d998" dependencies = [ "backtrace", "pin-project-lite", @@ -1658,9 +2063,15 @@ checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] +[[package]] +name = "triomphe" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6631e42e10b40c0690bf92f404ebcfe6e1fdb480391d15f17cc8e96eeed5369" + [[package]] name = "typenum" version = "1.17.0" @@ -1679,12 +2090,24 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" +[[package]] +name = "unicode-properties" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52ea75f83c0137a9b98608359a5f1af8144876eb67bcb1ce837368e906a9f524" + [[package]] name = "unicode-width" version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d" +[[package]] +name = "unicode-xid" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229730647fbc343e3a80e463c1db7f78f3855d3f3739bee0dda773c9a037c90a" + [[package]] name = "unindent" version = "0.2.3" @@ -1697,6 +2120,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + [[package]] name = "walkdir" version = "2.5.0" @@ -1735,7 +2164,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-shared", ] @@ -1769,7 +2198,7 @@ checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -1800,6 +2229,22 @@ dependencies = [ "safe_arch", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + [[package]] name = "winapi-util" version = "0.1.9" @@ -1809,13 +2254,52 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -1824,7 +2308,37 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -1833,28 +2347,64 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -1867,30 +2417,99 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "xshell" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db0ab86eae739efd1b054a8d3d16041914030ac4e01cd1dca0cf252fd8b6437" +dependencies = [ + "xshell-macros", +] + +[[package]] +name = "xshell-macros" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d422e8e38ec76e2f06ee439ccc765e9c6a9638b9e7c9f2e8255e4d41e8bd852" + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + [[package]] name = "zerocopy" version = "0.7.35" @@ -1909,5 +2528,5 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn", + "syn 2.0.76", ] diff --git a/Cargo.toml b/Cargo.toml index 1ac2c00287..2f131b19d7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,6 +18,7 @@ members = [ "compiler/qsc_partial_eval", "compiler/qsc_passes", "compiler/qsc_project", + "compiler/qsc_qasm3", "compiler/qsc_rir", "fuzz", "katas", diff --git a/build.py b/build.py index 9913f2e1ea..12c1869341 100755 --- a/build.py +++ b/build.py @@ -514,6 +514,8 @@ def run_ci_historic_benchmark(): "ipykernel", "nbconvert", "pandas", + "qsharp-widgets", + "qiskit>=1.2.0,<1.3.0", ] subprocess.run(pip_install_args, check=True, text=True, cwd=root_dir, env=pip_env) @@ -552,11 +554,11 @@ def run_ci_historic_benchmark(): dir for dir in os.walk(samples_src) if "qsharp.json" in dir[2] ] - test_projects_directories = [ dir - for dir, _, _ in project_directories - if dir.find("testing") != -1 + test_projects_directories = [ + dir for dir, _, _ in project_directories if dir.find("testing") != -1 ] + install_python_test_requirements(pip_src, python_bin) for test_project_dir in test_projects_directories: run_python_tests(test_project_dir, python_bin) step_end() diff --git a/compiler/qsc/Cargo.toml b/compiler/qsc/Cargo.toml index 99aabfed1b..06a4950dd6 100644 --- a/compiler/qsc/Cargo.toml +++ b/compiler/qsc/Cargo.toml @@ -28,6 +28,7 @@ qsc_ast = { path = "../qsc_ast" } qsc_fir = { path = "../qsc_fir" } qsc_hir = { path = "../qsc_hir" } qsc_passes = { path = "../qsc_passes" } +qsc_parse = { path = "../qsc_parse" } qsc_partial_eval = { path = "../qsc_partial_eval" } qsc_project = { path = "../qsc_project", features = ["fs"] } qsc_rca = { path = "../qsc_rca" } diff --git a/compiler/qsc/src/codegen.rs b/compiler/qsc/src/codegen.rs index 06f683ba9d..253b6f21ab 100644 --- a/compiler/qsc/src/codegen.rs +++ b/compiler/qsc/src/codegen.rs @@ -4,80 +4,155 @@ #[cfg(test)] mod tests; -use qsc_codegen::qir::fir_to_qir; -use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; -use qsc_frontend::{ - compile::{Dependencies, PackageStore, SourceMap}, - error::WithSource, -}; -use qsc_partial_eval::ProgramEntry; -use qsc_passes::{PackageType, PassContext}; +pub mod qsharp { + pub use qsc_codegen::qsharp::write_package_string; + pub use qsc_codegen::qsharp::write_stmt_string; +} -use crate::interpret::Error; +pub mod qir { + use qsc_codegen::qir::fir_to_qir; -pub fn get_qir( - sources: SourceMap, - language_features: LanguageFeatures, - capabilities: TargetCapabilityFlags, - mut package_store: PackageStore, - dependencies: &Dependencies, -) -> Result> { - if capabilities == TargetCapabilityFlags::all() { - return Err(vec![Error::UnsupportedRuntimeCapabilities]); - } + use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; + use qsc_frontend::{ + compile::{Dependencies, PackageStore, SourceMap}, + error::WithSource, + }; + use qsc_partial_eval::ProgramEntry; + use qsc_passes::{PackageType, PassContext}; - let (unit, errors) = crate::compile::compile( - &package_store, - dependencies, - sources, - PackageType::Exe, - capabilities, - language_features, - ); + use crate::interpret::Error; + pub fn get_qir_from_ast( + store: &mut PackageStore, + dependencies: &Dependencies, + ast_package: qsc_ast::ast::Package, + sources: SourceMap, + capabilities: TargetCapabilityFlags, + ) -> Result> { + if capabilities == TargetCapabilityFlags::all() { + return Err(vec![Error::UnsupportedRuntimeCapabilities]); + } - // Ensure it compiles before trying to add it to the store. - if !errors.is_empty() { - return Err(errors.iter().map(|e| Error::Compile(e.clone())).collect()); - } + let (unit, errors) = crate::compile::compile_ast( + store, + dependencies, + ast_package, + sources, + PackageType::Exe, + capabilities, + ); - let package_id = package_store.insert(unit); - let (fir_store, fir_package_id) = qsc_passes::lower_hir_to_fir(&package_store, package_id); - let package = fir_store.get(fir_package_id); - let entry = ProgramEntry { - exec_graph: package.entry_exec_graph.clone(), - expr: ( + // Ensure it compiles before trying to add it to the store. + if !errors.is_empty() { + return Err(errors.iter().map(|e| Error::Compile(e.clone())).collect()); + } + + let package_id = store.insert(unit); + let (fir_store, fir_package_id) = qsc_passes::lower_hir_to_fir(store, package_id); + let package = fir_store.get(fir_package_id); + let entry = ProgramEntry { + exec_graph: package.entry_exec_graph.clone(), + expr: ( + fir_package_id, + package + .entry + .expect("package must have an entry expression"), + ) + .into(), + }; + + let compute_properties = PassContext::run_fir_passes_on_fir( + &fir_store, fir_package_id, - package - .entry - .expect("package must have an entry expression"), + capabilities, ) - .into(), - }; + .map_err(|errors| { + let source_package = store.get(package_id).expect("package should be in store"); + errors + .iter() + .map(|e| Error::Pass(WithSource::from_map(&source_package.sources, e.clone()))) + .collect::>() + })?; - let compute_properties = - PassContext::run_fir_passes_on_fir(&fir_store, fir_package_id, capabilities).map_err( - |errors| { - let source_package = package_store - .get(package_id) - .expect("package should be in store"); - errors - .iter() - .map(|e| Error::Pass(WithSource::from_map(&source_package.sources, e.clone()))) - .collect::>() - }, - )?; + fir_to_qir(&fir_store, capabilities, Some(compute_properties), &entry).map_err(|e| { + let source_package_id = match e.span() { + Some(span) => span.package, + None => package_id, + }; + let source_package = store + .get(source_package_id) + .expect("package should be in store"); + vec![Error::PartialEvaluation(WithSource::from_map( + &source_package.sources, + e, + ))] + }) + } + pub fn get_qir( + sources: SourceMap, + language_features: LanguageFeatures, + capabilities: TargetCapabilityFlags, + mut package_store: PackageStore, + dependencies: &Dependencies, + ) -> Result> { + if capabilities == TargetCapabilityFlags::all() { + return Err(vec![Error::UnsupportedRuntimeCapabilities]); + } + + let (unit, errors) = crate::compile::compile( + &package_store, + dependencies, + sources, + PackageType::Exe, + capabilities, + language_features, + ); - fir_to_qir(&fir_store, capabilities, Some(compute_properties), &entry).map_err(|e| { - let source_package_id = match e.span() { - Some(span) => span.package, - None => package_id, + // Ensure it compiles before trying to add it to the store. + if !errors.is_empty() { + return Err(errors.iter().map(|e| Error::Compile(e.clone())).collect()); + } + + let package_id = package_store.insert(unit); + let (fir_store, fir_package_id) = qsc_passes::lower_hir_to_fir(&package_store, package_id); + let package = fir_store.get(fir_package_id); + let entry = ProgramEntry { + exec_graph: package.entry_exec_graph.clone(), + expr: ( + fir_package_id, + package + .entry + .expect("package must have an entry expression"), + ) + .into(), }; - let source_package = package_store - .get(source_package_id) - .expect("package should be in store"); - vec![Error::PartialEvaluation(WithSource::from_map( - &source_package.sources, - e, - ))] - }) + + let compute_properties = PassContext::run_fir_passes_on_fir( + &fir_store, + fir_package_id, + capabilities, + ) + .map_err(|errors| { + let source_package = package_store + .get(package_id) + .expect("package should be in store"); + errors + .iter() + .map(|e| Error::Pass(WithSource::from_map(&source_package.sources, e.clone()))) + .collect::>() + })?; + + fir_to_qir(&fir_store, capabilities, Some(compute_properties), &entry).map_err(|e| { + let source_package_id = match e.span() { + Some(span) => span.package, + None => package_id, + }; + let source_package = package_store + .get(source_package_id) + .expect("package should be in store"); + vec![Error::PartialEvaluation(WithSource::from_map( + &source_package.sources, + e, + ))] + }) + } } diff --git a/compiler/qsc/src/codegen/tests.rs b/compiler/qsc/src/codegen/tests.rs index 4dc3a85317..82ef545884 100644 --- a/compiler/qsc/src/codegen/tests.rs +++ b/compiler/qsc/src/codegen/tests.rs @@ -5,7 +5,7 @@ use expect_test::expect; use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; use qsc_frontend::compile::SourceMap; -use crate::codegen::get_qir; +use crate::codegen::qir::get_qir; #[test] fn code_with_errors_returns_errors() { @@ -64,7 +64,7 @@ mod base_profile { use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; use qsc_frontend::compile::SourceMap; - use crate::codegen::get_qir; + use crate::codegen::qir::get_qir; #[test] fn simple() { @@ -257,7 +257,7 @@ mod adaptive_profile { use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; use qsc_frontend::compile::SourceMap; - use crate::codegen::get_qir; + use crate::codegen::qir::get_qir; #[test] fn simple() { @@ -472,7 +472,7 @@ mod adaptive_ri_profile { use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; use qsc_frontend::compile::SourceMap; - use crate::codegen::get_qir; + use crate::codegen::qir::get_qir; #[test] fn simple() { diff --git a/compiler/qsc/src/incremental.rs b/compiler/qsc/src/incremental.rs index d891678417..91d39ca8bd 100644 --- a/compiler/qsc/src/incremental.rs +++ b/compiler/qsc/src/incremental.rs @@ -85,9 +85,18 @@ impl Compiler { source_package_id: PackageId, capabilities: TargetCapabilityFlags, language_features: LanguageFeatures, + dependencies: &Dependencies, ) -> Result { - let frontend = - qsc_frontend::incremental::Compiler::new(&store, &[], capabilities, language_features); + let mut dependencies = dependencies.iter().map(Clone::clone).collect::>(); + + dependencies.push((source_package_id, None)); + + let frontend = qsc_frontend::incremental::Compiler::new( + &store, + &dependencies[..], + capabilities, + language_features, + ); let store = store.open(); Ok(Self { diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index 2430ebb71e..cdbb937134 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -257,9 +257,16 @@ impl Interpreter { source_package_id: qsc_hir::hir::PackageId, capabilities: TargetCapabilityFlags, language_features: LanguageFeatures, + dependencies: &Dependencies, ) -> std::result::Result> { - let compiler = Compiler::from(store, source_package_id, capabilities, language_features) - .map_err(into_errors)?; + let compiler = Compiler::from( + store, + source_package_id, + capabilities, + language_features, + dependencies, + ) + .map_err(into_errors)?; let mut fir_store = fir::PackageStore::new(); for (id, unit) in compiler.package_store() { @@ -271,6 +278,26 @@ impl Interpreter { let source_package_id = compiler.source_package_id(); let package_id = compiler.package_id(); + let package = map_hir_package_to_fir(package_id); + if capabilities != TargetCapabilityFlags::all() { + let _ = PassContext::run_fir_passes_on_fir( + &fir_store, + map_hir_package_to_fir(source_package_id), + capabilities, + ) + .map_err(|caps_errors| { + let source_package = compiler + .package_store() + .get(source_package_id) + .expect("package should exist in the package store"); + + caps_errors + .into_iter() + .map(|error| Error::Pass(WithSource::from_map(&source_package.sources, error))) + .collect::>() + })?; + } + Ok(Self { compiler, lines: 0, @@ -282,7 +309,7 @@ impl Interpreter { sim: sim_circuit_backend(), quantum_seed: None, classical_seed: None, - package: map_hir_package_to_fir(package_id), + package, source_package: map_hir_package_to_fir(source_package_id), }) } @@ -320,10 +347,7 @@ impl Interpreter { /// Executes the entry expression until the end of execution. /// # Errors /// Returns a vector of errors if evaluating the entry point fails. - pub fn eval_entry( - &mut self, - receiver: &mut impl Receiver, - ) -> std::result::Result> { + pub fn eval_entry(&mut self, receiver: &mut impl Receiver) -> InterpretResult { let graph = self.get_entry_exec_graph()?; self.expr_graph = Some(graph.clone()); eval( @@ -344,7 +368,7 @@ impl Interpreter { &mut self, sim: &mut impl Backend>, receiver: &mut impl Receiver, - ) -> std::result::Result> { + ) -> InterpretResult { let graph = self.get_entry_exec_graph()?; self.expr_graph = Some(graph.clone()); if self.quantum_seed.is_some() { @@ -444,11 +468,7 @@ impl Interpreter { /// Runs the given entry expression on a new instance of the environment and simulator, /// but using the current compilation. - pub fn run( - &mut self, - receiver: &mut impl Receiver, - expr: Option<&str>, - ) -> std::result::Result> { + pub fn run(&mut self, receiver: &mut impl Receiver, expr: Option<&str>) -> InterpretResult { self.run_with_sim(&mut SparseSim::new(), receiver, expr) } @@ -560,6 +580,13 @@ impl Interpreter { Ok(circuit) } + /// Sets the entry expression for the interpreter. + pub fn set_entry_expr(&mut self, entry_expr: &str) -> std::result::Result<(), Vec> { + let (graph, _) = self.compile_entry_expr(entry_expr)?; + self.expr_graph = Some(graph); + Ok(()) + } + /// Runs the given entry expression on the given simulator with a new instance of the environment /// but using the current compilation. pub fn run_with_sim( @@ -567,7 +594,7 @@ impl Interpreter { sim: &mut impl Backend>, receiver: &mut impl Receiver, expr: Option<&str>, - ) -> std::result::Result> { + ) -> InterpretResult { let graph = if let Some(expr) = expr { let (graph, _) = self.compile_entry_expr(expr)?; self.expr_graph = Some(graph.clone()); @@ -580,7 +607,7 @@ impl Interpreter { sim.set_seed(self.quantum_seed); } - Ok(eval( + eval( self.package, self.classical_seed, graph, @@ -589,7 +616,7 @@ impl Interpreter { &mut Env::default(), sim, receiver, - )) + ) } fn run_with_sim_no_output( @@ -1071,7 +1098,8 @@ fn eval_error( vec![error::from_eval(error, package_store, stack_trace).into()] } -fn into_errors(errors: Vec) -> Vec { +#[must_use] +pub fn into_errors(errors: Vec) -> Vec { errors .into_iter() .map(|error| Error::Compile(error.into_with_source())) diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index b948b4bfb3..64eb45b3bb 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -4,7 +4,7 @@ #![allow(clippy::needless_raw_string_hashes)] mod given_interpreter { - use crate::interpret::{Error, InterpretResult, Interpreter}; + use crate::interpret::{InterpretResult, Interpreter}; use expect_test::Expect; use miette::Diagnostic; use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags}; @@ -22,18 +22,13 @@ mod given_interpreter { ) } - fn run( - interpreter: &mut Interpreter, - expr: &str, - ) -> (Result>, String) { + fn run(interpreter: &mut Interpreter, expr: &str) -> (InterpretResult, String) { let mut cursor = Cursor::new(Vec::::new()); let mut receiver = CursorReceiver::new(&mut cursor); (interpreter.run(&mut receiver, Some(expr)), receiver.dump()) } - fn entry( - interpreter: &mut Interpreter, - ) -> (Result>, String) { + fn entry(interpreter: &mut Interpreter) -> (InterpretResult, String) { let mut cursor = Cursor::new(Vec::::new()); let mut receiver = CursorReceiver::new(&mut cursor); (interpreter.eval_entry(&mut receiver), receiver.dump()) @@ -43,7 +38,7 @@ mod given_interpreter { interpreter: &mut Interpreter, fragments: &str, package: crate::ast::Package, - ) -> (Result>, String) { + ) -> (InterpretResult, String) { let mut cursor = Cursor::new(Vec::::new()); let mut receiver = CursorReceiver::new(&mut cursor); let result = interpreter.eval_ast_fragments(&mut receiver, fragments, package); @@ -1165,11 +1160,7 @@ mod given_interpreter { is_only_value(&result, &output, &Value::unit()); for _ in 0..4 { let (results, output) = run(&mut interpreter, "{use qs = Qubit[2]; Foo(qs)}"); - is_unit_with_output( - &results.expect("compilation should succeed"), - &output, - "STATE:\n|00⟩: 1+0i", - ); + is_unit_with_output(&results, &output, "STATE:\n|00⟩: 1+0i"); } } @@ -1195,11 +1186,7 @@ mod given_interpreter { let (result, output) = line(&mut interpreter, "operation Bar() : Int { 2 }"); is_only_value(&result, &output, &Value::unit()); let (result, output) = run(&mut interpreter, "{ Foo(); Bar() }"); - is_only_value( - &result.expect("compilation should succeed"), - &output, - &Value::Int(2), - ); + is_only_value(&result, &output, &Value::Int(2)); } #[test] @@ -1213,7 +1200,7 @@ mod given_interpreter { for _ in 0..1 { let (result, output) = run(&mut interpreter, "Foo()"); is_only_error( - &result.expect("compilation should succeed"), + &result, &output, &expect![[r#" runtime error: program failed: failed @@ -1233,11 +1220,7 @@ mod given_interpreter { is_only_value(&result, &output, &Value::unit()); for _ in 0..4 { let (result, output) = run(&mut interpreter, "Foo()"); - is_unit_with_output( - &result.expect("compilation should succeed"), - &output, - "hello!", - ); + is_unit_with_output(&result, &output, "hello!"); } } @@ -1288,7 +1271,7 @@ mod given_interpreter { } fn is_unit_with_output_eval_entry( - result: &Result>, + result: &InterpretResult, output: &str, expected_output: &str, ) { @@ -1745,7 +1728,7 @@ mod given_interpreter { None, )]; - let (unit, errors) = crate::compile::compile( + let (mut unit, errors) = crate::compile::compile( &store, &dependencies, sources, @@ -1753,15 +1736,21 @@ mod given_interpreter { capabilities, language_features, ); + unit.expose(); for e in &errors { eprintln!("{e:?}"); } assert!(errors.is_empty(), "compilation failed: {}", errors[0]); let package_id = store.insert(unit); - let mut interpreter = - Interpreter::from(store, package_id, capabilities, language_features) - .expect("interpreter should be created"); + let mut interpreter = Interpreter::from( + store, + package_id, + capabilities, + language_features, + &dependencies, + ) + .expect("interpreter should be created"); let (result, output) = entry(&mut interpreter); is_only_value( &result, diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index 5abac73206..65029ed79a 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -61,3 +61,11 @@ pub use qsc_doc_gen::{display, generate_docs}; pub mod circuit { pub use qsc_circuit::{operations::*, Circuit, Operation}; } + +pub mod parse { + pub use qsc_parse::top_level_nodes; +} + +pub mod partial_eval { + pub use qsc_partial_eval::Error; +} diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs index 7a264da1ba..8ffb9a6b46 100644 --- a/compiler/qsc_ast/src/ast.rs +++ b/compiler/qsc_ast/src/ast.rs @@ -1707,6 +1707,16 @@ pub enum Result { One, } +impl From for Result { + fn from(b: bool) -> Self { + if b { + Result::One + } else { + Result::Zero + } + } +} + /// A Pauli operator. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub enum Pauli { diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs index 3888cc2502..0c507bb3b9 100644 --- a/compiler/qsc_codegen/src/qsharp.rs +++ b/compiler/qsc_codegen/src/qsharp.rs @@ -8,7 +8,7 @@ mod spec_decls; mod tests; #[cfg(test)] -mod test_utils; +pub mod test_utils; use std::io::Write; use std::vec; @@ -61,6 +61,20 @@ pub fn write_package_string(package: &Package) -> String { format_str(&s) } +#[must_use] +pub fn write_stmt_string(stmt: &ast::Stmt) -> String { + let mut output = Vec::new(); + let mut gen = QSharpGen::new(&mut output); + gen.visit_stmt(stmt); + let s = match std::str::from_utf8(&output) { + Ok(v) => v.to_owned(), + Err(e) => format!("Invalid UTF-8 sequence: {e}"), + }; + + output.clear(); + format_str(&s) +} + struct QSharpGen { pub(crate) output: W, } diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs index 7b1a863c4b..d686e117a5 100644 --- a/compiler/qsc_frontend/src/compile.rs +++ b/compiler/qsc_frontend/src/compile.rs @@ -64,7 +64,7 @@ pub struct AstPackage { pub locals: Locals, } -#[derive(Debug, Default)] +#[derive(Clone, Debug, Default)] pub struct SourceMap { sources: Vec, /// The common prefix of the sources diff --git a/compiler/qsc_qasm3/Cargo.toml b/compiler/qsc_qasm3/Cargo.toml new file mode 100644 index 0000000000..79b4129f2b --- /dev/null +++ b/compiler/qsc_qasm3/Cargo.toml @@ -0,0 +1,35 @@ +[package] +name = "qsc_qasm3" +authors.workspace = true +homepage.workspace = true +repository.workspace = true +edition.workspace = true +license.workspace = true +version.workspace = true + +[dependencies] +bitflags = { workspace = true } +num-bigint = { workspace = true } +miette = { workspace = true } +qsc = { path = "../qsc" } +rustc-hash = { workspace = true } +thiserror = { workspace = true } + +oq3_source_file = "0.6.0" +oq3_syntax = "0.6.0" +oq3_parser = "0.6.0" +oq3_lexer = "0.6.0" +oq3_semantics = "0.6.0" + +[dev-dependencies] +difference = { workspace = true } +expect-test = { workspace = true } +indoc = { workspace = true } +miette = { workspace = true, features = ["fancy"] } +qsc_qasm3 = { path = ".", features = ["fs"] } + +[features] +fs = [] + +[lints] +workspace = true diff --git a/compiler/qsc_qasm3/README.md b/compiler/qsc_qasm3/README.md new file mode 100644 index 0000000000..482188c4b0 --- /dev/null +++ b/compiler/qsc_qasm3/README.md @@ -0,0 +1,38 @@ +# Q# QASM3 Compiler + +This crate implements a semantic transformation from OpenQASM 3 to Q#. At a high level it parses the OpenQASM program (and all includes) into an AST. Once this AST is parsed, it is compiled into Q#'s AST. + +Once the compiler gets to the AST phase, it no longer cares that it is processing Q#. At this point all input is indistinguishable from having been given Q# as input. This allows us to leverage capability analysis, runtime targeting, residual computation, partial evaluation, and code generation to the input. + +## Process Overview + +The OpenQASM code is parsed with their own lexer/parser and any errors are returned immediately before we get into further compilation. The OpenQASM parsing library hard-codes the file system into the types and parsing. Additionally, the library panics instead of surfacing `Result`. Because of this, there is a custom layer built to do the parsing so that the file system is abstracted with the `SourceResolver` trait and results with detailed errors are propagated instead of crashing. + +While it would be nice to use their semantic library to analyze and type check the code, they are missing many language features that are only available in the AST and the semantic library panics instead of pushing errors. + +With the source lexed and parsed, we can begin compilation. The program is compiled to the Q# AST. The OpenQASM ASG would be great to use for program as a validation pass once it is more developed, but for now we try to surface OpenQASM semantic/type errors as we encounter them. It is difficult to determine if a type error is from the AST compilation or from the source program as the implicit/explicit casting and type promotion rules are very complicated. + +As we process the AST we map the spans of the OpenQASM statements/expressions/literals to their corresponding Q# AST nodes. Additionally, we map the input source code (and it’s loaded externals) into a SourceMap with entry expressions. At this point we have enough information to hand off the rest of compilation to the Q# compiler. + +## Semantics +The two languages have many differences, and insofar as possible, the compilation preserves the source language semantics. + +- OpenQASM and Q# have different qubit and initialization semantics. + - Q# assumes that qubits are in the |0⟩ state when they are allocated. + - OpenQASM does not make this assumption and qubits are in an undefined state. +- Q# requires that qubits are reset to the |0⟩ at the end of excution. +- OpenQASM does not require this. +- Q# does no allow for variables to be uninitialized. All initialization is explicit. +- OpenQASM allows for implicit initialization. +- Q# does not allow for implicit casting or promotion of types. All conversions must be explicit. +- OpenQASM allows for implicit casting and promotion of types following C99 and custom rules. +- Q# does not have unsigned integers or an angle type. All integers are signed. + +QIR specific semantic constraints: +- OpenQASM ouput registers are declared with a fixed size and not all of the indexes may be populated with measurements. In QIR, `Result`s can only ever be acquired through measurement. So if all entries in an output register aren't measured into, a code generation error will occur. + +Semantic details +- Gates are implemented as lamba expressions capturing const variables from the global scope. + - There is an exception when using `@SimulatableIntrinsic`. Those are defined as full local `operation`s as they are not allowed to capture. + - We and change this in the future by copying `const` value decls into the `gate`/`function` scope, but this would require implementing a lot of inlining and partial evaluation which we already do in the compiler. +- OpenQASM `const` is modeled as Q# immutable bindings. This isn't fully correct as Q# can build `let` bindings with both mutable and immutable values, but as the translation is one way, we can do this mapping and know that any `const` value is assigned to an immutable `let` binding. Anything else, isn't guaranteed to be immutable. There are additional semantic checks as well to ensure that const declarations are not intialized to non-const values. diff --git a/compiler/qsc_qasm3/src/angle.rs b/compiler/qsc_qasm3/src/angle.rs new file mode 100644 index 0000000000..026084b640 --- /dev/null +++ b/compiler/qsc_qasm3/src/angle.rs @@ -0,0 +1,434 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use num_bigint::BigInt; + +use crate::oqasm_helpers::safe_u64_to_f64; +use std::convert::TryInto; +use std::f64::consts::PI; +use std::fmt; +use std::ops::{Add, AddAssign, Div, DivAssign, Mul, MulAssign, Neg, Sub, SubAssign}; + +/// A fixed-point angle type with a specified number of bits. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct Angle { + value: u64, + size: u32, +} + +impl Angle { + fn new(value: u64, size: u32) -> Self { + Angle { value, size } + } + + fn from_f64(val: f64, size: u32) -> Self { + #[allow(clippy::cast_precision_loss)] + let factor = (2.0 * PI) / (1u64 << size) as f64; + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + let value = (val / factor).round() as u64; + Angle { value, size } + } + + fn to_bitstring(self) -> String { + format!("{:0width$b}", self.value, width = self.size as usize) + } + + fn cast(&self, new_size: u32, truncate: bool) -> Self { + match new_size.cmp(&self.size) { + std::cmp::Ordering::Less => { + let value = if truncate { + let shift_amount = self.size - new_size; + self.value >> shift_amount + } else { + // Rounding + let shift_amount = self.size - new_size; + let half = 1u64 << (shift_amount - 1); + let mask = (1u64 << shift_amount) - 1; + let lower_bits = self.value & mask; + let upper_bits = self.value >> shift_amount; + + if lower_bits > half || (lower_bits == half && (upper_bits & 1) == 1) { + upper_bits + 1 + } else { + upper_bits + } + }; + Angle { + value, + size: new_size, + } + } + std::cmp::Ordering::Equal => { + // Same size, no change + *self + } + std::cmp::Ordering::Greater => { + // Padding with zeros + let new_value = self.value << (new_size - self.size); + Angle { + value: new_value, + size: new_size, + } + } + } + } +} + +impl Add for Angle { + type Output = Self; + + fn add(self, other: Self) -> Self { + assert_eq!(self.size, other.size, "Sizes must be the same"); + Angle { + value: (self.value + other.value) % (1u64 << self.size), + size: self.size, + } + } +} + +impl Sub for Angle { + type Output = Self; + + fn sub(self, other: Self) -> Self { + assert_eq!(self.size, other.size, "Sizes must be the same"); + Angle { + value: (self.value + (1u64 << self.size) - other.value) % (1u64 << self.size), + size: self.size, + } + } +} + +impl Mul for Angle { + type Output = Self; + + fn mul(self, factor: u64) -> Self { + Angle { + value: (self.value * factor) % (1u64 << self.size), + size: self.size, + } + } +} + +impl Mul for Angle { + type Output = Self; + + fn mul(self, factor: u128) -> Self { + let r = BigInt::from(self.value) * BigInt::from(factor); + let r = r % BigInt::from(1u128 << self.size); + Angle { + value: r.try_into().expect("Value is too large"), + size: self.size, + } + } +} + +impl Div for Angle { + type Output = Self; + + fn div(self, divisor: u64) -> Self { + Angle { + value: self.value / divisor, + size: self.size, + } + } +} + +impl Div for Angle { + type Output = u64; + + fn div(self, other: Self) -> u64 { + assert_eq!(self.size, other.size, "Sizes must be the same"); + self.value / other.value + } +} + +impl Neg for Angle { + type Output = Self; + + fn neg(self) -> Self { + Angle { + value: (1u64 << self.size) - self.value, + size: self.size, + } + } +} + +impl AddAssign for Angle { + fn add_assign(&mut self, other: Self) { + *self = *self + other; + } +} + +impl SubAssign for Angle { + fn sub_assign(&mut self, other: Self) { + *self = *self - other; + } +} + +impl MulAssign for Angle { + fn mul_assign(&mut self, factor: u64) { + *self = *self * factor; + } +} + +impl DivAssign for Angle { + fn div_assign(&mut self, divisor: u64) { + *self = *self / divisor; + } +} + +impl TryInto for Angle { + type Error = &'static str; + + fn try_into(self) -> Result { + if self.size > 64 { + return Err("Size exceeds 64 bits"); + } + let Some(denom) = safe_u64_to_f64(1u64 << self.size) else { + return Err("Denominator is too large"); + }; + let Some(value) = safe_u64_to_f64(self.value) else { + return Err("Value is too large"); + }; + let factor = (2.0 * PI) / denom; + Ok(value * factor) + } +} + +impl From for bool { + fn from(val: Angle) -> Self { + val.value != 0 + } +} + +impl fmt::Display for Angle { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", (*self).try_into().unwrap_or(f64::NAN)) + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::unwrap_used)] + #![allow(clippy::unreadable_literal)] + use super::*; + use std::f64::consts::PI; + + #[test] + fn test_angle() { + let angle1 = Angle::from_f64(PI, 4); + let angle2 = Angle::from_f64(PI / 2.0, 6); + let angle3 = Angle::from_f64(7.0 * (PI / 8.0), 8); + + assert_eq!(angle1.to_bitstring(), "1000"); + assert_eq!(angle2.to_bitstring(), "010000"); + assert_eq!(angle3.to_bitstring(), "01110000"); + } + + #[test] + fn test_angle_creation() { + let angle = Angle::from_f64(PI, 4); + assert_eq!(angle.value, 8); + assert_eq!(angle.size, 4); + } + + #[test] + fn test_angle_addition() { + let angle1 = Angle::from_f64(PI / 2.0, 4); + let angle2 = Angle::from_f64(PI / 2.0, 4); + let result = angle1 + angle2; + assert_eq!(result.value, 8); + let angle: f64 = result.try_into().unwrap(); + assert!((angle - PI).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_multiplication() { + let angle = Angle::from_f64(PI / 4.0, 4); + let result: Angle = angle * 2u64; + assert_eq!(result.value, 4); + let angle: f64 = result.try_into().unwrap(); + assert!((angle - PI / 2.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_multiplication_bigint() { + let angle = Angle::from_f64(PI / 4.0, 4); + let result: Angle = angle * 18446744073709551616u128; + assert_eq!(result.value, 0); + let angle: f64 = result.try_into().unwrap(); + assert!((angle - 0.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_multiplication_bigint2() { + let angle = Angle::from_f64(PI / 4.0, 4); + let result: Angle = angle * 9223372036854775806u128; + assert_eq!(result.value, 12); + let angle: f64 = result.try_into().unwrap(); + assert!((angle - ((3. * PI) / 2.)).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_division_int() { + let angle = Angle::from_f64(PI / 2.0, 4); + let result = angle / 2; + assert_eq!(result.value, 2); + let angle: f64 = result.try_into().unwrap(); + assert!((angle - PI / 4.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_division_by_angle() { + let angle1 = Angle::from_f64(PI, 4); + let angle2 = Angle::from_f64(PI / 4.0, 4); + let result = angle1 / angle2; + assert_eq!(result, 4); + } + + #[test] + fn test_angle_unary_negation() { + let angle = Angle::from_f64(PI / 4.0, 4); + let result = -angle; // "0010" + assert_eq!(result.value, 14); // 7*(pi/4) │ "1110" + } + + #[test] + fn test_angle_compound_addition() { + let mut angle1 = Angle::from_f64(PI / 2.0, 4); + let angle2 = Angle::from_f64(PI / 2.0, 4); + angle1 += angle2; + assert_eq!(angle1.value, 8); + let angle: f64 = angle1.try_into().unwrap(); + assert!((angle - PI).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_compound_subtraction() { + let mut angle1 = Angle::from_f64(PI, 4); + let angle2 = Angle::from_f64(PI / 2.0, 4); + angle1 -= angle2; + assert_eq!(angle1.value, 4); + let angle: f64 = angle1.try_into().unwrap(); + assert!((angle - PI / 2.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_compound_multiplication() { + let mut angle = Angle::from_f64(PI / 4.0, 4); + angle *= 2; + assert_eq!(angle.value, 4); + let angle: f64 = angle.try_into().unwrap(); + assert!((angle - PI / 2.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_compound_division() { + let mut angle = Angle::from_f64(PI / 2.0, 4); + angle /= 2; + assert_eq!(angle.value, 2); + let angle: f64 = angle.try_into().unwrap(); + assert!((angle - PI / 4.0).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_bitstring() { + let angle = Angle::from_f64(PI, 4); + assert_eq!(angle.to_bitstring(), "1000"); + } + + #[test] + fn test_angle_try_into_f64() { + let angle: Angle = Angle::from_f64(PI, 4); + let angle_f64: f64 = angle.try_into().unwrap(); + assert!((angle_f64 - PI).abs() < f64::EPSILON); + } + + #[test] + fn test_angle_display() { + let angle = Angle::from_f64(PI, 4); + assert_eq!(format!("{angle}"), format!("{PI}")); + } + + #[test] + fn from_f64_round_to_the_nearest_ties_to_even() { + let angle = Angle::from_f64(2.0 * PI * (127. / 512.), 8); + // 00111111 is equally close, but even rounds to 01000000 + assert_eq!(angle.to_bitstring(), "01000000"); + } + + #[test] + fn test_angle_into_bool() { + let angle = Angle { + value: 10, + size: 12, + }; + let result: bool = angle.into(); + assert!(result); + + let angle_zero = Angle { value: 0, size: 12 }; + let result_zero: bool = angle_zero.into(); + assert!(!result_zero); + } + + #[test] + fn test_angle_cast_round_padding() { + let angle = Angle { + value: 0b1010, + size: 4, + }; + let new_angle = angle.cast(8, false); + assert_eq!(new_angle.value, 0b10100000); + assert_eq!(new_angle.size, 8); + assert!( + (TryInto::::try_into(angle).unwrap() + - TryInto::::try_into(new_angle).unwrap()) + .abs() + < f64::EPSILON + ); + } + + #[test] + fn test_angle_cast_rounding() { + let angle = Angle { + value: 0b101011, + size: 6, + }; + let new_angle = angle.cast(4, false); + assert_eq!(new_angle.value, 0b1011); + assert_eq!(new_angle.size, 4); + } + + #[test] + fn test_angle_cast_rounding_ties_to_even() { + let angle = Angle { + value: 0b101010, + size: 6, + }; + let new_angle = angle.cast(4, false); + assert_eq!(new_angle.value, 0b1010); + assert_eq!(new_angle.size, 4); + } + #[test] + fn test_angle_cast_padding() { + let angle = Angle { + value: 0b1010, + size: 4, + }; + let new_angle = angle.cast(8, true); + assert_eq!(new_angle.value, 0b10100000); + assert_eq!(new_angle.size, 8); + } + + #[test] + fn test_angle_cast_truncation() { + let angle = Angle { + value: 0b101011, + size: 6, + }; + let new_angle = angle.cast(4, true); + assert_eq!(new_angle.value, 0b1010); + assert_eq!(new_angle.size, 4); + } +} diff --git a/compiler/qsc_qasm3/src/ast_builder.rs b/compiler/qsc_qasm3/src/ast_builder.rs new file mode 100644 index 0000000000..cf78c81b5c --- /dev/null +++ b/compiler/qsc_qasm3/src/ast_builder.rs @@ -0,0 +1,1562 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::rc::Rc; + +use num_bigint::BigInt; + +use qsc::{ + ast::{ + self, Attr, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Ident, Idents, + Item, Lit, Mutability, NodeId, Pat, PatKind, Path, QubitInit, QubitInitKind, QubitSource, + Stmt, StmtKind, TopLevelNode, Ty, TyKind, + }, + Span, +}; + +use crate::{ + runtime::RuntimeFunctions, + types::{ArrayDimensions, Complex}, +}; + +pub(crate) fn build_unmanaged_qubit_alloc(name: S, stmt_span: Span, name_span: Span) -> Stmt +where + S: AsRef, +{ + let alloc_ident = Ident { + name: Rc::from("__quantum__rt__qubit_allocate"), + ..Default::default() + }; + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments: Some(Idents(Box::new([ + Ident { + name: Rc::from("QIR"), + ..Default::default() + }, + Ident { + name: Rc::from("Runtime"), + ..Default::default() + }, + ]))), + name: Box::new(alloc_ident), + ..Default::default() + }))), + ..Default::default() + }; + let call_expr = Expr { + span: stmt_span, + kind: Box::new(ExprKind::Call( + Box::new(path_expr), + Box::new(create_unit_expr(Span::default())), + )), + ..Default::default() + }; + let rhs = call_expr; + + Stmt { + span: stmt_span, + kind: Box::new(StmtKind::Local( + Mutability::Immutable, + Box::new(Pat { + kind: Box::new(PatKind::Bind( + Box::new(Ident { + span: name_span, + name: Rc::from(name.as_ref()), + ..Default::default() + }), + None, + )), + ..Default::default() + }), + Box::new(rhs), + )), + ..Default::default() + } +} + +pub(crate) fn build_unmanaged_qubit_alloc_array( + name: S, + size: u32, + stmt_span: Span, + name_span: Span, + designator_span: Span, +) -> Stmt +where + S: AsRef, +{ + let alloc_ident = Ident { + name: Rc::from("AllocateQubitArray"), + ..Default::default() + }; + + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments: Some(Idents(Box::new([ + Ident { + name: Rc::from("QIR"), + ..Default::default() + }, + Ident { + name: Rc::from("Runtime"), + ..Default::default() + }, + ]))), + name: Box::new(alloc_ident), + ..Default::default() + }))), + ..Default::default() + }; + let call_expr = Expr { + id: NodeId::default(), + span: stmt_span, + kind: Box::new(ExprKind::Call( + Box::new(path_expr), + Box::new(Expr { + kind: Box::new(ExprKind::Paren(Box::new(Expr { + id: NodeId::default(), + span: designator_span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Int(i64::from(size))))), + }))), + ..Default::default() + }), + )), + }; + + Stmt { + id: NodeId::default(), + span: stmt_span, + kind: Box::new(StmtKind::Local( + Mutability::Immutable, + Box::new(Pat { + kind: Box::new(PatKind::Bind( + Box::new(Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }), + None, + )), + ..Default::default() + }), + Box::new(call_expr), + )), + } +} + +pub(crate) fn build_managed_qubit_alloc(name: S, stmt_span: Span, name_span: Span) -> Stmt +where + S: AsRef, +{ + let qubit_init = QubitInitKind::Single; + + let qubit = QubitInit { + id: NodeId::default(), + span: stmt_span, + kind: Box::new(qubit_init), + }; + + let ident: Ident = Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }; + let qubit_kind = StmtKind::Qubit( + QubitSource::Fresh, + Box::new(Pat { + kind: Box::new(PatKind::Bind(Box::new(ident), None)), + ..Default::default() + }), + Box::new(qubit), + None, + ); + Stmt { + kind: Box::new(qubit_kind), + ..Default::default() + } +} + +pub(crate) fn managed_qubit_alloc_array( + name: S, + size: u32, + stmt_span: Span, + name_span: Span, + designator_span: Span, +) -> Stmt +where + S: AsRef, +{ + let qubit_init = QubitInitKind::Array(Box::new(Expr { + span: designator_span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Int(i64::from(size))))), + ..Default::default() + })); + + let qubit = QubitInit { + span: name_span, + kind: Box::new(qubit_init), + ..Default::default() + }; + + let ident: Ident = Ident { + span: name_span, + name: Rc::from(name.as_ref()), + ..Default::default() + }; + let qubit_kind = StmtKind::Qubit( + QubitSource::Fresh, + Box::new(Pat { + kind: Box::new(PatKind::Bind(Box::new(ident), None)), + ..Default::default() + }), + Box::new(qubit), + None, + ); + Stmt { + span: stmt_span, + kind: Box::new(qubit_kind), + ..Default::default() + } +} + +pub(crate) fn build_lit_result_expr(value: qsc::ast::Result, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Result(value)))), + } +} + +pub(crate) fn build_lit_result_array_expr_from_bitstring>( + bitstring: S, + span: Span, +) -> Expr { + let values = bitstring + .as_ref() + .chars() + .filter_map(|c| { + if c == '0' { + Some(ast::Result::Zero) + } else if c == '1' { + Some(ast::Result::One) + } else { + None + } + }) + .collect(); + build_lit_result_array_expr(values, span) +} + +pub(crate) fn build_lit_result_array_expr(values: Vec, span: Span) -> Expr { + let exprs: Vec<_> = values + .into_iter() + .map(|v| build_lit_result_expr(v, Span::default())) + .collect(); + build_expr_array_expr(exprs, span) +} + +pub(crate) fn build_expr_array_expr(values: Vec, span: Span) -> Expr { + let exprs: Vec<_> = values.into_iter().map(Box::new).collect(); + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Array(exprs.into_boxed_slice())), + } +} + +pub(crate) fn build_default_result_array_expr(len: usize, span: Span) -> Expr { + let exprs: Vec<_> = (0..len) + .map(|_| Box::new(build_lit_result_expr(ast::Result::Zero, Span::default()))) + .collect(); + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Array(exprs.into_boxed_slice())), + } +} + +pub(crate) fn build_lit_bigint_expr(value: BigInt, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::BigInt(Box::new(value))))), + } +} + +pub(crate) fn build_lit_bool_expr(value: bool, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Bool(value)))), + } +} + +pub(crate) fn build_lit_int_expr(value: i64, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Int(value)))), + } +} + +pub(crate) fn build_lit_complex_expr(value: Complex, span: Span) -> Expr { + let real = build_lit_double_expr(value.real, Span::default()); + let img = build_lit_double_expr(value.imaginary, Span::default()); + build_math_call_from_exprs("Complex", vec![real, img], span) +} + +pub(crate) fn build_complex_from_expr(expr: Expr) -> Expr { + let img = build_lit_double_expr(0.0, Span::default()); + let span = expr.span; + build_math_call_from_exprs("Complex", vec![expr, img], span) +} + +pub(crate) fn build_binary_expr( + is_assignment: bool, + qsop: ast::BinOp, + lhs: ast::Expr, + rhs: ast::Expr, + span: Span, +) -> ast::Expr { + let expr_kind = if is_assignment { + ast::ExprKind::AssignOp(qsop, Box::new(lhs), Box::new(rhs)) + } else { + ast::ExprKind::BinOp(qsop, Box::new(lhs), Box::new(rhs)) + }; + ast::Expr { + id: NodeId::default(), + span, + kind: Box::new(expr_kind), + } +} + +pub(crate) fn is_complex_binop_supported(op: qsc::ast::BinOp) -> bool { + matches!( + op, + ast::BinOp::Add | ast::BinOp::Sub | ast::BinOp::Mul | ast::BinOp::Div | ast::BinOp::Exp + ) +} + +pub(crate) fn build_complex_binary_expr( + is_assignment: bool, + qsop: ast::BinOp, + lhs: ast::Expr, + rhs: ast::Expr, + span: Span, +) -> ast::Expr { + let name = match qsop { + ast::BinOp::Add => "PlusC", + ast::BinOp::Sub => "MinusC", + ast::BinOp::Mul => "TimesC", + ast::BinOp::Div => "DividedByC", + ast::BinOp::Exp => "PowC", + _ => unreachable!("Unsupported complex binary operation"), + }; + + if is_assignment { + unreachable!("Unsupported complex binary operation"); + } + build_math_call_from_exprs(name, vec![lhs, rhs], span) +} + +pub(crate) fn build_math_call_from_exprs(name: &str, exprs: Vec, span: Span) -> Expr { + let alloc_ident = Ident { + name: Rc::from(name), + ..Default::default() + }; + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments: Some(Idents(Box::new([ + Ident { + name: Rc::from("Microsoft"), + ..Default::default() + }, + Ident { + name: Rc::from("Quantum"), + ..Default::default() + }, + Ident { + name: Rc::from("Math"), + ..Default::default() + }, + ]))), + name: Box::new(alloc_ident), + ..Default::default() + }))), + ..Default::default() + }; + let exprs: Vec<_> = exprs.into_iter().map(Box::new).collect(); + let kind = if exprs.is_empty() { + ExprKind::Tuple(Box::new([])) + } else if exprs.len() == 1 { + ExprKind::Paren(exprs[0].clone()) + } else { + ExprKind::Tuple(exprs.into_boxed_slice()) + }; + let call = ExprKind::Call( + Box::new(path_expr), + Box::new(Expr { + kind: Box::new(kind), + ..Default::default() + }), + ); + Expr { + id: NodeId::default(), + span, + kind: Box::new(call), + } +} + +pub(crate) fn build_path_ident_expr>( + name: S, + name_span: Span, + expr_span: Span, +) -> ast::Expr { + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }; + let path = ast::Path { + id: NodeId::default(), + span: Span::default(), + segments: None, + name: Box::new(ident), + }; + let path_kind = ast::ExprKind::Path(Box::new(path)); + ast::Expr { + id: NodeId::default(), + span: expr_span, + kind: Box::new(path_kind), + } +} + +pub(crate) fn build_indexed_assignment_statement>( + name_span: Span, + string_name: S, + index_expr: ast::Expr, + rhs: Expr, + stmt_span: Span, +) -> ast::Stmt { + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(string_name.as_ref()), + }; + + let lhs = ast::Expr { + id: NodeId::default(), + span: name_span, + kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + id: NodeId::default(), + span: name_span, + segments: None, + name: Box::new(ident.clone()), + }))), + }; + + let assign_up = ast::StmtKind::Semi(Box::new(ast::Expr { + id: NodeId::default(), + span: Span::default(), + kind: Box::new(ast::ExprKind::AssignUpdate( + Box::new(lhs), + Box::new(index_expr), + Box::new(rhs), + )), + })); + ast::Stmt { + id: NodeId::default(), + span: stmt_span, + kind: Box::new(assign_up), + } +} + +pub(crate) fn build_assignment_statement>( + name_span: Span, + name: S, + rhs: Expr, + assignment_span: Span, +) -> ast::Stmt { + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }; + let path = ast::Path { + id: NodeId::default(), + span: name_span, + name: Box::new(ident), + segments: None, + }; + let lhs = ast::Expr { + id: NodeId::default(), + span: name_span, + kind: Box::new(ast::ExprKind::Path(Box::new(path))), + }; + let expr_kind = ast::ExprKind::Assign(Box::new(lhs), Box::new(rhs)); + let expr = ast::Expr { + id: NodeId::default(), + span: assignment_span, + kind: Box::new(expr_kind), + }; + let semi = ast::StmtKind::Semi(Box::new(expr)); + ast::Stmt { + id: NodeId::default(), + span: assignment_span, + kind: Box::new(semi), + } +} + +pub(crate) fn build_convert_call_expr(expr: Expr, name: &str) -> Expr { + let span = expr.span; + let cast_ident = Ident { + name: Rc::from(name), + ..Default::default() + }; + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments: Some(Idents(Box::new([ + Ident { + name: Rc::from("Microsoft"), + ..Default::default() + }, + Ident { + name: Rc::from("Quantum"), + ..Default::default() + }, + Ident { + name: Rc::from("Convert"), + ..Default::default() + }, + ]))), + name: Box::new(cast_ident), + ..Default::default() + }))), + ..Default::default() + }; + let call = ExprKind::Call( + Box::new(path_expr), + Box::new(Expr { + kind: Box::new(ExprKind::Paren(Box::new(expr))), + ..Default::default() + }), + ); + + Expr { + id: NodeId::default(), + span, + kind: Box::new(call), + } +} + +pub(crate) fn build_array_reverse_expr(expr: Expr) -> Expr { + let span = expr.span; + let cast_ident = Ident { + name: Rc::from("Reversed"), + ..Default::default() + }; + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments: Some(Idents(Box::new([ + Ident { + name: Rc::from("Microsoft"), + ..Default::default() + }, + Ident { + name: Rc::from("Quantum"), + ..Default::default() + }, + Ident { + name: Rc::from("Arrays"), + ..Default::default() + }, + ]))), + name: Box::new(cast_ident), + ..Default::default() + }))), + ..Default::default() + }; + let call = ExprKind::Call( + Box::new(path_expr), + Box::new(Expr { + kind: Box::new(ExprKind::Paren(Box::new(expr))), + ..Default::default() + }), + ); + + Expr { + id: NodeId::default(), + span, + kind: Box::new(call), + } +} + +#[allow(clippy::similar_names)] +pub(crate) fn build_range_expr(start: Expr, stop: Expr, step: Option, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Range( + Some(Box::new(start)), + step.map(Box::new), + Some(Box::new(stop)), + )), + } +} + +pub(crate) fn build_if_expr_then_block_else_expr( + cond: Expr, + then_expr: Block, + else_expr: Option, + span: Span, +) -> Expr { + let else_expr = else_expr.map(Box::new); + let if_kind = ExprKind::If(Box::new(cond), Box::new(then_expr), else_expr); + + ast::Expr { + id: NodeId::default(), + span, + kind: Box::new(if_kind), + } +} + +pub(crate) fn build_if_expr_then_expr_else_expr( + cond: Expr, + then_expr: Expr, + else_expr: Expr, + span: Span, +) -> Expr { + let if_kind = ExprKind::If( + Box::new(cond), + Box::new(build_expr_wrapped_block_expr(then_expr)), + Some(Box::new(build_wrapped_block_expr( + build_expr_wrapped_block_expr(else_expr), + ))), + ); + + ast::Expr { + id: NodeId::default(), + span, + kind: Box::new(if_kind), + } +} + +pub(crate) fn build_if_expr_then_block_else_block( + cond: Expr, + then_block: Block, + else_block: Block, + span: Span, +) -> Expr { + let if_kind = ExprKind::If( + Box::new(cond), + Box::new(then_block), + Some(Box::new(build_wrapped_block_expr(else_block))), + ); + + ast::Expr { + id: NodeId::default(), + span, + kind: Box::new(if_kind), + } +} + +pub(crate) fn build_if_expr_then_block(cond: Expr, then_block: Block, span: Span) -> Expr { + let if_kind = ExprKind::If(Box::new(cond), Box::new(then_block), None); + + ast::Expr { + id: NodeId::default(), + span, + kind: Box::new(if_kind), + } +} + +pub(crate) fn build_cast_call_two_params( + function: RuntimeFunctions, + fst: ast::Expr, + snd: ast::Expr, + name_span: Span, + operand_span: Span, +) -> ast::Expr { + let name = match function { + RuntimeFunctions::IntAsResultArrayBE => "__IntAsResultArrayBE__", + _ => panic!("Unsupported cast function"), + }; + + build_global_call_with_two_params(name, fst, snd, name_span, operand_span) +} + +pub(crate) fn build_cast_call( + function: RuntimeFunctions, + expr: ast::Expr, + name_span: Span, + operand_span: Span, +) -> ast::Expr { + let name = match function { + RuntimeFunctions::BoolAsResult => "__BoolAsResult__", + RuntimeFunctions::BoolAsInt => "__BoolAsInt__", + RuntimeFunctions::BoolAsBigInt => "__BoolAsBigInt__", + RuntimeFunctions::BoolAsDouble => "__BoolAsDouble__", + RuntimeFunctions::ResultAsBool => "__ResultAsBool__", + RuntimeFunctions::ResultAsInt => "__ResultAsInt__", + RuntimeFunctions::ResultAsBigInt => "__ResultAsBigInt__", + RuntimeFunctions::ResultArrayAsIntBE => "__ResultArrayAsIntBE__", + _ => panic!("Unsupported cast function"), + }; + build_global_call_with_one_param(name, expr, name_span, operand_span) +} + +pub(crate) fn build_measure_call( + expr: ast::Expr, + name_span: Span, + operand_span: Span, +) -> ast::Expr { + build_global_call_with_one_param("M", expr, name_span, operand_span) +} + +pub(crate) fn build_reset_call(expr: ast::Expr, name_span: Span, operand_span: Span) -> ast::Expr { + build_global_call_with_one_param("Reset", expr, name_span, operand_span) +} + +pub(crate) fn build_global_call_with_one_param>( + name: S, + expr: ast::Expr, + name_span: Span, + operand_span: Span, +) -> ast::Expr { + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }; + let callee_expr = ast::Expr { + id: NodeId::default(), + span: name_span, + kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + id: NodeId::default(), + span: Span::default(), + segments: None, + name: Box::new(ident), + }))), + }; + + let param_expr_kind = ast::ExprKind::Paren(Box::new(expr)); + + let param_expr = ast::Expr { + kind: Box::new(param_expr_kind), + span: operand_span, + ..Default::default() + }; + let call_kind = ast::ExprKind::Call(Box::new(callee_expr), Box::new(param_expr)); + ast::Expr { + kind: Box::new(call_kind), + ..Default::default() + } +} + +pub(crate) fn build_global_call_with_two_params>( + name: S, + fst: ast::Expr, + snd: ast::Expr, + name_span: Span, + operand_span: Span, +) -> ast::Expr { + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(name.as_ref()), + }; + let callee_expr = ast::Expr { + id: NodeId::default(), + span: name_span, + kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + id: NodeId::default(), + span: Span::default(), + segments: None, + name: Box::new(ident), + }))), + }; + + let param_expr_kind = ast::ExprKind::Tuple(Box::new([Box::new(fst), Box::new(snd)])); + + let param_expr = ast::Expr { + kind: Box::new(param_expr_kind), + span: operand_span, + ..Default::default() + }; + let call_kind = ast::ExprKind::Call(Box::new(callee_expr), Box::new(param_expr)); + ast::Expr { + kind: Box::new(call_kind), + ..Default::default() + } +} + +pub(crate) fn build_gate_call_with_params_and_callee( + param_expr: ast::Expr, + callee_expr: Expr, + expr_span: Span, +) -> ast::Expr { + let call_kind = ast::ExprKind::Call(Box::new(callee_expr), Box::new(param_expr)); + ast::Expr { + kind: Box::new(call_kind), + span: expr_span, + ..Default::default() + } +} + +pub(crate) fn build_gate_call_expr( + args: Vec, + name_span: Span, + gate_name: &str, + call_span: Span, + expr_span: Span, +) -> ast::Expr { + let param_expr = build_gate_call_param_expr(args, 0); + + let ident = ast::Ident { + id: NodeId::default(), + span: name_span, + name: Rc::from(gate_name), + }; + let callee_expr = ast::Expr { + id: NodeId::default(), + span: call_span, + kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + id: NodeId::default(), + span: Span::default(), + segments: None, + name: Box::new(ident), + }))), + }; + let call_kind = ast::ExprKind::Call(Box::new(callee_expr), Box::new(param_expr)); + ast::Expr { + kind: Box::new(call_kind), + span: expr_span, + ..Default::default() + } +} + +pub fn build_gate_call_param_expr(args: Vec, remaining: usize) -> Expr { + if args.len() == 1 && remaining > 0 { + return args[0].clone(); + } + let param_expr_kind = if args.len() == 1 { + ast::ExprKind::Paren(Box::new(args[0].clone())) + } else { + let args: Vec<_> = args.into_iter().map(Box::new).collect(); + ast::ExprKind::Tuple(args.into_boxed_slice()) + }; + ast::Expr { + kind: Box::new(param_expr_kind), + ..Default::default() + } +} + +pub(crate) fn build_math_call_no_params(name: &str, span: Span) -> Expr { + build_call_no_params(name, &["Microsoft", "Quantum", "Math"], span) +} + +pub(crate) fn build_call_no_params(name: &str, idents: &[&str], span: Span) -> Expr { + let idents = idents + .iter() + .map(|name| Ident { + name: Rc::from(*name), + ..Default::default() + }) + .collect::>() + .into_boxed_slice(); + let segments = if idents.is_empty() { + None + } else { + Some(Idents(idents)) + }; + let fn_name = Ident { + name: Rc::from(name), + ..Default::default() + }; + let path_expr = Expr { + kind: Box::new(ExprKind::Path(Box::new(Path { + segments, + name: Box::new(fn_name), + ..Default::default() + }))), + ..Default::default() + }; + let call = ExprKind::Call( + Box::new(path_expr), + Box::new(create_unit_expr(Span::default())), + ); + + Expr { + id: NodeId::default(), + span, + kind: Box::new(call), + } +} + +pub(crate) fn build_lit_double_expr(value: f64, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Double(value)))), + } +} + +pub(crate) fn build_lit_string_expr(value: f64, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Lit(Box::new(Lit::Double(value)))), + } +} + +pub(crate) fn build_stmt_semi_from_expr(expr: Expr) -> Stmt { + Stmt { + id: NodeId::default(), + span: expr.span, + kind: Box::new(StmtKind::Semi(Box::new(expr))), + } +} + +pub(crate) fn build_stmt_semi_from_block(block: Block) -> Stmt { + let expr = build_wrapped_block_expr(block); + Stmt { + id: NodeId::default(), + span: expr.span, + kind: Box::new(StmtKind::Semi(Box::new(expr))), + } +} + +pub(crate) fn build_wrapped_block_expr(block: Block) -> Expr { + Expr { + id: NodeId::default(), + span: block.span, + kind: Box::new(ast::ExprKind::Block(Box::new(block))), + } +} +pub(crate) fn build_stmt_wrapped_block_expr(stmt: Stmt) -> Block { + Block { + id: NodeId::default(), + span: stmt.span, + stmts: Box::new([Box::new(stmt)]), + } +} + +pub(crate) fn build_expr_wrapped_block_expr(expr: Expr) -> Block { + Block { + id: NodeId::default(), + span: expr.span, + stmts: Box::new([Box::new(Stmt { + span: expr.span, + kind: Box::new(StmtKind::Expr(Box::new(expr))), + ..Default::default() + })]), + } +} + +pub(crate) fn build_classical_decl( + name: S, + is_const: bool, + ty_span: Span, + decl_span: Span, + name_span: Span, + ty: &crate::types::Type, + expr: Expr, +) -> Stmt +where + S: AsRef, +{ + const USE_IMPLICIT_TYPE_DEF: bool = true; + + let ident = Ident { + id: NodeId::default(), + span: name_span, + name: name.as_ref().into(), + }; + + let result_ty_ident = Ident { + name: ty.to_string().into(), + ..Default::default() + }; + let result_ty_path = ast::Path { + name: Box::new(result_ty_ident), + ..Default::default() + }; + let result_ty_kind = ast::TyKind::Path(Box::new(result_ty_path)); + + let tydef = if USE_IMPLICIT_TYPE_DEF { + None + } else { + Some(Box::new(ast::Ty { + id: NodeId::default(), + span: ty_span, + kind: Box::new(result_ty_kind), + })) + }; + + let pat = Pat { + kind: Box::new(PatKind::Bind(Box::new(ident), tydef)), + ..Default::default() + }; + let mutability = if is_const { + Mutability::Immutable + } else { + Mutability::Mutable + }; + let local = StmtKind::Local(mutability, Box::new(pat), Box::new(expr)); + Stmt { + id: NodeId::default(), + span: decl_span, + kind: Box::new(local), + } +} + +pub(crate) fn build_tuple_expr(output_exprs: Vec) -> Expr { + let boxed_exprs = output_exprs.into_iter().map(Box::new).collect::>(); + let tuple_expr_kind = ExprKind::Tuple(boxed_exprs.into_boxed_slice()); + Expr { + kind: Box::new(tuple_expr_kind), + ..Default::default() + } +} + +pub(crate) fn build_implicit_return_stmt(output_expr: Expr) -> Stmt { + Stmt { + kind: Box::new(StmtKind::Expr(Box::new(output_expr))), + ..Default::default() + } +} + +pub(crate) fn build_explicit_return_stmt(output_expr: Expr) -> Stmt { + let span = output_expr.span; + Stmt { + kind: Box::new(StmtKind::Semi(Box::new(Expr { + kind: Box::new(ExprKind::Return(Box::new(output_expr))), + span, + ..Default::default() + }))), + span, + ..Default::default() + } +} + +pub(crate) fn build_path_ident_ty>(name: S) -> Ty { + let ident = ast::Ident { + name: Rc::from(name.as_ref()), + ..Default::default() + }; + let path = ast::Path { + name: Box::new(ident), + ..Default::default() + }; + let kind = TyKind::Path(Box::new(path)); + Ty { + kind: Box::new(kind), + ..Default::default() + } +} + +pub(crate) fn build_complex_ty_ident() -> Ty { + let ident = ast::Ident { + name: Rc::from("Complex"), + ..Default::default() + }; + let path = ast::Path { + name: Box::new(ident), + segments: Some(ast::Idents(Box::new([ + ast::Ident { + name: Rc::from("Microsoft"), + ..Default::default() + }, + ast::Ident { + name: Rc::from("Quantum"), + ..Default::default() + }, + ast::Ident { + name: Rc::from("Math"), + ..Default::default() + }, + ]))), + ..Default::default() + }; + let kind = TyKind::Path(Box::new(path)); + Ty { + kind: Box::new(kind), + ..Default::default() + } +} + +pub(crate) fn build_top_level_ns_with_item>( + whole_span: Span, + ns: S, + entry: ast::Item, +) -> TopLevelNode { + TopLevelNode::Namespace(qsc::ast::Namespace { + id: NodeId::default(), + span: whole_span, + name: qsc::ast::Idents(Box::new([Ident { + name: Rc::from(ns.as_ref()), + span: Span::default(), + id: NodeId::default(), + }])), + items: Box::new([Box::new(entry)]), + doc: "".into(), + }) +} + +pub(crate) fn build_operation_with_stmts>( + name: S, + input_pats: Vec, + output_ty: Ty, + stmts: Vec, + whole_span: Span, +) -> ast::Item { + let mut attrs = vec![]; + // If there are no input parameters, add an attribute to mark this + // as an entry point. We will get a Q# compilation error if we + // attribute an operation with EntryPoint and it has input parameters. + if input_pats.is_empty() { + attrs.push(Box::new(qsc::ast::Attr { + id: NodeId::default(), + span: Span::default(), + name: Box::new(qsc::ast::Ident { + name: Rc::from("EntryPoint"), + ..Default::default() + }), + arg: Box::new(create_unit_expr(Span::default())), + })); + } + let input_pats = input_pats.into_iter().map(Box::new).collect::>(); + let input = match input_pats.len() { + 0 => Box::new(Pat { + kind: Box::new(qsc::ast::PatKind::Tuple(input_pats.into_boxed_slice())), + ..Default::default() + }), + 1 => Box::new(Pat { + kind: Box::new(qsc::ast::PatKind::Paren(input_pats[0].clone())), + ..Default::default() + }), + _ => Box::new(qsc::ast::Pat { + kind: Box::new(qsc::ast::PatKind::Tuple(input_pats.into_boxed_slice())), + ..Default::default() + }), + }; + + let stmts = stmts + .into_iter() + .map(Box::new) + .collect::>() + .into_boxed_slice(); + qsc::ast::Item { + id: NodeId::default(), + span: whole_span, + doc: "".into(), + attrs: attrs.into_boxed_slice(), + kind: Box::new(qsc::ast::ItemKind::Callable(Box::new( + qsc::ast::CallableDecl { + id: NodeId::default(), + span: whole_span, + kind: qsc::ast::CallableKind::Operation, + name: Box::new(qsc::ast::Ident { + name: Rc::from(name.as_ref()), + ..Default::default() + }), + generics: Box::new([]), + input, + output: Box::new(output_ty), + functors: None, + body: Box::new(qsc::ast::CallableBody::Block(Box::new(qsc::ast::Block { + id: NodeId::default(), + span: whole_span, + stmts, + }))), + }, + ))), + } +} + +pub(crate) fn build_arg_pat(name: String, span: Span, ty: Ty) -> Pat { + qsc::ast::Pat { + kind: Box::new(qsc::ast::PatKind::Bind( + Box::new(qsc::ast::Ident { + name: Rc::from(name), + span, + ..Default::default() + }), + Some(Box::new(ty)), + )), + span, + ..Default::default() + } +} + +pub(crate) fn build_unary_op_expr(op: ast::UnOp, expr: ast::Expr, prefix_span: Span) -> ast::Expr { + ast::Expr { + span: prefix_span, + kind: Box::new(ast::ExprKind::UnOp(op, Box::new(expr))), + ..Default::default() + } +} + +pub(crate) fn map_qsharp_type_to_ast_ty(output_ty: &crate::types::Type) -> Ty { + match output_ty { + crate::types::Type::Result(_) => build_path_ident_ty("Result"), + crate::types::Type::Qubit => build_path_ident_ty("Qubit"), + crate::types::Type::BigInt(_) => build_path_ident_ty("BigInt"), + crate::types::Type::Int(_) => build_path_ident_ty("Int"), + crate::types::Type::Double(_) => build_path_ident_ty("Double"), + crate::types::Type::Complex(_) => build_complex_ty_ident(), + crate::types::Type::Bool(_) => build_path_ident_ty("Bool"), + crate::types::Type::ResultArray(dims, _) => build_array_type_name("Result", dims), + crate::types::Type::QubitArray(dims) => build_array_type_name("Qubit", dims), + crate::types::Type::BigIntArray(dims, _) => build_array_type_name("BigInt", dims), + crate::types::Type::IntArray(dims, _) => build_array_type_name("Int", dims), + crate::types::Type::DoubleArray(dims) => build_array_type_name("Double", dims), + crate::types::Type::BoolArray(dims, _) => build_array_type_name("Bool", dims), + crate::types::Type::Callable(_, _, _) => todo!(), + crate::types::Type::Range => build_path_ident_ty("Range"), + crate::types::Type::Tuple(tys) => { + if tys.is_empty() { + build_path_ident_ty("Unit") + } else { + let t = tys + .iter() + .map(map_qsharp_type_to_ast_ty) + .collect::>(); + + let kind = TyKind::Tuple(t.into_boxed_slice()); + Ty { + kind: Box::new(kind), + ..Default::default() + } + } + } + crate::types::Type::TupleArray(dims, tys) => { + assert!(!tys.is_empty()); + let ty = map_qsharp_type_to_ast_ty(&crate::types::Type::Tuple(tys.clone())); + wrap_array_ty_by_dims(dims, ty) + } + } +} + +fn wrap_array_ty_by_dims(dims: &ArrayDimensions, ty: Ty) -> Ty { + match dims { + ArrayDimensions::One(..) => wrap_ty_in_array(ty), + ArrayDimensions::Two(..) => wrap_ty_in_array(wrap_ty_in_array(ty)), + ArrayDimensions::Three(..) => wrap_ty_in_array(wrap_ty_in_array(wrap_ty_in_array(ty))), + } +} + +fn build_array_type_name>(name: S, dims: &ArrayDimensions) -> Ty { + let name = name.as_ref(); + let ty = build_path_ident_ty(name); + wrap_array_ty_by_dims(dims, ty) +} + +fn wrap_ty_in_array(ty: Ty) -> Ty { + let kind = TyKind::Array(Box::new(ty)); + Ty { + kind: Box::new(kind), + ..Default::default() + } +} + +pub(crate) fn build_for_stmt( + loop_var: &crate::symbols::Symbol, + iter: crate::types::QasmTypedExpr, + body: Block, + stmt_span: Span, +) -> Stmt { + Stmt { + kind: Box::new(StmtKind::Expr(Box::new(Expr { + kind: Box::new(ExprKind::For( + Box::new(Pat { + kind: Box::new(PatKind::Bind( + Box::new(Ident { + name: loop_var.name.clone().into(), + span: loop_var.span, + ..Default::default() + }), + Some(Box::new(map_qsharp_type_to_ast_ty(&loop_var.qsharp_ty))), + )), + ..Default::default() + }), + Box::new(iter.expr), + Box::new(body), + )), + span: stmt_span, + ..Default::default() + }))), + span: stmt_span, + ..Default::default() + } +} + +pub(crate) fn wrap_expr_in_parens(expr: Expr, span: Span) -> Expr { + Expr { + id: NodeId::default(), + span, + kind: Box::new(ExprKind::Paren(Box::new(expr))), + } +} + +pub(crate) fn build_while_stmt(expr: Expr, body: Block, stmt_span: Span) -> Stmt { + Stmt { + kind: Box::new(StmtKind::Expr(Box::new(Expr { + kind: Box::new(ExprKind::While(Box::new(expr), Box::new(body))), + span: stmt_span, + ..Default::default() + }))), + span: stmt_span, + ..Default::default() + } +} + +pub(crate) fn build_return_expr(expr: Expr, span: Span) -> Expr { + Expr { + kind: Box::new(ExprKind::Return(Box::new(expr))), + span, + ..Default::default() + } +} + +pub(crate) fn build_return_unit(span: Span) -> Expr { + build_return_expr(create_unit_expr(span), span) +} + +fn create_unit_expr(span: Span) -> Expr { + Expr { + span, + kind: Box::new(ExprKind::Tuple(Box::new([]))), + ..Default::default() + } +} + +pub(crate) fn build_end_stmt(span: Span) -> Stmt { + let message = Expr { + kind: Box::new(ExprKind::Lit(Box::new(Lit::String("end".into())))), + ..Default::default() + }; + + let kind = ExprKind::Fail(Box::new(message)); + Stmt { + kind: Box::new(StmtKind::Expr(Box::new(Expr { + kind: Box::new(kind), + span, + ..Default::default() + }))), + span, + ..Default::default() + } +} + +pub(crate) fn build_index_expr(expr: Expr, index_expr: Expr, span: Span) -> Expr { + let kind = ExprKind::Index(Box::new(expr), Box::new(index_expr)); + Expr { + kind: Box::new(kind), + span, + ..Default::default() + } +} + +pub(crate) fn build_barrier_call(span: Span) -> Stmt { + let expr = build_call_no_params("__quantum__qis__barrier__body", &[], span); + build_stmt_semi_from_expr(expr) +} + +pub(crate) fn build_attr(text: String, span: Span) -> Attr { + Attr { + id: NodeId::default(), + span, + name: Box::new(Ident { + name: Rc::from(text), + span, + ..Default::default() + }), + arg: Box::new(create_unit_expr(span)), + } +} + +pub(crate) fn build_gate_decl( + name: String, + cargs: Vec<(String, Ty, Pat)>, + qargs: Vec<(String, Ty, Pat)>, + body: Option, + name_span: Span, + body_span: Span, + gate_span: Span, +) -> Stmt { + let args = cargs + .into_iter() + .chain(qargs) + .map(|(_, _, pat)| Box::new(pat)) + .collect::>(); + + let lo = args + .iter() + .min_by_key(|x| x.span.lo) + .map(|x| x.span.lo) + .unwrap_or_default(); + + let hi = args + .iter() + .max_by_key(|x| x.span.hi) + .map(|x| x.span.hi) + .unwrap_or_default(); + + let input_pat_kind = if args.len() > 1 { + PatKind::Tuple(args.into_boxed_slice()) + } else { + PatKind::Paren(args[0].clone()) + }; + + let input_pat = Pat { + kind: Box::new(input_pat_kind), + span: Span { lo, hi }, + ..Default::default() + }; + let body = CallableBody::Block(Box::new(body.unwrap_or_else(|| Block { + id: NodeId::default(), + span: body_span, + stmts: Box::new([]), + }))); + let decl = CallableDecl { + id: NodeId::default(), + span: name_span, + kind: CallableKind::Operation, + name: Box::new(Ident { + name: name.into(), + ..Default::default() + }), + generics: Box::new([]), + input: Box::new(input_pat), + output: Box::new(build_path_ident_ty("Unit")), + functors: None, + body: Box::new(body), + }; + let item = Item { + span: gate_span, + kind: Box::new(ast::ItemKind::Callable(Box::new(decl))), + ..Default::default() + }; + + Stmt { + kind: Box::new(StmtKind::Item(Box::new(item))), + span: gate_span, + ..Default::default() + } +} + +pub(crate) fn build_gate_decl_lambda>( + name: S, + cargs: Vec<(String, Ty, Pat)>, + qargs: Vec<(String, Ty, Pat)>, + body: Option, + name_span: Span, + body_span: Span, + gate_span: Span, +) -> Stmt { + let args = cargs + .into_iter() + .chain(qargs) + .map(|(name, ty, pat)| (name, ty, pat.span)) + .collect::>(); + + let lo = args + .iter() + .min_by_key(|(_, _, span)| span.lo) + .map(|(_, _, span)| span.lo) + .unwrap_or_default(); + + let hi = args + .iter() + .max_by_key(|(_, _, span)| span.hi) + .map(|(_, _, span)| span.hi) + .unwrap_or_default(); + + let name_args = args + .iter() + .map(|(name, _, span)| Pat { + kind: Box::new(PatKind::Bind( + Box::new(Ident { + span: *span, + name: Rc::from(name.as_ref()), + ..Default::default() + }), + None, + )), + ..Default::default() + }) + .map(Box::new) + .collect::>(); + let input_pat = if args.len() > 1 { + ast::Pat { + kind: Box::new(PatKind::Tuple(name_args.into_boxed_slice())), + span: Span { lo, hi }, + ..Default::default() + } + } else { + ast::Pat { + kind: Box::new(ast::PatKind::Paren(name_args[0].clone())), + span: Span { lo, hi }, + ..Default::default() + } + }; + + let block_expr = build_wrapped_block_expr(body.map_or_else( + || Block { + id: NodeId::default(), + span: body_span, + stmts: Box::new([]), + }, + |block| block, + )); + let lambda_expr = Expr { + id: NodeId::default(), + kind: Box::new(ExprKind::Lambda( + CallableKind::Operation, + Box::new(input_pat), + Box::new(block_expr), + )), + span: gate_span, + }; + let ty_args = args.iter().map(|(_, ty, _)| ty.clone()).collect::>(); + let input_ty = if args.len() > 1 { + ast::Ty { + kind: Box::new(ast::TyKind::Tuple(ty_args.into_boxed_slice())), + ..Default::default() + } + } else { + ast::Ty { + kind: Box::new(ast::TyKind::Paren(Box::new(ty_args[0].clone()))), + ..Default::default() + } + }; + let lambda_ty = ast::Ty { + kind: Box::new(ast::TyKind::Arrow( + CallableKind::Operation, + Box::new(input_ty), + Box::new(build_path_ident_ty("Unit")), + None, + )), + ..Default::default() + }; + Stmt { + span: gate_span, + kind: Box::new(StmtKind::Local( + Mutability::Immutable, + Box::new(Pat { + kind: Box::new(PatKind::Bind( + Box::new(Ident { + span: name_span, + name: Rc::from(name.as_ref()), + ..Default::default() + }), + Some(Box::new(lambda_ty)), + )), + ..Default::default() + }), + Box::new(lambda_expr), + )), + ..Default::default() + } +} diff --git a/compiler/qsc_qasm3/src/compile.rs b/compiler/qsc_qasm3/src/compile.rs new file mode 100644 index 0000000000..10caf5909b --- /dev/null +++ b/compiler/qsc_qasm3/src/compile.rs @@ -0,0 +1,4364 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use core::panic; +use std::path::PathBuf; +use std::vec::Drain; + +use crate::ast_builder::{ + self, build_arg_pat, build_array_reverse_expr, build_assignment_statement, build_attr, + build_barrier_call, build_binary_expr, build_cast_call, build_cast_call_two_params, + build_classical_decl, build_complex_binary_expr, build_complex_from_expr, + build_convert_call_expr, build_default_result_array_expr, build_expr_array_expr, + build_gate_call_param_expr, build_gate_decl_lambda, build_if_expr_then_block, + build_if_expr_then_block_else_block, build_if_expr_then_block_else_expr, + build_if_expr_then_expr_else_expr, build_implicit_return_stmt, + build_indexed_assignment_statement, build_lit_bigint_expr, build_lit_bool_expr, + build_lit_complex_expr, build_lit_double_expr, build_lit_int_expr, + build_lit_result_array_expr_from_bitstring, build_lit_result_expr, build_managed_qubit_alloc, + build_math_call_no_params, build_measure_call, build_operation_with_stmts, + build_path_ident_expr, build_range_expr, build_reset_call, build_stmt_semi_from_expr, + build_stmt_wrapped_block_expr, build_top_level_ns_with_item, build_tuple_expr, + build_unary_op_expr, build_unmanaged_qubit_alloc, build_unmanaged_qubit_alloc_array, + build_wrapped_block_expr, is_complex_binop_supported, managed_qubit_alloc_array, + map_qsharp_type_to_ast_ty, +}; + +use crate::oqasm_helpers::{ + binop_requires_symmetric_int_conversion, can_cast_literal_with_value_knowledge, + extract_dims_from_designator, get_designator_from_scalar_type, requires_symmetric_conversion, + requires_types_already_match_conversion, safe_u128_to_f64, span_for_named_item, + span_for_syntax_node, span_for_syntax_token, +}; +use crate::oqasm_types::{promote_to_uint_ty, promote_types, types_equal_except_const}; +use crate::runtime::{get_runtime_function_decls, RuntimeFunctions}; +use crate::symbols::IOKind; +use crate::symbols::Symbol; +use crate::symbols::SymbolTable; +use crate::types::{get_indexed_type, get_qsharp_gate_name, GateModifier, QasmTypedExpr}; +use crate::{ + CompilerConfig, OperationSignature, OutputSemantics, ProgramType, QubitSemantics, + SemanticError, SemanticErrorKind, +}; + +use ast::NodeId; +use ast::Package; +use ast::TopLevelNode; +use num_bigint::BigInt; +use oq3_semantics::types::{ArrayDims, IsConst, Type}; +use oq3_syntax::ast::{ + AnnotationStatement, ArithOp, BinaryOp, BitString, CastExpression, DelayStmt, Expr, + GateOperand, HasArgList, HasName, Literal, LiteralKind, Modifier, ParamList, ParenExpr, + PragmaStatement, Stmt, TimeUnit, TimingLiteral, UnaryOp, +}; +use oq3_syntax::SyntaxNode; +use oq3_syntax::{AstNode, HasTextName}; +use qsc::ast; +use qsc::Span; +use qsc::{error::WithSource, SourceMap}; + +use crate::{ + parse::{QasmSource, QasmSourceTrait}, + QasmCompileUnit, +}; + +#[cfg(test)] +pub(crate) mod tests; + +/// Compiles a QASM3 source to a Q# AST package. +#[must_use] +pub fn qasm_to_program( + source: QasmSource, + source_map: SourceMap, + config: CompilerConfig, +) -> QasmCompileUnit { + assert!(!source.has_errors(), "Source has errors"); + assert!( + source.parse_result().have_parse(), + "Source has not been successfully parsed" + ); + let compiler = QasmCompiler { + source, + source_map, + config, + stmts: Vec::new(), + runtime: RuntimeFunctions::default(), + errors: Vec::new(), + file_stack: Vec::new(), + symbols: SymbolTable::new(), + version: None, + next_gate_as_item: false, + }; + compiler.compile_program() +} + +struct QasmCompiler { + /// The root QASM source to compile. + source: QasmSource, + /// The source map of QASM sources for error reporting. + source_map: SourceMap, + /// The configuration for the compiler. + /// This includes the qubit semantics to follow when compiling to Q# AST. + /// The output semantics to follow when compiling to Q# AST. + /// The program type to compile to. + config: CompilerConfig, + /// The compiled statments accumulated during compilation. + stmts: Vec, + /// The runtime functions that need to be included at the end of + /// compilation + runtime: RuntimeFunctions, + errors: Vec>, + /// The file stack is used to track the current file for error reporting. + /// When we include a file, we push the file path to the stack and pop it + /// when we are done with the file. + /// This allows us to report errors with the correct file path. + file_stack: Vec, + symbols: SymbolTable, + /// The QASM version parsed from the source file. This is a placeholder + /// for future use. We may want to use this to generate/parse different code + /// based on the QASM version. + version: Option, + /// If the next gate should be compiled as a top level item instead of a lambda. + /// In order to close over captured variables, we compile gates as a lambda + /// operations; however, if the gate is annotated, we need to compile it as a + /// top level item as attributes are not supported on lambdas. This isn't an + /// issue as any gates that need attributes can't be used in a lambda anyway. + /// This value is set once we encounter an annotation statement and is reset + /// after the next gate is compiled, we run out of statements, or we encounter + /// an error. + next_gate_as_item: bool, +} + +impl QasmCompiler { + pub fn push_unimplemented_error_message>( + &mut self, + message: S, + node: &SyntaxNode, + ) { + let span = span_for_syntax_node(node); + let kind = crate::ErrorKind::Unimplemented(message.as_ref().to_string(), span); + let error = self.create_err(kind); + self.errors.push(error); + } + pub fn push_missing_symbol_error>(&mut self, name: S, node: &SyntaxNode) { + let span = span_for_syntax_node(node); + let kind = SemanticErrorKind::UndefinedSymbol(name.as_ref().to_string(), span); + let kind = crate::ErrorKind::Semantic(SemanticError(kind)); + let error = self.create_err(kind); + self.errors.push(error); + } + pub fn push_redefined_symbol_error>(&mut self, name: S, span: Span) { + let kind = SemanticErrorKind::RedefinedSymbol(name.as_ref().to_string(), span); + self.push_semantic_error(kind); + } + pub fn push_semantic_error(&mut self, kind: SemanticErrorKind) { + let kind = crate::ErrorKind::Semantic(SemanticError(kind)); + let error = self.create_err(kind); + self.errors.push(error); + } + pub fn push_unsupported_error_message>(&mut self, message: S, node: &SyntaxNode) { + let span = span_for_syntax_node(node); + let kind = crate::ErrorKind::NotSupported(message.as_ref().to_string(), span); + let error = self.create_err(kind); + self.errors.push(error); + } + pub fn push_calibration_error(&mut self, node: &SyntaxNode) { + let span = span_for_syntax_node(node); + let text = node.text().to_string(); + let kind = crate::ErrorKind::CalibrationsNotSupported(text, span); + let error = self.create_err(kind); + self.errors.push(error); + } + pub fn push_pulse_control_error(&mut self, node: &SyntaxNode) { + let span = span_for_syntax_node(node); + let text = node.text().to_string(); + let kind = crate::ErrorKind::PulseControlNotSupported(text, span); + let error = self.create_err(kind); + self.errors.push(error); + } + fn create_err(&self, kind: crate::ErrorKind) -> WithSource { + let error = crate::Error(kind); + let path = self.file_stack.last().map_or("", |p| { + p.to_str().expect("expected source mapping to exist.") + }); + let source = self.source_map.find_by_name(path); + let offset = source.map_or(0, |x| x.offset); + let offset_error = error.with_offset(offset); + WithSource::from_map(&self.source_map, offset_error) + } + + pub fn drain_nodes(&mut self) -> Drain { + self.stmts.drain(..) + } + + /// The main entry into compilation. This function will compile the + /// source file and build the appropriate package based on the + /// configuration. + fn compile_program(mut self) -> QasmCompileUnit { + self.compile_source(&self.source.clone()); + self.prepend_runtime_decls(); + let program_ty = self.config.program_ty.clone(); + let (package, signature) = match program_ty { + ProgramType::File(name) => self.build_file(name), + ProgramType::Operation(name) => self.build_operation(name), + ProgramType::Fragments => (self.build_fragments(), None), + }; + + QasmCompileUnit::new(self.source_map, self.errors, Some(package), signature) + } + + /// Prepends the runtime declarations to the beginning of the statements. + /// Any runtime functions that are required by the compiled code are set + /// in the `self.runtime` field during compilation. + /// + /// We could declare these as top level functions when compiling to + /// `ProgramType::File`, but prepending them to the statements is the + /// most flexible approach. + fn prepend_runtime_decls(&mut self) { + let mut runtime = get_runtime_function_decls(self.runtime); + self.stmts.splice(0..0, runtime.drain(..)); + } + + /// Build a package with namespace and an operation + /// containing the compiled statements. + fn build_file>(&mut self, name: S) -> (Package, Option) { + let tree = self.source.tree(); + let whole_span = span_for_syntax_node(tree.syntax()); + let (operation, mut signature) = self.create_entry_operation(name, whole_span); + let ns = "qasm3_import"; + signature.ns = Some(ns.to_string()); + let top = build_top_level_ns_with_item(whole_span, ns, operation); + ( + Package { + nodes: Box::new([top]), + ..Default::default() + }, + Some(signature), + ) + } + + /// Creates an operation with the given name. + fn build_operation>(&mut self, name: S) -> (Package, Option) { + let tree = self.source.tree(); + let whole_span = span_for_syntax_node(tree.syntax()); + let (operation, signature) = self.create_entry_operation(name, whole_span); + ( + Package { + nodes: Box::new([TopLevelNode::Stmt(Box::new(ast::Stmt { + kind: Box::new(ast::StmtKind::Item(Box::new(operation))), + span: whole_span, + id: NodeId::default(), + }))]), + ..Default::default() + }, + Some(signature), + ) + } + + /// Turns the compiled statements into package of top level nodes + fn build_fragments(&mut self) -> Package { + let nodes = self + .drain_nodes() + .map(Box::new) + .map(TopLevelNode::Stmt) + .collect::>() + .into_boxed_slice(); + Package { + nodes, + ..Default::default() + } + } + + /// Root recursive function for compiling the source. + fn compile_source(&mut self, source: &QasmSource) { + // we push the file path to the stack so we can track the current file + // for reporting errors. This saves us from having to pass around + // the current QasmSource value. + self.file_stack.push(source.path()); + + let mut annotations = Vec::new(); + + // we keep an iterator of the includes so we can match them with the + // source includes. The include statements only have the path, but + // we have already loaded all of source files in the + // `source.includes()` + let mut includes = source.includes().iter(); + for stmt in source.tree().statements() { + match stmt { + Stmt::AnnotationStatement(annotation) => { + if let Some(annotation) = self.compile_annotation_stmt(&annotation) { + annotations.push(annotation); + self.next_gate_as_item = true; + } + continue; + } + Stmt::Include(include) => { + let Some(Some(path)) = include.file().map(|f| f.to_string()) else { + let span = span_for_syntax_node(include.syntax()); + let kind = SemanticErrorKind::IncludeStatementMissingPath(span); + self.push_semantic_error(kind); + continue; + }; + + // if we are not in the root we should not be able to include + // as this is a limitation of the QASM3 language + if !self.symbols.is_current_scope_global() { + let kind = SemanticErrorKind::IncludeNotInGlobalScope( + path.to_string(), + span_for_syntax_node(include.syntax()), + ); + self.push_semantic_error(kind); + continue; + } + + // special case for stdgates.inc + // it won't be in the includes list + if path.to_lowercase() == "stdgates.inc" { + self.define_stdgates(&include); + continue; + } + + let include = includes.next().expect("missing include"); + self.compile_source(include); + } + _ => { + if let Some(stmt) = self.compile_stmt(&stmt) { + if annotations.is_empty() { + self.stmts.push(stmt); + continue; + } + + // we drain the annotations regardless of whether the statement + // can have them attached or not. This is to prevent the attrs + // from being attached to the wrong statement. + + // If there is an error, we record the error, push the stmt + // without the annotations, and continue. + // The error is fatal for overall compilation, but this way we + // can continue to compile the rest of the statements + let mut stmt = stmt; + self.apply_annotations_to_stmt(&mut annotations, &mut stmt); + self.stmts.push(stmt); + self.next_gate_as_item = false; + } + } + } + } + + if !annotations.is_empty() { + let span = annotations.last().map(|x| x.span).unwrap_or_default(); + let kind = SemanticErrorKind::AnnotationWithoutStatement(span); + self.push_semantic_error(kind); + self.next_gate_as_item = false; + } + + // Finally we pop the file path from the stack so that we + // can return to the previous file for error handling. + self.file_stack.pop(); + } + + fn compile_stmts(&mut self, stmt: &[oq3_syntax::ast::Stmt]) -> Vec { + let mut annotations = Vec::new(); + let mut stmts = Vec::new(); + for stmt in stmt { + if let Stmt::AnnotationStatement(annotation) = stmt { + // we compile the annotation and push it to the annotations. + // If compiling fails, we record the error and continue. + if let Some(annotation) = self.compile_annotation_stmt(annotation) { + annotations.push(annotation); + self.next_gate_as_item = true; + } + continue; + } + let stmt = self.compile_stmt(stmt); + if stmt.is_none() { + continue; + } + let stmt = stmt.expect("stmt is not None"); + if annotations.is_empty() { + stmts.push(stmt); + continue; + } + let mut stmt = stmt; + + // we drain the annotations regardless of whether the statement + // can have them attached or not. This is to prevent the attrs + // from being attached to the wrong statement. + + // If there is an error, we record the error, push the stmt + // without the annotations, and continue. + // The error is fatal for overall compilation, but this way we + // can continue to compile the rest of the statements + + self.apply_annotations_to_stmt(&mut annotations, &mut stmt); + self.next_gate_as_item = false; + } + if !annotations.is_empty() { + let span = annotations.last().map(|x| x.span).unwrap_or_default(); + let kind = SemanticErrorKind::AnnotationWithoutStatement(span); + self.push_semantic_error(kind); + self.next_gate_as_item = false; + } + stmts + } + + fn apply_annotations_to_stmt( + &mut self, + annotations: &mut Vec, + stmt: &mut ast::Stmt, + ) { + let current_annotations: Vec<_> = annotations.drain(..).map(Box::new).collect(); + if let ast::StmtKind::Item(item) = stmt.kind.as_mut() { + if let ast::ItemKind::Callable(_) = item.kind.as_ref() { + let mut existing_attrs = item.attrs.to_vec(); + existing_attrs.extend(current_annotations); + item.attrs = existing_attrs.into_boxed_slice(); + } else { + let kind = SemanticErrorKind::InvalidAnnotationTarget(stmt.span); + self.push_semantic_error(kind); + } + } else { + let kind = SemanticErrorKind::InvalidAnnotationTarget(stmt.span); + self.push_semantic_error(kind); + } + } + + /// Match against the different types of statements and compile them + /// to the appropriate AST statement. There should be no logic here. + fn compile_stmt(&mut self, stmt: &oq3_syntax::ast::Stmt) -> Option { + match stmt { + Stmt::AliasDeclarationStatement(alias) => self.compile_alias_decl(alias), + Stmt::AssignmentStmt(assignment) => self.compile_assignment_stmt(assignment), + Stmt::Barrier(barrier) => self.compile_barrier_stmt(barrier), + Stmt::BreakStmt(break_stmt) => self.compile_break_stmt(break_stmt), + Stmt::ClassicalDeclarationStatement(decl) => self.compile_classical_decl(decl), + Stmt::ContinueStmt(continue_stmt) => self.compile_continue_stmt(continue_stmt), + Stmt::Def(def) => self.compile_def_decl(def), + Stmt::EndStmt(end) => Some(compile_end_stmt(end)), + Stmt::ExprStmt(expr) => self.compile_expr_stmt(expr), + Stmt::ForStmt(for_stmt) => self.compile_for_stmt(for_stmt), + Stmt::Gate(gate) => self.compile_gate_decl(gate), + Stmt::IfStmt(if_stmt) => self.compile_if_stmt(if_stmt), + Stmt::IODeclarationStatement(io_decl) => self.compile_io_decl_stmt(io_decl), + Stmt::LetStmt(let_stmt) => self.compile_let_stmt(let_stmt), + Stmt::Measure(measure) => self.compile_measure_stmt(measure), + Stmt::QuantumDeclarationStatement(decl) => self.compile_quantum_decl(decl), + Stmt::Reset(reset) => self.compile_reset_call(reset), + Stmt::SwitchCaseStmt(switch_case) => self.compile_switch_stmt(switch_case), + Stmt::VersionString(version) => self.compile_version_stmt(version), + Stmt::WhileStmt(while_stmt) => self.compile_while_stmt(while_stmt), + Stmt::Include(include) => self.compile_include_stmt(include), + Stmt::Cal(..) | Stmt::DefCal(..) | Stmt::DefCalGrammar(..) => { + self.compile_calibration_stmt(stmt) + } + Stmt::AnnotationStatement(annotation) => { + let message = "Annotation statements should have been handled in compile_stmts"; + let span = span_for_syntax_node(annotation.syntax()); + let kind = SemanticErrorKind::UnexpectedParserError(message.to_string(), span); + self.push_semantic_error(kind); + None + } + Stmt::DelayStmt(delay) => self.compile_delay_stmt(delay), + Stmt::PragmaStatement(pragma) => self.compile_pragma_stmt(pragma), + } + } + + fn compile_pragma_stmt(&mut self, stmt: &PragmaStatement) -> Option { + self.push_unsupported_error_message("Pragma statements", stmt.syntax()); + None + } + + fn compile_delay_stmt(&mut self, stmt: &DelayStmt) -> Option { + self.push_unsupported_error_message("Delay statements", stmt.syntax()); + None + } + + /// Annotations are defined by the compiler and their behavior are not part of the QASM3 + /// specification. They start with an `@` symbol and are used to define attributes for + /// the next statement. Their values are not validated by the compiler and are passed + /// directly to the AST as a string containing the whole line. It is up to the compiler + /// to interpret the annotation. + /// + /// We use annotations to define intrinsic gates that are simulatable in Q# mapping the + /// annotation to a Q# attribute. We also only allow the annotation to be attached to + /// gates. Any other usage will result in a semantic error. + fn compile_annotation_stmt(&mut self, stmt: &AnnotationStatement) -> Option { + let text = stmt.annotation_text(); + let span = span_for_syntax_node(stmt.syntax()); + if let "@SimulatableIntrinsic" = text.as_str() { + let (_at, name) = text.split_at(1); + Some(build_attr(name.to_string(), span)) + } else { + let span = span_for_syntax_node(stmt.syntax()); + let kind = SemanticErrorKind::UnknownAnnotation(text.to_string(), span); + self.push_semantic_error(kind); + None + } + } + + /// We don't support callibration statements in Q# so we push an error + fn compile_calibration_stmt(&mut self, stmt: &Stmt) -> Option { + self.push_calibration_error(stmt.syntax()); + None + } + + /// This function is always a indication of a error. Either the + /// program is declaring include in a non-global scope or the + /// include is not handled in `self.compile_source` properly. + fn compile_include_stmt(&mut self, include: &oq3_syntax::ast::Include) -> Option { + // if we are not in the root we should not be able to include + if !self.symbols.is_current_scope_global() { + let name = include.to_string(); + let span = span_for_syntax_node(include.syntax()); + let kind = SemanticErrorKind::IncludeNotInGlobalScope(name, span); + self.push_semantic_error(kind); + return None; + } + // if we are at the root and we have an include, we should have + // already handled it and we are in an invalid state + panic!("Include should have been handled in compile_source") + } + + /// Aliasing allows declared qubit bits and registers to be referred to by another name. + /// Aliasing can slice a qubit array or register. Aliasing uses the `let` keyword. + /// Example: + /// ```qasm + /// qubit[4] = q; + /// let qubit = q[1:3]; // qubit now points to the middle two qubits of q + /// // qubit[0] is now q[1], and qubit[1] is now q[2] + /// ``` + /// We can eventually support this for qubits, but we don't support it for registers. + /// Creating an array slice of results in Q# would be difficult as it would normally + /// copy the conents of the array. We could potentially create a view of the array, but + /// further investigation is needed. + fn compile_alias_decl( + &mut self, + alias: &oq3_syntax::ast::AliasDeclarationStatement, + ) -> Option { + let name = alias.name().expect("Alias declaration must have a name"); + let name_span = span_for_named_item(alias); + let decl_span = span_for_syntax_node(alias.syntax()); + let rhs = alias.expr().and_then(|f| self.compile_expr(&f))?; + + if !matches!(rhs.ty, Type::Qubit | Type::QubitArray(_)) { + let kind = SemanticErrorKind::CannotAliasType(format!("{:?}", rhs.ty), decl_span); + self.push_semantic_error(kind); + return None; + } + + let qsharp_ty = self.convert_semantic_type_to_qsharp_type(&rhs.ty, name.syntax())?; + let symbol = Symbol { + name: name.to_string(), + span: span_for_syntax_node(name.syntax()), + ty: rhs.ty.clone(), + qsharp_ty, + io_kind: IOKind::Default, + }; + + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name.to_string(), name_span); + return None; + } + + let name = name.to_string(); + let is_const = true; + let ty_span = Span::default(); + let qsharp_ty = self.convert_semantic_type_to_qsharp_type(&rhs.ty, alias.syntax())?; + let stmt = build_classical_decl( + name, is_const, ty_span, decl_span, name_span, &qsharp_ty, rhs.expr, + ); + Some(stmt) + } + + /// Assignment statements have two forms: simple and indexed. + /// Simple assignments are of the form `ident = expr;` and indexed + /// assignments are of the form `ident[index] = expr;`. + /// This function will dispatch to the appropriate function based + /// on the type of assignment. + /// + /// While they are similar, the indexed assignment is far more complex. + fn compile_assignment_stmt( + &mut self, + assignment: &oq3_syntax::ast::AssignmentStmt, + ) -> Option { + if let Some(name) = assignment.identifier() { + self.compile_simple_assignment_stmt(assignment, &name) + } else { + self.compile_indexed_assignment_stmt(assignment) + } + } + + /// `ident = expr;` + fn compile_simple_assignment_stmt( + &mut self, + assignment: &oq3_syntax::ast::AssignmentStmt, + name: &oq3_syntax::ast::Identifier, + ) -> Option { + let name_span = span_for_named_item(assignment); + let assignment_span = span_for_syntax_node(assignment.syntax()); + let lhs_symbol = self + .symbols + .get_symbol_by_name(name.to_string().as_str())? + .clone(); + if lhs_symbol.ty.is_const() { + let kind = SemanticErrorKind::CannotUpdateConstVariable(name.to_string(), name_span); + self.push_semantic_error(kind); + // usually we'd return None here, but we'll continue to compile the rhs + // looking for more errors. There is nothing in this type of error that + // would prevent us from compiling the rhs. + } + // resolve the rhs expression to match the lhs type + let rhs = self.compile_expr_to_ty_with_casts( + assignment.rhs(), + &lhs_symbol.ty, + assignment.syntax(), + )?; + let stmt = build_assignment_statement(name_span, name.to_string(), rhs, assignment_span); + Some(stmt) + } + + /// `ident[index] = expr;` + fn compile_indexed_assignment_stmt( + &mut self, + assignment: &oq3_syntax::ast::AssignmentStmt, + ) -> Option { + let indexed_ident = assignment + .indexed_identifier() + .expect("assignment without name must have an indexed identifier"); + let name = indexed_ident + .identifier() + .expect("indexed identifier must have a name"); + let string_name = name.to_string(); + let name_span = span_for_named_item(&indexed_ident); + let stmt_span = span_for_syntax_node(assignment.syntax()); + let rhs_span = span_for_syntax_node(assignment.rhs()?.syntax()); + + // resolve the index expression + // we only support single index expressions for now + // but in the future we may support slice/range/array indexing + let indices: Vec<_> = indexed_ident + .index_operators() + .filter_map(|op| self.compile_index_operator(&op)) + .flatten() + .collect(); + + if indices.len() != 1 { + // This is a temporary limitation. We can only handle + // single index expressions for now. + let kind = SemanticErrorKind::IndexMustBeSingleExpr(span_for_syntax_node( + indexed_ident.syntax(), + )); + self.push_semantic_error(kind); + return None; + } + let index = indices[0].clone(); + + let lhs_symbol = self + .symbols + .get_symbol_by_name(name.to_string().as_str())? + .clone(); + if index.ty.num_dims() > lhs_symbol.ty.num_dims() { + let kind = SemanticErrorKind::TypeRankError(rhs_span); + self.push_semantic_error(kind); + } + let index_expr = index.expr.clone(); + + let Some(indexed_ty) = get_indexed_type(&lhs_symbol.ty) else { + let kind = SemanticErrorKind::CannotIndexType(format!("{:?}", lhs_symbol.ty), rhs_span); + self.push_semantic_error(kind); + return None; + }; + let rhs = + self.compile_expr_to_ty_with_casts(assignment.rhs(), &indexed_ty, assignment.syntax())?; + let stmt = + build_indexed_assignment_statement(name_span, string_name, index_expr, rhs, stmt_span); + Some(stmt) + } + + /// Barrier isn't supported in Q# so we insert a runtime function that is + /// a no-op for simulation but is `@SimulatableIntrinsic()` so that it can + /// be emited in code gen as `__quantum__qis__barrier__body()`. This + /// matches the existing `qiskit-qir` behavior. Qiskit barriers are + /// variadic, but QIR doesn't support variadic gates. + /// We should look at how to handle this better in the future. + fn compile_barrier_stmt(&mut self, barrier: &oq3_syntax::ast::Barrier) -> Option { + let qubit_args: Vec<_> = if let Some(qubits) = barrier.qubit_list() { + qubits + .gate_operands() + .map(|op| self.compile_gate_operand(&op).map(|x| x.expr)) + .collect() + } else { + vec![] + }; + + if qubit_args.iter().any(Option::is_none) { + // if any of the qubit arguments failed to compile, we can't proceed + // This can happen if the qubit is not defined or if the qubit was + // a hardware qubit + return None; + } + let call_span = span_for_syntax_node(barrier.syntax()); + // we don't support barrier, but we can insert a runtime function + // which will generate a barrier call in QIR + self.runtime.insert(RuntimeFunctions::Barrier); + Some(build_barrier_call(call_span)) + } + + /// Need to add break support in Q# to support break statements + fn compile_break_stmt(&mut self, break_stmt: &oq3_syntax::ast::BreakStmt) -> Option { + self.push_unsupported_error_message("break statements", break_stmt.syntax()); + None + } + + /// Classical decls are used to declare classical variables. They have two + /// main forms: + /// - `type ident;` + /// - `type ident = expr;` + /// + /// Q# requires classical variables to be initialized, so we will use the + /// default value for the type to initialize the variable. In theory this + /// isn't a problem as any classical variable that is used should be + /// initialized before use and would be a bug anyway. This leads to awkward + /// code in Q# where we have to initialize classical variables that are + /// always overwritten before use. + fn compile_classical_decl( + &mut self, + decl: &oq3_syntax::ast::ClassicalDeclarationStatement, + ) -> Option { + let name = decl.name().expect("classical declaration must have a name"); + let name_span = span_for_syntax_node(name.syntax()); + let scalar_ty = decl + .scalar_type() + .expect("Classical declaration must have a scalar type"); + let is_const = decl.const_token().is_some(); + // if we can't convert the scalar type, we can't proceed, an error has been pushed + let ty = self.get_semantic_type_from_scalar_type(&scalar_ty, is_const)?; + let qsharp_ty = self.convert_semantic_type_to_qsharp_type(&ty, name.syntax())?; + + let symbol = Symbol { + name: name.to_string(), + span: name_span, + ty: ty.clone(), + qsharp_ty: qsharp_ty.clone(), + io_kind: IOKind::Default, + }; + + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name.to_string(), name_span); + return None; + } + + // if there is an expression, compile it to match the decl type + let rhs = self.compile_expr_to_ty_with_casts(decl.expr(), &ty, decl.syntax())?; + + // create the let binding and assign the rhs to the lhs + let ty_span = span_for_syntax_node(scalar_ty.syntax()); + let stmt_span = span_for_syntax_node(decl.syntax()); + let stmt = build_classical_decl( + name.to_string(), + is_const, + ty_span, + stmt_span, + name_span, + &qsharp_ty, + rhs, + ); + + Some(stmt) + } + + /// The expr type is fixed so we try to resolve the expression to match the type + /// via implicit casts or literal conversion if possible. + fn compile_expr_to_ty_with_casts( + &mut self, + expr: Option, + ty: &Type, + node: &SyntaxNode, + ) -> Option { + let Some(expr) = expr else { + // In OpenQASM, classical variables may be uninitialized, but in Q#, + // they must be initialized. We will use the default value for the type + // to initialize the variable. + return self.get_default_value(ty, node); + }; + + // since we have an expr, we can refine the node for errors + let span = span_for_syntax_node(expr.syntax()); + + let rhs = self.compile_expr(&expr)?; + let rhs_ty = rhs.ty.clone(); + + // if we have an exact type match, we can use the rhs as is + if types_equal_except_const(ty, &rhs_ty) { + return Some(rhs.expr); + } + + if let Expr::Literal(literal) = &expr { + // if the rhs is a literal, we can try to cast it to the lhs type + // we can do better than just types given we have a literal value + if can_cast_literal(ty, &rhs_ty) || can_cast_literal_with_value_knowledge(ty, literal) { + return Some(self.cast_literal_expr_to_type(ty, &rhs, literal)?.expr); + } + // if we can't cast the literal, we can't proceed + // create a semantic error and return + let kind = SemanticErrorKind::CannotAssignToType( + format!("{:?}", rhs.ty), + format!("{ty:?}"), + span, + ); + self.push_semantic_error(kind); + return None; + } + let is_negated_lit = if let Expr::PrefixExpr(prefix_expr) = &expr { + if let Some(UnaryOp::Neg) = prefix_expr.op_kind() { + matches!(&prefix_expr.expr(), Some(Expr::Literal(..))) + } else { + false + } + } else { + false + }; + if matches!(ty, Type::UInt(..)) && is_negated_lit { + let kind = SemanticErrorKind::CannotAssignToType( + "Negative Int".to_string(), + format!("{ty:?}"), + span, + ); + self.push_semantic_error(kind); + return None; + } + if let Expr::PrefixExpr(prefix_expr) = &expr { + if let Some(UnaryOp::Neg) = prefix_expr.op_kind() { + if let Some(Expr::Literal(literal)) = &prefix_expr.expr() { + // if the rhs is a literal, we can try to cast it to the lhs type + // we can do better than just types given we have a literal value + + if can_cast_literal(ty, &rhs_ty) + || can_cast_literal_with_value_knowledge(ty, literal) + { + // if the literal is negated, we need to compile it as a negated literal + // This will only work for int/float as we can't express any other + // kind of negated literal + return Some(self.compile_negated_literal_as_ty(literal, Some(ty))?.expr); + } + // if we can't cast the literal, we can't proceed + // create a semantic error and return + let kind = SemanticErrorKind::CannotAssignToType( + format!("{:?}", rhs.ty), + format!("{ty:?}"), + span_for_syntax_node(node), + ); + self.push_semantic_error(kind); + return None; + } + } + } + // the lhs has a type, but the rhs may be of a different type with + // implicit and explicit conversions. We need to cast the rhs to the + // lhs type, but if that cast fails, we will have already pushed an error + // and we can't proceed + + Some(self.cast_expr_to_type(ty, &rhs, node)?.expr) + } + + /// Need to add continue support in Q# to support continue statements + fn compile_continue_stmt( + &mut self, + continue_stmt: &oq3_syntax::ast::ContinueStmt, + ) -> Option { + self.push_unsupported_error_message("continue statements", continue_stmt.syntax()); + None + } + + /// + /// Qiskit can't handle def statements, so we push an error + /// and return None. We also push a semantic error if the def + /// statement is not in the global scope. + fn compile_def_decl(&mut self, def: &oq3_syntax::ast::Def) -> Option { + let def_span = span_for_syntax_node(def.syntax()); + if !self.symbols.is_current_scope_global() { + let kind = SemanticErrorKind::QuantumDeclarationInNonGlobalScope(def_span); + self.push_semantic_error(kind); + return None; + } + self.push_unsupported_error_message("def declarations", def.syntax()); + None + } + + /// Some statements don't fall into the normal categories, so they + /// are handled here. This is a catch-all for non-declaration + /// assignments and calculations. + /// Example: + /// ```qasm + /// input int a; + /// input int b; + /// a * b; // this is an expr statement + /// ``` + fn compile_expr_stmt(&mut self, expr: &oq3_syntax::ast::ExprStmt) -> Option { + let expr = expr.expr()?; + let texpr = self.compile_expr(&expr)?; + Some(build_stmt_semi_from_expr(texpr.expr)) + } + + /// This is the heart of compilation. All statements eventually call this + /// function and it is where we start to build the AST. From here we start + /// to return `QasmTypedExpr` so that we can track the QASM type of for the + /// Q# expresssion that we are building. This is needed for type checking + /// and casting. We must make sure all types match while building the AST + /// as Q# doesn't have implicit casting and would otherwise fail to compile. + fn compile_expr(&mut self, expr: &oq3_syntax::ast::Expr) -> Option { + match expr { + Expr::ArrayExpr(array_expr) => self.compile_array_expr(array_expr, expr), + Expr::ArrayLiteral(array_literal) => { + self.compile_array_literal_expr(array_literal, expr) + } + Expr::BinExpr(bin_expr) => self.compile_bin_expr(bin_expr, expr), + Expr::BlockExpr(_) => { + // block expressions are handled by their containing statements + panic!("Block expressions should not be compiled directly") + } + Expr::BoxExpr(box_expr) => self.compile_box_expr(box_expr), + Expr::CallExpr(call_expr) => self.compile_call_expr(call_expr), + Expr::CastExpression(cast_expr) => self.compile_cast_expr(cast_expr), + Expr::GateCallExpr(gate_call_expr) => self.compile_gate_call_expr(gate_call_expr, expr), + Expr::GPhaseCallExpr(_) => { + panic!("GPhase expressions should not be compiled directly") + } + Expr::HardwareQubit(hardware_qubit) => { + self.compile_hardware_qubit_expr(hardware_qubit, expr) + } + Expr::Identifier(identifier) => self.compile_identifier_expr(identifier, expr), + Expr::IndexExpr(index_expr) => self.compile_index_expr(index_expr), + Expr::IndexedIdentifier(indexed_identifier) => { + self.compile_indexed_identifier_expr(indexed_identifier) + } + Expr::Literal(lit) => self.compile_literal_expr(lit, expr), + Expr::TimingLiteral(lit) => self.compile_timing_literal_expr(lit, expr), + Expr::MeasureExpression(measure_expr) => self.compile_measure_expr(measure_expr, expr), + Expr::ModifiedGateCallExpr(modified_gate_call_expr) => { + self.compile_modified_gate_call_expr(modified_gate_call_expr) + } + Expr::ParenExpr(paren_expr) => self.compile_paren_expr(paren_expr), + Expr::PrefixExpr(prefix_expr) => self.compile_prefix_expr(prefix_expr), + Expr::RangeExpr(range_expr) => self.compile_range_expr(range_expr, expr.syntax()), + Expr::ReturnExpr(return_expr) => self.compile_return_expr(return_expr), + } + } + + /// Qubit and bit registers are handled in the their own statements. + /// Arrays for classical variables would be handled here. Qiskit + /// can't handle array expressions yet, so we push an error and return None. + fn compile_array_expr( + &mut self, + _array_expr: &oq3_syntax::ast::ArrayExpr, + expr: &Expr, + ) -> Option { + self.push_unimplemented_error_message("array expressions", expr.syntax()); + None + } + + /// Qubit and bit registers are handled in the their own statements. + /// Arrays for classical variables would be handled here. Qiskit + /// can't handle array expressions yet, so we push an error and return None. + fn compile_array_literal_expr( + &mut self, + _array_literal: &oq3_syntax::ast::ArrayLiteral, + expr: &Expr, + ) -> Option { + self.push_unimplemented_error_message("array literal expressions", expr.syntax()); + None + } + + /// Create a binary expression from the given binary expression node + /// The binary expression is created by recursively compiling the left and right + /// expressions and then creating a binary expression from the compiled expressions + /// + /// This is more complex than it seems because we need to handle type promotion + /// and casting. The `OpenQASM3` language has a specific set of rules for casting + /// between types. The rules can be found at: + /// + /// + /// This harder than decl statements as we need to deal with promotion and casting + /// for both the lhs and rhs expressions instead of having a fixed LHS type. + /// + /// complex > float > int/uint + /// within type widths are promoted to the larger type + #[allow(clippy::too_many_lines)] + fn compile_bin_expr( + &mut self, + bin_expr: &oq3_syntax::ast::BinExpr, + expr: &Expr, + ) -> Option { + // We don't need to worry about semantic errors as binary expression + // must have a lhs and rhs expression and an operator. This is + // verified in the binary expression tests. + let op = bin_expr.op_kind()?; + let lhs_expr = bin_expr.lhs()?; + let rhs_expr = bin_expr.rhs()?; + + let lhs = self.compile_expr(&lhs_expr)?; + let rhs = self.compile_expr(&rhs_expr)?; + + if lhs.ty.is_quantum() { + let kind = SemanticErrorKind::QuantumTypesInBinaryExpression(lhs.expr.span); + self.push_semantic_error(kind); + } + if rhs.ty.is_quantum() { + let kind = SemanticErrorKind::QuantumTypesInBinaryExpression(rhs.expr.span); + self.push_semantic_error(kind); + } + + let qsop = self.map_bin_op(op, expr.syntax())?; + let is_assignment = matches!(op, oq3_syntax::ast::BinaryOp::Assignment { op: _ }); + + let left_type = lhs.ty.clone(); + let right_type = rhs.ty.clone(); + + if binop_requires_bitwise_conversion(op, &left_type) { + // if the operator requires bitwise conversion, we need to determine + // what size of UInt to promote to. If we can't promote to a UInt, we + // push an error and return None. + let (ty, lhs_uint_promotion, rhs_uint_promotion) = + promote_to_uint_ty(&left_type, &right_type); + let Some(ty) = ty else { + let target_ty = Type::UInt(None, IsConst::False); + if lhs_uint_promotion.is_none() { + let target_str: String = format!("{target_ty:?}"); + let kind = SemanticErrorKind::CannotCast( + format!("{left_type:?}"), + target_str, + lhs.expr.span, + ); + self.push_semantic_error(kind); + } + if rhs_uint_promotion.is_none() { + let target_str: String = format!("{target_ty:?}"); + let kind = SemanticErrorKind::CannotCast( + format!("{right_type:?}"), + target_str, + rhs.expr.span, + ); + self.push_semantic_error(kind); + } + return None; + }; + // Now that we know the effective Uint type, we can cast the lhs and rhs + // so that operations can be performed on them. + let new_lhs = self.cast_expr_to_type(&ty, &lhs, lhs_expr.syntax())?; + // only cast the rhs if the operator requires symmetric conversion + let new_rhs = if binop_requires_bitwise_symmetric_conversion(op) { + self.cast_expr_to_type(&ty, &rhs, rhs_expr.syntax())? + } else { + rhs + }; + let expr = build_binary_expr( + is_assignment, + qsop, + new_lhs.expr, + new_rhs.expr, + span_for_syntax_node(expr.syntax()), + ); + let texpr = QasmTypedExpr { ty, expr }; + let final_expr = self.cast_expr_to_type(&left_type, &texpr, bin_expr.syntax())?; + return Some(final_expr); + } + + let span = span_for_syntax_node(expr.syntax()); + + if requires_types_already_match_conversion(op) { + if !types_equal_except_const(&left_type, &right_type) { + let kind = SemanticErrorKind::CannotApplyOperatorToTypes( + format!("{op:?}"), + format!("{left_type:?}"), + format!("{right_type:?}"), + span, + ); + self.push_semantic_error(kind); + return None; + } + let expr = build_binary_expr(is_assignment, qsop, lhs.expr, rhs.expr, span); + return Some(QasmTypedExpr { + ty: left_type, + expr, + }); + } + + // for int, uint, float, and complex, the lesser of the two types is cast to + // the greater of the two. Within each level of complex and float, types with + // greater width are greater than types with lower width. + // complex > float > int/uint + // Q# has built-in functions: IntAsDouble, IntAsBigInt to handle two cases. + // If the width of a float is greater than 64, we can't represent it as a double. + + let (lhs, rhs, ty) = if binop_requires_bool_conversion_for_type(op) { + let ty = Type::Bool(IsConst::False); + let new_lhs = self.cast_expr_to_type(&ty, &lhs, lhs_expr.syntax())?; + let new_rhs = self.cast_expr_to_type(&ty, &rhs, rhs_expr.syntax())?; + (new_lhs.expr, new_rhs.expr, ty) + } else if binop_requires_int_conversion_for_type(op, &left_type, &rhs.ty) { + let ty = Type::Int(None, IsConst::False); + let new_lhs = self.cast_expr_to_type(&ty, &lhs, lhs_expr.syntax())?; + let new_rhs = self.cast_expr_to_type(&ty, &rhs, rhs_expr.syntax())?; + (new_lhs.expr, new_rhs.expr, ty) + } else if requires_symmetric_conversion(op) { + let promoted_type = try_promote_with_casting(&left_type, &right_type); + let new_left = if promoted_type == left_type { + lhs + } else { + let node = lhs_expr.syntax(); + match &lhs_expr { + Expr::Literal(literal) => { + if can_cast_literal(&promoted_type, &left_type) + || can_cast_literal_with_value_knowledge(&promoted_type, literal) + { + self.cast_literal_expr_to_type(&promoted_type, &lhs, literal)? + } else { + self.cast_expr_to_type(&promoted_type, &lhs, node)? + } + } + _ => self.cast_expr_to_type(&promoted_type, &lhs, node)?, + } + }; + let new_right = if promoted_type == right_type { + rhs + } else { + let node = rhs_expr.syntax(); + match &rhs_expr { + Expr::Literal(literal) => { + if can_cast_literal(&promoted_type, &right_type) + || can_cast_literal_with_value_knowledge(&promoted_type, literal) + { + self.cast_literal_expr_to_type(&promoted_type, &rhs, literal)? + } else { + self.cast_expr_to_type(&promoted_type, &rhs, node)? + } + } + _ => self.cast_expr_to_type(&promoted_type, &rhs, node)?, + } + }; + (new_left.expr, new_right.expr, promoted_type) + } else { + // we don't have symmetric promotion, so we need to promote the rhs only + if is_assignment { + let oq3_syntax::ast::BinaryOp::Assignment { op: arith_op } = op else { + unreachable!() + }; + let (lhs, rhs) = + if arith_op.is_some() && binop_requires_symmetric_int_conversion(op) { + let ty = Type::Int(None, IsConst::False); + let lhs = self.cast_expr_to_type(&ty, &lhs, lhs_expr.syntax())?.expr; + let rhs = self.cast_expr_to_type(&ty, &rhs, rhs_expr.syntax())?.expr; + (lhs, rhs) + } else { + let rhs = self.compile_expr_to_ty_with_casts( + Some(rhs_expr.clone()), + &left_type, + rhs_expr.syntax(), + )?; + (lhs.expr, rhs) + }; + + (lhs, rhs, left_type) + } else if binop_requires_symmetric_int_conversion(op) { + let ty = Type::Int(None, IsConst::False); + let new_rhs = self.cast_expr_to_type(&ty, &rhs, rhs_expr.syntax())?; + (lhs.expr, new_rhs.expr, left_type) + } else { + (lhs.expr, rhs.expr, left_type) + } + }; + + // now that we have the lhs and rhs expressions, we can create the binary expression + // but we need to check if the chosen operator is supported by the types after + // promotion and conversion. + + let expr = if matches!(ty, Type::Complex(..)) { + if is_assignment { + let kind = SemanticErrorKind::ComplexBinaryAssignment(span); + self.push_semantic_error(kind); + None + } else if is_complex_binop_supported(qsop) { + Some(build_complex_binary_expr( + is_assignment, + qsop, + lhs, + rhs, + span, + )) + } else { + let kind = SemanticErrorKind::OperatorNotSupportedForTypes( + format!("{qsop:?}"), + format!("{ty:?}"), + format!("{ty:?}"), + span, + ); + self.push_semantic_error(kind); + None + } + } else { + Some(build_binary_expr(is_assignment, qsop, lhs, rhs, span)) + }; + let expr = expr?; + let ty = match &op { + BinaryOp::CmpOp(..) | BinaryOp::LogicOp(..) => Type::Bool(IsConst::False), + _ => ty, + }; + Some(QasmTypedExpr { ty, expr }) + } + + fn map_bin_op( + &mut self, + op: oq3_syntax::ast::BinaryOp, + node: &SyntaxNode, + ) -> Option { + match op { + oq3_syntax::ast::BinaryOp::LogicOp(logic_op) => Some(match logic_op { + oq3_syntax::ast::LogicOp::And => ast::BinOp::AndL, + oq3_syntax::ast::LogicOp::Or => ast::BinOp::OrL, + }), + oq3_syntax::ast::BinaryOp::ArithOp(arith) => Some(match arith { + oq3_syntax::ast::ArithOp::Add => ast::BinOp::Add, + oq3_syntax::ast::ArithOp::Mul => ast::BinOp::Mul, + oq3_syntax::ast::ArithOp::Sub => ast::BinOp::Sub, + oq3_syntax::ast::ArithOp::Div => ast::BinOp::Div, + oq3_syntax::ast::ArithOp::Rem => ast::BinOp::Mod, + oq3_syntax::ast::ArithOp::Shl => ast::BinOp::Shl, + oq3_syntax::ast::ArithOp::Shr => ast::BinOp::Shr, + oq3_syntax::ast::ArithOp::BitXor => ast::BinOp::XorB, + oq3_syntax::ast::ArithOp::BitOr => ast::BinOp::OrB, + oq3_syntax::ast::ArithOp::BitAnd => ast::BinOp::AndB, + }), + oq3_syntax::ast::BinaryOp::CmpOp(cmp_op) => Some(match cmp_op { + oq3_syntax::ast::CmpOp::Eq { negated } => { + if negated { + ast::BinOp::Neq + } else { + ast::BinOp::Eq + } + } + oq3_syntax::ast::CmpOp::Ord { ordering, strict } => match ordering { + oq3_syntax::ast::Ordering::Less => { + if strict { + ast::BinOp::Lt + } else { + ast::BinOp::Lte + } + } + oq3_syntax::ast::Ordering::Greater => { + if strict { + ast::BinOp::Gt + } else { + ast::BinOp::Gte + } + } + }, + }), + oq3_syntax::ast::BinaryOp::ConcatenationOp => { + // This is only used for types which we don't currently support. + self.push_unimplemented_error_message("Concatenation operators", node); + None + } + oq3_syntax::ast::BinaryOp::Assignment { op } => op.map(|op| match op { + oq3_syntax::ast::ArithOp::Add => ast::BinOp::Add, + oq3_syntax::ast::ArithOp::Mul => ast::BinOp::Mul, + oq3_syntax::ast::ArithOp::Sub => ast::BinOp::Sub, + oq3_syntax::ast::ArithOp::Div => ast::BinOp::Div, + oq3_syntax::ast::ArithOp::Rem => ast::BinOp::Mod, + oq3_syntax::ast::ArithOp::Shl => ast::BinOp::Shl, + oq3_syntax::ast::ArithOp::Shr => ast::BinOp::Shr, + oq3_syntax::ast::ArithOp::BitXor => ast::BinOp::XorB, + oq3_syntax::ast::ArithOp::BitOr => ast::BinOp::OrB, + oq3_syntax::ast::ArithOp::BitAnd => ast::BinOp::AndB, + }), + } + } + + fn compile_block_expr(&mut self, block_expr: &oq3_syntax::ast::BlockExpr) -> ast::Block { + let stmts = self.compile_stmts(&block_expr.statements().collect::>()); + let stmts = stmts + .into_iter() + .map(Box::new) + .collect::>() + .into_boxed_slice(); + + let span = span_for_syntax_node(block_expr.syntax()); + ast::Block { + id: NodeId::default(), + span, + stmts, + } + } + + /// + /// Qiskit can't emit the box/delay, so we push an error and return None. + /// The QASM grammar specifies this is a statement, but the parser has it as an expression. + /// + /// We don't really have anything in Q# the box statement is for scoping the + /// timing of a particular part of the circuit. We could generates call around + /// a new block, but that would be a bit of a hack. + fn compile_box_expr(&mut self, box_expr: &oq3_syntax::ast::BoxExpr) -> Option { + self.push_unimplemented_error_message("box expressions", box_expr.syntax()); + None + } + + /// Qiskit can't handle call expressions yet, so we push an error and return None. + fn compile_call_expr( + &mut self, + call_expr: &oq3_syntax::ast::CallExpr, + ) -> Option { + self.push_unimplemented_error_message("call expressions", call_expr.syntax()); + None + } + + /// explicit casts have no defined behavior AFAICT from the spec. I'm + /// guessing that they are a best effort for the compiler implementor. + fn compile_cast_expr(&mut self, cast_expr: &CastExpression) -> Option { + let scalar_ty = cast_expr + .scalar_type() + .expect("cast expression must have a scalar type"); + let is_const = false; + let ty = self.get_semantic_type_from_scalar_type(&scalar_ty, is_const)?; + let expr = self.compile_expr_to_ty_with_casts(cast_expr.expr(), &ty, cast_expr.syntax())?; + Some(QasmTypedExpr { ty, expr }) + } + + /// Gate call expression. We delegate compilation to + /// `compile_gate_call_expr_impl` with empty modifiers so that we can reuse + /// gate call compilation logic. + fn compile_gate_call_expr( + &mut self, + gate_call_expr: &oq3_syntax::ast::GateCallExpr, + expr: &Expr, + ) -> Option { + let expr_span = span_for_syntax_node(expr.syntax()); + self.compile_gate_call_expr_impl(gate_call_expr, expr_span, &[]) + } + + /// Compile gate call expression with modifiers. + /// We have to resolve the gate name and modifiers as some of the stdgates + /// have implicit modifiers and different gate names that we have to map + /// into Q# names with the appropriate modifiers/fuctors. + /// - The `inv` modifier is the `Adjoint` fuctor. + /// - The `pow` modifier is the `__Pow__` function which we define as a + /// runtime function at the end of code generation if it is used. + /// - the `ctrl` modifier is the `Controlled` functor. + /// the `negctrl` modifier is a special case equivalent to + /// `ApplyControlledOnInt(0, _, _, _)`. + /// + /// Apply the modifiers are applied in reverse order to the gate call. + /// A simplified binding of the modifiers to the gate call with all + /// operations being on a single qubit gate would look like: + /// `a @ b @ c g(r) q0, q1, q2` `=>` `(a @ (b @ (c g(r) q0), q1), q2)` + /// + /// This get more complex when we have multiple qubit gates and controls. + #[allow(clippy::too_many_lines)] + fn compile_gate_call_expr_impl( + &mut self, + gate_call_expr: &oq3_syntax::ast::GateCallExpr, + expr_span: Span, + modifiers: &[crate::types::GateModifier], + ) -> Option { + let name = gate_call_expr + .identifier() + .expect("gate call must have a name"); + let name_span = span_for_syntax_node(name.syntax()); + let name_text = name.to_string(); + let call_span = span_for_syntax_node(gate_call_expr.syntax()); + // if we fail to map the name, we don't have a valid Q# gate + // but the user may have defined their own. We check the symbol + // table looking for such a definition. + let gate_name = get_qsharp_gate_name(&name_text).unwrap_or(&name_text); + let (gate_name, additional_modifiers) = get_implicit_modifiers(gate_name, name_span); + let Some(sym) = self.symbols.get_symbol_by_name(&gate_name) else { + self.push_missing_symbol_error(name_text, name.syntax()); + return None; + }; + let Type::Gate(cargs_len, qargs_len) = sym.ty else { + let kind = SemanticErrorKind::CannotCallNonGate(call_span); + self.push_semantic_error(kind); + return None; + }; + + let classical_args = self.compile_gate_call_classical_args(gate_call_expr, cargs_len)?; + let mut qubit_args = self.compile_gate_call_quantum_args(gate_call_expr)?; + + // at this point we all of the information we need, but we have to deal with modifiers + // We have the modifiers which we have been given, plus the implicit modifiers + // from the gate definition. We need to merge these two sets of modifiers + // See: ch, crx, cry, crz, sdg, and tdg + let modifiers = modifiers + .iter() + .chain(additional_modifiers.iter()) + .rev() + .collect::>(); + let num_ctrls = calculate_num_ctrls(&modifiers); + self.verify_qubit_args_match_gate_and_ctrls( + &qubit_args, + qargs_len, + num_ctrls, + gate_call_expr, + )?; + // take the nuber of qubit args that the gates expects from the source qubits + let gate_qubits = qubit_args.split_off(qubit_args.len() - qargs_len); + // and then merge the classical args with the qubit args + // this will give us the args for the call prior to wrapping in tuples + // for controls + let args: Vec<_> = classical_args.into_iter().chain(gate_qubits).collect(); + let mut args = build_gate_call_param_expr(args, qubit_args.len()); + let mut callee = build_path_ident_expr(&gate_name, name_span, expr_span); + + for modifier in modifiers { + match modifier { + GateModifier::Inv(mod_span) => { + callee = build_unary_op_expr( + ast::UnOp::Functor(ast::Functor::Adj), + callee, + *mod_span, + ); + } + GateModifier::Pow(exponent, mod_span) => { + // The exponent is only an option when initially parsing the gate + // call. The stmt would not have been created. If we don't have an + // an eponent at this point it is a bug + let exponent = exponent.expect("Exponent must be present"); + let exponent_expr = build_lit_int_expr(exponent, *mod_span); + self.runtime |= RuntimeFunctions::Pow; + args = build_tuple_expr(vec![exponent_expr, callee, args]); + callee = build_path_ident_expr("__Pow__", *mod_span, expr_span); + } + GateModifier::Ctrl(controls, mod_span) => { + // remove the last n qubits from the qubit list + let num_ctrls = controls.unwrap_or(1); + if qubit_args.len() < num_ctrls { + let kind = + SemanticErrorKind::InvalidNumberOfQubitArgs(qargs_len, 0, call_span); + self.push_semantic_error(kind); + return None; + } + let ctrl = qubit_args.split_off(qubit_args.len() - num_ctrls); + let ctrls = build_expr_array_expr(ctrl, *mod_span); + args = build_tuple_expr(vec![ctrls, args]); + callee = build_unary_op_expr( + ast::UnOp::Functor(ast::Functor::Ctl), + callee, + *mod_span, + ); + } + GateModifier::NegCtrl(controls, mod_span) => { + // remove the last n qubits from the qubit list + let num_ctrls = controls.unwrap_or(1); + if qubit_args.len() < num_ctrls { + let kind = + SemanticErrorKind::InvalidNumberOfQubitArgs(qargs_len, 0, call_span); + self.push_semantic_error(kind); + return None; + } + let ctrl = qubit_args.split_off(qubit_args.len() - num_ctrls); + let ctrls = build_expr_array_expr(ctrl, *mod_span); + let lit_0 = build_lit_int_expr(0, Span::default()); + args = build_tuple_expr(vec![lit_0, callee, ctrls, args]); + callee = build_path_ident_expr("ApplyControlledOnInt", *mod_span, expr_span); + } + } + } + + self.validate_all_quantum_args_have_been_consumed(&qubit_args, qargs_len, call_span)?; + + let expr = ast_builder::build_gate_call_with_params_and_callee(args, callee, expr_span); + Some(QasmTypedExpr { + ty: Type::Void, + expr, + }) + } + + /// Push if all qubit args have not been consumed. + /// Resurns None for an error, Some(()) for success. + /// This allows short-circuiting of the function. + fn validate_all_quantum_args_have_been_consumed( + &mut self, + qubit_args: &[ast::Expr], + qargs_len: usize, + call_span: Span, + ) -> Option<()> { + // This is a safety check. We should have peeled off all the controls + // but if we haven't, we need to push an error + if qubit_args.is_empty() { + return Some(()); + } + let kind = + SemanticErrorKind::InvalidNumberOfQubitArgs(qargs_len, qubit_args.len(), call_span); + self.push_semantic_error(kind); + None + } + + /// Raises an error if the number of qubit arguments does not match the number + /// of qubit arguments expected by the gate and the number of controls. + fn verify_qubit_args_match_gate_and_ctrls( + &mut self, + qubit_args: &[ast::Expr], + qargs_len: usize, + num_ctrls: u64, + gate_call_expr: &oq3_syntax::ast::GateCallExpr, + ) -> Option<()> { + let gate_call_span = span_for_syntax_node(gate_call_expr.syntax()); + let Some(num_ctrls) = usize::try_from(num_ctrls).ok() else { + let kind = SemanticErrorKind::TooManyControls(gate_call_span); + self.push_semantic_error(kind); + return None; + }; + + if qubit_args.len() != qargs_len + num_ctrls { + let span = if qubit_args.is_empty() { + gate_call_span + } else { + span_for_syntax_node( + gate_call_expr + .qubit_list() + .expect("Qubit list must exist") + .syntax(), + ) + }; + let kind = + SemanticErrorKind::InvalidNumberOfQubitArgs(qargs_len, qubit_args.len(), span); + self.push_semantic_error(kind); + return None; + }; + Some(()) + } + + /// Compiles the gate call qubit arguments. This is a helper function + fn compile_gate_call_quantum_args( + &mut self, + gate_call_expr: &oq3_syntax::ast::GateCallExpr, + ) -> Option> { + let qubit_args: Vec<_> = gate_call_expr + .qubit_list() + .expect("Cannot call a gate without qubit arguments") + .gate_operands() + .map(|op| self.compile_gate_operand(&op).map(|x| x.expr)) + .collect(); + if qubit_args.iter().any(Option::is_none) { + // if any of the qubit arguments failed to compile, we can't proceed + // This can happen if the qubit is not defined or if the qubit was + // a hardware qubit + return None; + } + let qubit_args = qubit_args + .into_iter() + .map(|x| x.expect("All items should have value")) + .collect::>(); + Some(qubit_args) + } + + /// Compiles the gate call classical argument expressions. This is a helper function + fn compile_gate_call_classical_args( + &mut self, + gate_call_expr: &oq3_syntax::ast::GateCallExpr, + cargs_len: usize, + ) -> Option> { + let classical_args = match gate_call_expr.arg_list() { + Some(params) => { + let list = params + .expression_list() + .expect("Arg list must have an expression list"); + + // the classical args list is a list of expressions + // but the type of the args is fixed by the gate definition + // which should always move to float. + let angle_ty = Type::Float(None, IsConst::False); + let exprs = list + .exprs() + .map(|expr| { + self.compile_expr_to_ty_with_casts( + Some(expr), + &angle_ty, + gate_call_expr.syntax(), + ) + }) + .collect::>(); + + if !exprs.iter().all(Option::is_some) { + // There was an issue with one of the expressions + // and an error was pushed + return None; + } + exprs + .into_iter() + .map(|expr| expr.expect("All items should have value")) + .collect::>() + } + None => Vec::new(), + }; + + if classical_args.len() != cargs_len { + let gate_call_span = span_for_syntax_node(gate_call_expr.syntax()); + let span = if classical_args.is_empty() { + gate_call_span + } else { + span_for_syntax_node( + gate_call_expr + .arg_list() + .expect("Qubit list must exist") + .syntax(), + ) + }; + let kind = SemanticErrorKind::InvalidNumberOfClassicalArgs( + cargs_len, + classical_args.len(), + span, + ); + self.push_semantic_error(kind); + return None; + } + Some(classical_args) + } + + /// Compiles the expression list. Returns None if any of the expressions + /// fail to compile. If all expressions compile, returns a vector of + /// the compiled expressions. An error is pushed if any of the expressions + /// fail to compile. + fn compile_expression_list( + &mut self, + expr_list: &oq3_syntax::ast::ExpressionList, + ) -> Option> { + let exprs: Vec<_> = expr_list.exprs().collect(); + let exprs_len = exprs.len(); + let mapped_exprs: Vec<_> = exprs + .into_iter() + .filter_map(|expr| self.compile_expr(&expr)) + .collect(); + if exprs_len == mapped_exprs.len() { + return Some(mapped_exprs); + } + let kind = SemanticErrorKind::FailedToCompileExpressionList(span_for_syntax_node( + expr_list.syntax(), + )); + self.push_semantic_error(kind); + None + } + + /// Compiles the expression list attempting to coerce the expressions to a + /// specific type. + /// Returns None if any of the expressions fail to compile and an error is + /// pushed. If all expressions compile, returns a vector of the compiled + /// expressions. + fn compile_typed_expression_list( + &mut self, + expr_list: &oq3_syntax::ast::ExpressionList, + ty: &Type, + ) -> Option> { + let exprs: Vec<_> = expr_list.exprs().collect(); + let exprs_len = exprs.len(); + let mapped_exprs: Vec<_> = exprs + .into_iter() + .filter_map(|expr| { + self.compile_expr_to_ty_with_casts(Some(expr.clone()), ty, expr.syntax()) + .map(|expr| QasmTypedExpr { + expr, + ty: ty.clone(), + }) + }) + .collect(); + if exprs_len == mapped_exprs.len() { + return Some(mapped_exprs); + } + let kind = SemanticErrorKind::FailedToCompileExpressionList(span_for_syntax_node( + expr_list.syntax(), + )); + self.push_semantic_error(kind); + None + } + + /// Compiles qubit arguments for an instruction call. + fn compile_gate_operand(&mut self, op: &GateOperand) -> Option { + let op_span = span_for_syntax_node(op.syntax()); + match op { + GateOperand::HardwareQubit(hw) => { + // We don't support hardware qubits, so we need to push an error + // but we can still create an identifier for the hardware qubit + // and let the rest of the containing expression compile to + // catch any other errors + let message = "Hardware qubit operands"; + self.push_unsupported_error_message(message, hw.syntax()); + + let name = hw.to_string(); + let name_span = span_for_syntax_node(hw.syntax()); + let ident = build_path_ident_expr(name, name_span, op_span); + Some(QasmTypedExpr { + ty: Type::HardwareQubit, + expr: ident, + }) + } + GateOperand::Identifier(ident) => { + let name = ident.to_string(); + let name_span = span_for_syntax_node(ident.syntax()); + let Some(sym) = self.symbols.get_symbol_by_name(name.as_str()) else { + self.push_missing_symbol_error(name.as_str(), op.syntax()); + return None; + }; + let ty = sym.ty.clone(); + if !matches!(ty, Type::Qubit | Type::QubitArray(_)) { + let kind = SemanticErrorKind::InvalidGateOperand(op_span); + self.push_semantic_error(kind); + } + let ident = build_path_ident_expr(name, name_span, op_span); + Some(QasmTypedExpr { ty, expr: ident }) + } + GateOperand::IndexedIdentifier(indexed_ident) => { + let expr: QasmTypedExpr = self.compile_indexed_identifier_expr(indexed_ident)?; + // the type of the ident may be been Type::QubitArray, but the type of + // the returned expression should be Type::Qubit + if !matches!(expr.ty, Type::Qubit) { + let kind = SemanticErrorKind::InvalidIndexedGateOperand(op_span); + self.push_semantic_error(kind); + } + Some(expr) + } + } + } + fn compile_index_operator( + &mut self, + op: &oq3_syntax::ast::IndexOperator, + ) -> Option> { + match op.index_kind() { + Some(oq3_syntax::ast::IndexKind::SetExpression(expr)) => { + let expr = expr.expression_list()?; + self.compile_expression_list(&expr) + } + Some(oq3_syntax::ast::IndexKind::ExpressionList(expr)) => { + self.compile_expression_list(&expr) + } + None => { + let span = span_for_syntax_node(op.syntax()); + let kind = SemanticErrorKind::UnknownIndexedOperatorKind(span); + self.push_semantic_error(kind); + None + } + } + } + + fn compile_gphase_call_expr( + &mut self, + gphase_call_expr: &oq3_syntax::ast::GPhaseCallExpr, + expr: &Expr, + ) -> Option { + let expr_span = span_for_syntax_node(expr.syntax()); + self.compile_gphase_call_expr_impl(gphase_call_expr, expr_span, &[]) + } + + fn compile_gphase_call_expr_impl( + &mut self, + gphase_call_expr: &oq3_syntax::ast::GPhaseCallExpr, + _expr_span: Span, + _modifiers: &[crate::types::GateModifier], + ) -> Option { + self.push_unimplemented_error_message("gphase expressions", gphase_call_expr.syntax()); + None + } + + fn compile_hardware_qubit_expr( + &mut self, + _hardware_qubit: &oq3_syntax::ast::HardwareQubit, + expr: &Expr, + ) -> Option { + self.push_unsupported_error_message("hardware qubit expressions", expr.syntax()); + None + } + + fn compile_identifier_expr( + &mut self, + identifier: &oq3_syntax::ast::Identifier, + expr: &Expr, + ) -> Option { + let name = identifier.to_string(); + let Some(sym) = self.symbols.get_symbol_by_name(name.as_str()) else { + self.push_missing_symbol_error(&name, expr.syntax()); + return None; + }; + let span = span_for_syntax_node(identifier.syntax()); + let expr_span = span_for_syntax_node(expr.syntax()); + match sym.name.as_str() { + "euler" | "ℇ" => { + let expr = build_math_call_no_params("E", span); + let ty = Type::Float(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + "pi" | "π" => { + let expr = build_math_call_no_params("PI", span); + let ty = Type::Float(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + "tau" | "τ" => { + let expr = build_math_call_no_params("PI", span); + let ty = Type::Float(None, IsConst::True); + let expr = ast::Expr { + kind: Box::new(ast::ExprKind::BinOp( + ast::BinOp::Mul, + Box::new(build_lit_double_expr(2.0, span)), + Box::new(expr), + )), + span, + id: NodeId::default(), + }; + Some(QasmTypedExpr { ty, expr }) + } + _ => { + let expr = build_path_ident_expr(&sym.name, span, expr_span); + let ty = sym.ty.clone(); + Some(QasmTypedExpr { ty, expr }) + } + } + } + + fn compile_index_expr( + &mut self, + index_expr: &oq3_syntax::ast::IndexExpr, + ) -> Option { + let expr = index_expr.expr()?; + let expr_span = span_for_syntax_node(index_expr.syntax()); + let texpr = self.compile_expr(&expr)?; + let index = index_expr.index_operator()?; + let indices = self.compile_index_operator(&index)?; + let index_span = span_for_syntax_node(index.syntax()); + + if indices.len() != 1 { + // This is a temporary limitation. We can only handle + // single index expressions for now. + let kind = SemanticErrorKind::IndexMustBeSingleExpr(index_span); + self.push_semantic_error(kind); + return None; + } + let index = indices[0].clone(); + if index.ty.num_dims() > texpr.ty.num_dims() { + let kind = SemanticErrorKind::TypeRankError(index_span); + self.push_semantic_error(kind); + } + let index_expr = index.expr.clone(); + let Some(indexed_ty) = get_indexed_type(&texpr.ty) else { + let kind = + SemanticErrorKind::CannotIndexType(format!("{:?}", texpr.ty), texpr.expr.span); + self.push_semantic_error(kind); + return None; + }; + + let expr = ast_builder::build_index_expr(texpr.expr, index_expr, expr_span); + Some(QasmTypedExpr { + ty: indexed_ty, + expr, + }) + } + + /// Compiles a indexed expr `a[i]` where `a` is an identifier and `i` is an expression. + /// The type of the expression is determined by the indexed type of the identifier + /// resolved by `self.get_indexed_type`. + fn compile_indexed_identifier_expr( + &mut self, + indexed_ident: &oq3_syntax::ast::IndexedIdentifier, + ) -> Option { + let name = indexed_ident.identifier()?.to_string(); + let name_span = span_for_syntax_node(indexed_ident.syntax()); + let Some(sym) = self.symbols.get_symbol_by_name(name.as_str()) else { + self.push_missing_symbol_error(name.as_str(), indexed_ident.syntax()); + return None; + }; + let sym = sym.clone(); + let op_span = span_for_syntax_node(indexed_ident.syntax()); + + let index: Vec<_> = indexed_ident + .index_operators() + .filter_map(|op| self.compile_index_operator(&op)) + .flatten() + .collect(); + + assert!(index.len() == 1, "index must be a single expression"); + let ident = build_path_ident_expr(name, name_span, op_span); + let expr = ast::Expr { + id: NodeId::default(), + span: span_for_syntax_node(indexed_ident.syntax()), + kind: Box::new(ast::ExprKind::Index( + Box::new(ident), + Box::new(index[0].expr.clone()), + )), + }; + let Some(indexed_ty) = get_indexed_type(&sym.ty) else { + let kind = SemanticErrorKind::CannotIndexType(format!("{:?}", sym.ty), op_span); + self.push_semantic_error(kind); + return None; + }; + Some(QasmTypedExpr { + ty: indexed_ty, + expr, + }) + } + + fn compile_literal_expr( + &mut self, + lit: &oq3_syntax::ast::Literal, + expr: &Expr, + ) -> Option { + let span = span_for_syntax_node(lit.syntax()); + match lit.kind() { + LiteralKind::BitString(bitstring) => compile_bitstring(&bitstring, span), + LiteralKind::Bool(value) => { + let expr = build_lit_bool_expr(value, span); + let ty = Type::Bool(IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + LiteralKind::Byte(_) => { + self.push_unimplemented_error_message("byte literal expressions", expr.syntax()); + None + } + LiteralKind::Char(_) => { + self.push_unimplemented_error_message("char literal expressions", expr.syntax()); + None + } + LiteralKind::FloatNumber(value) => { + let expr = Self::compile_float_literal(&value, span); + let ty = Type::Float(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + LiteralKind::IntNumber(value) => { + let expr = Self::compile_int_literal(&value, span); + let ty = Type::UInt(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + LiteralKind::String(string) => self.compile_string_literal(&string, expr), + } + } + + /// Compiles a complex literal expression from a literal int. + fn compile_int_to_double_literal_to_complex( + &mut self, + value: &oq3_syntax::ast::IntNumber, + span: Span, + ) -> Option { + let value = value.value().expect("FloatNumber must have a value"); + if let Some(value) = safe_u128_to_f64(value) { + Some(build_complex_from_expr(build_lit_double_expr(value, span))) + } else { + let kind = SemanticErrorKind::InvalidCastValueRange( + "Integer".to_string(), + "Double".to_string(), + span, + ); + self.push_semantic_error(kind); + None + } + } + + /// Compiles a double expression from a literal int. + fn compile_int_to_double_literal( + &mut self, + value: &oq3_syntax::ast::IntNumber, + negate: bool, + span: Span, + ) -> Option { + let value = value.value().expect("FloatNumber must have a value"); + if let Some(value) = safe_u128_to_f64(value) { + let value = if negate { -value } else { value }; + Some(build_lit_double_expr(value, span)) + } else { + let kind = SemanticErrorKind::InvalidCastValueRange( + "Integer".to_string(), + "Double".to_string(), + span, + ); + self.push_semantic_error(kind); + None + } + } + + fn compile_int_literal_to_complex( + &mut self, + value: &oq3_syntax::ast::IntNumber, + span: Span, + ) -> Option { + let value = value.value().expect("FloatNumber must have a value"); + if let Some(value) = safe_u128_to_f64(value) { + Some(build_lit_complex_expr( + crate::types::Complex::new(value, 0.0), + span, + )) + } else { + let kind = SemanticErrorKind::InvalidCastValueRange( + "Integer".to_string(), + "Complex real".to_string(), + span, + ); + self.push_semantic_error(kind); + None + } + } + + fn compile_float_literal(value: &oq3_syntax::ast::FloatNumber, span: Span) -> ast::Expr { + build_lit_double_expr(value.value().expect("FloatNumber must have a value"), span) + } + + fn compile_int_literal(value: &oq3_syntax::ast::IntNumber, span: Span) -> ast::Expr { + if let Some(value) = value.value() { + match value.try_into() { + Ok(value) => build_lit_int_expr(value, span), + Err(_) => build_lit_bigint_expr(value.into(), span), + } + } else { + panic!("IntNumber must have a value"); + } + } + + fn compile_string_literal( + &mut self, + _string: &oq3_syntax::ast::String, + expr: &Expr, + ) -> Option { + self.push_unimplemented_error_message("string literal expressions", expr.syntax()); + None + } + + fn compile_timing_literal_expr( + &mut self, + lit: &oq3_syntax::ast::TimingLiteral, + expr: &Expr, + ) -> Option { + self.compile_timing_literal_as_complex(lit, expr, false) + } + + // OpenQASM parser bundles complex numbers with timing literals + // so we have to disambiguate them during timing literal compilation + fn compile_timing_literal_as_complex( + &mut self, + lit: &TimingLiteral, + expr: &Expr, + negate: bool, + ) -> Option { + if let Some(TimeUnit::Imaginary) = lit.time_unit() { + let literal = lit.literal()?; + match literal.kind() { + LiteralKind::FloatNumber(value) => { + let value = value.value().expect("FloatNumber must have a value"); + let value = if negate { -value } else { value }; + let expr = build_lit_complex_expr( + crate::types::Complex::new(0.0, value), + span_for_syntax_node(lit.syntax()), + ); + let ty = Type::Complex(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } + LiteralKind::IntNumber(value) => { + let value = value.value().expect("IntNumber must have a value"); + + if let Some(value) = safe_u128_to_f64(value) { + let value = if negate { -value } else { value }; + let expr = build_lit_complex_expr( + crate::types::Complex::new(0.0, value), + span_for_syntax_node(lit.syntax()), + ); + let ty = Type::Complex(None, IsConst::True); + Some(QasmTypedExpr { ty, expr }) + } else { + let kind = SemanticErrorKind::InvalidCastValueRange( + "Complex imaginary".to_string(), + "Float".to_string(), + span_for_syntax_node(literal.syntax()), + ); + self.push_semantic_error(kind); + None + } + } + _ => { + // parser bug + unreachable!( + "Expected float or int literal, there is a bug in the OpenQASM parser." + ) + } + } + } else { + self.push_unsupported_error_message("Timing literal expressions", expr.syntax()); + None + } + } + + fn compile_measure_expr( + &mut self, + measure_expr: &oq3_syntax::ast::MeasureExpression, + expr: &Expr, + ) -> Option { + let Some(measure_token) = measure_expr.measure_token() else { + let span = span_for_syntax_node(expr.syntax()); + let kind = SemanticErrorKind::MeasureExpressionsMustHaveName(span); + self.push_semantic_error(kind); + return None; + }; + let name_span = span_for_syntax_token(&measure_token); + + let Some(operand) = measure_expr.gate_operand() else { + let span = span_for_syntax_node(expr.syntax()); + let kind = SemanticErrorKind::MeasureExpressionsMustHaveGateOperand(span); + self.push_semantic_error(kind); + return None; + }; + + let args = self.compile_gate_operand(&operand)?; + let operand_span = span_for_syntax_node(operand.syntax()); + let expr = build_measure_call(args.expr, name_span, operand_span); + + Some(QasmTypedExpr { + ty: Type::Bit(IsConst::False), + expr, + }) + } + + fn compile_modified_gate_call_expr( + &mut self, + modified_gate_call_expr: &oq3_syntax::ast::ModifiedGateCallExpr, + ) -> Option { + let expr_span = span_for_syntax_node(modified_gate_call_expr.syntax()); + let modifiers = modified_gate_call_expr + .modifiers() + .map(|modifier| { + let span = span_for_syntax_node(modifier.syntax()); + match modifier { + Modifier::InvModifier(_) => GateModifier::Inv(span), + Modifier::PowModifier(pow_mod) => { + let Some(expr) = pow_mod.paren_expr() else { + let kind = SemanticErrorKind::PowModifierMustHaveExponent(span); + self.push_semantic_error(kind); + return GateModifier::Pow(None, span); + }; + extract_pow_exponent(&expr, span) + } + Modifier::CtrlModifier(ctrl_mod) => { + let ctrls = self.extract_controls_from_modifier(ctrl_mod.paren_expr()); + GateModifier::Ctrl(ctrls, span) + } + Modifier::NegCtrlModifier(neg_ctrl_mod) => { + let ctrls = self.extract_controls_from_modifier(neg_ctrl_mod.paren_expr()); + GateModifier::NegCtrl(ctrls, span) + } + } + }) + .collect::>(); + + if let Some(gate_call_expr) = modified_gate_call_expr.gate_call_expr() { + self.compile_gate_call_expr_impl(&gate_call_expr, expr_span, &modifiers) + } else { + let Some(gphase_call_expr) = modified_gate_call_expr.g_phase_call_expr() else { + // error + return None; + }; + self.compile_gphase_call_expr_impl(&gphase_call_expr, expr_span, &modifiers) + } + } + + /// Extracts the literal int from `ctrl(value)` and `negctrl(value)` modifiers. + /// Returns None if the modifier is invalid and pushes an error. + /// Returns Some(1) if the modifier is empty. + /// Returns Some(value) if the modifier is valid. + fn extract_controls_from_modifier(&mut self, paren_expr: Option) -> Option { + if let Some(paren_expr) = paren_expr { + if let Some((ctrl, sign)) = compile_paren_lit_int_expr(&paren_expr) { + if sign { + let kind = SemanticErrorKind::NegativeControlCount(span_for_syntax_node( + paren_expr.syntax(), + )); + self.push_semantic_error(kind); + } + Some(ctrl) + } else { + let kind = SemanticErrorKind::InvalidControlCount(span_for_syntax_node( + paren_expr.syntax(), + )); + self.push_semantic_error(kind); + None + } + } else { + Some(1) + } + } + + fn compile_paren_expr( + &mut self, + paren_expr: &oq3_syntax::ast::ParenExpr, + ) -> Option { + let span = span_for_syntax_node(paren_expr.syntax()); + let expr = paren_expr.expr()?; + let texpr = self.compile_expr(&expr)?; + let pexpr = ast_builder::wrap_expr_in_parens(texpr.expr, span); + Some(QasmTypedExpr { + ty: texpr.ty.clone(), + expr: pexpr, + }) + } + + #[allow(clippy::too_many_lines)] + fn compile_negated_literal_as_ty( + &mut self, + literal: &Literal, + ty: Option<&Type>, + ) -> Option { + let span = span_for_syntax_node(literal.syntax()); + match literal.kind() { + LiteralKind::IntNumber(value) => match ty { + Some(Type::Float(..)) => { + let expr = self.compile_int_to_double_literal(&value, true, span)?; + Some(QasmTypedExpr { + ty: ty.expect("Expected type").clone(), + expr, + }) + } + _ => Some(compile_intnumber_as_negated_int(&value, span)), + }, + LiteralKind::FloatNumber(value) => match ty { + Some(Type::Int(..) | Type::UInt(..)) => { + let value = value.value().expect("FloatNumber must have a value"); + #[allow(clippy::cast_possible_truncation)] + let converted_value = value.trunc() as i64; + #[allow(clippy::cast_precision_loss)] + if (converted_value as f64 - value).abs() > f64::EPSILON { + let span = span_for_syntax_node(literal.syntax()); + let kind = SemanticErrorKind::CastWouldCauseTruncation( + "Float".to_string(), + format!("{:?}", ty.expect("Expected type")), + span, + ); + self.push_semantic_error(kind); + None + } else { + let expr = build_lit_int_expr(-converted_value, span); + let ty = ty.expect("Expected type").clone(); + Some(QasmTypedExpr { ty, expr }) + } + } + _ => Some(compile_floatnumber_as_negated_double(&value, span)), + }, + _ => { + self.push_unimplemented_error_message( + "negated literal expressions", + literal.syntax(), + ); + None + } + } + } + + #[allow(clippy::too_many_lines)] + fn compile_prefix_expr( + &mut self, + prefix_expr: &oq3_syntax::ast::PrefixExpr, + ) -> Option { + let prefix_span = span_for_syntax_node(prefix_expr.syntax()); + match prefix_expr.op_kind() { + Some(UnaryOp::Neg) => match prefix_expr.expr() { + Some(Expr::Literal(lit)) => self.compile_negated_literal_as_ty(&lit, None), + Some(Expr::TimingLiteral(lit)) => { + let expr = prefix_expr + .expr() + .expect("TimingLiteral must have an expression"); + self.compile_timing_literal_as_complex(&lit, &expr, true) + } + Some(expr) => { + let texpr = self.compile_expr(&expr)?; + let expr = build_unary_op_expr(ast::UnOp::Neg, texpr.expr, prefix_span); + let ty = texpr.ty; + Some(QasmTypedExpr { ty, expr }) + } + None => { + self.push_unimplemented_error_message( + "negated empty expressions", + prefix_expr.syntax(), + ); + None + } + }, + Some(UnaryOp::LogicNot) => { + // bug in QASM parser, logical not and bitwise not are backwards + if let Some(prefix) = prefix_expr.expr() { + let texpr = self.compile_expr(&prefix)?; + let expr = build_unary_op_expr(ast::UnOp::NotB, texpr.expr, prefix_span); + let ty = texpr.ty; + Some(QasmTypedExpr { ty, expr }) + } else { + self.push_unimplemented_error_message( + "bitwise not empty expressions", + prefix_expr.syntax(), + ); + None + } + } + Some(UnaryOp::Not) => { + // bug in QASM parser, logical not and bitwise not are backwards + // THIS CODE IS FOR LOGICAL NOT + let ty = Type::Bool(IsConst::False); + let expr = self.compile_expr_to_ty_with_casts( + prefix_expr.expr(), + &ty, + prefix_expr.syntax(), + )?; + let expr = build_unary_op_expr(ast::UnOp::NotL, expr, prefix_span); + Some(QasmTypedExpr { ty, expr }) + } + None => None, + } + } + + #[allow(clippy::similar_names)] + fn compile_range_expr( + &mut self, + range_expr: &oq3_syntax::ast::RangeExpr, + node: &SyntaxNode, + ) -> Option { + let (start, step, stop) = range_expr.start_step_stop(); + let Some(start) = start else { + let span = span_for_syntax_node(range_expr.syntax()); + let kind = SemanticErrorKind::RangeExpressionsMustHaveStart(span); + self.push_semantic_error(kind); + return None; + }; + let Some(stop) = stop else { + let span = span_for_syntax_node(range_expr.syntax()); + let kind = SemanticErrorKind::RangeExpressionsMustHaveStop(span); + self.push_semantic_error(kind); + return None; + }; + let start_texpr = self.compile_expr(&start)?; + let stop_texpr = self.compile_expr(&stop)?; + let step_texpr = if let Some(step) = step { + Some(self.compile_expr(&step)?.expr) + } else { + None + }; + Some(QasmTypedExpr { + ty: Type::Range, + expr: build_range_expr( + start_texpr.expr, + stop_texpr.expr, + step_texpr, + span_for_syntax_node(node), + ), + }) + } + + fn compile_return_expr( + &mut self, + return_expr: &oq3_syntax::ast::ReturnExpr, + ) -> Option { + let stmt_span = span_for_syntax_node(return_expr.syntax()); + if !self.symbols.is_scope_rooted_in_subroutine() { + let kind = SemanticErrorKind::ReturnNotInSubroutine(stmt_span); + self.push_semantic_error(kind); + } + // the containing function will have an explicit return type + // or default of Void. We don't need to check the return type + // as that will be handled by Q# type checker. If there is no + // expression, we return Unit which Void maps to in Q#. + if let Some(expr) = return_expr.expr() { + let texpr = self.compile_expr(&expr)?; + let expr = ast_builder::build_return_expr(texpr.expr, stmt_span); + Some(QasmTypedExpr { ty: texpr.ty, expr }) + } else { + let expr = ast_builder::build_return_unit(stmt_span); + Some(QasmTypedExpr { + ty: Type::Void, + expr, + }) + } + } + + fn compile_for_stmt(&mut self, for_stmt: &oq3_syntax::ast::ForStmt) -> Option { + let loop_var = for_stmt + .loop_var() + .expect("For statement must have a loop variable"); + let loop_var_span = span_for_syntax_node(loop_var.syntax()); + let loop_var_scalar_ty = for_stmt + .scalar_type() + .expect("For statement must have a scalar type"); + let for_iterable = for_stmt + .for_iterable() + .expect("For statement must have an iterable"); + let stmt_span = span_for_syntax_node(for_stmt.syntax()); + let iterable = self.compile_for_iterable(&for_iterable)?; + let loop_var_sem_ty = + self.get_semantic_type_from_scalar_type(&loop_var_scalar_ty, false)?; + let qsharp_ty = + self.convert_semantic_type_to_qsharp_type(&loop_var_sem_ty, loop_var.syntax())?; + + let loop_var_symbol = Symbol { + name: loop_var.to_string(), + span: loop_var_span, + ty: loop_var_sem_ty.clone(), + qsharp_ty: qsharp_ty.clone(), + io_kind: IOKind::Default, + }; + + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + if self.symbols.insert_symbol(loop_var_symbol.clone()).is_err() { + self.push_redefined_symbol_error(loop_var.to_string(), loop_var_span); + return None; + } + + let body = if let Some(stmt) = for_stmt.stmt() { + let stmt = self.compile_stmt(&stmt); + self.symbols.pop_scope(); + build_stmt_wrapped_block_expr(stmt?) + } else if let Some(block) = for_stmt.body() { + let block = self.compile_block_expr(&block); + self.symbols.pop_scope(); + block + } else { + let span = span_for_syntax_node(for_stmt.syntax()); + let kind = SemanticErrorKind::ForStatementsMustHaveABodyOrStatement(span); + self.push_semantic_error(kind); + None? + }; + + Some(ast_builder::build_for_stmt( + &loop_var_symbol, + iterable, + body, + stmt_span, + )) + } + + fn compile_for_iterable( + &mut self, + for_iterable: &oq3_syntax::ast::ForIterable, + ) -> Option { + if let Some(expr) = for_iterable.set_expression() { + let expr_list = expr.expression_list()?; + + let expression_list = self + .compile_expression_list(&expr_list)? + .into_iter() + .map(|e| e.expr) + .collect(); + + let expr = build_expr_array_expr(expression_list, span_for_syntax_node(expr.syntax())); + Some(QasmTypedExpr { + ty: Type::Set, + expr, + }) + } else if let Some(expr) = for_iterable.range_expr() { + self.compile_range_expr(&expr, for_iterable.syntax()) + } else if let Some(expr) = for_iterable.for_iterable_expr() { + // For iterating over something like bit[n] + self.compile_expr(&expr) + } else { + let span = span_for_syntax_node(for_iterable.syntax()); + let kind = SemanticErrorKind::ForIterableInvalidExpression(span); + self.push_semantic_error(kind); + None + } + } + + #[allow(clippy::too_many_lines)] + fn compile_gate_decl(&mut self, gate: &oq3_syntax::ast::Gate) -> Option { + let name = gate.name()?; + + //let angle_ty = Type::Angle(None, IsConst::True); + let angle_ty = Type::Float(None, IsConst::True); + let qs_angle_ty = self.convert_semantic_type_to_qsharp_type(&angle_ty, gate.syntax())?; + let ast_angle_ty = map_qsharp_type_to_ast_ty(&qs_angle_ty); + let qubit_ty = Type::Qubit; + let qs_qubit_ty = self.convert_semantic_type_to_qsharp_type(&qubit_ty, gate.syntax())?; + let ast_qubit_ty = map_qsharp_type_to_ast_ty(&qs_qubit_ty); + + let gate_span = span_for_syntax_node(gate.syntax()); + if !self.symbols.is_current_scope_global() { + let kind = SemanticErrorKind::QuantumDeclarationInNonGlobalScope(gate_span); + self.push_semantic_error(kind); + return None; + } + // get the name of the args and their spans + let cargs = gate + .angle_params() + .iter() + .flat_map(ParamList::params) + .map(|e| { + ( + e.text().to_string(), + ast_angle_ty.clone(), + build_arg_pat( + e.text().to_string(), + span_for_named_item(&e), + ast_angle_ty.clone(), + ), + ) + }) + .collect::>(); + + let qargs = gate + .qubit_params() + .iter() + .flat_map(ParamList::params) + .map(|e| { + ( + e.text().to_string(), + ast_qubit_ty.clone(), + build_arg_pat( + e.text().to_string(), + span_for_named_item(&e), + ast_qubit_ty.clone(), + ), + ) + }) + .collect::>(); + + self.symbols.push_scope(crate::symbols::ScopeKind::Gate); + // bind the cargs, qargs and body + + for (name, _, pat) in &cargs { + let symbol = Symbol { + name: name.clone(), + span: pat.span, + ty: angle_ty.clone(), + qsharp_ty: qs_angle_ty.clone(), + io_kind: IOKind::Default, + }; + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name, pat.span); + } + } + for (name, _, pat) in &qargs { + let symbol = Symbol { + name: name.clone(), + span: pat.span, + ty: qubit_ty.clone(), + qsharp_ty: qs_qubit_ty.clone(), + io_kind: IOKind::Default, + }; + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name, pat.span); + } + } + let body = gate.body().map(|body| self.compile_block_expr(&body)); + let body_span = gate + .body() + .map(|body| span_for_syntax_node(body.syntax())) + .unwrap_or_default(); + self.symbols.pop_scope(); + // create a gate symbol with type information with num cargs and qargs + + let gate_ty = Type::Gate(cargs.len(), qargs.len()); + let qs_gate_ty = self.convert_semantic_type_to_qsharp_type(&gate_ty, gate.syntax())?; + let name_span = span_for_syntax_node(name.syntax()); + let symbol = Symbol { + name: name.to_string(), + span: name_span, + ty: gate_ty, + qsharp_ty: qs_gate_ty, + io_kind: IOKind::Default, + }; + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name.to_string(), span_for_syntax_node(name.syntax())); + return None; + } + + if self.next_gate_as_item { + Some(ast_builder::build_gate_decl( + name.to_string(), + cargs, + qargs, + body, + name_span, + body_span, + gate_span, + )) + } else { + Some(build_gate_decl_lambda( + name.to_string(), + cargs, + qargs, + body, + name_span, + body_span, + gate_span, + )) + } + } + + fn compile_if_stmt(&mut self, if_stmt: &oq3_syntax::ast::IfStmt) -> Option { + let stmt_span = span_for_syntax_node(if_stmt.syntax()); + + let Some(condition) = if_stmt.condition() else { + let kind = + SemanticErrorKind::IfStmtMissingExpression("condition".to_string(), stmt_span); + self.push_semantic_error(kind); + return None; + }; + let node = condition.syntax(); + let cond_ty = Type::Bool(IsConst::False); + let cond = self + .compile_expr_to_ty_with_casts(Some(condition.clone()), &cond_ty, node) + .map(|expr| QasmTypedExpr { ty: cond_ty, expr }); + + let Some(block) = if_stmt.then_branch() else { + let kind = + SemanticErrorKind::IfStmtMissingExpression("then block".to_string(), stmt_span); + self.push_semantic_error(kind); + return None; + }; + + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + let then_block = self.compile_block_expr(&block); + self.symbols.pop_scope(); + let else_block = if_stmt.else_branch().map(|block_expr| { + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + let else_expr = self.compile_block_expr(&block_expr); + self.symbols.pop_scope(); + else_expr + }); + + // The cond may have failed to compile, in which case we return None + // we let it go this far so that we could accumulate any errors in + // the block. + let cond = cond?; + let if_expr = if let Some(else_block) = else_block { + build_if_expr_then_block_else_block(cond.expr, then_block, else_block, stmt_span) + } else { + build_if_expr_then_block(cond.expr, then_block, stmt_span) + }; + + Some(build_stmt_semi_from_expr(if_expr)) + } + + fn compile_io_decl_stmt( + &mut self, + decl: &oq3_syntax::ast::IODeclarationStatement, + ) -> Option { + if decl.array_type().is_some() { + self.push_unimplemented_error_message("array io declarations", decl.syntax()); + return None; + } + let name = decl.name().expect("io declaration must have a name"); + let scalar_ty = decl + .scalar_type() + .expect("io declaration must have a scalar type"); + let io_kind = match decl.input_token() { + Some(_) => IOKind::Input, + None => IOKind::Output, + }; + // if we can't convert the scalar type, we can't proceed, an error has been pushed + let ty = self.get_semantic_type_from_scalar_type(&scalar_ty, false)?; + let qsharp_ty = self.convert_semantic_type_to_qsharp_type(&ty, name.syntax())?; + let symbol = Symbol { + name: name.to_string(), + span: span_for_syntax_node(name.syntax()), + ty: ty.clone(), + qsharp_ty: qsharp_ty.clone(), + io_kind: io_kind.clone(), + }; + + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name.to_string(), span_for_syntax_node(name.syntax())); + return None; + } + + // if we have output, we need to assign a default value to declare the variable + // if we have input, we can keep return none as we would promote the variable + // to a parameter in the function signature once we generate the function + if io_kind == IOKind::Output { + let rhs = self.get_default_value(&ty, name.syntax())?; + let stmt = build_classical_decl( + name.to_string(), + false, + span_for_syntax_node(scalar_ty.syntax()), + span_for_syntax_node(decl.syntax()), + span_for_syntax_node(name.syntax()), + &qsharp_ty, + rhs, + ); + Some(stmt) + } else { + None + } + } + + /// Let statements shouldn't make it into parsing + /// Looking at the parser, this statement seems + /// anachronistic and should be removed from the parser + fn compile_let_stmt(&mut self, let_stmt: &oq3_syntax::ast::LetStmt) -> Option { + self.push_unsupported_error_message("let statements", let_stmt.syntax()); + None + } + + /// Measure statements shouldn't make it into parsing + /// Looking at the parser, this statement seems + /// anachronistic and should be removed from the parser + fn compile_measure_stmt(&mut self, measure: &oq3_syntax::ast::Measure) -> Option { + self.push_unsupported_error_message("measure statements", measure.syntax()); + None + } + + fn compile_quantum_decl( + &mut self, + decl: &oq3_syntax::ast::QuantumDeclarationStatement, + ) -> Option { + let decl_span = span_for_syntax_node(decl.syntax()); + if !self.symbols.is_current_scope_global() { + let kind = SemanticErrorKind::QuantumDeclarationInNonGlobalScope(decl_span); + self.push_semantic_error(kind); + return None; + } + let qubit_ty = decl + .qubit_type() + .expect("Quantum declaration must have a qubit type"); + let name = decl.name().expect("Quantum declaration must have a name"); + + let designator = match qubit_ty.designator() { + Some(designator) => { + let width_span = span_for_syntax_node(designator.syntax()); + let width = extract_dims_from_designator(Some(designator)) + .expect("Designator must be a literal integer"); + + Some((width, width_span)) + } + None => None, + }; + let ty = if let Some((width, _)) = designator { + Type::QubitArray(ArrayDims::D1(width as usize)) + } else { + Type::Qubit + }; + let qsharp_ty = self.convert_semantic_type_to_qsharp_type(&ty, name.syntax())?; + let symbol = Symbol { + name: name.to_string(), + span: span_for_syntax_node(name.syntax()), + ty, + qsharp_ty, + io_kind: IOKind::Default, + }; + + if self.symbols.insert_symbol(symbol).is_err() { + self.push_redefined_symbol_error(name.to_string(), span_for_syntax_node(name.syntax())); + return None; + } + let name = name.to_string(); + let name_span = span_for_named_item(decl); + let stmt = match self.config.qubit_semantics { + QubitSemantics::QSharp => { + if let Some((width, designator_span)) = designator { + managed_qubit_alloc_array(name, width, decl_span, name_span, designator_span) + } else { + build_managed_qubit_alloc(name, decl_span, name_span) + } + } + QubitSemantics::Qiskit => { + if let Some((width, span)) = designator { + build_unmanaged_qubit_alloc_array(name, width, decl_span, name_span, span) + } else { + build_unmanaged_qubit_alloc(name, decl_span, name_span) + } + } + }; + Some(stmt) + } + + fn compile_reset_call(&mut self, expr: &oq3_syntax::ast::Reset) -> Option { + let Some(token) = expr.reset_token() else { + let span = span_for_syntax_node(expr.syntax()); + let kind = SemanticErrorKind::ResetExpressionMustHaveName(span); + self.push_semantic_error(kind); + return None; + }; + let name_span = span_for_syntax_token(&token); + + let Some(operand) = expr.gate_operand() else { + let span = span_for_syntax_node(expr.syntax()); + let kind = SemanticErrorKind::ResetExpressionMustHaveGateOperand(span); + self.push_semantic_error(kind); + return None; + }; + let args = self.compile_gate_operand(&operand)?; + let operand_span = span_for_syntax_node(operand.syntax()); + let expr = build_reset_call(args.expr, name_span, operand_span); + + Some(build_stmt_semi_from_expr(expr)) + } + + fn compile_switch_stmt( + &mut self, + switch_case: &oq3_syntax::ast::SwitchCaseStmt, + ) -> Option { + // The condition for the switch statement must be an integer type + // so instead of using `compile_expr` we use `resolve_rhs_expr_with_casts` + // forcing the type to be an integer type with implicit casts if necessary + let cond_ty = Type::Int(None, IsConst::False); + // We try to compile all expressions first to accumulate errors + let control = switch_case.control().and_then(|control| { + self.compile_expr_to_ty_with_casts(Some(control), &cond_ty, switch_case.syntax()) + }); + let cases: Vec<_> = switch_case + .case_exprs() + .map(|case| { + let lhs = case + .expression_list() + .and_then(|expr| self.compile_typed_expression_list(&expr, &cond_ty)); + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + let rhs = case + .block_expr() + .map(|block| self.compile_block_expr(&block)); + self.symbols.pop_scope(); + (lhs, rhs) + }) + .collect(); + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + let default_block = switch_case + .default_block() + .map(|block| self.compile_block_expr(&block)); + self.symbols.pop_scope(); + + // at this point we tried to compile everything, bail if we have any errors + if control.is_none() + || cases + .iter() + .any(|(lhs, rhs)| lhs.is_none() || rhs.is_none()) + || cases.is_empty() + { + // See tests, but it is a parse error to have a switch statement with + // no cases, even if the default block is present. Getting here means + // the parser is broken or they changed the grammar. Either way, an + // empty switch is a noop + return None; + } + + // update bindings based on what we checked above + let control = control?; + let cases: Vec<_> = cases + .into_iter() + .map(|(lhs, rhs)| { + let lhs = lhs.expect("Case must have a lhs"); + let rhs = rhs.expect("Case must have a rhs"); + (lhs, rhs) + }) + .collect(); + + // Semantics of switch case is that the outer block doesn't introduce + // a new scope but each case rhs does. + + // Can we add a new scope anyway to hold a temporary variable? + // if we do that, we can refer to a new variable instead of the control + // expr this would allow us to avoid the need to resolve the control + // expr multiple times in the case where we have to coerce the control + // expr to the correct type. Introducing a new variable without a new + // scope would effect output semantics. + + // For each case, convert the lhs into a sequence of equality checks + // and then fold them into a single expression of logical ors for + // the if expr + let cases: Vec<_> = cases + .into_iter() + .map(|(lhs, rhs)| { + let case = lhs + .iter() + .map(|texpr| { + ast_builder::build_binary_expr( + false, + ast::BinOp::Eq, + control.clone(), + texpr.expr.clone(), + texpr.expr.span, + ) + }) + .fold(None, |acc, expr| match acc { + None => Some(expr), + Some(acc) => { + let qsop = ast::BinOp::OrL; + let span = Span { + lo: acc.span.lo, + hi: expr.span.hi, + }; + Some(build_binary_expr(false, qsop, acc, expr, span)) + } + }); + // The type checker doesn't know that we have at least one case + // so we have to unwrap here since the accumulation is guaranteed + // to have Some(value) + let case = case.expect("Case must have at least one expression"); + (case, rhs) + }) + .collect(); + + // cond is resolved, cases are resolved, default is resolved + // we can now build the if expression backwards. The default block + // is the last else block, the last case is the then block, and the rest + // are built as if-else blocks with the last case as the else block + let default_expr = default_block.map(build_wrapped_block_expr); + let if_expr = cases + .into_iter() + .rev() + .fold(default_expr, |else_expr, (cond, block)| { + let span = Span { + lo: cond.span.lo, + hi: block.span.hi, + }; + Some(build_if_expr_then_block_else_expr( + cond, block, else_expr, span, + )) + }); + if_expr.map(build_stmt_semi_from_expr) + } + + // This is a no-op in Q# but we will save it for future use + fn compile_version_stmt( + &mut self, + version: &oq3_syntax::ast::VersionString, + ) -> Option { + if let Some(version) = version.version() { + let version_str = format!("{version}"); + if !version_str.starts_with("3.") { + self.push_unsupported_error_message( + "OpenQASM versions other than 3", + version.syntax(), + ); + } + self.version = Some(version_str); + } + None + } + + /// Note: From the ``OpenQASM`` 3.0 specification: + /// This clearly allows users to write code that does not terminate. + /// We do not discuss implementation details here, but one possibility + /// is to compile into target code that imposes iteration limits. + fn compile_while_stmt(&mut self, while_stmt: &oq3_syntax::ast::WhileStmt) -> Option { + let stmt_span = span_for_syntax_node(while_stmt.syntax()); + let Some(condition) = while_stmt.condition() else { + let kind = + SemanticErrorKind::WhileStmtMissingExpression("condition".to_string(), stmt_span); + self.push_semantic_error(kind); + return None; + }; + + let node = condition.syntax(); + let cond_ty = Type::Bool(IsConst::False); + let cond = self + .compile_expr_to_ty_with_casts(Some(condition.clone()), &cond_ty, node) + .map(|expr| QasmTypedExpr { ty: cond_ty, expr }); + + // if cond is none, an error was pushed + // or the expression couldn't be resolved + // We keep going to catch more errors but only if the condition + // expression can be compiled + let cond = match cond { + Some(cond) => cond, + None => self.compile_expr(&condition)?, + }; + + let Some(block) = while_stmt.body() else { + let kind = SemanticErrorKind::WhileStmtMissingExpression("body".to_string(), stmt_span); + self.push_semantic_error(kind); + return None; + }; + + self.symbols.push_scope(crate::symbols::ScopeKind::Block); + let block_body = self.compile_block_expr(&block); + self.symbols.pop_scope(); + Some(ast_builder::build_while_stmt( + cond.expr, block_body, stmt_span, + )) + } + + fn convert_semantic_type_to_qsharp_type( + &mut self, + ty: &Type, + node: &SyntaxNode, + ) -> Option { + let is_const = ty.is_const(); + match ty { + Type::Bit(_) => Some(crate::types::Type::Result(is_const)), + Type::Qubit => Some(crate::types::Type::Qubit), + Type::HardwareQubit => unimplemented!("HardwareQubit to Q# type"), + Type::Int(width, _) | Type::UInt(width, _) => { + if let Some(width) = width { + if *width > 64 { + Some(crate::types::Type::BigInt(is_const)) + } else { + Some(crate::types::Type::Int(is_const)) + } + } else { + Some(crate::types::Type::Int(is_const)) + } + } + Type::Float(_, _) | Type::Angle(_, _) => Some(crate::types::Type::Double(is_const)), + Type::Complex(_, _) => Some(crate::types::Type::Complex(is_const)), + Type::Bool(_) => Some(crate::types::Type::Bool(is_const)), + Type::Duration(_) => { + self.push_unsupported_error_message("Duration type values", node); + None + } + Type::Stretch(_) => { + self.push_unsupported_error_message("Stretch type values", node); + None + } + Type::BitArray(dims, _) => Some(crate::types::Type::ResultArray(dims.into(), is_const)), + Type::QubitArray(dims) => Some(crate::types::Type::QubitArray(dims.into())), + Type::IntArray(dims) | Type::UIntArray(dims) => { + Some(crate::types::Type::IntArray(dims.into(), is_const)) + } + Type::FloatArray(dims) => Some(crate::types::Type::DoubleArray(dims.into())), + Type::AngleArray(_) => todo!("AngleArray to Q# type"), + Type::ComplexArray(_) => todo!("ComplexArray to Q# type"), + Type::BoolArray(dims) => Some(crate::types::Type::BoolArray(dims.into(), is_const)), + Type::Gate(cargs, qargs) => Some(crate::types::Type::Callable( + crate::types::CallableKind::Operation, + *cargs, + *qargs, + )), + Type::Range => Some(crate::types::Type::Range), + Type::Set => todo!("Set to Q# type"), + Type::Void => Some(crate::types::Type::Tuple(vec![])), + _ => { + let msg = format!("Converting {ty:?} to Q# type"); + self.push_unimplemented_error_message(msg, node); + None + } + } + } + + fn get_default_value(&mut self, ty: &Type, node: &SyntaxNode) -> Option { + let span = span_for_syntax_node(node); + match ty { + Type::Bit(_) => Some(build_lit_result_expr(ast::Result::Zero, span)), + Type::Qubit => unimplemented!("Qubit default values"), + Type::HardwareQubit => unimplemented!("HardwareQubit default values"), + Type::Int(width, _) | Type::UInt(width, _) => { + if let Some(width) = width { + if *width > 64 { + Some(build_lit_bigint_expr(BigInt::from(0), span)) + } else { + Some(build_lit_int_expr(0, span)) + } + } else { + Some(build_lit_int_expr(0, span)) + } + } + Type::Float(_, _) => Some(build_lit_double_expr(0.0, span)), + Type::Angle(_, _) => todo!("Angle default values"), + Type::Complex(_, _) => Some(build_lit_complex_expr( + crate::types::Complex::new(0.0, 0.0), + span, + )), + Type::Bool(_) => Some(build_lit_bool_expr(false, span)), + Type::Duration(_) => { + self.push_unsupported_error_message( + "Duration type values are not supported.", + node, + ); + None + } + Type::Stretch(_) => { + self.push_unsupported_error_message("Stretch type values are not supported.", node); + None + } + Type::BitArray(dims, _) => match dims { + ArrayDims::D1(len) => Some(build_default_result_array_expr(*len, span)), + ArrayDims::D2(_, _) => { + self.push_unsupported_error_message( + "2-dim Bit Arrays without default values", + node, + ); + None + } + ArrayDims::D3(_, _, _) => { + self.push_unsupported_error_message( + "3-dim Bit Arrays without default values", + node, + ); + None + } + }, + Type::QubitArray(_) => unimplemented!("QubitArray default values"), + Type::IntArray(_) + | Type::UIntArray(_) + | Type::FloatArray(_) + | Type::AngleArray(_) + | Type::ComplexArray(_) + | Type::BoolArray(_) => { + self.push_unsupported_error_message("Arrays without default values", node); + None + } + Type::DurationArray(_) => { + self.push_unsupported_error_message( + "DurationArray type values are not supported.", + node, + ); + None + } + Type::Gate(_, _) => unimplemented!("Gate default values"), + Type::Range => unimplemented!("Range default values"), + Type::Set => unimplemented!("Set default values"), + Type::Void => unimplemented!("Void default values"), + Type::ToDo => unimplemented!("ToDo default values"), + Type::Undefined => unimplemented!("Undefined default values"), + } + } + + #[allow(clippy::too_many_lines)] + fn define_stdgates(&mut self, include: &oq3_syntax::ast::Include) { + // TODO: sdg - Adjoint S + // TODO: tdg - Adjoint T + + let gates = vec![ + Symbol { + name: "X".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "Y".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "Z".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "H".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "S".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "T".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "Rx".to_string(), + ty: Type::Gate(1, 1), + ..Default::default() + }, + Symbol { + name: "Rxx".to_string(), + ty: Type::Gate(1, 2), + ..Default::default() + }, + Symbol { + name: "Ry".to_string(), + ty: Type::Gate(1, 1), + ..Default::default() + }, + Symbol { + name: "Ryy".to_string(), + ty: Type::Gate(1, 2), + ..Default::default() + }, + Symbol { + name: "Rz".to_string(), + ty: Type::Gate(1, 1), + ..Default::default() + }, + Symbol { + name: "Rzz".to_string(), + ty: Type::Gate(1, 2), + ..Default::default() + }, + Symbol { + name: "CNOT".to_string(), + ty: Type::Gate(0, 2), + ..Default::default() + }, + Symbol { + name: "CY".to_string(), + ty: Type::Gate(0, 2), + ..Default::default() + }, + Symbol { + name: "CZ".to_string(), + ty: Type::Gate(0, 2), + ..Default::default() + }, + Symbol { + name: "CH".to_string(), + ty: Type::Gate(0, 2), + ..Default::default() + }, + Symbol { + name: "I".to_string(), + ty: Type::Gate(0, 1), + ..Default::default() + }, + Symbol { + name: "SWAP".to_string(), + ty: Type::Gate(0, 2), + ..Default::default() + }, + Symbol { + name: "CCNOT".to_string(), + ty: Type::Gate(0, 3), + ..Default::default() + }, + ]; + for gate in gates { + let name = gate.name.clone(); + if self.symbols.insert_symbol(gate).is_err() { + self.push_redefined_symbol_error( + name.as_str(), + span_for_syntax_node(include.syntax()), + ); + } + } + } + + fn get_semantic_type_from_scalar_type( + &mut self, + scalar_ty: &oq3_syntax::ast::ScalarType, + is_const: bool, + ) -> Option { + let designator = get_designator_from_scalar_type(scalar_ty); + let is_const = is_const.into(); + let width = if let Some(designator) = designator { + match designator.expr() { + Some(oq3_syntax::ast::Expr::Literal(ref literal)) => match literal.kind() { + oq3_syntax::ast::LiteralKind::IntNumber(int_num) => { + let size: u32 = u32::try_from(int_num.value()?).ok()?; + Some(size) + } + _ => None, + }, + Some(expr) => { + let span = span_for_syntax_node(expr.syntax()); + let kind = SemanticErrorKind::DesignatorMustBeIntLiteral(span); + self.push_semantic_error(kind); + return None; + } + None => None, + } + } else { + None + }; + + let ty = match scalar_ty.kind() { + oq3_syntax::ast::ScalarTypeKind::Angle => { + oq3_semantics::types::Type::Angle(width, is_const) + } + oq3_syntax::ast::ScalarTypeKind::Bit => match width { + Some(width) => { + oq3_semantics::types::Type::BitArray(ArrayDims::D1(width as usize), is_const) + } + None => oq3_semantics::types::Type::Bit(is_const), + }, + oq3_syntax::ast::ScalarTypeKind::Bool => oq3_semantics::types::Type::Bool(is_const), + oq3_syntax::ast::ScalarTypeKind::Complex => { + oq3_semantics::types::Type::Complex(width, is_const) + } + oq3_syntax::ast::ScalarTypeKind::Duration => { + oq3_semantics::types::Type::Duration(is_const) + } + oq3_syntax::ast::ScalarTypeKind::Float => { + oq3_semantics::types::Type::Float(width, is_const) + } + oq3_syntax::ast::ScalarTypeKind::Int => { + oq3_semantics::types::Type::Int(width, is_const) + } + oq3_syntax::ast::ScalarTypeKind::Qubit => match width { + Some(width) => { + oq3_semantics::types::Type::QubitArray(ArrayDims::D1(width as usize)) + } + None => oq3_semantics::types::Type::Qubit, + }, + oq3_syntax::ast::ScalarTypeKind::Stretch => { + oq3_semantics::types::Type::Stretch(is_const) + } + oq3_syntax::ast::ScalarTypeKind::UInt => { + oq3_semantics::types::Type::UInt(width, is_const) + } + oq3_syntax::ast::ScalarTypeKind::None => { + let msg = "ScalarTypeKind::None should have been handled by the parser".to_string(); + let span = span_for_syntax_node(scalar_ty.syntax()); + let kind = SemanticErrorKind::UnexpectedParserError(msg, span); + self.push_semantic_error(kind); + return None; + } + }; + Some(ty) + } + + fn try_cast_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + node: &SyntaxNode, + ) -> Option { + if *ty == rhs.ty { + // Base case, we shouldn't have gotten here + // but if we did, we can just return the rhs + return Some(rhs.clone()); + } + if types_equal_except_const(ty, &rhs.ty) { + if rhs.ty.is_const() { + // lhs isn't const, but rhs is, we can just return the rhs + return Some(rhs.clone()); + } + // the lsh is supposed to be const but is being initialized + // to a non-const value, we can't allow this + return None; + } + // if the target type is wider, we can try to relax the rhs type + // We only do this for float and complex. Int types + // require extra handling for BigInts + match (ty, &rhs.ty) { + (Type::Float(w1, _), Type::Float(w2, _)) + | (Type::Complex(w1, _), Type::Complex(w2, _)) => { + if w1.is_none() && w2.is_some() { + return Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }); + } + + if *w1 >= *w2 { + return Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }); + } + } + _ => {} + } + // Casting of literals is handled elsewhere. This is for casting expressions + // which cannot be bypassed and must be handled by built-in Q# casts from + // the standard library. + match &rhs.ty { + Type::Angle(_, _) => self.cast_angle_expr_to_type(ty, rhs, node), + Type::Bit(_) => self.cast_bit_expr_to_type(ty, rhs, node), + Type::Bool(_) => self.cast_bool_expr_to_type(ty, rhs), + Type::Complex(_, _) => cast_complex_expr_to_type(ty, rhs), + Type::Float(_, _) => self.cast_float_expr_to_type(ty, rhs, node), + Type::Int(_, _) | Type::UInt(_, _) => self.cast_int_expr_to_type(ty, rhs), + Type::BitArray(dims, _) => self.cast_bitarray_expr_to_type(dims, ty, rhs), + _ => None, + } + } + + fn cast_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + node: &SyntaxNode, + ) -> Option { + let cast_expr = self.try_cast_expr_to_type(ty, rhs, node); + if cast_expr.is_none() { + let rhs_ty_name = format!("{:?}", rhs.ty); + let lhs_ty_name = format!("{ty:?}"); + let span = span_for_syntax_node(node); + let kind = SemanticErrorKind::CannotCast(rhs_ty_name, lhs_ty_name, span); + self.push_semantic_error(kind); + } + cast_expr + } + + #[allow(clippy::too_many_lines)] + fn cast_literal_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + literal: &Literal, + ) -> Option { + if *ty == rhs.ty { + // Base case, we shouldn't have gotten here + // but if we did, we can just return the rhs + return Some(rhs.clone()); + } + if types_equal_except_const(ty, &rhs.ty) { + if rhs.ty.is_const() { + // lhs isn't const, but rhs is, we can just return the rhs + return Some(rhs.clone()); + } + // the lsh is supposed to be const but is being initialized + // to a non-const value, we can't allow this + return None; + } + assert!( + can_cast_literal(ty, &rhs.ty) || can_cast_literal_with_value_knowledge(ty, literal) + ); + let lhs_ty = ty.clone(); + let rhs_ty = rhs.ty.clone(); + let span = rhs.expr.span; + + if matches!(lhs_ty, Type::Bit(..)) { + if let LiteralKind::IntNumber(value) = literal.kind() { + return compile_intnumber_as_bit(&value, span, ty); + } else if let LiteralKind::Bool(value) = literal.kind() { + let expr = build_lit_result_expr(value.into(), span); + return Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }); + } + } + // if lhs_ty is 1 dim bitarray and rhs is int/uint, we can cast + let (is_int_to_bit_array, size) = match &lhs_ty { + Type::BitArray(dims, _) => { + if matches!(rhs.ty, Type::Int(..) | Type::UInt(..)) { + match dims { + ArrayDims::D1(size) => (true, *size), + _ => (false, 0), + } + } else { + (false, 0) + } + } + _ => (false, 0), + }; + if is_int_to_bit_array { + if let LiteralKind::IntNumber(value) = literal.kind() { + // Value can't be negative as IntNumber is unsigned + // any sign would come from a prefix expression + if let Some(value) = value.value() { + if let Ok(value) = value.try_into() { + let value: i64 = value; + if value >= 0 && value < (1 << size) { + let bitstring = format!("{value:0size$b}"); + let expr = build_lit_result_array_expr_from_bitstring(bitstring, span); + return Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }); + } + return None; + } + } + } + } + if matches!(lhs_ty, Type::UInt(..)) { + if let LiteralKind::IntNumber(value) = literal.kind() { + // Value can't be negative as IntNumber is unsigned + // any sign would come from a prefix expression + if let Some(value) = value.value() { + if let Ok(value) = value.try_into() { + let value: i64 = value; + let expr = build_lit_int_expr(value, span); + let ty = Type::Int(None, IsConst::True); + return Some(QasmTypedExpr { ty, expr }); + } + } + } + } + let result = match (&lhs_ty, &rhs_ty) { + (Type::Float(..), Type::Int(..) | Type::UInt(..)) => { + // the qasm type is int/uint, but the expr will be q# int + if let LiteralKind::IntNumber(value) = literal.kind() { + let expr = self.compile_int_to_double_literal(&value, false, span)?; + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + panic!("Literal must be an IntNumber") + } + } + (Type::Float(..), Type::Float(..)) => { + if let LiteralKind::FloatNumber(value) = literal.kind() { + let value = value.value().expect("FloatNumber must have a value"); + let expr = build_lit_double_expr(value, span); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + panic!("Literal must be an FloatNumber") + } + } + (Type::Complex(..), Type::Float(..)) => { + let expr = build_complex_from_expr(rhs.expr.clone()); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + (Type::Complex(..), Type::Int(..) | Type::UInt(..)) => { + // complex requires a double as input, so we need to + // convert the int to a double, then create the complex + if let LiteralKind::IntNumber(value) = literal.kind() { + if let Some(expr) = self.compile_int_to_double_literal_to_complex(&value, span) + { + return Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }); + } + } + panic!("Literal must be an IntNumber") + } + (Type::Bit(..), Type::Int(..) | Type::UInt(..)) => { + // we've already checked that the value is 0 or 1 + if let LiteralKind::IntNumber(value) = literal.kind() { + let value = value.value().expect("IntNumber must have a value"); + if value == 0 || value == 1 { + let expr = build_lit_result_expr((value == 1).into(), rhs.expr.span); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + panic!("Value must be 0 or 1"); + } + } else { + panic!("Literal must be an IntNumber"); + } + } + (Type::Int(..), Type::Int(..) | Type::UInt(..)) => { + // we've already checked that this conversion can happen + if let LiteralKind::IntNumber(value) = literal.kind() { + let value = value.value().expect("IntNumber must have a value"); + let expr = if let Ok(value) = value.try_into() { + let value: i64 = value; + build_lit_int_expr(value, span) + } else { + build_lit_bigint_expr(BigInt::from(value), span) + }; + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + panic!("Literal must be an IntNumber"); + } + } + _ => None, + }; + if result.is_none() { + // we assert that the type can be casted + // but we didn't cast it, so this is a bug + panic!("Literal type cast failed lhs: {:?}, rhs: {:?}", ty, rhs.ty); + } else { + result + } + } + + fn create_entry_operation>( + &mut self, + name: S, + whole_span: Span, + ) -> (ast::Item, OperationSignature) { + let stmts = self.drain_nodes().collect::>(); + let input = self.symbols.get_input(); + let output = self.symbols.get_output(); + self.create_entry_item( + name, + stmts, + input, + output, + whole_span, + self.config.output_semantics, + ) + } + + /// +----------------+-------------------------------------------------------------+ + /// | Allowed casts | Casting To | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | angle | Yes | No | No | No | - | Yes | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + fn cast_angle_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + node: &SyntaxNode, + ) -> Option { + assert!(matches!(rhs.ty, Type::Bit(..))); + match ty { + Type::Bit(..) => { + let msg = "Cast angle to bit"; + self.push_unimplemented_error_message(msg, node); + None + } + Type::Bool(..) => { + let msg = "Cast angle to bool"; + self.push_unimplemented_error_message(msg, node); + None + } + + _ => None, + } + } + + /// +----------------+-------------------------------------------------------------+ + /// | Allowed casts | Casting To | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | bit | Yes | Yes | Yes | No | Yes | - | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + fn cast_bit_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + node: &SyntaxNode, + ) -> Option { + assert!(matches!(rhs.ty, Type::Bit(..))); + // There is no operand, choosing the span of the node + // but we could use the expr span as well. + let operand_span = span_for_syntax_node(node); + let name_span = rhs.expr.span; + match ty { + &Type::Angle(..) => { + let msg = "Cast bit to angle"; + self.push_unimplemented_error_message(msg, node); + None + } + &Type::Bool(..) => { + self.runtime |= RuntimeFunctions::ResultAsBool; + Some(QasmTypedExpr { + ty: ty.clone(), + expr: build_cast_call( + RuntimeFunctions::ResultAsBool, + rhs.expr.clone(), + name_span, + operand_span, + ), + }) + } + &Type::Float(..) => { + // The spec says that this cast isn't supported, but it + // casts to other types that case to float. For now, we'll + // say it is invalid like the spec. + None + } + &Type::Int(w, _) | &Type::UInt(w, _) => { + let function = if let Some(width) = w { + if width > 64 { + RuntimeFunctions::ResultAsBigInt + } else { + RuntimeFunctions::ResultAsInt + } + } else { + RuntimeFunctions::ResultAsInt + }; + self.runtime |= function; + let expr = build_cast_call(function, rhs.expr.clone(), name_span, operand_span); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + + _ => None, + } + } + + /// +----------------+-------------------------------------------------------------+ + /// | Allowed casts | Casting To | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | float | Yes | Yes | Yes | - | Yes | No | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// + /// Additional cast to complex + fn cast_float_expr_to_type( + &mut self, + ty: &Type, + rhs: &QasmTypedExpr, + node: &SyntaxNode, + ) -> Option { + assert!(matches!(rhs.ty, Type::Float(..))); + match ty { + &Type::Complex(..) => { + let expr = build_complex_from_expr(rhs.expr.clone()); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + &Type::Angle(..) => { + let msg = "Cast float to angle"; + self.push_unimplemented_error_message(msg, node); + None + } + &Type::Int(w, _) | &Type::UInt(w, _) => { + let span = span_for_syntax_node(node); + let expr = ast_builder::build_math_call_from_exprs( + "Truncate", + vec![rhs.expr.clone()], + span, + ); + let expr = if let Some(w) = w { + if w > 64 { + build_convert_call_expr(expr, "IntAsBigInt") + } else { + expr + } + } else { + expr + }; + + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + &Type::Bool(..) => { + let span = span_for_syntax_node(node); + let expr = ast_builder::build_math_call_from_exprs( + "Truncate", + vec![rhs.expr.clone()], + span, + ); + let const_int_zero_expr = build_lit_int_expr(0, rhs.expr.span); + let qsop = ast::BinOp::Eq; + let cond = ast_builder::build_binary_expr( + false, + qsop, + expr, + const_int_zero_expr, + rhs.expr.span, + ); + let coerce_expr = build_if_expr_then_expr_else_expr( + cond, + build_lit_bool_expr(false, rhs.expr.span), + build_lit_bool_expr(true, rhs.expr.span), + rhs.expr.span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr: coerce_expr, + }) + } + _ => None, + } + } + + fn create_entry_item>( + &mut self, + name: S, + stmts: Vec, + input: Option>, + output: Option>, + whole_span: Span, + output_semantics: OutputSemantics, + ) -> (ast::Item, OperationSignature) { + let mut stmts = stmts; + let is_qiskit = matches!(output_semantics, OutputSemantics::Qiskit); + let mut signature = OperationSignature { + input: vec![], + output: String::new(), + name: name.as_ref().to_string(), + ns: None, + }; + let output_ty = if matches!(output_semantics, OutputSemantics::ResourceEstimation) { + // we have no output, but need to set the entry point return type + crate::types::Type::Tuple(vec![]) + } else if let Some(output) = output { + let output_exprs = if is_qiskit { + output + .iter() + .rev() + .filter(|symbol| matches!(symbol.ty, Type::BitArray(..))) + .map(|symbol| { + let ident = + build_path_ident_expr(symbol.name.as_str(), symbol.span, symbol.span); + + build_array_reverse_expr(ident) + }) + .collect::>() + } else { + output + .iter() + .map(|symbol| { + build_path_ident_expr(symbol.name.as_str(), symbol.span, symbol.span) + }) + .collect::>() + }; + // this is the output whether it is inferred or explicitly defined + // map the output symbols into a return statement, add it to the nodes list, + // and get the entry point return type + let output_types = if is_qiskit { + output + .iter() + .rev() + .filter(|symbol| matches!(symbol.ty, Type::BitArray(..))) + .map(|symbol| symbol.qsharp_ty.clone()) + .collect::>() + } else { + output + .iter() + .map(|symbol| symbol.qsharp_ty.clone()) + .collect::>() + }; + + let (output_ty, output_expr) = if output_types.len() == 1 { + (output_types[0].clone(), output_exprs[0].clone()) + } else { + let output_ty = crate::types::Type::Tuple(output_types); + let output_expr = build_tuple_expr(output_exprs); + (output_ty, output_expr) + }; + + let return_stmt = build_implicit_return_stmt(output_expr); + stmts.push(return_stmt); + output_ty + } else { + if is_qiskit { + let kind = SemanticErrorKind::QiskitEntryPointMissingOutput(whole_span); + self.push_semantic_error(kind); + } + crate::types::Type::Tuple(vec![]) + }; + + let ast_ty = map_qsharp_type_to_ast_ty(&output_ty); + signature.output = format!("{output_ty}"); + // TODO: This can create a collision on multiple compiles when interactive + // We also have issues with the new entry point inference logic + let input_desc = input + .iter() + .flat_map(|s| { + s.iter() + .map(|s| (s.name.to_string(), format!("{}", s.qsharp_ty))) + }) + .collect::>(); + signature.input = input_desc; + let input_pats = input + .into_iter() + .flat_map(|s| { + s.into_iter() + .map(|s| build_arg_pat(s.name, s.span, map_qsharp_type_to_ast_ty(&s.qsharp_ty))) + }) + .collect::>(); + + ( + build_operation_with_stmts(name, input_pats, ast_ty, stmts, whole_span), + signature, + ) + } + + /// +----------------+-------------------------------------------------------------+ + /// | Allowed casts | Casting To | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | bool | - | Yes | Yes | Yes | No | Yes | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + fn cast_bool_expr_to_type(&mut self, ty: &Type, rhs: &QasmTypedExpr) -> Option { + assert!(matches!(rhs.ty, Type::Bool(..))); + let name_span = rhs.expr.span; + let operand_span = rhs.expr.span; + match ty { + &Type::Bit(..) => { + self.runtime |= RuntimeFunctions::BoolAsResult; + let expr = build_cast_call( + RuntimeFunctions::BoolAsResult, + rhs.expr.clone(), + name_span, + operand_span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + &Type::Float(..) => { + self.runtime |= RuntimeFunctions::BoolAsDouble; + let expr = build_cast_call( + RuntimeFunctions::BoolAsDouble, + rhs.expr.clone(), + name_span, + operand_span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + &Type::Int(w, _) | &Type::UInt(w, _) => { + let function = if let Some(width) = w { + if width > 64 { + RuntimeFunctions::BoolAsBigInt + } else { + RuntimeFunctions::BoolAsInt + } + } else { + RuntimeFunctions::BoolAsInt + }; + self.runtime |= function; + let expr = build_cast_call(function, rhs.expr.clone(), name_span, operand_span); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + _ => None, + } + } + + /// +----------------+-------------------------------------------------------------+ + /// | Allowed casts | Casting To | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | int | Yes | - | Yes | Yes | No | Yes | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// | uint | Yes | Yes | - | Yes | No | Yes | No | No | + /// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ + /// + /// Additional cast to ``BigInt`` + #[allow(clippy::too_many_lines)] + fn cast_int_expr_to_type(&mut self, ty: &Type, rhs: &QasmTypedExpr) -> Option { + assert!(matches!(rhs.ty, Type::Int(..) | Type::UInt(..))); + let name_span = rhs.expr.span; + let operand_span = rhs.expr.span; + match ty { + Type::BitArray(dims, _) => { + self.runtime |= RuntimeFunctions::IntAsResultArrayBE; + let size = match dims { + ArrayDims::D1(size) => i64::try_from(*size).ok()?, + _ => 0, + }; + + let size_expr = build_lit_int_expr(size, Span::default()); + let expr = build_cast_call_two_params( + RuntimeFunctions::IntAsResultArrayBE, + rhs.expr.clone(), + size_expr, + name_span, + operand_span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + Type::Float(..) => { + let expr = build_convert_call_expr(rhs.expr.clone(), "IntAsDouble"); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + Type::Int(tw, _) | Type::UInt(tw, _) => { + // uint to int, or int/uint to BigInt + if let Some(tw) = tw { + if *tw > 64 { + let expr = build_convert_call_expr(rhs.expr.clone(), "IntAsBigInt"); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }) + } + } else { + Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }) + } + } + Type::Bool(..) => { + let const_int_zero_expr = build_lit_int_expr(0, rhs.expr.span); + let qsop = ast::BinOp::Eq; + let cond = ast_builder::build_binary_expr( + false, + qsop, + rhs.expr.clone(), + const_int_zero_expr, + rhs.expr.span, + ); + let coerce_expr = build_if_expr_then_expr_else_expr( + cond, + build_lit_bool_expr(false, rhs.expr.span), + build_lit_bool_expr(true, rhs.expr.span), + rhs.expr.span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr: coerce_expr, + }) + } + Type::Bit(..) => { + let const_int_zero_expr = build_lit_int_expr(0, rhs.expr.span); + let qsop = ast::BinOp::Eq; + let cond = ast_builder::build_binary_expr( + false, + qsop, + rhs.expr.clone(), + const_int_zero_expr, + rhs.expr.span, + ); + let coerce_expr = build_if_expr_then_expr_else_expr( + cond, + build_lit_result_expr(ast::Result::One, rhs.expr.span), + build_lit_result_expr(ast::Result::Zero, rhs.expr.span), + rhs.expr.span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr: coerce_expr, + }) + } + Type::Complex(..) => { + let expr = build_convert_call_expr(rhs.expr.clone(), "IntAsDouble"); + let expr = build_complex_from_expr(expr); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } + _ => None, + } + } + + fn cast_bitarray_expr_to_type( + &mut self, + dims: &ArrayDims, + ty: &Type, + rhs: &QasmTypedExpr, + ) -> Option { + let ArrayDims::D1(array_width) = dims else { + return None; + }; + if !matches!(ty, Type::Int(..) | Type::UInt(..)) { + return None; + } + // we know we have a bit array being cast to an int/uint + // verfiy widths + let int_width = ty.width(); + + if int_width.is_none() || (int_width == Some(u32::try_from(*array_width).ok()?)) { + let name_span = rhs.expr.span; + let operand_span = rhs.expr.span; + self.runtime |= RuntimeFunctions::ResultArrayAsIntBE; + let expr = build_cast_call( + RuntimeFunctions::ResultArrayAsIntBE, + rhs.expr.clone(), + name_span, + operand_span, + ); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + None + } + } +} + +fn compile_end_stmt(end: &oq3_syntax::ast::EndStmt) -> ast::Stmt { + ast_builder::build_end_stmt(span_for_syntax_node(end.syntax())) +} + +/// This is missing bitwise negation, but it is impossible to test +/// as the parser doesn't support it. +fn binop_requires_bitwise_conversion(op: BinaryOp, left_type: &Type) -> bool { + match op { + BinaryOp::ArithOp(arith) => match arith { + ArithOp::BitAnd | ArithOp::BitOr | ArithOp::BitXor => matches!( + left_type, + Type::Bit(..) + | Type::UInt(..) + | Type::Angle(..) + | Type::BitArray(ArrayDims::D1(_), _) + ), + ArithOp::Shl | ArithOp::Shr => matches!( + left_type, + Type::Bit(..) + | Type::UInt(..) + | Type::Angle(..) + | Type::BitArray(ArrayDims::D1(_), _) + ), + _ => false, + }, + _ => false, + } +} + +fn binop_requires_bitwise_symmetric_conversion(op: BinaryOp) -> bool { + match op { + BinaryOp::ArithOp(arith) => { + matches!(arith, ArithOp::BitAnd | ArithOp::BitOr | ArithOp::BitXor) + } + _ => false, + } +} + +fn calculate_num_ctrls(modifiers: &[&GateModifier]) -> u64 { + let num_ctrls: u64 = modifiers + .iter() + .map(|m| match m { + GateModifier::Inv(_) | GateModifier::Pow(_, _) => 0, + GateModifier::Ctrl(ctls, _) | GateModifier::NegCtrl(ctls, _) => { + TryInto::::try_into(ctls.unwrap_or(1)) + .ok() + .unwrap_or(0) + } + }) + .sum(); + num_ctrls +} + +fn get_implicit_modifiers>( + gate_name: S, + name_span: Span, +) -> (String, Vec) { + // ch, crx, cry, crz, sdg, and tdg + match gate_name.as_ref() { + "ch" => ("H".to_string(), vec![GateModifier::Ctrl(None, name_span)]), + "crx" => ("Rx".to_string(), vec![GateModifier::Ctrl(None, name_span)]), + "cry" => ("Ry".to_string(), vec![GateModifier::Ctrl(None, name_span)]), + "crz" => ("Rz".to_string(), vec![GateModifier::Ctrl(None, name_span)]), + "sdg" => ("S".to_string(), vec![GateModifier::Inv(name_span)]), + "tdg" => ("T".to_string(), vec![GateModifier::Inv(name_span)]), + _ => (gate_name.as_ref().to_string(), vec![]), + } +} + +/// Bit arrays can be compared, but need to be converted to int first +fn binop_requires_int_conversion_for_type(op: BinaryOp, ty_1: &Type, ty_2: &Type) -> bool { + match op { + BinaryOp::CmpOp(_) => match (ty_1, ty_2) { + (Type::BitArray(ArrayDims::D1(d1), _), Type::BitArray(ArrayDims::D1(d2), _)) => { + d1 == d2 + } + _ => false, + }, + _ => false, + } +} + +fn binop_requires_int_magic(op: BinaryOp, ty_1: &Type, ty_2: &Type) -> bool { + match op { + BinaryOp::CmpOp(_) => match (ty_1, ty_2) { + (Type::BitArray(ArrayDims::D1(d1), _), Type::BitArray(ArrayDims::D1(d2), _)) => { + d1 == d2 + } + _ => false, + }, + _ => false, + } +} + +fn binop_requires_bool_conversion_for_type(op: BinaryOp) -> bool { + matches!(op, BinaryOp::LogicOp(..)) +} + +fn compile_intnumber_as_bit( + value: &oq3_syntax::ast::IntNumber, + span: Span, + ty: &Type, +) -> Option { + let value = value.value().expect("IntNumber must have a value"); + if value == 0 || value == 1 { + let expr = build_lit_result_expr((value == 1).into(), span); + Some(QasmTypedExpr { + ty: ty.clone(), + expr, + }) + } else { + None + } +} + +fn compile_floatnumber_as_negated_double( + value: &oq3_syntax::ast::FloatNumber, + span: Span, +) -> QasmTypedExpr { + let expr = build_lit_double_expr(-value.value().expect("FloatNumber must have a value"), span); + let ty = Type::Float(None, IsConst::True); + QasmTypedExpr { ty, expr } +} + +fn compile_intnumber_as_negated_int( + value: &oq3_syntax::ast::IntNumber, + span: Span, +) -> QasmTypedExpr { + let value = value.value().expect("IntNumber must have a value"); + if let Ok(value) = value.try_into() { + let value: i64 = value; + let expr = build_lit_int_expr(-value, span); + let ty = Type::Int(None, IsConst::True); + QasmTypedExpr { ty, expr } + } else { + let expr = build_lit_bigint_expr(BigInt::from(-1) * BigInt::from(value), span); + let ty = Type::Int(Some(128), IsConst::True); + QasmTypedExpr { ty, expr } + } +} + +/// +----------------+-------------------------------------------------------------+ +/// | Allowed casts | Casting To | +/// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ +/// | Casting From | bool | int | uint | float | angle | bit | duration | qubit | +/// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ +/// | complex | ?? | ?? | ?? | ?? | No | ?? | No | No | +/// +----------------+-------+-----+------+-------+-------+-----+----------+-------+ +fn cast_complex_expr_to_type(ty: &Type, rhs: &QasmTypedExpr) -> Option { + assert!(matches!(rhs.ty, Type::Complex(..))); + + if matches!((ty, &rhs.ty), (Type::Complex(..), Type::Complex(..))) { + // we are both complex, but our widths are different. If both + // had implicit widths, we would have already matched for the + // (None, None). If the rhs width is bigger, we will return + // None to let the cast fail + + // Here, we can safely cast the rhs to the lhs type if the + // lhs width can hold the rhs's width + if ty.width().is_none() && rhs.ty.width().is_some() { + return Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }); + } + if ty.width() >= rhs.ty.width() { + return Some(QasmTypedExpr { + ty: ty.clone(), + expr: rhs.expr.clone(), + }); + } + } + None +} + +fn try_promote_with_casting(left_type: &Type, right_type: &Type) -> Type { + let promoted_type = promote_types(left_type, right_type); + + if promoted_type != Type::Void { + return promoted_type; + } + if let Some(value) = try_promote_bitarray_to_int(left_type, right_type) { + return value; + } + // simple promotion failed, try a lossless cast + // each side to double + let promoted_rhs = promote_types(&Type::Float(None, IsConst::False), right_type); + let promoted_lhs = promote_types(left_type, &Type::Float(None, IsConst::False)); + + match (promoted_lhs, promoted_rhs) { + (Type::Void, Type::Void) => Type::Float(None, IsConst::False), + (Type::Void, promoted_rhs) => promoted_rhs, + (promoted_lhs, Type::Void) => promoted_lhs, + (promoted_lhs, promoted_rhs) => { + // return the greater of the two promoted types + if matches!(promoted_lhs, Type::Complex(..)) { + promoted_lhs + } else if matches!(promoted_rhs, Type::Complex(..)) { + promoted_rhs + } else if matches!(promoted_lhs, Type::Float(..)) { + promoted_lhs + } else if matches!(promoted_rhs, Type::Float(..)) { + promoted_rhs + } else { + Type::Float(None, IsConst::False) + } + } + } +} + +fn try_promote_bitarray_to_int(left_type: &Type, right_type: &Type) -> Option { + if matches!( + (left_type, right_type), + (Type::Int(..) | Type::UInt(..), Type::BitArray(..)) + ) { + let ty = left_type; + let r = right_type.dims().expect("")[0]; + + if ty.dims().is_none() || (ty.num_dims() == 1 && ty.dims().expect("")[0] == r) { + return Some(left_type.clone()); + } + } + if matches!( + (left_type, right_type), + (Type::BitArray(..), Type::Int(..) | Type::UInt(..)) + ) { + let ty = right_type; + let r = left_type.dims().expect("")[0]; + + if ty.dims().is_none() || (ty.num_dims() == 1 && ty.dims().expect("")[0] == r) { + return Some(right_type.clone()); + } + } + None +} + +fn compile_bitstring(bitstring: &BitString, span: Span) -> Option { + let width = bitstring + .to_string() + .chars() + .filter(|c| *c == '0' || *c == '1') + .count(); + let expr = bitstring + .value() + .map(|value| build_lit_result_array_expr_from_bitstring(value, span))?; + let ty = Type::BitArray(ArrayDims::D1(width), IsConst::True); + Some(QasmTypedExpr { ty, expr }) +} + +pub fn can_cast_literal(lhs_ty: &Type, ty_lit: &Type) -> bool { + if matches!(lhs_ty, Type::Int(..)) && matches!(ty_lit, Type::UInt(..)) { + return true; + } + if matches!(lhs_ty, Type::UInt(..)) { + return matches!(ty_lit, Type::Complex(..)); + } + oq3_semantics::types::can_cast_literal(lhs_ty, ty_lit) + || { + matches!(lhs_ty, Type::Bit(..) | Type::Bool(..)) + && matches!(ty_lit, Type::Bit(..) | Type::Bool(..)) + } + || { + match lhs_ty { + Type::BitArray(dims, _) => { + matches!(dims, ArrayDims::D1(_)) + && matches!(ty_lit, Type::Int(..) | Type::UInt(..)) + } + _ => false, + } + } +} + +fn extract_pow_exponent(expr: &oq3_syntax::ast::ParenExpr, span: Span) -> GateModifier { + let lit = compile_paren_lit_int_expr(expr); + if let Some((exponent, sign)) = lit { + let exponent = i64::try_from(exponent).ok(); + let Some(exponent) = exponent else { + return GateModifier::Pow(None, span); + }; + if sign { + GateModifier::Pow(Some(-exponent), span) + } else { + GateModifier::Pow(Some(exponent), span) + } + } else { + GateModifier::Pow(None, span) + } +} + +fn compile_paren_lit_int_expr(paren_expr: &oq3_syntax::ast::ParenExpr) -> Option<(usize, bool)> { + let expr = paren_expr.expr()?; + match expr { + Expr::Literal(lit) => match lit.kind() { + LiteralKind::IntNumber(value) => { + let size: usize = usize::try_from(value.value()?).ok()?; + Some((size, false)) + } + _ => None, + }, + Expr::PrefixExpr(prefix) => match prefix.op_kind() { + Some(UnaryOp::Neg) => { + let expr = prefix.expr()?; + match expr { + Expr::Literal(lit) => match lit.kind() { + LiteralKind::IntNumber(value) => { + let size: usize = usize::try_from(value.value()?).ok()?; + Some((size, true)) + } + _ => None, + }, + _ => None, + } + } + _ => None, + }, + _ => None, + } +} diff --git a/compiler/qsc_qasm3/src/compile/tests.rs b/compiler/qsc_qasm3/src/compile/tests.rs new file mode 100644 index 0000000000..e68307a113 --- /dev/null +++ b/compiler/qsc_qasm3/src/compile/tests.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::tests::{parse_all, print_compilation_errors, qasm_to_program_fragments}; +use miette::Report; + +#[test] +fn programs_with_includes_with_includes_can_be_parsed() -> miette::Result<(), Vec> { + let source0 = r#"include "stdgates.inc"; + include "source1.qasm"; + "#; + let source1 = r#"include "source2.qasm"; + "#; + let source2 = ""; + let all_sources = [ + ("source0.qasm".into(), source0.into()), + ("source1.qasm".into(), source1.into()), + ("source2.qasm".into(), source2.into()), + ]; + + let res = parse_all("source0.qasm", all_sources)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + print_compilation_errors(&unit); + assert!(!unit.has_errors()); + Ok(()) +} + +#[test] +fn including_stdgates_multiple_times_causes_symbol_redifintion_errors( +) -> miette::Result<(), Vec> { + let source0 = r#"include "stdgates.inc"; + include "source1.qasm"; + "#; + let source1 = r#"include "source2.qasm"; + "#; + let source2 = r#"include "stdgates.inc";"#; + let all_sources = [ + ("source0.qasm".into(), source0.into()), + ("source1.qasm".into(), source1.into()), + ("source2.qasm".into(), source2.into()), + ]; + + let res = parse_all("source0.qasm", all_sources)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + + assert!(unit.has_errors()); + for error in unit.errors() { + assert!(error.to_string().contains("Redefined symbol: ")); + } + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/io.rs b/compiler/qsc_qasm3/src/io.rs new file mode 100644 index 0000000000..e11a6aea1b --- /dev/null +++ b/compiler/qsc_qasm3/src/io.rs @@ -0,0 +1,89 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::{ + path::{Path, PathBuf}, + sync::Arc, +}; + +use rustc_hash::FxHashMap; + +use miette::IntoDiagnostic; + +/// A trait for resolving include file paths to their contents. +/// This is used by the parser to resolve `include` directives. +/// Implementations of this trait can be provided to the parser +/// to customize how include files are resolved. +pub trait SourceResolver { + #[cfg(feature = "fs")] + fn resolve

(&self, path: P) -> miette::Result<(PathBuf, String)> + where + P: AsRef, + { + let path = std::fs::canonicalize(path).map_err(|e| { + crate::Error(crate::ErrorKind::IO(format!( + "Could not resolve include file path: {e}" + ))) + })?; + match std::fs::read_to_string(&path) { + Ok(source) => Ok((path, source)), + Err(_) => Err(crate::Error(crate::ErrorKind::NotFound(format!( + "Could not resolve include file: {}", + path.display() + )))) + .into_diagnostic(), + } + } + #[cfg(not(feature = "fs"))] + fn resolve

(&self, path: P) -> miette::Result<(PathBuf, String)> + where + P: AsRef; +} + +/// A source resolver that resolves include files from the file system. +#[cfg(feature = "fs")] +pub mod fs { + use super::SourceResolver; + + #[derive(Default)] + pub struct FsSourceResolver; + impl SourceResolver for FsSourceResolver {} +} + +/// A source resolver that resolves include files from an in-memory map. +/// This is useful for testing or environments in which file system access +/// is not available. +/// +/// This requires users to build up a map of include file paths to their +/// contents prior to parsing. +pub struct InMemorySourceResolver { + sources: FxHashMap, +} + +impl FromIterator<(Arc, Arc)> for InMemorySourceResolver { + fn from_iter, Arc)>>(iter: T) -> Self { + let mut map = FxHashMap::default(); + for (path, source) in iter { + map.insert(PathBuf::from(path.to_string()), source.to_string()); + } + + InMemorySourceResolver { sources: map } + } +} + +impl SourceResolver for InMemorySourceResolver { + fn resolve

(&self, path: P) -> miette::Result<(PathBuf, String)> + where + P: AsRef, + { + let path = path.as_ref(); + match self.sources.get(path) { + Some(source) => Ok((path.to_owned(), source.clone())), + None => Err(crate::Error(crate::ErrorKind::NotFound(format!( + "Could not resolve include file: {}", + path.display() + )))) + .into_diagnostic(), + } + } +} diff --git a/compiler/qsc_qasm3/src/lib.rs b/compiler/qsc_qasm3/src/lib.rs new file mode 100644 index 0000000000..5881acd061 --- /dev/null +++ b/compiler/qsc_qasm3/src/lib.rs @@ -0,0 +1,595 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(dead_code)] + +mod angle; +mod ast_builder; +mod compile; +pub use compile::qasm_to_program; +pub mod io; +mod oqasm_helpers; +mod oqasm_types; +pub mod parse; +mod runtime; +mod symbols; +mod types; + +#[cfg(test)] +pub(crate) mod tests; + +use std::fmt::Write; + +use miette::Diagnostic; +use qsc::Span; +use thiserror::Error; + +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] +#[diagnostic(transparent)] +#[error(transparent)] +pub struct Error(pub ErrorKind); + +impl Error { + #[must_use] + pub fn with_offset(self, offset: u32) -> Self { + Self(self.0.with_offset(offset)) + } + + #[must_use] + pub fn is_syntax_error(&self) -> bool { + matches!(self.0, ErrorKind::Parse(_, _)) + } + + #[must_use] + pub fn is_semantic_error(&self) -> bool { + matches!(self.0, ErrorKind::Semantic(..)) + } +} + +/// Represents the kind of error that occurred during compilation of a QASM file(s). +/// The errors fall into a few categories: +/// - Unimplemented features +/// - Not supported features +/// - Parsing errors (converted from the parser) +/// - Semantic errors +/// - IO errors +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] +pub enum ErrorKind { + #[error("this statement is not yet handled during OpenQASM 3 import: {0}")] + Unimplemented(String, #[label] Span), + #[error("pulse control statements are not supported: {0}")] + PulseControlNotSupported(String, #[label] Span), + #[error("calibration statements are not supported: {0}")] + CalibrationsNotSupported(String, #[label] Span), + #[error("{0} are not supported.")] + NotSupported(String, #[label] Span), + #[error("QASM3 Parse Error: {0}")] + Parse(String, #[label] Span), + #[error(transparent)] + #[diagnostic(transparent)] + Semantic(#[from] crate::SemanticError), + #[error("QASM3 Parse Error: Not Found {0}")] + NotFound(String), + #[error("IO Error: {0}")] + IO(String), +} + +impl ErrorKind { + fn with_offset(self, offset: u32) -> Self { + match self { + ErrorKind::Unimplemented(error, span) => Self::Unimplemented(error, span + offset), + ErrorKind::PulseControlNotSupported(error, span) => { + Self::PulseControlNotSupported(error, span + offset) + } + ErrorKind::CalibrationsNotSupported(error, span) => { + Self::CalibrationsNotSupported(error, span + offset) + } + ErrorKind::NotSupported(error, span) => Self::NotSupported(error, span + offset), + ErrorKind::Parse(error, span) => Self::Parse(error, span + offset), + ErrorKind::Semantic(error) => ErrorKind::Semantic(error.with_offset(offset)), + ErrorKind::NotFound(error) => Self::NotFound(error), + ErrorKind::IO(error) => Self::IO(error), + } + } +} + +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] +#[error(transparent)] +#[diagnostic(transparent)] +pub struct SemanticError(SemanticErrorKind); + +impl SemanticError { + #[must_use] + pub fn with_offset(self, offset: u32) -> Self { + Self(self.0.with_offset(offset)) + } +} + +/// Represents the kind of semantic error that occurred during compilation of a QASM file(s). +/// For the most part, these errors are fatal and prevent compilation and are +/// safety checks to ensure that the QASM code is valid. +/// +/// We can't use the semantics library for this: +/// - it is unsafe to use (heavy use of panic and unwrap) +/// - it is missing many language features +#[derive(Clone, Debug, Diagnostic, Eq, Error, PartialEq)] +enum SemanticErrorKind { + #[error("Annotation missing target statement.")] + #[diagnostic(code("Qsc.Qasm3.Compile.AnnotationWithoutStatement"))] + AnnotationWithoutStatement(#[label] Span), + #[error("Cannot alias type {0}. Only qubit and qubit[] can be aliased.")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotAliasType"))] + CannotAliasType(String, Span), + #[error("Cannot apply operator {0} to types {1} and {2}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotApplyOperatorToTypes"))] + CannotApplyOperatorToTypes(String, String, String, #[label] Span), + #[error("Cannot assign a value of {0} type to a classical variable of {1} type.")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotAssignToType"))] + CannotAssignToType(String, String, #[label] Span), + #[error("Cannot call a gate that is not a gate.")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotCallNonGate"))] + CannotCallNonGate(#[label] Span), + #[error("Cannot cast expression of type {0} to type {1}")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotCast"))] + CannotCast(String, String, #[label] Span), + #[error("Cannot index variables of type {0}")] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotIndexType"))] + CannotIndexType(String, #[label] Span), + #[error("Cannot update const variable {0}")] + #[diagnostic(help("mutable variables must be declared without the keyword `const`."))] + #[diagnostic(code("Qsc.Qasm3.Compile.CannotUpdateConstVariable"))] + CannotUpdateConstVariable(String, #[label] Span), + #[error("Cannot cast expression of type {0} to type {1} as it would cause truncation.")] + #[diagnostic(code("Qsc.Qasm3.Compile.CastWouldCauseTruncation"))] + CastWouldCauseTruncation(String, String, #[label] Span), + #[error("Complex numbers in assignment binary expressions are not yet supported.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ComplexBinaryAssignment"))] + ComplexBinaryAssignment(#[label] Span), + #[error("Designator must be a literal integer.")] + #[diagnostic(code("Qsc.Qasm3.Compile.DesignatorMustBeIntLiteral"))] + DesignatorMustBeIntLiteral(#[label] Span), + #[error("Failed to compile all expressions in expression list.")] + #[diagnostic(code("Qsc.Qasm3.Compile.FailedToCompileExpressionList"))] + FailedToCompileExpressionList(#[label] Span), + #[error("For iterable must have a set expression, range expression, or iterable expression.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ForIterableInvalidExpression"))] + ForIterableInvalidExpression(#[label] Span), + #[error("For statements must have a body or statement.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ForStatementsMustHaveABodyOrStatement"))] + ForStatementsMustHaveABodyOrStatement(#[label] Span), + #[error("If statement missing {0} expression.")] + #[diagnostic(code("Qsc.Qasm3.Compile.IfStmtMissingExpression"))] + IfStmtMissingExpression(String, #[label] Span), + #[error("include {0} could not be found.")] + #[diagnostic(code("Qsc.Qasm3.Compile.IncludeNotFound"))] + IncludeNotFound(String, #[label] Span), + #[error("include {0} must be declared in global scope.")] + #[diagnostic(code("Qsc.Qasm3.Compile.IncludeNotInGlobalScope"))] + IncludeNotInGlobalScope(String, #[label] Span), + #[error("include {0} must be declared in global scope.")] + #[diagnostic(code("Qsc.Qasm3.Compile.IncludeStatementMissingPath"))] + IncludeStatementMissingPath(#[label] Span), + #[error("Indexed must be a single expression.")] + #[diagnostic(code("Qsc.Qasm3.Compile.IndexMustBeSingleExpr"))] + IndexMustBeSingleExpr(#[label] Span), + #[error("Annotations only valid on gate definitions.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidAnnotationTarget"))] + InvalidAnnotationTarget(Span), + #[error("Assigning {0} values to {1} must be in a range that be converted to {1}.")] + InvalidCastValueRange(String, String, #[label] Span), + #[error("Gate operands other than qubits or qubit arrays are not supported.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidGateOperand"))] + InvalidGateOperand(#[label] Span), + #[error("Control counts must be integer literals.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidControlCount"))] + InvalidControlCount(#[label] Span), + #[error("Gate operands other than qubit arrays are not supported.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidIndexedGateOperand"))] + InvalidIndexedGateOperand(#[label] Span), + #[error("Gate expects {0} classical arguments, but {1} were provided.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidNumberOfClassicalArgs"))] + InvalidNumberOfClassicalArgs(usize, usize, #[label] Span), + #[error("Gate expects {0} qubit arguments, but {1} were provided.")] + #[diagnostic(code("Qsc.Qasm3.Compile.InvalidNumberOfQubitArgs"))] + InvalidNumberOfQubitArgs(usize, usize, #[label] Span), + #[error("Measure statements must have a name.")] + #[diagnostic(code("Qsc.Qasm3.Compile.MeasureExpressionsMustHaveName"))] + MeasureExpressionsMustHaveName(#[label] Span), + #[error("Measure statements must have a gate operand name.")] + #[diagnostic(code("Qsc.Qasm3.Compile.MeasureExpressionsMustHaveGateOperand"))] + MeasureExpressionsMustHaveGateOperand(#[label] Span), + #[error("Control counts must be postitive integers.")] + #[diagnostic(code("Qsc.Qasm3.Compile.NegativeControlCount"))] + NegativeControlCount(#[label] Span), + #[error("The operator {0} is not valid with lhs {1} and rhs {2}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.OperatorNotSupportedForTypes"))] + OperatorNotSupportedForTypes(String, String, String, #[label] Span), + #[error("Pow gate modifiers must have an exponent.")] + #[diagnostic(code("Qsc.Qasm3.Compile.PowModifierMustHaveExponent"))] + PowModifierMustHaveExponent(#[label] Span), + #[error("Qiskit circuits must have output registers.")] + #[diagnostic(code("Qsc.Qasm3.Compile.QiskitEntryPointMissingOutput"))] + QiskitEntryPointMissingOutput(#[label] Span), + #[error("Quantum declarations must be done in global scope.")] + #[diagnostic(code("Qsc.Qasm3.Compile.QuantumDeclarationInNonGlobalScope"))] + QuantumDeclarationInNonGlobalScope(#[label] Span), + #[error("Quantum typed values cannot be used in binary expressions.")] + #[diagnostic(code("Qsc.Qasm3.Compile.QuantumTypesInBinaryExpression"))] + QuantumTypesInBinaryExpression(#[label] Span), + #[error("Range expressions must have a start.")] + #[diagnostic(code("Qsc.Qasm3.Compile.RangeExpressionsMustHaveStart"))] + RangeExpressionsMustHaveStart(#[label] Span), + #[error("Range expressions must have a stop.")] + #[diagnostic(code("Qsc.Qasm3.Compile.RangeExpressionsMustHaveStop"))] + RangeExpressionsMustHaveStop(#[label] Span), + #[error("Redefined symbol: {0}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.RedefinedSymbol"))] + RedefinedSymbol(String, #[label] Span), + #[error("Reset expression must have a gate operand.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ResetExpressionMustHaveGateOperand"))] + ResetExpressionMustHaveGateOperand(#[label] Span), + #[error("Reset expression must have a name.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ResetExpressionMustHaveName"))] + ResetExpressionMustHaveName(#[label] Span), + #[error("Return statements are only allowed within subroutines.")] + #[diagnostic(code("Qsc.Qasm3.Compile.ReturnNotInSubroutine"))] + ReturnNotInSubroutine(#[label] Span), + #[error("Too many controls specified.")] + #[diagnostic(code("Qsc.Qasm3.Compile.TooManyControls"))] + TooManyControls(#[label] Span), + #[error("Types differ by dimensions and are incompatible.")] + #[diagnostic(code("Qsc.Qasm3.Compile.TypeRankError"))] + TypeRankError(#[label] Span), + #[error("Undefined symbol: {0}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.UndefinedSymbol"))] + UndefinedSymbol(String, #[label] Span), + #[error("Unexpected parser error: {0}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.UnexpectedParserError"))] + UnexpectedParserError(String, #[label] Span), + #[error("Unexpected annotation: {0}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.UnknownAnnotation"))] + UnknownAnnotation(String, #[label] Span), + #[error("Undefined symbol: {0}.")] + #[diagnostic(code("Qsc.Qasm3.Compile.UnknownIndexedOperatorKind"))] + UnknownIndexedOperatorKind(#[label] Span), + #[error("While statement missing {0} expression.")] + #[diagnostic(code("Qsc.Qasm3.Compile.WhileStmtMissingExpression"))] + WhileStmtMissingExpression(String, Span), +} + +impl SemanticErrorKind { + /// The semantic errors are reported with the span of the syntax that caused the error. + /// This offset is relative to the start of the file in which the error occurred. + /// This method is used to adjust the span of the error to be relative to where the + /// error was reported in the entire compilation unit as part of the source map. + #[allow(clippy::too_many_lines)] + fn with_offset(self, offset: u32) -> Self { + match self { + Self::AnnotationWithoutStatement(span) => { + Self::AnnotationWithoutStatement(span + offset) + } + Self::CannotCast(lhs, rhs, span) => Self::CannotCast(lhs, rhs, span + offset), + Self::CastWouldCauseTruncation(lhs, rhs, span) => { + Self::CastWouldCauseTruncation(lhs, rhs, span + offset) + } + Self::CannotAliasType(name, span) => Self::CannotAliasType(name, span + offset), + Self::CannotApplyOperatorToTypes(op, lhs, rhs, span) => { + Self::CannotApplyOperatorToTypes(op, lhs, rhs, span + offset) + } + Self::CannotAssignToType(lhs, rhs, span) => { + Self::CannotAssignToType(lhs, rhs, span + offset) + } + Self::CannotCallNonGate(span) => Self::CannotCallNonGate(span + offset), + Self::CannotIndexType(name, span) => Self::CannotIndexType(name, span + offset), + Self::CannotUpdateConstVariable(name, span) => { + Self::CannotUpdateConstVariable(name, span + offset) + } + Self::ComplexBinaryAssignment(span) => Self::ComplexBinaryAssignment(span + offset), + Self::DesignatorMustBeIntLiteral(span) => { + Self::DesignatorMustBeIntLiteral(span + offset) + } + Self::FailedToCompileExpressionList(span) => { + Self::FailedToCompileExpressionList(span + offset) + } + Self::ForIterableInvalidExpression(span) => { + Self::ForIterableInvalidExpression(span + offset) + } + Self::ForStatementsMustHaveABodyOrStatement(span) => { + Self::ForStatementsMustHaveABodyOrStatement(span + offset) + } + Self::IfStmtMissingExpression(name, span) => { + Self::IfStmtMissingExpression(name, span + offset) + } + Self::IncludeNotFound(name, span) => Self::IncludeNotFound(name, span + offset), + Self::IncludeNotInGlobalScope(name, span) => { + Self::IncludeNotInGlobalScope(name, span + offset) + } + Self::IncludeStatementMissingPath(span) => { + Self::IncludeStatementMissingPath(span + offset) + } + Self::IndexMustBeSingleExpr(span) => Self::IndexMustBeSingleExpr(span + offset), + Self::InvalidAnnotationTarget(span) => Self::InvalidAnnotationTarget(span + offset), + Self::InvalidControlCount(span) => Self::InvalidControlCount(span + offset), + Self::InvalidNumberOfClassicalArgs(expected, actual, span) => { + Self::InvalidNumberOfClassicalArgs(expected, actual, span + offset) + } + Self::InvalidNumberOfQubitArgs(expected, actual, span) => { + Self::InvalidNumberOfQubitArgs(expected, actual, span + offset) + } + Self::InvalidCastValueRange(lhs, rhs, span) => { + Self::InvalidCastValueRange(lhs, rhs, span + offset) + } + Self::InvalidGateOperand(span) => Self::InvalidGateOperand(span + offset), + Self::InvalidIndexedGateOperand(span) => Self::InvalidIndexedGateOperand(span + offset), + Self::MeasureExpressionsMustHaveGateOperand(span) => { + Self::MeasureExpressionsMustHaveGateOperand(span + offset) + } + Self::MeasureExpressionsMustHaveName(span) => { + Self::MeasureExpressionsMustHaveName(span + offset) + } + Self::NegativeControlCount(span) => Self::NegativeControlCount(span + offset), + Self::OperatorNotSupportedForTypes(op, lhs, rhs, span) => { + Self::OperatorNotSupportedForTypes(op, lhs, rhs, span + offset) + } + Self::PowModifierMustHaveExponent(span) => { + Self::PowModifierMustHaveExponent(span + offset) + } + Self::QiskitEntryPointMissingOutput(span) => { + Self::QiskitEntryPointMissingOutput(span + offset) + } + Self::QuantumDeclarationInNonGlobalScope(span) => { + Self::QuantumDeclarationInNonGlobalScope(span + offset) + } + Self::QuantumTypesInBinaryExpression(span) => { + Self::QuantumTypesInBinaryExpression(span + offset) + } + Self::RangeExpressionsMustHaveStart(span) => { + Self::RangeExpressionsMustHaveStart(span + offset) + } + Self::RangeExpressionsMustHaveStop(span) => { + Self::RangeExpressionsMustHaveStop(span + offset) + } + Self::RedefinedSymbol(name, span) => Self::RedefinedSymbol(name, span + offset), + Self::ResetExpressionMustHaveGateOperand(span) => { + Self::ResetExpressionMustHaveGateOperand(span + offset) + } + Self::ResetExpressionMustHaveName(span) => { + Self::ResetExpressionMustHaveName(span + offset) + } + Self::ReturnNotInSubroutine(span) => Self::ReturnNotInSubroutine(span + offset), + Self::TooManyControls(span) => Self::TooManyControls(span + offset), + Self::TypeRankError(span) => Self::TypeRankError(span + offset), + Self::UndefinedSymbol(name, span) => Self::UndefinedSymbol(name, span + offset), + Self::UnexpectedParserError(error, span) => { + Self::UnexpectedParserError(error, span + offset) + } + Self::UnknownAnnotation(name, span) => Self::UnknownAnnotation(name, span + offset), + Self::UnknownIndexedOperatorKind(span) => { + Self::UnknownIndexedOperatorKind(span + offset) + } + Self::WhileStmtMissingExpression(name, span) => { + Self::WhileStmtMissingExpression(name, span + offset) + } + } + } +} + +/// Qubit semantics differ between Q# and Qiskit. This enum is used to +/// specify which semantics to use when compiling QASM to Q#. +/// +/// Q# requires qubits to be in the 0 state before and after use. +/// Qiskit makes no assumptions about the state of qubits before or after use. +/// +/// During compliation, if Qiskit semantics are used, the compiler will insert +/// calls to create qubits instead of `use` bindings. This means that later +/// compiler passes won't generate the Q# code that would check the qubits. +/// +/// If Q# semantics are used, the compiler will insert `use` bindings. +/// +/// The Qiskit semantics can also be useful if we ever want to do state +/// vector simulation as it will allow us to get the simulator state at +/// the end of the program. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum QubitSemantics { + QSharp, + Qiskit, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CompilerConfig { + pub qubit_semantics: QubitSemantics, + pub output_semantics: OutputSemantics, + pub program_ty: ProgramType, +} + +impl CompilerConfig { + #[must_use] + pub fn new( + qubit_semantics: QubitSemantics, + output_semantics: OutputSemantics, + program_ty: ProgramType, + ) -> Self { + Self { + qubit_semantics, + output_semantics, + program_ty, + } + } +} + +impl Default for CompilerConfig { + fn default() -> Self { + Self { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::Fragments, + } + } +} + +/// Represents the type of compilation out to create +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ProgramType { + /// Creates an operation in a namespace as if the program is a standalone + /// file. The param is the name of the operation to create. Input are + /// lifted to the operation params. Output are lifted to the operation + /// return type. The operation is marked as `@EntryPoint` as long as there + /// are no input parameters. + File(String), + /// Creates an operation program is a standalone function. The param is the + /// name of the operation to create. Input are lifted to the operation + /// params. Output are lifted to the operation return type. + Operation(String), + /// Creates a list of statements from the program. This is useful for + /// interactive environments where the program is a list of statements + /// imported into the current scope. + /// This is also useful for testing indifidual statements compilation. + Fragments, +} + +use qsc::{ast::Package, error::WithSource, SourceMap}; + +/// Represents the signature of an operation. +/// This is used to create a function signature for the +/// operation that is created from the QASM source code. +/// This is the human readable form of the operation. +pub struct OperationSignature { + pub name: String, + pub ns: Option, + pub input: Vec<(String, String)>, + pub output: String, +} + +impl OperationSignature { + /// Creates a human readable operation signature of the form: + /// `ns.name(input)` + /// which is required to call the operation from other code. + #[must_use] + pub fn create_entry_expr_from_params>(&self, params: S) -> String { + let mut expr = String::new(); + if let Some(ns) = &self.ns { + write!(expr, "{ns}.").unwrap(); + } + write!(expr, "{}(", self.name).unwrap(); + write!(expr, "{}", params.as_ref()).unwrap(); + write!(expr, ")").unwrap(); + + expr + } + + /// Renders the input parameters as a string of comma separated + /// pairs. + #[must_use] + pub fn input_params(&self) -> String { + let mut expr = String::new(); + for (i, (name, ty)) in self.input.iter().enumerate() { + if i > 0 { + write!(expr, ", ").unwrap(); + } + write!(expr, "{name}: {ty}").unwrap(); + } + expr + } +} + +impl std::fmt::Display for OperationSignature { + /// Renders the operation signature as a human readable string. + /// The signature is of the form: + /// `ns.name(input) -> output` + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + if let Some(ns) = &self.ns { + write!(f, "{ns}.")?; + } + write!(f, "{}(", self.name)?; + for (i, (name, ty)) in self.input.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{name}: {ty}")?; + } + write!(f, ") -> {}", self.output) + } +} + +/// A unit of compilation for QASM source code. +/// This is the result of parsing and compiling a QASM source file. +pub struct QasmCompileUnit { + /// Source map created from the accumulated source files, + source_map: SourceMap, + /// Semantic errors encountered during compilation. + /// These are always fatal errors that prevent compilation. + errors: Vec>, + /// The compiled AST package, if compilation was successful. + /// There is no guarantee that this package is valid unless + /// there are no errors. + package: Option, + /// The signature of the operation created from the QASM source code. + /// None if the program type is `ProgramType::Fragments`. + signature: Option, +} + +/// Represents a QASM compilation unit. +/// This is the result of parsing and compiling a QASM source file. +/// The result contains the source map, errors, and the compiled package. +/// The package is only valid if there are no errors. +impl QasmCompileUnit { + #[must_use] + pub fn new( + source_map: SourceMap, + errors: Vec>, + package: Option, + signature: Option, + ) -> Self { + Self { + source_map, + errors, + package, + signature, + } + } + + /// Returns true if there are errors in the compilation unit. + #[must_use] + pub fn has_errors(&self) -> bool { + !self.errors.is_empty() + } + + /// Returns a list of errors in the compilation unit. + #[must_use] + pub fn errors(&self) -> Vec> { + self.errors.clone() + } + + /// Deconstructs the compilation unit into its owned parts. + #[must_use] + pub fn into_tuple( + self, + ) -> ( + SourceMap, + Vec>, + Option, + Option, + ) { + (self.source_map, self.errors, self.package, self.signature) + } +} + +/// Represents the output semantics of the compilation. +/// Each has implications on the output of the compilation +/// and the semantic checks that are performed. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum OutputSemantics { + /// The output is in Qiskit format meaning that the output + /// is all of the classical registers, in reverse order + /// in which they were added to the circuit with each + /// bit within each register in reverse order. + Qiskit, + /// [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output) + /// - If the programmer provides one or more `output` declarations, then + /// variables described as outputs will be returned as output. + /// The spec make no mention of endianness or order of the output. + /// - Otherwise, assume all of the declared variables are returned as output. + OpenQasm, + /// No output semantics are applied. The entry point returns `Unit`. + ResourceEstimation, +} diff --git a/compiler/qsc_qasm3/src/oqasm_helpers.rs b/compiler/qsc_qasm3/src/oqasm_helpers.rs new file mode 100644 index 0000000000..8ef1483224 --- /dev/null +++ b/compiler/qsc_qasm3/src/oqasm_helpers.rs @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use oq3_semantics::types::Type; +use oq3_syntax::ast::{ArithOp, BinaryOp, Designator, Expr, Literal, LiteralKind}; +use qsc::Span; + +/// Extracts a Q# ```Span``` from the QASM3 syntax named element +pub(crate) fn span_for_named_item(value: &T) -> Span { + let Some(name) = value.name() else { + return Span::default(); + }; + let Some(ident) = name.ident_token() else { + return Span::default(); + }; + text_range_to_span(ident.text_range()) +} + +/// Converts the QASM3 syntax ```TextRange``` to a Q# ```Span``` +pub(crate) fn text_range_to_span(range: oq3_syntax::TextRange) -> Span { + Span { + lo: range.start().into(), + hi: range.end().into(), + } +} + +/// Extracts a Q# ```Span``` from the QASM3 syntax node +pub(crate) fn span_for_syntax_node(node: &oq3_syntax::SyntaxNode) -> Span { + text_range_to_span(node.text_range()) +} + +/// Extracts a Q# ```Span``` from the QASM3 syntax token +pub(crate) fn span_for_syntax_token(token: &oq3_syntax::SyntaxToken) -> Span { + text_range_to_span(token.text_range()) +} + +/// The QASM3 parser stores integers as u128, any conversion we do +/// must not crash if the value is too large to fit in the target type +/// and instead return None. +/// Safe in the following functions means that the conversion will not +/// panic if the value is too large to fit in the target type. +/// +/// Values may be truncated or rounded as necessary. + +pub(crate) fn safe_u128_to_f64(value: u128) -> Option { + if value <= u128::from(i64::MAX as u64) { + let value = i64::try_from(value).ok()?; + safe_i64_to_f64(value) + } else { + None + } +} + +pub(crate) fn safe_i64_to_f64(value: i64) -> Option { + #[allow(clippy::cast_possible_truncation)] + if value <= f64::MAX as i64 { + #[allow(clippy::cast_precision_loss)] + Some(value as f64) + } else { + None + } +} + +pub(crate) fn safe_u64_to_f64(value: u64) -> Option { + #[allow(clippy::cast_possible_truncation)] + #[allow(clippy::cast_sign_loss)] + if value <= f64::MAX as u64 { + #[allow(clippy::cast_precision_loss)] + Some(value as f64) + } else { + None + } +} + +/// The spec defines a designator as ```designator: LBRACKET expression RBRACKET;``` +/// However, in every use case, the expression is a literal integer. +/// This isn't enforced by the parser/grammar, but we can assume it here. +/// For example, when describing qubit arrays, the spec says: +/// - The label ```name[j]``` refers to a qubit of this register, where +/// ```j element_of {0, 1, ... size(name)-1}``` is an integer. +pub(crate) fn extract_dims_from_designator(designator: Option) -> Option { + let designator = designator?; + match designator.expr() { + Some(Expr::Literal(lit)) => match lit.kind() { + LiteralKind::IntNumber(int) => { + // qasm parser stores ints as u128 + let value = int.value().expect("Designator must be a literal integer"); + let value: u32 = u32::try_from(value).expect("Designator must fit in u32"); + Some(value) + } + _ => { + unreachable!("designator must be a literal integer") + } + }, + None => None, + _ => { + unreachable!("designator must be a literal integer") + } + } +} + +/// The designator must be accessed differently depending on the type. +/// For complex types, the designator is stored in the scalar type. +/// For other types, the designator is stored in the type itself. +pub(crate) fn get_designator_from_scalar_type( + ty: &oq3_syntax::ast::ScalarType, +) -> Option { + if let Some(scalar_ty) = ty.scalar_type() { + // we have a complex type, need to grab the inner + // designator for the width + scalar_ty.designator() + } else { + ty.designator() + } +} + +/// Symmetric arithmetic conversions are applied to: +/// binary arithmetic *, /, %, +, - +/// relational operators <, >, <=, >=, ==, != +/// binary bitwise arithmetic &, ^, |, +pub(crate) fn requires_symmetric_conversion(op: BinaryOp) -> bool { + match op { + BinaryOp::LogicOp(_) | BinaryOp::CmpOp(_) => true, + BinaryOp::ArithOp(arith_op) => match arith_op { + ArithOp::Mul + | ArithOp::Div + | ArithOp::Rem + | ArithOp::Add + | ArithOp::Sub + | ArithOp::BitAnd + | ArithOp::BitXor + | ArithOp::BitOr => true, + ArithOp::Shl | ArithOp::Shr => false, + }, + #[allow(clippy::match_same_arms)] + BinaryOp::ConcatenationOp => { + // concatenation is a special case where we can't have a symmetric conversion + // as the lhs and rhs must be of the same type + false + } + BinaryOp::Assignment { op: _ } => false, + } +} + +pub(crate) fn requires_types_already_match_conversion(op: BinaryOp) -> bool { + match op { + BinaryOp::ConcatenationOp => { + // concatenation is a special case where we can't have a symmetric conversion + // as the lhs and rhs must be of the same type + true + } + _ => false, + } +} + +// integer promotions are applied only to both operands of +// the shift operators << and >> +pub(crate) fn binop_requires_symmetric_int_conversion(op: BinaryOp) -> bool { + match op { + BinaryOp::ArithOp(arith_op) => matches!(arith_op, ArithOp::Shl | ArithOp::Shr), + BinaryOp::Assignment { op } => matches!(op, Some(ArithOp::Shl | ArithOp::Shr)), + _ => false, + } +} + +/// some literals can be cast to a specific type if the value is known +/// This is useful to avoid generating a cast expression in the AST +pub(crate) fn can_cast_literal_with_value_knowledge(lhs_ty: &Type, literal: &Literal) -> bool { + if matches!(lhs_ty, &Type::Bit(..)) { + if let LiteralKind::IntNumber(value) = literal.kind() { + let value = value.value().expect("IntNumber must have a value"); + return value == 0 || value == 1; + } + } + if matches!(lhs_ty, &Type::UInt(..)) { + if let LiteralKind::IntNumber(_) = literal.kind() { + // Value can't be negative as IntNumber is unsigned + // any sign would come from a prefix expression + return true; + } + } + false +} diff --git a/compiler/qsc_qasm3/src/oqasm_types.rs b/compiler/qsc_qasm3/src/oqasm_types.rs new file mode 100644 index 0000000000..a2475b246d --- /dev/null +++ b/compiler/qsc_qasm3/src/oqasm_types.rs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::cmp::max; + +use oq3_semantics::types::{ArrayDims, IsConst, Type}; + +/// When two types are combined, the result is a type that can represent both. +/// For constness, the result is const iff both types are const. +fn relax_constness(lhs_ty: &Type, rhs_ty: &Type) -> IsConst { + IsConst::from(lhs_ty.is_const() && rhs_ty.is_const()) +} + +/// Having no width means that the type is not a fixed-width type +/// and can hold any explicit width. If both types have a width, +/// the result is the maximum of the two. Otherwise, the result +/// is a type without a width. +fn promote_width(lhs_ty: &Type, rhs_ty: &Type) -> Option { + match (lhs_ty.width(), rhs_ty.width()) { + (Some(w1), Some(w2)) => Some(max(w1, w2)), + (Some(_) | None, None) | (None, Some(_)) => None, + } +} + +fn get_effective_width(lhs_ty: &Type, rhs_ty: &Type) -> Option { + match (lhs_ty.width(), rhs_ty.width()) { + (Some(w1), Some(w2)) => Some(max(w1, w2)), + (Some(w), None) | (None, Some(w)) => Some(w), + (None, None) => None, + } +} + +/// If both can be promoted to a common type, the result is that type. +/// If the types are not compatible, the result is `Type::Void`. +pub fn promote_types(lhs_ty: &Type, rhs_ty: &Type) -> Type { + if types_equal_except_const(lhs_ty, rhs_ty) { + return lhs_ty.clone(); + } + let ty = promote_types_symmetric(lhs_ty, rhs_ty); + if ty != Type::Void { + return ty; + } + let ty = promote_types_asymmetric(lhs_ty, rhs_ty); + if ty == Type::Void { + return promote_types_asymmetric(rhs_ty, lhs_ty); + } + ty +} + +pub(crate) fn promote_to_uint_ty( + lhs_ty: &Type, + rhs_ty: &Type, +) -> (Option, Option, Option) { + let is_const = relax_constness(lhs_ty, rhs_ty); + let lhs_ty = get_uint_ty(lhs_ty); + let rhs_ty = get_uint_ty(rhs_ty); + match (lhs_ty, rhs_ty) { + (Some(lhs_ty), Some(rhs_ty)) => { + let width = get_effective_width(&lhs_ty, &rhs_ty); + ( + Some(Type::UInt(width, is_const)), + Some(lhs_ty), + Some(rhs_ty), + ) + } + (Some(lhs_ty), None) => (None, Some(lhs_ty), None), + (None, Some(rhs_ty)) => (None, None, Some(rhs_ty)), + (None, None) => (None, None, None), + } +} + +fn get_uint_ty(ty: &Type) -> Option { + if matches!(ty, Type::UInt(..) | Type::Angle(..)) { + Some(Type::UInt(ty.width(), ty.is_const().into())) + } else if let Type::BitArray(dims, _) = ty { + match dims { + ArrayDims::D1(d) => Some(Type::UInt( + Some(u32::try_from(*d).ok()?), + ty.is_const().into(), + )), + _ => None, + } + } else { + None + } +} + +/// Promotes two types if they share a common base type with +/// their constness relaxed, and their width promoted. +/// If the types are not compatible, the result is `Type::Void`. +fn promote_types_symmetric(lhs_ty: &Type, rhs_ty: &Type) -> Type { + let is_const = relax_constness(lhs_ty, rhs_ty); + match (lhs_ty, rhs_ty) { + (Type::Bit(..), Type::Bit(..)) => Type::Bit(is_const), + (Type::Bool(..), Type::Bool(..)) => Type::Bool(is_const), + (Type::Int(..), Type::Int(..)) => Type::Int(promote_width(lhs_ty, rhs_ty), is_const), + (Type::UInt(..), Type::UInt(..)) => Type::UInt(promote_width(lhs_ty, rhs_ty), is_const), + (Type::Angle(..), Type::Angle(..)) => Type::Angle(promote_width(lhs_ty, rhs_ty), is_const), + (Type::Float(..), Type::Float(..)) => Type::Float(promote_width(lhs_ty, rhs_ty), is_const), + (Type::Complex(..), Type::Complex(..)) => { + Type::Complex(promote_width(lhs_ty, rhs_ty), is_const) + } + _ => Type::Void, + } +} + +/// Promotion follows casting rules. We only match one way, as the +/// both combinations are covered by calling this function twice +/// with the arguments swapped. +/// +/// If the types are not compatible, the result is `Type::Void`. +/// +/// The left-hand side is the type to promote from, and the right-hand +/// side is the type to promote to. So any promotion goes from lesser +/// type to greater type. +/// +/// This is more complicated as we have C99 promotion for simple types, +/// but complex types like `Complex`, and `Angle` don't follow those rules. +fn promote_types_asymmetric(lhs_ty: &Type, rhs_ty: &Type) -> Type { + let is_const = relax_constness(lhs_ty, rhs_ty); + #[allow(clippy::match_same_arms)] + match (lhs_ty, rhs_ty) { + (Type::Bit(..), Type::Bool(..)) => Type::Bool(is_const), + (Type::Bit(..), Type::Int(w, _)) => Type::Int(*w, is_const), + (Type::Bit(..), Type::UInt(w, _)) => Type::UInt(*w, is_const), + + (Type::Bit(..), Type::Angle(w, _)) => Type::Angle(*w, is_const), + + (Type::Bool(..), Type::Int(w, _)) => Type::Int(*w, is_const), + (Type::Bool(..), Type::UInt(w, _)) => Type::UInt(*w, is_const), + (Type::Bool(..), Type::Float(w, _)) => Type::Float(*w, is_const), + (Type::Bool(..), Type::Complex(w, _)) => Type::Complex(*w, is_const), + + (Type::UInt(..), Type::Int(..)) => Type::Int(promote_width(lhs_ty, rhs_ty), is_const), + (Type::UInt(..), Type::Float(..)) => Type::Float(promote_width(lhs_ty, rhs_ty), is_const), + (Type::UInt(..), Type::Complex(..)) => { + Type::Complex(promote_width(lhs_ty, rhs_ty), is_const) + } + + (Type::Int(..), Type::Float(..)) => Type::Float(promote_width(lhs_ty, rhs_ty), is_const), + (Type::Int(..), Type::Complex(..)) => { + Type::Complex(promote_width(lhs_ty, rhs_ty), is_const) + } + (Type::Angle(..), Type::Float(..)) => Type::Float(promote_width(lhs_ty, rhs_ty), is_const), + (Type::Float(..), Type::Complex(..)) => { + Type::Complex(promote_width(lhs_ty, rhs_ty), is_const) + } + _ => Type::Void, + } +} + +/// Compares two types for equality, ignoring constness. +pub(crate) fn types_equal_except_const(lhs: &Type, rhs: &Type) -> bool { + match (lhs, rhs) { + (Type::Bit(_), Type::Bit(_)) + | (Type::Qubit, Type::Qubit) + | (Type::HardwareQubit, Type::HardwareQubit) + | (Type::Bool(_), Type::Bool(_)) + | (Type::Duration(_), Type::Duration(_)) + | (Type::Stretch(_), Type::Stretch(_)) + | (Type::Range, Type::Range) + | (Type::Set, Type::Set) + | (Type::Void, Type::Void) + | (Type::ToDo, Type::ToDo) + | (Type::Undefined, Type::Undefined) => true, + (Type::Int(lhs_width, _), Type::Int(rhs_width, _)) + | (Type::UInt(lhs_width, _), Type::UInt(rhs_width, _)) + | (Type::Float(lhs_width, _), Type::Float(rhs_width, _)) + | (Type::Angle(lhs_width, _), Type::Angle(rhs_width, _)) + | (Type::Complex(lhs_width, _), Type::Complex(rhs_width, _)) => lhs_width == rhs_width, + (Type::BitArray(lhs_dims, _), Type::BitArray(rhs_dims, _)) + | (Type::QubitArray(lhs_dims), Type::QubitArray(rhs_dims)) + | (Type::IntArray(lhs_dims), Type::IntArray(rhs_dims)) + | (Type::UIntArray(lhs_dims), Type::UIntArray(rhs_dims)) + | (Type::FloatArray(lhs_dims), Type::FloatArray(rhs_dims)) + | (Type::AngleArray(lhs_dims), Type::AngleArray(rhs_dims)) + | (Type::ComplexArray(lhs_dims), Type::ComplexArray(rhs_dims)) + | (Type::BoolArray(lhs_dims), Type::BoolArray(rhs_dims)) => lhs_dims == rhs_dims, + (Type::Gate(lhs_cargs, lhs_qargs), Type::Gate(rhs_cargs, rhs_qargs)) => { + lhs_cargs == rhs_cargs && lhs_qargs == rhs_qargs + } + _ => false, + } +} diff --git a/compiler/qsc_qasm3/src/parse.rs b/compiler/qsc_qasm3/src/parse.rs new file mode 100644 index 0000000000..313fd2f0ff --- /dev/null +++ b/compiler/qsc_qasm3/src/parse.rs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::io::SourceResolver; +use crate::oqasm_helpers::text_range_to_span; +use oq3_syntax::SyntaxNode; +use oq3_syntax::{ast::Stmt, ParseOrErrors, SourceFile}; +use qsc::{error::WithSource, SourceMap}; +use std::path::{Path, PathBuf}; +use std::sync::Arc; + +#[cfg(test)] +pub(crate) mod tests; + +pub struct QasmParseResult { + pub source: QasmSource, + pub source_map: SourceMap, +} + +impl QasmParseResult { + #[must_use] + pub fn new(source: QasmSource) -> QasmParseResult { + let source_map = create_source_map(&source); + QasmParseResult { source, source_map } + } + + #[must_use] + pub fn has_errors(&self) -> bool { + self.source.has_errors() + } + + pub fn all_errors(&self) -> Vec> { + let mut self_errors = self.errors(); + let include_errors = self + .source + .includes() + .iter() + .flat_map(QasmSourceTrait::all_errors) + .map(|e| self.map_error(e)); + + self_errors.extend(include_errors); + self_errors + } + + #[must_use] + pub fn errors(&self) -> Vec> { + self.source + .errors() + .iter() + .map(|e| self.map_error(e.clone())) + .collect() + } + + fn map_error(&self, error: crate::Error) -> WithSource { + let path = self.source.path().display().to_string(); + let source = self.source_map.find_by_name(&path); + let offset = source.map_or(0, |source| source.offset); + + let offset_error = error.with_offset(offset); + + WithSource::from_map(&self.source_map, offset_error) + } +} + +/// Parse a QASM file and return the parse result. +/// This function will resolve includes using the provided resolver. +/// If an include file cannot be resolved, an error will be returned. +/// If a file is included recursively, a stack overflow occurs. +pub fn parse_source(source: S, path: P, resolver: &R) -> miette::Result +where + S: AsRef, + P: AsRef, + R: SourceResolver, +{ + let res = parse_qasm_source(source, path, resolver)?; + Ok(QasmParseResult::new(res)) +} + +/// Creates a Q# source map from a QASM parse output. The `QasmSource` +/// has all of the recursive includes resolved with their own source +/// and parse results. +fn create_source_map(source: &QasmSource) -> SourceMap { + let mut files: Vec<(Arc, Arc)> = Vec::new(); + files.push(( + Arc::from(source.path().to_string_lossy().to_string()), + Arc::from(source.source()), + )); + // Collect all source files from the includes, this + // begins the recursive process of collecting all source files. + for include in source.includes() { + collect_source_files(include, &mut files); + } + // Map the main source file to the entry point expression + // This may be incorrect, but it's the best we can do for now. + SourceMap::new(files, Some(Arc::from(source.source()))) +} + +/// Recursively collect all source files from the includes +fn collect_source_files(source: &QasmSource, files: &mut Vec<(Arc, Arc)>) { + files.push(( + Arc::from(source.path().to_string_lossy().to_string()), + Arc::from(source.source()), + )); + for include in source.includes() { + collect_source_files(include, files); + } +} + +/// Represents a QASM source file that has been parsed. +#[derive(Clone, Debug)] +pub struct QasmSource { + /// The path to the source file. This is used for error reporting. + /// This path is just a name, it does not have to exist on disk. + path: PathBuf, + /// The source code of the file. + source: String, + /// The parsed AST of the source file or any parse errors. + ast: ParseOrErrors, + /// Any included files that were resolved. + /// Note that this is a recursive structure. + included: Vec, +} + +pub trait QasmSourceTrait { + fn has_errors(&self) -> bool { + if !self.parse_result().errors().is_empty() { + return true; + } + self.includes().iter().any(QasmSourceTrait::has_errors) + } + fn all_errors(&self) -> Vec { + let mut self_errors = self.errors(); + let include_errors = self.includes().iter().flat_map(QasmSourceTrait::all_errors); + self_errors.extend(include_errors); + self_errors + } + fn includes(&self) -> &Vec; + fn source(&self) -> &str; + fn parse_result(&self) -> &ParseOrErrors; + fn errors(&self) -> Vec; + fn path(&self) -> PathBuf; + + fn tree(&self) -> oq3_syntax::SourceFile { + self.parse_result().tree() + } + fn syntax_node(&self) -> SyntaxNode { + self.parse_result().syntax_node() + } +} + +impl QasmSource { + pub fn new, P: AsRef>( + source: T, + file_path: P, + ast: ParseOrErrors, + included: Vec, + ) -> QasmSource { + QasmSource { + source: source.as_ref().to_owned(), + path: file_path.as_ref().to_owned(), + ast, + included, + } + } +} + +impl QasmSourceTrait for QasmSource { + fn includes(&self) -> &Vec { + self.included.as_ref() + } + + fn parse_result(&self) -> &ParseOrErrors { + &self.ast + } + + fn path(&self) -> PathBuf { + self.path.clone() + } + + fn errors(&self) -> Vec { + self.parse_result() + .errors() + .iter() + .map(|e| { + crate::Error(crate::ErrorKind::Parse( + e.message().to_string(), + text_range_to_span(e.range()), + )) + }) + .collect() + } + + fn source(&self) -> &str { + self.source.as_ref() + } +} + +/// Parse a QASM file and return the parse result using the provided resolver. +/// Returns `Err` if the resolver cannot resolve the file. +/// Returns `Ok` otherwise. Any parse errors will be included in the result. +/// +/// This function is the start of a recursive process that will resolve all +/// includes in the QASM file. Any includes are parsed as if their contents +/// were defined where the include statement is. +fn parse_qasm_file(path: P, resolver: &R) -> miette::Result +where + P: AsRef, + R: SourceResolver, +{ + let (path, source) = resolver.resolve(&path)?; + parse_qasm_source(source, path, resolver) +} + +fn parse_qasm_source(source: S, path: P, resolver: &R) -> miette::Result +where + S: AsRef, + P: AsRef, + R: SourceResolver, +{ + let (parse_result, includes) = parse_source_and_includes(source.as_ref(), resolver)?; + Ok(QasmSource::new(source, path, parse_result, includes)) +} + +fn parse_source_and_includes, R>( + source: P, + resolver: &R, +) -> miette::Result<(ParseOrErrors, Vec)> +where + R: SourceResolver, +{ + let parse_result = oq3_syntax::SourceFile::parse_check_lex(source.as_ref()); + if parse_result.have_parse() { + let included = parse_includes(&parse_result, resolver)?; + Ok((parse_result, included)) + } else { + Ok((parse_result, vec![])) + } +} + +fn parse_includes( + syntax_ast: &ParseOrErrors, + resolver: &R, +) -> miette::Result> +where + R: SourceResolver, +{ + let mut includes = vec![]; + for stmt in syntax_ast.tree().statements() { + if let Stmt::Include(include) = stmt { + if let Some(file) = include.file() { + if let Some(file_path) = file.to_string() { + // Skip the standard gates include file. + // Handling of this file is done by the compiler. + if file_path.to_lowercase() == "stdgates.inc" { + continue; + } + let source = parse_qasm_file(file_path, resolver)?; + includes.push(source); + } + } + } + } + + Ok(includes) +} diff --git a/compiler/qsc_qasm3/src/parse/tests.rs b/compiler/qsc_qasm3/src/parse/tests.rs new file mode 100644 index 0000000000..72a49b4367 --- /dev/null +++ b/compiler/qsc_qasm3/src/parse/tests.rs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + parse::QasmSourceTrait, + tests::{parse, parse_all}, +}; +use miette::Report; + +#[test] +fn simple_programs_can_be_parsed() -> miette::Result<(), Vec> { + let source = r#"OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + "#; + let _ = parse(source)?; + Ok(()) +} + +#[test] +fn programs_with_includes_can_be_parsed() -> miette::Result<(), Vec> { + let source0 = r#"OPENQASM 3.0; + include "stdgates.inc"; + include "source1.qasm"; + qubit q1; + "#; + let source1 = "qubit q2; + "; + let all_sources = [ + ("source0.qasm".into(), source0.into()), + ("source1.qasm".into(), source1.into()), + ]; + + let res = parse_all("source0.qasm", all_sources)?; + assert!(res.source.includes().len() == 1); + Ok(()) +} + +#[test] +fn programs_with_includes_with_includes_can_be_parsed() -> miette::Result<(), Vec> { + let source0 = r#"OPENQASM 3.0; + include "stdgates.inc"; + include "source1.qasm"; + qubit q1; + "#; + let source1 = r#"include "source2.qasm"; + qubit q2; + "#; + let source2 = "qubit q3; + "; + let all_sources = [ + ("source0.qasm".into(), source0.into()), + ("source1.qasm".into(), source1.into()), + ("source2.qasm".into(), source2.into()), + ]; + + let res = parse_all("source0.qasm", all_sources)?; + assert!(res.source.includes().len() == 1); + assert!(res.source.includes()[0].includes().len() == 1); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/runtime.rs b/compiler/qsc_qasm3/src/runtime.rs new file mode 100644 index 0000000000..c06c609567 --- /dev/null +++ b/compiler/qsc_qasm3/src/runtime.rs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use bitflags::bitflags; + +use qsc::{ + ast::{Stmt, TopLevelNode}, + LanguageFeatures, +}; + +/// Runtime functions that are used in the generated AST. +/// These functions are not part of the QASM3 standard, but are used to implement +/// utility fuctions that would be cumbersome to implement building the AST +/// directly. + +/// The POW function is used to implement the `pow` modifier in QASM3 for integers. +const POW: &str = " +operation __Pow__<'T>(N: Int, op: ('T => Unit is Adj), target : 'T) : Unit is Adj { + let op = if N > 0 { () => op(target) } else { () => Adjoint op(target) }; + for _ in 1..Microsoft.Quantum.Math.AbsI(N) { + op() + } +} +"; + +/// The ``BARRIER`` function is used to implement the `barrier` statement in QASM3. +/// The `@SimulatableIntrinsic` attribute is used to mark the operation for QIR +/// generation. +/// Q# doesn't support barriers, so this is a no-op. We need to figure out what +/// barriers mean in the context of QIR in the future for better support. +const BARRIER: &str = " +@SimulatableIntrinsic() +operation __quantum__qis__barrier__body() : Unit {} +"; + +/// The ``BOOL_AS_RESULT`` function is used to implement the cast expr in QASM3 for bool to bit. +/// This already exists in the Q# library, but is defined as a marker for casts from QASM3. +const BOOL_AS_RESULT: &str = " +function __BoolAsResult__(input: Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) +} +"; + +/// The ``BOOL_AS_INT`` function is used to implement the cast expr in QASM3 for bool to int. +const BOOL_AS_INT: &str = " +function __BoolAsInt__(value: Bool) : Int { + if value { + 1 + } else { + 0 + } +} +"; + +/// The ``BOOL_AS_BIGINT`` function is used to implement the cast expr in QASM3 for bool to big int. +const BOOL_AS_BIGINT: &str = " +function __BoolAsBigInt__(value: Bool) : BigInt { + if value { + 1L + } else { + 0L + } +} +"; + +/// The ``BOOL_AS_DOUBLE`` function is used to implement the cast expr in QASM3 for bool to int. +const BOOL_AS_DOUBLE: &str = " +function __BoolAsDouble__(value: Bool) : Double { + if value { + 1. + } else { + 0. + } +} +"; + +/// The ``RESULT_AS_BOOL`` function is used to implement the cast expr in QASM3 for bit to bool. +/// This already exists in the Q# library, but is defined as a marker for casts from QASM3. +const RESULT_AS_BOOL: &str = " +function __ResultAsBool__(input: Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) +} +"; + +/// The ``RESULT_AS_INT`` function is used to implement the cast expr in QASM3 for bit to bool. +const RESULT_AS_INT: &str = " +function __ResultAsInt__(input: Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } +} +"; + +/// The ``RESULT_AS_BIGINT`` function is used to implement the cast expr in QASM3 for bit to bool. +const RESULT_AS_BIGINT: &str = " +function __ResultAsBigInt__(input: Result) : BigInt { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1L + } else { + 0L + } +} +"; + +/// The ``INT_AS_RESULT_ARRAY_BE`` function is used to implement the cast expr in QASM3 for int to bit[]. +/// with big-endian order. This is needed for round-trip conversion for bin ops. +const INT_AS_RESULT_ARRAY_BE: &str = " +function __IntAsResultArrayBE__(number : Int, bits : Int) : Result[] { + mutable runningValue = number; + mutable result = []; + for _ in 1..bits { + set result += [__BoolAsResult__((runningValue &&& 1) != 0)]; + set runningValue >>>= 1; + } + Microsoft.Quantum.Arrays.Reversed(result) +} +"; + +/// The ``RESULT_ARRAY_AS_INT_BE`` function is used to implement the cast expr in QASM3 for bit[] to uint. +/// with big-endian order. This is needed for round-trip conversion for bin ops. +const RESULT_ARRAY_AS_INT_BE: &str = " +function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) +} +"; + +/// Runtime functions that are used in the generated AST. +/// Once compilation is complete, we can use this to determine +/// which runtime functions need to be included in the AST. +#[derive(Clone, Debug, PartialOrd, Ord, PartialEq, Eq, Copy)] +pub struct RuntimeFunctions(u16); + +bitflags! { + impl RuntimeFunctions: u16 { + const Pow = 0b1; + const Barrier = 0b10; + const BoolAsResult = 0b100; + const BoolAsInt = 0b1_000; + const BoolAsBigInt = 0b10_000; + const BoolAsDouble = 0b100_000; + const ResultAsBool = 0b1_000_000; + const ResultAsInt = 0b10_000_000; + const ResultAsBigInt = 0b100_000_000; + /// IntAsResultArray requires BoolAsResult to be included. + const IntAsResultArrayBE = 0b1_000_000_000 | 0b100; + const ResultArrayAsIntBE = 0b10_000_000_000; + } +} + +impl Default for RuntimeFunctions { + fn default() -> Self { + RuntimeFunctions::empty() + } +} + +pub(crate) fn get_pow_decl() -> Stmt { + parse_stmt(POW) +} + +pub(crate) fn get_barrier_decl() -> Stmt { + parse_stmt(BARRIER) +} + +pub(crate) fn get_bool_as_result_decl() -> Stmt { + parse_stmt(BOOL_AS_RESULT) +} + +pub(crate) fn get_bool_as_int_decl() -> Stmt { + parse_stmt(BOOL_AS_INT) +} + +pub(crate) fn get_bool_as_bigint_decl() -> Stmt { + parse_stmt(BOOL_AS_BIGINT) +} + +pub(crate) fn get_bool_as_double_decl() -> Stmt { + parse_stmt(BOOL_AS_DOUBLE) +} + +pub(crate) fn get_int_as_result_array_be_decl() -> Stmt { + parse_stmt(INT_AS_RESULT_ARRAY_BE) +} + +pub(crate) fn get_result_as_bool_decl() -> Stmt { + parse_stmt(RESULT_AS_BOOL) +} + +pub(crate) fn get_result_as_bigint_decl() -> Stmt { + parse_stmt(RESULT_AS_BIGINT) +} + +pub(crate) fn get_result_as_int_decl() -> Stmt { + parse_stmt(RESULT_AS_INT) +} + +pub(crate) fn get_result_array_as_int_be_decl() -> Stmt { + parse_stmt(RESULT_ARRAY_AS_INT_BE) +} + +fn parse_stmt(name: &str) -> Stmt { + let (nodes, errors) = qsc::parse::top_level_nodes(name, LanguageFeatures::default()); + assert!(errors.is_empty(), "Failed to parse POW: {errors:?}"); + assert!( + nodes.len() == 1, + "Expected one top-level node, found {:?}", + nodes.len() + ); + match nodes.into_iter().next().expect("no top-level nodes found") { + TopLevelNode::Namespace(..) => { + panic!("Expected operation, got Namespace") + } + TopLevelNode::Stmt(stmt) => *stmt, + } +} + +pub(crate) fn get_runtime_function_decls(runtime: RuntimeFunctions) -> Vec { + let mut stmts = vec![]; + if runtime.contains(RuntimeFunctions::Pow) { + let stmt = crate::runtime::get_pow_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::Barrier) { + let stmt = crate::runtime::get_barrier_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::BoolAsBigInt) { + let stmt = crate::runtime::get_bool_as_bigint_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::BoolAsDouble) { + let stmt = crate::runtime::get_bool_as_double_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::BoolAsInt) { + let stmt = crate::runtime::get_bool_as_int_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::BoolAsResult) { + let stmt = crate::runtime::get_bool_as_result_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::IntAsResultArrayBE) { + let stmt = crate::runtime::get_int_as_result_array_be_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::ResultAsBool) { + let stmt = crate::runtime::get_result_as_bool_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::ResultAsBigInt) { + let stmt = crate::runtime::get_result_as_bigint_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::ResultAsInt) { + let stmt = crate::runtime::get_result_as_int_decl(); + stmts.push(stmt); + } + if runtime.contains(RuntimeFunctions::ResultArrayAsIntBE) { + let stmt = crate::runtime::get_result_array_as_int_be_decl(); + stmts.push(stmt); + } + stmts +} diff --git a/compiler/qsc_qasm3/src/symbols.rs b/compiler/qsc_qasm3/src/symbols.rs new file mode 100644 index 0000000000..5d03ad4d2f --- /dev/null +++ b/compiler/qsc_qasm3/src/symbols.rs @@ -0,0 +1,458 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use oq3_semantics::types::{IsConst, Type}; +use qsc::Span; +use rustc_hash::FxHashMap; + +/// We need a symbol table to keep track of the symbols in the program. +/// The scoping rules for QASM3 are slightly different from Q#. This also +/// means that we need to keep track of the input and output symbols in the +/// program. Additionally, we need to keep track of the types of the symbols +/// in the program for type checking. +/// Q# and QASM have different type systems, so we track the QASM semantic +/// type in addition to the corresponding Q# type. + +/// A symbol ID is a unique identifier for a symbol in the symbol table. +/// This is used to look up symbols in the symbol table. +/// Every symbol in the symbol table has a unique ID. +#[derive(Debug, Default, Clone, Copy)] +pub struct SymbolId(pub u32); + +impl SymbolId { + /// The successor of this ID. + #[must_use] + pub fn successor(self) -> Self { + Self(self.0 + 1) + } +} + +impl From for SymbolId { + fn from(val: u32) -> Self { + SymbolId(val) + } +} + +impl From for u32 { + fn from(id: SymbolId) -> Self { + id.0 + } +} + +impl From for usize { + fn from(value: SymbolId) -> Self { + value.0 as usize + } +} + +impl From for SymbolId { + fn from(value: usize) -> Self { + SymbolId( + value.try_into().unwrap_or_else(|_| { + panic!("Value, {value}, does not fit into {}", stringify!($id)) + }), + ) + } +} + +impl PartialEq for SymbolId { + fn eq(&self, other: &Self) -> bool { + self.0 == other.0 + } +} + +impl Eq for SymbolId {} + +impl PartialOrd for SymbolId { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for SymbolId { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.0.cmp(&other.0) + } +} + +impl std::hash::Hash for SymbolId { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + +impl std::fmt::Display for SymbolId { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + std::fmt::Display::fmt(&self.0, f) + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct Symbol { + pub name: String, + pub span: Span, + pub ty: Type, + pub qsharp_ty: crate::types::Type, + pub io_kind: IOKind, +} + +/// A symbol in the symbol table. +/// Default Q# type is Unit +impl Default for Symbol { + fn default() -> Self { + Self { + name: String::default(), + span: Span::default(), + ty: Type::Undefined, + qsharp_ty: crate::types::Type::Tuple(vec![]), + io_kind: IOKind::default(), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SymbolError { + /// The symbol was not found in the symbol table. + NotFound, + /// The symbol already exists in the symbol table, at the current scope. + AlreadyExists, +} + +/// Symbols have a an I/O kind that determines if they are input or output, or unspecified. +/// The default I/O kind means no explicit kind was part of the decl. +/// There is a specific statement for io decls which sets the I/O kind appropriately. +/// This is used to determine the input and output symbols in the program. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IOKind { + Default, + Input, + Output, +} + +impl Default for IOKind { + fn default() -> Self { + Self::Default + } +} + +/// A scope is a collection of symbols and a kind. The kind determines semantic +/// rules for compliation as shadowing and decl rules vary by scope kind. +pub struct Scope { + /// A map from symbol name to symbol ID. + name_to_id: FxHashMap, + /// A map from symbol ID to symbol. + id_to_symbol: FxHashMap, + /// The order in which symbols were inserted into the scope. + /// This is used to determine the order of symbols in the output. + order: Vec, + /// The kind of the scope. + kind: ScopeKind, +} + +impl Scope { + pub fn new(kind: ScopeKind) -> Self { + Self { + name_to_id: FxHashMap::default(), + id_to_symbol: FxHashMap::default(), + order: vec![], + kind, + } + } + + /// Inserts the symbol into the current scope. + /// Returns the ID of the symbol. + /// + /// # Errors + /// + /// This function will return an error if a symbol of the same name has already + /// been declared in this scope. + pub fn insert_symbol(&mut self, id: SymbolId, symbol: Symbol) -> Result<(), SymbolError> { + if self.name_to_id.contains_key(&symbol.name) { + return Err(SymbolError::AlreadyExists); + } + self.name_to_id.insert(symbol.name.clone(), id); + self.id_to_symbol.insert(id, symbol); + self.order.push(id); + Ok(()) + } + + pub fn get_symbol_by_name(&self, name: &str) -> Option<&Symbol> { + self.name_to_id + .get(name) + .and_then(|id| self.id_to_symbol.get(id)) + } + + pub fn get_symbol_by_id(&self, id: SymbolId) -> Option<&Symbol> { + self.id_to_symbol.get(&id) + } + + fn get_ordered_symbols(&self) -> Vec { + self.order + .iter() + .map(|id| self.id_to_symbol.get(id).expect("ID should exist").clone()) + .collect() + } +} + +/// A symbol table is a collection of scopes and manages the symbol ids. +pub struct SymbolTable { + scopes: Vec, + current_id: SymbolId, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ScopeKind { + /// Global scope, which is the current scope only when no other scopes are active. + /// This is the only scope where gates, qubits, and arrays can be declared. + Global, + Function, + Gate, + Block, +} + +const BUILTIN_SYMBOLS: [&str; 6] = ["pi", "π", "tau", "τ", "euler", "ℇ"]; + +impl SymbolTable { + pub fn new() -> Self { + let global = Scope::new(ScopeKind::Global); + + let mut slf = Self { + scopes: vec![global], + current_id: SymbolId::default(), + }; + + // Define global constants + for symbol in BUILTIN_SYMBOLS { + slf.insert_symbol(Symbol { + name: symbol.to_string(), + span: Span::default(), + ty: Type::Float(None, IsConst::True), + qsharp_ty: crate::types::Type::Double(true), + io_kind: IOKind::Default, + }) + .unwrap_or_else(|_| panic!("Failed to insert symbol: {symbol}")); + } + + slf + } + + pub fn push_scope(&mut self, kind: ScopeKind) { + assert!(kind != ScopeKind::Global, "Cannot push a global scope"); + self.scopes.push(Scope::new(kind)); + } + + pub fn pop_scope(&mut self) { + assert!(self.scopes.len() != 1, "Cannot pop the global scope"); + self.scopes.pop(); + } + + pub fn insert_symbol(&mut self, symbol: Symbol) -> Result { + let id = self.current_id; + self.current_id = self.current_id.successor(); + self.scopes + .last_mut() + .expect("At least one scope should be available") + .insert_symbol(id, symbol)?; + + Ok(id) + } + + pub fn get_symbol_by_name(&self, name: &str) -> Option<&Symbol> { + let scopes = self.scopes.iter().rev(); + let predicate = |x: &Scope| { + x.kind == ScopeKind::Block || x.kind == ScopeKind::Function || x.kind == ScopeKind::Gate + }; + + // Use scan to track the last item that returned false + let mut last_false = None; + let _ = scopes + .scan(&mut last_false, |state, item| { + if !predicate(item) { + **state = Some(item); + } + Some(predicate(item)) + }) + .take_while(|&x| x) + .last(); + let mut scopes = self.scopes.iter().rev(); + while let Some(scope) = scopes + .by_ref() + .take_while(|arg0: &&Scope| predicate(arg0)) + .next() + { + if let Some(symbol) = scope.get_symbol_by_name(name) { + return Some(symbol); + } + } + + if let Some(scope) = last_false { + if let Some(symbol) = scope.get_symbol_by_name(name) { + if symbol.ty.is_const() + || matches!(symbol.ty, Type::Gate(..) | Type::Void) + || self.is_scope_rooted_in_global() + { + return Some(symbol); + } + } + } + // we should be at the global, function, or gate scope now + for scope in scopes { + if let Some(symbol) = scope.get_symbol_by_name(name) { + if symbol.ty.is_const() || matches!(symbol.ty, Type::Gate(..) | Type::Void) { + return Some(symbol); + } + } + } + + None + } + + pub fn get_symbol_by_id(&self, id: SymbolId) -> Option<&Symbol> { + for scope in self.scopes.iter().rev() { + if let Some(symbol) = scope.get_symbol_by_id(id) { + return Some(symbol); + } + } + None + } + + pub fn is_current_scope_global(&self) -> bool { + matches!(self.scopes.last(), Some(scope) if scope.kind == ScopeKind::Global) + } + + pub fn is_scope_rooted_in_subroutine(&self) -> bool { + self.scopes + .iter() + .rev() + .any(|scope| scope.kind == ScopeKind::Function) + } + + pub fn is_scope_rooted_in_gate(&self) -> bool { + self.scopes + .iter() + .rev() + .any(|scope| scope.kind == ScopeKind::Gate) + } + + pub fn is_scope_rooted_in_global(&self) -> bool { + for scope in self.scopes.iter().rev() { + if scope.kind == ScopeKind::Function { + return false; + } + if scope.kind == ScopeKind::Gate { + return false; + } + } + true + } + + /// Get the input symbols in the program. + pub(crate) fn get_input(&self) -> Option> { + let io_input = self.get_io_input(); + if io_input.is_empty() { + None + } else { + Some(io_input) + } + } + + /// Get the output symbols in the program. + /// Output symbols are either inferred or explicitly declared. + /// If there are no explicitly declared output symbols, then the inferred + /// output symbols are returned. + pub(crate) fn get_output(&self) -> Option> { + let io_ouput = self.get_io_output(); + if io_ouput.is_some() { + io_ouput + } else { + self.get_inferred_output() + } + } + + /// Get all symbols in the global scope that are inferred output symbols. + /// Any global symbol that is not a built-in symbol and has a type that is + /// inferred to be an output type is considered an inferred output symbol. + fn get_inferred_output(&self) -> Option> { + let mut symbols = vec![]; + self.scopes + .iter() + .filter(|scope| scope.kind == ScopeKind::Global) + .for_each(|scope| { + for symbol in scope + .get_ordered_symbols() + .into_iter() + .filter(|symbol| !BUILTIN_SYMBOLS.contains(&symbol.name.as_str())) + .filter(|symbol| symbol.io_kind == IOKind::Default) + { + if is_inferred_output_type(&symbol.ty) { + symbols.push(symbol); + } + } + }); + if symbols.is_empty() { + None + } else { + Some(symbols) + } + } + + /// Get all symbols in the global scope that are output symbols. + fn get_io_output(&self) -> Option> { + let mut symbols = vec![]; + for scope in self + .scopes + .iter() + .filter(|scope| scope.kind == ScopeKind::Global) + { + for symbol in scope.get_ordered_symbols() { + if symbol.io_kind == IOKind::Output { + symbols.push(symbol); + } + } + } + if symbols.is_empty() { + None + } else { + Some(symbols) + } + } + + /// Get all symbols in the global scope that are input symbols. + fn get_io_input(&self) -> Vec { + let mut symbols = vec![]; + for scope in self + .scopes + .iter() + .filter(|scope| scope.kind == ScopeKind::Global) + { + for symbol in scope.get_ordered_symbols() { + if symbol.io_kind == IOKind::Input { + symbols.push(symbol); + } + } + } + symbols + } +} + +fn is_inferred_output_type(ty: &Type) -> bool { + matches!( + ty, + Type::Bit(_) + | Type::Int(_, _) + | Type::UInt(_, _) + | Type::Float(_, _) + | Type::Angle(_, _) + | Type::Complex(_, _) + | Type::Bool(_) + | Type::BitArray(_, _) + | Type::IntArray(_) + | Type::UIntArray(_) + | Type::FloatArray(_) + | Type::AngleArray(_) + | Type::ComplexArray(_) + | Type::BoolArray(_) + | Type::Range + | Type::Set + ) +} diff --git a/compiler/qsc_qasm3/src/tests.rs b/compiler/qsc_qasm3/src/tests.rs new file mode 100644 index 0000000000..8fe61be022 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests.rs @@ -0,0 +1,311 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + parse::{QasmParseResult, QasmSource}, + qasm_to_program, CompilerConfig, OutputSemantics, ProgramType, QasmCompileUnit, QubitSemantics, +}; +use miette::Report; +use qsc::{ + ast::{mut_visit::MutVisitor, Package}, + target::Profile, + PackageStore, SourceMap, Span, +}; + +use std::{path::Path, sync::Arc}; + +use crate::{ + io::{InMemorySourceResolver, SourceResolver}, + parse::{parse_source, QasmSourceTrait}, +}; + +pub(crate) mod assignment; +pub(crate) mod declaration; +pub(crate) mod expression; +pub(crate) mod output; +pub(crate) mod sample_circuits; +pub(crate) mod scopes; +pub(crate) mod statement; + +pub(crate) fn fail_on_compilation_errors(unit: &QasmCompileUnit) { + if unit.has_errors() { + print_compilation_errors(unit); + panic!("Errors found in compilation"); + } +} + +pub(crate) fn print_compilation_errors(unit: &QasmCompileUnit) { + if unit.has_errors() { + for e in unit.errors() { + println!("{:?}", miette::Report::new(e.clone())); + } + } +} + +pub(crate) fn gen_qsharp(package: &qsc::ast::Package) -> String { + qsc::codegen::qsharp::write_package_string(package) +} + +/// Generates QIR from an AST package. +/// This function is used for testing purposes only. +/// The interactive environment uses a different mechanism to generate QIR. +/// As we need an entry expression to generate QIR in those cases. +/// +/// This function assumes that the AST package was designed as an entry point. +pub(crate) fn generate_qir_from_ast( + ast_package: Package, + source_map: SourceMap, + profile: Profile, +) -> Result> { + let mut store = PackageStore::new(qsc::compile::core()); + let mut dependencies = Vec::new(); + let capabilities = profile.into(); + dependencies.push((store.insert(qsc::compile::std(&store, capabilities)), None)); + + qsc::codegen::qir::get_qir_from_ast( + &mut store, + &dependencies, + ast_package, + source_map, + capabilities, + ) +} + +fn compile_qasm_to_qir(source: &str, profile: Profile) -> Result> { + let res = parse(source)?; + assert!(!res.has_errors()); + + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let package = unit.package.expect("no package found"); + let qir = generate_qir_from_ast(package, unit.source_map, profile).map_err(|errors| { + errors + .iter() + .map(|e| Report::new(e.clone())) + .collect::>() + })?; + Ok(qir) +} + +pub(crate) fn gen_qsharp_stmt(stmt: &qsc::ast::Stmt) -> String { + qsc::codegen::qsharp::write_stmt_string(stmt) +} + +pub(crate) fn compare_compilation_to_qsharp(unit: &QasmCompileUnit, expected: &str) { + let package = unit.package.as_ref().expect("package must exist"); + let despanned_ast = AstDespanner.despan(package); + let qsharp = gen_qsharp(&despanned_ast); + difference::assert_diff!(&qsharp, expected, "\n", 0); +} + +pub(crate) fn parse(source: S) -> miette::Result> +where + S: AsRef, +{ + let resolver = InMemorySourceResolver::from_iter([("test".into(), source.as_ref().into())]); + let res = parse_source(source, "test", &resolver).map_err(|e| vec![e])?; + if res.source.has_errors() { + let errors = res + .errors() + .into_iter() + .map(|e| miette::Report::new(e.clone())) + .collect(); + return Err(errors); + } + Ok(res) +} + +pub(crate) fn parse_all

( + path: P, + sources: impl IntoIterator, Arc)>, +) -> miette::Result> +where + P: AsRef, +{ + let resolver = InMemorySourceResolver::from_iter(sources); + let source = resolver.resolve(path.as_ref()).map_err(|e| vec![e])?.1; + let res = parse_source(source, path, &resolver).map_err(|e| vec![e])?; + if res.source.has_errors() { + let errors = res + .errors() + .into_iter() + .map(|e| miette::Report::new(e.clone())) + .collect(); + Err(errors) + } else { + Ok(res) + } +} + +pub fn qasm_to_program_fragments(source: QasmSource, source_map: SourceMap) -> QasmCompileUnit { + qasm_to_program( + source, + source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + program_ty: ProgramType::Fragments, + output_semantics: OutputSemantics::OpenQasm, + }, + ) +} + +pub fn compile_qasm_to_qsharp_file(source: &str) -> miette::Result> { + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + if unit.has_errors() { + let errors = unit.errors.into_iter().map(Report::new).collect(); + return Err(errors); + } + let Some(package) = unit.package else { + panic!("Expected package, got None"); + }; + let qsharp = gen_qsharp(&package); + Ok(qsharp) +} + +pub fn compile_qasm_to_qsharp_operation(source: &str) -> miette::Result> { + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::Operation("Test".to_string()), + }, + ); + if unit.has_errors() { + let errors = unit.errors.into_iter().map(Report::new).collect(); + return Err(errors); + } + let Some(package) = unit.package else { + panic!("Expected package, got None"); + }; + let qsharp = gen_qsharp(&package); + Ok(qsharp) +} + +pub fn compile_qasm_to_qsharp(source: &str) -> miette::Result> { + compile_qasm_to_qsharp_with_semantics(source, QubitSemantics::Qiskit) +} + +pub fn compile_qasm_to_qsharp_with_semantics( + source: &str, + qubit_semantics: QubitSemantics, +) -> miette::Result> { + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::Fragments, + }, + ); + qsharp_from_qasm_compilation(unit) +} + +pub fn qsharp_from_qasm_compilation(unit: QasmCompileUnit) -> miette::Result> { + if unit.has_errors() { + let errors = unit.errors.into_iter().map(Report::new).collect(); + return Err(errors); + } + let Some(package) = unit.package else { + panic!("Expected package, got None"); + }; + let qsharp = gen_qsharp(&package); + Ok(qsharp) +} + +pub fn compile_qasm_stmt_to_qsharp(source: &str) -> miette::Result> { + compile_qasm_stmt_to_qsharp_with_semantics(source, QubitSemantics::Qiskit) +} + +pub fn compile_qasm_stmt_to_qsharp_with_semantics( + source: &str, + qubit_semantics: QubitSemantics, +) -> miette::Result> { + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::Fragments, + }, + ); + if unit.has_errors() { + let errors = unit.errors.into_iter().map(Report::new).collect(); + return Err(errors); + } + let Some(package) = unit.package else { + panic!("Expected package, got None"); + }; + let qsharp = get_first_statement_as_qsharp(&package); + Ok(qsharp) +} + +fn get_first_statement_as_qsharp(package: &qsc::ast::Package) -> String { + let qsharp = match package.nodes.first() { + Some(i) => match i { + qsc::ast::TopLevelNode::Namespace(_) => panic!("Expected Stmt, got Namespace"), + qsc::ast::TopLevelNode::Stmt(stmt) => gen_qsharp_stmt(stmt.as_ref()), + }, + None => panic!("Expected Stmt, got None"), + }; + qsharp +} + +pub struct AstDespanner; +impl AstDespanner { + pub fn despan(&mut self, package: &qsc::ast::Package) -> qsc::ast::Package { + let mut p = package.clone(); + self.visit_package(&mut p); + p + } +} + +impl MutVisitor for AstDespanner { + fn visit_span(&mut self, span: &mut Span) { + span.hi = 0; + span.lo = 0; + } +} + +struct HirDespanner; +impl HirDespanner { + fn despan(&mut self, package: &qsc::hir::Package) -> qsc::hir::Package { + let mut p = package.clone(); + qsc::hir::mut_visit::MutVisitor::visit_package(self, &mut p); + p + } +} + +impl qsc::hir::mut_visit::MutVisitor for HirDespanner { + fn visit_span(&mut self, span: &mut Span) { + span.hi = 0; + span.lo = 0; + } +} diff --git a/compiler/qsc_qasm3/src/tests/assignment.rs b/compiler/qsc_qasm3/src/tests/assignment.rs new file mode 100644 index 0000000000..23e5e21041 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/assignment.rs @@ -0,0 +1,77 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod alias; +mod slices; + +use crate::tests::{fail_on_compilation_errors, parse, qasm_to_program_fragments}; +use miette::Report; + +#[test] +#[ignore = "unimplemented"] +fn classical() -> miette::Result<(), Vec> { + let source = " + bit[2] a; + creg b[2]; + qubit[3] q; + int[10] x = 12; + a[0] = b[1]; + x += int[10](a[1]); + measure q[1] -> a[0]; + a = measure q[1:2]; + measure q[0]; + b = a == 0; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +#[ignore = "unimplemented"] +fn quantum() -> miette::Result<(), Vec> { + let source = " + bit[2] a; + creg b[2]; + qubit[3] q; + int[10] x = 12; + a[0] = b[1]; + x += int[10](a[1]); + measure q[1] -> a[0]; + a = measure q[1:2]; + measure q[0]; + b = a == 0; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +#[ignore = "qasm3 parser does not support old-style decls yet"] +fn classical_old_style_decls() -> miette::Result<(), Vec> { + let source = " + bit[2] a; + creg b[2]; + qubit[3] q; + int[10] x = 12; + a[0] = b[1]; + x += int[10](a[1]); + measure q[1] -> a[0]; + a = measure q[1:2]; + measure q[0]; + b = a == 0; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/assignment/alias.rs b/compiler/qsc_qasm3/src/tests/assignment/alias.rs new file mode 100644 index 0000000000..830646fc2b --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/assignment/alias.rs @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::tests::{fail_on_compilation_errors, parse, qasm_to_program_fragments}; +use miette::Report; + +#[test] +#[ignore = "unimplemented"] +fn classical() -> miette::Result<(), Vec> { + let source = " + bit[2] a; + bit[2] b; + let c = a[{0,1}] ++ b[1:2]; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +#[ignore = "unimplemented"] +fn quantum() -> miette::Result<(), Vec> { + let source = " + qubit[5] q1; + qubit[7] q2; + let q = q1 ++ q2; + let c = a[{0,1}] ++ b[1:2]; + let qq = q1[{1,3,4}]; + let qqq = qq ++ q2[1:2:6]; + let d = c; + let e = d[1]; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +#[ignore = "qasm3 parser does not support old-style decls yet"] +fn classical_old_style_decls() -> miette::Result<(), Vec> { + let source = " + creg a[2]; + creg b[2]; + let c = a[{0,1}] ++ b[1:2]; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +#[ignore = "qasm3 parser does not support old-style decls yet"] +fn quantum_old_style_decls() -> miette::Result<(), Vec> { + let source = " + qreg q1[5]; + qreg q2[7]; + let q = q1 ++ q2; + let c = a[{0,1}] ++ b[1:2]; + let qq = q1[{1,3,4}]; + let qqq = qq ++ q2[1:2:6]; + let d = c; + let e = d[1]; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/assignment/slices.rs b/compiler/qsc_qasm3/src/tests/assignment/slices.rs new file mode 100644 index 0000000000..fc36ab244f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/assignment/slices.rs @@ -0,0 +1,2 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. diff --git a/compiler/qsc_qasm3/src/tests/declaration.rs b/compiler/qsc_qasm3/src/tests/declaration.rs new file mode 100644 index 0000000000..3590540731 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration.rs @@ -0,0 +1,115 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +mod array; +mod bit; +mod bool; +mod complex; +mod float; +mod gate; +mod integer; +mod io; +mod qubit; +mod unsigned_integer; + +use crate::{ + tests::{fail_on_compilation_errors, parse, qasm_to_program_fragments}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; + +use miette::Report; + +#[test] +#[ignore = "oq3 parser bug, can't read float with leading dot"] +fn classical() -> miette::Result<(), Vec> { + let source = r#" + int[10] a; + int[10] b; + uint[32] c = 0xFa_1F; + uint[32] d = 0XFa_1F; + uint[16] e = 0o12_34; + uint[16] f = 0b1001_1001; + uint[16] g = 0B1001_1001; + uint h; + qubit[6] q1; + qubit q2; + bit[4] b1 = "0100"; + bit[8] b2 = "1001_0100"; + bit b3 = "1"; + bool i = true; + bool j = false; + const float[64] k = 5.5e3; + const float[64] l = 5; + float[32] m = .1e+3; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +fn duration_literal() -> miette::Result<(), Vec> { + let source = " + duration dur0; + duration dur1 = 1000dt; + duration dur2 = 10 ms; + duration dur3 = 8 us; + duration dur4 = 1s; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = crate::qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::Fragments, + }, + ); + println!("{:?}", unit.errors); + assert!(unit.errors.len() == 5); + for error in &unit.errors { + assert!( + error + .to_string() + .contains("Duration type values are not supported.") + || error + .to_string() + .contains("Timing literal expressions are not supported.") + ); + } + + Ok(()) +} + +#[test] +fn stretch() { + let source = " + stretch s; + "; + + let res = parse(source).expect("should parse"); + assert!(!res.has_errors()); + let unit = crate::compile::qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::Fragments, + }, + ); + assert!(unit.has_errors()); + println!("{:?}", unit.errors); + assert!(unit.errors.len() == 1); + assert!(unit.errors[0] + .to_string() + .contains("Stretch type values are not supported."),); +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/array.rs b/compiler/qsc_qasm3/src/tests/declaration/array.rs new file mode 100644 index 0000000000..a6e2a874e5 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/array.rs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod bit; +mod qubit; + +use crate::tests::{fail_on_compilation_errors, parse, qasm_to_program_fragments}; +use miette::Report; + +#[test] +#[ignore = "unimplemented"] +fn arrays() -> miette::Result<(), Vec> { + let source = " + array[uint[16], 1] x; + array[int[8], 4] x; + array[float[64], 4, 2] x; + array[angle[32], 4, 3, 2] x; + array[bit[8], 2] x; + array[bit[16], 2, 2] x; + array[complex[float[32]], 4] x; + array[bool, 3] x; + array[int[8], 4] x = {1, 2, 3, 4}; + array[int[8], 4] x = y; + array[int[8], 2] x = {y, y+y}; + array[uint[32], 2, 2] x = {{3, 4}, {2-3, 5*y}}; + array[uint[32], 2, 2] x = {z, {2-3, 5*y}}; + array[uint[32], 2, 2] x = {2*z, {1, 2}}; + array[uint[32], 2, 2] x = y; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/array/bit.rs b/compiler/qsc_qasm3/src/tests/declaration/array/bit.rs new file mode 100644 index 0000000000..28689b99a7 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/array/bit.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::tests::compile_qasm_stmt_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn bitarray_with_bitstring() -> miette::Result<(), Vec> { + let source = r#" + bit[4] b = "0100"; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = [Zero, One, Zero, Zero]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bitarray_with_formatted_bitstring() -> miette::Result<(), Vec> { + let source = r#" + bit[8] b = "1001_0100"; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = [One, Zero, Zero, One, Zero, One, Zero, Zero]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bitarray_with_no_initializer() -> miette::Result<(), Vec> { + let source = r#" + bit[8] b; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bitarray_with_int_initializer() -> miette::Result<(), Vec> { + let source = r#" + bit[3] b = 7; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = [One, One, One]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/array/qubit.rs b/compiler/qsc_qasm3/src/tests/declaration/array/qubit.rs new file mode 100644 index 0000000000..9853f5e3bc --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/array/qubit.rs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + tests::{compile_qasm_stmt_to_qsharp, compile_qasm_stmt_to_qsharp_with_semantics}, + QubitSemantics, +}; +use expect_test::expect; +use miette::Report; + +#[test] +fn qubit_array_decl() -> miette::Result<(), Vec> { + let source = " + qubit[5] my_qubit; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_qubit = QIR.Runtime.AllocateQubitArray(5); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn qubit_array_decl_with_qsharp_semantics() -> miette::Result<(), Vec> { + let source = " + qubit[5] my_qubits; + "; + + let qsharp = compile_qasm_stmt_to_qsharp_with_semantics(source, QubitSemantics::QSharp)?; + expect![ + r#" + use my_qubits = Qubit[5]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/bit.rs b/compiler/qsc_qasm3/src/tests/declaration/bit.rs new file mode 100644 index 0000000000..538b730367 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/bit.rs @@ -0,0 +1,88 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn bit_with_no_initializer() -> miette::Result<(), Vec> { + let source = r#" + bit b; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = Zero; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_with_initializer_lit_one() -> miette::Result<(), Vec> { + let source = r#" + bit b = 1; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = One; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_with_initializer_lit_zero() -> miette::Result<(), Vec> { + let source = r#" + bit b = 0; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable b = Zero; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_bit_with_initializer_lit_one() -> miette::Result<(), Vec> { + let source = r#" + const bit b = 1; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let b = One; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_bit_with_initializer_lit_zero() -> miette::Result<(), Vec> { + let source = r#" + const bit b = 0; + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let b = Zero; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/bool.rs b/compiler/qsc_qasm3/src/tests/declaration/bool.rs new file mode 100644 index 0000000000..161ebfea5b --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/bool.rs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; + +use expect_test::expect; +use miette::Report; + +#[test] +fn bool_default_decl() -> miette::Result<(), Vec> { + let source = " + bool x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = false; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_bool_default_decl() -> miette::Result<(), Vec> { + let source = " + const bool x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = false; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_true_decl() -> miette::Result<(), Vec> { + let source = " + bool x = true; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = true; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_bool_true_decl() -> miette::Result<(), Vec> { + let source = " + const bool x = true; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = true; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_false_decl() -> miette::Result<(), Vec> { + let source = " + bool x = false; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = false; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_bool_false_decl() -> miette::Result<(), Vec> { + let source = " + const bool x = false; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = false; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/complex.rs b/compiler/qsc_qasm3/src/tests/declaration/complex.rs new file mode 100644 index 0000000000..76b6db69ce --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/complex.rs @@ -0,0 +1,374 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_stmt_to_qsharp, gen_qsharp, qasm_to_program_fragments}; +use crate::{tests::fail_on_compilation_errors, tests::parse}; + +use expect_test::expect; +use miette::Report; + +#[test] +// TODO: break this into multiple tests for binops +fn complex() -> miette::Result<(), Vec> { + let source = " + complex[float] a; + complex[float] b = 4 - 5.5im; + complex[float[64]] c = a + 3 im; + complex[float[32]] d = a * b; + complex[float] e = 1im; + complex[float] f = 1 im; + complex g = c * d; + complex h = g / 2.2im; + complex z; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + let Some(package) = &unit.package else { + panic!("no package found"); + }; + let qsharp = gen_qsharp(package); + println!("{qsharp}"); + Ok(()) +} + +#[test] +fn implicit_bitness_default_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(0., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_default_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn explicit_bitness_default_decl() -> miette::Result<(), Vec> { + let source = " + complex[float[42]] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(0., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_default_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float[42]] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_double_img_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1.01im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 1.01); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_img_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 1.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_double_img_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float[42]] x = 1.01im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 1.01); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_int_img_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float[42]] x = 1im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(0., 1.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_double_img_only_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1.01im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(0., 1.01); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_img_only_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(0., 1.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +// This test is based on spec decls which show this exact case +#[test] +fn implicit_bitness_int_img_only_tab_between_suffix_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1 im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(0., 1.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_double_real_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1.01; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(1.01, 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_real_only_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.Complex(1., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_double_real_only_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1.01; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(1.01, 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_real_only_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.Complex(1., 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_simple_double_pos_im_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1.1 + 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.PlusC(Microsoft.Quantum.Math.Complex(1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_simple_double_pos_im_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1.1 + 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.PlusC(Microsoft.Quantum.Math.Complex(1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_simple_double_neg_im_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = 1.1 - 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.MinusC(Microsoft.Quantum.Math.Complex(1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_simple_double_neg_im_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = 1.1 - 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.MinusC(Microsoft.Quantum.Math.Complex(1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_simple_double_neg_real_decl() -> miette::Result<(), Vec> { + let source = " + complex[float] x = -1.1 + 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.PlusC(Microsoft.Quantum.Math.Complex(-1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_simple_double_neg_real_decl() -> miette::Result<(), Vec> { + let source = " + const complex[float] x = -1.1 + 2.2im; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = Microsoft.Quantum.Math.PlusC(Microsoft.Quantum.Math.Complex(-1.1, 0.), Microsoft.Quantum.Math.Complex(0., 2.2)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/float.rs b/compiler/qsc_qasm3/src/tests/declaration/float.rs new file mode 100644 index 0000000000..87396a696f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/float.rs @@ -0,0 +1,348 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; + +use expect_test::expect; +use miette::Report; + +#[test] +fn implicit_bitness_default_decl() -> miette::Result<(), Vec> { + let source = " + float x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_default_decl() -> miette::Result<(), Vec> { + let source = " + const float x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl() -> miette::Result<(), Vec> { + let source = " + float x = 42.1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl() -> miette::Result<(), Vec> { + let source = " + const float x = 42.1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_explicit_width_decl() -> miette::Result<(), Vec> { + let source = " + float[64] x = 42.1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_width_lit_decl() -> miette::Result<(), Vec> { + let source = " + const float[64] x = 42.1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, can't read float with leading dot"] +fn lit_decl_leading_dot() -> miette::Result<(), Vec> { + let source = " + float x = .421; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0.421; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, can't read float with leading dot"] +fn const_lit_decl_leading_dot() -> miette::Result<(), Vec> { + let source = " + const float x = .421; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0.421; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, can't read float with leading dot"] +fn const_lit_decl_leading_dot_scientific() -> miette::Result<(), Vec> { + let source = " + const float x = .421e2; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl_trailing_dot() -> miette::Result<(), Vec> { + let source = " + float x = 421.; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 421.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_trailing_dot() -> miette::Result<(), Vec> { + let source = " + const float x = 421.; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 421.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl_scientific() -> miette::Result<(), Vec> { + let source = " + float x = 4.21e1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_scientific() -> miette::Result<(), Vec> { + let source = " + const float x = 4.21e1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl_scientific_signed_pos() -> miette::Result<(), Vec> { + let source = " + float x = 4.21e+1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_scientific_signed_pos() -> miette::Result<(), Vec> { + let source = " + const float x = 4.21e+1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl_scientific_cap_e() -> miette::Result<(), Vec> { + let source = " + float x = 4.21E1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_scientific_cap_e() -> miette::Result<(), Vec> { + let source = " + const float x = 4.21E1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lit_decl_scientific_signed_neg() -> miette::Result<(), Vec> { + let source = " + float x = 421.0e-1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_scientific_signed_neg() -> miette::Result<(), Vec> { + let source = " + const float x = 421.0e-1; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42.1; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_signed_float_lit_cast_neg() -> miette::Result<(), Vec> { + let source = " + const float x = -7.; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = -7.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_lit_decl_signed_int_lit_cast_neg() -> miette::Result<(), Vec> { + let source = " + const float x = -7; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = -7.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/gate.rs b/compiler/qsc_qasm3/src/tests/declaration/gate.rs new file mode 100644 index 0000000000..e23bfd676b --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/gate.rs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn single_qubit() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + gate my_h q { + h q; + } + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_h : (Qubit) => Unit = (q) => { + H(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn two_qubits() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + gate my_h q, q2 { + h q2; + h q; + } + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_h : (Qubit, Qubit) => Unit = (q, q2) => { + H(q2); + H(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn single_angle_single_qubit() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + gate my_h(θ) q { + rx(θ) q; + } + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_h : (Double, Qubit) => Unit = (θ, q) => { + Rx(θ, q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn two_angles_two_qubits() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + gate my_h(θ, φ) q, q2 { + rx(θ) q2; + ry(φ) q; + } + "#; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_h : (Double, Double, Qubit, Qubit) => Unit = (θ, φ, q, q2) => { + Rx(θ, q2); + Ry(φ, q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/integer.rs b/compiler/qsc_qasm3/src/tests/declaration/integer.rs new file mode 100644 index 0000000000..61dddcd4c5 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/integer.rs @@ -0,0 +1,393 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; + +use expect_test::expect; +use miette::Report; + +#[test] +fn implicit_bitness_int_negative_decl() -> miette::Result<(), Vec> { + let source = " + int x = -42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = -42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_const_negative_decl() -> miette::Result<(), Vec> { + let source = " + const int x = -42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = -42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + int x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + const int x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_lit_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital X is not recognized as hex"] +fn implicit_bitness_int_hex_cap_decl() -> miette::Result<(), Vec> { + let source = " + int x = 0XFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_hex_low_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 0xFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital X is not recognized as hex"] +fn const_implicit_bitness_int_hex_cap_decl() -> miette::Result<(), Vec> { + let source = " + const int y = 0XFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_octal_decl() -> miette::Result<(), Vec> { + let source = " + int x = 0o42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 34; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_octal_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 0o42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 34; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_binary_low_decl() -> miette::Result<(), Vec> { + let source = " + int x = 0b1001_1001; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 153; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital B is not recognized as binary"] +fn implicit_bitness_int_binary_cap_decl() -> miette::Result<(), Vec> { + let source = " + int x = 0B1010; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 10; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_binary_low_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 0b1001_1001; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 153; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital B is not recognized as binary"] +fn const_implicit_bitness_int_binary_cap_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 0B1010; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 10; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_formatted_decl() -> miette::Result<(), Vec> { + let source = " + int x = 2_0_00; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 2000; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_formatted_decl() -> miette::Result<(), Vec> { + let source = " + const int x = 2_0_00; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 2000; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn explicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + int[10] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + const int[10] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn explicit_bitness_int_decl() -> miette::Result<(), Vec> { + let source = " + int[10] x = 42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_int_decl() -> miette::Result<(), Vec> { + let source = " + const int[10] x = 42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn assigning_uint_to_negative_lit_results_in_semantic_error() { + let source = " + const uint[10] x = -42; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected error"); + }; + expect![ + r#"Cannot assign a value of Negative Int type to a classical variable of UInt(Some(10), True) type."# + ] + .assert_eq(&errors[0].to_string()); +} + +#[test] +fn implicit_bitness_uint_const_negative_decl_raises_semantic_error() { + let source = " + const uint x = -42; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected error"); + }; + expect![ + r#"Cannot assign a value of Negative Int type to a classical variable of UInt(None, True) type."# + ] + .assert_eq(&errors[0].to_string()); +} + +#[test] +fn explicit_bitness_uint_const_negative_decl_raises_semantic_error() { + let source = " + const uint[32] x = -42; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected error"); + }; + expect![ + r#"Cannot assign a value of Negative Int type to a classical variable of UInt(Some(32), True) type."# + ] + .assert_eq(&errors[0].to_string()); +} + +#[test] +fn implicit_bitness_int_negative_float_decl_causes_semantic_error() { + let source = " + int x = -42.; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected error"); + }; + expect![ + r#"Cannot assign a value of Float(None, True) type to a classical variable of Int(None, False) type."# + ] + .assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/io.rs b/compiler/qsc_qasm3/src/tests/declaration/io.rs new file mode 100644 index 0000000000..662d2ff2cb --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/io.rs @@ -0,0 +1,6 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod explicit_input; +mod explicit_output; +mod implicit_output; diff --git a/compiler/qsc_qasm3/src/tests/declaration/io/explicit_input.rs b/compiler/qsc_qasm3/src/tests/declaration/io/explicit_input.rs new file mode 100644 index 0000000000..37ee9ecb41 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/io/explicit_input.rs @@ -0,0 +1,225 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp_operation; +use expect_test::expect; +use miette::Report; + +#[test] +fn bit_array_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +OPENQASM 3.0; +input bit[2] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(c : Result[]) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +OPENQASM 3.0; +input bit c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(c : Result) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input bool c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(c : Bool) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn complex_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input complex[float] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(c : Microsoft.Quantum.Math.Complex) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_implicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input float f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(f : Double) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_explicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input float[60] f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(f : Double) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_implicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input int i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(i : Int) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_explicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input int[60] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(i : Int) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_implicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input uint i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(i : Int) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_explicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input uint[60] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(i : Int) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bigint_explicit_bitness_is_lifted() -> miette::Result<(), Vec> { + let source = r#" +input int[65] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(i : BigInt) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn lifting_qubit_raises_parse_error() { + let source = r#" +input qubit q; +"#; + + let Err(error) = compile_qasm_to_qsharp_operation(source) else { + panic!("Expected error") + }; + + assert!(error[0] + .to_string() + .contains("QASM3 Parse Error: Quantum type found in input/output declaration.")); +} + +#[test] +fn order_is_preserved_with_multiple_inputs() -> miette::Result<(), Vec> { + let source = r#" +input int[65] bi; +input int[6] i; +input uint[60] ui; +input uint u; +input float f; +input bool b; +input bit c; +input complex[float] cf; +input bit[2] b2; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +operation Test(bi : BigInt, i : Int, ui : Int, u : Int, f : Double, b : Bool, c : Result, cf : Microsoft.Quantum.Math.Complex, b2 : Result[]) : Unit {} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/io/explicit_output.rs b/compiler/qsc_qasm3/src/tests/declaration/io/explicit_output.rs new file mode 100644 index 0000000000..f3bd0f3bad --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/io/explicit_output.rs @@ -0,0 +1,279 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp_operation; +use expect_test::expect; +use miette::Report; + +#[test] +fn bit_array_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output bit[2] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Result[] { + mutable c = [Zero, Zero]; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output bit c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Result { + mutable c = Zero; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output bool c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Bool { + mutable c = false; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn complex_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output complex[float] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Microsoft.Quantum.Math.Complex { + mutable c = Microsoft.Quantum.Math.Complex(0., 0.); + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_implicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output float f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Double { + mutable f = 0.; + f +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_explicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output float[42] f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Double { + mutable f = 0.; + f +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_explicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output int[42] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_implicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output int i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_implicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output uint i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_explicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output uint[42] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bigint_explicit_width_is_returned() -> miette::Result<(), Vec> { + let source = r#" +output int[65] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : BigInt { + mutable i = 0L; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn qubit_explicit_output_raises_parse_error() { + let source = r#" +output qubit q; +"#; + + let Err(error) = compile_qasm_to_qsharp_operation(source) else { + panic!("Expected error") + }; + + assert!(error[0] + .to_string() + .contains("QASM3 Parse Error: Quantum type found in input/output declaration.")); +} + +#[test] +fn order_is_preserved_with_multiple_inputs() -> miette::Result<(), Vec> { + let source = r#" +output int[65] bi; +output int[6] i; +output uint[60] ui; +output uint u; +output float f; +output bool b; +output bit c; +output complex[float] cf; +output bit[2] b2; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : (BigInt, Int, Int, Int, Double, Bool, Result, Microsoft.Quantum.Math.Complex, Result[]) { + mutable bi = 0L; + mutable i = 0; + mutable ui = 0; + mutable u = 0; + mutable f = 0.; + mutable b = false; + mutable c = Zero; + mutable cf = Microsoft.Quantum.Math.Complex(0., 0.); + mutable b2 = [Zero, Zero]; + (bi, i, ui, u, f, b, c, cf, b2) +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/io/implicit_output.rs b/compiler/qsc_qasm3/src/tests/declaration/io/implicit_output.rs new file mode 100644 index 0000000000..8d312c0e38 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/io/implicit_output.rs @@ -0,0 +1,264 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp_operation; +use expect_test::expect; +use miette::Report; + +#[test] +fn bit_array_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +bit[2] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Result[] { + mutable c = [Zero, Zero]; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +bit c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Result { + mutable c = Zero; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +bool c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Bool { + mutable c = false; + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn complex_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +complex[float] c; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Microsoft.Quantum.Math.Complex { + mutable c = Microsoft.Quantum.Math.Complex(0., 0.); + c +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_implicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +float f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Double { + mutable f = 0.; + f +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_explicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +float[42] f; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Double { + mutable f = 0.; + f +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_explicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +int[42] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_implicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +int i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_implicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +uint i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_explicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +uint[42] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : Int { + mutable i = 0; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bigint_explicit_width_is_inferred_and_returned() -> miette::Result<(), Vec> { + let source = r#" +int[65] i; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : BigInt { + mutable i = 0L; + i +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn order_is_preserved_with_multiple_inputs() -> miette::Result<(), Vec> { + let source = r#" +int[65] bi; +int[6] i; +uint[60] ui; +uint u; +float f; +bool b; +bit c; +complex[float] cf; +bit[2] b2; +"#; + + let qsharp = compile_qasm_to_qsharp_operation(source)?; + expect![ + r#" +@EntryPoint() +operation Test() : (BigInt, Int, Int, Int, Double, Bool, Result, Microsoft.Quantum.Math.Complex, Result[]) { + mutable bi = 0L; + mutable i = 0; + mutable ui = 0; + mutable u = 0; + mutable f = 0.; + mutable b = false; + mutable c = Zero; + mutable cf = Microsoft.Quantum.Math.Complex(0., 0.); + mutable b2 = [Zero, Zero]; + (bi, i, ui, u, f, b, c, cf, b2) +} +"# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/qubit.rs b/compiler/qsc_qasm3/src/tests/declaration/qubit.rs new file mode 100644 index 0000000000..a39c90a0a4 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/qubit.rs @@ -0,0 +1,59 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::{fail_on_compilation_errors, parse, qasm_to_program_fragments}; +use crate::{ + tests::{compile_qasm_stmt_to_qsharp, compile_qasm_stmt_to_qsharp_with_semantics}, + QubitSemantics, +}; + +#[test] +fn quantum() -> miette::Result<(), Vec> { + let source = " + qubit[6] q1; + qubit q2; + "; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program_fragments(res.source, res.source_map); + fail_on_compilation_errors(&unit); + Ok(()) +} + +#[test] +fn single_qubit_decl() -> miette::Result<(), Vec> { + let source = " + qubit my_qubit; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let my_qubit = QIR.Runtime.__quantum__rt__qubit_allocate(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn single_qubit_decl_with_qsharp_semantics() -> miette::Result<(), Vec> { + let source = " + qubit my_qubit; + "; + + let qsharp = compile_qasm_stmt_to_qsharp_with_semantics(source, QubitSemantics::QSharp)?; + expect![ + " + use my_qubit = Qubit(); + " + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/declaration/unsigned_integer.rs b/compiler/qsc_qasm3/src/tests/declaration/unsigned_integer.rs new file mode 100644 index 0000000000..11261c5cfd --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/declaration/unsigned_integer.rs @@ -0,0 +1,269 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_stmt_to_qsharp; + +use expect_test::expect; +use miette::Report; + +#[test] +fn implicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + uint x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_default_decl() -> miette::Result<(), Vec> { + let source = " + const uint x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_lit_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 42; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital X is not recognized as hex"] +fn implicit_bitness_int_hex_cap_decl() -> miette::Result<(), Vec> { + let source = " + uint x = 0XFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_hex_low_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 0xFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital X is not recognized as hex"] +fn const_implicit_bitness_int_hex_cap_decl() -> miette::Result<(), Vec> { + let source = " + const uint y = 0XFa_1F; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 64031; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_octal_decl() -> miette::Result<(), Vec> { + let source = " + uint x = 0o42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 34; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_octal_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 0o42; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 34; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_binary_low_decl() -> miette::Result<(), Vec> { + let source = " + uint x = 0b1001_1001; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 153; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital B is not recognized as binary"] +fn implicit_bitness_int_binary_cap_decl() -> miette::Result<(), Vec> { + let source = " + uint x = 0B1010; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 10; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_binary_low_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 0b1001_1001; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 153; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "oq3 parser bug, capital B is not recognized as binary"] +fn const_implicit_bitness_int_binary_cap_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 0B1010; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 10; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn implicit_bitness_int_formatted_decl() -> miette::Result<(), Vec> { + let source = " + uint x = 2_0_00; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 2000; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_implicit_bitness_int_formatted_decl() -> miette::Result<(), Vec> { + let source = " + const uint x = 2_0_00; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 2000; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn explicit_bitness_int_decl() -> miette::Result<(), Vec> { + let source = " + uint[10] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_explicit_bitness_int_decl() -> miette::Result<(), Vec> { + let source = " + const uint[10] x; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + let x = 0; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression.rs b/compiler/qsc_qasm3/src/tests/expression.rs new file mode 100644 index 0000000000..8ee361df0c --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression.rs @@ -0,0 +1,14 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod binary; +mod bits; +mod ident; +mod implicit_cast_from_bit; +mod implicit_cast_from_bitarray; +mod implicit_cast_from_bool; +mod implicit_cast_from_complex; +mod implicit_cast_from_float; +mod implicit_cast_from_int; +mod indexed; +mod unary; diff --git a/compiler/qsc_qasm3/src/tests/expression/binary.rs b/compiler/qsc_qasm3/src/tests/expression/binary.rs new file mode 100644 index 0000000000..b050ea7b7f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary.rs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; + +use crate::tests::compile_qasm_to_qsharp; + +mod arithmetic_conversions; +mod comparison; +mod ident; +mod literal; + +#[test] +fn binary_expr_fail_parse_missing_op() { + let source = r#" + input int a; + input int b; + a b; + "#; + + assert!(compile_qasm_to_qsharp(source).is_err()); +} + +#[test] +fn binary_expr_fail_parse_missing_lhs() { + let source = r#" + input int b; + < b; + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + + expect![r#"QASM3 Parse Error: atom_expr: expected expression"#] + .assert_eq(&errors[0].to_string()); +} + +#[test] +fn binary_expr_fail_parse_missing_rhs() { + let source = r#" + input int a; + a <; + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + + expect![r#"QASM3 Parse Error: expr_bp: expected expression"#].assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/expression/binary/arithmetic_conversions.rs b/compiler/qsc_qasm3/src/tests/expression/binary/arithmetic_conversions.rs new file mode 100644 index 0000000000..0479783076 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary/arithmetic_conversions.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn int_idents_without_width_can_be_multiplied() -> miette::Result<(), Vec> { + let source = " + int x = 5; + int y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_idents_with_same_width_can_be_multiplied() -> miette::Result<(), Vec> { + let source = " + int[32] x = 5; + int[32] y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_idents_with_different_width_can_be_multiplied() -> miette::Result<(), Vec> { + let source = " + int[32] x = 5; + int[64] y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiplying_int_idents_with_different_width_result_in_higher_width_result( +) -> miette::Result<(), Vec> { + let source = " + int[32] x = 5; + int[64] y = 3; + int[64] z = x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + mutable z = x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiplying_int_idents_with_different_width_result_in_no_width_result( +) -> miette::Result<(), Vec> { + let source = " + int[32] x = 5; + int[64] y = 3; + int z = x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + mutable z = x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiplying_int_idents_with_width_greater_than_64_result_in_bigint_result( +) -> miette::Result<(), Vec> { + let source = " + int[32] x = 5; + int[64] y = 3; + int[67] z = x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + mutable z = Microsoft.Quantum.Convert.IntAsBigInt(x * y); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/binary/comparison.rs b/compiler/qsc_qasm3/src/tests/expression/binary/comparison.rs new file mode 100644 index 0000000000..7421372465 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary/comparison.rs @@ -0,0 +1,276 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp_file; + +/// These tests use manually constructed QASM with parens exprs +/// as there is a bug in the QASM parser with complex RHS exprs + +#[test] +fn int_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = " + int x = 5; + int y = 3; + bool f = (x > y); + bool e = (x >= y); + bool a = (x < y); + bool c = (x <= y); + bool b = (x == y); + bool d = (x != y); + "; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Int, Int, Bool, Bool, Bool, Bool, Bool, Bool) { + mutable x = 5; + mutable y = 3; + mutable f = (x > y); + mutable e = (x >= y); + mutable a = (x < y); + mutable c = (x <= y); + mutable b = (x == y); + mutable d = (x != y); + (x, y, f, e, a, c, b, d) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn uint_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = " + uint x = 5; + uint y = 3; + bool f = (x > y); + bool e = (x >= y); + bool a = (x < y); + bool c = (x <= y); + bool b = (x == y); + bool d = (x != y); + "; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Int, Int, Bool, Bool, Bool, Bool, Bool, Bool) { + mutable x = 5; + mutable y = 3; + mutable f = (x > y); + mutable e = (x >= y); + mutable a = (x < y); + mutable c = (x <= y); + mutable b = (x == y); + mutable d = (x != y); + (x, y, f, e, a, c, b, d) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + bit y = 0; + bool f = (x > y); + bool e = (x >= y); + bool a = (x < y); + bool c = (x <= y); + bool b = (x == y); + bool d = (x != y); + "; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Result, Result, Bool, Bool, Bool, Bool, Bool, Bool) { + mutable x = One; + mutable y = Zero; + mutable f = (x > y); + mutable e = (x >= y); + mutable a = (x < y); + mutable c = (x <= y); + mutable b = (x == y); + mutable d = (x != y); + (x, y, f, e, a, c, b, d) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bitarray_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = r#" + bit[1] x = "1"; + bit[1] y = "0"; + bool f = (x > y); + bool e = (x >= y); + bool a = (x < y); + bool c = (x <= y); + bool b = (x == y); + bool d = (x != y); + "#; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Result[], Result[], Bool, Bool, Bool, Bool, Bool, Bool) { + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable x = [One]; + mutable y = [Zero]; + mutable f = (__ResultArrayAsIntBE__(x) > __ResultArrayAsIntBE__(y)); + mutable e = (__ResultArrayAsIntBE__(x) >= __ResultArrayAsIntBE__(y)); + mutable a = (__ResultArrayAsIntBE__(x) < __ResultArrayAsIntBE__(y)); + mutable c = (__ResultArrayAsIntBE__(x) <= __ResultArrayAsIntBE__(y)); + mutable b = (__ResultArrayAsIntBE__(x) == __ResultArrayAsIntBE__(y)); + mutable d = (__ResultArrayAsIntBE__(x) != __ResultArrayAsIntBE__(y)); + (x, y, f, e, a, c, b, d) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bitarray_var_comparison_to_int_can_be_translated() -> miette::Result<(), Vec> { + let source = r#" + bit[1] x = "1"; + input int y; + bool a = (x > y); + bool b = (x >= y); + bool c = (x < y); + bool d = (x <= y); + bool e = (x == y); + bool f = (x != y); + bool g = (y > x); + bool h = (y >= x); + bool i = (y < x); + bool j = (y <= x); + bool k = (y == x); + bool l = (y != x); + "#; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + operation Test(y : Int) : (Result[], Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool) { + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable x = [One]; + mutable a = (__ResultArrayAsIntBE__(x) > y); + mutable b = (__ResultArrayAsIntBE__(x) >= y); + mutable c = (__ResultArrayAsIntBE__(x) < y); + mutable d = (__ResultArrayAsIntBE__(x) <= y); + mutable e = (__ResultArrayAsIntBE__(x) == y); + mutable f = (__ResultArrayAsIntBE__(x) != y); + mutable g = (y > __ResultArrayAsIntBE__(x)); + mutable h = (y >= __ResultArrayAsIntBE__(x)); + mutable i = (y < __ResultArrayAsIntBE__(x)); + mutable j = (y <= __ResultArrayAsIntBE__(x)); + mutable k = (y == __ResultArrayAsIntBE__(x)); + mutable l = (y != __ResultArrayAsIntBE__(x)); + (x, a, b, c, d, e, f, g, h, i, j, k, l) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = " + float x = 5; + float y = 3; + bool f = (x > y); + bool e = (x >= y); + bool a = (x < y); + bool c = (x <= y); + bool b = (x == y); + bool d = (x != y); + "; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Double, Double, Bool, Bool, Bool, Bool, Bool, Bool) { + mutable x = 5.; + mutable y = 3.; + mutable f = (x > y); + mutable e = (x >= y); + mutable a = (x < y); + mutable c = (x <= y); + mutable b = (x == y); + mutable d = (x != y); + (x, y, f, e, a, c, b, d) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bool_var_comparisons_can_be_translated() -> miette::Result<(), Vec> { + let source = " + bool x = true; + bool y = false; + bool a = (x && y); + bool b = (x || y); + bool c = (!x && !y); + bool d = (!x || !y); + bool e = (!x && y); + bool f = (!x || y); + bool g = (x && !y); + bool h = (x || !y); + "; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool, Bool) { + mutable x = true; + mutable y = false; + mutable a = (x and y); + mutable b = (x or y); + mutable c = (not x and not y); + mutable d = (not x or not y); + mutable e = (not x and y); + mutable f = (not x or y); + mutable g = (x and not y); + mutable h = (x or not y); + (x, y, a, b, c, d, e, f, g, h) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/binary/ident.rs b/compiler/qsc_qasm3/src/tests/expression/binary/ident.rs new file mode 100644 index 0000000000..d7b99a3190 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary/ident.rs @@ -0,0 +1,70 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn mutable_int_idents_without_width_can_be_multiplied() -> miette::Result<(), Vec> { + let source = " + int x = 5; + int y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_int_idents_without_width_can_be_multiplied() -> miette::Result<(), Vec> { + let source = " + const int x = 5; + const int y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let x = 5; + let y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn const_int_idents_widthless_lhs_can_be_multiplied_by_explicit_width_int( +) -> miette::Result<(), Vec> { + let source = " + const int[32] x = 5; + const int y = 3; + x * y; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let x = 5; + let y = 3; + x * y; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/binary/literal.rs b/compiler/qsc_qasm3/src/tests/expression/binary/literal.rs new file mode 100644 index 0000000000..aa07b02a7e --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary/literal.rs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod multiplication; diff --git a/compiler/qsc_qasm3/src/tests/expression/binary/literal/multiplication.rs b/compiler/qsc_qasm3/src/tests/expression/binary/literal/multiplication.rs new file mode 100644 index 0000000000..5f554a59b5 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/binary/literal/multiplication.rs @@ -0,0 +1,73 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::{compile_qasm_stmt_to_qsharp, compile_qasm_to_qsharp}; + +#[test] +fn int_float_lhs_promoted_to_float() -> miette::Result<(), Vec> { + let source = " + 5 * 0.3; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + 5. * 0.3; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_int_rhs_promoted_to_float() -> miette::Result<(), Vec> { + let source = " + 0.3 * 5; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + 0.3 * 5.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn int_int_stays_int() -> miette::Result<(), Vec> { + let source = " + 3 * 5; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + 3 * 5; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn float_float_stays_float() -> miette::Result<(), Vec> { + let source = " + 3. * 5.; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + 3. * 5.; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/bits.rs b/compiler/qsc_qasm3/src/tests/expression/bits.rs new file mode 100644 index 0000000000..f57b46c570 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/bits.rs @@ -0,0 +1,103 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_to_qsharp, compile_qasm_to_qsharp_file}; + +use expect_test::expect; +use miette::Report; + +#[test] +fn bit_array_bits_and_register_ops() -> miette::Result<(), Vec> { + let source = r#" + bit[8] a = "10001111"; + bit[8] b = "01110000"; + output bit[8] ls_a_1; + ls_a_1 = (a << 1); // Bit shift left produces "00011110" + output bit[8] a_or_b; + a_or_b = (a | b); // Bitwise OR produces "11111111" + output bit[8] a_and_b; + a_and_b = (a & b); // Bitwise AND produces "00000000" + output bit[8] a_xor_b; + a_xor_b = (a ^ b); // Bitwise XOR produces "11111111" + //output bit[8] not_a; + //not_a = ~a; // Bitwise NOT produces "01110000" + output bit[8] rs_a_1; + rs_a_1 = (a >> 1); // Bit shift right produces "01000111" + "#; + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : (Result[], Result[], Result[], Result[], Result[]) { + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + function __IntAsResultArrayBE__(number : Int, bits : Int) : Result[] { + mutable runningValue = number; + mutable result = []; + for _ in 1..bits { + set result += [__BoolAsResult__((runningValue &&& 1) != 0)]; + set runningValue >>>= 1; + } + Microsoft.Quantum.Arrays.Reversed(result) + } + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable a = [One, Zero, Zero, Zero, One, One, One, One]; + mutable b = [Zero, One, One, One, Zero, Zero, Zero, Zero]; + mutable ls_a_1 = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set ls_a_1 = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) <<< 1, 8)); + mutable a_or_b = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set a_or_b = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) ||| __ResultArrayAsIntBE__(b), 8)); + mutable a_and_b = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set a_and_b = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) &&& __ResultArrayAsIntBE__(b), 8)); + mutable a_xor_b = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set a_xor_b = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) ^^^ __ResultArrayAsIntBE__(b), 8)); + mutable rs_a_1 = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set rs_a_1 = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) >>> 1, 8)); + (ls_a_1, a_or_b, a_and_b, a_xor_b, rs_a_1) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_array_left_shift() -> miette::Result<(), Vec> { + let source = r#" + bit[8] a = "10001111"; + output bit[8] ls_a_1; + ls_a_1 = (a << 1); // Bit shift left produces "00011110" + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + function __IntAsResultArrayBE__(number : Int, bits : Int) : Result[] { + mutable runningValue = number; + mutable result = []; + for _ in 1..bits { + set result += [__BoolAsResult__((runningValue &&& 1) != 0)]; + set runningValue >>>= 1; + } + Microsoft.Quantum.Arrays.Reversed(result) + } + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable a = [One, Zero, Zero, Zero, One, One, One, One]; + mutable ls_a_1 = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + set ls_a_1 = (__IntAsResultArrayBE__(__ResultArrayAsIntBE__(a) <<< 1, 8)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/ident.rs b/compiler/qsc_qasm3/src/tests/expression/ident.rs new file mode 100644 index 0000000000..760886957f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/ident.rs @@ -0,0 +1,152 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_stmt_to_qsharp, compile_qasm_to_qsharp}; + +use expect_test::expect; +use miette::Report; + +#[test] +fn unresolved_idenfiers_raise_symbol_error() { + let source = " + float x = t; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected an error"); + }; + assert_eq!(1, errors.len(), "Expected one error"); + expect![r#"Undefined symbol: t."#].assert_eq(&errors[0].to_string()); +} + +// this test verifies QASM behavior that would normally be allowed +// by the Q# compiler +#[test] +fn redefining_symbols_in_same_scope_raise_symbol_error() { + let source = " + float x = 0; + float x = 5; + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected an error"); + }; + assert_eq!(1, errors.len(), "Expected one error"); + expect![r#"Redefined symbol: x."#].assert_eq(&errors[0].to_string()); +} + +#[test] +fn resolved_idenfiers_are_compiled_as_refs() -> miette::Result<(), Vec> { + let source = " + float p = pi; + float x = p; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable p = Microsoft.Quantum.Math.PI(); + mutable x = p; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn euler_latin_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = euler; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.E(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn euler_unicode_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = ℇ; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.E(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn pi_latin_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = pi; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.PI(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn pi_unicode_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = π; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = Microsoft.Quantum.Math.PI(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn tau_latin_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = tau; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 2. * Microsoft.Quantum.Math.PI(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn tau_unicode_is_resolved() -> miette::Result<(), Vec> { + let source = " + float x = τ; + "; + + let qsharp = compile_qasm_stmt_to_qsharp(source)?; + expect![ + r#" + mutable x = 2. * Microsoft.Quantum.Math.PI(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bit.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bit.rs new file mode 100644 index 0000000000..a389025afc --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bit.rs @@ -0,0 +1,206 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn to_bool_and_back_implicitly() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + input bit a; + bool _bool0; + bool _bool1; + _bool0 = true; + _bool1 = a; + _bool0 = _bool1; + _bool0 = _bool1; + a = _bool1; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable _bool0 = false; + mutable _bool1 = false; + set _bool0 = true; + set _bool1 = __ResultAsBool__(a); + set _bool0 = _bool1; + set _bool0 = _bool1; + set a = __BoolAsResult__(_bool1); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_bool_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + bool y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable x = One; + mutable y = __ResultAsBool__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + int y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable x = One; + mutable y = __ResultAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + int[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable x = One; + mutable y = __ResultAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + uint y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable x = One; + mutable y = __ResultAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + uint[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable x = One; + mutable y = __ResultAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_bigint_implicitly() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + int[65] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBigInt__(input : Result) : BigInt { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1L + } else { + 0L + } + } + mutable x = One; + mutable y = __ResultAsBigInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_float_implicitly_fails() { + let source = " + bit x = 1; + float y = x; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type Bit(False) to type Float(None, False)"#] + .assert_eq(&error[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bitarray.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bitarray.rs new file mode 100644 index 0000000000..1a8b4d4223 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bitarray.rs @@ -0,0 +1,159 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn to_int_decl_implicitly() -> miette::Result<(), Vec> { + let source = r#" + bit[5] reg; + int b = reg; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable reg = [Zero, Zero, Zero, Zero, Zero]; + mutable b = __ResultArrayAsIntBE__(reg); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_int_assignment_implicitly() -> miette::Result<(), Vec> { + let source = r#" + bit[5] reg; + int a; + a = reg; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable reg = [Zero, Zero, Zero, Zero, Zero]; + mutable a = 0; + set a = __ResultArrayAsIntBE__(reg); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_int_with_equal_width_in_assignment_implicitly() -> miette::Result<(), Vec> { + let source = r#" + bit[5] reg; + int[5] a; + a = reg; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable reg = [Zero, Zero, Zero, Zero, Zero]; + mutable a = 0; + set a = __ResultArrayAsIntBE__(reg); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_int_with_equal_width_in_decl_implicitly() -> miette::Result<(), Vec> { + let source = r#" + bit[5] reg; + int[5] a = reg; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + mutable reg = [Zero, Zero, Zero, Zero, Zero]; + mutable a = __ResultArrayAsIntBE__(reg); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_int_with_higher_width_implicitly_fails() { + let source = " + int[6] a; + bit[5] reg; + a = reg; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type BitArray(D1(5), False) to type Int(Some(6), False)"#] + .assert_eq(&error[0].to_string()); +} + +#[test] +fn to_int_with_higher_width_decl_implicitly_fails() { + let source = " + bit[5] reg; + int[6] a = reg; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type BitArray(D1(5), False) to type Int(Some(6), False)"#] + .assert_eq(&error[0].to_string()); +} + +#[test] +fn to_int_with_lower_width_implicitly_fails() { + let source = " + input int[4] a; + bit[5] reg; + a = reg; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type BitArray(D1(5), False) to type Int(Some(4), False)"#] + .assert_eq(&error[0].to_string()); +} + +#[test] +fn to_int_with_lower_width_decl_implicitly_fails() { + let source = " + bit[5] reg; + int[4] a = reg; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type BitArray(D1(5), False) to type Int(Some(4), False)"#] + .assert_eq(&error[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bool.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bool.rs new file mode 100644 index 0000000000..b9b14e9a1a --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_bool.rs @@ -0,0 +1,241 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn to_bit_and_back_implicitly() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + input bool a; + bit _bit0; + bit _bit1; + _bit0 = true; + _bit1 = a; + _bit0 = _bit1; + _bit0 = _bit1; + a = _bit1; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable _bit0 = Zero; + mutable _bit1 = Zero; + set _bit0 = One; + set _bit1 = __BoolAsResult__(a); + set _bit0 = _bit1; + set _bit0 = _bit1; + set a = __ResultAsBool__(_bit1); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_bit_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + bit y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + mutable x = true; + mutable y = __BoolAsResult__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + int y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsInt__(value : Bool) : Int { + if value { + 1 + } else { + 0 + } + } + mutable x = true; + mutable y = __BoolAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + int[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsInt__(value : Bool) : Int { + if value { + 1 + } else { + 0 + } + } + mutable x = true; + mutable y = __BoolAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + uint y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsInt__(value : Bool) : Int { + if value { + 1 + } else { + 0 + } + } + mutable x = true; + mutable y = __BoolAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + uint[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsInt__(value : Bool) : Int { + if value { + 1 + } else { + 0 + } + } + mutable x = true; + mutable y = __BoolAsInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_bigint_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + int[65] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsBigInt__(value : Bool) : BigInt { + if value { + 1L + } else { + 0L + } + } + mutable x = true; + mutable y = __BoolAsBigInt__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + float y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsDouble__(value : Bool) : Double { + if value { + 1. + } else { + 0. + } + } + mutable x = true; + mutable y = __BoolAsDouble__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + bool x = true; + float[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsDouble__(value : Bool) : Double { + if value { + 1. + } else { + 0. + } + } + mutable x = true; + mutable y = __BoolAsDouble__(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_complex.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_complex.rs new file mode 100644 index 0000000000..fc36ab244f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_complex.rs @@ -0,0 +1,2 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_float.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_float.rs new file mode 100644 index 0000000000..b0a4a01626 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_float.rs @@ -0,0 +1,223 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn to_bit_implicitly() { + let source = " + float x = 42.; + bit y = x; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type Float(None, False) to type Bit(False)"#] + .assert_eq(&error[0].to_string()); +} + +#[test] +fn explicit_width_to_bit_implicitly() { + let source = " + float[64] x = 42.; + bit y = x; + "; + + let Err(error) = compile_qasm_to_qsharp(source) else { + panic!("Expected error") + }; + + expect![r#"Cannot cast expression of type Float(Some(64), False) to type Bit(False)"#] + .assert_eq(&error[0].to_string()); +} + +#[test] +fn to_bool_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + bool y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = if Microsoft.Quantum.Math.Truncate(x) == 0 { + false + } else { + true + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + int y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Truncate(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + int[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Truncate(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + uint y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Truncate(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + uint[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Truncate(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_bigint_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + int[65] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Convert.IntAsBigInt(Microsoft.Quantum.Math.Truncate(x)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + float y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + float[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_complex_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + complex[float] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Complex(x, 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_complex_implicitly() -> miette::Result<(), Vec> { + let source = " + float x = 42.; + complex[float[32]] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42.; + mutable y = Microsoft.Quantum.Math.Complex(x, 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_int.rs b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_int.rs new file mode 100644 index 0000000000..77dc46e194 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/implicit_cast_from_int.rs @@ -0,0 +1,215 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +fn to_bit_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + bit y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = if x == 0 { + One + } else { + Zero + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_bool_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + bool y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = if x == 0 { + false + } else { + true + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + int y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_int_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + int[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + uint y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_uint_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + uint[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_bigint_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + int[65] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = Microsoft.Quantum.Convert.IntAsBigInt(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + float y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = Microsoft.Quantum.Convert.IntAsDouble(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_float_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + float[32] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = Microsoft.Quantum.Convert.IntAsDouble(x); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_implicit_complex_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + complex[float] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = Microsoft.Quantum.Math.Complex(Microsoft.Quantum.Convert.IntAsDouble(x), 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn to_explicit_complex_implicitly() -> miette::Result<(), Vec> { + let source = " + int x = 42; + complex[float[32]] y = x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 42; + mutable y = Microsoft.Quantum.Math.Complex(Microsoft.Quantum.Convert.IntAsDouble(x), 0.); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/indexed.rs b/compiler/qsc_qasm3/src/tests/expression/indexed.rs new file mode 100644 index 0000000000..0e35ff62b4 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/indexed.rs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_stmt_to_qsharp, compile_qasm_to_qsharp}; + +use expect_test::expect; +use miette::Report; + +#[test] +fn indexed_bit_cannot_be_implicitly_converted_to_float() { + let source = " + bit[5] x; + if (x[0] == 1.) { + } + "; + + let Err(errors) = compile_qasm_stmt_to_qsharp(source) else { + panic!("Expected an error"); + }; + + assert_eq!(1, errors.len(), "Expected one error"); + expect![r#"Cannot cast expression of type Bit(False) to type Float(None, False)"#] + .assert_eq(&errors[0].to_string()); +} + +#[test] +fn indexed_bit_can_implicitly_convert_to_int() -> miette::Result<(), Vec> { + let source = " + bit[5] x; + if (x[0] == 1) { + x[1] = 1; + } + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable x = [Zero, Zero, Zero, Zero, Zero]; + if __ResultAsInt__(x[0]) == 1 { + set x w/= 1 <- One; + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn indexed_bit_can_implicitly_convert_to_bool() -> miette::Result<(), Vec> { + let source = " + bit[5] x; + if (x[0]) { + x[1] = 1; + } + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable x = [Zero, Zero, Zero, Zero, Zero]; + if __ResultAsBool__(x[0]) { + set x w/= 1 <- One; + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn bit_indexed_ty_is_same_as_element_ty() -> miette::Result<(), Vec> { + let source = " + bit[5] x; + bit y = x[0]; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = [Zero, Zero, Zero, Zero, Zero]; + mutable y = x[0]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "Not yet implemented"] +fn bool_indexed_ty_is_same_as_element_ty() -> miette::Result<(), Vec> { + let source = " + bool[5] x; + bool y = x[0]; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = [false, false, false, false, false]; + mutable y = x[0]; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/expression/unary.rs b/compiler/qsc_qasm3/src/tests/expression/unary.rs new file mode 100644 index 0000000000..70d1d2c18f --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/expression/unary.rs @@ -0,0 +1,134 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use expect_test::expect; +use miette::Report; + +use crate::tests::compile_qasm_to_qsharp; + +#[test] +#[ignore = "OPENQASM 3.0 parser bug"] +fn bitwise_not_int() -> miette::Result<(), Vec> { + let source = " + int x = 5; + int y = ~x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 5; + mutable y = ~~~x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn not_bool() -> miette::Result<(), Vec> { + let source = " + bool x = true; + bool y = !x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = true; + mutable y = not x; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn not_result() -> miette::Result<(), Vec> { + let source = " + bit x = 1; + bit y = !x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __BoolAsResult__(input : Bool) : Result { + Microsoft.Quantum.Convert.BoolAsResult(input) + } + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable x = One; + mutable y = __BoolAsResult__(not __ResultAsBool__(x)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn logical_not_int() -> miette::Result<(), Vec> { + let source = " + int x = 159; + bool y = !x; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable x = 159; + mutable y = not if x == 0 { + false + } else { + true + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "OPENQASM 3.0 parser bug"] +fn bitwise_not_result() -> miette::Result<(), Vec> { + let source = " + bit[1] x; + bool success = ~x[0]; + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn logical_not_indexed_bit_array_in_if_cond() -> miette::Result<(), Vec> { + let source = " + bit[10] Classical; + if (!Classical[1]) { + Classical[0] = 1; + } + "; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + mutable Classical = [Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero, Zero]; + if not __ResultAsBool__(Classical[1]) { + set Classical w/= 0 <- One; + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/output.rs b/compiler/qsc_qasm3/src/tests/output.rs new file mode 100644 index 0000000000..1cb10cd7ae --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/output.rs @@ -0,0 +1,344 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::{ + qasm_to_program, + tests::{fail_on_compilation_errors, gen_qsharp, parse}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; +use expect_test::expect; +use miette::Report; +use qsc::target::Profile; + +use super::compile_qasm_to_qir; + +#[test] +fn using_re_semantics_removes_output() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + output bit[2] c; + qubit[2] q; + input float[64] theta; + input int[64] beta; + output float[64] gamma; + output float[64] delta; + rz(theta) q[0]; + h q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::ResourceEstimation, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let qsharp = gen_qsharp(&unit.package.expect("no package found")); + expect![ + r#" + namespace qasm3_import { + operation Test(theta : Double, beta : Int) : Unit { + mutable c = [Zero, Zero]; + let q = QIR.Runtime.AllocateQubitArray(2); + mutable gamma = 0.; + mutable delta = 0.; + Rz(theta, q[0]); + H(q[0]); + CNOT(q[0], q[1]); + set c w/= 0 <- M(q[0]); + set c w/= 1 <- M(q[1]); + } + }"# + ] + .assert_eq(&qsharp); + + Ok(()) +} + +#[test] +fn using_qasm_semantics_captures_all_classical_decls_as_output() -> miette::Result<(), Vec> +{ + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + output bit[2] c; + qubit[2] q; + input float[64] theta; + input int[64] beta; + output float[64] gamma; + output float[64] delta; + rz(theta) q[0]; + h q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let qsharp = gen_qsharp(&unit.package.expect("no package found")); + expect![ + r#" + namespace qasm3_import { + operation Test(theta : Double, beta : Int) : (Result[], Double, Double) { + mutable c = [Zero, Zero]; + let q = QIR.Runtime.AllocateQubitArray(2); + mutable gamma = 0.; + mutable delta = 0.; + Rz(theta, q[0]); + H(q[0]); + CNOT(q[0], q[1]); + set c w/= 0 <- M(q[0]); + set c w/= 1 <- M(q[1]); + (c, gamma, delta) + } + }"# + ] + .assert_eq(&qsharp); + + Ok(()) +} + +#[test] +fn using_qiskit_semantics_only_bit_array_is_captured_and_reversed( +) -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + output bit[2] c; + qubit[2] q; + input float[64] theta; + input int[64] beta; + output float[64] gamma; + output float[64] delta; + rz(theta) q[0]; + h q[0]; + cx q[0], q[1]; + c[0] = measure q[0]; + c[1] = measure q[1]; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let qsharp = gen_qsharp(&unit.package.expect("no package found")); + expect![ + r#" +namespace qasm3_import { + operation Test(theta : Double, beta : Int) : Result[] { + mutable c = [Zero, Zero]; + let q = QIR.Runtime.AllocateQubitArray(2); + mutable gamma = 0.; + mutable delta = 0.; + Rz(theta, q[0]); + H(q[0]); + CNOT(q[0], q[1]); + set c w/= 0 <- M(q[0]); + set c w/= 1 <- M(q[1]); + Microsoft.Quantum.Arrays.Reversed(c) + } +}"# + ] + .assert_eq(&qsharp); + + Ok(()) +} + +#[test] +fn using_qiskit_semantics_multiple_bit_arrays_are_reversed_in_order_and_reversed_in_content( +) -> miette::Result<(), Vec> { + let source = r#" +OPENQASM 3.0; +include "stdgates.inc"; +output bit[2] c; +output bit[3] c2; +qubit[5] q; +input float[64] theta; +input int[64] beta; +output float[64] gamma; +output float[64] delta; +rz(theta) q[0]; +h q[0]; +cx q[0], q[1]; +x q[2]; +id q[3]; +x q[4]; +c[0] = measure q[0]; +c[1] = measure q[1]; +c2[0] = measure q[2]; +c2[1] = measure q[3]; +c2[2] = measure q[4]; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let package = unit.package.expect("no package found"); + let qsharp = gen_qsharp(&package.clone()); + expect![ + r#" +namespace qasm3_import { + operation Test(theta : Double, beta : Int) : (Result[], Result[]) { + mutable c = [Zero, Zero]; + mutable c2 = [Zero, Zero, Zero]; + let q = QIR.Runtime.AllocateQubitArray(5); + mutable gamma = 0.; + mutable delta = 0.; + Rz(theta, q[0]); + H(q[0]); + CNOT(q[0], q[1]); + X(q[2]); + I(q[3]); + X(q[4]); + set c w/= 0 <- M(q[0]); + set c w/= 1 <- M(q[1]); + set c2 w/= 0 <- M(q[2]); + set c2 w/= 1 <- M(q[3]); + set c2 w/= 2 <- M(q[4]); + (Microsoft.Quantum.Arrays.Reversed(c2), Microsoft.Quantum.Arrays.Reversed(c)) + } +}"# + ] + .assert_eq(&qsharp); + + Ok(()) +} + +#[test] +fn qir_generation_using_qiskit_semantics_multiple_bit_arrays_are_reversed_in_order_and_reversed_in_content( +) -> miette::Result<(), Vec> { + let source = r#" +OPENQASM 3.0; +include "stdgates.inc"; +output bit[2] c; +output bit[3] c2; +qubit[5] q; +float[64] theta = 0.5; +int[64] beta = 4; +output float[64] gamma; +output float[64] delta; +rz(theta) q[0]; +h q[0]; +cx q[0], q[1]; +x q[2]; +id q[3]; +x q[4]; +barrier q[0], q[1]; +c[0] = measure q[0]; +c[1] = measure q[1]; +c2[0] = measure q[2]; +c2[1] = measure q[3]; +c2[2] = measure q[4]; + "#; + + let qir = compile_qasm_to_qir(source, Profile::AdaptiveRI)?; + expect![ + r#" +%Result = type opaque +%Qubit = type opaque + +define void @ENTRYPOINT__main() #0 { +block_0: + call void @__quantum__qis__rz__body(double 0.5, %Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__cx__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__x__body(%Qubit* inttoptr (i64 4 to %Qubit*)) + call void @__quantum__qis__barrier__body() + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 1 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 2 to %Qubit*), %Result* inttoptr (i64 2 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 3 to %Qubit*), %Result* inttoptr (i64 3 to %Result*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 4 to %Qubit*), %Result* inttoptr (i64 4 to %Result*)) + call void @__quantum__rt__tuple_record_output(i64 2, i8* null) + call void @__quantum__rt__array_record_output(i64 3, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 4 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 3 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 2 to %Result*), i8* null) + call void @__quantum__rt__array_record_output(i64 2, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 1 to %Result*), i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + ret void +} + +declare void @__quantum__qis__rz__body(double, %Qubit*) + +declare void @__quantum__qis__h__body(%Qubit*) + +declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*) + +declare void @__quantum__qis__x__body(%Qubit*) + +declare void @__quantum__qis__barrier__body() + +declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +declare void @__quantum__rt__array_record_output(i64, i8*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="5" "required_num_results"="5" } +attributes #1 = { "irreversible" } + +; module flags + +!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + +!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} +!4 = !{i32 1, !"classical_ints", i1 true} +!5 = !{i32 1, !"qubit_resetting", i1 true} +!6 = !{i32 1, !"classical_floats", i1 false} +!7 = !{i32 1, !"backwards_branching", i1 false} +!8 = !{i32 1, !"classical_fixed_points", i1 false} +!9 = !{i32 1, !"user_functions", i1 false} +!10 = !{i32 1, !"multiple_target_branching", i1 false} +"# + ] + .assert_eq(&qir); + + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/sample_circuits.rs b/compiler/qsc_qasm3/src/tests/sample_circuits.rs new file mode 100644 index 0000000000..b778631a8d --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/sample_circuits.rs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod bell_pair; +mod rgqft_multiplier; diff --git a/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs b/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs new file mode 100644 index 0000000000..d16e3685ac --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/sample_circuits/bell_pair.rs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + qasm_to_program, + tests::{gen_qsharp, parse, print_compilation_errors}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; + +const SOURCE: &str = r#" +OPENQASM 3.0; +include "stdgates.inc"; +bit[2] c; +qubit[2] q; +h q[0]; +cx q[0], q[1]; +barrier q[0], q[1]; +c[0] = measure q[0]; +c[1] = measure q[1]; +"#; + +#[test] +fn it_compiles() { + let source = SOURCE; + + let res = parse(source).expect("should parse"); + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + print_compilation_errors(&unit); + assert!(!unit.has_errors()); + let Some(package) = &unit.package else { + panic!("no package found"); + }; + let qsharp = gen_qsharp(package); + println!("{qsharp}"); +} diff --git a/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs b/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs new file mode 100644 index 0000000000..b1585934b7 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/sample_circuits/rgqft_multiplier.rs @@ -0,0 +1,127 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + qasm_to_program, + tests::{gen_qsharp, parse, print_compilation_errors}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; + +#[test] +fn it_compiles() { + let source = SOURCE; + + let res = parse(source).expect("should parse"); + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::OpenQasm, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + print_compilation_errors(&unit); + assert!(!unit.has_errors()); + let Some(package) = &unit.package else { + panic!("no package found"); + }; + let qsharp = gen_qsharp(package); + println!("{qsharp}"); +} + +const SOURCE: &str = r#" +OPENQASM 3.0; +include "stdgates.inc"; +qubit[1] a; +qubit[1] b; +qubit[2] out; +h out[1]; +rz(pi/4) out[1]; +cx out[1], out[0]; +rz(-pi/4) out[0]; +cx out[1], out[0]; +rz(pi/4) out[0]; +h out[0]; +cx a[0], out[0]; +rz(-pi/8) out[0]; +rx(pi/2) out[0]; +rz(pi) out[0]; +rx(pi/2) out[0]; +rz(9.032078879070655) out[0]; +cx b[0], out[0]; +rz(-7*pi/8) out[0]; +rx(pi/2) out[0]; +rz(pi) out[0]; +rx(pi/2) out[0]; +rz(6.675884388878311) out[0]; +cx a[0], out[0]; +rz(-pi/8) out[0]; +rx(pi/2) out[0]; +rz(pi) out[0]; +rx(pi/2) out[0]; +rz(9.032078879070655) out[0]; +cx b[0], out[0]; +rz(pi/4) b[0]; +rx(pi/2) b[0]; +rz(pi) b[0]; +rx(pi/2) b[0]; +rz(3*pi) b[0]; +cx a[0], b[0]; +rz(-pi/4) b[0]; +rx(pi/2) b[0]; +rz(pi) b[0]; +rx(pi/2) b[0]; +rz(3*pi) b[0]; +cx a[0], b[0]; +rz(pi/4) a[0]; +cx a[0], out[1]; +rz(-7*pi/8) out[0]; +rx(pi/2) out[0]; +rz(pi) out[0]; +rx(pi/2) out[0]; +rz(6.675884388878311) out[0]; +h out[0]; +rz(-pi/16) out[1]; +rx(pi/2) out[1]; +rz(pi) out[1]; +rx(pi/2) out[1]; +rz(9.228428419920018) out[1]; +cx b[0], out[1]; +rz(-15*pi/16) out[1]; +rx(pi/2) out[1]; +rz(pi) out[1]; +rx(pi/2) out[1]; +rz(6.4795348480289485) out[1]; +cx a[0], out[1]; +rz(-pi/16) out[1]; +rx(pi/2) out[1]; +rz(pi) out[1]; +rx(pi/2) out[1]; +rz(9.228428419920018) out[1]; +cx b[0], out[1]; +rz(pi/8) b[0]; +rx(pi/2) b[0]; +rz(pi) b[0]; +rx(pi/2) b[0]; +rz(3*pi) b[0]; +cx a[0], b[0]; +rz(-pi/8) b[0]; +rx(pi/2) b[0]; +rz(pi) b[0]; +rx(pi/2) b[0]; +rz(3*pi) b[0]; +cx a[0], b[0]; +rz(pi/8) a[0]; +rz(-15*pi/16) out[1]; +rx(pi/2) out[1]; +rz(pi) out[1]; +rx(pi/2) out[1]; +rz(6.4795348480289485) out[1]; +rz(-pi/4) out[1]; +cx out[1], out[0]; +rz(pi/4) out[0]; +cx out[1], out[0]; +rz(-pi/4) out[0]; +h out[1];"#; diff --git a/compiler/qsc_qasm3/src/tests/scopes.rs b/compiler/qsc_qasm3/src/tests/scopes.rs new file mode 100644 index 0000000000..9ca041b5b7 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/scopes.rs @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn can_access_const_decls_from_global_scope() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + const int i = 7; + gate my_h q { + if (i == 0) { + h q; + } + } + qubit q; + my_h q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let i = 7; + let my_h : (Qubit) => Unit = (q) => { + if i == 0 { + H(q); + }; + }; + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + my_h(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn cannot_access_mutable_decls_from_global_scope() { + let source = r#" + include "stdgates.inc"; + int i; + gate my_h q { + if (i == 0) { + h q; + } + } + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected an error"); + }; + expect![r#"Undefined symbol: i."#].assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/statement.rs b/compiler/qsc_qasm3/src/tests/statement.rs new file mode 100644 index 0000000000..9938cb4160 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement.rs @@ -0,0 +1,15 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod annotation; +mod end; +mod for_loop; +mod gate_call; +mod if_stmt; +mod implicit_modified_gate_call; +mod include; +mod measure; +mod modified_gate_call; +mod reset; +mod switch; +mod while_loop; diff --git a/compiler/qsc_qasm3/src/tests/statement/annotation.rs b/compiler/qsc_qasm3/src/tests/statement/annotation.rs new file mode 100644 index 0000000000..2a747673dc --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/annotation.rs @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn simulatable_intrinsic_can_be_applied_to_gate() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + @SimulatableIntrinsic + gate my_h q { + h q; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + @SimulatableIntrinsic() + operation my_h(q : Qubit) : Unit { + H(q); + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn unknown_annotation_raises_error() { + let source = r#" + include "stdgates.inc"; + @SomeUnknownAnnotation + gate my_h q { + h q; + } + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected an error"); + }; + expect![r#"Unexpected annotation: @SomeUnknownAnnotation."#].assert_eq(&errors[0].to_string()); +} + +#[test] +fn annotation_without_target_in_global_scope_raises_error() { + let source = r#" + int i; + @SimulatableIntrinsic + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected an error"); + }; + expect![r#"Annotation missing target statement."#].assert_eq(&errors[0].to_string()); +} + +#[test] +fn annotation_without_target_in_block_scope_raises_error() { + let source = r#" + int i; + if (0 == 1) { + @SimulatableIntrinsic + } + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected an error"); + }; + expect![r#"Annotation missing target statement."#].assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/statement/end.rs b/compiler/qsc_qasm3/src/tests/statement/end.rs new file mode 100644 index 0000000000..988748628c --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/end.rs @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn end_can_be_in_nested_scope() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in {1, 5, 10} { + end; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0; + for i : Int in [1, 5, 10] { + fail "end" + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn end_can_be_in_global_scope() -> miette::Result<(), Vec> { + let source = r#" + end; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![r#"fail "end""#].assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/for_loop.rs b/compiler/qsc_qasm3/src/tests/statement/for_loop.rs new file mode 100644 index 0000000000..a758c52ca5 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/for_loop.rs @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn for_loops_can_iterate_over_discrete_set() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in {1, 5, 10} { + sum += i; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0; + for i : Int in [1, 5, 10] { + set sum += i; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn for_loops_can_have_stmt_bodies() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in {1, 5, 10} + sum += i; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0; + for i : Int in [1, 5, 10] { + set sum += i; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn for_loops_can_iterate_over_range() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + for int i in [0:2:20] { + sum += i; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0; + for i : Int in 0..2..20 { + set sum += i; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn for_loops_can_iterate_float_set() -> miette::Result<(), Vec> { + let source = r#" + float sum = 0.; + for float[64] f in {1.2, -3.4, 0.5, 9.8} { + sum += f; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0.; + for f : Double in [1.2, -3.4, 0.5, 9.8] { + set sum += f; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "Array literals not yet supported"] +fn for_loops_can_iterate_float_array_symbol() -> miette::Result<(), Vec> { + let source = r#" + float sum = 0.; + const array[float[64], 4] my_floats = {1.2, -3.4, 0.5, 9.8}; + for float[64] f in my_floats { + sum += f; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable sum = 0.; + let my_floats = [1.2, -3.4, 0.5, 9.8]; + for f : Double in my_floats { + set sum += f; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +// Spec says these should work, but they fail to parse: +// bit[5] register; +// for b in register {} +// let alias = register[1:3]; +// for b in alias {}? +#[test] +fn for_loops_can_iterate_bit_register() -> miette::Result<(), Vec> { + let source = r#" + int sum = 0; + const bit[5] reg = "10101"; + for bit b in reg { + sum += b; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsInt__(input : Result) : Int { + if Microsoft.Quantum.Convert.ResultAsBool(input) { + 1 + } else { + 0 + } + } + mutable sum = 0; + let reg = [One, Zero, One, Zero, One]; + for b : Result in reg { + set sum += __ResultAsInt__(b); + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn loop_variables_should_be_scoped_to_for_loop() { + let source = r#" + int sum = 0; + for int i in {1, 5, 10} { } + sum += i; + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + + expect![r#"Undefined symbol: i."#].assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/statement/gate_call.rs b/compiler/qsc_qasm3/src/tests/statement/gate_call.rs new file mode 100644 index 0000000000..7e35301bb6 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/gate_call.rs @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_to_qir, compile_qasm_to_qsharp}; +use expect_test::expect; +use miette::Report; +use qsc::target::Profile; + +#[test] +fn x_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + x q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + X(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn barrier_can_be_called_on_single_qubit() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + barrier q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + @SimulatableIntrinsic() + operation __quantum__qis__barrier__body() : Unit {} + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + __quantum__qis__barrier__body(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn barrier_can_be_called_without_qubits() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + barrier; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + @SimulatableIntrinsic() + operation __quantum__qis__barrier__body() : Unit {} + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + __quantum__qis__barrier__body(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn barrier_generates_qir() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + bit[1] c; + qubit[2] q; + barrier q[0], q[1]; + barrier q[0]; + barrier; + barrier q[0], q[1], q[0]; + c[0] = measure q[0]; + "#; + + let qsharp = compile_qasm_to_qir(source, Profile::AdaptiveRI)?; + expect![ + r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__barrier__body() + call void @__quantum__qis__barrier__body() + call void @__quantum__qis__barrier__body() + call void @__quantum__qis__barrier__body() + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__rt__array_record_output(i64 1, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__barrier__body() + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__array_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="1" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + + !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} + !4 = !{i32 1, !"classical_ints", i1 true} + !5 = !{i32 1, !"qubit_resetting", i1 true} + !6 = !{i32 1, !"classical_floats", i1 false} + !7 = !{i32 1, !"backwards_branching", i1 false} + !8 = !{i32 1, !"classical_fixed_points", i1 false} + !9 = !{i32 1, !"user_functions", i1 false} + !10 = !{i32 1, !"multiple_target_branching", i1 false} + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn barrier_can_be_called_on_two_qubit() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + barrier q[0], q[1]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + @SimulatableIntrinsic() + operation __quantum__qis__barrier__body() : Unit {} + let q = QIR.Runtime.AllocateQubitArray(2); + __quantum__qis__barrier__body(); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/if_stmt.rs b/compiler/qsc_qasm3/src/tests/statement/if_stmt.rs new file mode 100644 index 0000000000..37d0426d3d --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/if_stmt.rs @@ -0,0 +1,164 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn can_use_cond_with_implicit_cast_to_bool() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + + h q; + bit result = measure q; + if (result) { + reset q; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + H(q); + mutable result = M(q); + if __ResultAsBool__(result) { + Reset(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn can_use_negated_cond_with_implicit_cast_to_bool() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + + h q; + bit result = measure q; + if (!result) { + reset q; + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + H(q); + mutable result = M(q); + if not __ResultAsBool__(result) { + Reset(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +/// +/// Both true-body and false-body can be a single statement terminated +/// by a semicolon, or a program block of several statements { stmt1; stmt2; }. +/// The stmts can also be on the next line. + +#[test] +#[ignore = "QASM3 Parser bug"] +fn then_branch_can_be_stmt() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + if (0 == 1) z q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + if 0 == 1 { + Z(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "QASM3 Parser bug"] +fn else_branch_can_be_stmt() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + if (0 == 1) {z q;} + else y q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + if 0 == 1 { + Z(q); + } else { + Y(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "QASM3 Parser bug"] +fn then_and_else_branch_can_be_stmt() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + if (0 == 1) z q; + else y q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + if 0 == 1 { + Z(q); + } else { + Y(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn using_cond_that_cannot_implicit_cast_to_bool_fail() { + let source = r#" + qubit q; + if (q) { + reset q; + } + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + + expect![r#"Cannot cast expression of type Qubit to type Bool(False)"#] + .assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/tests/statement/implicit_modified_gate_call.rs b/compiler/qsc_qasm3/src/tests/statement/implicit_modified_gate_call.rs new file mode 100644 index 0000000000..138ee9fec8 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/implicit_modified_gate_call.rs @@ -0,0 +1,122 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn sdg_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + sdg q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + Adjoint S(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn tdg_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + tdg q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + Adjoint T(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn crx_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + crx(0.5) q[1], q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + Controlled Rx([q[1]], (0.5, q[0])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn cry_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + cry(0.5) q[1], q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + Controlled Ry([q[1]], (0.5, q[0])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn crz_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + crz(0.5) q[1], q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + Controlled Rz([q[1]], (0.5, q[0])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn ch_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + ch q[1], q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + Controlled H([q[1]], q[0]); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/include.rs b/compiler/qsc_qasm3/src/tests/statement/include.rs new file mode 100644 index 0000000000..284d917a28 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/include.rs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::{ + qasm_to_program, + tests::{parse_all, qsharp_from_qasm_compilation}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; +use expect_test::expect; +use miette::Report; + +#[test] +fn programs_with_includes_can_be_parsed() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + include "custom_intrinsics.inc"; + bit[1] c; + qubit[1] q; + my_gate q[0]; + c[0] = measure q[0]; + "#; + let custom_intrinsics = r#" + @SimulatableIntrinsic + gate my_gate q { + x q; + } + "#; + let all_sources = [ + ("source0.qasm".into(), source.into()), + ("custom_intrinsics.inc".into(), custom_intrinsics.into()), + ]; + + let res = parse_all("source0.qasm", all_sources)?; + let r = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + let qsharp = qsharp_from_qasm_compilation(r)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : Result[] { + @SimulatableIntrinsic() + operation my_gate(q : Qubit) : Unit { + X(q); + } + mutable c = [Zero]; + let q = QIR.Runtime.AllocateQubitArray(1); + my_gate(q[0]); + set c w/= 0 <- M(q[0]); + Microsoft.Quantum.Arrays.Reversed(c) + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/measure.rs b/compiler/qsc_qasm3/src/tests/statement/measure.rs new file mode 100644 index 0000000000..0939e5d49c --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/measure.rs @@ -0,0 +1,129 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn single_qubit_can_be_measured_into_single_bit() -> miette::Result<(), Vec> { + let source = r#" + bit c; + qubit q; + c = measure q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable c = Zero; + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + set c = M(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "Arrow syntax is not supported yet in the parser"] +fn single_qubit_can_be_arrow_measured_into_single_bit() -> miette::Result<(), Vec> { + let source = r#" + bit c; + qubit q; + measure q -> c; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable c = Zero; + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + set c = M(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn indexed_single_qubit_can_be_measured_into_indexed_bit_register( +) -> miette::Result<(), Vec> { + let source = r#" + bit[1] c; + qubit[1] q; + c[0] = measure q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable c = [Zero]; + let q = QIR.Runtime.AllocateQubitArray(1); + set c w/= 0 <- M(q[0]); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn indexed_single_qubit_can_be_measured_into_single_bit_register() -> miette::Result<(), Vec> +{ + let source = r#" + bit c; + qubit[1] q; + c = measure q[0]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable c = Zero; + let q = QIR.Runtime.AllocateQubitArray(1); + set c = M(q[0]); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn measuring_hardware_qubits_generates_an_error() { + let source = r#" + bit c; + c = measure $2; + "#; + + let Err(err) = compile_qasm_to_qsharp(source) else { + panic!("Measuring HW qubit should have generated an error"); + }; + assert!( + err.len() == 1, + "Expected a single error when measuring a HW qubit, got: {err:#?}" + ); + + assert!(err[0] + .to_string() + .contains("Hardware qubit operands are not supported")); +} + +#[test] +fn value_from_measurement_can_be_dropped() -> miette::Result<(), Vec> { + let source = r#" + qubit q; + measure q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + M(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/modified_gate_call.rs b/compiler/qsc_qasm3/src/tests/statement/modified_gate_call.rs new file mode 100644 index 0000000000..a2a75a2bdc --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/modified_gate_call.rs @@ -0,0 +1,257 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn adj_x_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + inv @ x q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + Adjoint X(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn adj_adj_x_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + inv @ inv @ x q; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + Adjoint Adjoint X(q); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiple_controls_on_x_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[3] q; + qubit f; + ctrl(3) @ x q[1], q[0], q[2], f; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(3); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Controlled X([q[1], q[0], q[2]], f); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn repeated_multi_controls_on_x_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + qubit[3] r; + qubit f; + ctrl(2) @ ctrl(3) @ x q[1], r[0], q[0], f, r[1], r[2]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + let r = QIR.Runtime.AllocateQubitArray(3); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Controlled Controlled X([q[1], r[0]], ([q[0], f, r[1]], r[2])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn repeated_multi_controls_on_x_gate_can_be_mixed_with_inv() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[2] q; + qubit[3] r; + qubit f; + ctrl(2) @ inv @ ctrl(3) @ inv @ x q[1], r[0], q[0], f, r[1], r[2]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(2); + let r = QIR.Runtime.AllocateQubitArray(3); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Controlled Adjoint Controlled Adjoint X([q[1], r[0]], ([q[0], f, r[1]], r[2])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiple_controls_on_cx_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[4] q; + qubit f; + ctrl(3) @ cx q[1], q[0], q[2], f, q[3]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(4); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Controlled CNOT([q[1], q[0], q[2]], (f, q[3])); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn multiple_controls_on_crx_gate_can_be_called() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[4] q; + qubit f; + ctrl(3) @ inv @ crx(0.5) q[1], q[0], q[2], f, q[3]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(4); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Controlled Adjoint Controlled Rx([q[1], q[0], q[2]], ([f], (0.5, q[3]))); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn neg_ctrl_can_be_applied_and_wrapped_in_another_modifier() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[4] q; + qubit f; + inv @ negctrl(3) @ inv @ crx(0.5) q[1], q[0], q[2], f, q[3]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(4); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + Adjoint ApplyControlledOnInt(0, Adjoint Controlled Rx, [q[1], q[0], q[2]], ([f], (0.5, q[3]))); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn neg_ctrl_can_wrap_another_neg_crtl_modifier() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[6] q; + qubit f; + negctrl(3) @ negctrl(2) @ crx(0.5) q[1], q[0], q[2], q[3], q[4], f, q[5]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(6); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + ApplyControlledOnInt(0, ApplyControlledOnInt, [q[1], q[0], q[2]], (0, Controlled Rx, [q[3], q[4]], ([f], (0.5, q[5])))); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn modifiers_can_be_repeated_many_times() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit[6] q; + qubit f; + pow(1) @ pow(1) @ pow(1) @ crx(0.5) f, q[5]; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + operation __Pow__ < 'T > (N : Int, op : ('T => Unit is Adj), target : 'T) : Unit is Adj { + let op = if N > 0 { + () => op(target) + } else { + () => Adjoint op(target) + }; + for _ in 1..Microsoft.Quantum.Math.AbsI(N) { + op() + } + } + let q = QIR.Runtime.AllocateQubitArray(6); + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + __Pow__(1, __Pow__, (1, __Pow__, (1, Controlled Rx, ([f], (0.5, q[5]))))); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn pow_can_be_applied_on_a_simple_gate() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit f; + pow(2) @ x f; + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + operation __Pow__ < 'T > (N : Int, op : ('T => Unit is Adj), target : 'T) : Unit is Adj { + let op = if N > 0 { + () => op(target) + } else { + () => Adjoint op(target) + }; + for _ in 1..Microsoft.Quantum.Math.AbsI(N) { + op() + } + } + let f = QIR.Runtime.__quantum__rt__qubit_allocate(); + __Pow__(2, X, (f)); + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/reset.rs b/compiler/qsc_qasm3/src/tests/statement/reset.rs new file mode 100644 index 0000000000..6c03040c21 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/reset.rs @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::{ + qasm_to_program, + tests::{fail_on_compilation_errors, gen_qsharp, parse}, + CompilerConfig, OutputSemantics, ProgramType, QubitSemantics, +}; +use expect_test::expect; +use miette::Report; +use qsc::target::Profile; + +use crate::tests::compile_qasm_to_qir; + +#[test] +fn reset_calls_are_generated_from_qasm() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + bit[1] meas; + qubit[1] q; + reset q[0]; + h q[0]; + meas[0] = measure q[0]; + "#; + + let res = parse(source)?; + assert!(!res.has_errors()); + let unit = qasm_to_program( + res.source, + res.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics: OutputSemantics::Qiskit, + program_ty: ProgramType::File("Test".to_string()), + }, + ); + fail_on_compilation_errors(&unit); + let qsharp = gen_qsharp(&unit.package.expect("no package found")); + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : Result[] { + mutable meas = [Zero]; + let q = QIR.Runtime.AllocateQubitArray(1); + Reset(q[0]); + H(q[0]); + set meas w/= 0 <- M(q[0]); + Microsoft.Quantum.Arrays.Reversed(meas) + } + }"# + ] + .assert_eq(&qsharp); + + Ok(()) +} + +#[test] +fn reset_with_base_profile_is_rewritten_without_resets() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + bit[1] meas; + qubit[1] q; + reset q[0]; + h q[0]; + meas[0] = measure q[0]; + "#; + + let qir = compile_qasm_to_qir(source, Profile::Base)?; + expect![ + r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + 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__cz__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Qubit* inttoptr (i64 2 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 1 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 1 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__rt__array_record_output(i64 1, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__h__body(%Qubit*) + + declare void @__quantum__qis__cz__body(%Qubit*, %Qubit*) + + declare void @__quantum__rt__array_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="base_profile" "required_num_qubits"="3" "required_num_results"="1" } + 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} +"# + ] + .assert_eq(&qir); + + Ok(()) +} + +#[test] +fn reset_with_adaptive_ri_profile_generates_reset_qir() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + bit[1] meas; + qubit[1] q; + reset q[0]; + h q[0]; + meas[0] = measure q[0]; + "#; + + let qir = compile_qasm_to_qir(source, Profile::AdaptiveRI)?; + expect![ + r#" + %Result = type opaque + %Qubit = type opaque + + define void @ENTRYPOINT__main() #0 { + block_0: + call void @__quantum__qis__reset__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__h__body(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__rt__array_record_output(i64 1, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + ret void + } + + declare void @__quantum__qis__reset__body(%Qubit*) + + declare void @__quantum__qis__h__body(%Qubit*) + + declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + + declare void @__quantum__rt__array_record_output(i64, i8*) + + declare void @__quantum__rt__result_record_output(%Result*, i8*) + + attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } + attributes #1 = { "irreversible" } + + ; module flags + + !llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + + !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} + !4 = !{i32 1, !"classical_ints", i1 true} + !5 = !{i32 1, !"qubit_resetting", i1 true} + !6 = !{i32 1, !"classical_floats", i1 false} + !7 = !{i32 1, !"backwards_branching", i1 false} + !8 = !{i32 1, !"classical_fixed_points", i1 false} + !9 = !{i32 1, !"user_functions", i1 false} + !10 = !{i32 1, !"multiple_target_branching", i1 false} +"# + ] + .assert_eq(&qir); + + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/switch.rs b/compiler/qsc_qasm3/src/tests/statement/switch.rs new file mode 100644 index 0000000000..957d14a83d --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/switch.rs @@ -0,0 +1,302 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::{compile_qasm_to_qsharp, compile_qasm_to_qsharp_file}; +use expect_test::expect; +use miette::Report; + +#[test] +fn default_is_optional() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + int i = 15; + switch (i) { + case 1 { + i = 2; + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + mutable i = 15; + if i == 1 { + set i = 2; + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn default_as_only_case_causes_parse_error() { + let source = r#" + OPENQASM 3.0; + int i = 15; + switch (i) { + default { + i = 2; + } + } + "#; + + let res = compile_qasm_to_qsharp(source); + let Err(errors) = res else { + panic!("Expected an error, got {res:?}"); + }; + assert_eq!(errors.len(), 1); + expect![r#"QASM3 Parse Error: expecting `case` keyword"#].assert_eq(&errors[0].to_string()); +} + +#[test] +fn no_cases_causes_parse_error() { + let source = r#" + OPENQASM 3.0; + int i = 15; + switch (i) { + } + "#; + + let res = compile_qasm_to_qsharp(source); + let Err(errors) = res else { + panic!("Expected an error, got {res:?}"); + }; + assert_eq!(errors.len(), 1); + expect![r#"QASM3 Parse Error: expecting `case` keyword"#].assert_eq(&errors[0].to_string()); +} + +#[test] +fn spec_case_1() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + + int i = 15; + + switch (i) { + case 1, 3, 5 { + h q; + } + case 2, 4, 6 { + x q; + } + case -1 { + y q; + } + default { + z q; + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + mutable i = 15; + if i == 1 or i == 3 or i == 5 { + H(q); + } elif i == 2 or i == 4 or i == 6 { + X(q); + } elif i == -1 { + Y(q); + } else { + Z(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn spec_case_2() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + + const int A = 0; + const int B = 1; + int i = 15; + + switch (i) { + case A { + h q; + } + case B { + x q; + } + case B+1 { + y q; + } + default { + z q; + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + let A = 0; + let B = 1; + mutable i = 15; + if i == A { + H(q); + } elif i == B { + X(q); + } elif i == B + 1 { + Y(q); + } else { + Z(q); + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn spec_case_3() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + bit[2] b; + // int(b) orginally, but we don't support that yet + switch (b) { + case 0b00 { + h q; + } + case 0b01 { + x q; + } + case 0b10 { + y q; + } + case 0b11 { + z q; + } + } + "#; + + let qsharp = compile_qasm_to_qsharp_file(source)?; + expect![ + r#" + namespace qasm3_import { + @EntryPoint() + operation Test() : Result[] { + function __ResultArrayAsIntBE__(results : Result[]) : Int { + Microsoft.Quantum.Convert.ResultArrayAsInt(Microsoft.Quantum.Arrays.Reversed(results)) + } + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + mutable b = [Zero, Zero]; + if __ResultArrayAsIntBE__(b) == 0 { + H(q); + } elif __ResultArrayAsIntBE__(b) == 1 { + X(q); + } elif __ResultArrayAsIntBE__(b) == 2 { + Y(q); + } elif __ResultArrayAsIntBE__(b) == 3 { + Z(q); + }; + b + } + }"# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +#[ignore = "Function decls are not supported yet"] +fn spec_case_4() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + qubit q; + bit[2] b; + def foo(int i, qubit[8] d) -> bit { + return measure d[i]; + } + + int i = 15; + + int j = 1; + int k = 2; + + bit c1; + + qubit[8] q0; + + switch (i) { + case 1 { + j = k + foo(k, q0); + } + case 2 { + float[64] d = j / k; + } + case 3 { + } + default { + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn spec_case_5() -> miette::Result<(), Vec> { + let source = r#" + OPENQASM 3.0; + include "stdgates.inc"; + + + qubit[8] q; + + int j = 30; + int i; + + switch (i) { + case 1, 2, 5, 12 { + } + case 3 { + switch (j) { + case 10, 15, 20 { + h q; + } + } + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + let q = QIR.Runtime.AllocateQubitArray(8); + mutable j = 30; + mutable i = 0; + if i == 1 or i == 2 or i == 5 or i == 12 {} elif i == 3 { + if j == 10 or j == 15 or j == 20 { + H(q); + }; + }; + "# + ] + .assert_eq(&qsharp); + Ok(()) +} diff --git a/compiler/qsc_qasm3/src/tests/statement/while_loop.rs b/compiler/qsc_qasm3/src/tests/statement/while_loop.rs new file mode 100644 index 0000000000..6cafb09d13 --- /dev/null +++ b/compiler/qsc_qasm3/src/tests/statement/while_loop.rs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#![allow(clippy::needless_raw_string_hashes)] + +use crate::tests::compile_qasm_to_qsharp; +use expect_test::expect; +use miette::Report; + +#[test] +fn can_iterate_over_mutable_var_cmp_expr() -> miette::Result<(), Vec> { + let source = r#" + include "stdgates.inc"; + qubit q; + bit result; + + int i = 0; + while (i < 10) { + h q; + result = measure q; + if (result) { + i += 1; + } + } + "#; + + let qsharp = compile_qasm_to_qsharp(source)?; + expect![ + r#" + function __ResultAsBool__(input : Result) : Bool { + Microsoft.Quantum.Convert.ResultAsBool(input) + } + let q = QIR.Runtime.__quantum__rt__qubit_allocate(); + mutable result = Zero; + mutable i = 0; + while i < 10 { + H(q); + set result = M(q); + if __ResultAsBool__(result) { + set i += 1; + }; + } + "# + ] + .assert_eq(&qsharp); + Ok(()) +} + +#[test] +fn using_cond_that_cannot_implicit_cast_to_bool_fail() { + let source = r#" + qubit q; + while (q) { + reset q; + } + "#; + + let Err(errors) = compile_qasm_to_qsharp(source) else { + panic!("Expected error"); + }; + + expect![r#"Cannot cast expression of type Qubit to type Bool(False)"#] + .assert_eq(&errors[0].to_string()); +} diff --git a/compiler/qsc_qasm3/src/types.rs b/compiler/qsc_qasm3/src/types.rs new file mode 100644 index 0000000000..1f25090902 --- /dev/null +++ b/compiler/qsc_qasm3/src/types.rs @@ -0,0 +1,316 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::fmt::{self, Display, Formatter}; + +use oq3_semantics::types::ArrayDims; +use qsc::Span; +use rustc_hash::FxHashMap; + +thread_local! { + /// + pub static GATE_MAP: FxHashMap<&'static str, &'static str> = { + let mut m = FxHashMap::default(); + // p is rz, should have been replaced by rz by transpile + + m.insert("x", "X"); + m.insert("y", "Y"); + m.insert("z", "Z"); + + m.insert("h", "H"); + + m.insert("s", "S"); + m.insert("sdg", "sdg"); + + m.insert("t", "T"); + m.insert("tdg", "tdg"); + + // sx q is Rx(pi/2, q), should have been replaced by Rx by transpile + + m.insert("crx", "crx"); + m.insert("cry", "cry"); + m.insert("crz", "crz"); + + m.insert("rx", "Rx"); + m.insert("ry", "Ry"); + m.insert("rz", "Rz"); + + m.insert("rxx", "Rxx"); + m.insert("ryy", "Ryy"); + m.insert("rzz", "Rzz"); + + m.insert("cx", "CNOT"); + m.insert("cy", "CY"); + m.insert("cz", "CZ"); + + // cp (controlled-phase), should have been replaced by transpile + + m.insert("ch", "ch"); + + m.insert("id", "I"); + + m.insert("swap", "SWAP"); + + m.insert("ccx", "CCNOT"); + + // cswap (controlled-swap), should have been replaced by transpile + + // cu (controlled-U), should have been replaced by transpile + + // openqasm 2.0 gates should have been replaced by transpile + // CX, phase, cphase, id, u1, u2, u3 + m + }; +} + +pub(crate) fn get_qsharp_gate_name>(gate_name: S) -> Option<&'static str> { + GATE_MAP.with(|map| map.get(gate_name.as_ref()).copied()) +} + +/// When compiling QASM3 expressions, we need to keep track of the sematic QASM +/// type of the expression. This allows us to perform type checking and casting +/// when necessary. +#[derive(Debug, Clone, PartialEq)] +pub struct QasmTypedExpr { + pub ty: oq3_semantics::types::Type, + pub expr: qsc::ast::Expr, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GateModifier { + /// The `adjoint` modifier. + Inv(Span), + Pow(Option, Span), + Ctrl(Option, Span), + NegCtrl(Option, Span), +} + +impl QasmTypedExpr { + pub fn new(ty: oq3_semantics::types::Type, expr: qsc::ast::Expr) -> Self { + Self { ty, expr } + } +} + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct Complex { + pub real: f64, + pub imaginary: f64, +} + +impl Complex { + pub fn new(real: f64, imaginary: f64) -> Self { + Self { real, imaginary } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) enum Type { + Bool(bool), + BigInt(bool), + Complex(bool), + Int(bool), + Double(bool), + Qubit, + Result(bool), + Tuple(Vec), + Range, + BoolArray(ArrayDimensions, bool), + BigIntArray(ArrayDimensions, bool), + IntArray(ArrayDimensions, bool), + DoubleArray(ArrayDimensions), + QubitArray(ArrayDimensions), + ResultArray(ArrayDimensions, bool), + TupleArray(ArrayDimensions, Vec), + /// Function or operation, with the number of classical parameters and qubits. + Callable(CallableKind, usize, usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CallableKind { + /// A function. + Function, + /// An operation. + Operation, +} + +impl Display for CallableKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CallableKind::Function => write!(f, "Function"), + CallableKind::Operation => write!(f, "Operation"), + } + } +} + +/// QASM supports up to seven dimensions, but we are going to limit it to three. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ArrayDimensions { + One(usize), + Two(usize, usize), + Three(usize, usize, usize), +} + +impl From<&ArrayDims> for ArrayDimensions { + fn from(value: &ArrayDims) -> Self { + match value { + ArrayDims::D1(dim) => ArrayDimensions::One(*dim), + ArrayDims::D2(dim1, dim2) => ArrayDimensions::Two(*dim1, *dim2), + ArrayDims::D3(dim1, dim2, dim3) => ArrayDimensions::Three(*dim1, *dim2, *dim3), + } + } +} + +impl Display for Type { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + Type::Bool(_) => write!(f, "bool"), + Type::BigInt(_) => write!(f, "BigInt"), + Type::Complex(_) => write!(f, "Complex"), + Type::Int(_) => write!(f, "Int"), + Type::Double(_) => write!(f, "Double"), + Type::Qubit => write!(f, "Qubit"), + Type::Range => write!(f, "Range"), + Type::Result(_) => write!(f, "Result"), + Type::Tuple(types) => { + write!(f, "(")?; + for (i, ty) in types.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{ty}")?; + } + write!(f, ")") + } + Type::BoolArray(dim, _) => write!(f, "bool{dim}"), + Type::BigIntArray(dim, _) => write!(f, "BigInt{dim}"), + Type::IntArray(dim, _) => write!(f, "Int{dim}"), + Type::DoubleArray(dim) => write!(f, "Double{dim}"), + Type::QubitArray(dim) => write!(f, "Qubit{dim}"), + Type::ResultArray(dim, _) => write!(f, "Result{dim}"), + Type::TupleArray(dim, types) => { + write!(f, "(")?; + for (i, ty) in types.iter().enumerate() { + if i > 0 { + write!(f, ", ")?; + } + write!(f, "{ty}")?; + } + write!(f, "){dim}") + } + Type::Callable(kind, num_classical, num_qubits) => { + write!(f, "Callable({kind}, {num_classical}, {num_qubits})") + } + } + } +} + +impl Display for ArrayDimensions { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + ArrayDimensions::One(..) => write!(f, "[]"), + ArrayDimensions::Two(..) => write!(f, "[][]"), + ArrayDimensions::Three(..) => write!(f, "[][][]"), + } + } +} + +fn can_cast_type_in_assignment(lhs: &Type, rhs: &Type) -> bool { + if types_equal_ignore_const(lhs, rhs) { + return true; + } + matches!( + (lhs, rhs), + (Type::Double(_) | Type::Complex(_), Type::Int(_)) | (Type::Complex(_), Type::Double(_)) + ) +} + +fn types_equal_ignore_const(lhs: &Type, rhs: &Type) -> bool { + match (lhs, rhs) { + (Type::Bool(_), Type::Bool(_)) + | (Type::Int(_), Type::Int(_)) + | (Type::Double(_), Type::Double(_)) + | (Type::Complex(_), Type::Complex(_)) + | (Type::Result(_), Type::Result(_)) + | (Type::BigInt(_), Type::BigInt(_)) + | (Type::Qubit, Type::Qubit) => true, + (Type::Tuple(lhs), Type::Tuple(rhs)) => { + if lhs.len() != rhs.len() { + return false; + } + lhs.iter() + .zip(rhs.iter()) + .all(|(lhs, rhs)| types_equal_ignore_const(lhs, rhs)) + } + (Type::ResultArray(lhs_dims, _), Type::ResultArray(rhs_dims, _)) + | (Type::BigIntArray(lhs_dims, _), Type::BigIntArray(rhs_dims, _)) + | (Type::BoolArray(lhs_dims, _), Type::BoolArray(rhs_dims, _)) + | (Type::IntArray(lhs_dims, _), Type::IntArray(rhs_dims, _)) + | (Type::DoubleArray(lhs_dims), Type::DoubleArray(rhs_dims)) + | (Type::QubitArray(lhs_dims), Type::QubitArray(rhs_dims)) => lhs_dims == rhs_dims, + (Type::TupleArray(lhs_dims, lhs), Type::TupleArray(rhs_dims, rhs)) => { + lhs_dims == rhs_dims + && lhs.len() == rhs.len() + && lhs + .iter() + .zip(rhs.iter()) + .all(|(lhs, rhs)| types_equal_ignore_const(lhs, rhs)) + } + _ => false, + } +} + +/// Get the indexed type of a given type. +/// For example, if the type is `Int[2][3]`, the indexed type is `Int[2]`. +/// If the type is `Int[2]`, the indexed type is `Int`. +/// If the type is `Int`, the indexed type is `None`. +/// +/// This is useful for determining the type of an array element. +pub(crate) fn get_indexed_type( + ty: &oq3_semantics::types::Type, +) -> Option { + use oq3_semantics::types::{IsConst, Type}; + let ty = match &ty { + Type::AngleArray(dims) => match dims { + ArrayDims::D1(_) => Type::Angle(None, IsConst::False), + ArrayDims::D2(l, _) => Type::AngleArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::AngleArray(ArrayDims::D2(*l, *w)), + }, + Type::BitArray(dims, is_const) => match dims { + ArrayDims::D1(_) => Type::Bit(is_const.clone()), + ArrayDims::D2(l, _) => Type::BitArray(ArrayDims::D1(*l), is_const.clone()), + ArrayDims::D3(l, w, _) => Type::BitArray(ArrayDims::D2(*l, *w), is_const.clone()), + }, + Type::BoolArray(dims) => match dims { + ArrayDims::D1(_) => Type::Bool(IsConst::False), + ArrayDims::D2(l, _) => Type::BoolArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::BoolArray(ArrayDims::D2(*l, *w)), + }, + Type::ComplexArray(dims) => match dims { + ArrayDims::D1(_) => Type::Complex(None, IsConst::False), + ArrayDims::D2(l, _) => Type::ComplexArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::ComplexArray(ArrayDims::D2(*l, *w)), + }, + Type::FloatArray(dims) => match dims { + ArrayDims::D1(_) => Type::Float(None, IsConst::False), + ArrayDims::D2(l, _) => Type::FloatArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::FloatArray(ArrayDims::D2(*l, *w)), + }, + Type::IntArray(dims) => match dims { + ArrayDims::D1(_) => Type::Int(None, IsConst::False), + ArrayDims::D2(l, _) => Type::IntArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::IntArray(ArrayDims::D2(*l, *w)), + }, + Type::QubitArray(dims) => match dims { + ArrayDims::D1(_) => Type::Qubit, + ArrayDims::D2(l, _) => Type::QubitArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::QubitArray(ArrayDims::D2(*l, *w)), + }, + Type::UIntArray(dims) => match dims { + ArrayDims::D1(_) => Type::UInt(None, IsConst::False), + ArrayDims::D2(l, _) => Type::UIntArray(ArrayDims::D1(*l)), + ArrayDims::D3(l, w, _) => Type::UIntArray(ArrayDims::D2(*l, *w)), + }, + _ => return None, + }; + Some(ty) +} diff --git a/pip/Cargo.toml b/pip/Cargo.toml index 5a200ee815..e032dfcfd5 100644 --- a/pip/Cargo.toml +++ b/pip/Cargo.toml @@ -14,6 +14,7 @@ noisy_simulator = { path = "../noisy_simulator" } num-bigint = { workspace = true } num-complex = { workspace = true } qsc = { path = "../compiler/qsc" } +qsc_qasm3 = { path = "../compiler/qsc_qasm3", features = ["fs"]} resource_estimator = { path = "../resource_estimator" } miette = { workspace = true, features = ["fancy-no-syscall"] } rustc-hash = { workspace = true } diff --git a/pip/pyproject.toml b/pip/pyproject.toml index 79e85acdeb..eaf1f91d01 100644 --- a/pip/pyproject.toml +++ b/pip/pyproject.toml @@ -20,6 +20,7 @@ classifiers = [ [project.optional-dependencies] jupyterlab = ["qsharp-jupyterlab"] widgets = ["qsharp-widgets"] +qiskit = ["qiskit>=1.2.0,<1.3.0"] [build-system] requires = ["maturin ~= 1.2.0"] diff --git a/pip/qsharp/_fs.py b/pip/qsharp/_fs.py index 91f48790c1..3f14a01df3 100644 --- a/pip/qsharp/_fs.py +++ b/pip/qsharp/_fs.py @@ -1,24 +1,58 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +""" +_fs.py + +This module provides file system utility functions for working with the file +system as Python sees it. These are used as callbacks passed into native code +to allow the native code to interact with the file system in an +environment-specific way. +""" + import os from typing import Dict, List, Tuple def read_file(path: str) -> Tuple[str, str]: + """ + Read the contents of a file. + + Args: + path (str): The path to the file. + + Returns: + Tuple[str, str]: A tuple containing the path and the file contents. + """ with open(path, mode="r", encoding="utf-8-sig") as f: return (path, f.read()) def list_directory(dir_path: str) -> List[Dict[str, str]]: + """ + Lists the contents of a directory and returns a list of dictionaries, + where each dictionary represents an entry in the directory. + + Args: + dir_path (str): The path of the directory to list. + + Returns: + List[Dict[str, str]]: A list of dictionaries representing the entries + in the directory. Each dictionary contains the following keys: + - "path": The full path of the entry. + - "entry_name": The name of the entry. + - "type": The type of the entry: "file", "folder", or "unknown". + """ + def map_dir(e: str) -> Dict[str, str]: + path = os.path.join(dir_path, e) return { - "path": os.path.join(dir_path, e), + "path": path, "entry_name": e, "type": ( "file" - if os.path.isfile(os.path.join(dir_path, e)) - else "folder" if os.path.isdir(os.path.join(dir_path, e)) else "unknown" + if os.path.isfile(path) + else "folder" if os.path.isdir(path) else "unknown" ), } @@ -26,12 +60,41 @@ def map_dir(e: str) -> Dict[str, str]: def resolve(base: str, path: str) -> str: + """ + Resolves a relative path with respect to a base path. + + Args: + base (str): The base path. + path (str): The relative path. + + Returns: + str: The resolved path. + """ return os.path.normpath(join(base, path)) def exists(path) -> bool: + """ + Check if a file or directory exists at the given path. + + Args: + path (str): The path to the file or directory. + + Returns: + bool: True if the file or directory exists, False otherwise. + """ return os.path.exists(path) def join(path: str, *paths) -> str: + """ + Joins one or more path components intelligently. + + Args: + path (str): The base path. + *paths (str): Additional path components to be joined. + + Returns: + str: The concatenated path. + """ return os.path.join(path, *paths) diff --git a/pip/qsharp/_http.py b/pip/qsharp/_http.py index 9438ac94c9..61ebcf3700 100644 --- a/pip/qsharp/_http.py +++ b/pip/qsharp/_http.py @@ -1,8 +1,32 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +""" +_http.py + +This module provides HTTP utility functions for interacting with +GitHub repositories. +""" + def fetch_github(owner: str, repo: str, ref: str, path: str) -> str: + """ + Fetches the content of a file from a GitHub repository. + + Args: + owner (str): The owner of the GitHub repository. + repo (str): The name of the GitHub repository. + ref (str): The reference (branch, tag, or commit) of the repository. + path (str): The path to the file within the repository. + + Returns: + str: The content of the file as a string. + + Raises: + urllib.error.HTTPError: If there is an error fetching the file from GitHub. + urllib.error.URLError: If there is an error with the URL. + """ + import urllib path_no_leading_slash = path[1:] if path.startswith("/") else path diff --git a/pip/qsharp/_ipython.py b/pip/qsharp/_ipython.py index 8b74d4cb28..1879e48285 100644 --- a/pip/qsharp/_ipython.py +++ b/pip/qsharp/_ipython.py @@ -1,6 +1,13 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +""" +_ipython.py + +This module provides IPython magic functions for integrating Q# code +execution within Jupyter notebooks. +""" + from IPython.display import display, Javascript, clear_output from IPython.core.magic import register_cell_magic from ._native import QSharpError @@ -26,6 +33,7 @@ def callback(output): try: return get_interpreter().interpret(cell, callback) except QSharpError as e: + # pylint: disable=raise-missing-from raise QSharpCellError(str(e)) diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 1deabfc449..3f6e679a31 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -2,9 +2,13 @@ # Licensed under the MIT License. from enum import Enum -from typing import Any, Callable, ClassVar, Optional, Dict, List +from typing import Any, Callable, Optional, Dict, List, Tuple -class TargetProfile: +# pylint: disable=unused-argument +# E302 is fighting with the formatter for number of blank lines +# flake8: noqa: E302 + +class TargetProfile(Enum): """ A Q# target profile. @@ -12,7 +16,7 @@ class TargetProfile: which will be used to run the Q# program. """ - Base: ClassVar[Any] + Base: TargetProfile """ Target supports the minimal set of capabilities required to run a quantum program. @@ -20,7 +24,7 @@ class TargetProfile: This option maps to the Base Profile as defined by the QIR specification. """ - Adaptive_RI: ClassVar[Any] + Adaptive_RI: TargetProfile """ Target supports the Adaptive profile with integer computation and qubit reset capabilities. @@ -30,11 +34,45 @@ class TargetProfile: reset capabilities, as defined by the QIR specification. """ - Unrestricted: ClassVar[Any] + Unrestricted: TargetProfile """ Describes the unrestricted set of capabilities required to run any Q# program. """ +class StateDumpData: + """ + A state dump returned from the Q# interpreter. + """ + + qubit_count: int + """ + The number of allocated qubits at the time of the dump. + """ + + def get_dict(self) -> dict: + """ + Get the amplitudes of the state vector as a dictionary from state integer to + complex amplitudes. + """ + ... + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def _repr_html_(self) -> str: ... + def _repr_latex_(self) -> Optional[str]: ... + +class Output: + """ + An output returned from the Q# interpreter. + Outputs can be a state dumps or messages. These are normally printed to the console. + """ + + def __repr__(self) -> str: ... + def __str__(self) -> str: ... + def _repr_html_(self) -> str: ... + def _repr_latex_(self) -> Optional[str]: ... + def state_dump(self) -> Optional[StateDumpData]: ... + class Interpreter: """A Q# interpreter.""" @@ -43,8 +81,8 @@ class Interpreter: target_profile: TargetProfile, language_features: Optional[List[str]], project_root: Optional[str], - read_file: Callable[[str], str], - list_directory: Callable[[str], str], + read_file: Callable[[str], Tuple[str, str]], + list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], ) -> None: """ @@ -177,38 +215,6 @@ class Pauli(Enum): Y: int Z: int -class Output: - """ - An output returned from the Q# interpreter. - Outputs can be a state dumps or messages. These are normally printed to the console. - """ - - def __repr__(self) -> str: ... - def __str__(self) -> str: ... - def _repr_html_(self) -> str: ... - def _repr_latex_(self) -> Optional[str]: ... - def state_dump(self) -> Optional[StateDumpData]: ... - -class StateDumpData: - """ - A state dump returned from the Q# interpreter. - """ - - """ - The number of allocated qubits at the time of the dump. - """ - qubit_count: int - - """ - Get the amplitudes of the state vector as a dictionary from state integer to - complex amplitudes. - """ - def get_dict(self) -> dict: ... - def __repr__(self) -> str: ... - def __str__(self) -> str: ... - def _repr_html_(self) -> str: ... - def _repr_latex_(self) -> Optional[str]: ... - class Circuit: def json(self) -> str: ... def __repr__(self) -> str: ... @@ -221,6 +227,13 @@ class QSharpError(BaseException): ... +class QasmError(BaseException): + """ + An error returned from the OpenQASM parser. + """ + + ... + def physical_estimates(logical_resources: str, params: str) -> str: """ Estimates physical resources from pre-calculated logical resources. @@ -231,3 +244,135 @@ def physical_estimates(logical_resources: str, params: str) -> str: :returns resources: The estimated resources. """ ... + +def resource_estimate_qasm3( + source: str, + job_params: str, + read_file: Callable[[str], Tuple[str, str]], + list_directory: Callable[[str], List[Dict[str, str]]], + resolve_path: Callable[[str, str], str], + fetch_github: Callable[[str, str, str, str], str], + **kwargs +) -> str: + """ + Estimates the resource requirements for executing QASM3 source code. + + Note: + This call while exported is not intended to be used directly by the user. + It is intended to be used by the Python wrapper which will handle the + callbacks and other Python specific details. + + Args: + source (str): The QASM3 source code to estimate the resource requirements for. + job_params (str): The parameters for the job. + read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path. + list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory. + resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path. + fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub. + **kwargs: Additional keyword arguments to pass to the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'. + - search_path (str): The optional search path for resolving imports. + Returns: + str: The estimated resource requirements for executing the QASM3 source code. + """ + ... + +def run_qasm3( + source: str, + output_fn: Callable[[Output], None], + read_file: Callable[[str], Tuple[str, str]], + list_directory: Callable[[str], List[Dict[str, str]]], + resolve_path: Callable[[str, str], str], + fetch_github: Callable[[str, str, str, str], str], + **kwargs +) -> Any: + """ + Executes QASM3 source code using the specified target profile. + + Note: + This call while exported is not intended to be used directly by the user. + It is intended to be used by the Python wrapper which will handle the + callbacks and other Python specific details. + + Args: + source (str): The QASM3 source code to execute. + output_fn (Callable[[Output], None]): The function to handle the output of the execution. + read_file (Callable[[str], Tuple[str, str]]): The function to read a file and return its contents. + list_directory (Callable[[str], List[Dict[str, str]]]): The function to list the contents of a directory. + resolve_path (Callable[[str, str], str]): The function to resolve a path given a base path and a relative path. + fetch_github (Callable[[str, str, str, str], str]): The function to fetch a file from GitHub. + **kwargs: Additional keyword arguments to pass to the execution. + - target_profile (TargetProfile): The target profile to use for execution. + - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'. + - search_path (str): The optional search path for resolving imports. + - shots (int): The number of shots to run the program for. Defaults to 1. + - seed (int): The seed to use for the random number generator. + + Returns: + Any: The result of the execution. + """ + ... + +def compile_qasm3_to_qir( + source: str, + read_file: Callable[[str], Tuple[str, str]], + list_directory: Callable[[str], List[Dict[str, str]]], + resolve_path: Callable[[str, str], str], + fetch_github: Callable[[str, str, str, str], str], + **kwargs +) -> str: + """ + Converts a Qiskit QuantumCircuit to QIR (Quantum Intermediate Representation). + + Note: + This call while exported is not intended to be used directly by the user. + It is intended to be used by the Python wrapper which will handle the + callbacks and other Python specific details. + + Args: + source (str): The QASM3 source code to estimate the resource requirements for. + read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path. + list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory. + resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path. + fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub. + **kwargs: Additional keyword arguments to pass to the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. + - entry_expr (str, optional): The entry expression for the QIR conversion. Defaults to None. + - target_profile (TargetProfile): The target profile to use for code generation. + - search_path (Optional[str]): The optional search path for resolving file references. + + Returns: + str: The converted QIR code as a string. + """ + ... + +def compile_qasm3_to_qsharp( + source: str, + read_file: Callable[[str], Tuple[str, str]], + list_directory: Callable[[str], List[Dict[str, str]]], + resolve_path: Callable[[str, str], str], + fetch_github: Callable[[str, str, str, str], str], + **kwargs +) -> str: + """ + Converts a Qiskit QuantumCircuit to Q#. + + Note: + This call while exported is not intended to be used directly by the user. + It is intended to be used by the Python wrapper which will handle the + callbacks and other Python specific details. + + Args: + source (str): The QASM3 source code to estimate the resource requirements for. + read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path. + list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory. + resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path. + fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub. + **kwargs: Additional keyword arguments to pass to the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. + - search_path (Optional[str]): The optional search path for resolving file references. + + Returns: + str: The converted Q# code as a string. + """ + ... diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 1df3d81dfe..8b4a970bef 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -9,8 +9,16 @@ Output, Circuit, ) -from warnings import warn -from typing import Any, Callable, Dict, Optional, Tuple, TypedDict, Union, List +from typing import ( + Any, + Callable, + Dict, + Optional, + TypedDict, + Union, + List, + overload, +) from .estimator._estimator import EstimatorResult, EstimatorParams import json import os @@ -304,8 +312,10 @@ def circuit( return get_interpreter().circuit(entry_expr, operation) +@overload def estimate( - entry_expr, params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None + entry_expr: str, + params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = ..., ) -> EstimatorResult: """ Estimates resources for Q# source code. @@ -313,10 +323,37 @@ def estimate( :param entry_expr: The entry expression. :param params: The parameters to configure physical estimation. - :returns resources: The estimated resources. + :returns `EstimatorResult`: The estimated resources. """ - ipython_helper() + ... + +@overload +def estimate( + circuit: "QuantumCircuit", + params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None, + **options, +) -> EstimatorResult: + """ + Estimates resources for Qiskit QuantumCircuit. + + :param circuit: The input Qiskit QuantumCircuit object. + :param params: The parameters to configure physical estimation. + :**options: Additional options for the execution. + - Any options for the transpiler, exporter, or Qiskit passes + configuration. Defaults to backend config values. Common + values include: 'optimization_level', 'basis_gates', + 'includes', 'search_path'. + :raises QasmError: If there is an error generating or parsing QASM. + + :returns `EstimatorResult`: The estimated resources. + """ + ... + + +def _coerce_estimator_params( + params: Optional[Union[Dict[str, Any], List, EstimatorParams]] = None +) -> List[Dict[str, Any]]: if params is None: params = [{}] elif isinstance(params, EstimatorParams): @@ -326,9 +363,60 @@ def estimate( params = [params.as_dict()] elif isinstance(params, dict): params = [params] - return EstimatorResult( - json.loads(get_interpreter().estimate(entry_expr, json.dumps(params))) - ) + return params + + +def estimate( + estimate_input: Union[str, "QuantumCircuit"], + params: Optional[ + Union[Dict[str, Any], List[Dict[str, Any]], EstimatorParams] + ] = None, + **options, +) -> EstimatorResult: + """ + Estimates resources for the quantum algorithm. + + :param estimate_input: The Q# entry expression or `QuantumCircuit`. + :param params: The parameters to configure physical estimation. + :**options: Additional options for the `QiskitCircuit` input. + - Any options for the transpiler, exporter, or Qiskit passes + configuration. Defaults to backend config values. Common + values include: 'optimization_level', 'basis_gates', + 'includes', 'search_path'. + :raises `QasmError`: If there is an error generating or parsing QASM. + :returns `EstimatorResult`: The estimated resources. + """ + + ipython_helper() + params = _coerce_estimator_params(params) + param_str = json.dumps(params) + if isinstance(estimate_input, str): + res_str = get_interpreter().estimate(estimate_input, param_str) + res = json.loads(res_str) + return EstimatorResult(res) + else: + try: + # We delay import Qisit to avoid the user needing to install it + # if they are not using the `estimate` function with a `QuantumCircuit`. + from qiskit import QuantumCircuit + + if isinstance(estimate_input, QuantumCircuit): + from qsharp.interop.qiskit.backends import ResourceEstimatorBackend + + backend = ResourceEstimatorBackend( + target_profile=TargetProfile.Unrestricted + ) + job = backend.run(estimate_input, params=params, **options) + return job.result() + else: + raise ValueError("Unsupported input type") + except ImportError as ex: + # We can't really diffentiate between invalid input and missing Qiskit + # as we can't check the input type without importing Qiskit. + message = ( + "Could not load Qiskit. Please install Qiskit to use this function." + ) + raise ValueError(message) from ex def set_quantum_seed(seed: Optional[int]) -> None: diff --git a/pip/qsharp/interop/__init__.py b/pip/qsharp/interop/__init__.py new file mode 100644 index 0000000000..c285977094 --- /dev/null +++ b/pip/qsharp/interop/__init__.py @@ -0,0 +1,33 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +def _test_qiskit_import() -> bool: + try: + import qiskit + + return True + except ImportError: + return False + + +if _test_qiskit_import(): + from .qiskit import ( + QasmError, + QSharpBackend, + QsJob, + ReJob, + ResourceEstimatorBackend, + DetaultExecutor, + ) + + __all__ = [ + "QasmError", + "QSharpBackend", + "QsJob", + "ReJob", + "ResourceEstimatorBackend", + "DetaultExecutor", + ] +else: + __all__ = [] diff --git a/pip/qsharp/interop/qiskit/__init__.py b/pip/qsharp/interop/qiskit/__init__.py new file mode 100644 index 0000000000..a66cce3ec6 --- /dev/null +++ b/pip/qsharp/interop/qiskit/__init__.py @@ -0,0 +1,7 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from ..._native import QasmError +from .backends import QSharpBackend, ResourceEstimatorBackend, QirTarget +from .jobs import QsJob, QsSimJob, ReJob, QsJobSet +from .execution import DetaultExecutor diff --git a/pip/qsharp/interop/qiskit/backends/__init__.py b/pip/qsharp/interop/qiskit/backends/__init__.py new file mode 100644 index 0000000000..9af2b51854 --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/__init__.py @@ -0,0 +1,8 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .compilation import Compilation +from .errors import Errors +from .qirtarget import QirTarget +from .qsharp_backend import QSharpBackend +from .re_backend import ResourceEstimatorBackend diff --git a/pip/qsharp/interop/qiskit/backends/backend_base.py b/pip/qsharp/interop/qiskit/backends/backend_base.py new file mode 100644 index 0000000000..1fae6b3f60 --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/backend_base.py @@ -0,0 +1,534 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from abc import ABC, abstractmethod +import datetime +import logging +import time +from typing import Dict, Any, List, Optional, Union +from warnings import warn + +from qiskit import transpile +from qiskit.circuit import ( + QuantumCircuit, +) + +from qiskit.qasm3.exporter import Exporter +from qiskit.providers import BackendV2, Options +from qiskit.result import Result +from qiskit.transpiler import PassManager +from qiskit.transpiler.passes import RemoveBarriers, RemoveResetInZeroState +from qiskit.transpiler.target import Target + +from .compilation import Compilation +from .errors import Errors +from .qirtarget import QirTarget +from ..jobs import QsJob +from ..passes import RemoveDelays +from .... import TargetProfile + +logger = logging.getLogger(__name__) + +_QISKIT_NON_GATE_INSTRUCTIONS = [ + "control_flow", + "if_else", + "switch_case", + "while_loop", + "break", + "continue", + "store", + "for_loop", + "measure", + "reset", +] + +_QISKIT_STDGATES = [ + "p", + "x", + "y", + "z", + "h", + "s", + "sdg", + "t", + "tdg", + "sx", + "rx", + "ry", + "rz", + "cx", + "cy", + "cz", + "cp", + "crx", + "cry", + "crz", + "ch", + "swap", + "ccx", + "cswap", + "cu", + "CX", + "phase", + "cphase", + "id", + "u1", + "u2", + "u3", + "U", +] + + +def filter_kwargs(func, **kwargs) -> Dict[str, Any]: + import inspect + + sig = inspect.signature(func) + supported_args = set(sig.parameters.keys()) + extracted_kwargs = { + k: kwargs.get(k) for k in list(kwargs.keys()) if k in supported_args + } + return extracted_kwargs + + +def get_transpile_options(**kwargs) -> Dict[str, Any]: + args = filter_kwargs(transpile, **kwargs) + return args + + +def get_exporter_options(**kwargs) -> Dict[str, Any]: + return filter_kwargs(Exporter.__init__, **kwargs) + + +class BackendBase(BackendV2, ABC): + """ + A virtual backend for transpiling to a Q# ecosystem compatible format. + """ + + def __init__( + self, + target: Optional[Target] = None, + qiskit_pass_options: Optional[Dict[str, Any]] = None, + transpile_options: Optional[Dict[str, Any]] = None, + qasm_export_options: Optional[Dict[str, Any]] = None, + skip_transpilation: bool = False, + **fields, + ): + """ + Parameters: + target (Target): The target to use for the backend. + qiskit_pass_options (Dict): Options for the Qiskit passes. + transpile_options (Dict): Options for the transpiler. + qasm_export_options (Dict): Options for the QASM3 exporter. + **options: Additional keyword arguments to pass to the + execution used by subclasses. + """ + super().__init__( + name="QSharpBackend", + description="A virtual BackendV2 for transpiling to a Q# compatible format.", + backend_version="0.0.1", + ) + + if fields is not None: + # we need to rename the seed_simulator to seed. This + # is a convenience for aer users. + # if the user passes in seed_simulator, we will rename it to seed + # but only if the seed field is defined in the backend options. + if "seed_simulator" in fields and "seed" in self._options.data: + warn("seed_simulator passed, but field is called seed.") + fields["seed"] = fields.pop("seed_simulator") + + # updates the options with the fields passed in, if the backend + # doesn't have the field, it will raise an error. + self.set_options(**fields) + + self._qiskit_pass_options = Options( + supports_barrier=False, + supports_delay=False, + remove_reset_in_zero_state=True, + ) + self._skip_transpilation = skip_transpilation + + # we need to set the target after the options are set + # so that the target_profile can be used to determine + # which gates/instructions are available + if target is not None: + # update the properties so that we are internally consistent + self._qiskit_pass_options.update_options( + **{ + "supports_barrier": target.instruction_supported("barrier"), + "supports_delay": target.instruction_supported("delay"), + "remove_reset_in_zero_state": True, + } + ) + + self._target = target + else: + self._target = self._create_target() + + self._transpile_options = {} + + basis_gates = None + if qasm_export_options is not None and "basis_gates" in qasm_export_options: + basis_gates = qasm_export_options.pop("basis_gates") + else: + # here we get the gates that are in the target but not in qasm's + # stdgates so that we can build the basis gates list for the exporter. + # A user can override this list by passing in a basis_gates list + # We also remove any non-gate instructions from the list. + target_gates = set(self.target.operation_names) + target_gates -= set(_QISKIT_NON_GATE_INSTRUCTIONS) + target_gates -= set(_QISKIT_STDGATES) + basis_gates = list(target_gates) + + # selt the default options for the exporter + self._qasm_export_options = { + "includes": ("stdgates.inc",), + "alias_classical_registers": False, + "allow_aliasing": False, + "disable_constants": True, + "basis_gates": basis_gates, + } + + if qiskit_pass_options is not None: + self._qiskit_pass_options.update_options(**qiskit_pass_options) + if transpile_options is not None: + self._transpile_options.update(**transpile_options) + if qasm_export_options is not None: + self._qasm_export_options.update(**qasm_export_options) + + def _create_target(self) -> Target: + supports_barrier = self._qiskit_pass_options["supports_barrier"] + supports_delay = self._qiskit_pass_options["supports_delay"] + return QirTarget( + target_profile=self._options["target_profile"], + supports_barrier=supports_barrier, + supports_delay=supports_delay, + ) + + @property + def target(self) -> Target: + """Returns the target of the Backend object.""" + return self._target + + @property + def max_circuits(self): + """ + Returns the maximum number of circuits that can be executed simultaneously. + """ + return None + + @abstractmethod + def _execute(self, programs: List[Compilation], **input_params) -> Dict[str, Any]: + """Execute circuits on the backend. + + Parameters: + programs (List of QuantumCompilation): simulator input. + input_params (Dict): configuration for simulation/compilation. + + Returns: + dict: return a dictionary of results. + """ + + @abstractmethod + def run( + self, + run_input: Union[QuantumCircuit, List[QuantumCircuit]], + **options, + ) -> QsJob: + pass + + def _run( + self, + run_input: List[QuantumCircuit], + **options, + ) -> QsJob: + if "name" not in options and len(run_input) == 1: + options["name"] = run_input[0].name + + # Get out default options + # Look at all of the kwargs and see if they match any of the options + # If they do, set the option to the value of the kwarg as an override + # We only to remove the options that are in the backend options for + # the run so that other options can be passed to other calls. + input_params: Dict[str, Any] = vars(self.options).copy() + input_params.update(options) + + return self._submit_job(run_input, **input_params) + + def run_job( + self, run_input: List[QuantumCircuit], job_id: str, **options + ) -> Result: + start = time.time() + + compilations = self._compile(run_input, **options) + + output = self._execute(compilations, **options) + + if not isinstance(output, dict): + logger.error("%s: run failed.", self.name) + if output: + logger.error("Output: %s", output) + from .... import QSharpError + + raise QSharpError(str(Errors.RUN_TERMINATED_WITHOUT_OUTPUT)) + + output["job_id"] = job_id + output["date"] = str(datetime.datetime.now().isoformat()) + output["status"] = "COMPLETED" + output["backend_name"] = self.name + output["backend_version"] = self.backend_version + output["time_taken"] = str(time.time() - start) + output["config"] = { + "qasm_export_options": str(self._build_qasm_export_options(**options)), + "qiskit_pass_options": str(self._build_qiskit_pass_options(**options)), + "transpile_options": str(self._build_transpile_options(**options)), + } + output["header"] = {} + return self._create_results(output) + + @abstractmethod + def _submit_job(self, run_input: List[QuantumCircuit], **input_params) -> QsJob: + pass + + def _compile(self, run_input: List[QuantumCircuit], **options) -> List[Compilation]: + # for each run input, convert to qasm3 + compilations = [] + for circuit in run_input: + args = options.copy() + assert isinstance( + circuit, QuantumCircuit + ), "Input must be a QuantumCircuit." + start = time.time() + qasm = self.qasm3(circuit, **args) + end = time.time() + time_taken = str(end - start) + compilation = Compilation(circuit, qasm, time_taken) + compilations.append(compilation) + return compilations + + @abstractmethod + def _create_results(self, output: Dict[str, Any]) -> Any: + pass + + def _transpile(self, circuit: QuantumCircuit, **options) -> QuantumCircuit: + if self._skip_transpilation: + return circuit + + circuit = self.run_qiskit_passes(circuit, options) + + orig = self.target.num_qubits + try: + self.target.num_qubits = circuit.num_qubits + transpile_options = self._build_transpile_options(**options) + backend = transpile_options.pop("backend", self) + target = transpile_options.pop("target", self.target) + # in 1.3 add qubits_initially_zero=True to the transpile call + transpiled_circuit = transpile( + circuit, backend=backend, target=target, **transpile_options + ) + return transpiled_circuit + finally: + self.target.num_qubits = orig + + def run_qiskit_passes(self, circuit, options): + pass_options = self._build_qiskit_pass_options(**options) + + pass_manager = PassManager() + if not pass_options["supports_barrier"]: + pass_manager.append(RemoveBarriers()) + if not pass_options["supports_delay"]: + pass_manager.append(RemoveDelays()) + if pass_options["remove_reset_in_zero_state"]: + # when doing state initialization, qiskit will reset all qubits to 0 + # As our semantics are different, we can remove these resets + # as it will double the number of qubits if we have to reset them + # before using them when using the base profile. + pass_manager.append(RemoveResetInZeroState()) + + circuit = pass_manager.run(circuit) + return circuit + + def _build_qiskit_pass_options(self, **kwargs) -> Dict[str, Any]: + params: Dict[str, Any] = vars(self._qiskit_pass_options).copy() + for opt in params.copy(): + if opt in kwargs: + params[opt] = kwargs.pop(opt) + if "supports_barrier" not in params: + params["supports_barrier"] = False + if "supports_delay" not in params: + params["supports_delay"] = False + if "remove_reset_in_zero_state" not in params: + params["remove_reset_in_zero_state"] = True + + return params + + def _build_transpile_options(self, **kwargs) -> Dict[str, Any]: + # create the default options from the backend + args = self._transpile_options.copy() + # gather any remaining options that are not in the default list + transpile_args = get_transpile_options(**kwargs) + args.update(transpile_args) + return args + + def _build_qasm_export_options(self, **kwargs) -> Dict[str, Any]: + # Disable aliasing until we decide want to support it + # The exporter defaults to only having the U gate. + # When it sees the stdgates.inc in the default includes list, it adds + # bodyless symbols for that fixed gate set. + # We set the basis gates for any gates that we want that wouldn't + # be defined when stdgates.inc is included. + + # any gates that are not in the stdgates.inc file need to be defined + # in the basis gates list passed to the exporter. The exporter doesn't + # know about the gates defined in the backend's target. + # Anything in the basis_gates gets added to the qasm builder's global + # namespace as an opaque gate. All parameter information comes from the + # gate object itself in the circuit. + + # create the default options from the backend + args = self._qasm_export_options.copy() + # gather any remaining options that are not in the default list + exporter_args = get_exporter_options(**kwargs) + args.update(exporter_args) + return args + + def transpile(self, circuit: QuantumCircuit, **options) -> QuantumCircuit: + transpiled_circuit = self._transpile(circuit, **options) + return transpiled_circuit + + def qasm3(self, circuit: QuantumCircuit, **options) -> str: + """Converts a Qiskit QuantumCircuit to QASM 3 for the current backend. + + Args: + circuit (QuantumCircuit): The QuantumCircuit to be executed. + **options: Additional options for the execution. + - Any options for the transpiler, exporter, or Qiskit passes + configuration. Defaults to backend config values. Common + values include: 'optimization_level', 'basis_gates', + 'includes', 'search_path'. + + Returns: + str: The converted QASM3 code as a string. Any supplied includes + are emitted as include statements at the top of the program. + + :raises QasmError: If there is an error generating or parsing QASM. + """ + + try: + export_options = self._build_qasm_export_options(**options) + exporter = Exporter(**export_options) + transpiled_circuit = self.transpile(circuit, **options) + qasm3_source = exporter.dumps(transpiled_circuit) + return qasm3_source + except Exception as ex: + from .. import QasmError + + raise QasmError(str(Errors.FAILED_TO_EXPORT_QASM)) from ex + + def qsharp(self, circuit: QuantumCircuit, **kwargs) -> str: + """ + Converts a Qiskit QuantumCircuit to Q# for the current backend. + + The generated Q# code will not be idiomatic Q# code, but will be + a direct translation of the Qiskit circuit. + + Args: + circuit (QuantumCircuit): The QuantumCircuit to be executed. + **options: Additional options for the execution. + - Any options for the transpiler, exporter, or Qiskit passes + configuration. Defaults to backend config values. Common + values include: 'optimization_level', 'basis_gates', + 'includes', 'search_path'. + + Returns: + str: The converted QASM3 code as a string. Any supplied includes + are emitted as include statements at the top of the program. + + :raises QSharpError: If there is an error evaluating the source code. + :raises QasmError: If there is an error generating, parsing, or compiling QASM. + """ + + qasm3_source = self.qasm3(circuit, **kwargs) + + args = { + "name": kwargs.get("name", circuit.name), + "search_path": kwargs.get("search_path", "."), + } + qsharp_source = self._qsharp(qasm3_source, **args) + return qsharp_source + + def qir( + self, + circuit: QuantumCircuit, + **kwargs, + ) -> str: + """ + Converts a Qiskit QuantumCircuit to QIR (Quantum Intermediate Representation). + + Args: + circuit ('QuantumCircuit'): The input Qiskit QuantumCircuit object. + **kwargs: Additional options for the execution. + - params (str, optional): The entry expression for the QIR conversion. Defaults to None. + - target_profile (TargetProfile, optional): The target profile for the backend. Defaults to backend config value. + - search_path (str, optional): The search path for the backend. Defaults to '.'. + + Returns: + str: The converted QIR code as a string. + + :raises QSharpError: If there is an error evaluating the source code. + :raises QasmError: If there is an error generating, parsing, or compiling QASM. + :raises ValueError: If the backend configuration does not support QIR generation. + """ + name = kwargs.pop("name", circuit.name) + target_profile = kwargs.pop("target_profile", self.options.target_profile) + if target_profile == TargetProfile.Unrestricted: + raise ValueError(str(Errors.UNRESTRICTED_INVALID_QIR_TARGET)) + + qasm3_source = self.qasm3(circuit, **kwargs) + + qir_args = { + "name": name, + "target_profile": target_profile, + "search_path": kwargs.pop("search_path", "."), + } + params = kwargs.pop("params", None) + if params is not None: + qir_args["params"] = params + + return self._qir(qasm3_source, **qir_args) + + def _qir( + self, + source: str, + **kwargs, + ) -> str: + from ...._native import compile_qasm3_to_qir + from ...._fs import read_file, list_directory, resolve + from ...._http import fetch_github + + return compile_qasm3_to_qir( + source, + read_file, + list_directory, + resolve, + fetch_github, + **kwargs, + ) + + def _qsharp( + self, + source: str, + **kwargs, + ) -> str: + from ...._native import compile_qasm3_to_qsharp + from ...._fs import read_file, list_directory, resolve + from ...._http import fetch_github + + return compile_qasm3_to_qsharp( + source, + read_file, + list_directory, + resolve, + fetch_github, + **kwargs, + ) diff --git a/pip/qsharp/interop/qiskit/backends/compilation.py b/pip/qsharp/interop/qiskit/backends/compilation.py new file mode 100644 index 0000000000..de0eba284f --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/compilation.py @@ -0,0 +1,36 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from qiskit import QuantumCircuit + + +class Compilation(dict): + def __init__(self, circuit: QuantumCircuit, qasm: str, time_taken: str): + super().__init__() + self["circuit"] = circuit + self["qasm"] = qasm + self["compilation_time_taken"] = time_taken + + @property + def circuit(self) -> QuantumCircuit: + return self["circuit"] + + @circuit.setter + def circuit(self, value: QuantumCircuit): + self["circuit"] = value + + @property + def qasm(self) -> str: + return self["qasm"] + + @qasm.setter + def qasm(self, value: str): + self["qasm"] = value + + @property + def time_taken(self) -> str: + return self["compilation_time_taken"] + + @time_taken.setter + def time_taken(self, value: str): + self["compilation_time_taken"] = value diff --git a/pip/qsharp/interop/qiskit/backends/errors.py b/pip/qsharp/interop/qiskit/backends/errors.py new file mode 100644 index 0000000000..9fa54c9ecc --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/errors.py @@ -0,0 +1,29 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from enum import Enum + + +class Errors(Enum): + UNRESTRICTED_INVALID_QIR_TARGET = 1 + RUN_TERMINATED_WITHOUT_OUTPUT = 2 + FAILED_TO_EXPORT_QASM = 3 + MISSING_NUMBER_OF_SHOTS = 4 + INPUT_MUST_BE_QC = 5 + ONLY_ONE_CIRCUIT_ALLOWED = 6 + + def __str__(self): + if self == Errors.UNRESTRICTED_INVALID_QIR_TARGET: + return "The Unrestricted profile is not valid when generating QIR." + elif self == Errors.RUN_TERMINATED_WITHOUT_OUTPUT: + return "Run terminated without valid output." + elif self == Errors.FAILED_TO_EXPORT_QASM: + return "Failed to export QASM3 source." + elif self == Errors.MISSING_NUMBER_OF_SHOTS: + return "The number of shots must be specified." + elif self == Errors.INPUT_MUST_BE_QC: + return "Input must be a QuantumCircuit." + elif self == Errors.ONLY_ONE_CIRCUIT_ALLOWED: + return "Only one QuantumCircuit can be estimated at a time." + else: + return "Unknown option." diff --git a/pip/qsharp/interop/qiskit/backends/qirtarget.py b/pip/qsharp/interop/qiskit/backends/qirtarget.py new file mode 100644 index 0000000000..7954094e95 --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/qirtarget.py @@ -0,0 +1,123 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import logging + +from qiskit.circuit import ( + Barrier, + Delay, + Measure, + Parameter, + Reset, + Store, +) +from qiskit.circuit.controlflow import ( + ControlFlowOp, + ForLoopOp, + IfElseOp, + SwitchCaseOp, + WhileLoopOp, +) +from qiskit.circuit.library.standard_gates import ( + CHGate, + CCXGate, + CXGate, + CYGate, + CZGate, + CRXGate, + CRYGate, + CRZGate, + RXGate, + RXXGate, + RYGate, + RYYGate, + RZGate, + RZZGate, + HGate, + SGate, + SdgGate, + SwapGate, + TGate, + TdgGate, + XGate, + YGate, + ZGate, + IGate, +) + +from qiskit.transpiler.target import Target +from .... import TargetProfile + +logger = logging.getLogger(__name__) + + +class QirTarget(Target): + def __init__( + self, + num_qubits=None, + target_profile=TargetProfile.Base, + supports_barrier=False, + supports_delay=False, + ): + super().__init__(num_qubits=num_qubits) + + if target_profile != TargetProfile.Base: + self.add_instruction(ControlFlowOp, name="control_flow") + self.add_instruction(IfElseOp, name="if_else") + self.add_instruction(SwitchCaseOp, name="switch_case") + self.add_instruction(WhileLoopOp, name="while_loop") + + # We don't currently support break or continue statements + # in Q#, so we don't include them yet. + # self.add_instruction(BreakLoopOp, name="break") + # self.add_instruction(ContinueLoopOp, name="continue") + + self.add_instruction(Store, name="store") + + if supports_barrier: + self.add_instruction(Barrier, name="barrier") + if supports_delay: + self.add_instruction(Delay, name="delay") + + # For loops should be fully deterministic in Qiskit/QASM + self.add_instruction(ForLoopOp, name="for_loop") + self.add_instruction(Measure, name="measure") + + # While reset is technically not supported in base profile, + # the compiler can use decompositions to implement workarounds + self.add_instruction(Reset, name="reset") + + self.add_instruction(CCXGate, name="ccx") + self.add_instruction(CXGate, name="cx") + self.add_instruction(CYGate, name="cy") + self.add_instruction(CZGate, name="cz") + + self.add_instruction(RXGate(Parameter("theta")), name="rx") + self.add_instruction(RXXGate(Parameter("theta")), name="rxx") + self.add_instruction(CRXGate(Parameter("theta")), name="crx") + + self.add_instruction(RYGate(Parameter("theta")), name="ry") + self.add_instruction(RYYGate(Parameter("theta")), name="ryy") + self.add_instruction(CRYGate(Parameter("theta")), name="cry") + + self.add_instruction(RZGate(Parameter("theta")), name="rz") + self.add_instruction(RZZGate(Parameter("theta")), name="rzz") + self.add_instruction(CRZGate(Parameter("theta")), name="crz") + + self.add_instruction(HGate, name="h") + + self.add_instruction(SGate, name="s") + self.add_instruction(SdgGate, name="sdg") + + self.add_instruction(SwapGate, name="swap") + + self.add_instruction(TGate, name="t") + self.add_instruction(TdgGate, name="tdg") + + self.add_instruction(XGate, name="x") + self.add_instruction(YGate, name="y") + self.add_instruction(ZGate, name="z") + + self.add_instruction(IGate, name="id") + + self.add_instruction(CHGate, name="ch") diff --git a/pip/qsharp/interop/qiskit/backends/qsharp_backend.py b/pip/qsharp/interop/qiskit/backends/qsharp_backend.py new file mode 100644 index 0000000000..a424bb6203 --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/qsharp_backend.py @@ -0,0 +1,268 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from collections import Counter +from concurrent.futures import Executor +import logging +from typing import Any, Dict, List, Optional, Tuple, Union +from uuid import uuid4 + +from qiskit import QuantumCircuit +from qiskit.providers import Options +from qiskit.transpiler.target import Target +from .... import Result, TargetProfile +from ..execution import DetaultExecutor +from ..jobs import QsSimJob, QsJobSet +from .backend_base import BackendBase +from .compilation import Compilation +from .errors import Errors + +logger = logging.getLogger(__name__) + + +def _map_qsharp_value_to_bit(v) -> str: + if isinstance(v, Result): + if v == Result.One: + return "1" + else: + return "0" + return str(v) + + +# Convert Q# output to the result format expected by Qiskit +def _to_qiskit_bitstring(obj): + if isinstance(obj, tuple): + return " ".join([_to_qiskit_bitstring(term) for term in obj]) + elif isinstance(obj, list): + return "".join([_map_qsharp_value_to_bit(bit) for bit in obj]) + else: + return obj + + +class QSharpBackend(BackendBase): + """ + A virtual backend for running Qiskit circuits using the Q# simulator. + """ + + # This init is included for the docstring + # pylint: disable=useless-parent-delegation + def __init__( + self, + target: Optional[Target] = None, + qiskit_pass_options: Optional[Dict[str, Any]] = None, + transpile_options: Optional[Dict[str, Any]] = None, + qasm_export_options: Optional[Dict[str, Any]] = None, + skip_transpilation: bool = False, + **fields, + ): + """ + Parameters: + target (Target): The target to use for the backend. + qiskit_pass_options (Dict): Options for the Qiskit passes. + transpile_options (Dict): Options for the transpiler. + qasm_export_options (Dict): Options for the QASM3 exporter. + **options: Additional options for the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. + The circuit name will be used if not specified. + - target_profile (TargetProfile): The target profile to use for the compilation. + - shots (int): The number of shots to run the program for. Defaults to 1024. + - seed (int): The seed to use for the random number generator. Defaults to None. + - search_path (str): The path to search for imports. Defaults to '.'. + - output_fn (Callable[[Output], None]): A callback function to + receive the output of the circuit. Defaults to None. + - executor(ThreadPoolExecutor or other Executor): + The executor to be used to submit the job. Defaults to SynchronousExecutor. + """ + + super().__init__( + target, + qiskit_pass_options, + transpile_options, + qasm_export_options, + skip_transpilation, + **fields, + ) + + @classmethod + def _default_options(cls): + return Options( + name="program", + params=None, + search_path=".", + shots=1024, + seed=None, + output_fn=None, + target_profile=TargetProfile.Unrestricted, + executor=DetaultExecutor(), + ) + + def run( + self, + run_input: Union[QuantumCircuit, List[QuantumCircuit]], + **options, + ) -> QsSimJob: + """ + Runs the given QuantumCircuit using the Q# simulator. + + Args: + run_input (QuantumCircuit): The QuantumCircuit to be executed. + **options: Additional options for the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. + The circuit name will be used if not specified. + - params (Optional[str]): The entry expression to use for the program. Defaults to None. + - target_profile (TargetProfile): The target profile to use for the compilation. + - shots (int): The number of shots to run the program for. Defaults to 1024. + - seed (int): The seed to use for the random number generator. Defaults to None. + - search_path (str): The path to search for imports. Defaults to '.'. + - output_fn (Callable[[Output], None]): A callback function to + receive the output of the circuit. + - executor(ThreadPoolExecutor or other Executor): + The executor to be used to submit the job. + Returns: + QSharpJob: The simulation job + + :raises QSharpError: If there is an error evaluating the source code. + :raises QasmError: If there is an error generating, parsing, or compiling QASM. + :raises ValueError: If the run_input is not a QuantumCircuit + or List[QuantumCircuit]. + """ + + if not isinstance(run_input, list): + run_input = [run_input] + for circuit in run_input: + if not isinstance(circuit, QuantumCircuit): + raise ValueError(str(Errors.INPUT_MUST_BE_QC)) + + return self._run(run_input, **options) + + def _execute(self, programs: List[Compilation], **input_params) -> Dict[str, Any]: + exec_results: List[Tuple[Compilation, Dict[str, Any]]] = [ + ( + program, + _run_qasm3(program.qasm, vars(self.options).copy(), **input_params), + ) + for program in programs + ] + job_results = [] + + shots = input_params.get("shots") + if shots is None: + raise ValueError(str(Errors.MISSING_NUMBER_OF_SHOTS)) + + for program, exec_result in exec_results: + results = [_to_qiskit_bitstring(result) for result in exec_result] + + counts = Counter(results) + counts_dict = dict(counts) + probabilities = { + bitstring: (count / shots) for bitstring, count in counts_dict.items() + } + + job_result = { + "data": {"counts": counts_dict, "probabilities": probabilities}, + "success": True, + "header": { + "metadata": {"qasm": program.qasm}, + "name": program.circuit.name, + "compilation_time_taken": program.time_taken, + }, + "shots": shots, + } + job_results.append(job_result) + + # All of theses fields are required by the Result object + result_dict = { + "results": job_results, + "qobj_id": str(uuid4()), + "success": True, + } + + return result_dict + + def _create_results(self, output: Dict[str, Any]) -> Any: + from qiskit.result import Result + + result = Result.from_dict(output) + return result + + def _submit_job( + self, run_input: List[QuantumCircuit], **options + ) -> Union[QsSimJob, QsJobSet]: + job_id = str(uuid4()) + executor: Executor = options.pop("executor", DetaultExecutor()) + if len(run_input) == 1: + job = QsSimJob(self, job_id, self.run_job, run_input, options, executor) + else: + job = QsJobSet(self, job_id, self.run_job, run_input, options, executor) + job.submit() + return job + + +def _run_qasm3( + qasm: str, + default_options: Options, + **options, +) -> Any: + """ + Runs the supplied OpenQASM 3 program. + Gates defined by stdgates.inc will be overridden with definitions + from the Q# compiler. + + Any gates, such as matrix unitaries, that are not able to be + transpiled will result in an error. + + Parameters: + source (str): The input OpenQASM 3 string to be processed. + **options: Additional keyword arguments to pass to the execution. + - target_profile (TargetProfile): The target profile to use for execution. + - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'. + - search_path (str): The optional search path for resolving qasm3 imports. + - shots (int): The number of shots to run the program for. Defaults to 1. + - seed (int): The seed to use for the random number generator. + - output_fn (Optional[Callable[[Output], None]]): A callback function that will be called with each output. Defaults to None. + + :returns values: A result or runtime errors. + + :raises QSharpError: If there is an error evaluating the source code. + :raises QasmError: If there is an error generating, parsing, or compiling QASM. + """ + + from ...._native import Output + from ...._native import run_qasm3 + from ...._fs import read_file, list_directory, resolve + from ...._http import fetch_github + + def callback(output: Output) -> None: + print(output) + + output_fn = options.pop("output_fn", callback) + + name = options.pop("name", default_options["name"]) + target_profile = options.pop("target_profile", default_options["target_profile"]) + search_path = options.pop("search_path", default_options["search_path"]) + shots = options.pop("shots", default_options["shots"]) + seed = options.pop("seed", default_options["seed"]) + + # when passing the args into the rust layer, any kwargs with None values + # will cause an error, so we need to filter them out. + args = {} + if name is not None: + args["name"] = name + if target_profile is not None: + args["target_profile"] = target_profile + if search_path is not None: + args["search_path"] = search_path + if shots is not None: + args["shots"] = shots + if seed is not None: + args["seed"] = seed + + return run_qasm3( + qasm, + output_fn, + read_file, + list_directory, + resolve, + fetch_github, + **args, + ) diff --git a/pip/qsharp/interop/qiskit/backends/re_backend.py b/pip/qsharp/interop/qiskit/backends/re_backend.py new file mode 100644 index 0000000000..8aa397a208 --- /dev/null +++ b/pip/qsharp/interop/qiskit/backends/re_backend.py @@ -0,0 +1,190 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from concurrent.futures import Executor +import json +import logging +from typing import Any, Dict, List, Optional, Union +from uuid import uuid4 + +from qiskit import QuantumCircuit +from qiskit.providers import Options +from qiskit.transpiler.target import Target + +from .compilation import Compilation +from .errors import Errors +from .backend_base import BackendBase +from ..jobs import ReJob +from ..execution import DetaultExecutor +from ...._fs import read_file, list_directory, resolve +from ...._http import fetch_github +from ...._native import resource_estimate_qasm3 +from .... import TargetProfile +from ....estimator import ( + EstimatorResult, + EstimatorParams, +) + +logger = logging.getLogger(__name__) + + +class ResourceEstimatorBackend(BackendBase): + """ + A virtual backend for resource estimating Qiskit circuits levaraging + Q# resource estimation capabilities. + """ + + # This init is included for the docstring + # pylint: disable=useless-parent-delegation + def __init__( + self, + target: Optional[Target] = None, + qiskit_pass_options: Optional[Dict[str, Any]] = None, + transpile_options: Optional[Dict[str, Any]] = None, + qasm_export_options: Optional[Dict[str, Any]] = None, + skip_transpilation: bool = False, + **fields, + ): + """ + Parameters: + target (Target): The target to use for the backend. + qiskit_pass_options (Dict): Options for the Qiskit passes. + transpile_options (Dict): Options for the transpiler. + qasm_export_options (Dict): Options for the QASM3 exporter. + **options: Additional options for the execution. + - params (EstimatorParams): Configuration values for resource estimation. + - name (str): The name of the circuit. This is used as the entry point for the program. + The circuit name will be used if not specified. + - search_path (str): Path to search in for qasm3 imports. Defaults to '.'. + - target_profile (TargetProfile): The target profile to use for the backend. + - executor(ThreadPoolExecutor or other Executor): + The executor to be used to submit the job. Defaults to SynchronousExecutor. + """ + + super().__init__( + target, + qiskit_pass_options, + transpile_options, + qasm_export_options, + skip_transpilation, + **fields, + ) + + @property + def max_circuits(self): + """ + Returns the maximum number of circuits that can be executed simultaneously. + """ + return 1 + + @classmethod + def _default_options(cls): + return Options( + params=None, + name="program", + search_path=".", + target_profile=TargetProfile.Unrestricted, + executor=DetaultExecutor(), + ) + + def run( + self, + run_input: Union[QuantumCircuit, List[QuantumCircuit]], + params: Optional[EstimatorParams] = None, + **options, + ) -> ReJob: + """ + Performs resource estimation on the supplied QuantumCircuit via conversion + to OpenQASM 3. + + Parameters: + run_input ('QuantumCircuit'): The input Qiskit QuantumCircuit object. + params (Optional EstimatorParams): Configuration values for resource estimation. + **options: Additional options for the execution. + - name (str): The name of the circuit. This is used as the entry point for the program. + The circuit name will be used if not specified. + - search_path (str): Path to search in for qasm3 imports. Defaults to '.'. + - target_profile (TargetProfile): The target profile to use for the backend. + - executor(ThreadPoolExecutor or other Executor): + The executor to be used to submit the job. + Returns: + ReJob: The resource estimation job + + :raises QSharpError: If there is an error evaluating the source code. + :raises QasmError: If there is an error generating, parsing, or compiling QASM. + :raises ValueError: If the run_input is not a QuantumCircuit. + """ + if isinstance(run_input, QuantumCircuit): + run_input = [run_input] + if len(run_input) != 1: + raise ValueError(str(Errors.ONLY_ONE_CIRCUIT_ALLOWED)) + + if params is not None: + options["params"] = params + return self._run(run_input, **options) + + def _estimate_qasm3( + self, + source: str, + **input_params, + ) -> Dict[str, Any]: + """ + Estimates the resource usage of a QASM3 source code. + """ + params = input_params.pop("params", None) + if params is None: + params = [{}] + elif isinstance(params, EstimatorParams): + if params.has_items: + params = params.as_dict()["items"] + else: + params = [params.as_dict()] + elif isinstance(params, dict): + params = [params] + param_str = json.dumps(params) + kwargs = { + "name": input_params.pop("name"), + "search_path": input_params.pop("search_path", "."), + } + kwargs.update(input_params) + res_str = resource_estimate_qasm3( + source, + param_str, + read_file, + list_directory, + resolve, + fetch_github, + **kwargs, + ) + res = json.loads(res_str) + return res + + def _execute(self, programs: List[Compilation], **input_params) -> Dict: + exec_results = [ + (program, self._estimate_qasm3(program.qasm, **input_params)) + for program in programs + ] + success = ( + all( + "status" in res and res["status"] == "success" + for (_, res) in exec_results + ) + and len(exec_results) > 0 + ) + result_dict = { + "results": [res for (_, res) in exec_results], + "qobj_id": str(uuid4()), + "success": success, + } + + return result_dict + + def _create_results(self, output: Dict[str, Any]) -> EstimatorResult: + return EstimatorResult(output["results"][0]) + + def _submit_job(self, run_input: List[QuantumCircuit], **options) -> ReJob: + job_id = str(uuid4()) + executor: Executor = options.pop("executor", DetaultExecutor()) + job = ReJob(self, job_id, self.run_job, run_input, options, executor) + job.submit() + return job diff --git a/pip/qsharp/interop/qiskit/execution/__init__.py b/pip/qsharp/interop/qiskit/execution/__init__.py new file mode 100644 index 0000000000..3bef3d637f --- /dev/null +++ b/pip/qsharp/interop/qiskit/execution/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .default import DetaultExecutor diff --git a/pip/qsharp/interop/qiskit/execution/default.py b/pip/qsharp/interop/qiskit/execution/default.py new file mode 100644 index 0000000000..4eece33ecf --- /dev/null +++ b/pip/qsharp/interop/qiskit/execution/default.py @@ -0,0 +1,10 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from concurrent.futures import ThreadPoolExecutor + + +class DetaultExecutor(ThreadPoolExecutor): + def __init__(self) -> None: + super().__init__(max_workers=1) diff --git a/pip/qsharp/interop/qiskit/jobs/__init__.py b/pip/qsharp/interop/qiskit/jobs/__init__.py new file mode 100644 index 0000000000..a70db02097 --- /dev/null +++ b/pip/qsharp/interop/qiskit/jobs/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .qsjob import QsJob, QsSimJob, ReJob +from .qsjobset import QsJobSet diff --git a/pip/qsharp/interop/qiskit/jobs/qsjob.py b/pip/qsharp/interop/qiskit/jobs/qsjob.py new file mode 100644 index 0000000000..4dc7077e50 --- /dev/null +++ b/pip/qsharp/interop/qiskit/jobs/qsjob.py @@ -0,0 +1,117 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from abc import ABC, abstractmethod +from concurrent.futures import Executor, Future +import logging +from typing import Callable, Dict, Optional, Any + +from qiskit.providers import BackendV2 +from qiskit.circuit import ( + QuantumCircuit, +) + +from qiskit.result import Result +from qiskit.providers import JobV1, JobStatus, JobError + +from ..execution import DetaultExecutor + +from ....estimator import EstimatorResult + +logger = logging.getLogger(__name__) + +RunInputCallable = Callable[[QuantumCircuit, str, *Dict[str, Any]], Result] + + +class QsJob(JobV1, ABC): + + def __init__( + self, + backend: Optional[BackendV2], + job_id: str, + job_callable: RunInputCallable, + run_input: QuantumCircuit, + input_params: Dict[str, Any], + executor=None, + **kwargs, + ) -> None: + super().__init__(backend, job_id, **kwargs) + + self._run_input = run_input + self._input_params = input_params + self._future = None + self._executor: Executor = executor or DetaultExecutor() + self._job_callable = job_callable + self._status = JobStatus.INITIALIZING + + def submit(self): + """Submit the job to the backend for execution. + + Raises: + JobError: if trying to re-submit the job. + """ + if self._future is not None: + raise JobError("Job has already been submitted.") + + self._future = self._executor.submit( + self._job_callable, self._run_input, self.job_id(), **self._input_params + ) + + @abstractmethod + def result(self, timeout: Optional[float] = None) -> Any: + pass + + def _result(self, timeout: Optional[float] = None) -> Any: + """Return the results of the job.""" + if self._future is None: + raise JobError("Job has not been submitted.") + + return self._future.result(timeout=timeout) + + def status(self) -> JobStatus: + """Return the status of the job, among the values of ``JobStatus``.""" + if self._future is None: + return JobStatus.INITIALIZING + if self._future.cancelled(): + return JobStatus.CANCELLED + if self._future.done(): + if self._future.exception() is None: + return JobStatus.DONE + else: + return JobStatus.ERROR + if self._future.running(): + return JobStatus.RUNNING + + return JobStatus.INITIALIZING + + def backend(self) -> BackendV2: + """Return the backend where this job was executed.""" + + return super().backend() + + def cancel(self): + """Attempt to cancel the job.""" + if self._future is not None: + self._future.cancel() + + def error(self) -> Optional[JobError]: + """Return the error that occurred during the execution of the job.""" + if self._future is not None: + return self._future.exception() + return None + + def add_done_callback(self, fn: Callable[[Future[Result]], object]) -> None: + """Attaches a callable that will be called when the job finishes.""" + self._future.add_done_callback(fn) + + +class QsSimJob(QsJob): + + def result(self, timeout: Optional[float] = None) -> Result: + return self._result(timeout=timeout) + + +class ReJob(QsJob): + + def result(self, timeout: Optional[float] = None) -> EstimatorResult: + return self._result(timeout=timeout) diff --git a/pip/qsharp/interop/qiskit/jobs/qsjobset.py b/pip/qsharp/interop/qiskit/jobs/qsjobset.py new file mode 100644 index 0000000000..b02d017af6 --- /dev/null +++ b/pip/qsharp/interop/qiskit/jobs/qsjobset.py @@ -0,0 +1,123 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from concurrent.futures import Executor, Future +import datetime +import logging +from typing import Dict, List, Optional, Any +from uuid import uuid4 + + +from qiskit.circuit import QuantumCircuit +from qiskit.providers import JobV1 as Job +from qiskit.providers import BackendV2, JobStatus, JobError +from qiskit.result.result import Result, ExperimentResult + + +from .qsjob import QsSimJob, RunInputCallable +from ..execution import DetaultExecutor + +logger = logging.getLogger(__name__) + + +class QsJobSet(Job): + + def __init__( + self, + backend: Optional[BackendV2], + job_id: str, + job_callable: RunInputCallable, + run_input: List[QuantumCircuit], + input_params: Dict[str, Any], + executor=None, + **kwargs, + ) -> None: + super().__init__(backend, job_id, **kwargs) + + self._run_input: List[QuantumCircuit] = run_input + self._input_params: Dict[str, Any] = input_params + self._jobs: List[QsSimJob] = [] + self._job_indexes: List[int] = [] + self._executor: Executor = executor or DetaultExecutor() + self._job_callable = job_callable + self._start_time: datetime.datetime = None + self._end_time: datetime.datetime = None + + def submit(self): + """Submit the job to the backend for execution. + + Raises: + JobError: if trying to re-submit the job. + """ + if len(self._jobs) > 0: + raise JobError("Jobs have already been submitted.") + self._start_time = datetime.datetime.now() + job_index = 0 + for circuit in self._run_input: + job_id = str(uuid4()) + job = QsSimJob( + self._backend, + job_id, + self._job_callable, + [circuit], + self._input_params, + self._executor, + ) + self._job_indexes.append(job_index) + job.submit() + job.add_done_callback(self._job_done) + + self._jobs.append(job) + + def _job_done(self, _future: Future): + self._end_time = datetime.datetime.now() + + def cancel(self): + """Attempt to cancel the job.""" + for future in self._jobs: + future.cancel() + + def status(self) -> JobStatus: + """Return the status of the job, among the values of ``JobStatus``.""" + if all(job.in_final_state() for job in self._jobs): + if any(job.status() == JobStatus.ERROR for job in self._jobs): + return JobStatus.ERROR + elif any(job.status() == JobStatus.CANCELLED for job in self._jobs): + return JobStatus.CANCELLED + assert all(job.status() == JobStatus.DONE for job in self._jobs) + return JobStatus.DONE + else: + if any(job.status() == JobStatus.RUNNING for job in self._jobs): + return JobStatus.RUNNING + if any(job.status() == JobStatus.QUEUED for job in self._jobs): + return JobStatus.QUEUED + return JobStatus.INITIALIZING + + def result(self, timeout: Optional[float] = None) -> Result: + results: List[Result] = [] + for job in self._jobs: + results.append(job.result(timeout=timeout)) + + if len(results) == 1: + return results[0] + + output = results[0].to_dict() + + output["job_id"] = self.job_id() + output["date"] = str(datetime.datetime.now().isoformat()) + output["backend_name"] = self.backend().name + output["backend_version"] = self.backend().backend_version + output["time_taken"] = str(self._end_time - self._start_time) + output["header"] = { + "metadata": {}, + } + output["qobj_id"] = str(uuid4()) + output["success"] = all(result.success for result in results) + agg_result: List[ExperimentResult] = [] + for result in results: + for experiment_result in result.results: + agg_result.append(experiment_result.to_dict()) + output["results"] = agg_result + output = Result.from_dict(output) + return output diff --git a/pip/qsharp/interop/qiskit/passes/__init__.py b/pip/qsharp/interop/qiskit/passes/__init__.py new file mode 100644 index 0000000000..bc095ca4ba --- /dev/null +++ b/pip/qsharp/interop/qiskit/passes/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .remove_delay import RemoveDelays diff --git a/pip/qsharp/interop/qiskit/passes/remove_delay.py b/pip/qsharp/interop/qiskit/passes/remove_delay.py new file mode 100644 index 0000000000..65e8902d38 --- /dev/null +++ b/pip/qsharp/interop/qiskit/passes/remove_delay.py @@ -0,0 +1,22 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + + +from qiskit.dagcircuit import DAGCircuit +from qiskit.transpiler.basepasses import TransformationPass +from qiskit.transpiler.passes.utils import control_flow + + +class RemoveDelays(TransformationPass): + """Return a circuit with any delay removed. + + This transformation is not semantics preserving. + """ + + @control_flow.trivial_recurse + def run(self, dag: DAGCircuit) -> DAGCircuit: + """Run the RemoveDelays pass on `dag`.""" + + dag.remove_all_ops_named("delay") + + return dag diff --git a/pip/src/interop.rs b/pip/src/interop.rs new file mode 100644 index 0000000000..c9ad6ab63a --- /dev/null +++ b/pip/src/interop.rs @@ -0,0 +1,620 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use std::path::{Path, PathBuf}; + +use std::fmt::Write; + +use pyo3::exceptions::PyException; +use pyo3::prelude::*; +use pyo3::types::{PyDict, PyList}; +use qsc::interpret::output::Receiver; +use qsc::interpret::{into_errors, Interpreter}; +use qsc::target::Profile; +use qsc::{ + ast::Package, error::WithSource, interpret, project::FileSystem, LanguageFeatures, + PackageStore, SourceMap, +}; +use qsc::{Backend, PackageType, SparseSim}; +use qsc_qasm3::io::SourceResolver; +use qsc_qasm3::{ + qasm_to_program, CompilerConfig, OperationSignature, OutputSemantics, ProgramType, + QasmCompileUnit, QubitSemantics, +}; + +use crate::fs::file_system; +use crate::interpreter::{ + format_error, format_errors, OptionalCallbackReceiver, QSharpError, QasmError, TargetProfile, + ValueWrapper, +}; + +use resource_estimator::{self as re}; + +pub(crate) struct ImportResolver +where + T: FileSystem, +{ + fs: T, + path: PathBuf, +} + +impl ImportResolver +where + T: FileSystem, +{ + pub(crate) fn new>(fs: T, path: P) -> Self { + Self { + fs, + path: PathBuf::from(path.as_ref()), + } + } +} + +impl SourceResolver for ImportResolver +where + T: FileSystem, +{ + fn resolve

(&self, path: P) -> miette::Result<(PathBuf, String)> + where + P: AsRef, + { + let path = self.path.join(path); + let (path, source) = self.fs.read_file(path.as_ref())?; + Ok(( + PathBuf::from(path.as_ref().to_owned()), + source.as_ref().to_owned(), + )) + } +} + +/// This call while exported is not intended to be used directly by the user. +/// It is intended to be used by the Python wrapper which will handle the +/// callbacks and other Python specific details. +#[pyfunction] +#[allow(clippy::too_many_arguments)] +#[pyo3( + signature = (source, callback=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, **kwargs) +)] +pub fn run_qasm3( + py: Python, + source: &str, + callback: Option, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, + kwargs: Option>, +) -> PyResult { + let mut receiver = OptionalCallbackReceiver { callback, py }; + + let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); + + let target = get_target_profile(&kwargs)?; + let name = get_name(&kwargs)?; + let seed = get_seed(&kwargs); + let shots = get_shots(&kwargs)?; + let search_path = get_search_path(&kwargs)?; + + let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); + let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); + + let (package, source_map, signature) = compile_qasm_enriching_errors( + source, + &name, + &resolver, + ProgramType::File(name.to_string()), + OutputSemantics::Qiskit, + false, + )?; + + let package_type = PackageType::Exe; + let language_features = LanguageFeatures::default(); + let mut interpreter = + create_interpreter_from_ast(package, source_map, target, language_features, package_type) + .map_err(|errors| QSharpError::new_err(format_errors(errors)))?; + + let entry_expr = signature.create_entry_expr_from_params(String::new()); + interpreter + .set_entry_expr(&entry_expr) + .map_err(|errors| map_entry_compilation_errors(errors, &signature))?; + + match run_ast(&mut interpreter, &mut receiver, shots, seed) { + Ok(result) => Ok(PyList::new_bound( + py, + result.iter().map(|v| ValueWrapper(v.clone()).into_py(py)), + ) + .into_py(py)), + Err(errors) => Err(QSharpError::new_err(format_errors(errors))), + } +} + +pub(crate) fn run_ast( + interpreter: &mut Interpreter, + receiver: &mut impl Receiver, + shots: usize, + seed: Option, +) -> Result, Vec> { + let mut results = Vec::with_capacity(shots); + for i in 0..shots { + let mut sim = SparseSim::new(); + // If seed is provided, we want to use a different seed for each shot + // so that the results are different for each shot, but still deterministic + sim.set_seed(seed.map(|s| s + i as u64)); + let result = interpreter.run_with_sim(&mut sim, receiver, None)?; + results.push(result); + } + + Ok(results) +} + +/// This call while exported is not intended to be used directly by the user. +/// It is intended to be used by the Python wrapper which will handle the +/// callbacks and other Python specific details. +#[pyfunction] +#[allow(clippy::too_many_arguments)] +#[pyo3( + signature = (source, job_params, read_file, list_directory, resolve_path, fetch_github, **kwargs) +)] +pub(crate) fn resource_estimate_qasm3( + py: Python, + source: &str, + job_params: &str, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, + kwargs: Option>, +) -> PyResult { + let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); + + let name = get_name(&kwargs)?; + let search_path = get_search_path(&kwargs)?; + + let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); + let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); + + let program_type = ProgramType::File(name.to_string()); + let output_semantics = OutputSemantics::ResourceEstimation; + let unit = compile_qasm(source, &name, &resolver, program_type, output_semantics)?; + let (source_map, _, package, _) = unit.into_tuple(); + match crate::interop::estimate_qasm3( + package.expect("Package must exist when there are no errors"), + source_map, + job_params, + ) { + Ok(estimate) => Ok(estimate), + Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => { + Err(QSharpError::new_err(format_errors( + errors + .into_iter() + .map(|e| match e { + re::Error::Interpreter(e) => e, + re::Error::Estimation(_) => unreachable!(), + }) + .collect::>(), + ))) + } + Err(errors) => Err(QSharpError::new_err( + errors + .into_iter() + .map(|e| match e { + re::Error::Estimation(e) => e.to_string(), + re::Error::Interpreter(_) => unreachable!(), + }) + .collect::>() + .join("\n"), + )), + } +} + +/// This call while exported is not intended to be used directly by the user. +/// It is intended to be used by the Python wrapper which will handle the +/// callbacks and other Python specific details. +#[pyfunction] +#[allow(clippy::too_many_arguments)] +#[pyo3( + signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs) +)] +pub(crate) fn compile_qasm3_to_qir( + py: Python, + source: &str, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, + kwargs: Option>, +) -> PyResult { + let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); + + let target = get_target_profile(&kwargs)?; + let name = get_name(&kwargs)?; + let search_path = get_search_path(&kwargs)?; + + let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); + let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); + + let program_type = ProgramType::File(name.to_string()); + let (package, source_map, signature) = compile_qasm_enriching_errors( + source, + &name, + &resolver, + program_type, + OutputSemantics::Qiskit, + false, + )?; + + let package_type = PackageType::Lib; + let language_features = LanguageFeatures::default(); + let mut interpreter = + create_interpreter_from_ast(package, source_map, target, language_features, package_type) + .map_err(|errors| QSharpError::new_err(format_errors(errors)))?; + let entry_expr = signature.create_entry_expr_from_params(String::new()); + + generate_qir_from_ast(entry_expr, &mut interpreter) +} + +pub(crate) fn compile_qasm, R: SourceResolver>( + source: S, + name: S, + resolver: &R, + program_type: ProgramType, + output_semantics: OutputSemantics, +) -> PyResult { + let parse_result = + qsc_qasm3::parse::parse_source(source, format!("{}.qasm", name.as_ref()), resolver) + .map_err(|report| { + // this will only fail if a file cannot be read + // most likely due to a missing file or search path + QasmError::new_err(format!("{report:?}")) + })?; + + // + if parse_result.has_errors() { + return Err(QasmError::new_err(format_qasm_errors( + parse_result.errors(), + ))); + } + let unit = qasm_to_program( + parse_result.source, + parse_result.source_map, + CompilerConfig { + qubit_semantics: QubitSemantics::Qiskit, + output_semantics, + program_ty: program_type, + }, + ); + + if unit.has_errors() { + return Err(QasmError::new_err(format_qasm_errors(unit.errors()))); + } + Ok(unit) +} + +pub(crate) fn compile_qasm_enriching_errors, R: SourceResolver>( + source: S, + name: S, + resolver: &R, + program_type: ProgramType, + output_semantics: OutputSemantics, + allow_input_params: bool, +) -> PyResult<(Package, SourceMap, OperationSignature)> { + let unit = compile_qasm(source, name, resolver, program_type, output_semantics)?; + + if unit.has_errors() { + return Err(QasmError::new_err(format_qasm_errors(unit.errors()))); + } + let (source_map, _, package, sig) = unit.into_tuple(); + let Some(package) = package else { + return Err(QasmError::new_err("package should have had value")); + }; + + let Some(signature) = sig else { + return Err(QasmError::new_err( + "signature should have had value. This is a bug", + )); + }; + + if !signature.input.is_empty() && !allow_input_params { + // no entry expression is provided, but the signature has input parameters. + let mut message = String::new(); + message += "Circuit has unbound input parameters\n"; + message += &format!(" help: Parameters: {}", signature.input_params()); + + return Err(QSharpError::new_err(message)); + } + + Ok((package, source_map, signature)) +} + +fn generate_qir_from_ast>( + entry_expr: S, + interpreter: &mut Interpreter, +) -> PyResult { + interpreter + .qirgen(entry_expr.as_ref()) + .map_err(map_qirgen_errors) +} + +/// This call while exported is not intended to be used directly by the user. +/// It is intended to be used by the Python wrapper which will handle the +/// callbacks and other Python specific details. +#[pyfunction] +#[allow(clippy::too_many_arguments)] +#[pyo3( + signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs) +)] +pub(crate) fn compile_qasm3_to_qsharp( + py: Python, + source: &str, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, + kwargs: Option>, +) -> PyResult { + let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); + + let name = get_name(&kwargs)?; + let search_path = get_search_path(&kwargs)?; + + let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github); + let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); + + let program_type = ProgramType::File(name.to_string()); + let (package, _, _) = compile_qasm_enriching_errors( + source, + &name, + &resolver, + program_type, + OutputSemantics::Qiskit, + true, + )?; + + let qsharp = qsc::codegen::qsharp::write_package_string(&package); + Ok(qsharp) +} + +pub(crate) fn map_entry_compilation_errors( + errors: Vec, + sig: &OperationSignature, +) -> PyErr { + let mut semantic = vec![]; + for error in errors { + match &error { + interpret::Error::Compile(_) => { + // The entry expression is invalid. This is likely due to a type mismatch + // or missing parameter(s). We should provide a more helpful error message. + let mut message = format_error(&error); + writeln!(message).unwrap(); + writeln!(message, "failed to compile entry point.").unwrap(); + writeln!( + message, + " help: check that the parameter types match the supplied parameters" + ) + .unwrap(); + + message.push_str(&format!(" help: Parameters: {}", sig.input_params())); + + semantic.push(message); + } + _ => { + semantic.push(format_error(&error)); + } + } + } + let message = semantic.into_iter().collect::(); + QSharpError::new_err(message) +} + +fn map_qirgen_errors(errors: Vec) -> PyErr { + let mut semantic = vec![]; + for error in errors { + match &error { + interpret::Error::Compile(_) => { + // We've gotten this far with no compilation errors, so if we get one here + // then the entry expression is invalid. + let mut message = format_error(&error); + writeln!(message).unwrap(); + writeln!(message, "failed to compile entry point.").unwrap(); + writeln!( + message, + " help: check that the parameter types match the entry point signature" + ) + .unwrap(); + + semantic.push(message); + } + interpret::Error::PartialEvaluation(pe) => match pe.error() { + qsc::partial_eval::Error::OutputResultLiteral(..) => { + let mut message = format_error(&error); + writeln!(message).unwrap(); + writeln!( + message, + " help: ensure all output registers have been measured into." + ) + .unwrap(); + + semantic.push(message); + } + _ => { + semantic.push(format_error(&error)); + } + }, + _ => { + semantic.push(format_error(&error)); + } + } + } + let message = semantic.into_iter().collect::(); + QSharpError::new_err(message) +} + +fn estimate_qasm3( + ast_package: Package, + source_map: SourceMap, + params: &str, +) -> Result> { + let mut interpreter = create_interpreter_from_ast( + ast_package, + source_map, + Profile::Unrestricted, + LanguageFeatures::default(), + PackageType::Exe, + ) + .map_err(into_estimation_errors)?; + + resource_estimator::estimate_entry(&mut interpreter, params) +} + +fn into_estimation_errors(errors: Vec) -> Vec { + errors + .into_iter() + .map(|error| resource_estimator::Error::Interpreter(error.clone())) + .collect::>() +} + +pub(crate) fn format_qasm_errors(errors: Vec>) -> String { + errors + .into_iter() + .map(|e| { + let mut message = String::new(); + let report = miette::Report::new(e); + write!(message, "{report:?}").unwrap(); + message + }) + .collect::() +} + +pub(crate) fn create_filesystem_from_py( + py: Python, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, +) -> impl FileSystem + '_ { + file_system( + py, + read_file.expect("file system hooks should have been passed in with a read file callback"), + list_directory + .expect("file system hooks should have been passed in with a list directory callback"), + resolve_path + .expect("file system hooks should have been passed in with a resolve path callback"), + fetch_github + .expect("file system hooks should have been passed in with a fetch github callback"), + ) +} + +fn create_interpreter_from_ast( + ast_package: Package, + source_map: SourceMap, + profile: Profile, + language_features: LanguageFeatures, + package_type: PackageType, +) -> Result> { + let mut store = PackageStore::new(qsc::compile::core()); + let mut dependencies = Vec::new(); + + let capabilities = profile.into(); + + dependencies.push((store.insert(qsc::compile::std(&store, capabilities)), None)); + let (mut unit, errors) = qsc::compile::compile_ast( + &store, + &dependencies, + ast_package, + source_map, + package_type, + capabilities, + ); + + if !errors.is_empty() { + return Err(into_errors(errors)); + } + + unit.expose(); + let source_package_id = store.insert(unit); + + interpret::Interpreter::from( + store, + source_package_id, + capabilities, + language_features, + &dependencies, + ) +} + +pub(crate) fn sanitize_name>(name: S) -> String { + let name = name.as_ref(); + if name.is_empty() { + return "circuit".to_string(); + } + + let mut output = String::with_capacity(name.len()); + let c = name.chars().next().expect("name should not be empty"); + if c == '_' || c.is_alphabetic() { + output.push(c); + } else { + // invalid first character, replace with '_' + output.push('_'); + } + output.extend(name.chars().skip(1).filter_map(|c| { + if c == '-' { + Some('_') + } else if c == '_' || c.is_alphanumeric() { + Some(c) + } else { + None + } + })); + output +} + +pub(crate) fn get_search_path(kwargs: &Bound<'_, PyDict>) -> PyResult { + kwargs.get_item("search_path")?.map_or_else( + || { + Err(PyException::new_err( + "Could not parse search path".to_string(), + )) + }, + |x| x.extract::(), + ) +} + +pub(crate) fn get_run_type(kwargs: &Bound<'_, PyDict>) -> PyResult { + kwargs.get_item("run_type")?.map_or_else( + || Err(PyException::new_err("Could not parse run type".to_string())), + |x| x.extract::(), + ) +} + +pub(crate) fn get_name(kwargs: &Bound<'_, PyDict>) -> PyResult { + let name = kwargs + .get_item("name")? + .map_or_else(|| Ok("program".to_string()), |x| x.extract::())?; + + // sanitize the name to ensure it is a valid identifier + // When creating operation, we'll throw an error if the name is not a valid identifier + // so that the user gets the exact name they expect, but here it's better to sanitize. + Ok(sanitize_name(name)) +} + +pub(crate) fn get_target_profile(kwargs: &Bound<'_, PyDict>) -> PyResult { + let target = kwargs.get_item("target_profile")?.map_or_else( + || Ok(TargetProfile::Unrestricted), + |x| x.extract::(), + )?; + Ok(target.into()) +} + +pub(crate) fn get_shots(kwargs: &Bound<'_, PyDict>) -> PyResult { + kwargs.get_item("shots")?.map_or_else( + || Err(PyException::new_err("Could not parse shots".to_string())), + |x| x.extract::(), + ) +} + +pub(crate) fn get_seed(kwargs: &Bound<'_, PyDict>) -> Option { + kwargs + .get_item("seed") + .ok()? + .map_or_else(|| None::, |x| x.extract::().ok()) +} diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index c6ff589d27..dd3f053ea8 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -4,6 +4,10 @@ use crate::{ displayable_output::{DisplayableOutput, DisplayableState}, fs::file_system, + interop::{ + compile_qasm3_to_qir, compile_qasm3_to_qsharp, compile_qasm_enriching_errors, + map_entry_compilation_errors, resource_estimate_qasm3, run_ast, run_qasm3, ImportResolver, + }, noisy_simulator::register_noisy_simulator_submodule, }; use miette::{Diagnostic, Report}; @@ -27,11 +31,33 @@ use qsc::{ target::Profile, LanguageFeatures, PackageType, SourceMap, }; +use qsc_qasm3::{OutputSemantics, ProgramType}; use resource_estimator::{self as re, estimate_expr}; use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc}; +/// If the classes are not Send, the Python interpreter +/// will not be able to use them in a separate thread. +/// +/// This function is used to verify that the classes are Send. +/// The code will fail to compile if the classes are not Send. +fn verify_classes_are_sendable() { + fn is_send() {} + is_send::(); + is_send::(); + is_send::(); + is_send::(); + is_send::(); + is_send::(); + + // QSharpError, and QasmError are not Send, but we don't raise + // them. Instead, we raise PyErr from them which is Send. On + // the Python side, they PyErr is converted into the + // corresponding exception. +} + #[pymodule] fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { + verify_classes_are_sendable(); m.add_class::()?; m.add_class::()?; m.add_class::()?; @@ -42,12 +68,18 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { m.add_function(wrap_pyfunction!(physical_estimates, m)?)?; m.add("QSharpError", py.get_type_bound::())?; register_noisy_simulator_submodule(py, m)?; + // QASM3 interop + m.add("QasmError", py.get_type_bound::())?; + m.add_function(wrap_pyfunction!(resource_estimate_qasm3, m)?)?; + m.add_function(wrap_pyfunction!(run_qasm3, m)?)?; + m.add_function(wrap_pyfunction!(compile_qasm3_to_qir, m)?)?; + m.add_function(wrap_pyfunction!(compile_qasm3_to_qsharp, m)?)?; Ok(()) } // This ordering must match the _native.pyi file. #[derive(Clone, Copy, PartialEq)] -#[pyclass(unsendable, eq, eq_int)] +#[pyclass(eq, eq_int)] #[allow(non_camel_case_types)] /// A Q# target profile. /// @@ -70,6 +102,16 @@ pub(crate) enum TargetProfile { Unrestricted, } +impl From for Profile { + fn from(profile: TargetProfile) -> Self { + match profile { + TargetProfile::Base => Profile::Base, + TargetProfile::Adaptive_RI => Profile::AdaptiveRI, + TargetProfile::Unrestricted => Profile::Unrestricted, + } + } +} + #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, @@ -82,12 +124,12 @@ thread_local! { static PACKAGE_CACHE: Rc> = Rc::default(); impl Interpreter { #[allow(clippy::too_many_arguments)] #[allow(clippy::needless_pass_by_value)] - #[pyo3(signature = (target, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None))] + #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None))] #[new] /// Initializes a new Q# interpreter. pub(crate) fn new( py: Python, - target: TargetProfile, + target_profile: TargetProfile, language_features: Option>, project_root: Option, read_file: Option, @@ -95,11 +137,7 @@ impl Interpreter { resolve_path: Option, fetch_github: Option, ) -> PyResult { - let target = match target { - TargetProfile::Adaptive_RI => Profile::AdaptiveRI, - TargetProfile::Base => Profile::Base, - TargetProfile::Unrestricted => Profile::Unrestricted, - }; + let target = Into::::into(target_profile).into(); let language_features = LanguageFeatures::from_iter(language_features.unwrap_or_default()); @@ -118,7 +156,7 @@ impl Interpreter { return Err(project.errors.into_py_err()); } - BuildableProgram::new(target.into(), project.package_graph_sources) + BuildableProgram::new(target, project.package_graph_sources) } else { panic!("file system hooks should have been passed in with a manifest descriptor") } @@ -128,13 +166,13 @@ impl Interpreter { LanguageFeatures::from_iter(language_features), None, ); - BuildableProgram::new(target.into(), graph) + BuildableProgram::new(target, graph) }; match interpret::Interpreter::new( SourceMap::new(buildable_program.user_code.sources, None), PackageType::Lib, - target.into(), + target, buildable_program.user_code.language_features, buildable_program.store, &buildable_program.user_code_dependencies, @@ -203,10 +241,7 @@ impl Interpreter { ) -> PyResult { let mut receiver = OptionalCallbackReceiver { callback, py }; match self.interpreter.run(&mut receiver, entry_expr) { - Ok(result) => match result { - Ok(v) => Ok(ValueWrapper(v).into_py(py)), - Err(errors) => Err(QSharpError::new_err(format_errors(errors))), - }, + Ok(value) => Ok(ValueWrapper(value).into_py(py)), Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } } @@ -277,6 +312,78 @@ impl Interpreter { )), } } + + #[allow(clippy::too_many_arguments)] + #[pyo3( + signature = (source, callback=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, **kwargs) + )] + pub fn _run_qasm3( + &mut self, + py: Python, + source: &str, + callback: Option, + read_file: Option, + list_directory: Option, + resolve_path: Option, + fetch_github: Option, + kwargs: Option>, + ) -> PyResult { + let mut receiver = OptionalCallbackReceiver { callback, py }; + + let kwargs = kwargs.unwrap_or_else(|| PyDict::new_bound(py)); + + let name = crate::interop::get_name(&kwargs)?; + let seed = crate::interop::get_seed(&kwargs); + let shots = crate::interop::get_shots(&kwargs)?; + let search_path = crate::interop::get_search_path(&kwargs)?; + let run_type = crate::interop::get_run_type(&kwargs)?; + + let fs = crate::interop::create_filesystem_from_py( + py, + read_file, + list_directory, + resolve_path, + fetch_github, + ); + let resolver = ImportResolver::new(fs, PathBuf::from(search_path)); + let program_type = match run_type.as_str() { + "statements" => ProgramType::Fragments, + "operation" => ProgramType::Operation(name.to_string()), + _ => ProgramType::File(name.to_string()), + }; + let (package, _source_map, signature) = compile_qasm_enriching_errors( + source, + &name, + &resolver, + program_type.clone(), + OutputSemantics::Qiskit, + false, + )?; + + let value = self + .interpreter + .eval_ast_fragments(&mut receiver, source, package) + .map_err(|errors| QSharpError::new_err(format_errors(errors)))?; + + match program_type { + ProgramType::File(..) => { + let entry_expr = signature.create_entry_expr_from_params(String::new()); + self.interpreter + .set_entry_expr(&entry_expr) + .map_err(|errors| map_entry_compilation_errors(errors, &signature))?; + + match run_ast(&mut self.interpreter, &mut receiver, shots, seed) { + Ok(result) => Ok(PyList::new_bound( + py, + result.iter().map(|v| ValueWrapper(v.clone()).into_py(py)), + ) + .into_py(py)), + Err(errors) => Err(QSharpError::new_err(format_errors(errors))), + } + } + _ => Ok(ValueWrapper(value).into_py(py)), + } + } } #[pyfunction] @@ -294,26 +401,35 @@ create_exception!( "An error returned from the Q# interpreter." ); -fn format_errors(errors: Vec) -> String { +create_exception!( + module, + QasmError, + pyo3::exceptions::PyException, + "An error returned from the OpenQASM parser." +); + +pub(crate) fn format_errors(errors: Vec) -> String { errors .into_iter() - .map(|e| { - let mut message = String::new(); - if let Some(stack_trace) = e.stack_trace() { - write!(message, "{stack_trace}").unwrap(); - } - let additional_help = python_help(&e); - let report = Report::new(e.clone()); - write!(message, "{report:?}") - .unwrap_or_else(|err| panic!("writing error failed: {err} error was: {e:?}")); - if let Some(additional_help) = additional_help { - writeln!(message, "{additional_help}").unwrap(); - } - message - }) + .map(|e| format_error(&e)) .collect::() } +pub(crate) fn format_error(e: &interpret::Error) -> String { + let mut message = String::new(); + if let Some(stack_trace) = e.stack_trace() { + write!(message, "{stack_trace}").unwrap(); + } + let additional_help = python_help(e); + let report = Report::new(e.clone()); + write!(message, "{report:?}") + .unwrap_or_else(|err| panic!("writing error failed: {err} error was: {e:?}")); + if let Some(additional_help) = additional_help { + writeln!(message, "{additional_help}").unwrap(); + } + message +} + /// Additional help text for an error specific to the Python module fn python_help(error: &interpret::Error) -> Option { if matches!(error, interpret::Error::UnsupportedRuntimeCapabilities) { @@ -323,7 +439,7 @@ fn python_help(error: &interpret::Error) -> Option { } } -#[pyclass(unsendable)] +#[pyclass] pub(crate) struct Output(DisplayableOutput); #[pymethods] @@ -363,7 +479,7 @@ impl Output { } } -#[pyclass(unsendable)] +#[pyclass] /// Captured simlation state dump. pub(crate) struct StateDumpData(pub(crate) DisplayableState); @@ -414,8 +530,8 @@ impl StateDumpData { } } -#[derive(PartialEq)] -#[pyclass(unsendable, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +#[pyclass(eq, eq_int)] /// A Q# measurement result. pub(crate) enum Result { Zero, @@ -424,6 +540,7 @@ pub(crate) enum Result { #[pymethods] impl Result { + #[allow(clippy::trivially_copy_pass_by_ref)] fn __repr__(&self) -> String { match self { Result::Zero => "Zero".to_owned(), @@ -431,10 +548,12 @@ impl Result { } } + #[allow(clippy::trivially_copy_pass_by_ref)] fn __str__(&self) -> String { self.__repr__() } + #[allow(clippy::trivially_copy_pass_by_ref)] fn __hash__(&self) -> u32 { match self { Result::Zero => 0, @@ -443,8 +562,8 @@ impl Result { } } -#[derive(PartialEq)] -#[pyclass(unsendable, eq, eq_int)] +#[derive(Clone, Copy, PartialEq)] +#[pyclass(eq, eq_int)] /// A Q# Pauli operator. pub(crate) enum Pauli { I, @@ -454,7 +573,7 @@ pub(crate) enum Pauli { } // Mapping of Q# value types to Python value types. -struct ValueWrapper(Value); +pub(crate) struct ValueWrapper(pub(crate) Value); impl IntoPy for ValueWrapper { fn into_py(self, py: Python) -> PyObject { @@ -494,9 +613,9 @@ impl IntoPy for ValueWrapper { } } -struct OptionalCallbackReceiver<'a> { - callback: Option, - py: Python<'a>, +pub(crate) struct OptionalCallbackReceiver<'a> { + pub(crate) callback: Option, + pub(crate) py: Python<'a>, } impl Receiver for OptionalCallbackReceiver<'_> { @@ -537,7 +656,7 @@ impl Receiver for OptionalCallbackReceiver<'_> { } } -#[pyclass(unsendable)] +#[pyclass] struct Circuit(pub qsc::circuit::Circuit); #[pymethods] diff --git a/pip/src/lib.rs b/pip/src/lib.rs index c071a6d867..e8b8e2ec53 100644 --- a/pip/src/lib.rs +++ b/pip/src/lib.rs @@ -5,5 +5,6 @@ allocator::assign_global!(); mod displayable_output; mod fs; +mod interop; mod interpreter; mod noisy_simulator; diff --git a/pip/tests-integration/__init__.py b/pip/tests-integration/__init__.py new file mode 100644 index 0000000000..d84a4b96d5 --- /dev/null +++ b/pip/tests-integration/__init__.py @@ -0,0 +1,4 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +"""qsharp integration tests""" diff --git a/pip/tests-integration/conftest.py b/pip/tests-integration/conftest.py new file mode 100644 index 0000000000..3635045928 --- /dev/null +++ b/pip/tests-integration/conftest.py @@ -0,0 +1,13 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +This file is used to configure pytest for the test suite. + +- It attempts to import necessary modules from test_circuits. + +Fixtures and other configurations for pytest can be added to this file to +be shared across multiple test files. +""" + +from interop_qiskit.test_circuits import * diff --git a/pip/tests-integration/interop_qiskit/__init__.py b/pip/tests-integration/interop_qiskit/__init__.py new file mode 100644 index 0000000000..b39feb9091 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/__init__.py @@ -0,0 +1,26 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pytest + +try: + # pylint: disable=unused-import + import qiskit + + QISKIT_AVAILABLE = True +except ImportError: + QISKIT_AVAILABLE = False + +SKIP_REASON = "Qiskit is not available" + + +def ignore_on_failure(func): + def wrapper(*args, **kwargs): + try: + func(*args, **kwargs) + except Exception: + pytest.skip("Test failed, skipping for now.") + else: + raise AssertionError("Test passed, remove decorator.") + + return wrapper diff --git a/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc b/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc new file mode 100644 index 0000000000..76f61c94f2 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.inc @@ -0,0 +1,5 @@ + +@SimulatableIntrinsic +gate my_gate q { + x q; +} diff --git a/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll b/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll new file mode 100644 index 0000000000..aeaeae4fce --- /dev/null +++ b/pip/tests-integration/interop_qiskit/resources/custom_intrinsics.ll @@ -0,0 +1,38 @@ +%Result = type opaque +%Qubit = type opaque + +define void @ENTRYPOINT__main() #0 { +block_0: + call void @my_gate(%Qubit* inttoptr (i64 0 to %Qubit*)) + call void @__quantum__qis__m__body(%Qubit* inttoptr (i64 0 to %Qubit*), %Result* inttoptr (i64 0 to %Result*)) + call void @__quantum__rt__array_record_output(i64 1, i8* null) + call void @__quantum__rt__result_record_output(%Result* inttoptr (i64 0 to %Result*), i8* null) + ret void +} + +declare void @my_gate(%Qubit*) + +declare void @__quantum__qis__m__body(%Qubit*, %Result*) #1 + +declare void @__quantum__rt__array_record_output(i64, i8*) + +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" } +attributes #1 = { "irreversible" } + +; module flags + +!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7, !8, !9, !10} + +!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} +!4 = !{i32 1, !"classical_ints", i1 true} +!5 = !{i32 1, !"qubit_resetting", i1 true} +!6 = !{i32 1, !"classical_floats", i1 false} +!7 = !{i32 1, !"backwards_branching", i1 false} +!8 = !{i32 1, !"classical_fixed_points", i1 false} +!9 = !{i32 1, !"user_functions", i1 false} +!10 = !{i32 1, !"multiple_target_branching", i1 false} diff --git a/pip/tests-integration/interop_qiskit/test_circuits/__init__.py b/pip/tests-integration/interop_qiskit/test_circuits/__init__.py new file mode 100644 index 0000000000..ab28982187 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_circuits/__init__.py @@ -0,0 +1,47 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from .random import * +from .test_circuits import * + +core_tests = [] + random_fixtures +core_tests_small = [] + random_fixtures_small + + +def generate_repro_information( + circuit: "QuantumCircuit", backend: "QSharpBackend", **options +): + name = circuit.name + profile_name = str(backend.options.target_profile) + message = f"Error with Qiskit circuit '{name}'." + message += "\n" + message += f"Profile: {profile_name}" + message += "\n" + + try: + qasm3_source = backend.qasm3(circuit, **options) + message += "QASM3 source:" + message += "\n" + message += str(qasm3_source) + except Exception as ex: + # if the conversion fails, print the circuit as a string + # as a fallback since we won't have the qasm3 source + message += "\nFailed converting QuantumCircuit to QASM3:\n" + message += str(ex) + message += "\n" + message += "QuantumCircuit rendered:" + message += "\n" + circuit_str = str(circuit.draw(output="text")) + message += circuit_str + return message + + try: + qsharp_source = backend.qsharp(circuit, **options) + message += "Q# source:" + message += "\n" + message += str(qsharp_source) + except Exception as ex: + message += "\nFailed converting QuantumCircuit to Q#:\n" + message += str(ex) + + return message diff --git a/pip/tests-integration/interop_qiskit/test_circuits/random.py b/pip/tests-integration/interop_qiskit/test_circuits/random.py new file mode 100644 index 0000000000..5e7f7437f8 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_circuits/random.py @@ -0,0 +1,61 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pytest +from qsharp import TargetProfile + +from .. import QISKIT_AVAILABLE + +if QISKIT_AVAILABLE: + from qiskit.circuit.random import random_circuit + from qsharp.interop import QSharpBackend + + +def _generate_random_fixture( + num_qubits: int, depth: int, target_profile: TargetProfile, name: str +): + @pytest.fixture() + def random(): + if target_profile == TargetProfile.Base: + circuit = random_circuit(num_qubits, depth, measure=True, reset=True) + elif target_profile == TargetProfile.Adaptive_RI: + circuit = random_circuit( + num_qubits, depth, measure=True, conditional=True, reset=True + ) + else: + raise ValueError(f"Unsupported QIR profile: {target_profile}") + + backend = QSharpBackend(target_profile=target_profile) + circuit = backend.transpile(circuit) + circuit.name = name + return circuit + + return random + + +# Generate random fixtures +random_fixtures = [] +random_fixtures_small = [] + + +def get_name(num_qubits: int, depth: int, target_profile: TargetProfile, prefix: str): + if target_profile == TargetProfile.Base: + return f"random_{prefix}_{num_qubits}x{depth}_base" + else: + return f"random_{prefix}_{num_qubits}x{depth}_adaptive_ri" + + +if QISKIT_AVAILABLE: + for num_qubits, depth in [(i, j) for i in range(2, 11) for j in range(2, 11)]: + for target_profile in [TargetProfile.Base, TargetProfile.Adaptive_RI]: + name = get_name(num_qubits, depth, target_profile, "full") + fixture = _generate_random_fixture(num_qubits, depth, target_profile, name) + locals()[name] = fixture + random_fixtures.append(name) + + for num_qubits, depth in [(i, j) for i in range(2, 5) for j in range(2, 5)]: + for target_profile in [TargetProfile.Base, TargetProfile.Adaptive_RI]: + name = get_name(num_qubits, depth, target_profile, "small") + fixture = _generate_random_fixture(num_qubits, depth, target_profile, name) + locals()[name] = fixture + random_fixtures_small.append(name) diff --git a/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py b/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py new file mode 100644 index 0000000000..321d3461c4 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_circuits/test_circuits.py @@ -0,0 +1,683 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pytest + +import numpy as np +from typing import Tuple, List + +from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON + +if QISKIT_AVAILABLE: + from qiskit.circuit import QuantumCircuit + + +def get_parameterized_circuit(num_qubits: int) -> "QuantumCircuit": + from qiskit.circuit.parameter import Parameter + + theta = Parameter("θ") + circuit = QuantumCircuit(num_qubits, 1) + circuit.h(0) + for i in range(num_qubits - 1): + circuit.cx(i, i + 1) + circuit.rz(theta, range(num_qubits)) + for i in reversed(range(num_qubits - 1)): + circuit.cx(i, i + 1) + circuit.h(0) + circuit.measure(0, 0) + + return circuit + + +def random_bit() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (?) + """ + circuit = QuantumCircuit(1, 1) + circuit.name = "Single qubit random" + circuit.h(0) + circuit.measure(0, 0) + + return circuit, ["0", "1"] + + +def intrinsic_hixyz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 1, 1, 1, 1, 1) + """ + circuit = QuantumCircuit(6, 6) + circuit.name = "HIXYZ" + + # h in qs is h in qiskit + circuit.h(0) + circuit.z(0) + circuit.h(0) + # mresetz == measure() + circuit.measure(0, 0) + + # i target + circuit.x(1) + circuit.id(1) + # mresetz == measure() + circuit.measure(1, 1) + + # x + circuit.x(2) + circuit.measure(2, 2) + + # ya + circuit.y(3) + circuit.measure(3, 3) + + # yb + circuit.h(4) + circuit.y(4) + circuit.h(4) + circuit.measure(4, 4) + + # z + circuit.h(5) + circuit.z(5) + circuit.h(5) + circuit.measure(5, 5) + + return circuit, ["111111"] + + +def intrinsic_ccnot() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 1, 1, 0, 0, 1, 0, 0, 0) + """ + circuit = QuantumCircuit(9, 9) + circuit.name = "_CCNOT" + + circuit.ccx(0, 1, 2) + + circuit.measure(0, 0) + circuit.measure(1, 1) + circuit.measure(2, 2) + + circuit.x(3) + circuit.ccx(3, 4, 5) + + circuit.measure(3, 3) + circuit.measure(4, 4) + circuit.measure(5, 5) + + circuit.x(6) + circuit.x(7) + circuit.ccx(6, 7, 8) + + circuit.measure(6, 6) + circuit.measure(7, 7) + circuit.measure(8, 8) + + return circuit, ["111001000"] + + +def intrinsic_cnot() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 1, 0, 0) + """ + circuit = QuantumCircuit(4, 4) + circuit.name = "_CNOT" + + circuit.cx(0, 1) + + circuit.measure(0, 0) + circuit.measure(1, 1) + + circuit.x(2) + circuit.cx(2, 3) + + circuit.measure(2, 2) + circuit.measure(3, 3) + + return circuit, ["1100"] + + +def intrinsic_measure() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 0) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Intrinsic_M" + + circuit.measure(0, 0) + + circuit.x(1) + + circuit.measure(1, 1) + + return circuit, ["10"] + + +def intrinsic_stswap() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 0, 1, 1) + """ + circuit = QuantumCircuit(4, 4) + circuit.name = "STSWAP" + + circuit.h(0) + circuit.s(0) + circuit.s(0) + circuit.h(0) + circuit.measure(0, 0) + + circuit.h(1) + circuit.t(1) + circuit.t(1) + circuit.t(1) + circuit.t(1) + circuit.h(1) + circuit.measure(1, 1) + + circuit.x(2) + circuit.swap(2, 3) + circuit.measure(2, 2) + circuit.measure(3, 3) + + return circuit, ["1011"] + + +def exercise_reset() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 0) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test reset" + + circuit.measure(0, 0) + + circuit.x(1) + circuit.reset(1) + circuit.measure(1, 1) + + return circuit, ["00"] + + +def exercise_rx_ry_rz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 0, 0) + """ + # Create a quantum circuit with one qubit and one classical bit + circuit = QuantumCircuit(3, 3) + circuit.name = "Test_rx_ry_rz" + + # Apply RX, RY, and RZ gates with some arbitrary angles + for _ in range(4): + circuit.rx(np.pi, 0) # Rotate around the X-axis by pi radians + circuit.ry(np.pi, 1) # Rotate around the Y-axis by pi radians + circuit.rz(np.pi, 2) # Rotate around the Z-axis by pi radians + + circuit.measure(0, 0) + circuit.measure(1, 1) + circuit.measure(2, 2) + + return circuit, ["000"] + + +def exercise_tdg_sdg() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_tdg" + + circuit.h(0) # changes to + state + circuit.tdg(0) + circuit.tdg(0) + circuit.tdg(0) + circuit.tdg(0) + circuit.h(0) + circuit.measure(0, 0) + + circuit.h(1) + circuit.sdg(1) + circuit.sdg(1) + circuit.h(1) + circuit.measure(1, 1) + + return circuit, ["11"] + + +def exercise_rxx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_rxx" + + circuit.rxx(np.pi / 2, 0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00", "11"] + + +def exercise_ryy() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_ryy" + + circuit.ryy(np.pi / 2, 0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00", "11"] + + +def exercise_rzz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_rzz" + + circuit.h(0) + circuit.h(1) + circuit.rzz(np.pi / 2, 0, 1) + circuit.h(0) + circuit.h(1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00", "11"] + + +def exercise_barrier_delay() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1) + """ + circuit = QuantumCircuit(1, 1) + circuit.name = "Test_barrier_delay" + + circuit.x(0) + circuit.barrier() + circuit.x(0) + circuit.barrier() + circuit.x(0) + + circuit.delay(100, 0, unit="ns") # Introducing a delay of 100 nanoseconds + + circuit.measure(0, 0) + + return circuit, ["1"] + + +def exercise_initialize_prepare_state() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: The qubits are initialized to the state [1/√2, 1/√2, 0, 0].""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test initialize and prepare state" + + # State vector to initialize: |ψ⟩ = (|0⟩ - |1⟩) / √2 + circuit.initialize([1 / np.sqrt(2), -1 / np.sqrt(2)], 0) + circuit.h(0) + circuit.measure(0, 0) + + circuit.prepare_state([1 / np.sqrt(2), -1 / np.sqrt(2)], 1) + circuit.h(1) + circuit.measure(1, 1) + + return circuit, ["11"] + + +def exercise_dcx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0, 0) or (1, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_DCX" + + circuit.h(0) + circuit.dcx(0, 1) + circuit.h(1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00"] + + +def exercise_ecr() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the ECR gate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_ECR" + + circuit.ecr(0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["01", "11"] + + +def exercise_iswap() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the iSwap gate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_iSwap" + + circuit.x(0) + circuit.iswap(0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["10"] + + +def exercise_ms() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the MSGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_MS" + + circuit = QuantumCircuit(2, 2) + circuit.ms(np.pi / 2, [0, 1]) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00", "11"] + + +def exercise_p() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1) + """ + circuit = QuantumCircuit(1, 1) + circuit.name = "Test_Phase" + + circuit.h(0) + circuit.p(np.pi, 0) + circuit.h(0) + circuit.measure(0, 0) + + return circuit, ["1"] + + +def exercise_pauli() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1, 1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_Pauli" + + circuit.h(0) + circuit.pauli("XZ", [0, 1]) + circuit.h(0) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["11"] + + +def exercise_r_rccx_rcccx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the RC3XGate to qubits.""" + circuit = QuantumCircuit(8, 8) + circuit.name = "Test_R_RCCX_RC3X" + + circuit.r(np.pi, 0, 0) + circuit.measure(0, 0) + + circuit.x(1) + circuit.x(2) + circuit.rccx(1, 2, 3) + circuit.measure([1, 2, 3], [1, 2, 3]) + + circuit.x(4) + circuit.x(5) + circuit.x(6) + circuit.rcccx(4, 5, 6, 7) + circuit.measure([4, 5, 6, 7], [4, 5, 6, 7]) + + return circuit, ["11111111"] + + +def exercise_rzx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the RZXGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_RZX" + + circuit.rzx(np.pi / 2, 0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["00", "10"] + + +def exercise_sx_sxdg() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1) + """ + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_SX_SXDG" + + circuit.sx(0) + circuit.sx(0) + circuit.measure(0, 0) + + circuit.sxdg(1) + circuit.sxdg(1) + circuit.measure(1, 1) + + return circuit, ["11"] + + +def exercise_u() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (0) + """ + circuit = QuantumCircuit(1, 1) + circuit.name = "Test_U" + + for _ in range(4): + circuit.u(np.pi, -np.pi / 2, np.pi / 2, 0) + + circuit.measure(0, 0) + + return circuit, ["0"] + + +def exercise_unitary() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: + (1) + """ + circuit = QuantumCircuit(1, 1) + circuit.name = "Test_Unitary" + + # this is the unitary matrix for the pauli x gate + unitary_matrix = np.array([[0, 1], [1, 0]]) + circuit.unitary(unitary_matrix, [0]) + circuit.measure(0, 0) + + return circuit, ["1"] + + +def exercise_ccz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CCZGate to qubits.""" + circuit = QuantumCircuit(3, 3) + circuit.name = "Test_CCZ" + + circuit.x(0) + circuit.x(1) + circuit.h(2) + circuit.ccz(0, 1, 2) + circuit.h(2) + circuit.measure([0, 1, 2], [0, 1, 2]) + + return circuit, ["111"] + + +def exercise_ch() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CHGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_CH" + + circuit.x(0) + circuit.h(1) + circuit.z(1) + circuit.ch(0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["11"] + + +def exercise_cp() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CPhaseGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_CP" + + circuit.x(0) + circuit.h(1) + circuit.cp(np.pi, 0, 1) + circuit.h(1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["11"] + + +def exercise_crx_cry_crz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CRXGate to qubits.""" + circuit = QuantumCircuit(6, 6) + circuit.name = "Test_CRX_CRY_CRZ" + + circuit.x(0) + circuit.crx(np.pi, 0, 1) + circuit.measure([0, 1], [0, 1]) + + circuit.x(2) + circuit.cry(np.pi, 2, 3) + circuit.measure([2, 3], [2, 3]) + + circuit.x(4) + circuit.h(5) + circuit.crz(np.pi, 4, 5) + circuit.h(5) + circuit.measure([4, 5], [4, 5]) + + return circuit, ["111111"] + + +def exercise_cs_csdg() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CSGate and CSdgGate to qubits.""" + circuit = QuantumCircuit(4, 4) + circuit.name = "Test_CS_CSdg" + + circuit.x(0) + circuit.h(1) + circuit.cs(0, 1) + circuit.cs(0, 1) + circuit.h(1) + circuit.measure([0, 1], [0, 1]) + + circuit.x(2) + circuit.h(3) + circuit.csdg(2, 3) + circuit.csdg(2, 3) + circuit.h(3) + circuit.measure([2, 3], [2, 3]) + + return circuit, ["1111"] + + +def exercise_cswap() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CSwapGate to qubits.""" + circuit = QuantumCircuit(3, 3) + circuit.name = "Test_CSwap" + + circuit.x(0) + circuit.x(1) + circuit.cswap(0, 1, 2) + circuit.measure([0, 1, 2], [0, 1, 2]) + + return circuit, ["101"] + + +def exercise_csx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CSXGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_CSX" + + circuit.x(0) + circuit.csx(0, 1) + circuit.csx(0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["11"] + + +def exercise_cu() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CUGate to qubits.""" + circuit = QuantumCircuit(2, 2) + circuit.name = "Test_CU" + + circuit.u(np.pi, -np.pi / 2, np.pi / 2, 0) + circuit.cu(np.pi, np.pi, -np.pi / 2, np.pi / 2, 0, 1) + circuit.measure([0, 1], [0, 1]) + + return circuit, ["11"] + + +def exercise_cx_cy_cz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the CXGate to qubits.""" + circuit = QuantumCircuit(6, 6) + circuit.name = "Test_CX_CY_CZ" + + circuit.x(0) + circuit.cx(0, 1) + circuit.measure([0, 1], [0, 1]) + + circuit.x(2) + circuit.cy(2, 3) + circuit.measure([2, 3], [2, 3]) + + circuit.x(4) + circuit.h(5) + circuit.cz(4, 5) + circuit.h(5) + circuit.measure([4, 5], [4, 5]) + + return circuit, ["111111"] + + +def exercise_mcp() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the Multi-Controlled PhaseGate to qubits.""" + circuit = QuantumCircuit(3, 3) + circuit.name = "Test_MCP" + + circuit.x(0) + circuit.x(1) + circuit.h(2) + circuit.mcp(np.pi, [0, 1], 2) + circuit.h(2) + circuit.measure([0, 1, 2], [0, 1, 2]) + + return circuit, ["111"] + + +def exercise_mcrx_mcry_mcrz() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the Multi-Controlled RXGate to qubits.""" + circuit = QuantumCircuit(9, 9) + circuit.name = "Test_MCRX_MCRY_MCRZ" + + circuit.x(0) + circuit.x(1) + circuit.mcrx(np.pi, [0, 1], 2) + circuit.measure([0, 1, 2], [0, 1, 2]) + + circuit.x(3) + circuit.x(4) + circuit.mcry(np.pi, [3, 4], 5) + circuit.measure([3, 4, 5], [3, 4, 5]) + + circuit.x(6) + circuit.x(7) + circuit.h(8) + circuit.mcrz(np.pi, [6, 7], 8) + circuit.h(8) + circuit.measure([6, 7, 8], [6, 7, 8]) + + return circuit, ["111111111"] + + +def exercise_mcx() -> Tuple["QuantumCircuit", List[str]]: + """Expected result: Applying the Multi-Controlled X gate to qubits.""" + circuit = QuantumCircuit(3, 3) + circuit.name = "Test_MCX" + + circuit.x(0) + circuit.x(1) + circuit.mcx([0, 1], 2) + circuit.measure([0, 1, 2], [0, 1, 2]) + + return circuit, ["111"] diff --git a/pip/tests-integration/interop_qiskit/test_gate_correctness.py b/pip/tests-integration/interop_qiskit/test_gate_correctness.py new file mode 100644 index 0000000000..b2cf697cf3 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_gate_correctness.py @@ -0,0 +1,292 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import pytest +from qsharp import TargetProfile + +from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON + +if QISKIT_AVAILABLE: + from qiskit import QuantumCircuit + from qsharp.interop.qiskit import QSharpBackend + +from .test_circuits import ( + generate_repro_information, + random_bit, + intrinsic_measure, + intrinsic_hixyz, + intrinsic_ccnot, + intrinsic_cnot, + intrinsic_stswap, + exercise_reset, + exercise_rx_ry_rz, + exercise_tdg_sdg, + exercise_rxx, + exercise_ryy, + exercise_rzz, + exercise_dcx, + exercise_ecr, + exercise_initialize_prepare_state, + exercise_iswap, + exercise_barrier_delay, + exercise_ms, + exercise_p, + exercise_pauli, + exercise_r_rccx_rcccx, + exercise_rzx, + exercise_sx_sxdg, + exercise_u, + exercise_unitary, + exercise_ccz, + exercise_ch, + exercise_cp, + exercise_crx_cry_crz, + exercise_cs_csdg, + exercise_cswap, + exercise_csx, + exercise_cu, + exercise_cx_cy_cz, + exercise_mcrx_mcry_mcrz, + exercise_mcp, + exercise_mcx, +) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_random_bit() -> None: + _test_circuit(*random_bit()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_intrinsic_hixyz() -> None: + _test_circuit(*intrinsic_hixyz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_intrinsic_ccnot() -> None: + _test_circuit(*intrinsic_ccnot()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_intrinsic_cnot() -> None: + _test_circuit(*intrinsic_cnot()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_intrinsic_measure() -> None: + _test_circuit(*intrinsic_measure()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_intrinsic_stswap() -> None: + _test_circuit(*intrinsic_stswap()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_reset() -> None: + _test_circuit(*exercise_reset()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_rx_ry_rz() -> None: + _test_circuit(*exercise_rx_ry_rz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_tdg_sdg() -> None: + _test_circuit(*exercise_tdg_sdg()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_rxx() -> None: + _test_circuit(*exercise_rxx()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_ryy() -> None: + _test_circuit(*exercise_ryy()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_rzz() -> None: + _test_circuit(*exercise_rzz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_barrier_delay() -> None: + _test_circuit(*exercise_barrier_delay()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_initialize_prepare_state() -> None: + _test_circuit(*exercise_initialize_prepare_state()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_dcx() -> None: + _test_circuit(*exercise_dcx()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_ecr() -> None: + _test_circuit(*exercise_ecr()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_iswap() -> None: + _test_circuit(*exercise_iswap()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_ms() -> None: + _test_circuit(*exercise_ms()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_p() -> None: + _test_circuit(*exercise_p()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_pauli() -> None: + _test_circuit(*exercise_pauli()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_r_rccx_rcccx() -> None: + _test_circuit(*exercise_r_rccx_rcccx()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_rzx() -> None: + _test_circuit(*exercise_rzx()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_sx_sxdg() -> None: + _test_circuit(*exercise_sx_sxdg()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_u() -> None: + _test_circuit(*exercise_u()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_unitary() -> None: + _test_circuit(*exercise_unitary()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_ccz() -> None: + _test_circuit(*exercise_ccz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_ch() -> None: + _test_circuit(*exercise_ch()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_cp() -> None: + _test_circuit(*exercise_cp()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_crx_cry_crz() -> None: + _test_circuit(*exercise_crx_cry_crz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_cs_csdg() -> None: + _test_circuit(*exercise_cs_csdg()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_cswap() -> None: + _test_circuit(*exercise_cswap()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_csx() -> None: + _test_circuit(*exercise_csx()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_cu() -> None: + _test_circuit(*exercise_cu()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_cx_cy_cz() -> None: + _test_circuit(*exercise_cx_cy_cz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_mcp() -> None: + _test_circuit(*exercise_mcp()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_mcrx_mcry_mcrz() -> None: + _test_circuit(*exercise_mcrx_mcry_mcrz()) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qiskit_qir_exercise_mcx() -> None: + _test_circuit(*exercise_mcx()) + + +def _test_circuit( + circuit: "QuantumCircuit", peaks, results_len=1, num_shots=20, meas_level=2 +): + target_profile = TargetProfile.Base + seed = 42 + backend = QSharpBackend(target_profile=target_profile, seed=seed) + try: + job = backend.run(circuit, shots=num_shots) + result = job.result() + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + results = result.results + + assert len(results) == results_len + assert results[0].shots == num_shots + assert results[0].success + assert results[0].meas_level == meas_level + assert hasattr(results[0].data, "counts") + assert hasattr(results[0].data, "probabilities") + + counts = result.get_counts() + + # Check if the result is as expected + assert _check_peaks(counts, peaks) + + +def _check_peaks(counts, peaks) -> bool: + """ + This function checks if all values in `peaks` are the highest peaks in `counts`. + + Parameters: + counts (dict): A dictionary where keys are string return types and values are their counts. + peaks (list): A list of string return types expected to be the peaks. + + Returns: + bool: True if all values in peaks are the highest peaks in counts, otherwise False. + """ + + # Find the maximum count value in the histogram + max_count = max(counts.values()) + + # Find all the peaks in the histogram that have the maximum count value + actual_peaks = [key for key, value in counts.items() if value == max_count] + + print("actual peaks:", actual_peaks) + print("specified peaks", peaks) + + # Check if all actual peaks are in the expected peaks + return all(peak in peaks for peak in actual_peaks) diff --git a/pip/tests-integration/interop_qiskit/test_gateset_qasm.py b/pip/tests-integration/interop_qiskit/test_gateset_qasm.py new file mode 100644 index 0000000000..7d5927fd2f --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_gateset_qasm.py @@ -0,0 +1,169 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from typing import Callable +import pytest + +from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON + +if QISKIT_AVAILABLE: + from qiskit import QuantumCircuit + from qsharp.interop import QSharpBackend + + +def run_transpile_test( + operation: Callable[["QuantumCircuit"], None], expected_output: str, **options +) -> None: + circuit = QuantumCircuit(3, 3) + operation(circuit) + info = QSharpBackend().qasm3(circuit, **options) + lines = info.splitlines() + # remove the first four lines, which are the header + # OPENQASM 3.0; + # include "stdgates.inc"; + # bit[3] c; + # qubit[3] q; + remaining_lines = lines[4:] + result = "\n".join(remaining_lines) + assert result == expected_output + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_reset_instruction_transpiles() -> None: + run_transpile_test( + lambda circuit: circuit.reset(1), + "reset q[1];", + remove_reset_in_zero_state=False, + ) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_ccx_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.ccx(2, 0, 1), "ccx q[2], q[0], q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_cx_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.cx(2, 0), "cx q[2], q[0];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_cy_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.cy(1, 2), "cy q[1], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_cz_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.cz(0, 2), "cz q[0], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_rx_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.rx(0.5, 2), "rx(0.5) q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_rxx_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.rxx(0.5, 2, 0), "rxx(0.5) q[2], q[0];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_ry_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.ry(0.5, 1), "ry(0.5) q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_ryy_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.ryy(0.5, 1, 2), "ryy(0.5) q[1], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_rz_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.rz(0.5, 1), "rz(0.5) q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_rzz_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.rzz(0.5, 0, 2), "rzz(0.5) q[0], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_h_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.h(1), "h q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_s_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.s(1), "s q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_sdg_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.sdg(1), "sdg q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_sx_transpiles_to_rx_pi_over_2() -> None: + run_transpile_test( + lambda circuit: circuit.sx(1), "rx(pi/2) q[1];", disable_constants=False + ) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_sx_transpiles_to_rx_pi_over_2_approx() -> None: + run_transpile_test(lambda circuit: circuit.sx(1), "rx(1.5707963267948966) q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_swap_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.swap(1, 0), "swap q[1], q[0];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_t_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.t(1), "t q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_tdg_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.tdg(1), "tdg q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_x_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.x(1), "x q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_y_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.y(1), "y q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_z_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.z(1), "z q[1];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_crx_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.crx(0.5, 1, 2), "crx(0.5) q[1], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_cry_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.cry(0.5, 1, 2), "cry(0.5) q[1], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_crz_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.crz(0.5, 1, 2), "crz(0.5) q[1], q[2];") + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_id_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.id(1), "id q[1];", optimization_level=0) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_gate_ch_transpiles() -> None: + run_transpile_test(lambda circuit: circuit.ch(1, 0), "ch q[1], q[0];") diff --git a/pip/tests-integration/interop_qiskit/test_qir.py b/pip/tests-integration/interop_qiskit/test_qir.py new file mode 100644 index 0000000000..c3eb7b2603 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_qir.py @@ -0,0 +1,228 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +import os +from typing import Optional + +import pytest +from qsharp import TargetProfile, QSharpError + +from . import QISKIT_AVAILABLE, SKIP_REASON, ignore_on_failure + + +if QISKIT_AVAILABLE: + from .test_circuits import ( + core_tests, + generate_repro_information, + ) + from qsharp.interop.qiskit import QasmError, QirTarget + from qiskit.circuit import QuantumCircuit, Parameter, Gate + from qiskit.circuit.quantumcircuit import QubitSpecifier + from qsharp.interop.qiskit.backends import QSharpBackend +else: + core_tests = [] + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +@pytest.mark.parametrize("circuit_name", core_tests) +def test_random(circuit_name: str, request): + circuit = request.getfixturevalue(circuit_name) + if str.endswith(circuit_name.lower(), "base"): + target_profile = TargetProfile.Base + else: + target_profile = TargetProfile.Adaptive_RI + + backend = QSharpBackend(target_profile=target_profile) + try: + qir = backend.qir(circuit) + assert qir is not None + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_generating_qir_with_unbound_param_raises(): + theta = Parameter("theta") + + circuit = QuantumCircuit(1) + circuit.name = "test" + circuit.rx(theta, 0) + circuit.measure_all() + + target_profile = TargetProfile.Base + backend = QSharpBackend(target_profile=target_profile) + try: + with pytest.raises(QSharpError) as ex: + _ = backend.qir(circuit) + message = str(ex.value) + assert "Circuit has unbound input parameters" in message + assert "help: Parameters: theta: Double" in message + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_generating_qir_with_bound_param_with_wrong_type_raises(): + theta = Parameter("theta") + + circuit = QuantumCircuit(1) + circuit.name = "test" + circuit.rx(theta, 0) + circuit.measure_all() + + target_profile = TargetProfile.Base + backend = QSharpBackend(target_profile=target_profile) + try: + with pytest.raises(QSharpError) as ex: + _ = backend.qir(circuit, params="true") + message = str(ex) + assert "Circuit has unbound input parameters" in message + assert "help: Parameters: theta: Double" in message + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_generating_qir_with_bound_param(): + theta = Parameter("theta") + + circuit = QuantumCircuit(1) + circuit.name = "test" + circuit.rx(theta, 0) + circuit.measure_all() + circuit.assign_parameters({"theta": 0.5}, inplace=True) + + target_profile = TargetProfile.Base + backend = QSharpBackend(target_profile=target_profile) + try: + qir = backend.qir(circuit) + assert qir is not None + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_generating_qir_without_measuring_into_all_registers_raises(): + circuit = QuantumCircuit(2, 2) + circuit.rx(0.5, 0) + + target_profile = TargetProfile.Base + backend = QSharpBackend(target_profile=target_profile) + try: + with pytest.raises(QSharpError) as ex: + _ = backend.qir(circuit) + assert "ensure all output registers have been measured into." in str(ex) + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_generating_qir_without_registers_raises(): + circuit = QuantumCircuit(2) + circuit.rx(0.5, 0) + + target_profile = TargetProfile.Base + backend = QSharpBackend(target_profile=target_profile) + try: + with pytest.raises(QasmError) as ex: + _ = backend.qir(circuit) + assert "Qiskit circuits must have output registers." in str(ex) + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +def get_resource_path(file_name: Optional[str] = None) -> str: + current_directory = os.path.dirname(os.path.abspath(__file__)) + if file_name is None: + return os.path.join(current_directory, "resources") + return os.path.join(current_directory, "resources", file_name) + + +def read_resource_file(file_name: str) -> str: + resource_path = get_resource_path(file_name) + with open(resource_path, encoding="utf-8") as f: + return f.read() + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +@ignore_on_failure +def test_custom_qir_intrinsics_generates_qir(): + expected_qir = read_resource_file("custom_intrinsics.ll") + + def my_gate(self: QuantumCircuit, qubit: QubitSpecifier): + return self.append(Gate(name="my_gate", num_qubits=1, params=[]), [qubit]) + + QuantumCircuit.my_gate = my_gate + + class CustomTarget(QirTarget): + def __init__(self): + super().__init__() + self.add_instruction( + Gate(name="my_gate", num_qubits=1, params=[]), name="my_gate" + ) + + target = CustomTarget() + circuit = QuantumCircuit(1, 1) + circuit.my_gate(0) + circuit.measure(0, 0) + + target_profile = TargetProfile.Adaptive_RI + + options = { + "search_path": get_resource_path(), + "includes": ("stdgates.inc", "custom_intrinsics.inc"), + } + + backend = QSharpBackend(target_profile=target_profile, target=target) + qir = backend.qir(circuit, **options) + assert qir == expected_qir + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +@ignore_on_failure +def test_custom_qir_intrinsics_is_simulatable(): + def my_gate(self: QuantumCircuit, qubit: QubitSpecifier): + return self.append(Gate(name="my_gate", num_qubits=1, params=[]), [qubit]) + + QuantumCircuit.my_gate = my_gate + + class CustomTarget(QirTarget): + def __init__(self): + super().__init__() + self.add_instruction( + Gate(name="my_gate", num_qubits=1, params=[]), name="my_gate" + ) + + target = CustomTarget() + circuit = QuantumCircuit(1, 1) + circuit.my_gate(0) + circuit.measure(0, 0) + + target_profile = TargetProfile.Adaptive_RI + + options = { + "search_path": get_resource_path(), + "includes": ("stdgates.inc", "custom_intrinsics.inc"), + } + + backend = QSharpBackend(target_profile=target_profile, target=target) + result = backend.run(circuit, **options).result() + assert result.get_counts() == {"1": 1024} diff --git a/pip/tests-integration/interop_qiskit/test_re.py b/pip/tests-integration/interop_qiskit/test_re.py new file mode 100644 index 0000000000..f899bdf444 --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_re.py @@ -0,0 +1,97 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from concurrent.futures import ThreadPoolExecutor +import pytest + +from qsharp.estimator import ( + EstimatorParams, + QubitParams, + LogicalCounts, +) + + +from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON + +if QISKIT_AVAILABLE: + from qiskit import QuantumCircuit + from qiskit.circuit.library import RGQFTMultiplier + from qsharp.interop import ResourceEstimatorBackend + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_qsharp_estimation_with_single_params() -> None: + params = EstimatorParams() + params.error_budget = 0.333 + params.qubit_params.name = QubitParams.MAJ_NS_E4 + assert params.as_dict() == { + "qubitParams": {"name": "qubit_maj_ns_e4"}, + "errorBudget": 0.333, + } + circuit = QuantumCircuit(10, 10) + for index in range(10): + circuit.t(index) + circuit.measure(index, index) + sim = ResourceEstimatorBackend() + res = sim.run(circuit, params=params).result() + + assert res["status"] == "success" + assert res["physicalCounts"] is not None + assert res["jobParams"]["qubitParams"]["name"] == "qubit_maj_ns_e4" + assert res.logical_counts == LogicalCounts( + { + "numQubits": 10, + "tCount": 10, + "rotationCount": 0, + "rotationDepth": 0, + "cczCount": 0, + "measurementCount": 10, + } + ) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_estimate_qiskit_rgqft_multiplier() -> None: + bitwidth = 4 + circuit = RGQFTMultiplier(num_state_qubits=bitwidth) + params = EstimatorParams() + sim = ResourceEstimatorBackend() + job = sim.run(circuit, params=params, optimization_level=0) + res = job.result() + assert res["status"] == "success" + assert res.logical_counts == LogicalCounts( + { + "numQubits": 16, + "tCount": 90, + "rotationCount": 958, + "rotationDepth": 658, + "cczCount": 0, + "ccixCount": 0, + "measurementCount": 0, + } + ) + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_estimate_qiskit_rgqft_multiplier_in_threadpool() -> None: + bitwidth = 4 + circuit = RGQFTMultiplier(num_state_qubits=bitwidth) + params = EstimatorParams() + executor = ThreadPoolExecutor(max_workers=1) + sim = ResourceEstimatorBackend( + executor=executor, transpile_options={"optimization_level": 0} + ) + job = sim.run(circuit, params=params) + res = job.result() + assert res["status"] == "success" + assert res.logical_counts == LogicalCounts( + { + "numQubits": 16, + "tCount": 90, + "rotationCount": 958, + "rotationDepth": 658, + "cczCount": 0, + "ccixCount": 0, + "measurementCount": 0, + } + ) diff --git a/pip/tests-integration/interop_qiskit/test_run_sim.py b/pip/tests-integration/interop_qiskit/test_run_sim.py new file mode 100644 index 0000000000..283f4d4fef --- /dev/null +++ b/pip/tests-integration/interop_qiskit/test_run_sim.py @@ -0,0 +1,157 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +from concurrent.futures import ThreadPoolExecutor +import pytest +from qsharp import TargetProfile, QSharpError + +from interop_qiskit import QISKIT_AVAILABLE, SKIP_REASON + +if QISKIT_AVAILABLE: + from qiskit import QuantumCircuit + from qiskit_aer import AerSimulator + from qiskit.qasm3 import loads as from_qasm3 + from qiskit.providers import JobStatus + from qiskit import ClassicalRegister + from qsharp.interop import QSharpBackend + from .test_circuits import ( + core_tests_small, + generate_repro_information, + get_parameterized_circuit, + ) +else: + core_tests_small = [] + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_run_smoke() -> None: + circuit = QuantumCircuit(2, 2) + circuit.x(0) + circuit.cx(0, 1) + circuit.measure_all(add_bits=False) + backend = QSharpBackend() + res = backend.run(circuit, shots=1).result() + assert res is not None + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_run_state_prep_smoke() -> None: + circuit = QuantumCircuit(1) + circuit.initialize([0.6, 0.8]) + circuit.measure_all() + backend = QSharpBackend() + res = backend.run(circuit, shots=1).result() + assert res is not None + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +@pytest.mark.parametrize("circuit_name", core_tests_small) +def test_random(circuit_name: str, request): + circuit = request.getfixturevalue(circuit_name) + if str.endswith(circuit_name.lower(), "base"): + target_profile = TargetProfile.Base + else: + target_profile = TargetProfile.Adaptive_RI + backend = QSharpBackend(target_profile=target_profile) + try: + _ = backend.run(circuit, shots=1).result() + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_get_counts_matches_qiskit_simulator(): + target_profile = TargetProfile.Base + circuit = create_deterministic_test_circuit() + backend = QSharpBackend(target_profile=target_profile) + + try: + qasm3 = backend.qasm3(circuit) + circuit = from_qasm3(qasm3) + + aersim = AerSimulator() + job = aersim.run(circuit, shots=5) + qsjob = backend.run(circuit, shots=5) + qscounts = qsjob.result().get_counts() + assert qscounts == job.result().get_counts() + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def create_deterministic_test_circuit(): + cr0 = ClassicalRegister(2, "first") + cr1 = ClassicalRegister(3, "second") + circuit = QuantumCircuit(5) + circuit.add_register(cr0) + circuit.add_register(cr1) + circuit.x(0) + circuit.id(1) + circuit.id(2) + circuit.x(3) + circuit.x(4) + circuit.measure_all(add_bits=False) + return circuit + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_execution_works_with_threadpool_set_on_backend(): + target_profile = TargetProfile.Base + circuit = create_deterministic_test_circuit() + executor = ThreadPoolExecutor(max_workers=4) + backend = QSharpBackend(target_profile=target_profile, executor=executor, shots=5) + + try: + job = backend.run(circuit) + qscounts = job.result().get_counts() + assert str(qscounts) == "{'110 01': 5}" + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_execution_works_with_threadpool_set_on_run(): + target_profile = TargetProfile.Base + circuit = create_deterministic_test_circuit() + + backend = QSharpBackend(target_profile=target_profile, shots=5) + try: + executor = ThreadPoolExecutor(max_workers=1) + job = backend.run(circuit, executor=executor) + qscounts = job.result().get_counts() + assert str(qscounts) == "{'110 01': 5}" + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex + + +@pytest.mark.skipif(not QISKIT_AVAILABLE, reason=SKIP_REASON) +def test_get_counts_matches_qiskit_simulator_multiple_circuits(): + target_profile = TargetProfile.Base + circuit = create_deterministic_test_circuit() + circuit2 = create_deterministic_test_circuit() + backend = QSharpBackend(target_profile=target_profile) + + try: + qasm3 = backend.qasm3(circuit) + circuit = from_qasm3(qasm3) + + aersim = AerSimulator() + job = aersim.run([circuit, circuit2], shots=5) + qsjob = backend.run([circuit, circuit2], shots=5) + qscounts = qsjob.result().get_counts() + assert qscounts == job.result().get_counts() + except AssertionError: + raise + except Exception as ex: + additional_info = generate_repro_information(circuit, backend) + raise RuntimeError(additional_info) from ex diff --git a/pip/tests-integration/test_requirements.txt b/pip/tests-integration/test_requirements.txt index 826959fe2e..dc323dc7ac 100644 --- a/pip/tests-integration/test_requirements.txt +++ b/pip/tests-integration/test_requirements.txt @@ -1,3 +1,6 @@ pytest==8.2.2 +qiskit>=1.1.1,<2.0.0 qirrunner==0.7.1 -pyqir==0.10.2 \ No newline at end of file +pyqir==0.10.2 +qiskit-aer==0.14.2 +qiskit_qasm3_import==0.5.0 diff --git a/resource_estimator/src/lib.rs b/resource_estimator/src/lib.rs index 95a0ed6936..1125ce4482 100644 --- a/resource_estimator/src/lib.rs +++ b/resource_estimator/src/lib.rs @@ -58,8 +58,7 @@ pub fn estimate_expr( let mut out = GenericReceiver::new(&mut stdout); interpreter .run_with_sim(&mut counter, &mut out, Some(expr)) - .map_err(|e| e.into_iter().map(Error::Interpreter).collect::>())? - .map_err(|e| vec![Error::Interpreter(e[0].clone())])?; + .map_err(|e| e.into_iter().map(Error::Interpreter).collect::>())?; estimate_physical_resources(counter.logical_resources(), params) .map_err(|e| vec![Error::Estimation(e)]) } diff --git a/samples/estimation/estimation-qiskit.ipynb b/samples/estimation/estimation-qiskit.ipynb new file mode 100644 index 0000000000..01d2a97e5c --- /dev/null +++ b/samples/estimation/estimation-qiskit.ipynb @@ -0,0 +1,623 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Getting Started with QDK Resource Estimation using Qiskit\n", + "\n", + "👋 Welcome to the QDK Resource Estimator using Qiskit. In this notebook we will\n", + "guide you how to estimate and analyze the physical resource estimates of a\n", + "quantum program targeted for execution based on the architecture design of a\n", + "fault-tolerant quantum computer. As a running example we are using a multiplier." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Implementing the algorithm" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As a first step, we will create a sample application which will be used throughout this Resource Estimation notebook. To start, we'll import some utilities from the `qiskit` Python package." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# These are the Python imports that we use in this Qiskit-based example\n", + "from qiskit.circuit.library import RGQFTMultiplier" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We are creating a quantum circuit for a multiplier based on the construction presented in [arXiv:1411.5949](https://arxiv.org/abs/1411.5949) which uses the Quantum Fourier Transform to implement arithmetic. You can adjust the size of the multiplier by changing the `bitwidth` variable. The circuit generation is wrapped in a function that can be called with the bitwidth of the multiplier. The circuit will have two input registers with that bitwidth, and one output register with the size of twice the bitwidth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def create_algorithm(bitwidth):\n", + " print(f\"[INFO] Create a QFT-based multiplier with bitwidth {bitwidth}\")\n", + "\n", + " circ = RGQFTMultiplier(num_state_qubits=bitwidth)\n", + "\n", + " return circ\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Estimating the algorithm\n", + "Next we will create an instance of our algorithm using the `create_algorithm` function. You can adjust the size of the multiplier by changing the `bitwidth` variable." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "bitwidth = 4\n", + "\n", + "circ = create_algorithm(bitwidth)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's now estimate the physical resources for this circuit using the default assumptions.\n", + "\n", + "The `esitmate` call is overloaded to accept a `QuantumCircuit`, so we can simply call it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qsharp.estimator import EstimatorParams\n", + "from qsharp import estimate\n", + "\n", + "params = EstimatorParams()\n", + "result = estimate(circ, params)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Alternatively, we can use the `ResourceEstimatorBackend` to perform the estimation as the existing backend does." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qsharp.interop import ResourceEstimatorBackend\n", + "from qsharp.estimator import EstimatorParams\n", + "\n", + "params = EstimatorParams()\n", + "backend = ResourceEstimatorBackend()\n", + "\n", + "job = backend.run(circ, params)\n", + "result = job.result()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The simplest way to inspect the results of the job is to output them to the notebook. This will output a table with the overall physical resource counts. You can further inspect more details about the resource estimates by collapsing various groups which have more information. For example, if you collapse the *Logical qubit parameters* group, you can see that the quantum error correction (QEC) code distance is 15. In the last group you can see the physical qubit properties that were assumed for this estimation. For example, we see that the time to perform a single-qubit measurement and a single-qubit gate are assumed to be 100 ns and 50 ns, respectively." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qsharp_widgets import EstimateDetails\n", + "\n", + "EstimateDetails(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The distribution of physical qubits used for the execution of the algorithm instructions and the supporting T factories can provide us valuable information to guide us in applying space and time optimizations. We can visualize this distribution to better understand the estimated space requirements for our algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "from qsharp_widgets import SpaceChart\n", + "\n", + "SpaceChart(result)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also programmatically access all the values that can be passed to the job execution and see which default values were assumed:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result.data()[\"jobParams\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We see that there are three input parameters that can be customized: `qubitParams`, `qecScheme`, and `errorBudget`.\n", + "\n", + "### Qubit parameters\n", + "\n", + "The first parameter `qubitParams` is used to specify qubit parameters. When\n", + "modeling the physical qubit abstractions, we distinguish between two different\n", + "physical instruction sets that are used to operate the qubits. The physical\n", + "instruction set can be either *gate-based* or *Majorana*. A gate-based\n", + "instruction set provides single-qubit measurement, single-qubit gates (incl. T\n", + " gates), and two-qubit gates. A Majorana instruction set provides a physical T\n", + " gate, single-qubit measurement and two-qubit joint measurement operations.\n", + "\n", + "Qubit parameters can be completely customized. Before we show this, we show hot\n", + "to choose from six pre-defined qubit parameters, four of which have gate-based\n", + "instruction sets and two with a Majorana instruction set. An overview of all\n", + "pre-defined qubit parameters is provided by the following table:\n", + "\n", + "| Pre-defined qubit parameters | Instruction set | References |\n", + "|------------------------------|-----------------|------------------------------------------------------------------------------------------------------------|\n", + "| `\"qubit_gate_ns_e3\"` | gate-based | [arXiv:2003.00024](https://arxiv.org/abs/2003.00024), [arXiv:2111.11937](https://arxiv.org/abs/2111.11937) |\n", + "| `\"qubit_gate_ns_e4\"` | gate-based | [arXiv:2003.00024](https://arxiv.org/abs/2003.00024), [arXiv:2111.11937](https://arxiv.org/abs/2111.11937) |\n", + "| `\"qubit_gate_us_e3\"` | gate-based | [arXiv:1701.04195](https://arxiv.org/abs/1701.04195) |\n", + "| `\"qubit_gate_us_e4\"` | gate-based | [arXiv:1701.04195](https://arxiv.org/abs/1701.04195) |\n", + "| `\"qubit_maj_ns_e4\"` | Majorana | [arXiv:1610.05289](https://arxiv.org/abs/1610.05289) |\n", + "| `\"qubit_maj_ns_e6\"` | Majorana | [arXiv:1610.05289](https://arxiv.org/abs/1610.05289) |\n", + "\n", + "Pre-defined qubit parameters can be selected by specifying the `name` field in\n", + "the `qubitParams`. If no value is provided, `\"qubit_gate_ns_e3\"` is chosen as\n", + "the default qubit parameters.\n", + "\n", + "Let's re-run resource estimation for our running example on the Majorana-based\n", + "qubit parameters `\"qubit_maj_ns_e6\"`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "qubitParams = {\n", + " \"name\": \"qubit_maj_ns_e6\"\n", + "}\n", + "\n", + "result = backend.run(circ, qubitParams).result()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let's inspect the physical counts programmatically. For example, we can show all physical resource estimates and their breakdown using the `physicalCounts` field in the result data. This will show the logical qubit error and logical T-state error rates required to match the error budget. By default runtimes are shown in nanoseconds." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result.data()[\"physicalCounts\"]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can also explore details about the T factory that was created to execute this algorithm." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "result.data()[\"tfactory\"]\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, we are using this data to produce some explanations of how the T factories produce the required T states." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "data = result.data()\n", + "tfactory = data[\"tfactory\"]\n", + "breakdown = data[\"physicalCounts\"][\"breakdown\"]\n", + "producedTstates = breakdown[\"numTfactories\"] * breakdown[\"numTfactoryRuns\"] * tfactory[\"numTstates\"]\n", + "\n", + "print(f\"\"\"A single T factory produces {tfactory[\"logicalErrorRate\"]:.2e} T states with an error rate of (required T state error rate is {breakdown[\"requiredLogicalTstateErrorRate\"]:.2e}).\"\"\")\n", + "print(f\"\"\"{breakdown[\"numTfactories\"]} copie(s) of a T factory are executed {breakdown[\"numTfactoryRuns\"]} time(s) to produce {producedTstates} T states ({breakdown[\"numTstates\"]} are required by the algorithm).\"\"\")\n", + "print(f\"\"\"A single T factory is composed of {tfactory[\"numRounds\"]} rounds of distillation:\"\"\")\n", + "for round in range(tfactory[\"numRounds\"]):\n", + " print(f\"\"\"- {tfactory[\"numUnitsPerRound\"][round]} {tfactory[\"unitNamePerRound\"][round]} unit(s)\"\"\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Custom qubit parameters must completely specify all required parameters. These are the values that are\n", + "considered when the `instructionSet` is `\"GateBased\"`.\n", + "\n", + "| Field (*required) | Description |\n", + "|---------------------------------|----------------------------------------------------------------------|\n", + "| `name` | Some descriptive name for the parameters |\n", + "| `oneQubitMeasurementTime`* | Operation time for single-qubit measurement ($t_{\\rm meas}$) in ns |\n", + "| `oneQubitGateTime`* | Operation time for single-qubit Clifford gate ($t_{\\rm gate}$) in ns |\n", + "| `twoQubitGateTime` | Operation time for two-qubit Clifford gate in ns |\n", + "| `tGateTime` | Operation time for single-qubit non-Clifford gate in ns |\n", + "| `oneQubitMeasurementErrorRate`* | Error rate for single-qubit measurement |\n", + "| `oneQubitGateErrorRate`* | Error rate for single-qubit Clifford gate ($p$) |\n", + "| `twoQubitGateErrorRate` | Error rate for two-qubit Clifford gate |\n", + "| `tGateErrorRate` | Error rate to prepare single-qubit non-Clifford state ($p_T$) |\n", + "\n", + "The values for `twoQubitGateTime` and `tGateTime` default to `oneQubitGateTime`\n", + "when not specified; the values for `twoQubitGateErrorRate` and `tGateErrorRate`\n", + "default to `oneQubitGateErrorRate` when not specified.\n", + "\n", + "A minimum template for qubit parameters based on a gate-based instruction set\n", + "with all required values is:\n", + "\n", + "```json\n", + "{\n", + " \"qubitParams\": {\n", + " \"instructionSet\": \"GateBased\",\n", + " \"oneQubitMeasurementTime\":