Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions source/compiler/stim_compiler/src/qir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,16 @@ block_c{pauli}_exit:

fn write_declarations(&mut self) {
writeln!(self);
let decls: Vec<String> = self.used_intrinsics.values().cloned().collect();
let mut decls: Vec<String> = self.used_intrinsics.values().cloned().collect();
decls.sort_unstable();
for decl in decls {
writeln!(self, "{decl}");
}
}

fn write_definitions(&mut self) {
let definitions: Vec<String> = self.defined_functions.values().cloned().collect();
let mut definitions: Vec<String> = self.defined_functions.values().cloned().collect();
definitions.sort_unstable();
for definition in definitions {
writeln!(self);
writeln!(self, "{definition}");
Expand All @@ -274,13 +276,11 @@ block_c{pauli}_exit:
"attributes #0 = {{ \"entry_point\" \"output_labeling_schema\" \"qir_profiles\"=\"adaptive_profile\" \"required_num_qubits\"=\"{num_qubits}\" \"required_num_results\"=\"{num_results}\" }}"
);
writeln!(self, "attributes #1 = {{ \"irreversible\" }}");
writeln!(self);
writeln!(self, "; module flags");
writeln!(self);
if self.has_noise_intrinsic {
writeln!(self, "attributes #2 = {{ \"qdk_noise\" }}");
writeln!(self);
}
writeln!(self);
writeln!(self, "; module flags");
writeln!(
self,
"!llvm.module.flags = !{{!0, !1, !2, !3, !4, !5, !6, !7}}"
Expand Down
159 changes: 117 additions & 42 deletions source/compiler/stim_compiler/src/qir/tests.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

mod boilerplate;
mod collapsing_gates;
mod collapsing_gates_broadcasting;
mod generalized_pauli_product_gates;
Expand All @@ -19,61 +20,135 @@ mod two_qubit_gates;
mod two_qubit_gates_broadcasting;
mod unsupported_instructions;

use expect_test::{Expect, expect};
use expect_test::Expect;
use indoc::formatdoc;
use qdk_simulators::noise_config::NoiseConfig;

use crate::format_stim_errors;

// QIR boilerplate omitted from most snapshots is covered by dedicated tests.
const BOILERPLATE_BODY_LINES: [&str; 2] = [
"call void @__quantum__rt__initialize(ptr null)",
"ret i64 0",
];
const BOILERPLATE_OUTPUT_CALLS: [&str; 2] = [
"call void @__quantum__rt__array_record_output(",
"call void @__quantum__rt__result_record_output(",
];
const BOILERPLATE_DECLARATIONS: [&str; 3] = [
"declare void @__quantum__rt__array_record_output(i64, ptr)",
"declare void @__quantum__rt__initialize(ptr)",
"declare void @__quantum__rt__result_record_output(ptr, ptr)",
];

// This formats the QIR output to only include parts that aren't boilerplate,
// and to make it easier to read and compare in tests. It also includes the
// noise configuration, if noise instructions were used.
fn format_qir(qir: &str, noise: &NoiseConfig<f64, f64>) -> String {
let (body, remainder) = qir
.strip_prefix("define i64 @ENTRYPOINT__main() #0 {\n")
.and_then(|qir| qir.split_once("\n}\n"))
.expect("QIR boilerplate should contain the entry-point definition");

let definitions = remainder
.split_once("declare ")
.map(|(definitions, _)| definitions.trim())
.expect("QIR boilerplate should contain declarations");

let required_num_qubits = extract_parameter(qir, "required_num_qubits");
let required_num_results = extract_parameter(qir, "required_num_results");

let body = indent_lines(body.lines().filter(|line| {
let line = line.trim_start();
!BOILERPLATE_BODY_LINES.contains(&line)
&& !BOILERPLATE_OUTPUT_CALLS
.iter()
.any(|call| line.starts_with(call))
}));
let body = if body.is_empty() {
String::new()
} else {
format!("body:\n{body}\n\n")
};

let mut declarations = qir
.lines()
.filter(|line| line.starts_with("declare "))
.filter(|line| !BOILERPLATE_DECLARATIONS.contains(line))
.collect::<Vec<_>>();
declarations.sort_unstable();
let declarations = indent_lines(declarations);
let declarations = if declarations.is_empty() {
String::new()
} else {
format!("declarations:\n{declarations}\n\n")
};

let definitions = if definitions.is_empty() {
String::new()
} else {
format!("definitions:\n{}\n\n", indent_lines(definitions.lines()))
};

let uses_noise = if qir.contains("\"qdk_noise\"") {
"uses_noise: true\n"
} else {
""
};

let noise_config = if noise.is_noiseless() {
String::new()
} else {
noise.to_string()
};

formatdoc! {"
{body}\
{definitions}\
{declarations}\
required_num_qubits: {required_num_qubits}
required_num_results: {required_num_results}
{uses_noise}
{noise_config}
"}
.trim_end()
.to_string()
}

fn extract_parameter(qir: &str, name: &str) -> String {
let marker = format!("\"{name}\"=\"");
qir.split_once(&marker)
.and_then(|(_, value)| value.split_once('"'))
.map(|(value, _)| value.to_string())
.unwrap_or_else(|| panic!("QIR boilerplate should contain the {name} parameter"))
}

fn indent_lines<'a>(lines: impl IntoIterator<Item = &'a str>) -> String {
lines
.into_iter()
.map(|line| {
if line.is_empty() {
String::new()
} else {
format!(" {line}")
}
})
.collect::<Vec<_>>()
.join("\n")
}

/// Check that a stim source compiles to the
/// expected QIR or yields the expected errors.
/// expected formatted QIR or yields the expected errors.
fn check(source: &str, expect: &Expect) {
let mut noise = NoiseConfig::NOISELESS;
match crate::compile(source, &mut noise) {
Ok(qir) => {
let actual = if noise.is_noiseless() {
qir
} else {
noise.to_string() + "\n" + &qir
};
expect.assert_eq(&actual);
let formatted_qir = format_qir(&qir, &noise);
expect.assert_eq(&formatted_qir);
}
Err(errors) => {
let errors = format_stim_errors(errors);
expect.assert_eq(&errors);
}
}
}

#[test]
fn empty_src() {
check(
"",
&expect![[r#"
define i64 @ENTRYPOINT__main() #0 {
call void @__quantum__rt__initialize(ptr null)
call void @__quantum__rt__array_record_output(i64 0, ptr null)
ret i64 0
}

declare void @__quantum__rt__array_record_output(i64, ptr)
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__rt__result_record_output(ptr, ptr)

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" }
attributes #1 = { "irreversible" }

; module flags

!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}

!0 = !{i32 1, !"qir_major_version", i32 2}
!1 = !{i32 7, !"qir_minor_version", i32 1}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 5, !"int_computations", !{!"i64"}}
!5 = !{i32 5, !"float_computations", !{!"double"}}
!6 = !{i32 7, !"backwards_branching", i2 3}
!7 = !{i32 1, !"arrays", i1 true}
"#]],
);
}
131 changes: 131 additions & 0 deletions source/compiler/stim_compiler/src/qir/tests/boilerplate.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use expect_test::{Expect, expect};
use qdk_simulators::noise_config::NoiseConfig;

use crate::format_stim_errors;

fn check(source: &str, expect: &Expect) {
let mut noise = NoiseConfig::NOISELESS;
match crate::compile(source, &mut noise) {
Ok(qir) => expect.assert_eq(&qir),
Err(errors) => {
let errors = format_stim_errors(errors);
expect.assert_eq(&errors);
}
}
}

// Check that the QIR boilerplate is generally correct
#[test]
fn empty_src() {
check(
"",
&expect![[r#"
define i64 @ENTRYPOINT__main() #0 {
call void @__quantum__rt__initialize(ptr null)
call void @__quantum__rt__array_record_output(i64 0, ptr null)
ret i64 0
}

declare void @__quantum__rt__array_record_output(i64, ptr)
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__rt__result_record_output(ptr, ptr)

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="0" "required_num_results"="0" }
attributes #1 = { "irreversible" }

; module flags
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}

!0 = !{i32 1, !"qir_major_version", i32 2}
!1 = !{i32 7, !"qir_minor_version", i32 1}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 5, !"int_computations", !{!"i64"}}
!5 = !{i32 5, !"float_computations", !{!"double"}}
!6 = !{i32 7, !"backwards_branching", i2 3}
!7 = !{i32 1, !"arrays", i1 true}
"#]],
);
}

