From b55486e5f099242e247a43dd81e713f90dc2c24d Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Mon, 4 May 2026 08:08:26 -0500 Subject: [PATCH 1/3] test: cover dynamic validation mutation survivors --- AGENTS.md | 3 + src/lib.rs | 235 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 238 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 5e6f7cd..a787de7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +42,9 @@ Rule of thumb: - Prefer structure-aware fuzzing over arbitrary raw byte mutation when the goal is to exercise `qir-qis` logic rather than LLVM parser failure paths. - If expensive fixture compilation is repeated across many tests, cache it with `LazyLock` or similar. - Keep `make mutants` useful: kill meaningful mutants with tests, and keep `.cargo/mutants.toml` exclusions resilient to line movement. +- When adding or changing validation for external/runtime function signatures, add table-driven negative tests for each accepted function family, covering return type, arity, and each parameter kind or width. +- For numeric limits, test both the first rejected value and the largest accepted boundary value. +- During iteration on validation helpers, run a scoped mutation check for the changed helper, such as `cargo mutants --package qir-qis --all-features --test-tool cargo --file src/lib.rs --re ''`. ## LLVM and platform guidance diff --git a/src/lib.rs b/src/lib.rs index 07ee02a..4b372d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5274,6 +5274,214 @@ attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeli ); } + #[test] + fn test_validate_qir_rejects_malformed_dynamic_allocate_return_signature() { + let cases = [ + ( + "declare void @__quantum__rt__qubit_allocate(ptr)", + "__quantum__rt__qubit_allocate", + ), + ( + "declare void @__quantum__rt__result_allocate(ptr)", + "__quantum__rt__result_allocate", + ), + ]; + + for (declaration, fn_name) in cases { + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +{declaration} + +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 true}} +!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(&format!("{fn_name} should fail validation")); + assert!( + err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), + "reported unexpected error: {err}" + ); + } + } + + #[test] + fn test_validate_qir_rejects_malformed_dynamic_release_signatures() { + let cases = [ + ( + "wrong return", + "declare ptr @__quantum__rt__qubit_release(ptr)", + "__quantum__rt__qubit_release", + ), + ( + "wrong arity", + "declare void @__quantum__rt__qubit_release()", + "__quantum__rt__qubit_release", + ), + ( + "wrong param type", + "declare void @__quantum__rt__result_release(i64)", + "__quantum__rt__result_release", + ), + ]; + + for (case_name, declaration, fn_name) in cases { + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +{declaration} + +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 true}} +!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(&format!("{case_name} should fail validation")); + assert!( + err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), + "{case_name} reported unexpected error: {err}" + ); + } + } + + #[test] + fn test_validate_qir_rejects_malformed_dynamic_array_three_arg_signatures() { + let cases = [ + ( + "wrong return", + "declare ptr @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)", + "__quantum__rt__qubit_array_allocate", + ), + ( + "wrong arity", + "declare void @__quantum__rt__qubit_array_allocate(i64, ptr)", + "__quantum__rt__qubit_array_allocate", + ), + ( + "wrong length type", + "declare void @__quantum__rt__result_array_allocate(i32, ptr, ptr)", + "__quantum__rt__result_array_allocate", + ), + ( + "wrong backing param type", + "declare void @__quantum__rt__result_array_allocate(i64, i64, ptr)", + "__quantum__rt__result_array_allocate", + ), + ( + "wrong output param type", + "declare void @__quantum__rt__result_array_record_output(i64, ptr, i64)", + "__quantum__rt__result_array_record_output", + ), + ]; + + for (case_name, declaration, fn_name) in cases { + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +{declaration} + +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 true}} +!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(&format!("{case_name} should fail validation")); + assert!( + err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), + "{case_name} reported unexpected error: {err}" + ); + } + } + + #[test] + fn test_validate_qir_rejects_malformed_dynamic_array_release_signatures() { + let cases = [ + ( + "wrong return", + "declare ptr @__quantum__rt__qubit_array_release(i64, ptr)", + "__quantum__rt__qubit_array_release", + ), + ( + "wrong arity", + "declare void @__quantum__rt__qubit_array_release(i64)", + "__quantum__rt__qubit_array_release", + ), + ( + "wrong length type", + "declare void @__quantum__rt__result_array_release(i32, ptr)", + "__quantum__rt__result_array_release", + ), + ( + "wrong backing param type", + "declare void @__quantum__rt__result_array_release(i64, i64)", + "__quantum__rt__result_array_release", + ), + ]; + + for (case_name, declaration, fn_name) in cases { + let ll_text = format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +{declaration} + +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 true}} +!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(&format!("{case_name} should fail validation")); + assert!( + err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), + "{case_name} reported unexpected error: {err}" + ); + } + } + #[test] fn test_validate_qir_rejects_unsupported_rt_function_declaration() { let ll_text = r#" @@ -5485,6 +5693,33 @@ attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeli )); } + #[test] + fn test_validate_dynamic_result_array_record_output_i32_max_length_succeeds() { + let ll_text = r#" +@0 = internal constant [3 x i8] c"a0\00" + +define i64 @Entry_Point_Name() #0 { +entry: + %results = alloca [2147483647 x ptr], align 8 + call void @__quantum__rt__result_array_record_output(i64 2147483647, ptr %results, ptr @0) + ret i64 0 +} + +declare void @__quantum__rt__result_array_record_output(i64, ptr, 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(); + validate_qir(&bc_bytes, None).expect("i32::MAX result array output should validate"); + } + #[test] fn test_validate_dynamic_result_allocate_outside_entry_block_fails() { let ll_text = r#" From 106599b0599da49bc537d2459ee1544a8738f8e7 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Mon, 4 May 2026 08:18:09 -0500 Subject: [PATCH 2/3] test: deduplicate dynamic signature checks --- AGENTS.md | 5 +- src/lib.rs | 141 ++++++++++++++--------------------------------------- 2 files changed, 39 insertions(+), 107 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index a787de7..dcae934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,9 +42,8 @@ Rule of thumb: - Prefer structure-aware fuzzing over arbitrary raw byte mutation when the goal is to exercise `qir-qis` logic rather than LLVM parser failure paths. - If expensive fixture compilation is repeated across many tests, cache it with `LazyLock` or similar. - Keep `make mutants` useful: kill meaningful mutants with tests, and keep `.cargo/mutants.toml` exclusions resilient to line movement. -- When adding or changing validation for external/runtime function signatures, add table-driven negative tests for each accepted function family, covering return type, arity, and each parameter kind or width. -- For numeric limits, test both the first rejected value and the largest accepted boundary value. -- During iteration on validation helpers, run a scoped mutation check for the changed helper, such as `cargo mutants --package qir-qis --all-features --test-tool cargo --file src/lib.rs --re ''`. +- For external/runtime signature validation, prefer table-driven negative tests that cover return type, arity, and parameter kind/width; for numeric limits, cover both the largest accepted value and first rejected value. +- During validation-helper work, run a scoped mutation check such as `cargo mutants --package qir-qis --all-features --test-tool cargo --file src/lib.rs --re ''`. ## LLVM and platform guidance diff --git a/src/lib.rs b/src/lib.rs index 4b372d2..a43797c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3846,6 +3846,39 @@ attributes #0 = {{ {rendered_attrs} }} ) } + fn minimal_dynamic_rt_declaration_qir(declaration: &str) -> String { + format!( + r#" +define i64 @Entry_Point_Name() #0 {{ +entry: + ret i64 0 +}} + +{declaration} + +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 true}} +!3 = !{{i32 1, !"dynamic_result_management", i1 true}} +!4 = !{{i32 1, !"arrays", i1 true}} +"# + ) + } + + fn assert_malformed_dynamic_rt_declaration(case_name: &str, declaration: &str, fn_name: &str) { + let ll_text = minimal_dynamic_rt_declaration_qir(declaration); + let bc_bytes = qir_ll_to_bc(&ll_text).unwrap(); + let err = validate_qir(&bc_bytes, None) + .expect_err(&format!("{case_name} should fail validation")); + assert!( + err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), + "{case_name} reported unexpected error: {err}" + ); + } + fn minimal_qir_with_duplicate_major_flags(first_major: &str, second_major: &str) -> String { format!( r#" @@ -5288,32 +5321,7 @@ attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeli ]; for (declaration, fn_name) in cases { - let ll_text = format!( - r#" -define i64 @Entry_Point_Name() #0 {{ -entry: - ret i64 0 -}} - -{declaration} - -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 true}} -!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(&format!("{fn_name} should fail validation")); - assert!( - err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), - "reported unexpected error: {err}" - ); + assert_malformed_dynamic_rt_declaration(fn_name, declaration, fn_name); } } @@ -5338,32 +5346,7 @@ attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_label ]; for (case_name, declaration, fn_name) in cases { - let ll_text = format!( - r#" -define i64 @Entry_Point_Name() #0 {{ -entry: - ret i64 0 -}} - -{declaration} - -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 true}} -!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(&format!("{case_name} should fail validation")); - assert!( - err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), - "{case_name} reported unexpected error: {err}" - ); + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); } } @@ -5398,32 +5381,7 @@ attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_label ]; for (case_name, declaration, fn_name) in cases { - let ll_text = format!( - r#" -define i64 @Entry_Point_Name() #0 {{ -entry: - ret i64 0 -}} - -{declaration} - -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 true}} -!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(&format!("{case_name} should fail validation")); - assert!( - err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), - "{case_name} reported unexpected error: {err}" - ); + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); } } @@ -5453,32 +5411,7 @@ attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_label ]; for (case_name, declaration, fn_name) in cases { - let ll_text = format!( - r#" -define i64 @Entry_Point_Name() #0 {{ -entry: - ret i64 0 -}} - -{declaration} - -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 true}} -!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(&format!("{case_name} should fail validation")); - assert!( - err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")), - "{case_name} reported unexpected error: {err}" - ); + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); } } From cf8b5a20c7fc1cbc668bea565c7bbd4395756631 Mon Sep 17 00:00:00 2001 From: Kartik Singhal Date: Mon, 4 May 2026 08:43:41 -0500 Subject: [PATCH 3/3] ci: install nextest directly on windows --- .github/workflows/CI.yml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index b6e98d8..c53b13b 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -66,12 +66,6 @@ jobs: echo "RUSTFLAGS=-D warnings -C link-arg=-fuse-ld=lld" >>"$GITHUB_ENV" - uses: dtolnay/rust-toolchain@stable - uses: taiki-e/install-action@nextest - if: runner.os != 'Windows' - - uses: cargo-bins/cargo-binstall@v1.17.7 - if: runner.os == 'Windows' - - name: Install cargo-nextest - if: runner.os == 'Windows' - run: cargo binstall --no-confirm --force cargo-nextest - uses: Swatinem/rust-cache@v2 with: shared-key: ci-rust-${{ runner.os }}-${{ env.LLVM_RUST_CACHE_FAMILY }}