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 }} diff --git a/AGENTS.md b/AGENTS.md index 5e6f7cd..dcae934 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -42,6 +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. +- 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 07ee02a..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#" @@ -5274,6 +5307,114 @@ 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 { + assert_malformed_dynamic_rt_declaration(fn_name, declaration, fn_name); + } + } + + #[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 { + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); + } + } + + #[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 { + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); + } + } + + #[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 { + assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name); + } + } + #[test] fn test_validate_qir_rejects_unsupported_rt_function_declaration() { let ll_text = r#" @@ -5485,6 +5626,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#"