// Checks the result_record_output calls are correctly generated
#[test]
fn measured_qir_has_expected_boilerplate() {
check(
"M 0 1",
&expect![[r#"
define i64 @ENTRYPOINT__main() #0 {
call void @__quantum__rt__initialize(ptr null)
call void @__quantum__qis__m__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))
call void @__quantum__qis__m__body(ptr inttoptr (i64 1 to ptr), ptr inttoptr (i64 1 to ptr))
call void @__quantum__rt__array_record_output(i64 2, ptr null)
call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)
call void @__quantum__rt__result_record_output(ptr inttoptr (i64 1 to ptr), ptr null)
ret i64 0
}

declare void @__quantum__qis__m__body(ptr, ptr)
declare void @__quantum__rt__array_record_output(i64, ptr)
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__rt__result_record_output(ptr, ptr)

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="2" "required_num_results"="2" }
attributes #1 = { "irreversible" }

; module flags
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}

!0 = !{i32 1, !"qir_major_version", i32 2}
!1 = !{i32 7, !"qir_minor_version", i32 1}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 5, !"int_computations", !{!"i64"}}
!5 = !{i32 5, !"float_computations", !{!"double"}}
!6 = !{i32 7, !"backwards_branching", i2 3}
!7 = !{i32 1, !"arrays", i1 true}
"#]],
);
}

