diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 15d2604..d0c1974 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -90,13 +90,14 @@ jobs: # empty bin/example harnesses, which is especially expensive with LLVM 21. run: cargo nextest run --lib --all-features ${{ matrix.nextest_extra_args }} - uses: astral-sh/setup-uv@v7 - if: runner.os != 'Linux' - name: Rust API example smoke if: runner.os != 'Linux' run: cargo run --example rust_api --all-features --profile test - name: Python API example smoke if: runner.os != 'Linux' run: uv run examples/python_api.py + - name: Python main.py regression smoke + run: uv run -- python tests/test_main.py lint: runs-on: ubuntu-latest @@ -146,10 +147,8 @@ jobs: cache-on-failure: true cache-workspace-crates: true cache-targets: true - - name: Run cargo fmt (check if all code is rustfmt-ed) - run: cargo fmt --all --check - - name: Run cargo clippy (deny warnings) - run: cargo clippy --locked --all-targets --all-features -- -D warnings + - name: Run pre-commit lint + run: uvx prek run --all-files - name: Verify Python stubs are up to date if: github.event_name == 'pull_request' && steps.stub_changes.outputs.lib_rs == 'true' run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 54942dc..2c5d79e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,13 +14,13 @@ repos: - id: no-commit-to-branch - repo: https://github.com/crate-ci/typos - rev: v1.45.1 + rev: v1.45.2 hooks: - id: typos args: [] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.15.11 + rev: v0.15.12 hooks: - id: ruff-check args: [--fix, --exit-non-zero-on-fix] diff --git a/Cargo.lock b/Cargo.lock index 029bd93..869ee40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -131,9 +131,9 @@ checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "shlex", @@ -528,9 +528,9 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jiff" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a3546dc96b6d42c5f24902af9e2538e82e39ad350b0c766eb3fbf2d8f3d8359" +checksum = "f00b5dbd620d61dfdcb6007c9c1f6054ebd75319f163d886a9055cec1155073d" dependencies = [ "jiff-static", "log", @@ -541,9 +541,9 @@ dependencies = [ [[package]] name = "jiff-static" -version = "0.2.23" +version = "0.2.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a8c8b344124222efd714b73bb41f8b5120b27a7cc1c75593a6ff768d9d05aa4" +checksum = "e000de030ff8022ea1da3f466fbb0f3a809f5e51ed31f6dd931c35181ad8e6d7" dependencies = [ "proc-macro2", "quote", @@ -552,10 +552,12 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.95" +version = "0.3.97" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca" +checksum = "a1840c94c045fbcf8ba2812c95db44499f7c64910a912551aaaa541decebcacf" dependencies = [ + "cfg-if", + "futures-util", "once_cell", "wasm-bindgen", ] @@ -580,9 +582,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "linux-raw-sys" @@ -909,8 +911,8 @@ dependencies = [ "pyo3-stub-gen", "rstest", "tempfile", - "wasm-encoder 0.247.0", - "wasmparser 0.247.0", + "wasm-encoder 0.248.0", + "wasmparser 0.248.0", ] [[package]] @@ -1537,9 +1539,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89" +checksum = "df52b6d9b87e0c74c9edfa1eb2d9bf85e5d63515474513aa50fa181b3c4f5db1" dependencies = [ "cfg-if", "once_cell", @@ -1550,9 +1552,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed" +checksum = "78b1041f495fb322e64aca85f5756b2172e35cd459376e67f2a6c9dffcedb103" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1560,9 +1562,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904" +checksum = "9dcd0ff20416988a18ac686d4d4d0f6aae9ebf08a389ff5d29012b05af2a1b41" dependencies = [ "bumpalo", "proc-macro2", @@ -1573,9 +1575,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.118" +version = "0.2.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129" +checksum = "49757b3c82ebf16c57d69365a142940b384176c24df52a087fb748e2085359ea" dependencies = [ "unicode-ident", ] @@ -1592,12 +1594,12 @@ dependencies = [ [[package]] name = "wasm-encoder" -version = "0.247.0" +version = "0.248.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "30b6733b8b91d010a6ac5b0fb237dc46a19650bc4c67db66857e2e787d437204" +checksum = "ac92cf547bc18d27ecc521015c08c353b4f18b84ab388bb6d1b6b682c620d9b6" dependencies = [ "leb128fmt", - "wasmparser 0.247.0", + "wasmparser 0.248.0", ] [[package]] @@ -1626,9 +1628,9 @@ dependencies = [ [[package]] name = "wasmparser" -version = "0.247.0" +version = "0.248.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e6fb4c2bee46c5ea4d40f8cdb5c131725cd976718ec56f1c8e82fbde5fa2a80" +checksum = "aa4439c5eee9df71ee0c6efb37f63b1fcb1fec38f85f5142c54e7ed05d33091a" dependencies = [ "bitflags", "hashbrown 0.17.0", diff --git a/Cargo.toml b/Cargo.toml index 3db1a64..5fc768f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,7 +43,7 @@ pyo3-stub-gen = { version = "0.22", optional = true, features = [ "infer_signature", ], default-features = false } rstest = "0.26" -wasmparser = { version = "0.247", optional = true } +wasmparser = { version = "0.248", optional = true } [features] default = ["python"] @@ -57,4 +57,4 @@ crate-type = ["cdylib", "rlib"] insta = { version = "1", features = ["filters"] } proptest = "1" tempfile = "3" -wasm-encoder = "0.247" +wasm-encoder = "0.248" diff --git a/Makefile b/Makefile index 21032ef..10e678d 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ lint: .PHONY: test test: cargo nextest run --all-targets --all-features + $(PYTHON) tests/test_main.py .PHONY: mutants mutants: diff --git a/fuzz/fuzz_targets/mutated_fixture_contracts.rs b/fuzz/fuzz_targets/mutated_fixture_contracts.rs index 3606e44..d2d3756 100644 --- a/fuzz/fuzz_targets/mutated_fixture_contracts.rs +++ b/fuzz/fuzz_targets/mutated_fixture_contracts.rs @@ -9,10 +9,18 @@ const FIXTURES: &[&str] = &[ include_str!("../../tests/data/adaptive.ll"), include_str!("../../tests/data/qir2_base.ll"), include_str!("../../tests/data/qir2_adaptive.ll"), + include_str!("../../tests/data/dynamic_qubit_alloc.ll"), + include_str!("../../tests/data/dynamic_qubit_alloc_checked.ll"), + include_str!("../../tests/data/dynamic_qubit_array_checked.ll"), + include_str!("../../tests/data/dynamic_qubit_array_ssa.ll"), + include_str!("../../tests/data/dynamic_result_alloc.ll"), + include_str!("../../tests/data/dynamic_result_array.ll"), + include_str!("../../tests/data/dynamic_result_mixed_array_output.ll"), ]; const PROFILE_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz_"; const SCHEMA_ALPHABET: &[u8] = b"abcdefghijklmnopqrstuvwxyz_"; const NUMERIC_ALPHABET: &[u8] = b"0123456789"; +const BOOL_ALPHABET: &[u8] = b"falserut"; #[derive(Clone)] struct SpanSpec { @@ -41,6 +49,33 @@ fn find_module_flag_value_span(text: &str, flag_name: &str) -> Option Option> { + let needle = format!("\"{attr_name}\"=\""); + let start = text.find(&needle)? + needle.len(); + let end = text[start..] + .find(|ch: char| ch != 't' && ch != 'r' && ch != 'u' && ch != 'e' && ch != 'f' && ch != 'a' && ch != 'l' && ch != 's') + .map_or(text.len(), |idx| start + idx); + Some(start..end) +} + +fn find_boolean_module_flag_value_span(text: &str, flag_name: &str) -> Option> { + let needle = format!("!\"{flag_name}\", i1 "); + let start = text.find(&needle)? + needle.len(); + let end = text[start..] + .find(|ch: char| { + ch != 't' + && ch != 'r' + && ch != 'u' + && ch != 'e' + && ch != 'f' + && ch != 'a' + && ch != 'l' + && ch != 's' + }) + .map_or(text.len(), |idx| start + idx); + Some(start..end) +} + fn collect_spans(text: &str) -> Vec { let mut spans = Vec::new(); @@ -71,6 +106,21 @@ fn collect_spans(text: &str) -> Vec { }); } + for flag_name in [ + "dynamic_qubit_management", + "dynamic_result_management", + "arrays", + ] { + if let Some(range) = find_boolean_module_flag_value_span(text, flag_name) + .or_else(|| find_boolean_attr_value_span(text, flag_name)) + { + spans.push(SpanSpec { + range, + alphabet: BOOL_ALPHABET, + }); + } + } + spans } diff --git a/pyproject.toml b/pyproject.toml index 5d3a1b3..c04f937 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,9 @@ auditwheel = "repair" [dependency-groups] dev = [ "maturin~=1.13.1", - "qir-formatter~=0.1", + "qir-formatter~=0.2", "rich~=14.0", - "selene-sim~=0.2.11", + "selene-sim~=0.2.15", ] [tool.uv] @@ -42,9 +42,6 @@ prerelease = "allow" # Rebuild package when any rust files change cache-keys = [{ file = "src/**/*.rs" }] -[tool.uv.sources] -# selene-sim = { path = "../selene" } - [tool.deptry] known_first_party = ["qir_qis"] diff --git a/qtm-qir-reference.md b/qtm-qir-reference.md index 227719c..eb045f8 100644 --- a/qtm-qir-reference.md +++ b/qtm-qir-reference.md @@ -185,6 +185,37 @@ declare void @__quantum__rt__int_record_output(i64, i8*) declare void @__quantum__rt__double_record_output(double, i8*) ``` +### Dynamic Allocation and Arrays + +`qir-qis` accepts the optional Adaptive Profile capability flags: + +- `"dynamic_qubit_management"` +- `"dynamic_result_management"` +- `"arrays"` + +When enabled, the following QIR runtime APIs are accepted and lowered by the compiler: + +```llvm +declare ptr @__quantum__rt__qubit_allocate(ptr %out_err) +declare void @__quantum__rt__qubit_release(ptr %qubit) +declare ptr @__quantum__rt__result_allocate(ptr %out_err) +declare void @__quantum__rt__result_release(ptr %result) + +declare void @__quantum__rt__qubit_array_allocate(i64 %N, ptr %array, ptr %out_err) +declare void @__quantum__rt__qubit_array_release(i64 %N, ptr %array) +declare void @__quantum__rt__result_array_allocate(i64 %N, ptr %array, ptr %out_err) +declare void @__quantum__rt__result_array_release(i64 %N, ptr %array) +declare void @__quantum__rt__result_array_record_output(i64 %N, ptr %result_array, ptr %tag) +``` + +Support boundary: + +- Fixed-size LLVM pointer arrays are supported via `alloca`, `getelementptr`, `load`, `store`, `extractvalue`, and `insertvalue`. +- `required_num_qubits` is optional when `dynamic_qubit_management=true`. +- `required_num_results` is optional when `dynamic_result_management=true`. +- `__quantum__rt__result_array_record_output` currently requires an array length that fits in `i32` for the downstream `print_bool_arr(...)` ABI. +- Runtime-sized classical buffers remain out of scope. + ## Platform Utilities These QIR functions provide additional runtime capabilities. diff --git a/src/convert.rs b/src/convert.rs index 41da063..f14bca1 100644 --- a/src/convert.rs +++ b/src/convert.rs @@ -749,12 +749,9 @@ pub fn replace_rxy_call<'a>( ctx: &'a Context, module: &Module<'a>, old_call: InstructionValue<'a>, + dynamic_qubit_management: bool, ) -> Result<(), String> { - let get_idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; - - let _ = replace_native_call( + replace_native_call( ctx, module, old_call, @@ -766,19 +763,18 @@ pub fn replace_rxy_call<'a>( ], |args, builder| { let qubit_ptr = args[2].into_pointer_value(); - let idx_call = builder - .build_call(get_idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let handle = match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!("{LOAD_QUBIT_FN} did not return a basic value")); - } - }; + let handle = get_native_qubit_handle( + ctx, + module, + builder, + qubit_ptr, + dynamic_qubit_management, + "qbit", + )?; Ok(vec![handle, args[0], args[1]]) }, - ); - Ok(()) + ) + .map_err(|e| e.to_string()) } /// Replaces a call to `__quantum__qis__rz__body` with a call to `___rz`. @@ -789,11 +785,9 @@ pub fn replace_rz_call<'a>( ctx: &'a Context, module: &Module<'a>, old_call: InstructionValue<'a>, + dynamic_qubit_management: bool, ) -> Result<(), String> { - let get_idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; - let _ = replace_native_call( + replace_native_call( ctx, module, old_call, @@ -801,19 +795,18 @@ pub fn replace_rz_call<'a>( &[ctx.i64_type().into(), ctx.f64_type().into()], |args, builder| { let qubit_ptr = args[1].into_pointer_value(); - let idx_call = builder - .build_call(get_idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let handle = match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!("{LOAD_QUBIT_FN} did not return a basic value")); - } - }; + let handle = get_native_qubit_handle( + ctx, + module, + builder, + qubit_ptr, + dynamic_qubit_management, + "qbit", + )?; Ok(vec![handle, args[0]]) }, - ); - Ok(()) + ) + .map_err(|e| e.to_string()) } /// Replaces a call to `__quantum__qis__rzz__body` with a call to `___rzz`. @@ -824,11 +817,9 @@ pub fn replace_rzz_call<'a>( ctx: &'a Context, module: &Module<'a>, old_call: InstructionValue<'a>, + dynamic_qubit_management: bool, ) -> Result<(), String> { - let get_idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; - let _ = replace_native_call( + replace_native_call( ctx, module, old_call, @@ -839,30 +830,55 @@ pub fn replace_rzz_call<'a>( ctx.f64_type().into(), // angle ], |args, builder| { - let qubit_ptr = args[1].into_pointer_value(); - let idx_call = builder - .build_call(get_idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let q1 = match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!("{LOAD_QUBIT_FN} did not return a basic value")); - } - }; - let qubit_ptr = args[2].into_pointer_value(); - let idx_call = builder - .build_call(get_idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let q2 = match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!("{LOAD_QUBIT_FN} did not return a basic value")); - } - }; + let q1 = get_native_qubit_handle( + ctx, + module, + builder, + args[1].into_pointer_value(), + dynamic_qubit_management, + "qbit1", + )?; + let q2 = get_native_qubit_handle( + ctx, + module, + builder, + args[2].into_pointer_value(), + dynamic_qubit_management, + "qbit2", + )?; Ok(vec![q1, q2, args[0]]) }, - ); - Ok(()) + ) + .map_err(|e| e.to_string()) +} + +fn get_native_qubit_handle<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + qubit_ptr: inkwell::values::PointerValue<'ctx>, + dynamic_qubit_management: bool, + name: &str, +) -> Result, String> { + if dynamic_qubit_management { + return builder + .build_ptr_to_int(qubit_ptr, ctx.i64_type(), name) + .map(BasicValueEnum::from) + .map_err(|e| format!("Failed to convert qubit pointer to handle: {e}")); + } + + let get_idx_fn = module + .get_function(LOAD_QUBIT_FN) + .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; + let idx_call = builder + .build_call(get_idx_fn, &[qubit_ptr.into()], name) + .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; + match idx_call.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => Ok(bv), + inkwell::values::ValueKind::Instruction(_) => { + Err(format!("{LOAD_QUBIT_FN} did not return a basic value")) + } + } } /// Extracts the qubit index from an `IntToPtr` conversion string. @@ -1094,6 +1110,7 @@ pub fn process_ir_defined_q_fns<'a>( ctx: &'a Context, module: &Module<'a>, entry_fn: FunctionValue, + dynamic_qubit_management: bool, ) -> Result<(), String> { for defined_fn in module .get_functions() @@ -1107,7 +1124,14 @@ pub fn process_ir_defined_q_fns<'a>( .and_then(|f| f.get_name().to_str().ok().map(ToOwned::to_owned)) .ok_or_else(|| "Function call must have a name".to_string())?; - native_qir_to_qis_call(ctx, module, instr, &fn_name, defined_fn)?; + native_qir_to_qis_call( + ctx, + module, + instr, + &fn_name, + defined_fn, + dynamic_qubit_management, + )?; } } } @@ -1158,13 +1182,31 @@ fn native_qir_to_qis_call<'a>( instr: InstructionValue<'a>, fn_name: &str, defined_fn: FunctionValue, + dynamic_qubit_management: bool, ) -> Result<(), String> { match fn_name { - "__quantum__qis__rxy__body" => replace_rxy_call(ctx, module, instr)?, - "__quantum__qis__rzz__body" => replace_rzz_call(ctx, module, instr)?, - "__quantum__qis__rz__body" => replace_rz_call(ctx, module, instr)?, - "___qalloc" | "___reset" | "panic" => { - if defined_fn.get_name().to_str().ok() != Some(INIT_QARRAY_FN) { + "__quantum__qis__rxy__body" => { + replace_rxy_call(ctx, module, instr, dynamic_qubit_management)?; + } + "__quantum__qis__rzz__body" => { + replace_rzz_call(ctx, module, instr, dynamic_qubit_management)?; + } + "__quantum__qis__rz__body" => { + replace_rz_call(ctx, module, instr, dynamic_qubit_management)?; + } + "___qalloc" + | "___qfree" + | "___reset" + | "panic" + | "___read_future_bool" + | "___dec_future_refcount" + | "print_int" + | "print_bool" + | "print_bool_arr" => { + let defined_name = defined_fn.get_name().to_str().ok(); + if defined_name != Some(INIT_QARRAY_FN) + && !defined_name.is_some_and(|name| name.starts_with("qir_qis.")) + { log::error!( "Unexpected call to internal function: {fn_name} in function {}", defined_fn.get_name().to_str().unwrap_or("unknown") @@ -1559,7 +1601,7 @@ mod tests { create_qubit_array(&context, &module, func).unwrap(); let instr = call.try_as_basic_value().unwrap_instruction(); - replace_rz_call(&context, &module, instr).unwrap(); + replace_rz_call(&context, &module, instr, false).unwrap(); let rz = module.get_function("___rz"); assert!(rz.is_some()); @@ -2180,11 +2222,39 @@ entry: call.try_as_basic_value().unwrap_instruction(), "__quantum__qis__mystery__body", defined_fn, + false, ) .expect_err("unknown external declaration should fail"); assert!(err.contains("Unsupported function call")); } + #[test] + fn test_native_qir_to_qis_call_rejects_internal_helper_in_non_internal_function() { + let context = Context::create(); + let module = context.create_module("test"); + let builder = context.create_builder(); + let fn_type = context.void_type().fn_type(&[], false); + let defined_fn = module.add_function("defined_fn", fn_type, None); + let entry = context.append_basic_block(defined_fn, "entry"); + builder.position_at_end(entry); + + let internal_decl = module.add_function("___qalloc", fn_type, None); + let call = builder + .build_call(internal_decl, &[], "internal_call") + .expect("call should build"); + + let err = native_qir_to_qis_call( + &context, + &module, + call.try_as_basic_value().unwrap_instruction(), + "___qalloc", + defined_fn, + false, + ) + .expect_err("non-internal helper should not call compiler-internal functions"); + assert!(err.contains("Unexpected call to internal function")); + } + #[test] fn test_process_ir_defined_q_fns_skips_entry_function() { let context = Context::create(); @@ -2201,7 +2271,7 @@ entry: .expect("call should build"); let _ = builder.build_return(None); - process_ir_defined_q_fns(&context, &module, entry_fn) + process_ir_defined_q_fns(&context, &module, entry_fn, false) .expect("entry function should be excluded from IR-defined helper processing"); } @@ -2269,7 +2339,7 @@ entry: assert_eq!(label, ""); } - macro_rules! snapshot_cases { + macro_rules! conversion_cases { ($item:item) => { #[rstest] // Base profile tests @@ -2286,6 +2356,13 @@ entry: #[case("tests/data/adaptive_iter_fn.ll")] #[case("tests/data/adaptive_cond_loop.ll")] #[case("tests/data/adaptive_multi_ret.ll")] + #[case("tests/data/dynamic_qubit_alloc.ll")] + #[case("tests/data/dynamic_qubit_alloc_checked.ll")] + #[case("tests/data/dynamic_qubit_array_checked.ll")] + #[case("tests/data/dynamic_qubit_array_ssa.ll")] + #[case("tests/data/dynamic_result_alloc.ll")] + #[case("tests/data/dynamic_result_array.ll")] + #[case("tests/data/dynamic_result_mixed_array_output.ll")] // QIR 2.0 (opaque pointers) tests #[case("tests/data/qir2_base.ll")] #[case("tests/data/qir2_adaptive.ll")] @@ -2299,7 +2376,7 @@ entry: } #[cfg(not(windows))] - snapshot_cases! { + conversion_cases! { fn test_snapshot_conversion(#[case] llpath: &str) { use insta::Settings; @@ -2328,10 +2405,14 @@ entry: }} #[cfg(windows)] - snapshot_cases! { + conversion_cases! { // Windows runs this as a smoke test instead of snapshot matching because // cross-target (`aarch64`) optimized codegen in CI has shown backend - // instability and non-deterministic output differences. + // instability and non-deterministic output differences. Re-checked on + // Windows Arm64 on March 23, 2026: the new dynamic-allocation fixtures are + // stable under this `-O0`/`native` conversion+parse path, and the remaining + // instability is in the broader optimized Windows codegen path rather than + // in dynamic qubit/result lowering specifically. fn test_snapshot_conversion_windows_smoke(#[case] llpath: &str) { let ll_path = Path::new(llpath); let qir_bytes = get_qir_bytes(ll_path); diff --git a/src/lib.rs b/src/lib.rs index 09d4d07..07ee02a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -61,6 +61,8 @@ pub const DEFAULT_TARGET: &str = "native"; pub const DEFAULT_TARGET: &str = "aarch64"; mod aux { + #![allow(clippy::expect_used)] + use std::collections::{BTreeMap, BTreeSet, HashMap}; use crate::{ @@ -78,12 +80,13 @@ mod aux { use inkwell::{ AddressSpace, attributes::AttributeLoc, + basic_block::BasicBlock, context::Context, - module::Module, - types::{ArrayType, BasicTypeEnum}, + module::{Linkage, Module}, + types::{ArrayType, BasicMetadataTypeEnum, BasicTypeEnum, FunctionType}, values::{ AnyValue, BasicMetadataValueEnum, BasicValue, BasicValueEnum, CallSiteValue, - FunctionValue, PointerValue, + FunctionValue, InstructionOpcode, PointerValue, }, }; @@ -118,7 +121,7 @@ mod aux { // Note: barrier instructions with arbitrary arity are validated separately ]; - static ALLOWED_RT_FNS: [&str; 8] = [ + static BASE_ALLOWED_RT_FNS: [&str; 8] = [ "__quantum__rt__read_result", "__quantum__rt__initialize", "__quantum__rt__result_record_output", @@ -129,6 +132,98 @@ mod aux { "__quantum__rt__int_record_output", ]; + #[derive(Clone, Copy, Debug, Default)] + pub struct CapabilityFlags { + pub dynamic_qubit_management: bool, + pub dynamic_result_management: bool, + pub arrays: bool, + } + + fn is_capability_gated_rt_function(fn_name: &str) -> bool { + matches!( + fn_name, + "__quantum__rt__qubit_allocate" + | "__quantum__rt__qubit_release" + | "__quantum__rt__result_allocate" + | "__quantum__rt__result_release" + | "__quantum__rt__qubit_array_allocate" + | "__quantum__rt__qubit_array_release" + | "__quantum__rt__result_array_allocate" + | "__quantum__rt__result_array_release" + | "__quantum__rt__result_array_record_output" + ) + } + + fn is_i64_type(type_: BasicMetadataTypeEnum<'_>) -> bool { + type_.is_int_type() && type_.into_int_type().get_bit_width() == 64 + } + + fn is_ptr_type(type_: BasicMetadataTypeEnum<'_>) -> bool { + type_.is_pointer_type() + } + + fn is_void_return(fn_type: FunctionType<'_>) -> bool { + fn_type.get_return_type().is_none() + } + + fn is_ptr_return(fn_type: FunctionType<'_>) -> bool { + fn_type + .get_return_type() + .is_some_and(BasicTypeEnum::is_pointer_type) + } + + fn validate_dynamic_rt_signature( + fn_name: &str, + fn_type: FunctionType<'_>, + ) -> Result<(), String> { + let params = fn_type.get_param_types(); + let valid = match fn_name { + "__quantum__rt__qubit_allocate" | "__quantum__rt__result_allocate" => { + is_ptr_return(fn_type) && params.len() == 1 && is_ptr_type(params[0]) + } + "__quantum__rt__qubit_release" | "__quantum__rt__result_release" => { + is_void_return(fn_type) && params.len() == 1 && is_ptr_type(params[0]) + } + "__quantum__rt__qubit_array_allocate" + | "__quantum__rt__result_array_allocate" + | "__quantum__rt__result_array_record_output" => { + is_void_return(fn_type) + && params.len() == 3 + && is_i64_type(params[0]) + && is_ptr_type(params[1]) + && is_ptr_type(params[2]) + } + "__quantum__rt__qubit_array_release" | "__quantum__rt__result_array_release" => { + is_void_return(fn_type) + && params.len() == 2 + && is_i64_type(params[0]) + && is_ptr_type(params[1]) + } + _ => true, + }; + + if valid { + Ok(()) + } else { + Err(format!("Malformed QIR RT function declaration: {fn_name}")) + } + } + + pub fn get_capability_flags(module: &Module) -> CapabilityFlags { + let module_flags = collect_module_flags(module); + CapabilityFlags { + dynamic_qubit_management: module_flag_is_enabled( + &module_flags, + "dynamic_qubit_management", + ), + dynamic_result_management: module_flag_is_enabled( + &module_flags, + "dynamic_result_management", + ), + arrays: module_flag_is_enabled(&module_flags, "arrays"), + } + } + #[cfg(feature = "wasm")] static ALLOWED_QTM_FNS: [&str; 8] = [ "___get_current_shot", @@ -187,6 +282,12 @@ mod aux { continue; } let fn_name = fun.get_name().to_str().unwrap_or(""); + if fn_name.starts_with("qir_qis.") { + errors.push(format!( + "Input QIR must not define internal helper function: {fn_name}" + )); + continue; + } if fn_name.starts_with("__quantum__qis__") { // Check for barrier instructions with arbitrary arity (barrier1, barrier2, ...) let is_barrier = if fn_name.starts_with("__quantum__qis__barrier") @@ -213,8 +314,14 @@ mod aux { } continue; } else if fn_name.starts_with("__quantum__rt__") { - if !ALLOWED_RT_FNS.contains(&fn_name) { + if !BASE_ALLOWED_RT_FNS.contains(&fn_name) + && !is_capability_gated_rt_function(fn_name) + { errors.push(format!("Unsupported QIR RT function: {fn_name}")); + } else if is_capability_gated_rt_function(fn_name) + && let Err(err) = validate_dynamic_rt_signature(fn_name, fun.get_type()) + { + errors.push(err); } continue; } else if fn_name.starts_with("___") { @@ -339,15 +446,16 @@ mod aux { validate_exact_module_flag( &module_flags, "dynamic_qubit_management", - &["i1 false"], + &["i1 false", "i1 true"], errors, ); validate_exact_module_flag( &module_flags, "dynamic_result_management", - &["i1 false"], + &["i1 false", "i1 true"], errors, ); + validate_optional_module_flag(&module_flags, "arrays", &["i1 false", "i1 true"], errors); } pub struct ModuleFlags { @@ -433,6 +541,39 @@ mod aux { } } + fn module_flag_is_enabled(module_flags: &ModuleFlags, flag_name: &str) -> bool { + module_flags + .get(flag_name) + .is_some_and(|values| values.iter().any(|value| value == "i1 true")) + } + + fn validate_optional_module_flag( + module_flags: &ModuleFlags, + flag_name: &str, + expected_values: &[&str], + errors: &mut Vec, + ) { + let Some(actual_values) = module_flags.get(flag_name) else { + if module_flags.is_malformed(flag_name) { + errors.push(format!("Missing or unsupported module flag: {flag_name}")); + } + return; + }; + + if actual_values + .iter() + .any(|actual| expected_values.contains(&actual.as_str())) + { + return; + } + + let expected = if expected_values.len() == 1 { + expected_values[0].to_string() + } else { + format!("one of {}", expected_values.join(", ")) + }; + errors.push(format!("Unsupported {flag_name}: expected {expected}")); + } fn validate_exact_module_flag( module_flags: &ModuleFlags, flag_name: &str, @@ -463,17 +604,249 @@ mod aux { errors.push(format!("Unsupported {flag_name}: expected {expected}")); } - #[allow(dead_code)] - struct ProcessCallArgs<'a, 'ctx> { + fn get_fixed_pointer_array_len( + array_ptr: PointerValue<'_>, + opname: &str, + ) -> Result { + let array_backing_error = + || format!("{opname} requires a fixed-size backing array allocated as [N x ptr]"); + let Some(instr) = array_ptr.as_instruction_value() else { + return Err(array_backing_error()); + }; + let opcode = instr.get_opcode(); + + if opcode == InstructionOpcode::Alloca { + let allocated_type = instr + .get_allocated_type() + .map_err(|_| array_backing_error())?; + let BasicTypeEnum::ArrayType(array_type) = allocated_type else { + return Err(array_backing_error()); + }; + if !matches!(array_type.get_element_type(), BasicTypeEnum::PointerType(_)) { + return Err(array_backing_error()); + } + return Ok(u64::from(array_type.len())); + } + + if opcode == InstructionOpcode::BitCast || opcode == InstructionOpcode::AddrSpaceCast { + return instr + .get_operand(0) + .and_then(inkwell::values::Operand::value) + .map(BasicValueEnum::into_pointer_value) + .ok_or_else(array_backing_error) + .and_then(|backing_ptr| get_fixed_pointer_array_len(backing_ptr, opname)); + } + + if opcode == InstructionOpcode::GetElementPtr { + for operand_idx in 1..instr.get_num_operands() { + let Some(operand) = instr.get_operand(operand_idx) else { + return Err(array_backing_error()); + }; + let inkwell::values::Operand::Value(value) = operand else { + return Err(array_backing_error()); + }; + let idx = value + .into_int_value() + .get_zero_extended_constant() + .ok_or_else(array_backing_error)?; + if idx != 0 { + return Err(array_backing_error()); + } + } + + return instr + .get_operand(0) + .and_then(inkwell::values::Operand::value) + .map(BasicValueEnum::into_pointer_value) + .ok_or_else(array_backing_error) + .and_then(|backing_ptr| get_fixed_pointer_array_len(backing_ptr, opname)); + } + + Err(array_backing_error()) + } + + pub fn validate_dynamic_array_allocation_backing(module: &Module, errors: &mut Vec) { + for fun in module.get_functions() { + for bb in fun.get_basic_blocks() { + for instr in bb.get_instructions() { + let Ok(call) = CallSiteValue::try_from(instr) else { + continue; + }; + let Some(callee) = call.get_called_fn_value() else { + continue; + }; + let callee_global = callee.as_global_value(); + let callee_name = callee_global.get_name(); + let Some(fn_name) = callee_name.to_str().ok() else { + continue; + }; + if !matches!( + fn_name, + "__quantum__rt__qubit_array_allocate" + | "__quantum__rt__qubit_array_release" + | "__quantum__rt__result_array_allocate" + | "__quantum__rt__result_array_release" + | "__quantum__rt__result_array_record_output" + ) { + continue; + } + + let call_args: Vec = match extract_operands(&instr) { + Ok(args) => args, + Err(err) => { + errors.push(format!("Failed to inspect {fn_name} operands: {err}")); + continue; + } + }; + let requested_len = match extract_const_len(call_args[0], fn_name) { + Ok(len) => len, + Err(err) => { + errors.push(err); + continue; + } + }; + let backing_len = match get_fixed_pointer_array_len( + call_args[1].into_pointer_value(), + fn_name, + ) { + Ok(len) => len, + Err(err) => { + errors.push(err); + continue; + } + }; + if requested_len != backing_len { + errors.push(format!( + "{fn_name} requires a fixed-size backing array whose requested length {requested_len} does not match backing array length {backing_len}" + )); + } + if fn_name == "__quantum__rt__result_array_record_output" + && requested_len > i32::MAX as u64 + { + errors.push(format!( + "{fn_name} requires an array length that fits in i32 for RESULT_ARRAY output" + )); + } + } + } + } + } + + pub fn validate_capability_usage( + module: &Module, + flags: CapabilityFlags, + errors: &mut Vec, + ) { + for fun in module.get_functions() { + for bb in fun.get_basic_blocks() { + for instr in bb.get_instructions() { + let Ok(call) = CallSiteValue::try_from(instr) else { + continue; + }; + let Some(callee) = call.get_called_fn_value() else { + continue; + }; + let callee_global = callee.as_global_value(); + let callee_name = callee_global.get_name(); + let Some(fn_name) = callee_name.to_str().ok() else { + continue; + }; + + match fn_name { + "__quantum__rt__qubit_array_allocate" + | "__quantum__rt__qubit_array_release" + if !flags.arrays || !flags.dynamic_qubit_management => + { + errors.push(format!( + "{fn_name} requires both `arrays=true` and `dynamic_qubit_management=true`" + )); + } + "__quantum__rt__result_array_allocate" + | "__quantum__rt__result_array_release" + | "__quantum__rt__result_array_record_output" + if !flags.arrays || !flags.dynamic_result_management => + { + errors.push(format!( + "{fn_name} requires both `arrays=true` and `dynamic_result_management=true`" + )); + } + "__quantum__rt__qubit_allocate" | "__quantum__rt__qubit_release" + if !flags.dynamic_qubit_management => + { + errors.push(format!( + "{fn_name} requires `dynamic_qubit_management=true`" + )); + } + "__quantum__rt__result_allocate" | "__quantum__rt__result_release" + if !flags.dynamic_result_management => + { + errors.push(format!( + "{fn_name} requires `dynamic_result_management=true`" + )); + } + _ => {} + } + } + } + } + } + + pub fn validate_dynamic_result_allocation_placement( + module: &Module, + entry_fn: FunctionValue, + errors: &mut Vec, + ) { + for fun in module.get_functions() { + let allowed_block = if fun == entry_fn { + fun.get_first_basic_block() + } else { + None + }; + + for bb in fun.get_basic_blocks() { + for instr in bb.get_instructions() { + let Ok(call) = CallSiteValue::try_from(instr) else { + continue; + }; + let Some(callee) = call.get_called_fn_value() else { + continue; + }; + let callee_global = callee.as_global_value(); + let callee_name = callee_global.get_name(); + let Some(fn_name) = callee_name.to_str().ok() else { + continue; + }; + if matches!( + fn_name, + "__quantum__rt__result_allocate" | "__quantum__rt__result_array_allocate" + ) && Some(bb) != allowed_block + { + errors.push(format!( + "{fn_name} is only supported in the entry block because dynamic result slots are lowered to stack storage" + )); + } + } + } + } + } + + // SAFETY: `ProcessCallArgs` is created and consumed synchronously within a single + // `process_call_instruction` invocation. The raw pointers below point at + // stack-owned state from `process_entry_function` that outlives the handler + // call, and handlers never persist those pointers beyond the call. + struct ProcessCallArgs<'ctx> { ctx: &'ctx Context, - module: &'a Module<'ctx>, - instr: &'a inkwell::values::InstructionValue<'ctx>, - fn_name: &'a str, - wasm_fns: &'a BTreeMap, - qubit_array: PointerValue<'ctx>, - qubit_array_type: ArrayType<'ctx>, - global_mapping: &'a mut HashMap>, - result_ssa: &'a mut [Option<(BasicValueEnum<'ctx>, Option>)>], + module: *const Module<'ctx>, + instr: inkwell::values::InstructionValue<'ctx>, + fn_name: String, + // Reserved for downstream passthrough compatibility. + #[allow(dead_code)] + wasm_fns: *const BTreeMap, + qubit_array: Option>, + qubit_array_type: Option>, + capability_flags: CapabilityFlags, + global_mapping: *mut HashMap>, + result_ssa: *mut Vec, Option>)>>, } /// Primary translation loop over the entry function for translation to QIS. @@ -482,16 +855,27 @@ mod aux { module: &Module<'ctx>, entry_fn: FunctionValue<'ctx>, wasm_fns: &BTreeMap, - qubit_array: PointerValue<'ctx>, + qubit_array: Option>, + capability_flags: CapabilityFlags, ) -> Result<(), String> { let mut global_mapping = convert_globals(ctx, module)?; if global_mapping.is_empty() { log::warn!("No globals found in QIR module"); } - let mut result_ssa = get_result_vars(entry_fn)?; - let required_num_qubits = get_required_num_qubits_strict(entry_fn)?; - let qubit_array_type = ctx.i64_type().array_type(required_num_qubits); + let mut result_ssa = if capability_flags.dynamic_result_management { + Vec::new() + } else { + get_result_vars(entry_fn)? + }; + let qubit_array_type = if capability_flags.dynamic_qubit_management { + None + } else { + Some( + ctx.i64_type() + .array_type(get_required_num_qubits_strict(entry_fn)?), + ) + }; for bb in entry_fn.get_basic_blocks() { // Snapshot instructions before rewriting calls. Some rewrite paths @@ -508,18 +892,19 @@ mod aux { .ok() .map(ToOwned::to_owned) }) { - let mut args = ProcessCallArgs { + let args = ProcessCallArgs { ctx, module, - instr: &instr, - fn_name: &fn_name, - wasm_fns, + instr, + fn_name, + wasm_fns: std::ptr::from_ref(wasm_fns), qubit_array, qubit_array_type, - global_mapping: &mut global_mapping, - result_ssa: &mut result_ssa, + capability_flags, + global_mapping: &raw mut global_mapping, + result_ssa: &raw mut result_ssa, }; - process_call_instruction(&mut args)?; + process_call_instruction(args)?; } } } @@ -527,13 +912,13 @@ mod aux { Ok(()) } - fn process_call_instruction(args: &mut ProcessCallArgs) -> Result<(), String> { - let call = CallSiteValue::try_from(*args.instr) + fn process_call_instruction(mut args: ProcessCallArgs<'_>) -> Result<(), String> { + let call = CallSiteValue::try_from(args.instr) .map_err(|()| "Instruction is not a call site".to_string())?; - match args.fn_name { - name if name.starts_with("__quantum__qis__") => handle_qis_call(args), - name if name.starts_with("__quantum__rt__") => handle_rt_call(args), - name if name.starts_with("___") => handle_qtm_call(args), + match args.fn_name.as_str() { + name if name.starts_with("__quantum__qis__") => handle_qis_call(&args), + name if name.starts_with("__quantum__rt__") => handle_rt_call(&mut args), + name if name.starts_with("___") => handle_qtm_call(&args), _ => { if let Some(f) = call.get_called_fn_value() { // IR defined function calls @@ -582,27 +967,56 @@ mod aux { } } - fn handle_qis_call(args: &mut ProcessCallArgs) -> Result<(), String> { - match args.fn_name { + fn handle_qis_call(args: &ProcessCallArgs<'_>) -> Result<(), String> { + match args.fn_name.as_str() { "__quantum__qis__rxy__body" => { - replace_rxy_call(args.ctx, args.module, *args.instr)?; + replace_rxy_call( + args.ctx, + module_ref(args), + args.instr, + args.capability_flags.dynamic_qubit_management, + )?; } "__quantum__qis__rz__body" => { - replace_rz_call(args.ctx, args.module, *args.instr)?; + replace_rz_call( + args.ctx, + module_ref(args), + args.instr, + args.capability_flags.dynamic_qubit_management, + )?; } "__quantum__qis__rzz__body" => { - replace_rzz_call(args.ctx, args.module, *args.instr)?; + replace_rzz_call( + args.ctx, + module_ref(args), + args.instr, + args.capability_flags.dynamic_qubit_management, + )?; } "__quantum__qis__u1q__body" => { log::info!( "`__quantum__qis__u1q__body` used, synonym for `__quantum__qis__rxy__body`" ); - replace_rxy_call(args.ctx, args.module, *args.instr)?; + replace_rxy_call( + args.ctx, + module_ref(args), + args.instr, + args.capability_flags.dynamic_qubit_management, + )?; } "__quantum__qis__mz__body" | "__quantum__qis__m__body" | "__quantum__qis__mresetz__body" => { - handle_mz_call(args)?; + handle_mz_call( + args.ctx, + args.module.cast::<()>(), + &args.instr, + args.fn_name.as_str(), + args.capability_flags, + args.qubit_array, + args.qubit_array_type, + args.result_ssa.cast::<()>(), + )?; } "__quantum__qis__mz_leaked__body" => { handle_mz_leaked_call(args)?; @@ -617,9 +1031,8 @@ mod aux { // Under LLVM 21, decomposition functions may remain as IR-defined calls // rather than being fully inlined at this stage. Allow these calls to // pass through; their bodies are lowered by process_ir_defined_q_fns. - let is_ir_defined = args - .module - .get_function(args.fn_name) + let is_ir_defined = module_ref(args) + .get_function(args.fn_name.as_str()) .is_some_and(|f| f.count_basic_blocks() > 0); if !is_ir_defined { return Err(format!("Unsupported QIR QIS function: {}", args.fn_name)); @@ -629,21 +1042,57 @@ mod aux { Ok(()) } - fn handle_rt_call(args: &mut ProcessCallArgs) -> Result<(), String> { - match args.fn_name { + fn handle_rt_call(args: &mut ProcessCallArgs<'_>) -> Result<(), String> { + match args.fn_name.as_str() { "__quantum__rt__initialize" => { args.instr.erase_from_basic_block(); } + "__quantum__rt__qubit_allocate" => { + lower_dynamic_qubit_allocate(args)?; + } + "__quantum__rt__qubit_release" => { + lower_dynamic_qubit_release(args)?; + } + "__quantum__rt__qubit_array_allocate" => { + lower_dynamic_qubit_array_allocate(args)?; + } + "__quantum__rt__qubit_array_release" => { + lower_dynamic_qubit_array_release(args)?; + } + "__quantum__rt__result_allocate" => { + lower_dynamic_result_allocate(args)?; + } + "__quantum__rt__result_release" => { + lower_dynamic_result_release(args)?; + } + "__quantum__rt__result_array_allocate" => { + lower_dynamic_result_array_allocate(args)?; + } + "__quantum__rt__result_array_release" => { + lower_dynamic_result_array_release(args)?; + } + "__quantum__rt__result_array_record_output" => { + lower_dynamic_result_array_record_output(args)?; + } "__quantum__rt__read_result" | "__quantum__rt__result_record_output" => { - handle_read_result_call(args)?; + handle_read_result_call( + args.ctx, + args.module.cast::<()>(), + &args.instr, + args.fn_name.as_str(), + args.capability_flags, + args.global_mapping.cast::<()>(), + args.result_ssa.cast::<()>(), + )?; } "__quantum__rt__tuple_record_output" | "__quantum__rt__array_record_output" => { + let fn_name = args.fn_name.clone(); handle_tuple_or_array_output( args.ctx, - args.module, - *args.instr, - args.global_mapping, - args.fn_name, + module_ref(args), + args.instr, + unsafe { &mut *args.global_mapping }, + fn_name.as_str(), )?; } "__quantum__rt__bool_record_output" @@ -656,8 +1105,8 @@ mod aux { Ok(()) } - fn handle_qtm_call(args: &mut ProcessCallArgs) -> Result<(), String> { - match args.fn_name { + fn handle_qtm_call(args: &ProcessCallArgs<'_>) -> Result<(), String> { + match args.fn_name.as_str() { "___get_current_shot" => { handle_get_current_shot(args)?; } @@ -688,881 +1137,2187 @@ mod aux { Ok(()) } - fn handle_mz_call(args: &mut ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, - module, - instr, - fn_name, - qubit_array, - qubit_array_type, - result_ssa, - .. - } = args; + fn get_qubit_handle<'ctx>( + ctx: &'ctx Context, + capability_flags: CapabilityFlags, + qubit_array: Option>, + qubit_array_type: Option>, + builder: &inkwell::builder::Builder<'ctx>, + qubit_ptr: PointerValue<'ctx>, + ) -> Result, String> { + if capability_flags.dynamic_qubit_management { + return builder + .build_ptr_to_int(qubit_ptr, ctx.i64_type(), "qbit") + .map(BasicValueEnum::from) + .map_err(|e| format!("Failed to convert qubit pointer to handle: {e}")); + } - if *fn_name == "__quantum__qis__m__body" { - log::warn!( - "`__quantum__qis__m__body` is from Q# QDK, synonym for `__quantum__qis__mz__body`" - ); + let qubit_array = qubit_array.ok_or("Missing static qubit array for qubit lookup")?; + let qubit_array_type = + qubit_array_type.ok_or("Missing static qubit array type for qubit lookup")?; + let i64_type = ctx.i64_type(); + let index = get_index(qubit_ptr)?; + let index_val = i64_type.const_int(index, false); + let elem_ptr = unsafe { + builder.build_gep( + qubit_array_type, + qubit_array, + &[i64_type.const_zero(), index_val], + "", + ) } - let builder = ctx.create_builder(); - builder.position_before(instr); + .map_err(|e| format!("Failed to build GEP for qubit handle: {e}"))?; + builder + .build_load(i64_type, elem_ptr, "qbit") + .map_err(|e| format!("Failed to build load for qubit handle: {e}")) + } - // Extract qubit and result indices - let call_args: Vec = extract_operands(instr)?; - let qubit_ptr = call_args[0].into_pointer_value(); - let result_ptr = call_args[1].into_pointer_value(); + const fn module_ref<'ctx>(args: &ProcessCallArgs<'ctx>) -> &'ctx Module<'ctx> { + // SAFETY: `args.module` points to the borrowed module passed into + // `process_entry_function`, which outlives all handler calls. + unsafe { &*args.module } + } - // Load qubit handle - let q_handle = { - let i64_type = ctx.i64_type(); - let index = get_index(qubit_ptr)?; - let index_val = i64_type.const_int(index, false); - let elem_ptr = unsafe { - builder.build_gep( - *qubit_array_type, - *qubit_array, - &[i64_type.const_zero(), index_val], - "", - ) - } - .map_err(|e| format!("Failed to build GEP for qubit handle: {e}"))?; - builder - .build_load(i64_type, elem_ptr, "qbit") - .map_err(|e| format!("Failed to build load for qubit handle: {e}"))? - }; + fn get_or_create_qalloc_fail_global<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> inkwell::values::GlobalValue<'ctx> { + let panic_msg = ctx.const_string(b".EXIT:INT:No more qubits available to allocate.", false); + let panic_arr_ty = panic_msg.get_type(); + module.get_global("e_qalloc_fail").unwrap_or_else(|| { + let global = module.add_global(panic_arr_ty, None, "e_qalloc_fail"); + global.set_initializer(&panic_msg); + global.set_linkage(Linkage::Private); + global.set_constant(true); + global + }) + } - // Create ___lazy_measure call - let meas = { - let meas_func = get_or_create_function( - module, - "___lazy_measure", - ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), - ); + struct DynamicQubitAllocatePaths<'ctx> { + fail: BasicBlock<'ctx>, + fail_store: BasicBlock<'ctx>, + fail_panic: BasicBlock<'ctx>, + success: BasicBlock<'ctx>, + store_ok: BasicBlock<'ctx>, + ret_ok: BasicBlock<'ctx>, + } - let call = builder.build_call(meas_func, &[q_handle.into()], "meas"); - let call_result = - call.map_err(|e| format!("Failed to build call for lazy measure function: {e}"))?; - match call_result.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from lazy measure call".into()); - } - } - }; - - // Store measurement result - let result_idx = get_index(result_ptr)?; - let result_idx_usize = checked_result_index(result_idx, result_ssa.len())?; - result_ssa[result_idx_usize] = Some((meas, None)); + fn build_dynamic_qubit_allocate_fail_path<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + ptr_type: inkwell::types::PointerType<'ctx>, + out_err: PointerValue<'ctx>, + paths: &DynamicQubitAllocatePaths<'ctx>, + ) { + builder.position_at_end(paths.fail); + let out_err_is_null = + build_ptr_is_null(ctx, builder, out_err, "out_err_int").expect("out_err null check"); + builder + .build_conditional_branch(out_err_is_null, paths.fail_panic, paths.fail_store) + .expect("branch fail handler"); - if *fn_name == "__quantum__qis__mresetz__body" { - log::warn!("`__quantum__qis__mresetz__body` is from Q# QDK"); - // Create ___reset call - create_reset_call(ctx, module, &builder, q_handle); + builder.position_at_end(paths.fail_store); + let _ = builder.build_store(out_err, ctx.bool_type().const_all_ones()); + builder + .build_return(Some(&ptr_type.const_zero())) + .expect("return null qubit"); + + builder.position_at_end(paths.fail_panic); + let err_global = get_or_create_qalloc_fail_global(ctx, module); + let err_ty = err_global + .get_initializer() + .expect("panic global initializer") + .into_array_value() + .get_type(); + let err_gep = unsafe { + builder.build_gep( + err_ty, + err_global.as_pointer_value(), + &[ctx.i64_type().const_zero(), ctx.i64_type().const_zero()], + "err_gep", + ) } + .expect("panic msg gep"); + let panic_fn = get_or_create_function( + module, + "panic", + ctx.void_type() + .fn_type(&[ctx.i32_type().into(), ptr_type.into()], false), + ); + let _ = builder.build_call( + panic_fn, + &[ctx.i32_type().const_int(1001, false).into(), err_gep.into()], + "", + ); + builder.build_unreachable().expect("unreachable"); + } - // Remove original call - instr.erase_from_basic_block(); - Ok(()) + fn build_dynamic_qubit_allocate_success_path<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + ptr_type: inkwell::types::PointerType<'ctx>, + out_err: PointerValue<'ctx>, + qid: inkwell::values::IntValue<'ctx>, + paths: &DynamicQubitAllocatePaths<'ctx>, + ) { + builder.position_at_end(paths.success); + let out_err_is_null = + build_ptr_is_null(ctx, builder, out_err, "out_err_int_ok").expect("out_err null check"); + builder + .build_conditional_branch(out_err_is_null, paths.ret_ok, paths.store_ok) + .expect("branch success handler"); + + builder.position_at_end(paths.store_ok); + let _ = builder.build_store(out_err, ctx.bool_type().const_zero()); + builder + .build_unconditional_branch(paths.ret_ok) + .expect("jump ret"); + + builder.position_at_end(paths.ret_ok); + let ptr_val = builder + .build_int_to_ptr(qid, ptr_type, "qubit_ptr") + .expect("int to ptr"); + builder + .build_return(Some(&ptr_val)) + .expect("return qubit ptr"); } - fn handle_mz_leaked_call(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; - let builder = ctx.create_builder(); - builder.position_before(instr); - let call = CallSiteValue::try_from(*args.instr) - .map_err(|()| "Malformed mz_leaked call: instruction is not a call site".to_string())?; - let called_fn = call - .get_called_fn_value() - .ok_or_else(|| "Malformed mz_leaked call: missing callee".to_string())?; - let fn_type = called_fn.get_type(); - let param_types = fn_type.get_param_types(); - let has_expected_signature = fn_type - .get_return_type() - .is_some_and(|ty| ty.is_int_type() && ty.into_int_type().get_bit_width() == 64) - && param_types.len() == 1 - && param_types[0].is_pointer_type(); - if !has_expected_signature { - return Err("Malformed mz_leaked call: expected signature i64 (ptr)".to_string()); - } + struct DynamicQubitArrayAllocateBlocks<'ctx> { + loop_header: BasicBlock<'ctx>, + loop_body: BasicBlock<'ctx>, + loop_exit: BasicBlock<'ctx>, + continue_alloc: BasicBlock<'ctx>, + maybe_rollback: BasicBlock<'ctx>, + rollback: BasicBlock<'ctx>, + rollback_done: BasicBlock<'ctx>, + } - let call_args: Vec = extract_operands(instr)?; - let qubit_ptr = mz_leaked_qubit_operand(&call_args)?; + struct PointerArrayReleaseHelperArgs<'ctx> { + function: FunctionValue<'ctx>, + len: inkwell::values::IntValue<'ctx>, + array_ptr: PointerValue<'ctx>, + release_fn: FunctionValue<'ctx>, + ptr_elem_type: inkwell::types::PointerType<'ctx>, + gep_error: &'static str, + load_error: &'static str, + release_error: &'static str, + } - let q_handle = { - let idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; - let idx_call = builder - .build_call(idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!( - "Failed to get basic value from {LOAD_QUBIT_FN} call" - )); - } - } - }; + fn create_dynamic_qubit_array_allocate_blocks<'ctx>( + ctx: &'ctx Context, + function: FunctionValue<'ctx>, + ) -> DynamicQubitArrayAllocateBlocks<'ctx> { + DynamicQubitArrayAllocateBlocks { + loop_header: ctx.append_basic_block(function, "loop_header"), + loop_body: ctx.append_basic_block(function, "loop_body"), + loop_exit: ctx.append_basic_block(function, "loop_exit"), + continue_alloc: ctx.append_basic_block(function, "continue_alloc"), + maybe_rollback: ctx.append_basic_block(function, "maybe_rollback"), + rollback: ctx.append_basic_block(function, "rollback"), + rollback_done: ctx.append_basic_block(function, "rollback_done"), + } + } - let meas_handle = { - let meas_func = get_or_create_function( - module, - "___lazy_measure_leaked", - ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), - ); + fn build_dynamic_qubit_array_allocate_rollback<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + idx: inkwell::values::IntValue<'ctx>, + array_ptr: PointerValue<'ctx>, + release_array_fn: FunctionValue<'ctx>, + rollback: BasicBlock<'ctx>, + rollback_done: BasicBlock<'ctx>, + ) { + builder.position_at_end(rollback); + let rollback_len = builder + .build_int_add(idx, ctx.i64_type().const_int(1, false), "rollback_len") + .expect("rollback len"); + let _ = builder + .build_call( + release_array_fn, + &[rollback_len.into(), array_ptr.into()], + "", + ) + .expect("rollback release"); + builder + .build_unconditional_branch(rollback_done) + .expect("jump rollback_done"); + } - let call = builder.build_call(meas_func, &[q_handle.into()], "meas_leaked"); - let call_result = call.map_err(|e| { - format!("Failed to build call for lazy leaked measure function: {e}") - })?; - match call_result.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from lazy leaked measure call".into()); - } - } - }; + fn get_bool_cl_array_type(ctx: &Context) -> inkwell::types::StructType<'_> { + ctx.struct_type( + &[ + ctx.i32_type().into(), + ctx.i32_type().into(), + ctx.ptr_type(AddressSpace::default()).into(), + ctx.ptr_type(AddressSpace::default()).into(), + ], + true, + ) + } - let meas_value = { - let read_func = get_or_create_function( - module, - "___read_future_uint", - ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), - ); - let call = builder.build_call(read_func, &[meas_handle.into()], "meas_leaked_value"); - let call_result = - call.map_err(|e| format!("Failed to build call for read_future_uint: {e}"))?; - match call_result.try_as_basic_value() { + fn build_dynamic_result_array_values<'ctx>( + ctx: &'ctx Context, + function: FunctionValue<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + len: inkwell::values::IntValue<'ctx>, + array_ptr: PointerValue<'ctx>, + read_fn: FunctionValue<'ctx>, + ) -> Result, String> { + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let bool_arr = builder + .build_array_alloca(ctx.bool_type(), len, "result_arr_data") + .map_err(|e| format!("Failed to allocate result array data: {e}"))?; + build_dynamic_array_loop(ctx, function, builder, len, |builder, idx| { + let elem_ptr = unsafe { builder.build_gep(ptr_type, array_ptr, &[idx], "elem_ptr") } + .map_err(|e| format!("Failed to build result record array GEP: {e}"))?; + let result_ptr = builder + .build_load(ptr_type, elem_ptr, "result_ptr") + .map_err(|e| format!("Failed to load result pointer: {e}"))?; + let bool_call = builder + .build_call(read_fn, &[result_ptr.into()], "result_bool") + .map_err(|e| format!("Failed to read result array element: {e}"))?; + let bool_val = match bool_call.try_as_basic_value() { inkwell::values::ValueKind::Basic(bv) => bv, inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from read_future_uint call".into()); + return Err("Dynamic result read helper did not return a bool".to_string()); } - } - }; + }; + let out_ptr = + unsafe { builder.build_gep(ctx.bool_type(), bool_arr, &[idx], "result_bool_ptr") } + .map_err(|e| format!("Failed to index result bool array: {e}"))?; + let _ = builder + .build_store(out_ptr, bool_val) + .map_err(|e| format!("Failed to store result bool array element: {e}"))?; + Ok(()) + })?; + Ok(bool_arr) + } - let dec_func = get_or_create_function( - module, - "___dec_future_refcount", - ctx.void_type().fn_type(&[ctx.i64_type().into()], false), - ); + fn build_dynamic_result_array_descriptor<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + len: inkwell::values::IntValue<'ctx>, + bool_arr: PointerValue<'ctx>, + ) -> Result, String> { + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let array_desc_type = get_bool_cl_array_type(ctx); + let array_desc = builder + .build_alloca(array_desc_type, "result_arr_desc") + .map_err(|e| format!("Failed to allocate result array descriptor: {e}"))?; + let x_ptr = builder + .build_struct_gep(array_desc_type, array_desc, 0, "result_arr_x") + .map_err(|e| format!("Failed to build result array length GEP: {e}"))?; + let y_ptr = builder + .build_struct_gep(array_desc_type, array_desc, 1, "result_arr_y") + .map_err(|e| format!("Failed to build result array rank GEP: {e}"))?; + let data_ptr = builder + .build_struct_gep(array_desc_type, array_desc, 2, "result_arr_data_ptr") + .map_err(|e| format!("Failed to build result array data GEP: {e}"))?; + let mask_ptr = builder + .build_struct_gep(array_desc_type, array_desc, 3, "result_arr_mask_ptr") + .map_err(|e| format!("Failed to build result array mask GEP: {e}"))?; + let mask_zero = builder + .build_alloca(ctx.i32_type(), "result_arr_mask") + .map_err(|e| format!("Failed to allocate result array mask: {e}"))?; let _ = builder - .build_call(dec_func, &[meas_handle.into()], "") - .map_err(|e| format!("Failed to build call for dec_future_refcount: {e}"))?; + .build_store(mask_zero, ctx.i32_type().const_zero()) + .map_err(|e| format!("Failed to initialize result array mask: {e}"))?; + let len_i32 = builder + .build_int_truncate(len, ctx.i32_type(), "result_arr_len") + .map_err(|e| format!("Failed to truncate result array length: {e}"))?; + let _ = builder + .build_store(x_ptr, len_i32) + .map_err(|e| format!("Failed to store result array length: {e}"))?; + let _ = builder + .build_store(y_ptr, ctx.i32_type().const_int(1, false)) + .map_err(|e| format!("Failed to store result array rank: {e}"))?; + let data_as_ptr = builder + .build_bit_cast(bool_arr, ptr_type, "result_arr_data_cast") + .map_err(|e| format!("Failed to cast result array data pointer: {e}"))?; + let _ = builder + .build_store(data_ptr, data_as_ptr) + .map_err(|e| format!("Failed to store result array data pointer: {e}"))?; + let mask_as_ptr = builder + .build_bit_cast(mask_zero, ptr_type, "result_arr_mask_cast") + .map_err(|e| format!("Failed to cast result array mask pointer: {e}"))?; + let _ = builder + .build_store(mask_ptr, mask_as_ptr) + .map_err(|e| format!("Failed to store result array mask pointer: {e}"))?; + Ok(array_desc) + } - let instruction_val = meas_value - .as_instruction_value() - .ok_or("Failed to convert leaked measurement value to instruction value")?; - instr.replace_all_uses_with(&instruction_val); - instr.erase_from_basic_block(); - Ok(()) + struct DynamicResultSlotPtrs<'ctx> { + state: PointerValue<'ctx>, + cached: PointerValue<'ctx>, + future: PointerValue<'ctx>, } - pub fn mz_leaked_qubit_operand<'ctx>( - call_args: &[BasicValueEnum<'ctx>], - ) -> Result, String> { - match call_args { - [BasicValueEnum::PointerValue(ptr), _] => Ok(*ptr), - [_, _] => { - Err("Malformed mz_leaked call: expected first argument to be a pointer".into()) - } - _ => Err(format!( - "Malformed mz_leaked call: expected 1 argument plus callee, got {} operands", - call_args.len() - )), + fn get_dynamic_result_slot_ptrs<'ctx>( + builder: &inkwell::builder::Builder<'ctx>, + slot_type: inkwell::types::StructType<'ctx>, + result_ptr: PointerValue<'ctx>, + ) -> DynamicResultSlotPtrs<'ctx> { + DynamicResultSlotPtrs { + state: builder + .build_struct_gep(slot_type, result_ptr, 0, "state") + .expect("state gep"), + cached: builder + .build_struct_gep(slot_type, result_ptr, 1, "cached") + .expect("cached gep"), + future: builder + .build_struct_gep(slot_type, result_ptr, 2, "future") + .expect("future gep"), } } - fn handle_reset_call(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; - let builder = ctx.create_builder(); - builder.position_before(instr); - - // Extract qubit index - let call_args: Vec = extract_operands(instr)?; - let qubit_ptr = call_args[0].into_pointer_value(); - - // Load qubit handle - let idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; - let idx_call = builder - .build_call(idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let q_handle = match idx_call.try_as_basic_value() { + fn build_dynamic_result_pending_return<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + future_ptr: PointerValue<'ctx>, + cached_ptr: PointerValue<'ctx>, + state_ptr: PointerValue<'ctx>, + ) { + let future = builder + .build_load(ctx.i64_type(), future_ptr, "future") + .expect("load future"); + let read_fn = get_or_create_function( + module, + "___read_future_bool", + ctx.bool_type().fn_type(&[ctx.i64_type().into()], false), + ); + let dec_fn = get_or_create_function( + module, + "___dec_future_refcount", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), + ); + let bool_call = builder + .build_call(read_fn, &[future.into()], "bool") + .expect("read future"); + let bool_val = match bool_call.try_as_basic_value() { inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!( - "Failed to get basic value from {LOAD_QUBIT_FN} call" - )); - } + inkwell::values::ValueKind::Instruction(_) => unreachable!(), }; + let _ = builder.build_call(dec_fn, &[future.into()], ""); + let _ = builder.build_store(cached_ptr, bool_val); + let _ = builder.build_store(state_ptr, ctx.i8_type().const_int(2, false)); + builder + .build_return(Some(&bool_val.into_int_value())) + .expect("ret pending"); + } - // Create ___reset call - create_reset_call(ctx, module, &builder, q_handle); - - instr.erase_from_basic_block(); - Ok(()) + fn call_basic_value<'ctx>( + builder: &inkwell::builder::Builder<'ctx>, + callee: FunctionValue<'ctx>, + args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + build_error: &str, + value_error: &str, + ) -> Result, String> { + let call = builder + .build_call(callee, args, name) + .map_err(|e| format!("{build_error}: {e}"))?; + match call.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => Ok(bv), + inkwell::values::ValueKind::Instruction(_) => Err(value_error.to_string()), + } } - fn parse_barrier_arity(fn_name: &str) -> Result { - fn_name - .strip_prefix("__quantum__qis__barrier") - .and_then(|s| s.strip_suffix("__body")) - .and_then(|s| s.parse::().ok()) - .filter(|&n| n > 0) - .ok_or_else(|| format!("Invalid barrier function name: {fn_name}")) + fn clear_dynamic_result_slot<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + slot_ptrs: &DynamicResultSlotPtrs<'ctx>, + ) { + let _ = builder.build_store(slot_ptrs.state, ctx.i8_type().const_zero()); + let _ = builder.build_store(slot_ptrs.cached, ctx.bool_type().const_zero()); + let _ = builder.build_store(slot_ptrs.future, ctx.i64_type().const_zero()); } - #[allow(clippy::too_many_lines)] - fn handle_barrier_call(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, - module, - instr, - fn_name, - .. - } = args; - let builder = ctx.create_builder(); - builder.position_before(instr); + fn set_dynamic_result_pending<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + slot_ptrs: &DynamicResultSlotPtrs<'ctx>, + future: inkwell::values::IntValue<'ctx>, + ) { + let _ = builder.build_store(slot_ptrs.state, ctx.i8_type().const_int(1, false)); + let _ = builder.build_store(slot_ptrs.future, future); + } - let num_qubits = parse_barrier_arity(fn_name)?; + fn read_result_bool<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + result_ptr: PointerValue<'ctx>, + capability_flags: CapabilityFlags, + result_ssa: &mut [Option<(BasicValueEnum<'ctx>, Option>)>], + ) -> Result, String> { + if capability_flags.dynamic_result_management { + let read_func = ensure_dynamic_result_read(ctx, module); + return call_basic_value( + builder, + read_func, + &[result_ptr.into()], + "bool", + "Failed to build call for dynamic result read", + "Failed to get basic value from dynamic result read call", + ); + } - // Extract qubit arguments (excluding the last operand which is the function pointer) - let all_operands: Vec = extract_operands(instr)?; - let num_operands = all_operands - .len() - .checked_sub(1) - .ok_or("Expected at least one operand")?; + let result_idx = get_index(result_ptr)?; + let result_idx_usize = usize::try_from(result_idx) + .map_err(|e| format!("Failed to convert result index to usize: {e}"))?; + let meas_handle = result_ssa[result_idx_usize] + .ok_or_else(|| "Expected measurement handle".to_string())?; + result_ssa[result_idx_usize] + .and_then(|v| v.1) + .and_then(|val: BasicValueEnum<'_>| val.as_instruction_value()) + .map_or_else( + || { + let read_func = get_or_create_function( + module, + "___read_future_bool", + ctx.bool_type().fn_type(&[ctx.i64_type().into()], false), + ); + let bool_val = call_basic_value( + builder, + read_func, + &[meas_handle.0.into()], + "bool", + "Failed to build call for read_future_bool", + "Failed to get basic value from read_future_bool call", + )?; + let dec_func = get_or_create_function( + module, + "___dec_future_refcount", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), + ); + let _ = builder + .build_call(dec_func, &[meas_handle.0.into()], "") + .map_err(|e| { + format!("Failed to build call for dec_future_refcount: {e}") + })?; + result_ssa[result_idx_usize] = Some((meas_handle.0, Some(bool_val))); + Ok(bool_val) + }, + |val: inkwell::values::InstructionValue<'_>| { + val.as_any_value_enum() + .try_into() + .map_err(|()| "Expected BasicValueEnum".to_string()) + }, + ) + } - if num_operands != num_qubits { - return Err(format!( - "Barrier function {fn_name} expects {num_qubits} arguments, got {num_operands}" - )); + fn get_or_create_result_output_global<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + global_mapping: &mut HashMap>, + capability_flags: CapabilityFlags, + result_ptr: PointerValue<'ctx>, + gep: BasicValueEnum<'ctx>, + ) -> Result, String> { + if let Ok(old_global) = parse_gep(gep) { + return global_mapping + .get(old_global.as_str()) + .copied() + .ok_or_else(|| format!("Output global `{old_global}` not found in mapping")); } - let call_args = &all_operands[..num_operands]; - - // Load qubit handles into an array - let i64_type = ctx.i64_type(); - let array_type = i64_type.array_type( - u32::try_from(num_qubits).map_err(|e| format!("Failed to convert num_qubits: {e}"))?, - ); - let array_alloca = builder - .build_alloca(array_type, "barrier_qubits") - .map_err(|e| format!("Failed to allocate array for barrier qubits: {e}"))?; + let fallback_label = if capability_flags.dynamic_result_management { + "result_dynamic".to_string() + } else { + format!("result_{}", get_index(result_ptr)?) + }; + let (new_const, new_name) = + build_result_global(ctx, &fallback_label, &fallback_label, "RESULT", None)?; + let new_global = module.add_global(new_const.get_type(), None, &new_name); + new_global.set_initializer(&new_const); + new_global.set_linkage(inkwell::module::Linkage::Private); + new_global.set_constant(true); + global_mapping.insert(fallback_label, new_global); + Ok(new_global) + } - let idx_fn = module - .get_function(LOAD_QUBIT_FN) - .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; + fn get_dynamic_result_slot_type(ctx: &Context) -> inkwell::types::StructType<'_> { + ctx.struct_type( + &[ + ctx.i8_type().into(), // state: 0=false, 1=pending future, 2=cached bool + ctx.bool_type().into(), // cached bool + ctx.i64_type().into(), // future handle + ], + false, + ) + } - for (i, arg) in call_args.iter().enumerate() { - let qubit_ptr = arg.into_pointer_value(); - let idx_call = builder - .build_call(idx_fn, &[qubit_ptr.into()], "qbit") - .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; - let q_handle = match idx_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err(format!( - "Failed to get basic value from {LOAD_QUBIT_FN} call" - )); - } - }; + fn build_ptr_is_null<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + ptr: PointerValue<'ctx>, + name: &str, + ) -> Result, String> { + let ptr_as_int = builder + .build_ptr_to_int(ptr, ctx.i64_type(), name) + .map_err(|e| format!("Failed to convert pointer to int: {e}"))?; + builder + .build_int_compare( + inkwell::IntPredicate::EQ, + ptr_as_int, + ctx.i64_type().const_zero(), + "is_null", + ) + .map_err(|e| format!("Failed to compare pointer with null: {e}")) + } - let elem_ptr = unsafe { - builder.build_gep( - array_type, - array_alloca, - &[ - i64_type.const_zero(), - i64_type.const_int( - u64::try_from(i) - .map_err(|e| format!("Failed to convert index: {e}"))?, - false, - ), - ], - "", - ) - } - .map_err(|e| format!("Failed to build GEP for barrier array: {e}"))?; - builder - .build_store(elem_ptr, q_handle) - .map_err(|e| format!("Failed to store qubit handle in array: {e}"))?; + fn ensure_dynamic_qubit_allocate<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.qubit_allocate") { + return existing; } - let array_ptr = unsafe { - builder.build_gep( - array_type, - array_alloca, - &[i64_type.const_zero(), i64_type.const_zero()], - "barrier_array_ptr", + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let fn_type = ptr_type.fn_type(&[ptr_type.into()], false); + let function = + module.add_function("qir_qis.qubit_allocate", fn_type, Some(Linkage::Private)); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + let paths = DynamicQubitAllocatePaths { + fail: ctx.append_basic_block(function, "fail"), + fail_store: ctx.append_basic_block(function, "fail_store"), + fail_panic: ctx.append_basic_block(function, "fail_panic"), + success: ctx.append_basic_block(function, "success"), + store_ok: ctx.append_basic_block(function, "store_ok"), + ret_ok: ctx.append_basic_block(function, "ret_ok"), + }; + builder.position_at_end(entry); + + let out_err = function + .get_first_param() + .expect("qubit allocate helper has out_err") + .into_pointer_value(); + let qalloc_fn = + get_or_create_function(module, "___qalloc", ctx.i64_type().fn_type(&[], false)); + let call_result = builder + .build_call(qalloc_fn, &[], "qalloc") + .expect("qalloc call"); + let qid = match call_result.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv.into_int_value(), + inkwell::values::ValueKind::Instruction(_) => unreachable!(), + }; + let is_fail = builder + .build_int_compare( + inkwell::IntPredicate::EQ, + qid, + ctx.i64_type().const_int(u64::MAX, false), + "is_fail", ) - } - .map_err(|e| format!("Failed to build GEP for barrier array pointer: {e}"))?; + .expect("compare fail"); + builder + .build_conditional_branch(is_fail, paths.fail, paths.success) + .expect("branch fail"); + build_dynamic_qubit_allocate_fail_path(ctx, module, &builder, ptr_type, out_err, &paths); + build_dynamic_qubit_allocate_success_path(ctx, &builder, ptr_type, out_err, qid, &paths); + function + } - // void ___barrier(i64* %qbs, i64 %qbs_len) - let barrier_func = get_or_create_function( + fn ensure_dynamic_qubit_release<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.qubit_release") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let fn_type = ctx.void_type().fn_type(&[ptr_type.into()], false); + let function = + module.add_function("qir_qis.qubit_release", fn_type, Some(Linkage::Private)); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + let ret = ctx.append_basic_block(function, "ret"); + let body = ctx.append_basic_block(function, "body"); + builder.position_at_end(entry); + let qubit_ptr = function + .get_first_param() + .expect("qubit release param") + .into_pointer_value(); + let is_null = build_ptr_is_null(ctx, &builder, qubit_ptr, "qubit_int").expect("null check"); + builder + .build_conditional_branch(is_null, ret, body) + .expect("branch"); + builder.position_at_end(body); + let q_handle = builder + .build_ptr_to_int(qubit_ptr, ctx.i64_type(), "qbit") + .expect("ptr to int"); + let qfree_fn = get_or_create_function( module, - "___barrier", + "___qfree", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), + ); + let _ = builder.build_call(qfree_fn, &[q_handle.into()], ""); + builder.build_unconditional_branch(ret).expect("jump ret"); + builder.position_at_end(ret); + builder.build_return(None).expect("return"); + function + } + + fn build_dynamic_array_loop<'ctx, F>( + ctx: &'ctx Context, + function: FunctionValue<'ctx>, + builder: &inkwell::builder::Builder<'ctx>, + trip_count: inkwell::values::IntValue<'ctx>, + mut body_builder: F, + ) -> Result<(), String> + where + F: FnMut( + &inkwell::builder::Builder<'ctx>, + inkwell::values::IntValue<'ctx>, + ) -> Result<(), String>, + { + let entry_block = builder + .get_insert_block() + .ok_or("Missing loop entry block")?; + let loop_header = ctx.append_basic_block(function, "loop_header"); + let loop_body = ctx.append_basic_block(function, "loop_body"); + let loop_exit = ctx.append_basic_block(function, "loop_exit"); + builder + .build_unconditional_branch(loop_header) + .map_err(|e| format!("Failed to branch to loop header: {e}"))?; + builder.position_at_end(loop_header); + let idx_phi = builder + .build_phi(ctx.i64_type(), "idx") + .map_err(|e| format!("Failed to create loop phi: {e}"))?; + idx_phi.add_incoming(&[(&ctx.i64_type().const_zero(), entry_block)]); + let idx = idx_phi.as_basic_value().into_int_value(); + let cond = builder + .build_int_compare(inkwell::IntPredicate::ULT, idx, trip_count, "loop_cond") + .map_err(|e| format!("Failed to build loop condition: {e}"))?; + builder + .build_conditional_branch(cond, loop_body, loop_exit) + .map_err(|e| format!("Failed to build loop branch: {e}"))?; + builder.position_at_end(loop_body); + body_builder(builder, idx)?; + let next = builder + .build_int_add(idx, ctx.i64_type().const_int(1, false), "next_idx") + .map_err(|e| format!("Failed to increment loop index: {e}"))?; + builder + .build_unconditional_branch(loop_header) + .map_err(|e| format!("Failed to jump to loop header: {e}"))?; + idx_phi.add_incoming(&[(&next, loop_body)]); + builder.position_at_end(loop_exit); + Ok(()) + } + + fn build_pointer_array_release_helper<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + args: &PointerArrayReleaseHelperArgs<'ctx>, + ) -> Result<(), String> { + build_dynamic_array_loop(ctx, args.function, builder, args.len, |builder, idx| { + let elem_ptr = unsafe { + builder.build_gep(args.ptr_elem_type, args.array_ptr, &[idx], "elem_ptr") + } + .map_err(|e| format!("{}: {e}", args.gep_error))?; + let value_ptr = builder + .build_load(args.ptr_elem_type, elem_ptr, "value_ptr") + .map_err(|e| format!("{}: {e}", args.load_error))?; + let _ = builder + .build_call(args.release_fn, &[value_ptr.into()], "") + .map_err(|e| format!("{}: {e}", args.release_error))?; + Ok(()) + }) + } + + fn ensure_dynamic_qubit_array_allocate<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.qubit_array_allocate") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let function = module.add_function( + "qir_qis.qubit_array_allocate", ctx.void_type().fn_type( - &[ - ctx.ptr_type(AddressSpace::default()).into(), - i64_type.into(), - ], + &[ctx.i64_type().into(), ptr_type.into(), ptr_type.into()], false, ), + Some(Linkage::Private), ); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + builder.position_at_end(entry); + let len = function.get_nth_param(0).expect("len").into_int_value(); + let array_ptr = function + .get_nth_param(1) + .expect("array") + .into_pointer_value(); + let out_err = function + .get_nth_param(2) + .expect("out_err") + .into_pointer_value(); + let alloc_fn = ensure_dynamic_qubit_allocate(ctx, module); + let out_err_success_fn = ensure_out_err_success(ctx, module); + let _ = builder + .build_call(out_err_success_fn, &[out_err.into()], "") + .expect("initialize out_err"); + let release_array_fn = ensure_dynamic_qubit_array_release(ctx, module); + let entry_block = entry; + let blocks = create_dynamic_qubit_array_allocate_blocks(ctx, function); builder - .build_call( - barrier_func, - &[ - array_ptr.into(), - i64_type - .const_int( - u64::try_from(num_qubits) - .map_err(|e| format!("Failed to convert num_qubits: {e}"))?, - false, - ) - .into(), - ], - "", + .build_unconditional_branch(blocks.loop_header) + .expect("jump loop_header"); + builder.position_at_end(blocks.loop_header); + let idx_phi = builder.build_phi(ctx.i64_type(), "idx").expect("idx phi"); + idx_phi.add_incoming(&[(&ctx.i64_type().const_zero(), entry_block)]); + let idx = idx_phi.as_basic_value().into_int_value(); + let cond = builder + .build_int_compare(inkwell::IntPredicate::ULT, idx, len, "loop_cond") + .expect("loop cond"); + builder + .build_conditional_branch(cond, blocks.loop_body, blocks.loop_exit) + .expect("loop branch"); + + builder.position_at_end(blocks.loop_body); + let elem_ptr = unsafe { builder.build_gep(ptr_type, array_ptr, &[idx], "elem_ptr") } + .expect("elem gep"); + let slot = builder + .build_call(alloc_fn, &[out_err.into()], "qubit_slot") + .expect("alloc qubit"); + let slot = match slot.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv, + inkwell::values::ValueKind::Instruction(_) => { + unreachable!("Dynamic qubit allocation did not return a pointer"); + } + }; + let _ = builder.build_store(elem_ptr, slot).expect("store qubit"); + let out_err_is_null = + build_ptr_is_null(ctx, &builder, out_err, "out_err_int").expect("out_err null check"); + builder + .build_conditional_branch( + out_err_is_null, + blocks.continue_alloc, + blocks.maybe_rollback, ) - .map_err(|e| format!("Failed to build call to ___barrier: {e}"))?; + .expect("branch maybe_rollback"); + + builder.position_at_end(blocks.maybe_rollback); + let failed = builder + .build_load(ctx.bool_type(), out_err, "alloc_failed") + .expect("load out_err") + .into_int_value(); + builder + .build_conditional_branch(failed, blocks.rollback, blocks.continue_alloc) + .expect("branch rollback"); + build_dynamic_qubit_array_allocate_rollback( + ctx, + &builder, + idx, + array_ptr, + release_array_fn, + blocks.rollback, + blocks.rollback_done, + ); + + builder.position_at_end(blocks.continue_alloc); + let next = builder + .build_int_add(idx, ctx.i64_type().const_int(1, false), "next_idx") + .expect("next idx"); + builder + .build_unconditional_branch(blocks.loop_header) + .expect("jump loop_header"); + idx_phi.add_incoming(&[(&next, blocks.continue_alloc)]); + + builder.position_at_end(blocks.loop_exit); + builder.build_return(None).expect("return"); + builder.position_at_end(blocks.rollback_done); + builder.build_return(None).expect("return"); + function + } + + fn ensure_dynamic_qubit_array_release<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.qubit_array_release") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let function = module.add_function( + "qir_qis.qubit_array_release", + ctx.void_type() + .fn_type(&[ctx.i64_type().into(), ptr_type.into()], false), + Some(Linkage::Private), + ); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + builder.position_at_end(entry); + let len = function.get_nth_param(0).expect("len").into_int_value(); + let array_ptr = function + .get_nth_param(1) + .expect("array") + .into_pointer_value(); + let release_fn = ensure_dynamic_qubit_release(ctx, module); + build_pointer_array_release_helper( + ctx, + &builder, + &PointerArrayReleaseHelperArgs { + function, + len, + array_ptr, + release_fn, + ptr_elem_type: ptr_type, + gep_error: "Failed to build qubit array GEP", + load_error: "Failed to load dynamic qubit pointer", + release_error: "Failed to release dynamic qubit", + }, + ) + .expect("build dynamic qubit release loop"); + builder.build_return(None).expect("return"); + function + } + + fn replace_call_with_value<'ctx>( + instr: inkwell::values::InstructionValue<'ctx>, + value: BasicValueEnum<'ctx>, + ) -> Result<(), String> { + let instruction_val = value + .as_instruction_value() + .ok_or("Expected replacement value to be instruction-backed")?; + instr.replace_all_uses_with(&instruction_val); instr.erase_from_basic_block(); Ok(()) } - fn handle_read_result_call(args: &mut ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, - module, - instr, - fn_name, - global_mapping, - result_ssa, - .. - } = args; - let call_args: Vec = extract_operands(instr)?; - let result_ptr = call_args[0].into_pointer_value(); - let result_idx = get_index(result_ptr)?; - let result_idx_usize = checked_result_index(result_idx, result_ssa.len())?; - let meas_handle = result_ssa[result_idx_usize] - .ok_or_else(|| "Expected measurement handle".to_string())?; + fn initialize_dynamic_result_slot<'ctx>( + ctx: &'ctx Context, + builder: &inkwell::builder::Builder<'ctx>, + slot_ptr: PointerValue<'ctx>, + ) { + let slot_type = get_dynamic_result_slot_type(ctx); + let slot_ptrs = get_dynamic_result_slot_ptrs(builder, slot_type, slot_ptr); + clear_dynamic_result_slot(ctx, builder, &slot_ptrs); + } + fn ensure_out_err_success<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.out_err_success") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let function = module.add_function( + "qir_qis.out_err_success", + ctx.void_type().fn_type(&[ptr_type.into()], false), + Some(Linkage::Private), + ); let builder = ctx.create_builder(); - builder.position_before(instr); + let entry = ctx.append_basic_block(function, "entry"); + let ret = ctx.append_basic_block(function, "ret"); + let set_ok = ctx.append_basic_block(function, "set_ok"); + builder.position_at_end(entry); + let out_err = function + .get_first_param() + .expect("out_err") + .into_pointer_value(); + let is_null = build_ptr_is_null(ctx, &builder, out_err, "out_err_int").expect("null"); + builder + .build_conditional_branch(is_null, ret, set_ok) + .expect("branch"); + builder.position_at_end(set_ok); + let _ = builder.build_store(out_err, ctx.bool_type().const_zero()); + builder.build_unconditional_branch(ret).expect("jump"); + builder.position_at_end(ret); + builder.build_return(None).expect("ret"); + function + } - // Compute or reuse the bool value for this meas_handle - let bool_val = result_ssa[result_idx_usize] - .and_then(|v| v.1) - .and_then(|val| val.as_instruction_value()) - .map_or_else( - || { - let read_func = get_or_create_function( - module, - "___read_future_bool", - ctx.bool_type().fn_type(&[ctx.i64_type().into()], false), - ); - let bool_call = builder - .build_call(read_func, &[meas_handle.0.into()], "bool") - .map_err(|e| format!("Failed to build call for read_future_bool: {e}"))?; - let bool_val = match bool_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err( - "Failed to get basic value from read_future_bool call".into() - ); - } - }; + fn extract_const_len(value: BasicValueEnum<'_>, opname: &str) -> Result { + value + .into_int_value() + .get_zero_extended_constant() + .ok_or_else(|| format!("{opname} currently requires a constant array length")) + } - // Decrement refcount - let dec_func = get_or_create_function( - module, - "___dec_future_refcount", - ctx.void_type().fn_type(&[ctx.i64_type().into()], false), - ); - let _ = builder - .build_call(dec_func, &[meas_handle.0.into()], "") - .map_err(|e| { - format!("Failed to build call for dec_future_refcount: {e}") - })?; + fn lower_void_helper_call<'ctx>( + ctx: &'ctx Context, + instr: inkwell::values::InstructionValue<'ctx>, + helper: FunctionValue<'ctx>, + call_args: &[BasicValueEnum<'ctx>], + error_context: &str, + ) -> Result<(), String> { + let builder = ctx.create_builder(); + builder.position_before(&instr); + let metadata_args: Vec> = + call_args.iter().copied().map(Into::into).collect(); + let _ = builder + .build_call(helper, &metadata_args, "") + .map_err(|e| format!("{error_context}: {e}"))?; + instr.erase_from_basic_block(); + Ok(()) + } - // Store the result in SSA for reuse - result_ssa[result_idx_usize] = Some((meas_handle.0, Some(bool_val))); - Ok(bool_val) - }, - |val| { - val.as_any_value_enum() - .try_into() - .map_err(|()| "Expected BasicValueEnum".to_string()) - }, - )?; + fn lower_dynamic_qubit_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let builder = args.ctx.create_builder(); + builder.position_before(&args.instr); + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_qubit_allocate(args.ctx, module_ref(args)); + let value = call_basic_value( + &builder, + helper, + &[call_args[0].into()], + "dyn_q", + "Failed to lower dynamic qubit allocation", + "Dynamic qubit allocate helper did not return a pointer", + )?; + replace_call_with_value(args.instr, value) + } - if *fn_name == "__quantum__rt__read_result" { - let instruction_val = bool_val - .as_instruction_value() - .ok_or("Failed to convert bool_val to instruction value")?; - instr.replace_all_uses_with(&instruction_val); - } else { - // "__quantum__rt__result_record_output" - let gep = call_args[1]; - let new_global = if let Ok(old_global) = parse_gep(gep) { - global_mapping - .get(old_global.as_str()) - .copied() - .ok_or_else(|| format!("Output global `{old_global}` not found in mapping"))? - } else { - let fallback_label = format!("result_{result_idx}"); - let (new_const, new_name) = - build_result_global(ctx, &fallback_label, &fallback_label, "RESULT", None)?; - let new_global = module.add_global(new_const.get_type(), None, &new_name); - new_global.set_initializer(&new_const); - new_global.set_linkage(inkwell::module::Linkage::Private); - new_global.set_constant(true); - global_mapping.insert(fallback_label, new_global); - new_global - }; + fn lower_dynamic_qubit_release(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_qubit_release(args.ctx, module_ref(args)); + lower_void_helper_call( + args.ctx, + args.instr, + helper, + &call_args[..1], + "Failed to lower dynamic qubit release", + ) + } - let print_func = get_or_create_function( - module, - "print_bool", - ctx.void_type().fn_type( - &[ - ctx.ptr_type(AddressSpace::default()).into(), // ptr - ctx.i64_type().into(), // i64 - ctx.bool_type().into(), // i1 - ], - false, - ), - ); + fn lower_dynamic_qubit_array_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_qubit_array_allocate(args.ctx, module_ref(args)); + lower_void_helper_call( + args.ctx, + args.instr, + helper, + &call_args[..3], + "Failed to lower dynamic qubit array allocation", + ) + } - add_print_call(ctx, &builder, new_global, print_func, bool_val)?; - } - instr.erase_from_basic_block(); - Ok(()) + fn lower_dynamic_qubit_array_release(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_qubit_array_release(args.ctx, module_ref(args)); + lower_void_helper_call( + args.ctx, + args.instr, + helper, + &call_args[..2], + "Failed to lower dynamic qubit array release", + ) } - pub fn checked_result_index(result_idx: u64, result_ssa_len: usize) -> Result { - let result_idx_usize = usize::try_from(result_idx) - .map_err(|e| format!("Failed to convert result index to usize: {e}"))?; - if result_idx_usize >= result_ssa_len { - return Err(format!( - "Result index {result_idx} exceeds required_num_results ({result_ssa_len})" - )); + fn ensure_dynamic_result_setter<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.result_set_pending") { + return existing; } - Ok(result_idx_usize) + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let slot_type = get_dynamic_result_slot_type(ctx); + let function = module.add_function( + "qir_qis.result_set_pending", + ctx.void_type() + .fn_type(&[ptr_type.into(), ctx.i64_type().into()], false), + Some(Linkage::Private), + ); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + builder.position_at_end(entry); + let result_ptr = function + .get_nth_param(0) + .expect("result") + .into_pointer_value(); + let future = function.get_nth_param(1).expect("future").into_int_value(); + let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr); + set_dynamic_result_pending(ctx, &builder, &slot_ptrs, future); + builder.build_return(None).expect("ret"); + function } - fn handle_classical_record_output(args: &mut ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, - module, - instr, - fn_name, - global_mapping, - .. - } = args; - let call_args: Vec = extract_operands(instr)?; - let (print_func_name, value, type_tag) = match *fn_name { - "__quantum__rt__bool_record_output" => ( - "print_bool", - call_args[0].into_int_value().as_basic_value_enum(), - "BOOL", - ), - "__quantum__rt__int_record_output" => ( - "print_int", - call_args[0].into_int_value().as_basic_value_enum(), - "INT", - ), - "__quantum__rt__double_record_output" => ( - "print_float", - call_args[0].into_float_value().as_basic_value_enum(), - "FLOAT", - ), - _ => unreachable!(), - }; - - // Get the print function type based on the value type - let ret_type = ctx.void_type(); - let param_types = &[ - ctx.ptr_type(AddressSpace::default()).into(), // ptr - ctx.i64_type().into(), // i64 - match type_tag { - "BOOL" => ctx.bool_type().into(), - "INT" => ctx.i64_type().into(), - "FLOAT" => ctx.f64_type().into(), - _ => unreachable!(), - }, - ]; - let fn_type = ret_type.fn_type(param_types, false); - - let print_func = get_or_create_function(module, print_func_name, fn_type); - - let parsed_name = parse_gep(call_args[1]); - let old_name = parsed_name.clone().unwrap_or_else(|_| { - format!( - "anon_classical_{}", - match type_tag { - "BOOL" => "bool", - "INT" => "int", - "FLOAT" => "float", - _ => "value", - } + fn ensure_dynamic_result_read<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.result_read") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let slot_type = get_dynamic_result_slot_type(ctx); + let function = module.add_function( + "qir_qis.result_read", + ctx.bool_type().fn_type(&[ptr_type.into()], false), + Some(Linkage::Private), + ); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + let ret_false = ctx.append_basic_block(function, "ret_false"); + let pending = ctx.append_basic_block(function, "pending"); + let cached = ctx.append_basic_block(function, "cached"); + builder.position_at_end(entry); + let result_ptr = function + .get_first_param() + .expect("result") + .into_pointer_value(); + let is_null = + build_ptr_is_null(ctx, &builder, result_ptr, "result_int").expect("null check"); + let read_state = ctx.append_basic_block(function, "read_state"); + builder + .build_conditional_branch(is_null, ret_false, read_state) + .expect("branch"); + + builder.position_at_end(read_state); + let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr); + let state = builder + .build_load(ctx.i8_type(), slot_ptrs.state, "state") + .expect("load state") + .into_int_value(); + let is_pending = builder + .build_int_compare( + inkwell::IntPredicate::EQ, + state, + ctx.i8_type().const_int(1, false), + "is_pending", ) - }); - - let full_tag = if let Some(existing) = global_mapping.get(old_name.as_str()) { - get_string_label(*existing)? - } else if parsed_name.is_ok() { - return Err(format!("Output global `{old_name}` not found in mapping")); - } else { - old_name.clone() - }; - // Parse the label from the global string (format: USER:RESULT:tag) - let old_label = full_tag - .rfind(':') - .and_then(|pos| pos.checked_add(1)) - .map_or_else(|| full_tag.clone(), |pos| full_tag[pos..].to_string()); - - let (new_const, new_name) = - build_result_global(ctx, &old_label, &old_name, type_tag, None)?; - - let new_global = module.add_global(new_const.get_type(), None, &new_name); - new_global.set_initializer(&new_const); - new_global.set_linkage(inkwell::module::Linkage::Private); - new_global.set_constant(true); - global_mapping.insert(old_name, new_global); - record_classical_output(ctx, **instr, new_global, print_func, value)?; - Ok(()) - } + .expect("cmp"); + let is_cached = builder + .build_int_compare( + inkwell::IntPredicate::EQ, + state, + ctx.i8_type().const_int(2, false), + "is_cached", + ) + .expect("cmp"); + let check_cached = ctx.append_basic_block(function, "check_cached"); + builder + .build_conditional_branch(is_pending, pending, check_cached) + .expect("branch"); - fn handle_get_current_shot(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; - let builder = ctx.create_builder(); - builder.position_before(instr); + builder.position_at_end(check_cached); + builder + .build_conditional_branch(is_cached, cached, ret_false) + .expect("branch"); - let get_shot_func = get_or_create_function( + builder.position_at_end(pending); + build_dynamic_result_pending_return( + ctx, module, - // fun get_current_shot() -> uint - "get_current_shot", - ctx.i64_type().fn_type(&[], false), + &builder, + slot_ptrs.future, + slot_ptrs.cached, + slot_ptrs.state, ); - let shot_call = builder - .build_call(get_shot_func, &[], "current_shot") - .map_err(|e| format!("Failed to build call to get_current_shot: {e}"))?; - - let shot_result = match shot_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from get_current_shot call".into()); - } - }; - - if let Some(instr_val) = shot_result.as_instruction_value() { - instr.replace_all_uses_with(&instr_val); - } + builder.position_at_end(cached); + let cached_val = builder + .build_load(ctx.bool_type(), slot_ptrs.cached, "cached") + .expect("cached load"); + builder + .build_return(Some(&cached_val.into_int_value())) + .expect("ret cached"); - instr.erase_from_basic_block(); - Ok(()) + builder.position_at_end(ret_false); + builder + .build_return(Some(&ctx.bool_type().const_zero())) + .expect("ret false"); + function } - fn handle_random_seed(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; + fn ensure_dynamic_result_release<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.result_release") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let slot_type = get_dynamic_result_slot_type(ctx); + let function = module.add_function( + "qir_qis.result_release", + ctx.void_type().fn_type(&[ptr_type.into()], false), + Some(Linkage::Private), + ); let builder = ctx.create_builder(); - builder.position_before(instr); - - let call_args: Vec = extract_operands(instr)?; - let seed = call_args[0]; - - let random_seed_func = get_or_create_function( + let entry = ctx.append_basic_block(function, "entry"); + let ret = ctx.append_basic_block(function, "ret"); + let body = ctx.append_basic_block(function, "body"); + builder.position_at_end(entry); + let result_ptr = function + .get_first_param() + .expect("result") + .into_pointer_value(); + let is_null = + build_ptr_is_null(ctx, &builder, result_ptr, "result_int").expect("null check"); + builder + .build_conditional_branch(is_null, ret, body) + .expect("branch"); + builder.position_at_end(body); + let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr); + let state = builder + .build_load(ctx.i8_type(), slot_ptrs.state, "state") + .expect("load state") + .into_int_value(); + let is_pending = builder + .build_int_compare( + inkwell::IntPredicate::EQ, + state, + ctx.i8_type().const_int(1, false), + "is_pending", + ) + .expect("cmp"); + let dec_block = ctx.append_basic_block(function, "dec_future"); + let clear_block = ctx.append_basic_block(function, "clear"); + builder + .build_conditional_branch(is_pending, dec_block, clear_block) + .expect("branch"); + builder.position_at_end(dec_block); + let future = builder + .build_load(ctx.i64_type(), slot_ptrs.future, "future") + .expect("load future"); + let dec_fn = get_or_create_function( module, - // void random_seed(uint64_t seq) - "random_seed", + "___dec_future_refcount", ctx.void_type().fn_type(&[ctx.i64_type().into()], false), ); - - let _ = builder - .build_call(random_seed_func, &[seed.into()], "") - .map_err(|e| format!("Failed to build call to random_seed: {e}"))?; - - instr.erase_from_basic_block(); - Ok(()) + let _ = builder.build_call(dec_fn, &[future.into()], ""); + builder + .build_unconditional_branch(clear_block) + .expect("jump"); + builder.position_at_end(clear_block); + clear_dynamic_result_slot(ctx, &builder, &slot_ptrs); + builder.build_unconditional_branch(ret).expect("jump"); + builder.position_at_end(ret); + builder.build_return(None).expect("ret"); + function } - fn handle_random_int(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; + fn ensure_dynamic_result_array_release<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> FunctionValue<'ctx> { + if let Some(existing) = module.get_function("qir_qis.result_array_release") { + return existing; + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let function = module.add_function( + "qir_qis.result_array_release", + ctx.void_type() + .fn_type(&[ctx.i64_type().into(), ptr_type.into()], false), + Some(Linkage::Private), + ); let builder = ctx.create_builder(); - builder.position_before(instr); + let entry = ctx.append_basic_block(function, "entry"); + builder.position_at_end(entry); + let len = function.get_nth_param(0).expect("len").into_int_value(); + let array_ptr = function + .get_nth_param(1) + .expect("array") + .into_pointer_value(); + let release_fn = ensure_dynamic_result_release(ctx, module); + let ptr_elem_type = ctx.ptr_type(AddressSpace::default()); + build_pointer_array_release_helper( + ctx, + &builder, + &PointerArrayReleaseHelperArgs { + function, + len, + array_ptr, + release_fn, + ptr_elem_type, + gep_error: "Failed to build result array GEP", + load_error: "Failed to load dynamic result pointer", + release_error: "Failed to release dynamic result", + }, + ) + .expect("build dynamic result release loop"); + builder.build_return(None).expect("ret"); + function + } - let random_int_func = get_or_create_function( + fn ensure_dynamic_result_array_record_output<'ctx>( + ctx: &'ctx Context, + module: &Module<'ctx>, + ) -> Result, String> { + if let Some(existing) = module.get_function("qir_qis.result_array_record_output") { + return Ok(existing); + } + let ptr_type = ctx.ptr_type(AddressSpace::default()); + let function = module.add_function( + "qir_qis.result_array_record_output", + ctx.void_type().fn_type( + &[ + ctx.i64_type().into(), + ptr_type.into(), + ptr_type.into(), + ctx.i64_type().into(), + ], + false, + ), + Some(Linkage::Private), + ); + let builder = ctx.create_builder(); + let entry = ctx.append_basic_block(function, "entry"); + builder.position_at_end(entry); + let len = function.get_nth_param(0).expect("len").into_int_value(); + let array_ptr = function + .get_nth_param(1) + .expect("array") + .into_pointer_value(); + let tag_ptr = function.get_nth_param(2).expect("tag").into_pointer_value(); + let tag_len = function.get_nth_param(3).expect("tag_len").into_int_value(); + let print_bool_arr = get_or_create_function( module, - // uint32_t random_int() - "random_int", - ctx.i32_type().fn_type(&[], false), + "print_bool_arr", + ctx.void_type().fn_type( + &[ptr_type.into(), ctx.i64_type().into(), ptr_type.into()], + false, + ), ); + let read_fn = ensure_dynamic_result_read(ctx, module); + let bool_arr = + build_dynamic_result_array_values(ctx, function, &builder, len, array_ptr, read_fn)?; + let array_desc = build_dynamic_result_array_descriptor(ctx, &builder, len, bool_arr)?; + let _ = builder + .build_call( + print_bool_arr, + &[tag_ptr.into(), tag_len.into(), array_desc.into()], + "", + ) + .map_err(|e| format!("Failed to print result array: {e}"))?; + builder.build_return(None).expect("ret"); + Ok(function) + } - let random_call = builder - .build_call(random_int_func, &[], "rint") - .map_err(|e| format!("Failed to build call to random_int: {e}"))?; + fn lower_dynamic_result_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let builder = args.ctx.create_builder(); + builder.position_before(&args.instr); + let call_args: Vec = extract_operands(&args.instr)?; + let slot_ptr = builder + .build_alloca(get_dynamic_result_slot_type(args.ctx), "dyn_result") + .map_err(|e| format!("Failed to allocate dynamic result slot: {e}"))?; + initialize_dynamic_result_slot(args.ctx, &builder, slot_ptr); + let helper = ensure_out_err_success(args.ctx, module_ref(args)); + let _ = builder + .build_call(helper, &[call_args[0].into()], "") + .map_err(|e| format!("Failed to store dynamic result out_err success flag: {e}"))?; + replace_call_with_value(args.instr, slot_ptr.as_basic_value_enum()) + } - let random_result = match random_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from random_int call".into()); - } - }; + fn lower_dynamic_result_release(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_result_release(args.ctx, module_ref(args)); + lower_void_helper_call( + args.ctx, + args.instr, + helper, + &call_args[..1], + "Failed to lower dynamic result release", + ) + } - if let Some(instr_val) = random_result.as_instruction_value() { - instr.replace_all_uses_with(&instr_val); + fn lower_dynamic_result_array_allocate(args: &mut ProcessCallArgs<'_>) -> Result<(), String> { + let builder = args.ctx.create_builder(); + builder.position_before(&args.instr); + let call_args: Vec = extract_operands(&args.instr)?; + let len = extract_const_len(call_args[0], "__quantum__rt__result_array_allocate")?; + let array_ptr = call_args[1].into_pointer_value(); + let out_err = call_args[2].into_pointer_value(); + let ptr_type = args.ctx.ptr_type(AddressSpace::default()); + for idx in 0..len { + let elem_ptr = unsafe { + builder.build_gep( + ptr_type, + array_ptr, + &[args.ctx.i64_type().const_int(idx, false)], + "result_elem_ptr", + ) + } + .map_err(|e| format!("Failed to build result array element GEP: {e}"))?; + let slot_ptr = builder + .build_alloca(get_dynamic_result_slot_type(args.ctx), "dyn_result") + .map_err(|e| format!("Failed to allocate dynamic result slot: {e}"))?; + initialize_dynamic_result_slot(args.ctx, &builder, slot_ptr); + let _ = builder + .build_store(elem_ptr, slot_ptr) + .map_err(|e| format!("Failed to store dynamic result slot in array: {e}"))?; } - - instr.erase_from_basic_block(); + let helper = ensure_out_err_success(args.ctx, module_ref(args)); + let _ = builder + .build_call(helper, &[out_err.into()], "") + .map_err(|e| { + format!("Failed to store dynamic result array out_err success flag: {e}") + })?; + args.instr.erase_from_basic_block(); Ok(()) } - fn handle_random_float(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; - let builder = ctx.create_builder(); - builder.position_before(instr); + fn lower_dynamic_result_array_release(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let call_args: Vec = extract_operands(&args.instr)?; + let helper = ensure_dynamic_result_array_release(args.ctx, module_ref(args)); + lower_void_helper_call( + args.ctx, + args.instr, + helper, + &call_args[..2], + "Failed to lower dynamic result array release", + ) + } - let random_float_func = get_or_create_function( - module, - // double random_float() - "random_float", - ctx.f64_type().fn_type(&[], false), + fn lower_dynamic_result_array_record_output( + args: &mut ProcessCallArgs<'_>, + ) -> Result<(), String> { + let builder = args.ctx.create_builder(); + builder.position_before(&args.instr); + let call_args: Vec = extract_operands(&args.instr)?; + let old_name = parse_gep(call_args[2])?; + let full_tag = + if let Some(global) = unsafe { &mut *args.global_mapping }.get(old_name.as_str()) { + get_string_label(*global)? + } else { + return Err(format!("Output global `{old_name}` not found in mapping")); + }; + let old_label = full_tag + .rfind(':') + .and_then(|pos| pos.checked_add(1)) + .map_or_else(|| full_tag.clone(), |pos| full_tag[pos..].to_string()); + let (new_const, new_name) = + build_result_global(args.ctx, &old_label, &old_name, "RESULT_ARRAY", None)?; + let new_global = module_ref(args).add_global(new_const.get_type(), None, &new_name); + new_global.set_initializer(&new_const); + new_global.set_linkage(Linkage::Private); + new_global.set_constant(true); + unsafe { &mut *args.global_mapping }.insert(old_name, new_global); + let helper = ensure_dynamic_result_array_record_output(args.ctx, module_ref(args))?; + let tag_len = args.ctx.i64_type().const_int( + u64::from(new_const.get_type().len()).saturating_sub(1), + false, ); - - let random_call = builder - .build_call(random_float_func, &[], "rfloat") - .map_err(|e| format!("Failed to build call to random_float: {e}"))?; - - let random_result = match random_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from random_float call".into()); - } - }; - - if let Some(instr_val) = random_result.as_instruction_value() { - instr.replace_all_uses_with(&instr_val); + let tag_ptr = unsafe { + builder.build_gep( + new_const.get_type(), + new_global.as_pointer_value(), + &[ + args.ctx.i64_type().const_zero(), + args.ctx.i64_type().const_zero(), + ], + "tag_gep", + ) } - - instr.erase_from_basic_block(); + .map_err(|e| format!("Failed to build result array tag GEP: {e}"))?; + let _ = builder + .build_call( + helper, + &[ + call_args[0].into(), + call_args[1].into(), + tag_ptr.into(), + tag_len.into(), + ], + "", + ) + .map_err(|e| format!("Failed to lower result array record output: {e}"))?; + args.instr.erase_from_basic_block(); Ok(()) } - fn handle_random_int_bounded(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; + #[allow(clippy::too_many_arguments)] + fn handle_mz_call<'ctx>( + ctx: &'ctx Context, + module: *const (), + instr: &'ctx inkwell::values::InstructionValue<'ctx>, + fn_name: &str, + capability_flags: CapabilityFlags, + qubit_array: Option>, + qubit_array_type: Option>, + result_ssa: *mut (), + ) -> Result<(), String> { + let module = unsafe { &*module.cast::>() }; + if fn_name == "__quantum__qis__m__body" { + log::warn!( + "`__quantum__qis__m__body` is from Q# QDK, synonym for `__quantum__qis__mz__body`" + ); + } let builder = ctx.create_builder(); builder.position_before(instr); + // Extract qubit and result indices let call_args: Vec = extract_operands(instr)?; - let bound = call_args[0]; + let qubit_ptr = call_args[0].into_pointer_value(); + let result_ptr = call_args[1].into_pointer_value(); - let random_rng_func = get_or_create_function( - module, - // uint32_t random_rng(uint32_t bound) - "random_rng", - ctx.i32_type().fn_type(&[ctx.i32_type().into()], false), - ); + let q_handle = get_qubit_handle( + ctx, + capability_flags, + qubit_array, + qubit_array_type, + &builder, + qubit_ptr, + )?; - let rng_call = builder - .build_call(random_rng_func, &[bound.into()], "rintb") - .map_err(|e| format!("Failed to build call to random_rng: {e}"))?; + // Create ___lazy_measure call + let meas = { + let meas_func = get_or_create_function( + module, + "___lazy_measure", + ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), + ); - let rng_result = match rng_call.try_as_basic_value() { - inkwell::values::ValueKind::Basic(bv) => bv, - inkwell::values::ValueKind::Instruction(_) => { - return Err("Failed to get basic value from random_rng call".into()); + let call = builder.build_call(meas_func, &[q_handle.into()], "meas"); + let call_result = + call.map_err(|e| format!("Failed to build call for lazy measure function: {e}"))?; + match call_result.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv, + inkwell::values::ValueKind::Instruction(_) => { + return Err("Failed to get basic value from lazy measure call".into()); + } } }; - if let Some(instr_val) = rng_result.as_instruction_value() { - instr.replace_all_uses_with(&instr_val); + // Store measurement result + if capability_flags.dynamic_result_management { + let set_result_fn = ensure_dynamic_result_setter(ctx, module); + let _ = builder + .build_call(set_result_fn, &[result_ptr.into(), meas.into()], "") + .map_err(|e| format!("Failed to update dynamic result state: {e}"))?; + } else { + let result_ssa = unsafe { + &mut *result_ssa + .cast::, Option>)>>>() + }; + let result_idx = get_index(result_ptr)?; + let result_idx_usize = checked_result_index(result_idx, result_ssa.len())?; + result_ssa[result_idx_usize] = Some((meas, None)); + } + + if fn_name == "__quantum__qis__mresetz__body" { + log::warn!("`__quantum__qis__mresetz__body` is from Q# QDK"); + // Create ___reset call + create_reset_call(ctx, module, &builder, q_handle); } + // Remove original call instr.erase_from_basic_block(); Ok(()) } - fn handle_random_advance(args: &ProcessCallArgs) -> Result<(), String> { - let ProcessCallArgs { - ctx, module, instr, .. - } = args; + fn handle_mz_leaked_call(args: &ProcessCallArgs) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); let builder = ctx.create_builder(); builder.position_before(instr); + let call = CallSiteValue::try_from(args.instr) + .map_err(|()| "Malformed mz_leaked call: instruction is not a call site".to_string())?; + let called_fn = call + .get_called_fn_value() + .ok_or_else(|| "Malformed mz_leaked call: missing callee".to_string())?; + let fn_type = called_fn.get_type(); + let param_types = fn_type.get_param_types(); + let has_expected_signature = fn_type + .get_return_type() + .is_some_and(|ty| ty.is_int_type() && ty.into_int_type().get_bit_width() == 64) + && param_types.len() == 1 + && param_types[0].is_pointer_type(); + if !has_expected_signature { + return Err("Malformed mz_leaked call: expected signature i64 (ptr)".to_string()); + } - let call_args: Vec = extract_operands(instr)?; - let delta = call_args[0]; - - let random_advance_func = get_or_create_function( - module, - // void random_advance(uint64_t delta) - "random_advance", - ctx.void_type().fn_type(&[ctx.i64_type().into()], false), - ); - - let _ = builder - .build_call(random_advance_func, &[delta.into()], "") - .map_err(|e| format!("Failed to build call to random_advance: {e}"))?; + let call_args: Vec = extract_operands(instr)?; + let qubit_ptr = mz_leaked_qubit_operand(&call_args)?; - instr.erase_from_basic_block(); - Ok(()) - } -} + let q_handle = { + let idx_fn = module + .get_function(LOAD_QUBIT_FN) + .ok_or_else(|| format!("{LOAD_QUBIT_FN} not found"))?; + let idx_call = builder + .build_call(idx_fn, &[qubit_ptr.into()], "qbit") + .map_err(|e| format!("Failed to build call to {LOAD_QUBIT_FN}: {e}"))?; + match idx_call.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv, + inkwell::values::ValueKind::Instruction(_) => { + return Err(format!( + "Failed to get basic value from {LOAD_QUBIT_FN} call" + )); + } + } + }; -pub(crate) fn decode_llvm_bytes(value: &[u8]) -> Option<&str> { - std::str::from_utf8(value).ok() -} + let meas_handle = { + let meas_func = get_or_create_function( + module, + "___lazy_measure_leaked", + ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), + ); -pub(crate) fn decode_llvm_c_string(value: &std::ffi::CStr) -> Option<&str> { - value.to_str().ok() -} + let call = builder.build_call(meas_func, &[q_handle.into()], "meas_leaked"); + let call_result = call.map_err(|e| { + format!("Failed to build call for lazy leaked measure function: {e}") + })?; + match call_result.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv, + inkwell::values::ValueKind::Instruction(_) => { + return Err("Failed to get basic value from lazy leaked measure call".into()); + } + } + }; -fn create_memory_buffer_from_bytes( - bytes: &[u8], - name: &str, -) -> Result, String> { - use llvm_sys::core::LLVMCreateMemoryBufferWithMemoryRangeCopy; + let meas_value = { + let read_func = get_or_create_function( + module, + "___read_future_uint", + ctx.i64_type().fn_type(&[ctx.i64_type().into()], false), + ); + let call = builder.build_call(read_func, &[meas_handle.into()], "meas_leaked_value"); + let call_result = + call.map_err(|e| format!("Failed to build call for read_future_uint: {e}"))?; + match call_result.try_as_basic_value() { + inkwell::values::ValueKind::Basic(bv) => bv, + inkwell::values::ValueKind::Instruction(_) => { + return Err("Failed to get basic value from read_future_uint call".into()); + } + } + }; - let name = std::ffi::CString::new(name) - .map_err(|_| "Memory buffer name contains interior NUL byte".to_string())?; - let memory_buffer = unsafe { - LLVMCreateMemoryBufferWithMemoryRangeCopy(bytes.as_ptr().cast(), bytes.len(), name.as_ptr()) - }; - if memory_buffer.is_null() { - return Err( - "LLVM failed to create memory buffer from bytes: received null memory buffer pointer" - .to_string(), + let dec_func = get_or_create_function( + module, + "___dec_future_refcount", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), ); - } - - unsafe { Ok(inkwell::memory_buffer::MemoryBuffer::new(memory_buffer)) } -} + let _ = builder + .build_call(dec_func, &[meas_handle.into()], "") + .map_err(|e| format!("Failed to build call for dec_future_refcount: {e}"))?; -pub(crate) fn create_module_from_ir_text<'ctx>( - ctx: &'ctx inkwell::context::Context, - ll_text: &str, - name: &str, -) -> Result, String> { - let memory_buffer = create_memory_buffer_from_bytes(ll_text.as_bytes(), name)?; - ctx.create_module_from_ir(memory_buffer) - .map_err(|e| format!("Failed to create module from LLVM IR: {e}")) -} + let instruction_val = meas_value + .as_instruction_value() + .ok_or("Failed to convert leaked measurement value to instruction value")?; + instr.replace_all_uses_with(&instruction_val); + instr.erase_from_basic_block(); + Ok(()) + } -fn memory_buffer_to_owned_bytes( - memory_buffer: &inkwell::memory_buffer::MemoryBuffer<'_>, -) -> Vec { - let bytes = memory_buffer.as_slice(); - if bytes.last() == Some(&0) { - bytes[..bytes.len().saturating_sub(1)].to_vec() - } else { - bytes.to_vec() + pub fn mz_leaked_qubit_operand<'ctx>( + call_args: &[BasicValueEnum<'ctx>], + ) -> Result, String> { + match call_args { + [BasicValueEnum::PointerValue(ptr), _] => Ok(*ptr), + [_, _] => { + Err("Malformed mz_leaked call: expected first argument to be a pointer".into()) + } + _ => Err(format!( + "Malformed mz_leaked call: expected 1 argument plus callee, got {} operands", + call_args.len() + )), + } } -} -pub(crate) fn parse_bitcode_module<'ctx>( - ctx: &'ctx inkwell::context::Context, - bitcode: &[u8], - name: &str, -) -> Result, String> { - let memory_buffer = create_memory_buffer_from_bytes(bitcode, name)?; - inkwell::module::Module::parse_bitcode_from_buffer(&memory_buffer, ctx) - .map_err(|e| format!("Failed to parse bitcode: {e}")) -} + fn handle_reset_call(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let builder = ctx.create_builder(); + builder.position_before(instr); -/// Core QIR to QIS translation logic. -/// -/// # Arguments -/// - `bc_bytes` - The QIR bytes to translate. -/// - `opt_level` - The optimization level to use (0-3). Platform defaults are -/// exposed via [`DEFAULT_OPT_LEVEL`]. -/// - `target` - Target architecture ("aarch64", "x86-64", "native"). Platform -/// defaults are exposed via [`DEFAULT_TARGET`]. -/// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. -/// -/// # Errors -/// Returns an error string if the translation fails. -pub fn qir_to_qis( - bc_bytes: &[u8], - opt_level: u32, - target: &str, - _wasm_bytes: Option<&[u8]>, -) -> Result, String> { - use crate::{ - aux::process_entry_function, - convert::{ - add_qmain_wrapper, create_qubit_array, find_entry_function, free_all_qubits, - get_string_attrs, process_ir_defined_q_fns, prune_unused_ir_qis_helpers, - }, - decompose::add_decompositions, - opt::optimize, - utils::add_generator_metadata, - }; - use inkwell::{attributes::AttributeLoc, context::Context}; - use std::{collections::BTreeMap, env}; + // Extract qubit index + let call_args: Vec = extract_operands(instr)?; + let qubit_ptr = call_args[0].into_pointer_value(); - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; - crate::llvm_verify::verify_module(&module, "LLVM module verification failed after parse")?; + let q_handle = get_qubit_handle( + ctx, + args.capability_flags, + args.qubit_array, + args.qubit_array_type, + &builder, + qubit_ptr, + )?; - add_decompositions(&ctx, &module) - .map_err(|e| format!("Failed to add QIR decompositions: {e}"))?; - let entry_fn = find_entry_function(&module) - .map_err(|e| format!("Failed to find entry function in QIR module: {e}"))?; + // Create ___reset call + create_reset_call(ctx, module, &builder, q_handle); - let entry_fn_name = entry_fn - .get_name() - .to_str() - .map_err(|e| format!("Invalid UTF-8 in entry function name: {e}"))?; + instr.erase_from_basic_block(); + Ok(()) + } - log::trace!("Entry function: {entry_fn_name}"); - let new_name = format!("___user_qir_{entry_fn_name}"); - entry_fn.as_global_value().set_name(&new_name); - log::debug!("Renamed entry function to: {new_name}"); - let qubit_array = create_qubit_array(&ctx, &module, entry_fn)?; + fn parse_barrier_arity(fn_name: &str) -> Result { + fn_name + .strip_prefix("__quantum__qis__barrier") + .and_then(|s| s.strip_suffix("__body")) + .and_then(|s| s.parse::().ok()) + .filter(|&n| n > 0) + .ok_or_else(|| format!("Invalid barrier function name: {fn_name}")) + } - let wasm_fns: BTreeMap = BTreeMap::new(); - process_entry_function(&ctx, &module, entry_fn, &wasm_fns, qubit_array)?; + #[allow(clippy::too_many_lines)] + fn handle_barrier_call(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { + ctx, + instr, + fn_name, + .. + } = args; + let module = module_ref(args); + let builder = ctx.create_builder(); + builder.position_before(instr); - // Handle IR defined functions that take qubits - process_ir_defined_q_fns(&ctx, &module, entry_fn)?; + let num_qubits = parse_barrier_arity(fn_name)?; - free_all_qubits(&ctx, &module, entry_fn, qubit_array)?; + // Extract qubit arguments (excluding the last operand which is the function pointer) + let all_operands: Vec = extract_operands(instr)?; + let num_operands = all_operands + .len() + .checked_sub(1) + .ok_or("Expected at least one operand")?; - // Add qmain wrapper that calls setup, entry function, and teardown - let _ = add_qmain_wrapper(&ctx, &module, entry_fn); + if num_operands != num_qubits { + return Err(format!( + "Barrier function {fn_name} expects {num_qubits} arguments, got {num_operands}" + )); + } - crate::llvm_verify::verify_module(&module, "LLVM module verification failed")?; + let call_args = &all_operands[..num_operands]; - // Clean up the translated module - for attr in get_string_attrs(entry_fn) { - let kind = decode_string_attribute_kind(attr)?; - entry_fn.remove_string_attribute(AttributeLoc::Function, &kind); - } + // Load qubit handles into an array + let i64_type = ctx.i64_type(); + let array_type = i64_type.array_type( + u32::try_from(num_qubits).map_err(|e| format!("Failed to convert num_qubits: {e}"))?, + ); + let array_alloca = builder + .build_alloca(array_type, "barrier_qubits") + .map_err(|e| format!("Failed to allocate array for barrier qubits: {e}"))?; - // TODO: remove global module metadata - // seems inkwell doesn't support this yet + for (i, arg) in call_args.iter().enumerate() { + let qubit_ptr = arg.into_pointer_value(); + let q_handle = get_qubit_handle( + ctx, + args.capability_flags, + args.qubit_array, + args.qubit_array_type, + &builder, + qubit_ptr, + )?; + + let elem_ptr = unsafe { + builder.build_gep( + array_type, + array_alloca, + &[ + i64_type.const_zero(), + i64_type.const_int( + u64::try_from(i) + .map_err(|e| format!("Failed to convert index: {e}"))?, + false, + ), + ], + "", + ) + } + .map_err(|e| format!("Failed to build GEP for barrier array: {e}"))?; + builder + .build_store(elem_ptr, q_handle) + .map_err(|e| format!("Failed to store qubit handle in array: {e}"))?; + } + + let array_ptr = unsafe { + builder.build_gep( + array_type, + array_alloca, + &[i64_type.const_zero(), i64_type.const_zero()], + "barrier_array_ptr", + ) + } + .map_err(|e| format!("Failed to build GEP for barrier array pointer: {e}"))?; + + // void ___barrier(i64* %qbs, i64 %qbs_len) + let barrier_func = get_or_create_function( + module, + "___barrier", + ctx.void_type().fn_type( + &[ + ctx.ptr_type(AddressSpace::default()).into(), + i64_type.into(), + ], + false, + ), + ); + + builder + .build_call( + barrier_func, + &[ + array_ptr.into(), + i64_type + .const_int( + u64::try_from(num_qubits) + .map_err(|e| format!("Failed to convert num_qubits: {e}"))?, + false, + ) + .into(), + ], + "", + ) + .map_err(|e| format!("Failed to build call to ___barrier: {e}"))?; + + instr.erase_from_basic_block(); + Ok(()) + } + + fn handle_read_result_call<'ctx>( + ctx: &'ctx Context, + module: *const (), + instr: &'ctx inkwell::values::InstructionValue<'ctx>, + fn_name: &str, + capability_flags: CapabilityFlags, + global_mapping: *mut (), + result_ssa: *mut (), + ) -> Result<(), String> { + let module = unsafe { &*module.cast::>() }; + let global_mapping = unsafe { + &mut *global_mapping.cast::>>() + }; + let result_ssa = unsafe { + &mut *result_ssa + .cast::, Option>)>>>() + }; + let call_args: Vec = extract_operands(instr)?; + let result_ptr = call_args[0].into_pointer_value(); + + let builder = ctx.create_builder(); + builder.position_before(instr); + + let bool_val = read_result_bool( + ctx, + module, + &builder, + result_ptr, + capability_flags, + result_ssa, + )?; + + if fn_name == "__quantum__rt__read_result" { + let instruction_val = bool_val + .as_instruction_value() + .ok_or("Failed to convert bool_val to instruction value")?; + instr.replace_all_uses_with(&instruction_val); + } else { + let new_global = get_or_create_result_output_global( + ctx, + module, + global_mapping, + capability_flags, + result_ptr, + call_args[1], + )?; + + let print_func = get_or_create_function( + module, + "print_bool", + ctx.void_type().fn_type( + &[ + ctx.ptr_type(AddressSpace::default()).into(), // ptr + ctx.i64_type().into(), // i64 + ctx.bool_type().into(), // i1 + ], + false, + ), + ); + + add_print_call(ctx, &builder, new_global, print_func, bool_val)?; + } + instr.erase_from_basic_block(); + Ok(()) + } + + pub fn checked_result_index(result_idx: u64, result_ssa_len: usize) -> Result { + let result_idx_usize = usize::try_from(result_idx) + .map_err(|e| format!("Failed to convert result index to usize: {e}"))?; + if result_idx_usize >= result_ssa_len { + return Err(format!( + "Result index {result_idx} exceeds required_num_results ({result_ssa_len})" + )); + } + Ok(result_idx_usize) + } + + fn handle_classical_record_output(args: &mut ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { + ctx, + instr, + fn_name, + .. + } = args; + let module = unsafe { &*args.module }; + let global_mapping = unsafe { &mut *args.global_mapping }; + let call_args: Vec = extract_operands(instr)?; + let (print_func_name, value, type_tag) = match fn_name.as_str() { + "__quantum__rt__bool_record_output" => ( + "print_bool", + call_args[0].into_int_value().as_basic_value_enum(), + "BOOL", + ), + "__quantum__rt__int_record_output" => ( + "print_int", + call_args[0].into_int_value().as_basic_value_enum(), + "INT", + ), + "__quantum__rt__double_record_output" => ( + "print_float", + call_args[0].into_float_value().as_basic_value_enum(), + "FLOAT", + ), + _ => unreachable!(), + }; + + // Get the print function type based on the value type + let ret_type = ctx.void_type(); + let param_types = &[ + ctx.ptr_type(AddressSpace::default()).into(), // ptr + ctx.i64_type().into(), // i64 + match type_tag { + "BOOL" => ctx.bool_type().into(), + "INT" => ctx.i64_type().into(), + "FLOAT" => ctx.f64_type().into(), + _ => unreachable!(), + }, + ]; + let fn_type = ret_type.fn_type(param_types, false); + + let print_func = get_or_create_function(module, print_func_name, fn_type); + + let parsed_name = parse_gep(call_args[1]); + let old_name = parsed_name.clone().unwrap_or_else(|_| { + format!( + "anon_classical_{}", + match type_tag { + "BOOL" => "bool", + "INT" => "int", + "FLOAT" => "float", + _ => "value", + } + ) + }); + + let full_tag = if let Some(existing) = global_mapping.get(old_name.as_str()) { + get_string_label(*existing)? + } else if parsed_name.is_ok() { + return Err(format!("Output global `{old_name}` not found in mapping")); + } else { + old_name.clone() + }; + // Parse the label from the global string (format: USER:RESULT:tag) + let old_label = full_tag + .rfind(':') + .and_then(|pos| pos.checked_add(1)) + .map_or_else(|| full_tag.clone(), |pos| full_tag[pos..].to_string()); + + let (new_const, new_name) = + build_result_global(ctx, &old_label, &old_name, type_tag, None)?; + + let new_global = module.add_global(new_const.get_type(), None, &new_name); + new_global.set_initializer(&new_const); + new_global.set_linkage(inkwell::module::Linkage::Private); + new_global.set_constant(true); + global_mapping.insert(old_name, new_global); + record_classical_output(ctx, *instr, new_global, print_func, value)?; + Ok(()) + } + + fn handle_get_current_shot(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let get_shot_func = get_or_create_function( + module, + "get_current_shot", + ctx.i64_type().fn_type(&[], false), + ); + handle_runtime_value_call( + ctx, + *instr, + get_shot_func, + &[], + "current_shot", + "Failed to build call to get_current_shot", + "Failed to get basic value from get_current_shot call", + ) + } + + fn handle_runtime_value_call<'ctx>( + ctx: &'ctx Context, + instr: inkwell::values::InstructionValue<'ctx>, + callee: FunctionValue<'ctx>, + call_args: &[BasicMetadataValueEnum<'ctx>], + name: &str, + build_error: &str, + value_error: &str, + ) -> Result<(), String> { + let builder = ctx.create_builder(); + builder.position_before(&instr); + let value = call_basic_value(&builder, callee, call_args, name, build_error, value_error)?; + if let Some(instr_val) = value.as_instruction_value() { + instr.replace_all_uses_with(&instr_val); + } + instr.erase_from_basic_block(); + Ok(()) + } + + fn handle_runtime_void_call<'ctx>( + ctx: &'ctx Context, + instr: inkwell::values::InstructionValue<'ctx>, + callee: FunctionValue<'ctx>, + call_args: &[BasicMetadataValueEnum<'ctx>], + build_error: &str, + ) -> Result<(), String> { + let builder = ctx.create_builder(); + builder.position_before(&instr); + let _ = builder + .build_call(callee, call_args, "") + .map_err(|e| format!("{build_error}: {e}"))?; + instr.erase_from_basic_block(); + Ok(()) + } + + fn handle_random_seed(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let call_args: Vec = extract_operands(instr)?; + let random_seed_func = get_or_create_function( + module, + "random_seed", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), + ); + handle_runtime_void_call( + ctx, + *instr, + random_seed_func, + &[call_args[0].into()], + "Failed to build call to random_seed", + ) + } + + fn handle_random_int(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let random_int_func = + get_or_create_function(module, "random_int", ctx.i32_type().fn_type(&[], false)); + handle_runtime_value_call( + ctx, + *instr, + random_int_func, + &[], + "rint", + "Failed to build call to random_int", + "Failed to get basic value from random_int call", + ) + } + + fn handle_random_float(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let random_float_func = + get_or_create_function(module, "random_float", ctx.f64_type().fn_type(&[], false)); + handle_runtime_value_call( + ctx, + *instr, + random_float_func, + &[], + "rfloat", + "Failed to build call to random_float", + "Failed to get basic value from random_float call", + ) + } + + fn handle_random_int_bounded(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let call_args: Vec = extract_operands(instr)?; + let random_rng_func = get_or_create_function( + module, + "random_rng", + ctx.i32_type().fn_type(&[ctx.i32_type().into()], false), + ); + handle_runtime_value_call( + ctx, + *instr, + random_rng_func, + &[call_args[0].into()], + "rintb", + "Failed to build call to random_rng", + "Failed to get basic value from random_rng call", + ) + } + + fn handle_random_advance(args: &ProcessCallArgs<'_>) -> Result<(), String> { + let ProcessCallArgs { ctx, instr, .. } = args; + let module = module_ref(args); + let call_args: Vec = extract_operands(instr)?; + let random_advance_func = get_or_create_function( + module, + "random_advance", + ctx.void_type().fn_type(&[ctx.i64_type().into()], false), + ); + handle_runtime_void_call( + ctx, + *instr, + random_advance_func, + &[call_args[0].into()], + "Failed to build call to random_advance", + ) + } +} + +pub(crate) fn decode_llvm_bytes(value: &[u8]) -> Option<&str> { + std::str::from_utf8(value).ok() +} + +pub(crate) fn decode_llvm_c_string(value: &std::ffi::CStr) -> Option<&str> { + value.to_str().ok() +} + +fn create_memory_buffer_from_bytes( + bytes: &[u8], + name: &str, +) -> Result, String> { + use llvm_sys::core::LLVMCreateMemoryBufferWithMemoryRangeCopy; + + let name = std::ffi::CString::new(name) + .map_err(|_| "Memory buffer name contains interior NUL byte".to_string())?; + let memory_buffer = unsafe { + LLVMCreateMemoryBufferWithMemoryRangeCopy(bytes.as_ptr().cast(), bytes.len(), name.as_ptr()) + }; + if memory_buffer.is_null() { + return Err( + "LLVM failed to create memory buffer from bytes: received null memory buffer pointer" + .to_string(), + ); + } + + unsafe { Ok(inkwell::memory_buffer::MemoryBuffer::new(memory_buffer)) } +} + +pub(crate) fn create_module_from_ir_text<'ctx>( + ctx: &'ctx inkwell::context::Context, + ll_text: &str, + name: &str, +) -> Result, String> { + let memory_buffer = create_memory_buffer_from_bytes(ll_text.as_bytes(), name)?; + ctx.create_module_from_ir(memory_buffer) + .map_err(|e| format!("Failed to create module from LLVM IR: {e}")) +} + +fn memory_buffer_to_owned_bytes( + memory_buffer: &inkwell::memory_buffer::MemoryBuffer<'_>, +) -> Vec { + let bytes = memory_buffer.as_slice(); + if bytes.last() == Some(&0) { + bytes[..bytes.len().saturating_sub(1)].to_vec() + } else { + bytes.to_vec() + } +} + +pub(crate) fn parse_bitcode_module<'ctx>( + ctx: &'ctx inkwell::context::Context, + bitcode: &[u8], + name: &str, +) -> Result, String> { + let memory_buffer = create_memory_buffer_from_bytes(bitcode, name)?; + inkwell::module::Module::parse_bitcode_from_buffer(&memory_buffer, ctx) + .map_err(|e| format!("Failed to parse bitcode: {e}")) +} + +/// Core QIR to QIS translation logic. +/// +/// # Arguments +/// - `bc_bytes` - The QIR bytes to translate. +/// - `opt_level` - The optimization level to use (0-3). Platform defaults are +/// exposed via [`DEFAULT_OPT_LEVEL`]. +/// - `target` - Target architecture ("aarch64", "x86-64", "native"). Platform +/// defaults are exposed via [`DEFAULT_TARGET`]. +/// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. +/// +/// # Errors +/// Returns an error string if the translation fails. +pub fn qir_to_qis( + bc_bytes: &[u8], + opt_level: u32, + target: &str, + _wasm_bytes: Option<&[u8]>, +) -> Result, String> { + use crate::{ + aux::{get_capability_flags, process_entry_function}, + convert::{ + add_qmain_wrapper, create_qubit_array, find_entry_function, free_all_qubits, + get_string_attrs, process_ir_defined_q_fns, prune_unused_ir_qis_helpers, + }, + decompose::add_decompositions, + opt::optimize, + utils::add_generator_metadata, + }; + use inkwell::{attributes::AttributeLoc, context::Context}; + use std::{collections::BTreeMap, env}; + + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; + crate::llvm_verify::verify_module(&module, "LLVM module verification failed after parse")?; + + add_decompositions(&ctx, &module) + .map_err(|e| format!("Failed to add QIR decompositions: {e}"))?; + let entry_fn = find_entry_function(&module) + .map_err(|e| format!("Failed to find entry function in QIR module: {e}"))?; + + let entry_fn_name = entry_fn + .get_name() + .to_str() + .map_err(|e| format!("Invalid UTF-8 in entry function name: {e}"))?; + let capability_flags = get_capability_flags(&module); + + log::trace!("Entry function: {entry_fn_name}"); + let new_name = format!("___user_qir_{entry_fn_name}"); + entry_fn.as_global_value().set_name(&new_name); + log::debug!("Renamed entry function to: {new_name}"); + let qubit_array = if capability_flags.dynamic_qubit_management { + None + } else { + Some(create_qubit_array(&ctx, &module, entry_fn)?) + }; + + let wasm_fns: BTreeMap = BTreeMap::new(); + process_entry_function( + &ctx, + &module, + entry_fn, + &wasm_fns, + qubit_array, + capability_flags, + )?; + + // Handle IR defined functions that take qubits + process_ir_defined_q_fns( + &ctx, + &module, + entry_fn, + capability_flags.dynamic_qubit_management, + )?; + + if let Some(qubit_array) = qubit_array { + free_all_qubits(&ctx, &module, entry_fn, qubit_array)?; + } + + // Add qmain wrapper that calls setup, entry function, and teardown + let _ = add_qmain_wrapper(&ctx, &module, entry_fn); + + crate::llvm_verify::verify_module(&module, "LLVM module verification failed")?; + + // Clean up the translated module + for attr in get_string_attrs(entry_fn) { + let kind = decode_string_attribute_kind(attr)?; + entry_fn.remove_string_attribute(AttributeLoc::Function, &kind); + } + + // TODO: remove global module metadata + // seems inkwell doesn't support this yet // Add metadata to the module let md_string = ctx.metadata_string("mainlib"); @@ -1573,602 +3328,1209 @@ pub fn qir_to_qis( add_generator_metadata(&ctx, &module, "gen_name", env!("CARGO_PKG_NAME"))?; add_generator_metadata(&ctx, &module, "gen_version", env!("CARGO_PKG_VERSION"))?; - optimize(&module, opt_level, target)?; - prune_unused_ir_qis_helpers(&module); + optimize(&module, opt_level, target)?; + prune_unused_ir_qis_helpers(&module); + + Ok(memory_buffer_to_owned_bytes( + &module.write_bitcode_to_memory(), + )) +} + +/// Extract WASM function mapping from the given WASM bytes. +/// +/// # Errors +/// Returns an error string if parsing fails. +#[cfg(feature = "wasm")] +pub fn get_wasm_functions( + wasm_bytes: Option<&[u8]>, +) -> Result, String> { + use crate::utils::parse_wasm_functions; + use std::collections::BTreeMap; + + let mut wasm_fns: BTreeMap = BTreeMap::new(); + if let Some(bytes) = wasm_bytes { + wasm_fns = parse_wasm_functions(bytes)?; + log::debug!("WASM function map: {wasm_fns:?}"); + } + Ok(wasm_fns) +} + +#[cfg(not(feature = "wasm"))] +fn get_wasm_functions( + _wasm_bytes: Option<&[u8]>, +) -> Result, String> { + Ok(std::collections::BTreeMap::new()) +} + +/// Validate the given QIR bitcode. +/// +/// # Arguments +/// - `bc_bytes` - The QIR bytes to validate. +/// - `wasm_bytes` - Optional WASM bytes to validate against. +/// +/// # Errors +/// Returns an error string if validation fails. +pub fn validate_qir(bc_bytes: &[u8], wasm_bytes: Option<&[u8]>) -> Result<(), String> { + use crate::{ + aux::{ + get_capability_flags, validate_capability_usage, + validate_dynamic_array_allocation_backing, + validate_dynamic_result_allocation_placement, validate_functions, + validate_module_flags, validate_module_layout_and_triple, validate_result_slot_usage, + }, + convert::{ENTRY_ATTRIBUTE_KEYS, find_entry_function}, + }; + use inkwell::{attributes::AttributeLoc, context::Context}; + + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; + let mut errors = Vec::new(); + + let capability_flags = get_capability_flags(&module); + validate_module_layout_and_triple(&module); + let entry_fn = if let Ok(entry_fn) = find_entry_function(&module) { + if entry_fn.get_basic_blocks().is_empty() { + errors.push("Entry function has no basic blocks".to_string()); + } + + // Enforce required attributes + for attr in ENTRY_ATTRIBUTE_KEYS.iter().copied().filter(|attr| { + *attr != "entry_point" + && !(*attr == "required_num_qubits" && capability_flags.dynamic_qubit_management) + && !(*attr == "required_num_results" && capability_flags.dynamic_result_management) + }) { + let val = entry_fn.get_string_attribute(AttributeLoc::Function, attr); + if val.is_none() { + errors.push(format!("Missing required attribute: `{attr}`")); + } + } + + // `required_num_qubits` must stay positive. `required_num_results` + // may be zero for programs that only use classical-returning operations + // such as `mz_leaked`. + for (attr, type_) in [("required_num_qubits", "qubit")] { + if capability_flags.dynamic_qubit_management { + continue; + } + if entry_fn + .get_string_attribute(AttributeLoc::Function, attr) + .and_then(|a| { + decode_llvm_c_string(a.get_string_value())? + .parse::() + .ok() + }) + == Some(0) + { + errors.push(format!("Entry function must have at least one {type_}")); + } + } + entry_fn + } else { + errors.push("No entry function found in QIR module".to_string()); + return Err(errors.join("; ")); + }; + + let wasm_fns = get_wasm_functions(wasm_bytes)?; + + validate_functions(&module, entry_fn, &wasm_fns, &mut errors); + validate_result_slot_usage(&module, entry_fn, &mut errors); + validate_dynamic_result_allocation_placement(&module, entry_fn, &mut errors); + validate_dynamic_array_allocation_backing(&module, &mut errors); + + validate_module_flags(&module, &mut errors); + validate_capability_usage(&module, capability_flags, &mut errors); + + if !errors.is_empty() { + return Err(errors.join("; ")); + } + log::info!("QIR validation passed"); + Ok(()) +} + +/// Convert QIR LLVM IR text to QIR bitcode bytes. +/// +/// # Errors +/// Returns an error string if the LLVM IR is invalid. +pub fn qir_ll_to_bc(ll_text: &str) -> Result, String> { + use inkwell::context::Context; + + let ctx = Context::create(); + let module = create_module_from_ir_text(&ctx, ll_text, "qir")?; + + Ok(memory_buffer_to_owned_bytes( + &module.write_bitcode_to_memory(), + )) +} + +fn decode_string_attribute_kind(attr: inkwell::attributes::Attribute) -> Result { + use llvm_sys::core::LLVMGetStringAttributeKind; + use std::slice; + + let mut kind_len = 0_u32; + let kind_ptr = unsafe { LLVMGetStringAttributeKind(attr.as_mut_ptr(), &raw mut kind_len) }; + if kind_ptr.is_null() { + return Err("LLVM returned a null attribute kind pointer".to_string()); + } + let kind_len = usize::try_from(kind_len) + .map_err(|_| "Attribute kind length does not fit into usize".to_string())?; + let kind_bytes = unsafe { slice::from_raw_parts(kind_ptr.cast::(), kind_len) }; + std::str::from_utf8(kind_bytes) + .map_err(|e| format!("Invalid UTF-8 in attribute kind: {e}")) + .map(str::to_owned) +} + +fn decode_string_attribute_value( + attr: inkwell::attributes::Attribute, + kind: &str, +) -> Result, String> { + use llvm_sys::core::LLVMGetStringAttributeValue; + use std::slice; + + let mut value_len = 0_u32; + let value_ptr = unsafe { LLVMGetStringAttributeValue(attr.as_mut_ptr(), &raw mut value_len) }; + if value_len == 0 { + return Ok(None); + } + if value_ptr.is_null() { + return Err(format!( + "LLVM returned a null attribute value pointer for `{kind}`" + )); + } + let value_len = usize::try_from(value_len) + .map_err(|_| format!("Attribute `{kind}` value length does not fit into usize"))?; + let value_bytes = unsafe { slice::from_raw_parts(value_ptr.cast::(), value_len) }; + let value = std::str::from_utf8(value_bytes) + .map_err(|e| format!("Invalid UTF-8 in attribute `{kind}` value: {e}"))? + .to_owned(); + Ok(Some(value)) +} + +/// Get QIR entry point function attributes. +/// +/// These attributes are used to generate METADATA records in QIR output schemas. +/// This function assumes that QIR has been validated using `validate_qir`. +/// +/// # Errors +/// Returns an error string if the input bitcode is invalid. +pub fn get_entry_attributes( + bc_bytes: &[u8], +) -> Result>, String> { + use crate::convert::{find_entry_function, get_string_attrs}; + use inkwell::context::Context; + use std::collections::BTreeMap; + + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; + + let mut metadata = BTreeMap::new(); + if let Ok(entry_fn) = find_entry_function(&module) { + for attr in get_string_attrs(entry_fn) { + let kind_id = match decode_string_attribute_kind(attr) { + Ok(kind_id) => kind_id, + Err(err) => { + log::warn!("Skipping attribute with invalid kind: {err}"); + continue; + } + }; + match decode_string_attribute_value(attr, &kind_id) { + Ok(value) => { + metadata.insert(kind_id, value); + } + Err(err) => { + log::warn!("{err}"); + metadata.insert(kind_id, None); + } + } + } + } + Ok(metadata) +} + +#[cfg(feature = "python")] +mod exceptions { + use pyo3::exceptions::PyException; + use pyo3_stub_gen::create_exception; + + create_exception!( + qir_qis, + ValidationError, + PyException, + "QIR ValidationError.\n\nRaised when the QIR is invalid." + ); + create_exception!( + qir_qis, + CompilerError, + PyException, + "QIR CompilerError.\n\nRaised when QIR to QIS compilation fails." + ); +} + +#[cfg(feature = "python")] +#[pymodule] +mod qir_qis { + use std::borrow::Cow; + use std::collections::BTreeMap; + + use super::{PyErr, PyResult, pyfunction}; + + use pyo3_stub_gen::derive::gen_stub_pyfunction; + + #[pymodule_export] + use super::exceptions::CompilerError; + #[pymodule_export] + use super::exceptions::ValidationError; + + /// Validate the given QIR. + /// + /// # Arguments + /// - `bc_bytes` - The QIR bytes to validate. + /// - `wasm_bytes` - Optional WASM bytes to validate against. + /// + /// # Errors + /// Returns a `ValidationError`: + /// - If the QIR is invalid. + /// - If the WASM module is invalid. + /// - If a QIR-referenced WASM function is missing from the WASM module. + #[gen_stub_pyfunction] + #[pyfunction] + #[allow(clippy::needless_pass_by_value)] + #[pyo3(signature = (bc_bytes, *, wasm_bytes = None))] + pub fn validate_qir(bc_bytes: Cow<[u8]>, wasm_bytes: Option>) -> PyResult<()> { + crate::validate_qir(&bc_bytes, wasm_bytes.as_deref()) + .map_err(PyErr::new::) + } + + /// Translate QIR bitcode to Quantinuum QIS. + /// + /// # Arguments + /// - `bc_bytes` - The QIR bytes to translate. + /// - `opt_level` - The optimization level to use (0-3). Default is 2 on + /// Linux/macOS and 0 on Windows. + /// - `target` - Target architecture (default: "aarch64" on Linux/macOS and + /// "native" on Windows; options: "x86-64", "native"). + /// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. + /// + /// # Errors + /// Returns a `CompilerError` if the translation fails. + #[gen_stub_pyfunction] + #[pyfunction] + #[allow(clippy::needless_pass_by_value)] + #[allow(clippy::missing_errors_doc)] + #[cfg_attr( + windows, + pyo3(signature = (bc_bytes, *, opt_level = 0, target = "native", wasm_bytes = None)) + )] + #[cfg_attr( + not(windows), + pyo3(signature = (bc_bytes, *, opt_level = 2, target = "aarch64", wasm_bytes = None)) + )] + pub fn qir_to_qis<'a>( + bc_bytes: Cow<[u8]>, + opt_level: u32, + target: &'a str, + wasm_bytes: Option>, + ) -> PyResult> { + let result = crate::qir_to_qis(&bc_bytes, opt_level, target, wasm_bytes.as_deref()) + .map_err(PyErr::new::)?; + + Ok(result.into()) + } + + /// Convert QIR LLVM IR to QIR bitcode. + /// + /// # Errors + /// Returns a `ValidationError` if the LLVM IR is invalid. + #[gen_stub_pyfunction] + #[pyfunction] + fn qir_ll_to_bc(ll_text: &str) -> PyResult> { + let result = crate::qir_ll_to_bc(ll_text).map_err(PyErr::new::)?; + Ok(result.into()) + } + + /// Get QIR entry point function attributes. + /// + /// These attributes are used to generate METADATA records in QIR output schemas. + /// This function assumes that QIR has been validated using `validate_qir`. + /// + /// # Errors + /// Returns a `ValidationError` if the input bitcode is invalid. + #[gen_stub_pyfunction] + #[pyfunction] + #[allow(clippy::needless_pass_by_value)] + fn get_entry_attributes(bc_bytes: Cow<[u8]>) -> PyResult>> { + crate::get_entry_attributes(&bc_bytes).map_err(PyErr::new::) + } +} + +#[cfg(feature = "python")] +define_stub_info_gatherer!(stub_info); + +#[cfg(test)] +mod test { + #![allow(clippy::expect_used)] + #![allow(clippy::unwrap_used)] + use crate::{ + convert::get_string_label, create_module_from_ir_text, get_entry_attributes, + parse_bitcode_module, qir_ll_to_bc, qir_to_qis, validate_qir, + }; + use inkwell::{ + context::Context, + memory_buffer::MemoryBuffer, + module::Module, + values::{CallSiteValue, FunctionValue}, + }; + use proptest::prelude::*; + use std::{collections::BTreeMap, sync::LazyLock}; + #[cfg(feature = "wasm")] + use wasm_encoder::{ExportKind, ExportSection, Module as WasmModule}; + + const PROPERTY_FIXTURES: &[&str] = &[ + "tests/data/base.ll", + "tests/data/base_array.ll", + "tests/data/adaptive.ll", + "tests/data/qir2_base.ll", + "tests/data/qir2_adaptive.ll", + "tests/data/mz_leaked.ll", + ]; + const DYNAMIC_FEATURE_FIXTURES: &[&str] = &[ + "tests/data/dynamic_qubit_alloc.ll", + "tests/data/dynamic_qubit_alloc_checked.ll", + "tests/data/dynamic_qubit_array_checked.ll", + "tests/data/dynamic_qubit_array_ssa.ll", + "tests/data/dynamic_result_alloc.ll", + "tests/data/dynamic_result_mixed_array_output.ll", + ]; + static PROPERTY_FIXTURE_BITCODE: LazyLock>> = + LazyLock::new(|| { + PROPERTY_FIXTURES + .iter() + .map(|path| { + let ll_text = + std::fs::read_to_string(path).expect("Failed to read LLVM IR fixture"); + let bitcode = qir_ll_to_bc(&ll_text) + .expect("Failed to convert LLVM IR fixture to bitcode"); + (*path, bitcode) + }) + .collect() + }); + + fn conservative_translation_settings() -> (u32, &'static str) { + (0, "native") + } + + fn load_fixture_bitcode(path: &str) -> Vec { + PROPERTY_FIXTURE_BITCODE + .get(path) + .cloned() + .expect("Fixture bitcode should be precompiled") + } + + fn verify_bitcode_module(bitcode: &[u8], name: &str) -> Result<(), String> { + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, bitcode, name)?; + crate::llvm_verify::verify_module(&module, "LLVM verifier rejected translated module") + } + + fn parse_bitcode_as_file(bitcode: &[u8], name: &str) -> Result<(), String> { + let mut temp_file = tempfile::Builder::new() + .prefix(name) + .suffix(".bc") + .tempfile() + .map_err(|e| format!("Failed to create temp bitcode file: {e}"))?; + std::io::Write::write_all(&mut temp_file, bitcode) + .map_err(|e| format!("Failed to write temp bitcode: {e}"))?; + + let ctx = Context::create(); + let memory_buffer = MemoryBuffer::create_from_file(temp_file.path()) + .map_err(|e| format!("Failed to read temp bitcode: {e}"))?; + Module::parse_bitcode_from_buffer(&memory_buffer, &ctx) + .map(|_| ()) + .map_err(|e| format!("Failed to parse bitcode: {e}")) + } + + fn assert_public_bitcode_round_trips_from_file(bitcode: &[u8], name: &str) { + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, bitcode, name) + .expect("Bitcode should reparse through qir-qis helpers"); + let raw_buffer = module.write_bitcode_to_memory(); + let expected_len = bitcode + .len() + .checked_add(1) + .expect("bitcode length should not overflow"); + assert_eq!( + raw_buffer.as_slice().len(), + expected_len, + "Public bitcode bytes should exclude LLVM's implicit trailing NUL" + ); + assert_eq!(raw_buffer.as_slice().last(), Some(&0)); + parse_bitcode_as_file(bitcode, name) + .expect("Public bitcode should parse when consumed from a file"); + } + + #[cfg(feature = "wasm")] + fn build_wasm_exports(exports: &[(String, u32)]) -> Vec { + let mut module = WasmModule::new(); + let mut export_section = ExportSection::new(); + for (name, index) in exports { + export_section.export(name, ExportKind::Func, *index); + } + module.section(&export_section); + module.finish() + } + + fn minimal_qir_with_body( + required_num_qubits: &str, + required_num_results: &str, + qir_major_flag: &str, + extra_decl: &str, + body: &str, + ) -> String { + format!( + r#"%Qubit = type opaque +%Result = type opaque + +{extra_decl} + +define i64 @Entry_Point_Name() #0 {{ +entry: +{body} + ret i64 0 +}} + +attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="{required_num_qubits}" "required_num_results"="{required_num_results}" }} + +!llvm.module.flags = !{{!0, !1, !2, !3}} +!0 = !{{i32 1, !"qir_major_version", i32 {qir_major_flag}}} +!1 = !{{i32 7, !"qir_minor_version", i32 0}} +!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}} +!3 = !{{i32 1, !"dynamic_result_management", i1 false}} +"# + ) + } + + fn minimal_qir_missing_attr(missing_attr: &str) -> String { + let attrs = [ + ("entry_point", None), + ("qir_profiles", Some("base_profile")), + ("output_labeling_schema", Some("schema_id")), + ("required_num_qubits", Some("1")), + ("required_num_results", Some("1")), + ]; + let rendered_attrs = attrs + .into_iter() + .filter(|(name, _)| *name != missing_attr) + .map(|(name, value)| { + value.map_or_else( + || format!(r#""{name}""#), + |value| format!(r#""{name}"="{value}""#), + ) + }) + .collect::>() + .join(" "); + + format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +attributes #0 = {{ {rendered_attrs} }} + +!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}} +"# + ) + } + + fn minimal_qir_with_duplicate_major_flags(first_major: &str, second_major: &str) -> String { + format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }} + +!llvm.module.flags = !{{!0, !1, !2, !3, !4}} +!0 = !{{i32 1, !"qir_major_version", i32 {first_major}}} +!1 = !{{i32 1, !"qir_major_version", i32 {second_major}}} +!2 = !{{i32 7, !"qir_minor_version", i32 0}} +!3 = !{{i32 1, !"dynamic_qubit_management", i1 false}} +!4 = !{{i32 1, !"dynamic_result_management", i1 false}} +"# + ) + } + + fn minimal_qir_with_duplicate_dynamic_flags(first_flag: &str, second_flag: &str) -> String { + format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }} + +!llvm.module.flags = !{{!0, !1, !2, !3, !4}} +!0 = !{{i32 1, !"qir_major_version", i32 1}} +!1 = !{{i32 7, !"qir_minor_version", i32 0}} +!2 = !{{i32 1, !"dynamic_qubit_management", i1 {first_flag}}} +!3 = !{{i32 1, !"dynamic_qubit_management", i1 {second_flag}}} +!4 = !{{i32 1, !"dynamic_result_management", i1 false}} +"# + ) + } + + fn collect_called_function_names(helper: FunctionValue<'_>) -> Vec { + let mut calls = Vec::new(); + for bb in helper.get_basic_blocks() { + for instr in bb.get_instructions() { + let Ok(call) = CallSiteValue::try_from(instr) else { + continue; + }; + let Some(callee) = call.get_called_fn_value() else { + continue; + }; + let Ok(name) = callee.get_name().to_str() else { + continue; + }; + calls.push(name.to_string()); + } + } + calls + } + + #[test] + fn test_get_entry_attributes() { + let ll_text = std::fs::read_to_string("tests/data/base-attrs.ll") + .expect("Failed to read base-attrs.ll"); + let bc_bytes = qir_ll_to_bc(&ll_text).unwrap(); + let attrs = get_entry_attributes(&bc_bytes).unwrap(); + assert!(matches!(attrs.get("entry_point"), Some(None))); + assert_eq!( + attrs.get("qir_profiles"), + Some(&Some("base_profile".to_string())) + ); + assert_eq!( + attrs.get("output_labeling_schema"), + Some(&Some("labeled".to_string())) + ); + assert_eq!( + attrs.get("required_num_qubits"), + Some(&Some("2".to_string())) + ); + assert_eq!( + attrs.get("required_num_results"), + Some(&Some("2".to_string())) + ); + } + + #[test] + fn test_entry_attributes_includes_optional_custom_attr() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} + +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" } + +!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} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let attrs = get_entry_attributes(&bc_bytes).unwrap(); + assert_eq!( + attrs.get("custom_attr"), + Some(&Some("custom_value".to_string())) + ); + } + + #[test] + fn test_get_entry_attributes_is_order_independent() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} + +attributes #0 = { "custom_attr"="custom_value" "required_num_results"="2" "output_labeling_schema"="labeled" "entry_point" "required_num_qubits"="2" "qir_profiles"="base_profile" } + +!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} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let attrs = get_entry_attributes(&bc_bytes).expect("entry attributes should parse"); + assert!(matches!(attrs.get("entry_point"), Some(None))); + assert_eq!( + attrs.get("qir_profiles"), + Some(&Some("base_profile".to_string())) + ); + assert_eq!( + attrs.get("custom_attr"), + Some(&Some("custom_value".to_string())) + ); + } + + #[test] + fn test_qir_to_qis_strips_custom_entry_attrs() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} + +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" } + +!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} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let (opt_level, target) = if cfg!(windows) { + (0, "native") + } else { + (2, "aarch64") + }; + let qis_bytes = qir_to_qis(&bc_bytes, opt_level, target, None).unwrap(); + + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap(); + let entry_fn = module.get_function("___user_qir_Entry_Point_Name").unwrap(); - Ok(memory_buffer_to_owned_bytes( - &module.write_bitcode_to_memory(), - )) + assert!( + entry_fn + .get_string_attribute(inkwell::attributes::AttributeLoc::Function, "custom_attr") + .is_none() + ); + } + + #[test] + fn test_platform_default_conversion_settings_match_expectations() { + if cfg!(windows) { + assert_eq!(crate::DEFAULT_OPT_LEVEL, 0); + assert_eq!(crate::DEFAULT_TARGET, "native"); + } else { + assert_eq!(crate::DEFAULT_OPT_LEVEL, 2); + assert_eq!(crate::DEFAULT_TARGET, "aarch64"); + } + } + + #[cfg(windows)] + #[test] + fn test_windows_optimized_conversion_returns_actionable_error() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -/// Extract WASM function mapping from the given WASM bytes. -/// -/// # Errors -/// Returns an error string if parsing fails. -#[cfg(feature = "wasm")] -pub fn get_wasm_functions( - wasm_bytes: Option<&[u8]>, -) -> Result, String> { - use crate::utils::parse_wasm_functions; - use std::collections::BTreeMap; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="1" "required_num_results"="1" } - let mut wasm_fns: BTreeMap = BTreeMap::new(); - if let Some(bytes) = wasm_bytes { - wasm_fns = parse_wasm_functions(bytes)?; - log::debug!("WASM function map: {wasm_fns:?}"); +!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} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = qir_to_qis(&bc_bytes, 1, "native", None) + .expect_err("optimized conversion should fail fast on Windows"); + assert!(err.contains("currently unavailable on Windows")); + assert!(err.contains("opt_level=0")); } - Ok(wasm_fns) -} -#[cfg(not(feature = "wasm"))] -fn get_wasm_functions( - _wasm_bytes: Option<&[u8]>, -) -> Result, String> { - Ok(std::collections::BTreeMap::new()) -} + #[test] + fn test_qir_ll_to_bc_accepts_legacy_typed_pointers() { + let ll_text = + std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); + let bc_bytes = qir_ll_to_bc(&ll_text).unwrap(); + assert!(!bc_bytes.is_empty()); + } -/// Validate the given QIR bitcode. -/// -/// # Arguments -/// - `bc_bytes` - The QIR bytes to validate. -/// - `wasm_bytes` - Optional WASM bytes to validate against. -/// -/// # Errors -/// Returns an error string if validation fails. -pub fn validate_qir(bc_bytes: &[u8], wasm_bytes: Option<&[u8]>) -> Result<(), String> { - use crate::{ - aux::{ - validate_functions, validate_module_flags, validate_module_layout_and_triple, - validate_result_slot_usage, - }, - convert::{ENTRY_ATTRIBUTE_KEYS, find_entry_function}, - }; - use inkwell::{attributes::AttributeLoc, context::Context}; + #[test] + fn test_qir_ll_to_bc_output_parses_when_read_from_file() { + let ll_text = + std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode"); - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; - let mut errors = Vec::new(); + assert_public_bitcode_round_trips_from_file(&bc_bytes, "public_qir_output"); + } - validate_module_layout_and_triple(&module); + #[test] + fn test_qir2_base_fixture_validate_and_compile() { + let ll_text = std::fs::read_to_string("tests/data/qir2_base.ll") + .expect("Failed to read qir2_base.ll"); + let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_base.ll to bitcode"); - let entry_fn = if let Ok(entry_fn) = find_entry_function(&module) { - if entry_fn.get_basic_blocks().is_empty() { - errors.push("Entry function has no basic blocks".to_string()); - } + validate_qir(&input_bc, None).expect("QIR 2.0 base fixture should validate"); + let output_bc = + qir_to_qis(&input_bc, 0, "native", None).expect("QIR 2.0 base fixture should compile"); - // Enforce required attributes - for attr in ENTRY_ATTRIBUTE_KEYS - .iter() - .copied() - .filter(|attr| *attr != "entry_point") - { - let val = entry_fn.get_string_attribute(AttributeLoc::Function, attr); - if val.is_none() { - errors.push(format!("Missing required attribute: `{attr}`")); - } - } + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") + .expect("Compiled QIS bitcode should parse"); + assert!(module.get_function("qmain").is_some()); + assert!(module.get_function("qir_qis.load_qubit").is_some()); + } - // `required_num_qubits` must stay positive. `required_num_results` - // may be zero for programs that only use classical-returning operations - // such as `mz_leaked`. - for (attr, type_) in [("required_num_qubits", "qubit")] { - if entry_fn - .get_string_attribute(AttributeLoc::Function, attr) - .and_then(|a| { - decode_llvm_c_string(a.get_string_value())? - .parse::() - .ok() - }) - == Some(0) - { - errors.push(format!("Entry function must have at least one {type_}")); - } - } - entry_fn - } else { - errors.push("No entry function found in QIR module".to_string()); - return Err(errors.join("; ")); - }; + #[test] + fn test_qir2_adaptive_fixture_validate_and_compile() { + let ll_text = std::fs::read_to_string("tests/data/qir2_adaptive.ll") + .expect("Failed to read qir2_adaptive.ll"); + let input_bc = + qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_adaptive.ll to bitcode"); - let wasm_fns = get_wasm_functions(wasm_bytes)?; + validate_qir(&input_bc, None).expect("QIR 2.0 adaptive fixture should validate"); + let output_bc = qir_to_qis(&input_bc, 0, "native", None) + .expect("QIR 2.0 adaptive fixture should compile"); - validate_functions(&module, entry_fn, &wasm_fns, &mut errors); - validate_result_slot_usage(&module, entry_fn, &mut errors); + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") + .expect("Compiled QIS bitcode should parse"); + assert!(module.get_function("qmain").is_some()); + assert!(module.get_function("___lazy_measure").is_some()); + } - validate_module_flags(&module, &mut errors); + #[test] + fn test_qir_to_qis_output_parses_with_raw_llvm_buffer() { + let ll_text = + std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); + let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode"); + let output_bc = + qir_to_qis(&input_bc, 0, "native", None).expect("base fixture should compile"); - if !errors.is_empty() { - return Err(errors.join("; ")); + assert_public_bitcode_round_trips_from_file(&output_bc, "selene_qis_output"); } - log::info!("QIR validation passed"); - Ok(()) + + #[test] + fn test_validate_module_flags_are_checked_cross_platform() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -/// Convert QIR LLVM IR text to QIR bitcode bytes. -/// -/// # Errors -/// Returns an error string if the LLVM IR is invalid. -pub fn qir_ll_to_bc(ll_text: &str) -> Result, String> { - use inkwell::context::Context; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - let ctx = Context::create(); - let module = create_module_from_ir_text(&ctx, ll_text, "qir")?; +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + validate_qir(&bc_bytes, None).expect("Module flags should validate on every platform"); + } - Ok(memory_buffer_to_owned_bytes( - &module.write_bitcode_to_memory(), - )) + #[test] + fn test_module_flag_parser_reads_existing_flags() { + use crate::aux::collect_module_flags; + use inkwell::context::Context; + + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -fn decode_string_attribute_kind(attr: inkwell::attributes::Attribute) -> Result { - use llvm_sys::core::LLVMGetStringAttributeKind; - use std::slice; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - let mut kind_len = 0_u32; - let kind_ptr = unsafe { LLVMGetStringAttributeKind(attr.as_mut_ptr(), &raw mut kind_len) }; - if kind_ptr.is_null() { - return Err("LLVM returned a null attribute kind pointer".to_string()); +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +"#; + + let ctx = Context::create(); + let module = create_module_from_ir_text(&ctx, ll_text, "qir") + .expect("Failed to create module from inline IR"); + + let flags = collect_module_flags(&module); + assert_eq!( + flags.get("qir_major_version").map(<[String]>::to_vec), + Some(vec!["i32 2".to_string()]) + ); + assert_eq!( + flags.get("qir_minor_version").map(<[String]>::to_vec), + Some(vec!["i32 0".to_string()]) + ); + assert_eq!( + flags + .get("dynamic_qubit_management") + .map(<[String]>::to_vec), + Some(vec!["i1 false".to_string()]) + ); + assert_eq!( + flags + .get("dynamic_result_management") + .map(<[String]>::to_vec), + Some(vec!["i1 false".to_string()]) + ); } - let kind_len = usize::try_from(kind_len) - .map_err(|_| "Attribute kind length does not fit into usize".to_string())?; - let kind_bytes = unsafe { slice::from_raw_parts(kind_ptr.cast::(), kind_len) }; - std::str::from_utf8(kind_bytes) - .map_err(|e| format!("Invalid UTF-8 in attribute kind: {e}")) - .map(str::to_owned) + + #[test] + fn test_validate_module_flags_accept_duplicate_entries_if_one_matches() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -fn decode_string_attribute_value( - attr: inkwell::attributes::Attribute, - kind: &str, -) -> Result, String> { - use llvm_sys::core::LLVMGetStringAttributeValue; - use std::slice; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - let mut value_len = 0_u32; - let value_ptr = unsafe { LLVMGetStringAttributeValue(attr.as_mut_ptr(), &raw mut value_len) }; - if value_len == 0 { - return Ok(None); - } - if value_ptr.is_null() { - return Err(format!( - "LLVM returned a null attribute value pointer for `{kind}`" - )); +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 99} +!1 = !{i32 1, !"qir_major_version", i32 2} +!2 = !{i32 7, !"qir_minor_version", i32 0} +!3 = !{i32 1, !"dynamic_qubit_management", i1 false} +!4 = !{i32 1, !"dynamic_result_management", i1 false} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + validate_qir(&bc_bytes, None) + .expect("Module flags should validate when any duplicate entry matches"); } - let value_len = usize::try_from(value_len) - .map_err(|_| format!("Attribute `{kind}` value length does not fit into usize"))?; - let value_bytes = unsafe { slice::from_raw_parts(value_ptr.cast::(), value_len) }; - let value = std::str::from_utf8(value_bytes) - .map_err(|e| format!("Invalid UTF-8 in attribute `{kind}` value: {e}"))? - .to_owned(); - Ok(Some(value)) + + #[test] + fn test_validate_module_flags_reports_malformed_required_flag() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -/// Get QIR entry point function attributes. -/// -/// These attributes are used to generate METADATA records in QIR output schemas. -/// This function assumes that QIR has been validated using `validate_qir`. -/// -/// # Errors -/// Returns an error string if the input bitcode is invalid. -pub fn get_entry_attributes( - bc_bytes: &[u8], -) -> Result>, String> { - use crate::convert::{find_entry_function, get_string_attrs}; - use inkwell::context::Context; - use std::collections::BTreeMap; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?; +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", !4} +!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 99} +"#; - let mut metadata = BTreeMap::new(); - if let Ok(entry_fn) = find_entry_function(&module) { - for attr in get_string_attrs(entry_fn) { - let kind_id = match decode_string_attribute_kind(attr) { - Ok(kind_id) => kind_id, - Err(err) => { - log::warn!("Skipping attribute with invalid kind: {err}"); - continue; - } - }; - match decode_string_attribute_value(attr, &kind_id) { - Ok(value) => { - metadata.insert(kind_id, value); - } - Err(err) => { - log::warn!("{err}"); - metadata.insert(kind_id, None); - } - } - } + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None).expect_err("Malformed module flag should fail"); + assert!(err.contains("Missing or unsupported module flag: qir_major_version")); } - Ok(metadata) -} - -#[cfg(feature = "python")] -mod exceptions { - use pyo3::exceptions::PyException; - use pyo3_stub_gen::create_exception; - create_exception!( - qir_qis, - ValidationError, - PyException, - "QIR ValidationError.\n\nRaised when the QIR is invalid." - ); - create_exception!( - qir_qis, - CompilerError, - PyException, - "QIR CompilerError.\n\nRaised when QIR to QIS compilation fails." - ); + #[test] + fn test_validate_qir_reports_exact_single_expected_module_flag_value() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 } -#[cfg(feature = "python")] -#[pymodule] -mod qir_qis { - use std::borrow::Cow; - use std::collections::BTreeMap; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - use super::{PyErr, PyResult, pyfunction}; +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 99} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +"#; - use pyo3_stub_gen::derive::gen_stub_pyfunction; + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("unsupported single-valued module flag should fail"); + assert!(err.contains("Unsupported qir_minor_version: expected i32 0")); + } - #[pymodule_export] - use super::exceptions::CompilerError; - #[pymodule_export] - use super::exceptions::ValidationError; + #[test] + fn test_validate_qir_reports_unsupported_optional_arrays_module_flag_value() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} - /// Validate the given QIR. - /// - /// # Arguments - /// - `bc_bytes` - The QIR bytes to validate. - /// - `wasm_bytes` - Optional WASM bytes to validate against. - /// - /// # Errors - /// Returns a `ValidationError`: - /// - If the QIR is invalid. - /// - If the WASM module is invalid. - /// - If a QIR-referenced WASM function is missing from the WASM module. - #[gen_stub_pyfunction] - #[pyfunction] - #[allow(clippy::needless_pass_by_value)] - #[pyo3(signature = (bc_bytes, *, wasm_bytes = None))] - pub fn validate_qir(bc_bytes: Cow<[u8]>, wasm_bytes: Option>) -> PyResult<()> { - crate::validate_qir(&bc_bytes, wasm_bytes.as_deref()) - .map_err(PyErr::new::) - } +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - /// Translate QIR bitcode to Quantinuum QIS. - /// - /// # Arguments - /// - `bc_bytes` - The QIR bytes to translate. - /// - `opt_level` - The optimization level to use (0-3). Default is 2 on - /// Linux/macOS and 0 on Windows. - /// - `target` - Target architecture (default: "aarch64" on Linux/macOS and - /// "native" on Windows; options: "x86-64", "native"). - /// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. - /// - /// # Errors - /// Returns a `CompilerError` if the translation fails. - #[gen_stub_pyfunction] - #[pyfunction] - #[allow(clippy::needless_pass_by_value)] - #[allow(clippy::missing_errors_doc)] - #[cfg_attr( - windows, - pyo3(signature = (bc_bytes, *, opt_level = 0, target = "native", wasm_bytes = None)) - )] - #[cfg_attr( - not(windows), - pyo3(signature = (bc_bytes, *, opt_level = 2, target = "aarch64", wasm_bytes = None)) - )] - pub fn qir_to_qis<'a>( - bc_bytes: Cow<[u8]>, - opt_level: u32, - target: &'a str, - wasm_bytes: Option>, - ) -> PyResult> { - let result = crate::qir_to_qis(&bc_bytes, opt_level, target, wasm_bytes.as_deref()) - .map_err(PyErr::new::)?; +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!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, !"arrays", i32 7} +"#; - Ok(result.into()) + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("unsupported optional arrays flag should fail validation"); + assert!(err.contains("Unsupported arrays: expected one of i1 false, i1 true")); } - /// Convert QIR LLVM IR to QIR bitcode. - /// - /// # Errors - /// Returns a `ValidationError` if the LLVM IR is invalid. - #[gen_stub_pyfunction] - #[pyfunction] - fn qir_ll_to_bc(ll_text: &str) -> PyResult> { - let result = crate::qir_ll_to_bc(ll_text).map_err(PyErr::new::)?; - Ok(result.into()) - } + #[test] + fn test_validate_qir_reports_malformed_optional_arrays_module_flag() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} - /// Get QIR entry point function attributes. - /// - /// These attributes are used to generate METADATA records in QIR output schemas. - /// This function assumes that QIR has been validated using `validate_qir`. - /// - /// # Errors - /// Returns a `ValidationError` if the input bitcode is invalid. - #[gen_stub_pyfunction] - #[pyfunction] - #[allow(clippy::needless_pass_by_value)] - fn get_entry_attributes(bc_bytes: Cow<[u8]>) -> PyResult>> { - crate::get_entry_attributes(&bc_bytes).map_err(PyErr::new::) +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!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, !"arrays", !5} +!5 = !{i32 99} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = + validate_qir(&bc_bytes, None).expect_err("malformed optional arrays flag should fail"); + assert!(err.contains("Missing or unsupported module flag: arrays")); } -} -#[cfg(feature = "python")] -define_stub_info_gatherer!(stub_info); + #[test] + fn test_validate_qir_missing_required_module_flag_reports_exact_message() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} -#[cfg(test)] -mod test { - #![allow(clippy::expect_used)] - #![allow(clippy::unwrap_used)] - use crate::{ - create_module_from_ir_text, get_entry_attributes, parse_bitcode_module, qir_ll_to_bc, - qir_to_qis, validate_qir, - }; - use inkwell::{context::Context, memory_buffer::MemoryBuffer, module::Module}; - use proptest::prelude::*; - use std::{collections::BTreeMap, sync::LazyLock}; - #[cfg(feature = "wasm")] - use wasm_encoder::{ExportKind, ExportSection, Module as WasmModule}; +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } - const PROPERTY_FIXTURES: &[&str] = &[ - "tests/data/base.ll", - "tests/data/base_array.ll", - "tests/data/adaptive.ll", - "tests/data/qir2_base.ll", - "tests/data/qir2_adaptive.ll", - "tests/data/mz_leaked.ll", - ]; - static PROPERTY_FIXTURE_BITCODE: LazyLock>> = - LazyLock::new(|| { - PROPERTY_FIXTURES - .iter() - .map(|path| { - let ll_text = - std::fs::read_to_string(path).expect("Failed to read LLVM IR fixture"); - let bitcode = qir_ll_to_bc(&ll_text) - .expect("Failed to convert LLVM IR fixture to bitcode"); - (*path, bitcode) - }) - .collect() - }); +!llvm.module.flags = !{!0, !1, !2} +!0 = !{i32 7, !"qir_minor_version", i32 0} +!1 = !{i32 1, !"dynamic_qubit_management", i1 false} +!2 = !{i32 1, !"dynamic_result_management", i1 false} +"#; - fn conservative_translation_settings() -> (u32, &'static str) { - (0, "native") + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None).expect_err("Missing flag should fail"); + assert!(err.contains("Missing required module flag: qir_major_version")); } - fn load_fixture_bitcode(path: &str) -> Vec { - PROPERTY_FIXTURE_BITCODE - .get(path) - .cloned() - .expect("Fixture bitcode should be precompiled") - } + #[test] + fn test_qir_to_qis_bool_output_uses_bool_tag_and_print_bool() { + let ll_text = r#" +%Result = type opaque - fn verify_bitcode_module(bitcode: &[u8], name: &str) -> Result<(), String> { - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, bitcode, name)?; - crate::llvm_verify::verify_module(&module, "LLVM verifier rejected translated module") - } +@bool_out = private constant [2 x i8] c"b\00" - fn parse_bitcode_as_file(bitcode: &[u8], name: &str) -> Result<(), String> { - let mut temp_file = tempfile::Builder::new() - .prefix(name) - .suffix(".bc") - .tempfile() - .map_err(|e| format!("Failed to create temp bitcode file: {e}"))?; - std::io::Write::write_all(&mut temp_file, bitcode) - .map_err(|e| format!("Failed to write temp bitcode: {e}"))?; +declare void @__quantum__rt__bool_record_output(i1, ptr) - let ctx = Context::create(); - let memory_buffer = MemoryBuffer::create_from_file(temp_file.path()) - .map_err(|e| format!("Failed to read temp bitcode: {e}"))?; - Module::parse_bitcode_from_buffer(&memory_buffer, &ctx) - .map(|_| ()) - .map_err(|e| format!("Failed to parse bitcode: {e}")) - } +define i64 @Entry_Point_Name() #0 { +entry: + call void @__quantum__rt__bool_record_output(i1 true, ptr getelementptr inbounds ([2 x i8], ptr @bool_out, i64 0, i64 0)) + ret i64 0 +} + +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let output_bc = + qir_to_qis(&bc_bytes, 0, "native", None).expect("bool output should compile"); - fn assert_public_bitcode_round_trips_from_file(bitcode: &[u8], name: &str) { let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, bitcode, name) - .expect("Bitcode should reparse through qir-qis helpers"); - let raw_buffer = module.write_bitcode_to_memory(); - let expected_len = bitcode - .len() - .checked_add(1) - .expect("bitcode length should not overflow"); - assert_eq!( - raw_buffer.as_slice().len(), - expected_len, - "Public bitcode bytes should exclude LLVM's implicit trailing NUL" - ); - assert_eq!(raw_buffer.as_slice().last(), Some(&0)); - parse_bitcode_as_file(bitcode, name) - .expect("Public bitcode should parse when consumed from a file"); - } + let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") + .expect("Compiled QIS bitcode should parse"); + assert!(module.get_function("print_bool").is_some()); - #[cfg(feature = "wasm")] - fn build_wasm_exports(exports: &[(String, u32)]) -> Vec { - let mut module = WasmModule::new(); - let mut export_section = ExportSection::new(); - for (name, index) in exports { - export_section.export(name, ExportKind::Func, *index); + #[cfg(not(windows))] + { + let text = module.to_string(); + assert!(text.contains("USER:BOOL:b")); + } + + #[cfg(windows)] + { + let labels = module + .get_globals() + .filter_map(|global| crate::convert::get_string_label(global).ok()) + .collect::>(); + assert!(labels.iter().any(|label| label.contains("USER:BOOL:b"))); } - module.section(&export_section); - module.finish() } - fn minimal_qir_with_body( - required_num_qubits: &str, - required_num_results: &str, - qir_major_flag: &str, - extra_decl: &str, - body: &str, - ) -> String { - format!( - r#"%Qubit = type opaque -%Result = type opaque + #[test] + fn test_validate_qir_rejects_malformed_barrier_suffix() { + let ll_text = r#" +%Qubit = type opaque -{extra_decl} +declare void @__quantum__qis__barrier2__adj(%Qubit*, %Qubit*) -define i64 @Entry_Point_Name() #0 {{ +define i64 @Entry_Point_Name() #0 { entry: -{body} + %q0 = inttoptr i64 1 to %Qubit* + %q1 = inttoptr i64 2 to %Qubit* + call void @__quantum__qis__barrier2__adj(%Qubit* %q0, %Qubit* %q1) ret i64 0 -}} +} -attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="{required_num_qubits}" "required_num_results"="{required_num_results}" }} +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" } -!llvm.module.flags = !{{!0, !1, !2, !3}} -!0 = !{{i32 1, !"qir_major_version", i32 {qir_major_flag}}} -!1 = !{{i32 7, !"qir_minor_version", i32 0}} -!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}} -!3 = !{{i32 1, !"dynamic_result_management", i1 false}} -"# - ) +!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} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None).expect_err("malformed barrier suffix should fail"); + assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier2__adj")); } - fn minimal_qir_missing_attr(missing_attr: &str) -> String { - let attrs = [ - ("entry_point", None), - ("qir_profiles", Some("base_profile")), - ("output_labeling_schema", Some("schema_id")), - ("required_num_qubits", Some("1")), - ("required_num_results", Some("1")), - ]; - let rendered_attrs = attrs - .into_iter() - .filter(|(name, _)| *name != missing_attr) - .map(|(name, value)| { - value.map_or_else( - || format!(r#""{name}""#), - |value| format!(r#""{name}"="{value}""#), - ) - }) - .collect::>() - .join(" "); + #[test] + fn test_validate_qir_accepts_barrier_matching_required_qubits() { + let ll_text = r#" +%Qubit = type opaque - format!( - r#" -define i64 @Entry_Point_Name() #0 {{ +declare void @__quantum__qis__barrier2__body(%Qubit*, %Qubit*) + +define i64 @Entry_Point_Name() #0 { entry: + %q0 = inttoptr i64 1 to %Qubit* + %q1 = inttoptr i64 2 to %Qubit* + call void @__quantum__qis__barrier2__body(%Qubit* %q0, %Qubit* %q1) ret i64 0 -}} +} -attributes #0 = {{ {rendered_attrs} }} +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" } -!llvm.module.flags = !{{!0, !1, !2, !3}} -!0 = !{{i32 1, !"qir_major_version", i32 1}} -!1 = !{{i32 7, !"qir_minor_version", i32 0}} -!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}} -!3 = !{{i32 1, !"dynamic_result_management", i1 false}} -"# - ) +!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} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + validate_qir(&bc_bytes, None) + .expect("barrier arity matching required_num_qubits should validate"); } - fn minimal_qir_with_duplicate_major_flags(first_major: &str, second_major: &str) -> String { - format!( - r#" -define i64 @Entry_Point_Name() #0 {{ + #[test] + fn test_validate_qir_rejects_zero_arity_barrier() { + let ll_text = r#" +%Qubit = type opaque + +declare void @__quantum__qis__barrier0__body() + +define i64 @Entry_Point_Name() #0 { entry: + call void @__quantum__qis__barrier0__body() ret i64 0 -}} +} -attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }} +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } -!llvm.module.flags = !{{!0, !1, !2, !3, !4}} -!0 = !{{i32 1, !"qir_major_version", i32 {first_major}}} -!1 = !{{i32 1, !"qir_major_version", i32 {second_major}}} -!2 = !{{i32 7, !"qir_minor_version", i32 0}} -!3 = !{{i32 1, !"dynamic_qubit_management", i1 false}} -!4 = !{{i32 1, !"dynamic_result_management", i1 false}} -"# - ) +!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} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None).expect_err("barrier0 should be rejected"); + assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier0__body")); } - fn minimal_qir_with_duplicate_dynamic_flags(first_flag: &str, second_flag: &str) -> String { - format!( - r#" -define i64 @Entry_Point_Name() #0 {{ + #[test] + fn test_validate_qir_rejects_unsupported_qtm_function() { + let ll_text = r#" +declare void @___unknown_qtm() + +define i64 @Entry_Point_Name() #0 { entry: + call void @___unknown_qtm() ret i64 0 -}} +} -attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }} +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } -!llvm.module.flags = !{{!0, !1, !2, !3, !4}} -!0 = !{{i32 1, !"qir_major_version", i32 1}} -!1 = !{{i32 7, !"qir_minor_version", i32 0}} -!2 = !{{i32 1, !"dynamic_qubit_management", i1 {first_flag}}} -!3 = !{{i32 1, !"dynamic_qubit_management", i1 {second_flag}}} -!4 = !{{i32 1, !"dynamic_result_management", i1 false}} -"# - ) - } +!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} +"#; - #[test] - fn test_get_entry_attributes() { - let ll_text = std::fs::read_to_string("tests/data/base-attrs.ll") - .expect("Failed to read base-attrs.ll"); - let bc_bytes = qir_ll_to_bc(&ll_text).unwrap(); - let attrs = get_entry_attributes(&bc_bytes).unwrap(); - assert!(matches!(attrs.get("entry_point"), Some(None))); - assert_eq!( - attrs.get("qir_profiles"), - Some(&Some("base_profile".to_string())) - ); - assert_eq!( - attrs.get("output_labeling_schema"), - Some(&Some("labeled".to_string())) - ); - assert_eq!( - attrs.get("required_num_qubits"), - Some(&Some("2".to_string())) - ); - assert_eq!( - attrs.get("required_num_results"), - Some(&Some("2".to_string())) - ); + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("unsupported QTM declarations should fail validation"); + assert!(err.contains("Unsupported Qtm QIS function: ___unknown_qtm")); } #[test] - fn test_entry_attributes_includes_optional_custom_attr() { + fn test_validate_qir_allows_ir_defined_non_main_helper() { let ll_text = r#" +%Qubit = type opaque + +define void @helper(%Qubit* %qubit) { +entry: + call void @__quantum__qis__h__body(%Qubit* %qubit) + ret void +} + define i64 @Entry_Point_Name() #0 { entry: + %q0 = inttoptr i64 1 to %Qubit* + call void @helper(%Qubit* %q0) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" } +declare void @__quantum__qis__h__body(%Qubit*) -!llvm.module.flags = !{!0, !1, !2, !3} +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +!llvm.module.flags = !{!0, !1, !2, !3} !0 = !{i32 1, !"qir_major_version", i32 1} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); - let attrs = get_entry_attributes(&bc_bytes).unwrap(); - assert_eq!( - attrs.get("custom_attr"), - Some(&Some("custom_value".to_string())) - ); + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + validate_qir(&bc_bytes, None) + .expect("IR-defined helper functions with non-main names should be allowed"); } #[test] - fn test_get_entry_attributes_is_order_independent() { + fn test_validate_qir_allows_external_pointer_returning_declarations() { let ll_text = r#" +%Qubit = type opaque + +declare ptr @external_helper() + define i64 @Entry_Point_Name() #0 { entry: ret i64 0 } -attributes #0 = { "custom_attr"="custom_value" "required_num_results"="2" "output_labeling_schema"="labeled" "entry_point" "required_num_qubits"="2" "qir_profiles"="base_profile" } +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } !llvm.module.flags = !{!0, !1, !2, !3} !0 = !{i32 1, !"qir_major_version", i32 1} @@ -2176,76 +4538,55 @@ attributes #0 = { "custom_attr"="custom_value" "required_num_results"="2" "outpu !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} "#; + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let attrs = get_entry_attributes(&bc_bytes).expect("entry attributes should parse"); - assert!(matches!(attrs.get("entry_point"), Some(None))); - assert_eq!( - attrs.get("qir_profiles"), - Some(&Some("base_profile".to_string())) - ); - assert_eq!( - attrs.get("custom_attr"), - Some(&Some("custom_value".to_string())) - ); + validate_qir(&bc_bytes, None) + .expect("external declarations without bodies should not be treated as IR-defined"); } #[test] - fn test_qir_to_qis_strips_custom_entry_attrs() { + fn test_validate_qir_rejects_ir_defined_pointer_returning_function() { let ll_text = r#" +define ptr @helper() { +entry: + ret ptr null +} + define i64 @Entry_Point_Name() #0 { entry: ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" } +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } !llvm.module.flags = !{!0, !1, !2, !3} - !0 = !{i32 1, !"qir_major_version", i32 1} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); - let (opt_level, target) = if cfg!(windows) { - (0, "native") - } else { - (2, "aarch64") - }; - let qis_bytes = qir_to_qis(&bc_bytes, opt_level, target, None).unwrap(); - - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap(); - let entry_fn = module.get_function("___user_qir_Entry_Point_Name").unwrap(); - - assert!( - entry_fn - .get_string_attribute(inkwell::attributes::AttributeLoc::Function, "custom_attr") - .is_none() - ); - } - #[test] - fn test_platform_default_conversion_settings_match_expectations() { - if cfg!(windows) { - assert_eq!(crate::DEFAULT_OPT_LEVEL, 0); - assert_eq!(crate::DEFAULT_TARGET, "native"); - } else { - assert_eq!(crate::DEFAULT_OPT_LEVEL, 2); - assert_eq!(crate::DEFAULT_TARGET, "aarch64"); - } + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("IR-defined pointer-returning helper should fail validation"); + assert!(err.contains("Function `helper` cannot return a pointer type")); } - #[cfg(windows)] #[test] - fn test_windows_optimized_conversion_returns_actionable_error() { + fn test_qir_to_qis_rejects_unknown_declared_qis_function() { let ll_text = r#" +%Qubit = type opaque + +declare void @__quantum__qis__mystery__body(%Qubit*) + define i64 @Entry_Point_Name() #0 { entry: + %q0 = inttoptr i64 1 to %Qubit* + call void @__quantum__qis__mystery__body(%Qubit* %q0) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="1" "required_num_results"="1" } +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } !llvm.module.flags = !{!0, !1, !2, !3} !0 = !{i32 1, !"qir_major_version", i32 1} @@ -2253,624 +4594,936 @@ attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_s !2 = !{i32 1, !"dynamic_qubit_management", i1 false} !3 = !{i32 1, !"dynamic_result_management", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); - let err = qir_to_qis(&bc_bytes, 1, "native", None) - .expect_err("optimized conversion should fail fast on Windows"); - assert!(err.contains("currently unavailable on Windows")); - assert!(err.contains("opt_level=0")); - } - #[test] - fn test_qir_ll_to_bc_accepts_legacy_typed_pointers() { - let ll_text = - std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); - let bc_bytes = qir_ll_to_bc(&ll_text).unwrap(); - assert!(!bc_bytes.is_empty()); + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = qir_to_qis(&bc_bytes, 0, "native", None) + .expect_err("unknown declared QIS function should fail"); + assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__mystery__body")); } #[test] - fn test_qir_ll_to_bc_output_parses_when_read_from_file() { - let ll_text = - std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode"); + fn test_qir_to_qis_u1q_synonym_lowers_to_rxy() { + let ll_text = r#" +%Qubit = type opaque - assert_public_bitcode_round_trips_from_file(&bc_bytes, "public_qir_output"); - } +declare void @__quantum__qis__u1q__body(double, double, %Qubit*) - #[test] - fn test_qir2_base_fixture_validate_and_compile() { - let ll_text = std::fs::read_to_string("tests/data/qir2_base.ll") - .expect("Failed to read qir2_base.ll"); - let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_base.ll to bitcode"); +define i64 @Entry_Point_Name() #0 { +entry: + %q0 = inttoptr i64 1 to %Qubit* + call void @__quantum__qis__u1q__body(double 1.0, double 0.5, %Qubit* %q0) + ret i64 0 +} - validate_qir(&input_bc, None).expect("QIR 2.0 base fixture should validate"); +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3} +!0 = !{i32 1, !"qir_major_version", i32 1} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +"#; + + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); let output_bc = - qir_to_qis(&input_bc, 0, "native", None).expect("QIR 2.0 base fixture should compile"); + qir_to_qis(&bc_bytes, 0, "native", None).expect("u1q synonym should compile"); let ctx = Context::create(); let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") .expect("Compiled QIS bitcode should parse"); - assert!(module.get_function("qmain").is_some()); - assert!(module.get_function("qir_qis.load_qubit").is_some()); + assert!(module.get_function("___rxy").is_some()); + + #[cfg(not(windows))] + { + let text = module.to_string(); + assert!(text.contains("___rxy")); + } } #[test] - fn test_qir2_adaptive_fixture_validate_and_compile() { - let ll_text = std::fs::read_to_string("tests/data/qir2_adaptive.ll") - .expect("Failed to read qir2_adaptive.ll"); - let input_bc = - qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_adaptive.ll to bitcode"); + fn test_checked_result_index_rejects_out_of_bounds_values() { + let err = crate::aux::checked_result_index(5, 1) + .expect_err("out-of-bounds result indices should fail cleanly"); + assert_eq!(err, "Result index 5 exceeds required_num_results (1)"); + } + + #[test] + fn test_validate_qir_allows_zero_required_num_results_for_mz_leaked() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + r#" +declare i64 @__quantum__qis__mz_leaked__body(%Qubit*) +declare void @__quantum__rt__int_record_output(i64, i8*) - validate_qir(&input_bc, None).expect("QIR 2.0 adaptive fixture should validate"); - let output_bc = qir_to_qis(&input_bc, 0, "native", None) - .expect("QIR 2.0 adaptive fixture should compile"); +@0 = private constant [7 x i8] c"leaked\00" +"#, + r" %q0 = inttoptr i64 0 to %Qubit* + %0 = call i64 @__quantum__qis__mz_leaked__body(%Qubit* %q0) + call void @__quantum__rt__int_record_output(i64 %0, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @0, i64 0, i64 0))", + ); - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") - .expect("Compiled QIS bitcode should parse"); - assert!(module.get_function("qmain").is_some()); - assert!(module.get_function("___lazy_measure").is_some()); + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + validate_qir(&bc_bytes, None) + .expect("mz_leaked-only programs should validate with zero result slots"); } #[test] - fn test_qir_to_qis_output_parses_with_raw_llvm_buffer() { - let ll_text = - std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll"); - let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode"); - let output_bc = - qir_to_qis(&input_bc, 0, "native", None).expect("base fixture should compile"); + fn test_validate_qir_reports_invalid_required_num_results_value() { + let ll_text = minimal_qir_with_body("1", "abc", "1", "", ""); - assert_public_bitcode_round_trips_from_file(&output_bc, "selene_qis_output"); + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("invalid required_num_results should fail validation"); + assert!(err.contains("Invalid required_num_results attribute value: abc")); } #[test] - fn test_validate_module_flags_are_checked_cross_platform() { + fn test_validate_qir_reports_missing_required_num_results_once() { let ll_text = r#" +%Qubit = type opaque + define i64 @Entry_Point_Name() #0 { entry: ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } !llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 2} +!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} "#; + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - validate_qir(&bc_bytes, None).expect("Module flags should validate on every platform"); + let err = + validate_qir(&bc_bytes, None).expect_err("missing required_num_results should fail"); + assert_eq!(err, "Missing required attribute: `required_num_results`"); } #[test] - fn test_module_flag_parser_reads_existing_flags() { - use crate::aux::collect_module_flags; - use inkwell::context::Context; - + fn test_validate_qir_rejects_result_usage_in_ir_defined_helper_with_zero_slots() { let ll_text = r#" +%Qubit = type opaque +%Result = type opaque + +declare i1 @__quantum__rt__read_result(%Result*) + +define internal void @helper() { +entry: + %0 = call i1 @__quantum__rt__read_result(%Result* null) + ret void +} + define i64 @Entry_Point_Name() #0 { entry: + call void @helper() ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="0" } !llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 2} +!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} "#; - let ctx = Context::create(); - let module = create_module_from_ir_text(&ctx, ll_text, "qir") - .expect("Failed to create module from inline IR"); + let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None).expect_err( + "result usage in IR-defined helpers should still respect required_num_results", + ); + assert!(err.contains("Result index 0 exceeds required_num_results (0)")); + } - let flags = collect_module_flags(&module); - assert_eq!( - flags.get("qir_major_version").map(<[String]>::to_vec), - Some(vec!["i32 2".to_string()]) + #[test] + fn test_validate_qir_rejects_zero_required_num_results_for_result_measurement() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)", + r" call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)", ); - assert_eq!( - flags.get("qir_minor_version").map(<[String]>::to_vec), - Some(vec!["i32 0".to_string()]) + + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("result-backed measurements should fail validation without result slots"); + assert!(err.contains("Result index 0 exceeds required_num_results (0)")); + } + + #[test] + fn test_validate_qir_rejects_zero_required_num_results_for_read_result() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare i1 @__quantum__rt__read_result(%Result*)", + r" %0 = call i1 @__quantum__rt__read_result(%Result* null)", ); - assert_eq!( - flags - .get("dynamic_qubit_management") - .map(<[String]>::to_vec), - Some(vec!["i1 false".to_string()]) + + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("result reads should fail validation without result slots"); + assert!(err.contains("Result index 0 exceeds required_num_results (0)")); + } + + #[test] + fn test_validate_qir_rejects_zero_required_num_results_for_result_record_output() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + r#" +declare void @__quantum__rt__result_record_output(%Result*, i8*) + +@0 = private constant [4 x i8] c"res\00" +"#, + r" call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i64 0, i64 0))", ); - assert_eq!( - flags - .get("dynamic_result_management") - .map(<[String]>::to_vec), - Some(vec!["i1 false".to_string()]) + + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("result output should fail validation without result slots"); + assert!(err.contains("Result index 0 exceeds required_num_results (0)")); + } + + #[test] + fn test_validate_qir_rejects_out_of_bounds_result_measurement_index() { + let ll_text = minimal_qir_with_body( + "1", + "1", + "1", + "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)", + r" call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly inttoptr (i64 5 to %Result*))", ); + + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = validate_qir(&bc_bytes, None) + .expect_err("out-of-bounds result indices should fail during validation"); + assert!(err.contains("Result index 5 exceeds required_num_results (1)")); } #[test] - fn test_validate_module_flags_accept_duplicate_entries_if_one_matches() { - let ll_text = r#" -define i64 @Entry_Point_Name() #0 { -entry: - ret i64 0 -} + fn test_qir_to_qis_rejects_malformed_mz_leaked_call() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare i64 @__quantum__qis__mz_leaked__body()", + r" %0 = call i64 @__quantum__qis__mz_leaked__body()", + ); -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = qir_to_qis(&bc_bytes, 0, "native", None) + .expect_err("malformed mz_leaked calls should fail cleanly"); + assert!(err.contains("Malformed mz_leaked call")); + } -!llvm.module.flags = !{!0, !1, !2, !3, !4} -!0 = !{i32 1, !"qir_major_version", i32 99} -!1 = !{i32 1, !"qir_major_version", i32 2} -!2 = !{i32 7, !"qir_minor_version", i32 0} -!3 = !{i32 1, !"dynamic_qubit_management", i1 false} -!4 = !{i32 1, !"dynamic_result_management", i1 false} -"#; + #[test] + fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_type() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare void @__quantum__qis__mz_leaked__body(%Qubit*)", + r" call void @__quantum__qis__mz_leaked__body(%Qubit* null)", + ); - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - validate_qir(&bc_bytes, None) - .expect("Module flags should validate when any duplicate entry matches"); + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = qir_to_qis(&bc_bytes, 0, "native", None) + .expect_err("mz_leaked with the wrong signature should fail cleanly"); + assert!(err.contains("Malformed mz_leaked call: expected signature i64 (ptr)")); } #[test] - fn test_validate_module_flags_reports_malformed_required_flag() { - let ll_text = r#" -define i64 @Entry_Point_Name() #0 { -entry: - ret i64 0 -} + fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_width() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare i1 @__quantum__qis__mz_leaked__body(%Qubit*)", + r" %0 = call i1 @__quantum__qis__mz_leaked__body(%Qubit* null)", + ); -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = qir_to_qis(&bc_bytes, 0, "native", None) + .expect_err("mz_leaked with the wrong return width should fail cleanly"); + assert_eq!( + err, + "Malformed mz_leaked call: expected signature i64 (ptr)" + ); + } + + #[test] + fn test_qir_to_qis_rejects_mz_leaked_with_non_pointer_parameter() { + let ll_text = minimal_qir_with_body( + "1", + "0", + "1", + "declare i64 @__quantum__qis__mz_leaked__body(i64)", + r" %0 = call i64 @__quantum__qis__mz_leaked__body(i64 0)", + ); + + let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); + let err = qir_to_qis(&bc_bytes, 0, "native", None) + .expect_err("mz_leaked with a non-pointer parameter should fail cleanly"); + assert_eq!( + err, + "Malformed mz_leaked call: expected signature i64 (ptr)" + ); + } + + #[test] + fn test_mz_leaked_operand_check_rejects_non_pointer_first_operand() { + let ctx = Context::create(); + let value = ctx.i64_type().const_zero().into(); + let err = crate::aux::mz_leaked_qubit_operand(&[value, value]) + .expect_err("non-pointer mz_leaked operands should fail cleanly"); + assert_eq!( + err, + "Malformed mz_leaked call: expected first argument to be a pointer" + ); + } + + #[cfg(not(windows))] + #[test] + fn test_qir_to_qis_mz_leaked_lowers_via_uint_future_runtime() { + let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll"); + let output_bc = qir_to_qis(&bc_bytes, 0, "native", None) + .expect("mz_leaked fixture should compile successfully"); -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", !4} -!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 99} -"#; + verify_bitcode_module(&output_bc, "mz_leaked_qis") + .expect("translated leaked-measure module should remain LLVM-verifiable"); - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None).expect_err("Malformed module flag should fail"); - assert!(err.contains("Missing or unsupported module flag: qir_major_version")); + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis") + .expect("Compiled QIS bitcode should parse"); + let text = module.to_string(); + assert!(text.contains("___lazy_measure_leaked")); + assert!(text.contains("___read_future_uint")); + assert!(text.contains("___dec_future_refcount")); + assert!(!text.contains("___read_future_bool")); } + #[cfg(windows)] #[test] - fn test_validate_qir_reports_exact_single_expected_module_flag_value() { - let ll_text = r#" -define i64 @Entry_Point_Name() #0 { -entry: - ret i64 0 -} + fn test_qir_to_qis_mz_leaked_windows_smoke() { + let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll"); + let output_bc = qir_to_qis(&bc_bytes, 0, "native", None) + .expect("mz_leaked fixture should compile successfully on Windows"); -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis") + .expect("Compiled QIS bitcode should parse on Windows"); + assert!(module.get_function("qmain").is_some()); + } -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} -!1 = !{i32 7, !"qir_minor_version", i32 99} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} -"#; + #[cfg(feature = "wasm")] + proptest! { + #[test] + fn prop_get_wasm_functions_round_trips_exact_exports( + exports in proptest::collection::btree_map("[A-Za-z_][A-Za-z0-9_]{0,8}", 0u32..32u32, 0..8) + ) { + let exports_vec = exports + .iter() + .map(|(name, index)| (name.clone(), *index)) + .collect::>(); + let wasm = build_wasm_exports(&exports_vec); + let parsed = crate::get_wasm_functions(Some(&wasm)) + .map_err(|err| TestCaseError::fail(format!("get_wasm_functions failed unexpectedly: {err}")))?; - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("unsupported single-valued module flag should fail"); - assert!(err.contains("Unsupported qir_minor_version: expected i32 0")); + prop_assert_eq!(parsed.len(), exports.len()); + for (name, index) in exports { + prop_assert_eq!(parsed.get(&name), Some(&u64::from(index))); + } + } } - #[test] - fn test_validate_qir_missing_required_module_flag_reports_exact_message() { - let ll_text = r#" -define i64 @Entry_Point_Name() #0 { -entry: - ret i64 0 -} + proptest! { + #[cfg(not(windows))] + #[test] + fn prop_qir_ll_to_bc_rejects_malformed_ir(suffix in "\\PC{0,128}") { + let ll_text = format!("this is not valid llvm ir\n{suffix}"); + prop_assert!(qir_ll_to_bc(&ll_text).is_err()); + } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + #[cfg(not(windows))] + #[test] + fn prop_validate_qir_rejects_malformed_bitcode(tail in proptest::collection::vec(any::(), 0..256)) { + let mut bytes = b"NOTQIR".to_vec(); + bytes.extend(tail); + prop_assert!(validate_qir(&bytes, None).is_err()); + } -!llvm.module.flags = !{!0, !1, !2} -!0 = !{i32 7, !"qir_minor_version", i32 0} -!1 = !{i32 1, !"dynamic_qubit_management", i1 false} -!2 = !{i32 1, !"dynamic_result_management", i1 false} -"#; + #[cfg(not(windows))] + #[test] + fn prop_qir_to_qis_rejects_malformed_bitcode(tail in proptest::collection::vec(any::(), 0..256)) { + let mut bytes = b"NOTQIR".to_vec(); + bytes.extend(tail); + let (opt_level, target) = conservative_translation_settings(); + prop_assert!(qir_to_qis(&bytes, opt_level, target, None).is_err()); + } - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None).expect_err("Missing flag should fail"); - assert!(err.contains("Missing required module flag: qir_major_version")); - } + #[test] + fn prop_valid_fixtures_translate_to_verifiable_qis(fixture in proptest::sample::select(PROPERTY_FIXTURES)) { + let input_bc = load_fixture_bitcode(fixture); + prop_assert!(validate_qir(&input_bc, None).is_ok()); - #[test] - fn test_qir_to_qis_bool_output_uses_bool_tag_and_print_bool() { - let ll_text = r#" -%Result = type opaque + let (opt_level, target) = conservative_translation_settings(); + let output_bc = qir_to_qis(&input_bc, opt_level, target, None) + .map_err(|err| TestCaseError::fail(format!("translation failed for {fixture}: {err}")))?; -@bool_out = private constant [2 x i8] c"b\00" + verify_bitcode_module(&output_bc, "property_qis_module") + .map_err(|err| TestCaseError::fail(format!("verification failed for {fixture}: {err}")))?; + } -declare void @__quantum__rt__bool_record_output(i1, ptr) + #[test] + fn prop_invalid_targets_fail_fast(target in "[a-z0-9_-]{1,12}") { + prop_assume!(target != "native"); + prop_assume!(target != "aarch64"); + prop_assume!(target != "x86-64"); -define i64 @Entry_Point_Name() #0 { -entry: - call void @__quantum__rt__bool_record_output(i1 true, ptr getelementptr inbounds ([2 x i8], ptr @bool_out, i64 0, i64 0)) - ret i64 0 -} + let input_bc = load_fixture_bitcode("tests/data/base.ll"); + prop_assert!(qir_to_qis(&input_bc, 0, &target, None).is_err()); + } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + #[test] + fn prop_missing_required_attrs_fail_validation( + missing_idx in 0usize..4usize + ) { + let missing_attr = [ + "qir_profiles", + "output_labeling_schema", + "required_num_qubits", + "required_num_results", + ][missing_idx]; + let ll_text = minimal_qir_missing_attr(missing_attr); + let bc = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; + let err = validate_qir(&bc, None) + .expect_err("validation should reject missing required attributes"); + let expected = format!("Missing required attribute: `{missing_attr}`"); + prop_assert!(err.contains(&expected)); + } -!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} -"#; + #[test] + fn prop_qir_major_versions_accept_only_one_or_two(major in 0u32..5u32) { + let ll_text = minimal_qir_with_body("1", "1", &major.to_string(), "", ""); + let bc = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; + let result = validate_qir(&bc, None); + if matches!(major, 1 | 2) { + prop_assert!(result.is_ok()); + } else { + let err = result.expect_err("invalid major versions must fail"); + prop_assert!(err.contains("Unsupported qir_major_version")); + } + } - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let output_bc = - qir_to_qis(&bc_bytes, 0, "native", None).expect("bool output should compile"); + #[test] + fn prop_duplicate_qir_major_flags_pass_if_any_match( + valid_first in any::(), + valid_second in any::(), + ) { + let first_major = if valid_first { "1" } else { "99" }; + let second_major = if valid_second { "2" } else { "100" }; + let ll_text = minimal_qir_with_duplicate_major_flags(first_major, second_major); + let bc = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; + let result = validate_qir(&bc, None); + if valid_first || valid_second { + prop_assert!(result.is_ok()); + } else { + let err = result.expect_err("all-invalid duplicate major flags must fail"); + prop_assert!(err.contains("Unsupported qir_major_version")); + } + } - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") - .expect("Compiled QIS bitcode should parse"); - assert!(module.get_function("print_bool").is_some()); + #[test] + fn prop_duplicate_dynamic_qubit_flags_accept_true_and_false_values( + first_is_true in any::(), + second_is_true in any::(), + ) { + let first_flag = if first_is_true { "true" } else { "false" }; + let second_flag = if second_is_true { "true" } else { "false" }; + let ll_text = minimal_qir_with_duplicate_dynamic_flags(first_flag, second_flag); + let bc = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; + prop_assert!(validate_qir(&bc, None).is_ok()); + } + + #[test] + fn prop_barrier_validation_tracks_required_qubits( + required_num_qubits in 1u32..5u32, + barrier_arity in 1u32..5u32, + ) { + let barrier_name = format!("__quantum__qis__barrier{barrier_arity}__body"); + let barrier_args = (0..barrier_arity) + .map(|idx| format!("%Qubit* %q{idx}")) + .collect::>() + .join(", "); + let extra_decl = format!( + "declare void @{barrier_name}({})", + std::iter::repeat_n("%Qubit*", usize::try_from(barrier_arity).unwrap_or(0)) + .collect::>() + .join(", ") + ); + let body = (0..barrier_arity) + .map(|idx| { + let one_based_idx = idx.saturating_add(1); + format!(" %q{idx} = inttoptr i64 {one_based_idx} to %Qubit*") + }) + .chain(std::iter::once(format!(" call void @{barrier_name}({barrier_args})"))) + .collect::>() + .join("\n"); + let ll_text = minimal_qir_with_body( + &required_num_qubits.to_string(), + "1", + "1", + &extra_decl, + &body, + ); + let bc = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; + let result = validate_qir(&bc, None); + if barrier_arity <= required_num_qubits { + prop_assert!(result.is_ok()); + } else { + let err = result.expect_err("oversized barrier arity must fail"); + prop_assert!(err.contains("Barrier arity")); + } + } #[cfg(not(windows))] - { - let text = module.to_string(); - assert!(text.contains("USER:BOOL:b")); + #[test] + fn prop_get_entry_attributes_rejects_malformed_bitcode( + tail in proptest::collection::vec(any::(), 0..256) + ) { + let mut bytes = b"NOTQIR".to_vec(); + bytes.extend(tail); + prop_assert!(get_entry_attributes(&bytes).is_err()); } + } - #[cfg(windows)] - { - let labels = module - .get_globals() - .filter_map(|global| crate::convert::get_string_label(global).ok()) - .collect::>(); - assert!(labels.iter().any(|label| label.contains("USER:BOOL:b"))); - } + #[test] + fn test_zero_qubits_fail_validation() { + let ll_text = minimal_qir_with_body("0", "1", "1", "", ""); + let bc = qir_ll_to_bc(&ll_text).expect("inline IR should parse"); + let err = validate_qir(&bc, None).expect_err("validation should reject zero qubits"); + assert!(err.contains("Entry function must have at least one qubit")); } #[test] - fn test_validate_qir_rejects_malformed_barrier_suffix() { + fn test_validate_dynamic_qubits_without_required_num_qubits() { let ll_text = r#" -%Qubit = type opaque - -declare void @__quantum__qis__barrier2__adj(%Qubit*, %Qubit*) - define i64 @Entry_Point_Name() #0 { entry: - %q0 = inttoptr i64 1 to %Qubit* - %q1 = inttoptr i64 2 to %Qubit* - call void @__quantum__qis__barrier2__adj(%Qubit* %q0, %Qubit* %q1) + %err = alloca i1, align 1 + %q = call ptr @__quantum__rt__qubit_allocate(ptr %err) + call void @__quantum__qis__h__body(ptr %q) + call void @__quantum__rt__qubit_release(ptr %q) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" } +declare ptr @__quantum__rt__qubit_allocate(ptr) +declare void @__quantum__rt__qubit_release(ptr) +declare void @__quantum__qis__h__body(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} !3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None).expect_err("malformed barrier suffix should fail"); - assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier2__adj")); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + validate_qir(&bc_bytes, None).expect("dynamic qubit fixture should validate"); + let qis_bytes = + qir_to_qis(&bc_bytes, 0, "native", None).expect("dynamic qubit fixture should compile"); + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap(); + assert!(module.get_function("qir_qis.qubit_allocate").is_some()); + assert!(module.get_function("qir_qis.qubit_release").is_some()); } #[test] - fn test_validate_qir_accepts_barrier_matching_required_qubits() { + fn test_validate_capability_usage_ignores_unused_rt_declarations() { let ll_text = r#" -%Qubit = type opaque - -declare void @__quantum__qis__barrier2__body(%Qubit*, %Qubit*) - define i64 @Entry_Point_Name() #0 { entry: - %q0 = inttoptr i64 1 to %Qubit* - %q1 = inttoptr i64 2 to %Qubit* - call void @__quantum__qis__barrier2__body(%Qubit* %q0, %Qubit* %q1) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" } +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !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, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); validate_qir(&bc_bytes, None) - .expect("barrier arity matching required_num_qubits should validate"); + .expect("unused dynamic result declarations should not fail validation"); } #[test] - fn test_validate_qir_rejects_zero_arity_barrier() { + fn test_validate_capability_usage_reports_called_rt_function_without_flag() { let ll_text = r#" -%Qubit = type opaque - -declare void @__quantum__qis__barrier0__body() - define i64 @Entry_Point_Name() #0 { entry: - call void @__quantum__qis__barrier0__body() + %err = alloca i1, align 1 + %r = call ptr @__quantum__rt__result_allocate(ptr %err) + call void @__quantum__rt__result_release(ptr %r) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !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, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None).expect_err("barrier0 should be rejected"); - assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier0__body")); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None) + .expect_err("called dynamic result functions should fail validation"); + assert!( + err.contains( + "__quantum__rt__result_allocate requires `dynamic_result_management=true`" + ) + ); } #[test] - fn test_validate_qir_rejects_unsupported_qtm_function() { + fn test_validate_capability_usage_reports_called_dynamic_qubit_rt_function_without_flag() { let ll_text = r#" -declare void @___unknown_qtm() - define i64 @Entry_Point_Name() #0 { entry: - call void @___unknown_qtm() + %err = alloca i1, align 1 + %q = call ptr @__quantum__rt__qubit_allocate(ptr %err) + call void @__quantum__rt__qubit_release(ptr %q) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +declare ptr @__quantum__rt__qubit_allocate(ptr) +declare void @__quantum__rt__qubit_release(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !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, !"arrays", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); let err = validate_qir(&bc_bytes, None) - .expect_err("unsupported QTM declarations should fail validation"); - assert!(err.contains("Unsupported Qtm QIS function: ___unknown_qtm")); + .expect_err("called dynamic qubit functions should fail validation"); + assert!( + err.contains("__quantum__rt__qubit_allocate requires `dynamic_qubit_management=true`") + ); } #[test] - fn test_validate_qir_allows_ir_defined_non_main_helper() { + fn test_validate_qir_rejects_malformed_dynamic_qubit_allocate_signature() { let ll_text = r#" -%Qubit = type opaque - -define void @helper(%Qubit* %qubit) { -entry: - call void @__quantum__qis__h__body(%Qubit* %qubit) - ret void -} - define i64 @Entry_Point_Name() #0 { entry: - %q0 = inttoptr i64 1 to %Qubit* - call void @helper(%Qubit* %q0) + %q = call ptr @__quantum__rt__qubit_allocate() + call void @__quantum__rt__qubit_release(ptr %q) ret i64 0 } -declare void @__quantum__qis__h__body(%Qubit*) +declare ptr @__quantum__rt__qubit_allocate() +declare void @__quantum__rt__qubit_release(ptr) -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} -!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} !3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - validate_qir(&bc_bytes, None) - .expect("IR-defined helper functions with non-main names should be allowed"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None) + .expect_err("malformed dynamic runtime declaration should fail validation"); + assert!( + err.contains("Malformed QIR RT function declaration: __quantum__rt__qubit_allocate") + ); } #[test] - fn test_validate_qir_allows_external_pointer_returning_declarations() { + fn test_validate_qir_rejects_unsupported_rt_function_declaration() { let ll_text = r#" -%Qubit = type opaque - -declare ptr @external_helper() +declare void @__quantum__rt__mystery() define i64 @Entry_Point_Name() #0 { entry: ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !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, !"arrays", i1 false} "#; - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - validate_qir(&bc_bytes, None) - .expect("external declarations without bodies should not be treated as IR-defined"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = + validate_qir(&bc_bytes, None).expect_err("unsupported RT declarations should fail"); + assert!(err.contains("Unsupported QIR RT function: __quantum__rt__mystery")); } #[test] - fn test_validate_qir_rejects_ir_defined_pointer_returning_function() { + fn test_validate_dynamic_results_without_required_num_results() { let ll_text = r#" -define ptr @helper() { -entry: - ret ptr null -} +@0 = internal constant [3 x i8] c"r0\00" define i64 @Entry_Point_Name() #0 { entry: + %r = call ptr @__quantum__rt__result_allocate(ptr null) + call void @__quantum__qis__mz__body(ptr null, ptr %r) + call void @__quantum__rt__result_record_output(ptr %r, ptr @0) + call void @__quantum__rt__result_release(ptr %r) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("IR-defined pointer-returning helper should fail validation"); - assert!(err.contains("Function `helper` cannot return a pointer type")); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + validate_qir(&bc_bytes, None).expect("dynamic result fixture should validate"); + let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None) + .expect("dynamic result fixture should compile"); + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap(); + assert!(module.get_function("qir_qis.result_read").is_some()); + assert!(module.get_function("qir_qis.out_err_success").is_some()); } #[test] - fn test_qir_to_qis_rejects_unknown_declared_qis_function() { + fn test_validate_dynamic_result_array_record_output() { let ll_text = r#" -%Qubit = type opaque - -declare void @__quantum__qis__mystery__body(%Qubit*) +@0 = internal constant [3 x i8] c"a0\00" define i64 @Entry_Point_Name() #0 { entry: - %q0 = inttoptr i64 1 to %Qubit* - call void @__quantum__qis__mystery__body(%Qubit* %q0) + %results = alloca [2 x ptr], align 8 + call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null) + %r0_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 0 + %r0 = load ptr, ptr %r0_ptr, align 8 + %r1_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 1 + %r1 = load ptr, ptr %r1_ptr, align 8 + call void @__quantum__qis__mz__body(ptr null, ptr %r0) + call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %r1) + call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0) + call void @__quantum__rt__result_array_release(i64 2, ptr %results) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr) +declare void @__quantum__rt__result_array_release(i64, ptr) +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} "#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + validate_qir(&bc_bytes, None).expect("dynamic result array fixture should validate"); + let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None) + .expect("dynamic result array fixture should compile"); + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap(); + assert!( + module + .get_function("qir_qis.result_array_record_output") + .is_some() + ); + assert!(module.get_function("qir_qis.out_err_success").is_some()); - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = qir_to_qis(&bc_bytes, 0, "native", None) - .expect_err("unknown declared QIS function should fail"); - assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__mystery__body")); + #[cfg(not(windows))] + { + let module_text = module.to_string(); + assert!( + module_text.contains("USER:RESULT_ARRAY:a0"), + "expected RESULT_ARRAY output tag, got module:\n{module_text}" + ); + let print_bool_arr_calls = module_text.matches("@print_bool_arr").count(); + assert!( + print_bool_arr_calls >= 2, + "expected print_bool_arr declaration and use, got module:\n{module_text}" + ); + assert!( + !module_text.contains("@print_bool("), + "expected array output lowering without scalar print_bool fallback, got module:\n{module_text}" + ); + } + + #[cfg(windows)] + { + let labels = module + .get_globals() + .filter_map(|global| crate::convert::get_string_label(global).ok()) + .collect::>(); + assert!( + labels + .iter() + .any(|label| label.contains("USER:RESULT_ARRAY:a0")), + "expected RESULT_ARRAY output label in globals, got labels: {labels:?}" + ); + assert!(module.get_function("print_bool_arr").is_some()); + assert!(module.get_function("print_bool").is_none()); + } } #[test] - fn test_qir_to_qis_u1q_synonym_lowers_to_rxy() { + fn test_validate_dynamic_result_array_record_output_length_mismatch_fails() { let ll_text = r#" -%Qubit = type opaque - -declare void @__quantum__qis__u1q__body(double, double, %Qubit*) +@0 = internal constant [3 x i8] c"a0\00" define i64 @Entry_Point_Name() #0 { entry: - %q0 = inttoptr i64 1 to %Qubit* - call void @__quantum__qis__u1q__body(double 1.0, double 0.5, %Qubit* %q0) + %results = alloca [1 x ptr], align 8 + call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let output_bc = - qir_to_qis(&bc_bytes, 0, "native", None).expect("u1q synonym should compile"); - - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") - .expect("Compiled QIS bitcode should parse"); - assert!(module.get_function("___rxy").is_some()); - - #[cfg(not(windows))] - { - let text = module.to_string(); - assert!(text.contains("___rxy")); - } - } - - #[test] - fn test_checked_result_index_rejects_out_of_bounds_values() { - let err = crate::aux::checked_result_index(5, 1) - .expect_err("out-of-bounds result indices should fail cleanly"); - assert_eq!(err, "Result index 5 exceeds required_num_results (1)"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None) + .expect_err("mismatched result array output backing should fail validation"); + assert!(err.contains( + "__quantum__rt__result_array_record_output requires a fixed-size backing array" + )); + assert!(err.contains("requested length 2 does not match backing array length 1")); } #[test] - fn test_validate_qir_allows_zero_required_num_results_for_mz_leaked() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - r#" -declare i64 @__quantum__qis__mz_leaked__body(%Qubit*) -declare void @__quantum__rt__int_record_output(i64, i8*) + fn test_validate_dynamic_result_array_record_output_large_length_fails() { + let ll_text = r#" +@0 = internal constant [3 x i8] c"a0\00" -@0 = private constant [7 x i8] c"leaked\00" -"#, - r" %q0 = inttoptr i64 0 to %Qubit* - %0 = call i64 @__quantum__qis__mz_leaked__body(%Qubit* %q0) - call void @__quantum__rt__int_record_output(i64 %0, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @0, i64 0, i64 0))", - ); +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [2147483648 x ptr], align 8 + call void @__quantum__rt__result_array_record_output(i64 2147483648, ptr %results, ptr @0) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - validate_qir(&bc_bytes, None) - .expect("mz_leaked-only programs should validate with zero result slots"); - } +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) - #[test] - fn test_validate_qir_reports_invalid_required_num_results_value() { - let ll_text = minimal_qir_with_body("1", "abc", "1", "", ""); +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); let err = validate_qir(&bc_bytes, None) - .expect_err("invalid required_num_results should fail validation"); - assert!(err.contains("Invalid required_num_results attribute value: abc")); + .expect_err("oversized result array output length should fail validation"); + assert!(err.contains( + "__quantum__rt__result_array_record_output requires an array length that fits in i32 for RESULT_ARRAY output" + )); } #[test] - fn test_validate_qir_reports_missing_required_num_results_once() { + fn test_validate_dynamic_result_allocate_outside_entry_block_fails() { let ll_text = r#" -%Qubit = type opaque - define i64 @Entry_Point_Name() #0 { entry: + br label %body + +body: + %r = call ptr @__quantum__rt__result_allocate(ptr null) + call void @__quantum__rt__result_release(ptr %r) ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = - validate_qir(&bc_bytes, None).expect_err("missing required_num_results should fail"); - assert_eq!(err, "Missing required attribute: `required_num_results`"); + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains("__quantum__rt__result_allocate is only supported in the entry block") + ); } #[test] - fn test_validate_qir_rejects_result_usage_in_ir_defined_helper_with_zero_slots() { + fn test_validate_dynamic_result_allocate_in_helper_fails() { let ll_text = r#" -%Qubit = type opaque -%Result = type opaque - -declare i1 @__quantum__rt__read_result(%Result*) - -define internal void @helper() { +define void @helper() { entry: - %0 = call i1 @__quantum__rt__read_result(%Result* null) + %r = call ptr @__quantum__rt__result_allocate(ptr null) + call void @__quantum__rt__result_release(ptr %r) ret void } @@ -2880,403 +5533,493 @@ entry: ret i64 0 } -attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="0" } +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) -!llvm.module.flags = !{!0, !1, !2, !3} -!0 = !{i32 1, !"qir_major_version", i32 1} +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} !1 = !{i32 7, !"qir_minor_version", i32 0} !2 = !{i32 1, !"dynamic_qubit_management", i1 false} -!3 = !{i32 1, !"dynamic_result_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 false} "#; - - let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None).expect_err( - "result usage in IR-defined helpers should still respect required_num_results", + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains("__quantum__rt__result_allocate is only supported in the entry block") ); - assert!(err.contains("Result index 0 exceeds required_num_results (0)")); } #[test] - fn test_validate_qir_rejects_zero_required_num_results_for_result_measurement() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)", - r" call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)", - ); + fn test_validate_input_defined_qir_qis_helper_fails() { + let ll_text = r#" +define ptr @qir_qis.qubit_allocate(ptr %out_err) { +entry: + ret ptr null +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("result-backed measurements should fail validation without result slots"); - assert!(err.contains("Result index 0 exceeds required_num_results (0)")); +define i64 @Entry_Point_Name() #0 { +entry: + ret i64 0 +} + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!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, !"arrays", i1 false} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!(err.contains("Input QIR must not define internal helper function")); + assert!(err.contains("qir_qis.qubit_allocate")); } #[test] - fn test_validate_qir_rejects_zero_required_num_results_for_read_result() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare i1 @__quantum__rt__read_result(%Result*)", - r" %0 = call i1 @__quantum__rt__read_result(%Result* null)", - ); + fn test_validate_dynamic_qubit_array_allocate_length_mismatch_fails() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [1 x ptr], align 8 + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr null) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("result reads should fail validation without result slots"); - assert!(err.contains("Result index 0 exceeds required_num_results (0)")); +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains("__quantum__rt__qubit_array_allocate requires a fixed-size backing array") + ); + assert!(err.contains("requested length 2 does not match backing array length 1")); } #[test] - fn test_validate_qir_rejects_zero_required_num_results_for_result_record_output() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - r#" -declare void @__quantum__rt__result_record_output(%Result*, i8*) + fn test_validate_dynamic_result_array_allocate_length_mismatch_fails() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [1 x ptr], align 8 + call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null) + ret i64 0 +} -@0 = private constant [4 x i8] c"res\00" -"#, - r" call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i64 0, i64 0))", - ); +declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr) - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("result output should fail validation without result slots"); - assert!(err.contains("Result index 0 exceeds required_num_results (0)")); +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains( + "__quantum__rt__result_array_allocate requires a fixed-size backing array" + ) + ); + assert!(err.contains("requested length 2 does not match backing array length 1")); } #[test] - fn test_validate_qir_rejects_out_of_bounds_result_measurement_index() { - let ll_text = minimal_qir_with_body( - "1", - "1", - "1", - "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)", - r" call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly inttoptr (i64 5 to %Result*))", - ); + fn test_validate_dynamic_qubit_array_release_length_mismatch_fails() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [1 x ptr], align 8 + call void @__quantum__rt__qubit_array_release(i64 2, ptr %qubits) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = validate_qir(&bc_bytes, None) - .expect_err("out-of-bounds result indices should fail during validation"); - assert!(err.contains("Result index 5 exceeds required_num_results (1)")); +declare void @__quantum__rt__qubit_array_release(i64, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains("__quantum__rt__qubit_array_release requires a fixed-size backing array") + ); + assert!(err.contains("requested length 2 does not match backing array length 1")); } #[test] - fn test_qir_to_qis_rejects_malformed_mz_leaked_call() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare i64 @__quantum__qis__mz_leaked__body()", - r" %0 = call i64 @__quantum__qis__mz_leaked__body()", - ); + fn test_validate_dynamic_result_array_release_length_mismatch_fails() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [1 x ptr], align 8 + call void @__quantum__rt__result_array_release(i64 2, ptr %results) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = qir_to_qis(&bc_bytes, 0, "native", None) - .expect_err("malformed mz_leaked calls should fail cleanly"); - assert!(err.contains("Malformed mz_leaked call")); +declare void @__quantum__rt__result_array_release(i64, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation"); + assert!( + err.contains("__quantum__rt__result_array_release requires a fixed-size backing array") + ); + assert!(err.contains("requested length 2 does not match backing array length 1")); } #[test] - fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_type() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare void @__quantum__qis__mz_leaked__body(%Qubit*)", - r" call void @__quantum__qis__mz_leaked__body(%Qubit* null)", - ); + fn test_validate_dynamic_qubit_array_allocate_bitcast_backing_succeeds() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [2 x ptr], align 8 + %backing = bitcast ptr %qubits to ptr + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = qir_to_qis(&bc_bytes, 0, "native", None) - .expect_err("mz_leaked with the wrong signature should fail cleanly"); - assert!(err.contains("Malformed mz_leaked call: expected signature i64 (ptr)")); +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + validate_qir(&bc_bytes, None).expect("bitcast-backed qubit array should validate"); } #[test] - fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_width() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare i1 @__quantum__qis__mz_leaked__body(%Qubit*)", - r" %0 = call i1 @__quantum__qis__mz_leaked__body(%Qubit* null)", - ); + fn test_validate_dynamic_qubit_array_allocate_zero_gep_backing_succeeds() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [2 x ptr], align 8 + %backing = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 0 + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null) + ret i64 0 +} - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = qir_to_qis(&bc_bytes, 0, "native", None) - .expect_err("mz_leaked with the wrong return width should fail cleanly"); - assert_eq!( - err, - "Malformed mz_leaked call: expected signature i64 (ptr)" - ); +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + validate_qir(&bc_bytes, None).expect("zero-index GEP backing should validate"); } #[test] - fn test_qir_to_qis_rejects_mz_leaked_with_non_pointer_parameter() { - let ll_text = minimal_qir_with_body( - "1", - "0", - "1", - "declare i64 @__quantum__qis__mz_leaked__body(i64)", - r" %0 = call i64 @__quantum__qis__mz_leaked__body(i64 0)", - ); - - let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode"); - let err = qir_to_qis(&bc_bytes, 0, "native", None) - .expect_err("mz_leaked with a non-pointer parameter should fail cleanly"); - assert_eq!( - err, - "Malformed mz_leaked call: expected signature i64 (ptr)" + fn test_validate_dynamic_qubit_array_allocate_nonzero_gep_backing_fails() { + let ll_text = r#" +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [2 x ptr], align 8 + %backing = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 1 + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null) + ret i64 0 +} + +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +"#; + let bc_bytes = qir_ll_to_bc(ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None).expect_err( + "non-zero GEP-backed pointer should not count as a fixed-size array backing", ); + assert!(err.contains( + "__quantum__rt__qubit_array_allocate requires a fixed-size backing array allocated as [N x ptr]" + )); } #[test] - fn test_mz_leaked_operand_check_rejects_non_pointer_first_operand() { + fn test_dynamic_qubit_array_allocate_rolls_back_on_failure() { + let ll_text = std::fs::read_to_string("tests/data/dynamic_qubit_array_checked.ll") + .expect("Failed to read dynamic_qubit_array_checked.ll"); + let input_bc = + qir_ll_to_bc(&ll_text).expect("Failed to convert dynamic_qubit_array_checked.ll"); + let output_bc = qir_to_qis(&input_bc, 0, "native", None) + .expect("dynamic qubit array checked fixture should compile"); + let ctx = Context::create(); - let value = ctx.i64_type().const_zero().into(); - let err = crate::aux::mz_leaked_qubit_operand(&[value, value]) - .expect_err("non-pointer mz_leaked operands should fail cleanly"); - assert_eq!( - err, - "Malformed mz_leaked call: expected first argument to be a pointer" + let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") + .expect("Compiled QIS bitcode should parse"); + let helper = module + .get_function("qir_qis.qubit_array_allocate") + .expect("qubit array allocate helper should exist"); + let called_functions = collect_called_function_names(helper); + assert!( + called_functions + .iter() + .any(|name| name == "qir_qis.qubit_array_release"), + "expected rollback release path in helper, got calls: {called_functions:?}" ); } - #[cfg(not(windows))] #[test] - fn test_qir_to_qis_mz_leaked_lowers_via_uint_future_runtime() { - let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll"); - let output_bc = qir_to_qis(&bc_bytes, 0, "native", None) - .expect("mz_leaked fixture should compile successfully"); - - verify_bitcode_module(&output_bc, "mz_leaked_qis") - .expect("translated leaked-measure module should remain LLVM-verifiable"); + fn test_dynamic_qubit_array_allocate_initializes_out_err() { + let ll_text = std::fs::read_to_string("tests/data/dynamic_qubit_array_checked.ll") + .expect("Failed to read dynamic_qubit_array_checked.ll"); + let input_bc = + qir_ll_to_bc(&ll_text).expect("Failed to convert dynamic_qubit_array_checked.ll"); + let output_bc = qir_to_qis(&input_bc, 0, "native", None) + .expect("dynamic qubit array checked fixture should compile"); let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis") + let module = parse_bitcode_module(&ctx, &output_bc, "qis_module") .expect("Compiled QIS bitcode should parse"); - let text = module.to_string(); - assert!(text.contains("___lazy_measure_leaked")); - assert!(text.contains("___read_future_uint")); - assert!(text.contains("___dec_future_refcount")); - assert!(!text.contains("___read_future_bool")); + let helper = module + .get_function("qir_qis.qubit_array_allocate") + .expect("qubit array allocate helper should exist"); + let called_functions = collect_called_function_names(helper); + assert!( + called_functions + .iter() + .any(|name| name == "qir_qis.out_err_success"), + "expected helper to initialize out_err, got calls: {called_functions:?}" + ); } - #[cfg(windows)] #[test] - fn test_qir_to_qis_mz_leaked_windows_smoke() { - let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll"); - let output_bc = qir_to_qis(&bc_bytes, 0, "native", None) - .expect("mz_leaked fixture should compile successfully on Windows"); - - let ctx = Context::create(); - let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis") - .expect("Compiled QIS bitcode should parse on Windows"); - assert!(module.get_function("qmain").is_some()); + fn test_dynamic_feature_fixtures_translate_to_verifiable_qis() { + let (opt_level, target) = conservative_translation_settings(); + for fixture in DYNAMIC_FEATURE_FIXTURES { + let ll_text = std::fs::read_to_string(fixture).expect("Failed to read fixture"); + let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert fixture to bitcode"); + validate_qir(&input_bc, None).expect("Dynamic fixture should validate"); + let output_bc = qir_to_qis(&input_bc, opt_level, target, None) + .expect("Dynamic fixture should translate"); + verify_bitcode_module(&output_bc, fixture) + .expect("Dynamic fixture should remain LLVM-verifiable"); + } } - #[cfg(feature = "wasm")] proptest! { #[test] - fn prop_get_wasm_functions_round_trips_exact_exports( - exports in proptest::collection::btree_map("[A-Za-z_][A-Za-z0-9_]{0,8}", 0u32..32u32, 0..8) + fn prop_dynamic_result_array_record_output_preserves_label( + label in "[A-Za-z0-9_]{1,8}", ) { - let exports_vec = exports - .iter() - .map(|(name, index)| (name.clone(), *index)) - .collect::>(); - let wasm = build_wasm_exports(&exports_vec); - let parsed = crate::get_wasm_functions(Some(&wasm)) - .map_err(|err| TestCaseError::fail(format!("get_wasm_functions failed unexpectedly: {err}")))?; - - prop_assert_eq!(parsed.len(), exports.len()); - for (name, index) in exports { - prop_assert_eq!(parsed.get(&name), Some(&u64::from(index))); - } - } - } + let label_len = label + .len() + .checked_add(1) + .expect("label length bound should leave room for null terminator"); + let ll_text = format!( + r#" +@0 = internal constant [{label_len} x i8] c"{label}\00" - proptest! { - #[cfg(not(windows))] - #[test] - fn prop_qir_ll_to_bc_rejects_malformed_ir(suffix in "\\PC{0,128}") { - let ll_text = format!("this is not valid llvm ir\n{suffix}"); - prop_assert!(qir_ll_to_bc(&ll_text).is_err()); - } +define i64 @Entry_Point_Name() #0 {{ +entry: + %results = alloca [2 x ptr], align 8 + call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null) + %r0_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 0 + %r0 = load ptr, ptr %r0_ptr, align 8 + %r1_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 1 + %r1 = load ptr, ptr %r1_ptr, align 8 + call void @__quantum__qis__mz__body(ptr null, ptr %r0) + call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %r1) + call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0) + call void @__quantum__rt__result_array_release(i64 2, ptr %results) + ret i64 0 +}} - #[cfg(not(windows))] - #[test] - fn prop_validate_qir_rejects_malformed_bitcode(tail in proptest::collection::vec(any::(), 0..256)) { - let mut bytes = b"NOTQIR".to_vec(); - bytes.extend(tail); - prop_assert!(validate_qir(&bytes, None).is_err()); - } +declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr) +declare void @__quantum__rt__result_array_release(i64, ptr) +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 - #[cfg(not(windows))] - #[test] - fn prop_qir_to_qis_rejects_malformed_bitcode(tail in proptest::collection::vec(any::(), 0..256)) { - let mut bytes = b"NOTQIR".to_vec(); - bytes.extend(tail); - let (opt_level, target) = conservative_translation_settings(); - prop_assert!(qir_to_qis(&bytes, opt_level, target, None).is_err()); - } +attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" }} +attributes #1 = {{ "irreversible" }} - #[test] - fn prop_valid_fixtures_translate_to_verifiable_qis(fixture in proptest::sample::select(PROPERTY_FIXTURES)) { - let input_bc = load_fixture_bitcode(fixture); - prop_assert!(validate_qir(&input_bc, None).is_ok()); +!llvm.module.flags = !{{!0, !1, !2, !3, !4}} +!0 = !{{i32 1, !"qir_major_version", i32 2}} +!1 = !{{i32 7, !"qir_minor_version", i32 0}} +!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}} +!3 = !{{i32 1, !"dynamic_result_management", i1 true}} +!4 = !{{i32 1, !"arrays", i1 true}} +"# + ); - let (opt_level, target) = conservative_translation_settings(); - let output_bc = qir_to_qis(&input_bc, opt_level, target, None) - .map_err(|err| TestCaseError::fail(format!("translation failed for {fixture}: {err}")))?; + let bc_bytes = qir_ll_to_bc(&ll_text) + .map_err(|err| TestCaseError::fail(format!("Failed to lower inline IR: {err}")))?; + validate_qir(&bc_bytes, None) + .map_err(|err| TestCaseError::fail(format!("Fixture should validate: {err}")))?; + let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None) + .map_err(|err| TestCaseError::fail(format!("Fixture should compile: {err}")))?; + let ctx = Context::create(); + let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module") + .map_err(|err| TestCaseError::fail(format!("Compiled module should parse: {err}")))?; + let expected_label = format!("USER:RESULT_ARRAY:{label}"); + let labels = module + .get_globals() + .filter_map(|global| get_string_label(global).ok()) + .collect::>(); - verify_bitcode_module(&output_bc, "property_qis_module") - .map_err(|err| TestCaseError::fail(format!("verification failed for {fixture}: {err}")))?; + prop_assert!( + labels.iter().any(|item| item.contains(&expected_label)), + "expected label payload {expected_label}, got globals: {labels:?}" + ); } #[test] - fn prop_invalid_targets_fail_fast(target in "[a-z0-9_-]{1,12}") { - prop_assume!(target != "native"); - prop_assume!(target != "aarch64"); - prop_assume!(target != "x86-64"); + fn prop_dynamic_array_allocate_requires_matching_fixed_backing( + backing_len in 1u32..4u32, + requested_len in 1u32..4u32, + is_result_array in any::(), + ) { + let (rt_decl, call, attrs, flags) = if is_result_array { + ( + "declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)", + format!( + " %results = alloca [{backing_len} x ptr], align 8\n call void @__quantum__rt__result_array_allocate(i64 {requested_len}, ptr %results, ptr null)" + ), + r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1""#, + "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n!3 = !{i32 1, !\"dynamic_result_management\", i1 true}\n!4 = !{i32 1, !\"arrays\", i1 true}", + ) + } else { + ( + "declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)", + format!( + " %qubits = alloca [{backing_len} x ptr], align 8\n call void @__quantum__rt__qubit_array_allocate(i64 {requested_len}, ptr %qubits, ptr null)" + ), + r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1""#, + "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 true}\n!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n!4 = !{i32 1, !\"arrays\", i1 true}", + ) + }; - let input_bc = load_fixture_bitcode("tests/data/base.ll"); - prop_assert!(qir_to_qis(&input_bc, 0, &target, None).is_err()); - } + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: +{call} + ret i64 0 +}} - #[test] - fn prop_missing_required_attrs_fail_validation( - missing_idx in 0usize..4usize - ) { - let missing_attr = [ - "qir_profiles", - "output_labeling_schema", - "required_num_qubits", - "required_num_results", - ][missing_idx]; - let ll_text = minimal_qir_missing_attr(missing_attr); - let bc = qir_ll_to_bc(&ll_text) - .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; - let err = validate_qir(&bc, None) - .expect_err("validation should reject missing required attributes"); - let expected = format!("Missing required attribute: `{missing_attr}`"); - prop_assert!(err.contains(&expected)); - } +{rt_decl} - #[test] - fn prop_qir_major_versions_accept_only_one_or_two(major in 0u32..5u32) { - let ll_text = minimal_qir_with_body("1", "1", &major.to_string(), "", ""); - let bc = qir_ll_to_bc(&ll_text) - .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; - let result = validate_qir(&bc, None); - if matches!(major, 1 | 2) { - prop_assert!(result.is_ok()); - } else { - let err = result.expect_err("invalid major versions must fail"); - prop_assert!(err.contains("Unsupported qir_major_version")); - } - } +attributes #0 = {{ {attrs} }} - #[test] - fn prop_duplicate_qir_major_flags_pass_if_any_match( - valid_first in any::(), - valid_second in any::(), - ) { - let first_major = if valid_first { "1" } else { "99" }; - let second_major = if valid_second { "2" } else { "100" }; - let ll_text = minimal_qir_with_duplicate_major_flags(first_major, second_major); +!llvm.module.flags = !{{!0, !1, !2, !3, !4}} +!0 = !{{i32 1, !"qir_major_version", i32 2}} +!1 = !{{i32 7, !"qir_minor_version", i32 0}} +{flags} +"# + ); let bc = qir_ll_to_bc(&ll_text) .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; let result = validate_qir(&bc, None); - if valid_first || valid_second { + if backing_len == requested_len { prop_assert!(result.is_ok()); } else { - let err = result.expect_err("all-invalid duplicate major flags must fail"); - prop_assert!(err.contains("Unsupported qir_major_version")); + let err = result.expect_err("mismatched fixed-size backing should fail"); + prop_assert!(err.contains("requires a fixed-size backing array")); + prop_assert!(err.contains("requested length")); } } #[test] - fn prop_duplicate_dynamic_qubit_flags_pass_if_any_match( - valid_first in any::(), - valid_second in any::(), + fn prop_dynamic_array_runtime_calls_require_both_arrays_and_matching_dynamic_flag( + arrays_enabled in any::(), + dynamic_enabled in any::(), + is_result_array in any::(), ) { - let first_flag = if valid_first { "false" } else { "true" }; - let second_flag = if valid_second { "false" } else { "true" }; - let ll_text = minimal_qir_with_duplicate_dynamic_flags(first_flag, second_flag); - let bc = qir_ll_to_bc(&ll_text) - .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; - let result = validate_qir(&bc, None); - if valid_first || valid_second { - prop_assert!(result.is_ok()); + let (call, rt_decl, attrs, flags, expected_error) = if is_result_array { + ( + " %results = alloca [2 x ptr], align 8\n call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null)", + "declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)", + r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1""#.to_string(), + format!( + "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}\n!3 = !{{i32 1, !\"dynamic_result_management\", i1 {dynamic_enabled}}}\n!4 = !{{i32 1, !\"arrays\", i1 {arrays_enabled}}}" + ), + "__quantum__rt__result_array_allocate requires both `arrays=true` and `dynamic_result_management=true`", + ) } else { - let err = result.expect_err("all-invalid duplicate dynamic flags must fail"); - prop_assert!(err.contains("dynamic_qubit_management")); - } - } + ( + " %qubits = alloca [2 x ptr], align 8\n call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr null)", + "declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)", + r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1""#.to_string(), + format!( + "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 {dynamic_enabled}}}\n!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}\n!4 = !{{i32 1, !\"arrays\", i1 {arrays_enabled}}}" + ), + "__quantum__rt__qubit_array_allocate requires both `arrays=true` and `dynamic_qubit_management=true`", + ) + }; - #[test] - fn prop_barrier_validation_tracks_required_qubits( - required_num_qubits in 1u32..5u32, - barrier_arity in 1u32..5u32, - ) { - let barrier_name = format!("__quantum__qis__barrier{barrier_arity}__body"); - let barrier_args = (0..barrier_arity) - .map(|idx| format!("%Qubit* %q{idx}")) - .collect::>() - .join(", "); - let extra_decl = format!( - "declare void @{barrier_name}({})", - std::iter::repeat_n("%Qubit*", usize::try_from(barrier_arity).unwrap_or(0)) - .collect::>() - .join(", ") - ); - let body = (0..barrier_arity) - .map(|idx| { - let one_based_idx = idx.saturating_add(1); - format!(" %q{idx} = inttoptr i64 {one_based_idx} to %Qubit*") - }) - .chain(std::iter::once(format!(" call void @{barrier_name}({barrier_args})"))) - .collect::>() - .join("\n"); - let ll_text = minimal_qir_with_body( - &required_num_qubits.to_string(), - "1", - "1", - &extra_decl, - &body, + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: +{call} + ret i64 0 +}} + +{rt_decl} + +attributes #0 = {{ {attrs} }} + +!llvm.module.flags = !{{!0, !1, !2, !3, !4}} +!0 = !{{i32 1, !"qir_major_version", i32 2}} +!1 = !{{i32 7, !"qir_minor_version", i32 0}} +{flags} +"# ); let bc = qir_ll_to_bc(&ll_text) .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?; let result = validate_qir(&bc, None); - if barrier_arity <= required_num_qubits { + if arrays_enabled && dynamic_enabled { prop_assert!(result.is_ok()); } else { - let err = result.expect_err("oversized barrier arity must fail"); - prop_assert!(err.contains("Barrier arity")); + let err = result.expect_err("missing capability flag combination should fail"); + prop_assert!(err.contains(expected_error)); } } - - #[cfg(not(windows))] - #[test] - fn prop_get_entry_attributes_rejects_malformed_bitcode( - tail in proptest::collection::vec(any::(), 0..256) - ) { - let mut bytes = b"NOTQIR".to_vec(); - bytes.extend(tail); - prop_assert!(get_entry_attributes(&bytes).is_err()); - } - } - - #[test] - fn test_zero_qubits_fail_validation() { - let ll_text = minimal_qir_with_body("0", "1", "1", "", ""); - let bc = qir_ll_to_bc(&ll_text).expect("inline IR should parse"); - let err = validate_qir(&bc, None).expect_err("validation should reject zero qubits"); - assert!(err.contains("Entry function must have at least one qubit")); } } diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..57bdf9c --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for Python-side regressions.""" diff --git a/tests/data/dynamic_qubit_alloc.ll b/tests/data/dynamic_qubit_alloc.ll new file mode 100644 index 0000000..a241c4d --- /dev/null +++ b/tests/data/dynamic_qubit_alloc.ll @@ -0,0 +1,27 @@ +@0 = internal constant [3 x i8] c"q0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %q = call ptr @__quantum__rt__qubit_allocate(ptr null) + call void @__quantum__qis__h__body(ptr %q) + call void @__quantum__qis__mz__body(ptr %q, ptr null) + call void @__quantum__rt__result_record_output(ptr null, ptr @0) + call void @__quantum__rt__qubit_release(ptr %q) + ret i64 0 +} + +declare ptr @__quantum__rt__qubit_allocate(ptr) +declare void @__quantum__rt__qubit_release(ptr) +declare void @__quantum__qis__h__body(ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} diff --git a/tests/data/dynamic_qubit_alloc_checked.ll b/tests/data/dynamic_qubit_alloc_checked.ll new file mode 100644 index 0000000..8ad73e4 --- /dev/null +++ b/tests/data/dynamic_qubit_alloc_checked.ll @@ -0,0 +1,39 @@ +@0 = internal constant [8 x i8] c"qcheck0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %err = alloca i1, align 1 + store i1 false, ptr %err, align 1 + %q = call ptr @__quantum__rt__qubit_allocate(ptr %err) + %failed = load i1, ptr %err, align 1 + br i1 %failed, label %alloc_failed, label %alloc_ok + +alloc_ok: + call void @__quantum__qis__x__body(ptr %q) + call void @__quantum__qis__mz__body(ptr %q, ptr null) + call void @__quantum__rt__result_record_output(ptr null, ptr @0) + call void @__quantum__rt__qubit_release(ptr %q) + br label %done + +alloc_failed: + br label %done + +done: + ret i64 0 +} + +declare ptr @__quantum__rt__qubit_allocate(ptr) +declare void @__quantum__rt__qubit_release(ptr) +declare void @__quantum__qis__x__body(ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} diff --git a/tests/data/dynamic_qubit_array_checked.ll b/tests/data/dynamic_qubit_array_checked.ll new file mode 100644 index 0000000..b519645 --- /dev/null +++ b/tests/data/dynamic_qubit_array_checked.ll @@ -0,0 +1,49 @@ +@0 = internal constant [8 x i8] c"acheck0\00" +@1 = internal constant [8 x i8] c"acheck1\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [2 x ptr], align 8 + %err = alloca i1, align 1 + store i1 false, ptr %err, align 1 + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr %err) + %failed = load i1, ptr %err, align 1 + br i1 %failed, label %alloc_failed, label %alloc_ok + +alloc_ok: + %q0_ptr = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 0 + %q0 = load ptr, ptr %q0_ptr, align 8 + %q1_ptr = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 1 + %q1 = load ptr, ptr %q1_ptr, align 8 + call void @__quantum__qis__h__body(ptr %q0) + call void @__quantum__qis__cnot__body(ptr %q0, ptr %q1) + call void @__quantum__qis__mz__body(ptr %q0, ptr null) + call void @__quantum__qis__mz__body(ptr %q1, ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__result_record_output(ptr null, ptr @0) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr @1) + call void @__quantum__rt__qubit_array_release(i64 2, ptr %qubits) + br label %done + +alloc_failed: + br label %done + +done: + ret i64 0 +} + +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) +declare void @__quantum__rt__qubit_array_release(i64, ptr) +declare void @__quantum__qis__h__body(ptr) +declare void @__quantum__qis__cnot__body(ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} diff --git a/tests/data/dynamic_qubit_array_ssa.ll b/tests/data/dynamic_qubit_array_ssa.ll new file mode 100644 index 0000000..02857e7 --- /dev/null +++ b/tests/data/dynamic_qubit_array_ssa.ll @@ -0,0 +1,40 @@ +@0 = internal constant [3 x i8] c"m0\00" +@1 = internal constant [3 x i8] c"m1\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %qubits = alloca [2 x ptr], align 8 + call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr null) + %loaded = load [2 x ptr], ptr %qubits, align 8 + %q1 = extractvalue [2 x ptr] %loaded, 1 + %tmp = insertvalue [2 x ptr] poison, ptr %q1, 0 + %q0 = extractvalue [2 x ptr] %loaded, 0 + %swapped = insertvalue [2 x ptr] %tmp, ptr %q0, 1 + %first = extractvalue [2 x ptr] %swapped, 0 + %second = extractvalue [2 x ptr] %swapped, 1 + call void @__quantum__qis__h__body(ptr %first) + call void @__quantum__qis__cnot__body(ptr %first, ptr %second) + call void @__quantum__qis__mz__body(ptr %first, ptr null) + call void @__quantum__qis__mz__body(ptr %second, ptr inttoptr (i64 1 to ptr)) + call void @__quantum__rt__result_record_output(ptr null, ptr @0) + call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr @1) + call void @__quantum__rt__qubit_array_release(i64 2, ptr %qubits) + ret i64 0 +} + +declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr) +declare void @__quantum__rt__qubit_array_release(i64, ptr) +declare void @__quantum__qis__h__body(ptr) +declare void @__quantum__qis__cnot__body(ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} diff --git a/tests/data/dynamic_result_alloc.ll b/tests/data/dynamic_result_alloc.ll new file mode 100644 index 0000000..35c8b0d --- /dev/null +++ b/tests/data/dynamic_result_alloc.ll @@ -0,0 +1,27 @@ +@0 = internal constant [3 x i8] c"r0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %r = call ptr @__quantum__rt__result_allocate(ptr null) + call void @__quantum__qis__x__body(ptr null) + call void @__quantum__qis__mz__body(ptr null, ptr %r) + call void @__quantum__rt__result_record_output(ptr %r, ptr @0) + call void @__quantum__rt__result_release(ptr %r) + ret i64 0 +} + +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_release(ptr) +declare void @__quantum__qis__x__body(ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 +declare void @__quantum__rt__result_record_output(ptr, ptr) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 false} diff --git a/tests/data/dynamic_result_array.ll b/tests/data/dynamic_result_array.ll new file mode 100644 index 0000000..99c3f36 --- /dev/null +++ b/tests/data/dynamic_result_array.ll @@ -0,0 +1,31 @@ +@0 = internal constant [3 x i8] c"a0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [2 x ptr], align 8 + call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null) + %r0_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 0 + %r0 = load ptr, ptr %r0_ptr, align 8 + %r1_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 1 + %r1 = load ptr, ptr %r1_ptr, align 8 + call void @__quantum__qis__mz__body(ptr null, ptr %r0) + call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %r1) + call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0) + call void @__quantum__rt__result_array_release(i64 2, ptr %results) + ret i64 0 +} + +declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr) +declare void @__quantum__rt__result_array_release(i64, ptr) +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} diff --git a/tests/data/dynamic_result_mixed_array_output.ll b/tests/data/dynamic_result_mixed_array_output.ll new file mode 100644 index 0000000..b1e1881 --- /dev/null +++ b/tests/data/dynamic_result_mixed_array_output.ll @@ -0,0 +1,34 @@ +@0 = internal constant [5 x i8] c"mix0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [2 x ptr], align 8 + %r0 = call ptr @__quantum__rt__result_allocate(ptr null) + %r1 = call ptr @__quantum__rt__result_allocate(ptr null) + %tmp = insertvalue [2 x ptr] poison, ptr %r0, 0 + %packed = insertvalue [2 x ptr] %tmp, ptr %r1, 1 + store [2 x ptr] %packed, ptr %results, align 8 + %loaded = load [2 x ptr], ptr %results, align 8 + %s0 = extractvalue [2 x ptr] %loaded, 0 + %s1 = extractvalue [2 x ptr] %loaded, 1 + call void @__quantum__qis__mz__body(ptr null, ptr %s0) + call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %s1) + call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0) + call void @__quantum__rt__result_array_release(i64 2, ptr %results) + ret i64 0 +} + +declare ptr @__quantum__rt__result_allocate(ptr) +declare void @__quantum__rt__result_array_release(i64, ptr) +declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr) +declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1 + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" } +attributes #1 = { "irreversible" } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} diff --git a/tests/snaps/ArithOps_switch.ll.snap b/tests/snaps/ArithOps_switch.ll.snap index 93287df..5f38c86 100644 --- a/tests/snaps/ArithOps_switch.ll.snap +++ b/tests/snaps/ArithOps_switch.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -176,16 +176,16 @@ block_10: ; preds = %block_9, %block_8 %var_54 = phi i64 [ %var_50, %block_8 ], [ %var_32, %block_9 ] %var_53 = phi i64 [ %var_49, %block_8 ], [ %var_31, %block_9 ] %var_52 = phi i64 [ %var_48, %block_8 ], [ %var_30, %block_9 ] - %qbit.i = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i) - %qbit.i45 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i45) - %qbit.i46 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i46) - %qbit.i47 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i47) - %qbit.i48 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i48) + %qbit13 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit13) + %qbit14 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit14) + %qbit15 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit15) + %qbit16 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit16) + %qbit17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit17) %current_shot = tail call i64 @get_current_shot() tail call void @print_int(ptr nonnull @res_s0.1, i64 11, i64 %current_shot) tail call void @random_seed(i64 42) diff --git a/tests/snaps/adaptive.ll.snap b/tests/snaps/adaptive.ll.snap index 0c1bda0..52b5b08 100644 --- a/tests/snaps/adaptive.ll.snap +++ b/tests/snaps/adaptive.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -144,8 +144,8 @@ qir_qis.init_qubit.exit47: ; preds = %qir_qis.init_qubit. tail call void @___rz(i64 %qbit.i4.i72, double 0x400921FB54442D18) %qbit = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 %meas = tail call i64 @___lazy_measure(i64 %qbit) - %qbit.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i) + %qbit1 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit1) %bool = tail call i1 @___read_future_bool(i64 %meas) tail call void @___dec_future_refcount(i64 %meas) br i1 %bool, label %then__1, label %continue__1 @@ -158,69 +158,69 @@ then__1: ; preds = %qir_qis.init_qubit. continue__1: ; preds = %then__1, %qir_qis.init_qubit.exit47 %qbit2 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 %meas3 = tail call i64 @___lazy_measure(i64 %qbit2) - %qbit.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i74) + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit4) %bool5 = tail call i1 @___read_future_bool(i64 %meas3) tail call void @___dec_future_refcount(i64 %meas3) br i1 %bool5, label %then__2, label %continue__2 then__2: ; preds = %continue__1 - %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i75, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i74, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__2 continue__2: ; preds = %then__2, %continue__1 - %qbit.i.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i.i76, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) - %qbit.i8.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - %qbit.i11.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rzz(i64 %qbit.i8.i77, i64 %qbit.i11.i78, double 0x3FF921FB54442D18) - %qbit.i14.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i14.i79, double 0xBFF921FB54442D18) - %qbit.i17.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i17.i80, double 0x3FF921FB54442D18, double 0x400921FB54442D18) - %qbit.i20.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rz(i64 %qbit.i20.i81, double 0xBFF921FB54442D18) - %qbit.i.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i82, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) - %qbit.i4.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i4.i83, double 0x400921FB54442D18) + %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i.i75, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i8.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit.i11.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rzz(i64 %qbit.i8.i76, i64 %qbit.i11.i77, double 0x3FF921FB54442D18) + %qbit.i14.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i14.i78, double 0xBFF921FB54442D18) + %qbit.i17.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i17.i79, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + %qbit.i20.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rz(i64 %qbit.i20.i80, double 0xBFF921FB54442D18) + %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i81, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + %qbit.i4.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i4.i82, double 0x400921FB54442D18) %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %meas7 = tail call i64 @___lazy_measure(i64 %qbit6) - %qbit.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i84) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit8) %bool9 = tail call i1 @___read_future_bool(i64 %meas7) tail call void @___dec_future_refcount(i64 %meas7) br i1 %bool9, label %then__3, label %continue__3 then__3: ; preds = %continue__2 - %qbit.i.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rz(i64 %qbit.i.i85, double 0x400921FB54442D18) + %qbit.i.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rz(i64 %qbit.i.i83, double 0x400921FB54442D18) br label %continue__3 continue__3: ; preds = %then__3, %continue__2 %qbit10 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 %meas11 = tail call i64 @___lazy_measure(i64 %qbit10) - %qbit.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i86) + %qbit12 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit12) %bool13 = tail call i1 @___read_future_bool(i64 %meas11) tail call void @___dec_future_refcount(i64 %meas11) br i1 %bool13, label %then__4, label %continue__4 then__4: ; preds = %continue__3 - %qbit.i.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rxy(i64 %qbit.i.i87, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rxy(i64 %qbit.i.i84, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__4 continue__4: ; preds = %then__4, %continue__3 %qbit14 = load i64, ptr @qis_qs, align 16 %meas15 = tail call i64 @___lazy_measure(i64 %qbit14) - %qbit.i88 = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i88) + %qbit16 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit16) %qbit17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 %meas18 = tail call i64 @___lazy_measure(i64 %qbit17) - %qbit.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___reset(i64 %qbit.i89) + %qbit19 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___reset(i64 %qbit19) %bool20 = tail call i1 @___read_future_bool(i64 %meas15) tail call void @___dec_future_refcount(i64 %meas15) tail call void @print_bool(ptr nonnull @res_0_t0, i64 16, i1 %bool20) diff --git a/tests/snaps/adaptive_cond_loop.ll.snap b/tests/snaps/adaptive_cond_loop.ll.snap index cd9a550..85c664f 100644 --- a/tests/snaps/adaptive_cond_loop.ll.snap +++ b/tests/snaps/adaptive_cond_loop.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -151,8 +151,8 @@ loop: ; preds = %loop, %qir_qis.init tail call void @___rz(i64 %qbit.i4.i78, double 0x400921FB54442D18) %qbit = load i64, ptr @qis_qs, align 16 %meas = tail call i64 @___lazy_measure(i64 %qbit) - %qbit.i = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i) + %qbit1 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit1) %bool = tail call i1 @___read_future_bool(i64 %meas) tail call void @___dec_future_refcount(i64 %meas) br i1 %bool, label %cont, label %loop @@ -160,83 +160,83 @@ loop: ; preds = %loop, %qir_qis.init cont: ; preds = %loop %qbit2 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 %meas3 = tail call i64 @___lazy_measure(i64 %qbit2) - %qbit.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i79) + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit4) %bool5 = tail call i1 @___read_future_bool(i64 %meas3) tail call void @___dec_future_refcount(i64 %meas3) br i1 %bool5, label %then__1, label %continue__1 then__1: ; preds = %cont - %qbit.i.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i.i80, double 0x400921FB54442D18) + %qbit.i.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i.i79, double 0x400921FB54442D18) br label %continue__1 continue__1: ; preds = %then__1, %cont %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 %meas7 = tail call i64 @___lazy_measure(i64 %qbit6) - %qbit.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i81) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit8) %bool9 = tail call i1 @___read_future_bool(i64 %meas7) tail call void @___dec_future_refcount(i64 %meas7) br i1 %bool9, label %then__2, label %continue__2 then__2: ; preds = %continue__1 - %qbit.i.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i82, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i80, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__2 continue__2: ; preds = %then__2, %continue__1 - %qbit.i.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i.i83, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) - %qbit.i8.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - %qbit.i11.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rzz(i64 %qbit.i8.i84, i64 %qbit.i11.i85, double 0x3FF921FB54442D18) - %qbit.i14.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i14.i86, double 0xBFF921FB54442D18) - %qbit.i17.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i17.i87, double 0x3FF921FB54442D18, double 0x400921FB54442D18) - %qbit.i20.i88 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rz(i64 %qbit.i20.i88, double 0xBFF921FB54442D18) - %qbit.i.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i89, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) - %qbit.i4.i90 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i4.i90, double 0x400921FB54442D18) + %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i.i81, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i8.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit.i11.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rzz(i64 %qbit.i8.i82, i64 %qbit.i11.i83, double 0x3FF921FB54442D18) + %qbit.i14.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i14.i84, double 0xBFF921FB54442D18) + %qbit.i17.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i17.i85, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + %qbit.i20.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rz(i64 %qbit.i20.i86, double 0xBFF921FB54442D18) + %qbit.i.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i87, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + %qbit.i4.i88 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i4.i88, double 0x400921FB54442D18) %qbit10 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %meas11 = tail call i64 @___lazy_measure(i64 %qbit10) - %qbit.i91 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i91) + %qbit12 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit12) %bool13 = tail call i1 @___read_future_bool(i64 %meas11) tail call void @___dec_future_refcount(i64 %meas11) br i1 %bool13, label %then__3, label %continue__3 then__3: ; preds = %continue__2 - %qbit.i.i92 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rz(i64 %qbit.i.i92, double 0x400921FB54442D18) + %qbit.i.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rz(i64 %qbit.i.i89, double 0x400921FB54442D18) br label %continue__3 continue__3: ; preds = %then__3, %continue__2 %qbit14 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 %meas15 = tail call i64 @___lazy_measure(i64 %qbit14) - %qbit.i93 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i93) + %qbit16 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit16) %bool17 = tail call i1 @___read_future_bool(i64 %meas15) tail call void @___dec_future_refcount(i64 %meas15) br i1 %bool17, label %then__4, label %continue__4 then__4: ; preds = %continue__3 - %qbit.i.i94 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rxy(i64 %qbit.i.i94, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i90 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rxy(i64 %qbit.i.i90, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__4 continue__4: ; preds = %then__4, %continue__3 %qbit18 = load i64, ptr @qis_qs, align 16 %meas19 = tail call i64 @___lazy_measure(i64 %qbit18) - %qbit.i95 = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i95) + %qbit20 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit20) %qbit21 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 %meas22 = tail call i64 @___lazy_measure(i64 %qbit21) - %qbit.i96 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___reset(i64 %qbit.i96) + %qbit23 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___reset(i64 %qbit23) %bool24 = tail call i1 @___read_future_bool(i64 %meas19) tail call void @___dec_future_refcount(i64 %meas19) tail call void @print_bool(ptr nonnull @res_0_t0, i64 16, i1 %bool24) diff --git a/tests/snaps/adaptive_ir_fns.ll.snap b/tests/snaps/adaptive_ir_fns.ll.snap index 042f0b4..17339ca 100644 --- a/tests/snaps/adaptive_ir_fns.ll.snap +++ b/tests/snaps/adaptive_ir_fns.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -194,8 +194,8 @@ qir_qis.init_qubit.exit47: ; preds = %qir_qis.init_qubit. tail call void @___rz(i64 %qbit.i4.i72, double 0x400921FB54442D18) %qbit = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 %meas = tail call i64 @___lazy_measure(i64 %qbit) - %qbit.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i) + %qbit1 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit1) %bool = tail call i1 @___read_future_bool(i64 %meas) tail call void @___dec_future_refcount(i64 %meas) br i1 %bool, label %then__1, label %continue__1 @@ -208,69 +208,69 @@ then__1: ; preds = %qir_qis.init_qubit. continue__1: ; preds = %then__1, %qir_qis.init_qubit.exit47 %qbit2 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 %meas3 = tail call i64 @___lazy_measure(i64 %qbit2) - %qbit.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i74) + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit4) %bool5 = tail call i1 @___read_future_bool(i64 %meas3) tail call void @___dec_future_refcount(i64 %meas3) br i1 %bool5, label %then__2, label %continue__2 then__2: ; preds = %continue__1 - %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i75, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i74, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__2 continue__2: ; preds = %then__2, %continue__1 - %qbit.i.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i.i76, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) - %qbit.i8.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - %qbit.i11.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rzz(i64 %qbit.i8.i77, i64 %qbit.i11.i78, double 0x3FF921FB54442D18) - %qbit.i14.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i14.i79, double 0xBFF921FB54442D18) - %qbit.i17.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i17.i80, double 0x3FF921FB54442D18, double 0x400921FB54442D18) - %qbit.i20.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rz(i64 %qbit.i20.i81, double 0xBFF921FB54442D18) - %qbit.i.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i82, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) - %qbit.i4.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i4.i83, double 0x400921FB54442D18) + %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i.i75, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i8.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit.i11.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rzz(i64 %qbit.i8.i76, i64 %qbit.i11.i77, double 0x3FF921FB54442D18) + %qbit.i14.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i14.i78, double 0xBFF921FB54442D18) + %qbit.i17.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i17.i79, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + %qbit.i20.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rz(i64 %qbit.i20.i80, double 0xBFF921FB54442D18) + %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i81, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + %qbit.i4.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i4.i82, double 0x400921FB54442D18) %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %meas7 = tail call i64 @___lazy_measure(i64 %qbit6) - %qbit.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i84) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit8) %bool9 = tail call i1 @___read_future_bool(i64 %meas7) tail call void @___dec_future_refcount(i64 %meas7) br i1 %bool9, label %then__3, label %continue__3 then__3: ; preds = %continue__2 - %qbit.i.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rz(i64 %qbit.i.i85, double 0x400921FB54442D18) + %qbit.i.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rz(i64 %qbit.i.i83, double 0x400921FB54442D18) br label %continue__3 continue__3: ; preds = %then__3, %continue__2 %qbit10 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 %meas11 = tail call i64 @___lazy_measure(i64 %qbit10) - %qbit.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i86) + %qbit12 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit12) %bool13 = tail call i1 @___read_future_bool(i64 %meas11) tail call void @___dec_future_refcount(i64 %meas11) br i1 %bool13, label %then__4, label %continue__4 then__4: ; preds = %continue__3 - %qbit.i.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rxy(i64 %qbit.i.i87, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rxy(i64 %qbit.i.i84, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__4 continue__4: ; preds = %then__4, %continue__3 %qbit14 = load i64, ptr @qis_qs, align 16 %meas15 = tail call i64 @___lazy_measure(i64 %qbit14) - %qbit.i88 = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i88) + %qbit16 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit16) %qbit17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 %meas18 = tail call i64 @___lazy_measure(i64 %qbit17) - %qbit.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___reset(i64 %qbit.i89) + %qbit19 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___reset(i64 %qbit19) %bool20 = tail call i1 @___read_future_bool(i64 %meas15) tail call void @___dec_future_refcount(i64 %meas15) tail call void @print_bool(ptr nonnull @res_0_t0, i64 16, i1 %bool20) diff --git a/tests/snaps/adaptive_iter.ll.snap b/tests/snaps/adaptive_iter.ll.snap index 8e7c081..2e026dd 100644 --- a/tests/snaps/adaptive_iter.ll.snap +++ b/tests/snaps/adaptive_iter.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -188,8 +188,8 @@ qir_qis.init_qubit.exit47: ; preds = %qir_qis.init_qubit. tail call void @___rz(i64 %qbit.i4.i78, double 0x400921FB54442D18) %qbit = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 %meas = tail call i64 @___lazy_measure(i64 %qbit) - %qbit.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i) + %qbit1 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit1) %bool = tail call i1 @___read_future_bool(i64 %meas) tail call void @___dec_future_refcount(i64 %meas) br i1 %bool, label %then__1, label %continue__1 @@ -202,69 +202,69 @@ then__1: ; preds = %qir_qis.init_qubit. continue__1: ; preds = %then__1, %qir_qis.init_qubit.exit47 %qbit2 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 %meas3 = tail call i64 @___lazy_measure(i64 %qbit2) - %qbit.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i80) + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit4) %bool5 = tail call i1 @___read_future_bool(i64 %meas3) tail call void @___dec_future_refcount(i64 %meas3) br i1 %bool5, label %then__2, label %continue__2 then__2: ; preds = %continue__1 - %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i81, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i80, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__2 continue__2: ; preds = %then__2, %continue__1 - %qbit.i.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i.i82, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) - %qbit.i8.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - %qbit.i11.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rzz(i64 %qbit.i8.i83, i64 %qbit.i11.i84, double 0x3FF921FB54442D18) - %qbit.i14.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i14.i85, double 0xBFF921FB54442D18) - %qbit.i17.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i17.i86, double 0x3FF921FB54442D18, double 0x400921FB54442D18) - %qbit.i20.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rz(i64 %qbit.i20.i87, double 0xBFF921FB54442D18) - %qbit.i.i88 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i88, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) - %qbit.i4.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i4.i89, double 0x400921FB54442D18) + %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i.i81, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i8.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit.i11.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rzz(i64 %qbit.i8.i82, i64 %qbit.i11.i83, double 0x3FF921FB54442D18) + %qbit.i14.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i14.i84, double 0xBFF921FB54442D18) + %qbit.i17.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i17.i85, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + %qbit.i20.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rz(i64 %qbit.i20.i86, double 0xBFF921FB54442D18) + %qbit.i.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i87, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + %qbit.i4.i88 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i4.i88, double 0x400921FB54442D18) %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %meas7 = tail call i64 @___lazy_measure(i64 %qbit6) - %qbit.i90 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i90) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit8) %bool9 = tail call i1 @___read_future_bool(i64 %meas7) tail call void @___dec_future_refcount(i64 %meas7) br i1 %bool9, label %then__3, label %continue__3 then__3: ; preds = %continue__2 - %qbit.i.i91 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rz(i64 %qbit.i.i91, double 0x400921FB54442D18) + %qbit.i.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rz(i64 %qbit.i.i89, double 0x400921FB54442D18) br label %continue__3 continue__3: ; preds = %then__3, %continue__2 %qbit10 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 %meas11 = tail call i64 @___lazy_measure(i64 %qbit10) - %qbit.i92 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i92) + %qbit12 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit12) %bool13 = tail call i1 @___read_future_bool(i64 %meas11) tail call void @___dec_future_refcount(i64 %meas11) br i1 %bool13, label %then__4, label %continue__4 then__4: ; preds = %continue__3 - %qbit.i.i93 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rxy(i64 %qbit.i.i93, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i90 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rxy(i64 %qbit.i.i90, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__4 continue__4: ; preds = %then__4, %continue__3 %qbit14 = load i64, ptr @qis_qs, align 16 %meas15 = tail call i64 @___lazy_measure(i64 %qbit14) - %qbit.i94 = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i94) + %qbit16 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit16) %qbit17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 %meas18 = tail call i64 @___lazy_measure(i64 %qbit17) - %qbit.i95 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___reset(i64 %qbit.i95) + %qbit19 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___reset(i64 %qbit19) %bool20 = tail call i1 @___read_future_bool(i64 %meas15) tail call void @___dec_future_refcount(i64 %meas15) tail call void @print_bool(ptr nonnull @res_0_t0, i64 16, i1 %bool20) diff --git a/tests/snaps/adaptive_iter_fn.ll.snap b/tests/snaps/adaptive_iter_fn.ll.snap index 540a4a8..9106bb9 100644 --- a/tests/snaps/adaptive_iter_fn.ll.snap +++ b/tests/snaps/adaptive_iter_fn.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -145,8 +145,8 @@ qir_qis.init_qubit.exit47: ; preds = %qir_qis.init_qubit. tail call void @___rz(i64 %qbit.i4.i72, double 0x400921FB54442D18) %qbit = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 %meas = tail call i64 @___lazy_measure(i64 %qbit) - %qbit.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - tail call void @___reset(i64 %qbit.i) + %qbit1 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + tail call void @___reset(i64 %qbit1) %bool = tail call i1 @___read_future_bool(i64 %meas) tail call void @___dec_future_refcount(i64 %meas) br i1 %bool, label %then__1, label %continue__1 @@ -159,69 +159,69 @@ then__1: ; preds = %qir_qis.init_qubit. continue__1: ; preds = %then__1, %qir_qis.init_qubit.exit47 %qbit2 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 %meas3 = tail call i64 @___lazy_measure(i64 %qbit2) - %qbit.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 - tail call void @___reset(i64 %qbit.i74) + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 + tail call void @___reset(i64 %qbit4) %bool5 = tail call i1 @___read_future_bool(i64 %meas3) tail call void @___dec_future_refcount(i64 %meas3) br i1 %bool5, label %then__2, label %continue__2 then__2: ; preds = %continue__1 - %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i75, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i74 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i74, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__2 continue__2: ; preds = %then__2, %continue__1 - %qbit.i.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i.i76, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) - %qbit.i8.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - %qbit.i11.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rzz(i64 %qbit.i8.i77, i64 %qbit.i11.i78, double 0x3FF921FB54442D18) - %qbit.i14.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i14.i79, double 0xBFF921FB54442D18) - %qbit.i17.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rxy(i64 %qbit.i17.i80, double 0x3FF921FB54442D18, double 0x400921FB54442D18) - %qbit.i20.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___rz(i64 %qbit.i20.i81, double 0xBFF921FB54442D18) - %qbit.i.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rxy(i64 %qbit.i.i82, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) - %qbit.i4.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___rz(i64 %qbit.i4.i83, double 0x400921FB54442D18) + %qbit.i.i75 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i.i75, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i8.i76 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit.i11.i77 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rzz(i64 %qbit.i8.i76, i64 %qbit.i11.i77, double 0x3FF921FB54442D18) + %qbit.i14.i78 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i14.i78, double 0xBFF921FB54442D18) + %qbit.i17.i79 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rxy(i64 %qbit.i17.i79, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + %qbit.i20.i80 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___rz(i64 %qbit.i20.i80, double 0xBFF921FB54442D18) + %qbit.i.i81 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rxy(i64 %qbit.i.i81, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + %qbit.i4.i82 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___rz(i64 %qbit.i4.i82, double 0x400921FB54442D18) %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %meas7 = tail call i64 @___lazy_measure(i64 %qbit6) - %qbit.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 - tail call void @___reset(i64 %qbit.i84) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + tail call void @___reset(i64 %qbit8) %bool9 = tail call i1 @___read_future_bool(i64 %meas7) tail call void @___dec_future_refcount(i64 %meas7) br i1 %bool9, label %then__3, label %continue__3 then__3: ; preds = %continue__2 - %qbit.i.i85 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rz(i64 %qbit.i.i85, double 0x400921FB54442D18) + %qbit.i.i83 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rz(i64 %qbit.i.i83, double 0x400921FB54442D18) br label %continue__3 continue__3: ; preds = %then__3, %continue__2 %qbit10 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 %meas11 = tail call i64 @___lazy_measure(i64 %qbit10) - %qbit.i86 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 - tail call void @___reset(i64 %qbit.i86) + %qbit12 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 24), align 8 + tail call void @___reset(i64 %qbit12) %bool13 = tail call i1 @___read_future_bool(i64 %meas11) tail call void @___dec_future_refcount(i64 %meas11) br i1 %bool13, label %then__4, label %continue__4 then__4: ; preds = %continue__3 - %qbit.i.i87 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___rxy(i64 %qbit.i.i87, double 0x400921FB54442D18, double 0.000000e+00) + %qbit.i.i84 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___rxy(i64 %qbit.i.i84, double 0x400921FB54442D18, double 0.000000e+00) br label %continue__4 continue__4: ; preds = %then__4, %continue__3 %qbit14 = load i64, ptr @qis_qs, align 16 %meas15 = tail call i64 @___lazy_measure(i64 %qbit14) - %qbit.i88 = load i64, ptr @qis_qs, align 16 - tail call void @___reset(i64 %qbit.i88) + %qbit16 = load i64, ptr @qis_qs, align 16 + tail call void @___reset(i64 %qbit16) %qbit17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 %meas18 = tail call i64 @___lazy_measure(i64 %qbit17) - %qbit.i89 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 - tail call void @___reset(i64 %qbit.i89) + %qbit19 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 40), align 8 + tail call void @___reset(i64 %qbit19) %bool20 = tail call i1 @___read_future_bool(i64 %meas15) tail call void @___dec_future_refcount(i64 %meas15) tail call void @print_bool(ptr nonnull @res_0_t0, i64 16, i1 %bool20) diff --git a/tests/snaps/barrier.ll.snap b/tests/snaps/barrier.ll.snap index 8f03cd4..2b8e23c 100644 --- a/tests/snaps/barrier.ll.snap +++ b/tests/snaps/barrier.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -49,8 +49,8 @@ qir_qis.init_qubit.exit14: ; preds = %qir_qis.init_qubit. %0 = load <2 x i64>, ptr @qis_qs, align 16 store <2 x i64> %0, ptr %barrier_qubits, align 16 call void @___barrier(ptr nonnull %barrier_qubits, i64 2) - %qbit.i.i17 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - call void @___rxy(i64 %qbit.i.i17, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i.i16 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + call void @___rxy(i64 %qbit.i.i16, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) %qbit.i8.i = load i64, ptr @qis_qs, align 16 %qbit.i11.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 call void @___rzz(i64 %qbit.i8.i, i64 %qbit.i11.i, double 0x3FF921FB54442D18) @@ -61,8 +61,8 @@ qir_qis.init_qubit.exit14: ; preds = %qir_qis.init_qubit. %qbit.i20.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 call void @___rz(i64 %qbit.i20.i, double 0xBFF921FB54442D18) %barrier_qubits2 = alloca [1 x i64], align 8 - %qbit.i18 = load i64, ptr @qis_qs, align 16 - store i64 %qbit.i18, ptr %barrier_qubits2, align 8 + %qbit3 = load i64, ptr @qis_qs, align 16 + store i64 %qbit3, ptr %barrier_qubits2, align 8 call void @___barrier(ptr nonnull %barrier_qubits2, i64 1) %qbit5 = load i64, ptr @qis_qs, align 16 %meas = call i64 @___lazy_measure(i64 %qbit5) diff --git a/tests/snaps/barrier_multi.ll.snap b/tests/snaps/barrier_multi.ll.snap index 5f750fd..48c99e6 100644 --- a/tests/snaps/barrier_multi.ll.snap +++ b/tests/snaps/barrier_multi.ll.snap @@ -1,6 +1,6 @@ --- source: src/convert.rs -expression: qis_text.unwrap().to_string() +expression: qis_text.to_string() --- ; ModuleID = 'qis_module' source_filename = "qir" @@ -170,12 +170,12 @@ qir_qis.init_qubit.exit87: ; preds = %qir_qis.init_qubit. %1 = getelementptr inbounds nuw i8, ptr %barrier_qubits, i64 16 %2 = load <2 x i64>, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 16), align 16 store <2 x i64> %2, ptr %1, align 16 - %qbit.i95 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 + %qbit4 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 32), align 16 %3 = getelementptr inbounds nuw i8, ptr %barrier_qubits, i64 32 - store i64 %qbit.i95, ptr %3, align 16 + store i64 %qbit4, ptr %3, align 16 call void @___barrier(ptr nonnull %barrier_qubits, i64 5) - %qbit.i.i96 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 - call void @___rxy(i64 %qbit.i.i96, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + %qbit.i.i92 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + call void @___rxy(i64 %qbit.i.i92, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) %qbit.i8.i = load i64, ptr @qis_qs, align 16 %qbit.i11.i = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 call void @___rzz(i64 %qbit.i8.i, i64 %qbit.i11.i, double 0x3FF921FB54442D18) diff --git a/tests/snaps/dynamic_qubit_alloc.ll.snap b/tests/snaps/dynamic_qubit_alloc.ll.snap new file mode 100644 index 0000000..99b150d --- /dev/null +++ b/tests/snaps/dynamic_qubit_alloc.ll.snap @@ -0,0 +1,82 @@ +--- +source: src/convert.rs +assertion_line: 1968 +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@res_q0 = private constant [15 x i8] c"\0EUSER:RESULT:q0" +@e_qalloc_fail = private constant [47 x i8] c".EXIT:INT:No more qubits available to allocate." +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i = tail call i64 @___qalloc() + %is_fail.i = icmp eq i64 %qalloc.i, -1 + br i1 %is_fail.i, label %fail_panic.i, label %qir_qis.qubit_allocate.exit + +fail_panic.i: ; preds = %entry + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +qir_qis.qubit_allocate.exit: ; preds = %entry + tail call void @___rxy(i64 %qalloc.i, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + tail call void @___rz(i64 %qalloc.i, double 0x400921FB54442D18) + %meas = tail call i64 @___lazy_measure(i64 %qalloc.i) + %bool = tail call i1 @___read_future_bool(i64 %meas) + tail call void @___dec_future_refcount(i64 %meas) + tail call void @print_bool(ptr nonnull @res_q0, i64 14, i1 %bool) + %is_null.i = icmp eq i64 %qalloc.i, 0 + br i1 %is_null.i, label %qir_qis.qubit_release.exit, label %body.i + +body.i: ; preds = %qir_qis.qubit_allocate.exit + tail call void @___qfree(i64 %qalloc.i) + br label %qir_qis.qubit_release.exit + +qir_qis.qubit_release.exit: ; preds = %body.i, %qir_qis.qubit_allocate.exit + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @panic(i32, ptr) local_unnamed_addr + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @print_bool(ptr, i64, i1) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +declare void @___rxy(i64, double, double) local_unnamed_addr + +declare void @___rz(i64, double) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_qubit_alloc_checked.ll.snap b/tests/snaps/dynamic_qubit_alloc_checked.ll.snap new file mode 100644 index 0000000..3ca721b --- /dev/null +++ b/tests/snaps/dynamic_qubit_alloc_checked.ll.snap @@ -0,0 +1,72 @@ +--- +source: src/convert.rs +assertion_line: 1970 +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@res_qcheck0 = private constant [20 x i8] c"\13USER:RESULT:qcheck0" +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i = tail call i64 @___qalloc() + %is_fail.i = icmp eq i64 %qalloc.i, -1 + br i1 %is_fail.i, label %done, label %alloc_ok + +alloc_ok: ; preds = %entry + tail call void @___rxy(i64 %qalloc.i, double 0x400921FB54442D18, double 0.000000e+00) + %meas = tail call i64 @___lazy_measure(i64 %qalloc.i) + %bool = tail call i1 @___read_future_bool(i64 %meas) + tail call void @___dec_future_refcount(i64 %meas) + tail call void @print_bool(ptr nonnull @res_qcheck0, i64 19, i1 %bool) + %is_null.i = icmp eq i64 %qalloc.i, 0 + br i1 %is_null.i, label %done, label %body.i + +body.i: ; preds = %alloc_ok + tail call void @___qfree(i64 %qalloc.i) + br label %done + +done: ; preds = %body.i, %alloc_ok, %entry + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @print_bool(ptr, i64, i1) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +declare void @___rxy(i64, double, double) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 false} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_qubit_array_checked.ll.snap b/tests/snaps/dynamic_qubit_array_checked.ll.snap new file mode 100644 index 0000000..905b93a --- /dev/null +++ b/tests/snaps/dynamic_qubit_array_checked.ll.snap @@ -0,0 +1,124 @@ +--- +source: src/convert.rs +assertion_line: 1977 +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@res_acheck0 = private constant [20 x i8] c"\13USER:RESULT:acheck0" +@res_acheck1 = private constant [20 x i8] c"\13USER:RESULT:acheck1" +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i.i = tail call i64 @___qalloc() + %is_fail.i.i = icmp eq i64 %qalloc.i.i, -1 + %qubit_ptr.i.i = inttoptr i64 %qalloc.i.i to ptr + %spec.select.i.i = select i1 %is_fail.i.i, ptr null, ptr %qubit_ptr.i.i + br i1 %is_fail.i.i, label %loop_body.i.i, label %continue_alloc.i + +continue_alloc.i: ; preds = %entry + %qalloc.i.1.i = tail call i64 @___qalloc() + %is_fail.i.1.i = icmp eq i64 %qalloc.i.1.i, -1 + %qubit_ptr.i.1.i = inttoptr i64 %qalloc.i.1.i to ptr + %spec.select.i.1.i = select i1 %is_fail.i.1.i, ptr null, ptr %qubit_ptr.i.1.i + br i1 %is_fail.i.1.i, label %loop_body.i.i, label %alloc_ok + +loop_body.i.i: ; preds = %continue_alloc.i, %entry + %qubits.sroa.2.0 = phi ptr [ undef, %entry ], [ %spec.select.i.1.i, %continue_alloc.i ] + %is_null.i.i.i = icmp eq ptr %spec.select.i.i, null + br i1 %is_null.i.i.i, label %qir_qis.qubit_release.exit.i.i, label %body.i.i.i + +body.i.i.i: ; preds = %loop_body.i.i + %qubit_int.i.i.i = ptrtoint ptr %spec.select.i.i to i64 + tail call void @___qfree(i64 %qubit_int.i.i.i) + br label %qir_qis.qubit_release.exit.i.i + +qir_qis.qubit_release.exit.i.i: ; preds = %body.i.i.i, %loop_body.i.i + %is_null.i.i.i.1 = icmp eq ptr %qubits.sroa.2.0, null + %or.cond = or i1 %is_fail.i.i, %is_null.i.i.i.1 + br i1 %or.cond, label %done, label %body.i.i.i.1 + +body.i.i.i.1: ; preds = %qir_qis.qubit_release.exit.i.i + %qubit_int.i.i.i.1 = ptrtoint ptr %qubits.sroa.2.0 to i64 + tail call void @___qfree(i64 %qubit_int.i.i.i.1) + br label %done + +alloc_ok: ; preds = %continue_alloc.i + tail call void @___rxy(i64 %qalloc.i.i, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + tail call void @___rz(i64 %qalloc.i.i, double 0x400921FB54442D18) + tail call void @___rxy(i64 %qalloc.i.1.i, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + tail call void @___rzz(i64 %qalloc.i.i, i64 %qalloc.i.1.i, double 0x3FF921FB54442D18) + tail call void @___rz(i64 %qalloc.i.i, double 0xBFF921FB54442D18) + tail call void @___rxy(i64 %qalloc.i.1.i, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + tail call void @___rz(i64 %qalloc.i.1.i, double 0xBFF921FB54442D18) + %meas = tail call i64 @___lazy_measure(i64 %qalloc.i.i) + %meas2 = tail call i64 @___lazy_measure(i64 %qalloc.i.1.i) + %bool = tail call i1 @___read_future_bool(i64 %meas) + tail call void @___dec_future_refcount(i64 %meas) + tail call void @print_bool(ptr nonnull @res_acheck0, i64 19, i1 %bool) + %bool3 = tail call i1 @___read_future_bool(i64 %meas2) + tail call void @___dec_future_refcount(i64 %meas2) + tail call void @print_bool(ptr nonnull @res_acheck1, i64 19, i1 %bool3) + %is_null.i.i = icmp eq i64 %qalloc.i.i, 0 + br i1 %is_null.i.i, label %qir_qis.qubit_release.exit.i, label %body.i.i + +body.i.i: ; preds = %alloc_ok + tail call void @___qfree(i64 %qalloc.i.i) + br label %qir_qis.qubit_release.exit.i + +qir_qis.qubit_release.exit.i: ; preds = %body.i.i, %alloc_ok + %is_null.i.i.1 = icmp eq i64 %qalloc.i.1.i, 0 + br i1 %is_null.i.i.1, label %done, label %body.i.i.1 + +body.i.i.1: ; preds = %qir_qis.qubit_release.exit.i + tail call void @___qfree(i64 %qalloc.i.1.i) + br label %done + +done: ; preds = %body.i.i.1, %qir_qis.qubit_release.exit.i, %body.i.i.i.1, %qir_qis.qubit_release.exit.i.i + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @print_bool(ptr, i64, i1) local_unnamed_addr + +declare void @___rxy(i64, double, double) local_unnamed_addr + +declare void @___rz(i64, double) local_unnamed_addr + +declare void @___rzz(i64, i64, double) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_qubit_array_ssa.ll.snap b/tests/snaps/dynamic_qubit_array_ssa.ll.snap new file mode 100644 index 0000000..f3ad20e --- /dev/null +++ b/tests/snaps/dynamic_qubit_array_ssa.ll.snap @@ -0,0 +1,107 @@ +--- +source: src/convert.rs +assertion_line: 1977 +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@res_m0 = private constant [15 x i8] c"\0EUSER:RESULT:m0" +@res_m1 = private constant [15 x i8] c"\0EUSER:RESULT:m1" +@e_qalloc_fail = private constant [47 x i8] c".EXIT:INT:No more qubits available to allocate." +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i.i = tail call i64 @___qalloc() + %is_fail.i.i = icmp eq i64 %qalloc.i.i, -1 + br i1 %is_fail.i.i, label %fail_panic.i.i, label %qir_qis.qubit_allocate.exit.i + +fail_panic.i.i: ; preds = %qir_qis.qubit_allocate.exit.i, %entry + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +qir_qis.qubit_allocate.exit.i: ; preds = %entry + %qalloc.i.1.i = tail call i64 @___qalloc() + %is_fail.i.1.i = icmp eq i64 %qalloc.i.1.i, -1 + br i1 %is_fail.i.1.i, label %fail_panic.i.i, label %qir_qis.qubit_array_allocate.exit + +qir_qis.qubit_array_allocate.exit: ; preds = %qir_qis.qubit_allocate.exit.i + tail call void @___rxy(i64 %qalloc.i.1.i, double 0x3FF921FB54442D18, double 0xBFF921FB54442D18) + tail call void @___rz(i64 %qalloc.i.1.i, double 0x400921FB54442D18) + tail call void @___rxy(i64 %qalloc.i.i, double 0xBFF921FB54442D18, double 0x3FF921FB54442D18) + tail call void @___rzz(i64 %qalloc.i.1.i, i64 %qalloc.i.i, double 0x3FF921FB54442D18) + tail call void @___rz(i64 %qalloc.i.1.i, double 0xBFF921FB54442D18) + tail call void @___rxy(i64 %qalloc.i.i, double 0x3FF921FB54442D18, double 0x400921FB54442D18) + tail call void @___rz(i64 %qalloc.i.i, double 0xBFF921FB54442D18) + %meas = tail call i64 @___lazy_measure(i64 %qalloc.i.1.i) + %meas2 = tail call i64 @___lazy_measure(i64 %qalloc.i.i) + %bool = tail call i1 @___read_future_bool(i64 %meas) + tail call void @___dec_future_refcount(i64 %meas) + tail call void @print_bool(ptr nonnull @res_m0, i64 14, i1 %bool) + %bool3 = tail call i1 @___read_future_bool(i64 %meas2) + tail call void @___dec_future_refcount(i64 %meas2) + tail call void @print_bool(ptr nonnull @res_m1, i64 14, i1 %bool3) + %is_null.i.i = icmp eq i64 %qalloc.i.i, 0 + br i1 %is_null.i.i, label %qir_qis.qubit_release.exit.i, label %body.i.i + +body.i.i: ; preds = %qir_qis.qubit_array_allocate.exit + tail call void @___qfree(i64 %qalloc.i.i) + br label %qir_qis.qubit_release.exit.i + +qir_qis.qubit_release.exit.i: ; preds = %body.i.i, %qir_qis.qubit_array_allocate.exit + %is_null.i.1.i = icmp eq i64 %qalloc.i.1.i, 0 + br i1 %is_null.i.1.i, label %qir_qis.qubit_array_release.exit, label %body.i.1.i + +body.i.1.i: ; preds = %qir_qis.qubit_release.exit.i + tail call void @___qfree(i64 %qalloc.i.1.i) + br label %qir_qis.qubit_array_release.exit + +qir_qis.qubit_array_release.exit: ; preds = %body.i.1.i, %qir_qis.qubit_release.exit.i + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @panic(i32, ptr) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @print_bool(ptr, i64, i1) local_unnamed_addr + +declare void @___rxy(i64, double, double) local_unnamed_addr + +declare void @___rz(i64, double) local_unnamed_addr + +declare void @___rzz(i64, i64, double) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 true} +!3 = !{i32 1, !"dynamic_result_management", i1 false} +!4 = !{i32 1, !"arrays", i1 true} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_result_alloc.ll.snap b/tests/snaps/dynamic_result_alloc.ll.snap new file mode 100644 index 0000000..6b7ea70 --- /dev/null +++ b/tests/snaps/dynamic_result_alloc.ll.snap @@ -0,0 +1,90 @@ +--- +source: src/convert.rs +assertion_line: 1938 +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@qis_qs = private unnamed_addr global [1 x i64] zeroinitializer +@e_qalloc_fail = private constant [47 x i8] c".EXIT:INT:No more qubits available to allocate." +@res_r0 = private constant [15 x i8] c"\0EUSER:RESULT:r0" +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i = tail call i64 @___qalloc() + %is_fail.i = icmp eq i64 %qalloc.i, -1 + br i1 %is_fail.i, label %qalloc_fail.i, label %qir_qis.result_read.exit + +qalloc_fail.i: ; preds = %entry + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +qir_qis.result_read.exit: ; preds = %entry + tail call void @___reset(i64 %qalloc.i) + store i64 %qalloc.i, ptr @qis_qs, align 8 + tail call void @___rxy(i64 %qalloc.i, double 0x400921FB54442D18, double 0.000000e+00) + %qbit = load i64, ptr @qis_qs, align 8 + %meas = tail call i64 @___lazy_measure(i64 %qbit) + %bool.i = tail call i1 @___read_future_bool(i64 %meas) + tail call void @___dec_future_refcount(i64 %meas) + tail call void @print_bool(ptr nonnull @res_r0, i64 14, i1 %bool.i) + %qbit1 = load i64, ptr @qis_qs, align 8 + tail call void @___qfree(i64 %qbit1) + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @panic(i32, ptr) local_unnamed_addr + +declare void @___reset(i64) local_unnamed_addr + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) +define i64 @qir_qis.load_qubit(ptr %0) local_unnamed_addr #0 { +entry: + %idx = ptrtoint ptr %0 to i64 + %qbit_ptr = getelementptr [1 x i64], ptr @qis_qs, i64 0, i64 %idx + %qbit = load i64, ptr %qbit_ptr, align 8 + ret i64 %qbit +} + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @print_bool(ptr, i64, i1) local_unnamed_addr + +declare void @___rxy(i64, double, double) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 false} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_result_array.ll.snap b/tests/snaps/dynamic_result_array.ll.snap new file mode 100644 index 0000000..0282759 --- /dev/null +++ b/tests/snaps/dynamic_result_array.ll.snap @@ -0,0 +1,218 @@ +--- +source: src/convert.rs +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@qis_qs = private unnamed_addr global [2 x i64] zeroinitializer +@e_qalloc_fail = private constant [47 x i8] c".EXIT:INT:No more qubits available to allocate." +@res_a0.1 = private constant [21 x i8] c"\14USER:RESULT_ARRAY:a0" +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i = tail call i64 @___qalloc() + %is_fail.i = icmp eq i64 %qalloc.i, -1 + br i1 %is_fail.i, label %qalloc_fail.i, label %qir_qis.init_qubit.exit + +qalloc_fail.i: ; preds = %entry + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +qir_qis.init_qubit.exit: ; preds = %entry + tail call void @___reset(i64 %qalloc.i) + store i64 %qalloc.i, ptr @qis_qs, align 8 + %qalloc.i10 = tail call i64 @___qalloc() + %is_fail.i11 = icmp eq i64 %qalloc.i10, -1 + br i1 %is_fail.i11, label %qalloc_fail.i12, label %body.i.i + +qalloc_fail.i12: ; preds = %qir_qis.init_qubit.exit + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +body.i.i: ; preds = %qir_qis.init_qubit.exit + tail call void @___reset(i64 %qalloc.i10) + store i64 %qalloc.i10, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + %results = alloca [2 x ptr], align 8 + %dyn_result = alloca { i8, i1, i64 }, align 8 + %cached = getelementptr inbounds nuw i8, ptr %dyn_result, i64 1 + %future = getelementptr inbounds nuw i8, ptr %dyn_result, i64 8 + store i8 0, ptr %dyn_result, align 8 + store i1 false, ptr %cached, align 1 + store i64 0, ptr %future, align 8 + store ptr %dyn_result, ptr %results, align 8 + %result_elem_ptr1 = getelementptr inbounds nuw i8, ptr %results, i64 8 + %dyn_result2 = alloca { i8, i1, i64 }, align 8 + %cached4 = getelementptr inbounds nuw i8, ptr %dyn_result2, i64 1 + %future5 = getelementptr inbounds nuw i8, ptr %dyn_result2, i64 8 + store i8 0, ptr %dyn_result2, align 8 + store i1 false, ptr %cached4, align 1 + store i64 0, ptr %future5, align 8 + store ptr %dyn_result2, ptr %result_elem_ptr1, align 8 + %qbit = load i64, ptr @qis_qs, align 8 + %meas = call i64 @___lazy_measure(i64 %qbit) + store i8 1, ptr %dyn_result, align 8 + store i64 %meas, ptr %future, align 8 + %qbit6 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + %meas7 = call i64 @___lazy_measure(i64 %qbit6) + store i8 1, ptr %dyn_result2, align 8 + store i64 %meas7, ptr %future5, align 8 + call fastcc void @qir_qis.result_array_record_output(ptr %results) + %state1.i.i = load i8, ptr %dyn_result, align 8 + %is_pending.i.i = icmp eq i8 %state1.i.i, 1 + br i1 %is_pending.i.i, label %dec_future.i.i, label %body.i.1.i + +dec_future.i.i: ; preds = %body.i.i + %future2.i.i = load i64, ptr %future, align 8 + call void @___dec_future_refcount(i64 %future2.i.i) + br label %body.i.1.i + +body.i.1.i: ; preds = %dec_future.i.i, %body.i.i + store i8 0, ptr %dyn_result, align 8 + store i1 false, ptr %cached, align 1 + store i64 0, ptr %future, align 8 + %state1.i.1.i = load i8, ptr %dyn_result2, align 8 + %is_pending.i.1.i = icmp eq i8 %state1.i.1.i, 1 + br i1 %is_pending.i.1.i, label %dec_future.i.1.i, label %qir_qis.result_array_release.exit + +dec_future.i.1.i: ; preds = %body.i.1.i + %future2.i.1.i = load i64, ptr %future5, align 8 + call void @___dec_future_refcount(i64 %future2.i.1.i) + br label %qir_qis.result_array_release.exit + +qir_qis.result_array_release.exit: ; preds = %dec_future.i.1.i, %body.i.1.i + store i8 0, ptr %dyn_result2, align 8 + store i1 false, ptr %cached4, align 1 + store i64 0, ptr %future5, align 8 + %qbit8 = load i64, ptr @qis_qs, align 8 + call void @___qfree(i64 %qbit8) + %qbit9 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + call void @___qfree(i64 %qbit9) + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @panic(i32, ptr) local_unnamed_addr + +declare void @___reset(i64) local_unnamed_addr + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) +define i64 @qir_qis.load_qubit(ptr %0) local_unnamed_addr #0 { +entry: + %idx = ptrtoint ptr %0 to i64 + %qbit_ptr = getelementptr [2 x i64], ptr @qis_qs, i64 0, i64 %idx + %qbit = load i64, ptr %qbit_ptr, align 8 + ret i64 %qbit +} + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +define private fastcc void @qir_qis.result_array_record_output(ptr nonnull readonly captures(none) %0) unnamed_addr { +entry: + %result_arr_data1 = alloca [2 x i1], align 1 + %result_ptr = load ptr, ptr %0, align 8 + %is_null.i = icmp eq ptr %result_ptr, null + br i1 %is_null.i, label %qir_qis.result_read.exit, label %read_state.i + +pending.i: ; preds = %read_state.i + %future.i = getelementptr inbounds nuw i8, ptr %result_ptr, i64 8 + %future3.i = load i64, ptr %future.i, align 4 + %bool.i = tail call i1 @___read_future_bool(i64 %future3.i) + tail call void @___dec_future_refcount(i64 %future3.i) + store i1 %bool.i, ptr %cached1.i, align 1 + store i8 2, ptr %result_ptr, align 1 + br label %qir_qis.result_read.exit + +cached.i: ; preds = %read_state.i + %cached4.i = load i1, ptr %cached1.i, align 1 + br label %qir_qis.result_read.exit + +read_state.i: ; preds = %entry + %cached1.i = getelementptr inbounds nuw i8, ptr %result_ptr, i64 1 + %state2.i = load i8, ptr %result_ptr, align 1 + switch i8 %state2.i, label %qir_qis.result_read.exit [ + i8 1, label %pending.i + i8 2, label %cached.i + ] + +qir_qis.result_read.exit: ; preds = %read_state.i, %cached.i, %pending.i, %entry + %common.ret.op.i = phi i1 [ %bool.i, %pending.i ], [ %cached4.i, %cached.i ], [ false, %entry ], [ false, %read_state.i ] + store i1 %common.ret.op.i, ptr %result_arr_data1, align 1 + %elem_ptr.1 = getelementptr i8, ptr %0, i64 8 + %result_ptr.1 = load ptr, ptr %elem_ptr.1, align 8 + %is_null.i.1 = icmp eq ptr %result_ptr.1, null + br i1 %is_null.i.1, label %qir_qis.result_read.exit.1, label %read_state.i.1 + +read_state.i.1: ; preds = %qir_qis.result_read.exit + %cached1.i.1 = getelementptr inbounds nuw i8, ptr %result_ptr.1, i64 1 + %state2.i.1 = load i8, ptr %result_ptr.1, align 1 + switch i8 %state2.i.1, label %qir_qis.result_read.exit.1 [ + i8 1, label %pending.i.1 + i8 2, label %cached.i.1 + ] + +cached.i.1: ; preds = %read_state.i.1 + %cached4.i.1 = load i1, ptr %cached1.i.1, align 1 + br label %qir_qis.result_read.exit.1 + +pending.i.1: ; preds = %read_state.i.1 + %future.i.1 = getelementptr inbounds nuw i8, ptr %result_ptr.1, i64 8 + %future3.i.1 = load i64, ptr %future.i.1, align 4 + %bool.i.1 = tail call i1 @___read_future_bool(i64 %future3.i.1) + tail call void @___dec_future_refcount(i64 %future3.i.1) + store i1 %bool.i.1, ptr %cached1.i.1, align 1 + store i8 2, ptr %result_ptr.1, align 1 + br label %qir_qis.result_read.exit.1 + +qir_qis.result_read.exit.1: ; preds = %pending.i.1, %cached.i.1, %read_state.i.1, %qir_qis.result_read.exit + %common.ret.op.i.1 = phi i1 [ %bool.i.1, %pending.i.1 ], [ %cached4.i.1, %cached.i.1 ], [ false, %qir_qis.result_read.exit ], [ false, %read_state.i.1 ] + %result_bool_ptr.1 = getelementptr inbounds nuw i8, ptr %result_arr_data1, i64 1 + store i1 %common.ret.op.i.1, ptr %result_bool_ptr.1, align 1 + %result_arr_desc = alloca <{ i32, i32, ptr, ptr }>, align 8 + %result_arr_data_ptr = getelementptr inbounds nuw i8, ptr %result_arr_desc, i64 8 + %result_arr_mask_ptr = getelementptr inbounds nuw i8, ptr %result_arr_desc, i64 16 + %result_arr_mask = alloca i32, align 4 + store i32 0, ptr %result_arr_mask, align 4 + store <2 x i32> , ptr %result_arr_desc, align 8 + store ptr %result_arr_data1, ptr %result_arr_data_ptr, align 8 + store ptr %result_arr_mask, ptr %result_arr_mask_ptr, align 8 + call void @print_bool_arr(ptr nonnull @res_a0.1, i64 20, ptr nonnull %result_arr_desc) + ret void +} + +declare void @print_bool_arr(ptr, i64, ptr) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} +!5 = !{!"mainlib"} diff --git a/tests/snaps/dynamic_result_mixed_array_output.ll.snap b/tests/snaps/dynamic_result_mixed_array_output.ll.snap new file mode 100644 index 0000000..e4ce58c --- /dev/null +++ b/tests/snaps/dynamic_result_mixed_array_output.ll.snap @@ -0,0 +1,218 @@ +--- +source: src/convert.rs +expression: qis_text.to_string() +--- +; ModuleID = 'qis_module' +source_filename = "qir" +target datalayout = "e-m:e-p270:32:32-p271:32:32-p272:64:64-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128-Fn32" +target triple = "aarch64-unknown-linux-gnu" + +@qis_qs = private unnamed_addr global [2 x i64] zeroinitializer +@e_qalloc_fail = private constant [47 x i8] c".EXIT:INT:No more qubits available to allocate." +@res_mix0.1 = private constant [23 x i8] c"\16USER:RESULT_ARRAY:mix0" +@gen_name = local_unnamed_addr global [7 x i8] c"qir-qis", section ",generator" +@gen_version = local_unnamed_addr global [5 x i8] c"0.0.0", section ",generator" + +define noundef i64 @___user_qir_Entry_Point_Name() local_unnamed_addr { +entry: + %qalloc.i = tail call i64 @___qalloc() + %is_fail.i = icmp eq i64 %qalloc.i, -1 + br i1 %is_fail.i, label %qalloc_fail.i, label %qir_qis.init_qubit.exit + +qalloc_fail.i: ; preds = %entry + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +qir_qis.init_qubit.exit: ; preds = %entry + tail call void @___reset(i64 %qalloc.i) + store i64 %qalloc.i, ptr @qis_qs, align 8 + %qalloc.i9 = tail call i64 @___qalloc() + %is_fail.i10 = icmp eq i64 %qalloc.i9, -1 + br i1 %is_fail.i10, label %qalloc_fail.i11, label %body.i.i + +qalloc_fail.i11: ; preds = %qir_qis.init_qubit.exit + tail call void @panic(i32 1001, ptr nonnull @e_qalloc_fail) + unreachable + +body.i.i: ; preds = %qir_qis.init_qubit.exit + tail call void @___reset(i64 %qalloc.i9) + store i64 %qalloc.i9, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + %results = alloca [2 x ptr], align 8 + %dyn_result = alloca { i8, i1, i64 }, align 8 + %cached = getelementptr inbounds nuw i8, ptr %dyn_result, i64 1 + %future = getelementptr inbounds nuw i8, ptr %dyn_result, i64 8 + store i8 0, ptr %dyn_result, align 8 + store i1 false, ptr %cached, align 1 + store i64 0, ptr %future, align 8 + %dyn_result1 = alloca { i8, i1, i64 }, align 8 + %cached3 = getelementptr inbounds nuw i8, ptr %dyn_result1, i64 1 + %future4 = getelementptr inbounds nuw i8, ptr %dyn_result1, i64 8 + store i8 0, ptr %dyn_result1, align 8 + store i1 false, ptr %cached3, align 1 + store i64 0, ptr %future4, align 8 + store ptr %dyn_result, ptr %results, align 8 + %packed.fca.1.gep = getelementptr inbounds nuw i8, ptr %results, i64 8 + store ptr %dyn_result1, ptr %packed.fca.1.gep, align 8 + %qbit = load i64, ptr @qis_qs, align 8 + %meas = call i64 @___lazy_measure(i64 %qbit) + store i8 1, ptr %dyn_result, align 8 + store i64 %meas, ptr %future, align 8 + %qbit5 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + %meas6 = call i64 @___lazy_measure(i64 %qbit5) + store i8 1, ptr %dyn_result1, align 8 + store i64 %meas6, ptr %future4, align 8 + call fastcc void @qir_qis.result_array_record_output(ptr %results) + %state1.i.i = load i8, ptr %dyn_result, align 8 + %is_pending.i.i = icmp eq i8 %state1.i.i, 1 + br i1 %is_pending.i.i, label %dec_future.i.i, label %body.i.1.i + +dec_future.i.i: ; preds = %body.i.i + %future2.i.i = load i64, ptr %future, align 8 + call void @___dec_future_refcount(i64 %future2.i.i) + br label %body.i.1.i + +body.i.1.i: ; preds = %dec_future.i.i, %body.i.i + store i8 0, ptr %dyn_result, align 8 + store i1 false, ptr %cached, align 1 + store i64 0, ptr %future, align 8 + %state1.i.1.i = load i8, ptr %dyn_result1, align 8 + %is_pending.i.1.i = icmp eq i8 %state1.i.1.i, 1 + br i1 %is_pending.i.1.i, label %dec_future.i.1.i, label %qir_qis.result_array_release.exit + +dec_future.i.1.i: ; preds = %body.i.1.i + %future2.i.1.i = load i64, ptr %future4, align 8 + call void @___dec_future_refcount(i64 %future2.i.1.i) + br label %qir_qis.result_array_release.exit + +qir_qis.result_array_release.exit: ; preds = %dec_future.i.1.i, %body.i.1.i + store i8 0, ptr %dyn_result1, align 8 + store i1 false, ptr %cached3, align 1 + store i64 0, ptr %future4, align 8 + %qbit7 = load i64, ptr @qis_qs, align 8 + call void @___qfree(i64 %qbit7) + %qbit8 = load i64, ptr getelementptr inbounds nuw (i8, ptr @qis_qs, i64 8), align 8 + call void @___qfree(i64 %qbit8) + ret i64 0 +} + +declare i64 @___qalloc() local_unnamed_addr + +declare void @panic(i32, ptr) local_unnamed_addr + +declare void @___reset(i64) local_unnamed_addr + +; Function Attrs: mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) +define i64 @qir_qis.load_qubit(ptr %0) local_unnamed_addr #0 { +entry: + %idx = ptrtoint ptr %0 to i64 + %qbit_ptr = getelementptr [2 x i64], ptr @qis_qs, i64 0, i64 %idx + %qbit = load i64, ptr %qbit_ptr, align 8 + ret i64 %qbit +} + +declare i64 @___lazy_measure(i64) local_unnamed_addr + +define private fastcc void @qir_qis.result_array_record_output(ptr nonnull readonly captures(none) %0) unnamed_addr { +entry: + %result_arr_data1 = alloca [2 x i1], align 1 + %result_ptr = load ptr, ptr %0, align 8 + %is_null.i = icmp eq ptr %result_ptr, null + br i1 %is_null.i, label %qir_qis.result_read.exit, label %read_state.i + +pending.i: ; preds = %read_state.i + %future.i = getelementptr inbounds nuw i8, ptr %result_ptr, i64 8 + %future3.i = load i64, ptr %future.i, align 4 + %bool.i = tail call i1 @___read_future_bool(i64 %future3.i) + tail call void @___dec_future_refcount(i64 %future3.i) + store i1 %bool.i, ptr %cached1.i, align 1 + store i8 2, ptr %result_ptr, align 1 + br label %qir_qis.result_read.exit + +cached.i: ; preds = %read_state.i + %cached4.i = load i1, ptr %cached1.i, align 1 + br label %qir_qis.result_read.exit + +read_state.i: ; preds = %entry + %cached1.i = getelementptr inbounds nuw i8, ptr %result_ptr, i64 1 + %state2.i = load i8, ptr %result_ptr, align 1 + switch i8 %state2.i, label %qir_qis.result_read.exit [ + i8 1, label %pending.i + i8 2, label %cached.i + ] + +qir_qis.result_read.exit: ; preds = %read_state.i, %cached.i, %pending.i, %entry + %common.ret.op.i = phi i1 [ %bool.i, %pending.i ], [ %cached4.i, %cached.i ], [ false, %entry ], [ false, %read_state.i ] + store i1 %common.ret.op.i, ptr %result_arr_data1, align 1 + %elem_ptr.1 = getelementptr i8, ptr %0, i64 8 + %result_ptr.1 = load ptr, ptr %elem_ptr.1, align 8 + %is_null.i.1 = icmp eq ptr %result_ptr.1, null + br i1 %is_null.i.1, label %qir_qis.result_read.exit.1, label %read_state.i.1 + +read_state.i.1: ; preds = %qir_qis.result_read.exit + %cached1.i.1 = getelementptr inbounds nuw i8, ptr %result_ptr.1, i64 1 + %state2.i.1 = load i8, ptr %result_ptr.1, align 1 + switch i8 %state2.i.1, label %qir_qis.result_read.exit.1 [ + i8 1, label %pending.i.1 + i8 2, label %cached.i.1 + ] + +cached.i.1: ; preds = %read_state.i.1 + %cached4.i.1 = load i1, ptr %cached1.i.1, align 1 + br label %qir_qis.result_read.exit.1 + +pending.i.1: ; preds = %read_state.i.1 + %future.i.1 = getelementptr inbounds nuw i8, ptr %result_ptr.1, i64 8 + %future3.i.1 = load i64, ptr %future.i.1, align 4 + %bool.i.1 = tail call i1 @___read_future_bool(i64 %future3.i.1) + tail call void @___dec_future_refcount(i64 %future3.i.1) + store i1 %bool.i.1, ptr %cached1.i.1, align 1 + store i8 2, ptr %result_ptr.1, align 1 + br label %qir_qis.result_read.exit.1 + +qir_qis.result_read.exit.1: ; preds = %pending.i.1, %cached.i.1, %read_state.i.1, %qir_qis.result_read.exit + %common.ret.op.i.1 = phi i1 [ %bool.i.1, %pending.i.1 ], [ %cached4.i.1, %cached.i.1 ], [ false, %qir_qis.result_read.exit ], [ false, %read_state.i.1 ] + %result_bool_ptr.1 = getelementptr inbounds nuw i8, ptr %result_arr_data1, i64 1 + store i1 %common.ret.op.i.1, ptr %result_bool_ptr.1, align 1 + %result_arr_desc = alloca <{ i32, i32, ptr, ptr }>, align 8 + %result_arr_data_ptr = getelementptr inbounds nuw i8, ptr %result_arr_desc, i64 8 + %result_arr_mask_ptr = getelementptr inbounds nuw i8, ptr %result_arr_desc, i64 16 + %result_arr_mask = alloca i32, align 4 + store i32 0, ptr %result_arr_mask, align 4 + store <2 x i32> , ptr %result_arr_desc, align 8 + store ptr %result_arr_data1, ptr %result_arr_data_ptr, align 8 + store ptr %result_arr_mask, ptr %result_arr_mask_ptr, align 8 + call void @print_bool_arr(ptr nonnull @res_mix0.1, i64 22, ptr nonnull %result_arr_desc) + ret void +} + +declare void @print_bool_arr(ptr, i64, ptr) local_unnamed_addr + +declare i1 @___read_future_bool(i64) local_unnamed_addr + +declare void @___dec_future_refcount(i64) local_unnamed_addr + +declare void @___qfree(i64) local_unnamed_addr + +define i64 @qmain(i64 %0) local_unnamed_addr { +entry: + tail call void @setup(i64 %0) + %1 = tail call i64 @___user_qir_Entry_Point_Name() + %retval = tail call i64 @teardown() + ret i64 %retval +} + +declare void @setup(i64) local_unnamed_addr + +declare i64 @teardown() local_unnamed_addr + +attributes #0 = { mustprogress nofree norecurse nosync nounwind willreturn memory(read, argmem: none, inaccessiblemem: none) } + +!llvm.module.flags = !{!0, !1, !2, !3, !4} +!name = !{!5} + +!0 = !{i32 1, !"qir_major_version", i32 2} +!1 = !{i32 7, !"qir_minor_version", i32 0} +!2 = !{i32 1, !"dynamic_qubit_management", i1 false} +!3 = !{i32 1, !"dynamic_result_management", i1 true} +!4 = !{i32 1, !"arrays", i1 true} +!5 = !{!"mainlib"} diff --git a/tests/test_main.py b/tests/test_main.py new file mode 100644 index 0000000..6c33ccf --- /dev/null +++ b/tests/test_main.py @@ -0,0 +1,72 @@ +"""Smoke tests for `main.py` output handling.""" + +import subprocess +import sys + +MAIN_TIMEOUT_SECONDS = 300 + + +def run_main(*args: str) -> str: + """Run `main.py` and return stdout.""" + try: + proc = subprocess.run( # noqa: S603 + [sys.executable, "main.py", *args], + check=True, + capture_output=True, + text=True, + timeout=MAIN_TIMEOUT_SECONDS, + ) + except subprocess.CalledProcessError as exc: + message = f"stdout:\n{exc.stdout}\n\nstderr:\n{exc.stderr}" + raise AssertionError(message) from exc + except subprocess.TimeoutExpired as exc: + message = ( + f"timed out after {MAIN_TIMEOUT_SECONDS}s\n\n" + f"stdout:\n{exc.stdout}\n\nstderr:\n{exc.stderr}" + ) + raise AssertionError(message) from exc + return proc.stdout + + +def run_main_spec(fixture: str) -> str: + """Run `main.py --spec` on a fixture and return stdout.""" + return run_main("--spec", fixture) + + +def test_base_spec_output() -> None: + """`base.ll` should emit baseline scalar results in spec format.""" + output = run_main_spec("tests/data/base.ll") + assert "HEADER schema_id labeled" in output, output # noqa: S101 + assert "OUTPUT TUPLE 2 t0" in output, output # noqa: S101 + assert "OUTPUT RESULT " in output, output # noqa: S101 + assert "OUTPUT RESULT_ARRAY" not in output, output # noqa: S101 + + +def test_base_plain_output() -> None: + """`base.ll` should emit labeled non-spec output through `main.py`.""" + output = run_main("tests/data/base.ll") + assert "'output_labeling_schema': 'labeled'" in output, output # noqa: S101 + assert "USER:QIRTUPLE:t0" in output, output # noqa: S101 + assert "USER:RESULT:r1" in output, output # noqa: S101 + assert "USER:RESULT:r2" in output, output # noqa: S101 + + +def test_dynamic_result_array_spec_output() -> None: + """`dynamic_result_array.ll` should emit spec-formatted result arrays.""" + output = run_main_spec("tests/data/dynamic_result_array.ll") + assert "OUTPUT RESULT_ARRAY 00 a0" in output, output # noqa: S101 + assert "USER:RESULT_ARRAY" not in output, output # noqa: S101 + + +def test_dynamic_result_mixed_array_spec_output() -> None: + """`dynamic_result_mixed_array_output.ll` should preserve the array label.""" + output = run_main_spec("tests/data/dynamic_result_mixed_array_output.ll") + assert "OUTPUT RESULT_ARRAY 00 mix0" in output, output # noqa: S101 + assert "USER:RESULT_ARRAY" not in output, output # noqa: S101 + + +if __name__ == "__main__": + test_base_spec_output() + test_base_plain_output() + test_dynamic_result_array_spec_output() + test_dynamic_result_mixed_array_spec_output() diff --git a/uv.lock b/uv.lock index ad93905..2c696fc 100644 --- a/uv.lock +++ b/uv.lock @@ -4,7 +4,8 @@ requires-python = ">=3.10, <3.15" resolution-markers = [ "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')", - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] [options] @@ -142,6 +143,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e3/4c/02df1befee243e4c14bf5740c391178ba4f7b4602ff08936da170341afe9/lief-0.17.6-cp314-cp314-win_arm64.whl", hash = "sha256:7dcefa6467f0f0d75413a10e7869e488344347f0c67eff5bc49ec216714f0674", size = 3462306, upload-time = "2026-03-18T06:58:54.937Z" }, ] +[[package]] +name = "llvmlite" +version = "0.45.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", +] +sdist = { url = "https://files.pythonhosted.org/packages/99/8d/5baf1cef7f9c084fb35a8afbde88074f0d6a727bc63ef764fe0e7543ba40/llvmlite-0.45.1.tar.gz", hash = "sha256:09430bb9d0bb58fc45a45a57c7eae912850bedc095cd0810a57de109c69e1c32", size = 185600, upload-time = "2025-10-01T17:59:52.046Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/6d/585c84ddd9d2a539a3c3487792b3cf3f988e28ec4fa281bf8b0e055e1166/llvmlite-0.45.1-cp310-cp310-macosx_10_15_x86_64.whl", hash = "sha256:1b1af0c910af0978aa55fa4f60bbb3e9f39b41e97c2a6d94d199897be62ba07a", size = 43043523, upload-time = "2025-10-01T18:02:58.621Z" }, + { url = "https://files.pythonhosted.org/packages/04/ad/9bdc87b2eb34642c1cfe6bcb4f5db64c21f91f26b010f263e7467e7536a3/llvmlite-0.45.1-cp311-cp311-macosx_10_15_x86_64.whl", hash = "sha256:60f92868d5d3af30b4239b50e1717cb4e4e54f6ac1c361a27903b318d0f07f42", size = 43043526, upload-time = "2025-10-01T18:03:15.051Z" }, + { url = "https://files.pythonhosted.org/packages/e2/7c/82cbd5c656e8991bcc110c69d05913be2229302a92acb96109e166ae31fb/llvmlite-0.45.1-cp312-cp312-macosx_10_15_x86_64.whl", hash = "sha256:28e763aba92fe9c72296911e040231d486447c01d4f90027c8e893d89d49b20e", size = 43043524, upload-time = "2025-10-01T18:03:30.666Z" }, + { url = "https://files.pythonhosted.org/packages/1d/e2/c185bb7e88514d5025f93c6c4092f6120c6cea8fe938974ec9860fb03bbb/llvmlite-0.45.1-cp313-cp313-macosx_10_15_x86_64.whl", hash = "sha256:d9ea9e6f17569a4253515cc01dade70aba536476e3d750b2e18d81d7e670eb15", size = 43043524, upload-time = "2025-10-01T18:03:43.249Z" }, +] + +[[package]] +name = "llvmlite" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "(python_full_version >= '3.11' and platform_machine != 'x86_64') or (python_full_version >= '3.11' and sys_platform != 'darwin')", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", +] +sdist = { url = "https://files.pythonhosted.org/packages/01/88/a8952b6d5c21e74cbf158515b779666f692846502623e9e3c39d8e8ba25f/llvmlite-0.47.0.tar.gz", hash = "sha256:62031ce968ec74e95092184d4b0e857e444f8fdff0b8f9213707699570c33ccc", size = 193614, upload-time = "2026-03-31T18:29:53.497Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/f5/a1bde3aa8c43524b0acaf3f72fb3d80a32dd29dbb42d7dc434f84584cdcc/llvmlite-0.47.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41270b0b1310717f717cf6f2a9c68d3c43bd7905c33f003825aebc361d0d1b17", size = 37232772, upload-time = "2026-03-31T18:28:12.198Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fb/76d88fc05ee1f9c1a6efe39eb493c4a727e5d1690412469017cd23bcb776/llvmlite-0.47.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9d118bc1dd7623e0e65ca9ac485ec6dd543c3b77bc9928ddc45ebd34e1e30a7", size = 56275179, upload-time = "2026-03-31T18:28:15.725Z" }, + { url = "https://files.pythonhosted.org/packages/4d/08/29da7f36217abd56a0c389ef9a18bea47960826e691ced1a36c92c6ce93c/llvmlite-0.47.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea5cfb04a6ab5b18e46be72b41b015975ba5980c4ddb41f1975b83e19031063", size = 55128632, upload-time = "2026-03-31T18:28:19.946Z" }, + { url = "https://files.pythonhosted.org/packages/df/f8/5e12e9ed447d65f04acf6fcf2d79cded2355640b5131a46cee4c99a5949d/llvmlite-0.47.0-cp310-cp310-win_amd64.whl", hash = "sha256:166b896a2262a2039d5fc52df5ee1659bd1ccd081183df7a2fba1b74702dd5ea", size = 38138402, upload-time = "2026-03-31T18:28:23.327Z" }, + { url = "https://files.pythonhosted.org/packages/34/0b/b9d1911cfefa61399821dfb37f486d83e0f42630a8d12f7194270c417002/llvmlite-0.47.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:74090f0dcfd6f24ebbef3f21f11e38111c4d7e6919b54c4416e1e357c3446b07", size = 37232770, upload-time = "2026-03-31T18:28:26.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/27/5799b020e4cdfb25a7c951c06a96397c135efcdc21b78d853bbd9c814c7d/llvmlite-0.47.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ca14f02e29134e837982497959a8e2193d6035235de1cb41a9cb2bd6da4eedbb", size = 56275177, upload-time = "2026-03-31T18:28:31.01Z" }, + { url = "https://files.pythonhosted.org/packages/7e/51/48a53fedf01cb1f3f43ef200be17ebf83c8d9a04018d3783c1a226c342c2/llvmlite-0.47.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12a69d4bb05f402f30477e21eeabe81911e7c251cecb192bed82cd83c9db10d8", size = 55128631, upload-time = "2026-03-31T18:28:36.046Z" }, + { url = "https://files.pythonhosted.org/packages/a2/50/59227d06bdc96e23322713c381af4e77420949d8cd8a042c79e0043096cc/llvmlite-0.47.0-cp311-cp311-win_amd64.whl", hash = "sha256:c37d6eb7aaabfa83ab9c2ff5b5cdb95a5e6830403937b2c588b7490724e05327", size = 38138400, upload-time = "2026-03-31T18:28:40.076Z" }, + { url = "https://files.pythonhosted.org/packages/fa/48/4b7fe0e34c169fa2f12532916133e0b219d2823b540733651b34fdac509a/llvmlite-0.47.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:306a265f408c259067257a732c8e159284334018b4083a9e35f67d19792b164f", size = 37232769, upload-time = "2026-03-31T18:28:43.735Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4b/e3f2cd17822cf772a4a51a0a8080b0032e6d37b2dbe8cfb724eac4e31c52/llvmlite-0.47.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5853bf26160857c0c2573415ff4efe01c4c651e59e2c55c2a088740acfee51cd", size = 56275178, upload-time = "2026-03-31T18:28:48.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/55/a3b4a543185305a9bdf3d9759d53646ed96e55e7dfd43f53e7a421b8fbae/llvmlite-0.47.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:003bcf7fa579e14db59c1a1e113f93ab8a06b56a4be31c7f08264d1d4072d077", size = 55128632, upload-time = "2026-03-31T18:28:52.901Z" }, + { url = "https://files.pythonhosted.org/packages/2f/f5/d281ae0f79378a5a91f308ea9fdb9f9cc068fddd09629edc0725a5a8fde1/llvmlite-0.47.0-cp312-cp312-win_amd64.whl", hash = "sha256:f3079f25bdc24cd9d27c4b2b5e68f5f60c4fdb7e8ad5ee2b9b006007558f9df7", size = 38138692, upload-time = "2026-03-31T18:28:57.147Z" }, + { url = "https://files.pythonhosted.org/packages/77/6f/4615353e016799f80fa52ccb270a843c413b22361fadda2589b2922fb9b0/llvmlite-0.47.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:a3c6a735d4e1041808434f9d440faa3d78d9b4af2ee64d05a66f351883b6ceec", size = 37232771, upload-time = "2026-03-31T18:29:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/31/b8/69f5565f1a280d032525878a86511eebed0645818492feeb169dfb20ae8e/llvmlite-0.47.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2699a74321189e812d476a43d6d7f652f51811e7b5aad9d9bba842a1c7927acb", size = 56275178, upload-time = "2026-03-31T18:29:05.748Z" }, + { url = "https://files.pythonhosted.org/packages/d6/da/b32cafcb926fb0ce2aa25553bf32cb8764af31438f40e2481df08884c947/llvmlite-0.47.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c6951e2b29930227963e53ee152441f0e14be92e9d4231852102d986c761e40", size = 55128632, upload-time = "2026-03-31T18:29:11.235Z" }, + { url = "https://files.pythonhosted.org/packages/46/9f/4898b44e4042c60fafcb1162dfb7014f6f15b1ec19bf29cfea6bf26df90d/llvmlite-0.47.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2e9adf8698d813a9a5efb2d4370caf344dbc1e145019851fee6a6f319ba760e", size = 38138695, upload-time = "2026-03-31T18:29:15.43Z" }, + { url = "https://files.pythonhosted.org/packages/1c/d4/33c8af00f0bf6f552d74f3a054f648af2c5bc6bece97972f3bfadce4f5ec/llvmlite-0.47.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:de966c626c35c9dff5ae7bf12db25637738d0df83fc370cf793bc94d43d92d14", size = 37232773, upload-time = "2026-03-31T18:29:19.453Z" }, + { url = "https://files.pythonhosted.org/packages/64/1d/a760e993e0c0ba6db38d46b9f48f6c7dceb8ac838824997fb9e25f97bc04/llvmlite-0.47.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ddbccff2aeaff8670368340a158abefc032fe9b3ccf7d9c496639263d00151aa", size = 56275176, upload-time = "2026-03-31T18:29:24.149Z" }, + { url = "https://files.pythonhosted.org/packages/84/3b/e679bc3b29127182a7f4aa2d2e9e5bea42adb93fb840484147d59c236299/llvmlite-0.47.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4a7b778a2e144fc64468fb9bf509ac1226c9813a00b4d7afea5d988c4e22fca", size = 55128631, upload-time = "2026-03-31T18:29:29.536Z" }, + { url = "https://files.pythonhosted.org/packages/be/f7/19e2a09c62809c9e63bbd14ce71fb92c6ff7b7b3045741bb00c781efc3c9/llvmlite-0.47.0-cp314-cp314-win_amd64.whl", hash = "sha256:694e3c2cdc472ed2bd8bd4555ca002eec4310961dd58ef791d508f57b5cc4c94", size = 39153826, upload-time = "2026-03-31T18:29:33.681Z" }, + { url = "https://files.pythonhosted.org/packages/40/a1/581a8c707b5e80efdbbe1dd94527404d33fe50bceb71f39d5a7e11bd57b7/llvmlite-0.47.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:92ec8a169a20b473c1c54d4695e371bde36489fc1efa3688e11e99beba0abf9c", size = 37232772, upload-time = "2026-03-31T18:29:37.952Z" }, + { url = "https://files.pythonhosted.org/packages/11/03/16090dd6f74ba2b8b922276047f15962fbeea0a75d5601607edb301ba945/llvmlite-0.47.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa1cbd800edd3b20bc141521f7fd45a6185a5b84109aa6855134e81397ffe72b", size = 56275178, upload-time = "2026-03-31T18:29:42.58Z" }, + { url = "https://files.pythonhosted.org/packages/f5/cb/0abf1dd4c5286a95ffe0c1d8c67aec06b515894a0dd2ac97f5e27b82ab0b/llvmlite-0.47.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6725179b89f03b17dabe236ff3422cb8291b4c1bf40af152826dfd34e350ae8", size = 55128632, upload-time = "2026-03-31T18:29:46.939Z" }, + { url = "https://files.pythonhosted.org/packages/4f/79/d3bbab197e86e0ff4f9c07122895b66a3e0d024247fcff7f12c473cb36d9/llvmlite-0.47.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6842cf6f707ec4be3d985a385ad03f72b2d724439e118fcbe99b2929964f0453", size = 39153839, upload-time = "2026-03-31T18:29:51.004Z" }, +] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -192,7 +245,8 @@ name = "networkx" version = "3.4.2" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } wheels = [ @@ -217,7 +271,8 @@ name = "numpy" version = "2.2.6" source = { registry = "https://pypi.org/simple" } resolution-markers = [ - "python_full_version < '3.11'", + "python_full_version < '3.11' and platform_machine == 'x86_64' and sys_platform == 'darwin'", + "(python_full_version < '3.11' and platform_machine != 'x86_64') or (python_full_version < '3.11' and sys_platform != 'darwin')", ] sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } wheels = [ @@ -627,9 +682,9 @@ dev = [ [package.metadata.requires-dev] dev = [ { name = "maturin", specifier = "~=1.13.1" }, - { name = "qir-formatter", specifier = "~=0.1" }, + { name = "qir-formatter", specifier = "~=0.2" }, { name = "rich", specifier = "~=14.0" }, - { name = "selene-sim", specifier = "~=0.2.11" }, + { name = "selene-sim", specifier = "~=0.2.15" }, ] [[package]] @@ -647,21 +702,23 @@ wheels = [ [[package]] name = "selene-core" -version = "0.2.6" +version = "0.2.9" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "hugr" }, { name = "lief" }, + { name = "llvmlite", version = "0.45.1", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine == 'x86_64' and sys_platform == 'darwin'" }, + { name = "llvmlite", version = "0.47.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_machine != 'x86_64' or sys_platform != 'darwin'" }, { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "pydantic" }, { name = "pydot" }, { name = "pyyaml" }, - { name = "qir-qis" }, { name = "typing-extensions" }, { name = "ziglang" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/33/28002766e84074564e6b6d9c91912c7882d56cde18dfc324bb183f5b5813/selene_core-0.2.6-py3-none-any.whl", hash = "sha256:1ae54f599bab727673c42316ea5269dd0e9df874dac21f312c2db9c68aad5554", size = 29073, upload-time = "2026-03-02T17:18:28.663Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a9/a94ad38852346869a9c8da037a92cff867b2fe72e9f512bae561a6bc6d23/selene_core-0.2.9-py3-none-any.whl", hash = "sha256:847c78ea393de43e736adf20c3aa7006ae8f96765299a401abbbda6af9af3128", size = 33096, upload-time = "2026-04-28T12:47:34.292Z" }, ] [[package]] @@ -678,7 +735,7 @@ wheels = [ [[package]] name = "selene-sim" -version = "0.2.14" +version = "0.2.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy", version = "2.2.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, @@ -689,11 +746,11 @@ dependencies = [ { name = "tqdm" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/89/2d/5ba388d08e8b28bbda5b01a393800398db936232df9c81011ee7250864bd/selene_sim-0.2.14-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e6e9ace6b2b2fa18922220e532b1462862eeee36a7cd436dd97b0e65b773ecc8", size = 4055016, upload-time = "2026-04-21T23:31:44.374Z" }, - { url = "https://files.pythonhosted.org/packages/7f/fe/5b26a1caad7514d3f36b4df42511869fc995348ddfe9fd851662b513d6ee/selene_sim-0.2.14-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:8d85f7f7b635bdeb390385a96352969e42be6b1a60753ec3f2d9472e4359555e", size = 4212311, upload-time = "2026-04-21T23:31:45.874Z" }, - { url = "https://files.pythonhosted.org/packages/34/48/045b45d4041d88c27185bf0ccc7fb322fe0dd7e0d8ece287c35ccd17b50e/selene_sim-0.2.14-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:1e6c0924841e543c6bdd78e29cab88275e69b3b2b9077224e306bfd3dc3bf2f7", size = 4499101, upload-time = "2026-04-21T23:31:47.499Z" }, - { url = "https://files.pythonhosted.org/packages/b2/e8/dfe81a0860905880336e9b64dfc4f11b400cf3ce0a61f27b8a91d6c75d65/selene_sim-0.2.14-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:624aae5a101d2f638642d977fb4956bdf7e1d0cdcb5cffb918cda5de583cfb63", size = 4672643, upload-time = "2026-04-21T23:31:48.968Z" }, - { url = "https://files.pythonhosted.org/packages/c5/80/e2763921b650208ef1cb86e265a66f59073775d27f4926f040aa82642fae/selene_sim-0.2.14-py3-none-win_amd64.whl", hash = "sha256:4757accd938b7ed413635d5337337314c9bab390d3a4da05538a666fbbce50a2", size = 9602356, upload-time = "2026-04-21T23:31:50.439Z" }, + { url = "https://files.pythonhosted.org/packages/7b/05/76931c2c757b9a4ac44a75c3b8b1e50209dd94716dad8bc91f8a0023c344/selene_sim-0.2.15-py3-none-macosx_11_0_arm64.whl", hash = "sha256:44f301aeb84de22ab4c0d4a2fc0bfa42a035ae66bc2ba3acbcd6f8c67b0a1284", size = 4055479, upload-time = "2026-04-28T13:13:59.353Z" }, + { url = "https://files.pythonhosted.org/packages/40/1c/90d029fe76a22fbd4462039b6dc35b6574d86775d4cf5fd27da760185d73/selene_sim-0.2.15-py3-none-macosx_11_0_x86_64.whl", hash = "sha256:7d5f67ac8c19b3422e653d697eace0c9599b28b82ea1bdc35cc1c3e09f14145b", size = 4212319, upload-time = "2026-04-28T13:14:00.688Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/87d3e225f32b05da5813bfe1ff8bf6c70f60c60919044ec48cae9c6ce625/selene_sim-0.2.15-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2667b07b0d826b770e6381f7d78eda7559487f38e04abe37c5f343b7dbb2703b", size = 4498690, upload-time = "2026-04-28T13:14:02.208Z" }, + { url = "https://files.pythonhosted.org/packages/ec/08/25d10f9c2c410b2d17cb2eb1c63bfe5dc84282e71081cd8e9e2b69db969d/selene_sim-0.2.15-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:aaf5dbee4c988d02f42b18fe3208aaec020743c17faf103e3e749b5459e5d059", size = 4672594, upload-time = "2026-04-28T13:14:03.59Z" }, + { url = "https://files.pythonhosted.org/packages/a8/ed/db54b8a8a23533754d2b4fd7befa38097f82c38e532a6b2dc529db3a7cb6/selene_sim-0.2.15-py3-none-win_amd64.whl", hash = "sha256:564e2ab77eebe4e193fdc373c31f24c650302a00877f3735dc1f5d97718e88eb", size = 9602115, upload-time = "2026-04-28T13:14:05.331Z" }, ] [[package]]