// Checks that the qdk_noise attribute is correctly generated whenever there is noise.
#[test]
fn noisy_qir_has_expected_boilerplate() {
check(
"M(0.1) 0",
&expect![[r#"
define i64 @ENTRYPOINT__main() #0 {
call void @__quantum__rt__initialize(ptr null)
call void @__quantum__qis__m__body(ptr inttoptr (i64 0 to ptr), ptr inttoptr (i64 0 to ptr))
call void @__quantum__rt__readout_noise(double 0.1, double 0.1, ptr inttoptr (i64 0 to ptr))
call void @__quantum__rt__array_record_output(i64 1, ptr null)
call void @__quantum__rt__result_record_output(ptr inttoptr (i64 0 to ptr), ptr null)
ret i64 0
}

declare void @__quantum__qis__m__body(ptr, ptr)
declare void @__quantum__rt__array_record_output(i64, ptr)
declare void @__quantum__rt__initialize(ptr)
declare void @__quantum__rt__readout_noise(double, double, ptr) #2
declare void @__quantum__rt__result_record_output(ptr, ptr)

attributes #0 = { "entry_point" "output_labeling_schema" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="1" }
attributes #1 = { "irreversible" }
attributes #2 = { "qdk_noise" }

; module flags
!llvm.module.flags = !{!0, !1, !2, !3, !4, !5, !6, !7}

!0 = !{i32 1, !"qir_major_version", i32 2}
!1 = !{i32 7, !"qir_minor_version", i32 1}
!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
!3 = !{i32 1, !"dynamic_result_management", i1 false}
!4 = !{i32 5, !"int_computations", !{!"i64"}}
!5 = !{i32 5, !"float_computations", !{!"double"}}
!6 = !{i32 7, !"backwards_branching", i2 3}
!7 = !{i32 1, !"arrays", i1 true}
"#]],
);
}
Loading