Skip to content

Commit 038a613

Browse files
authored
Better UX and error display for circuits (#1318)
This improves the circuit UX and the handling of various known error cases. ## Circuit ![image](https://github.com/microsoft/qsharp/assets/16928427/c4057853-fe1a-4e98-a2df-e843118a104f) ## Operation circuit ![Screenshot 2024-03-26 151611](https://github.com/microsoft/qsharp/assets/16928427/1c8aaf75-c22d-4ed2-b2cc-dacb0a3b9249) ## Unsupported due to result comparison ![Screenshot 2024-03-26 151551](https://github.com/microsoft/qsharp/assets/16928427/27ac98a5-04d6-4bf3-aaa6-2fb2fafba528) ## Runtime failure ![Screenshot 2024-03-26 152053](https://github.com/microsoft/qsharp/assets/16928427/bc2e75d3-049c-4b2e-9977-2bc27be7804a) ## Compiler error ![Screenshot 2024-03-26 152033](https://github.com/microsoft/qsharp/assets/16928427/5dc2dbe5-c81c-4a0a-ad33-d4e753b146be) ## Too much circuit ![Screenshot 2024-03-26 152020](https://github.com/microsoft/qsharp/assets/16928427/86a7d715-d0c5-414f-9987-d5a2e8b2610e) ## Not enough circuit ![Screenshot 2024-03-26 151736](https://github.com/microsoft/qsharp/assets/16928427/1b74b418-da97-40cc-8cc1-23bd6898b1cd)
1 parent 513d078 commit 038a613

File tree

13 files changed

+276
-130
lines changed

13 files changed

+276
-130
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

compiler/qsc/src/interpret.rs

Lines changed: 44 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ use qsc_circuit::{
3939
};
4040
use qsc_codegen::qir_base::BaseProfSim;
4141
use qsc_data_structures::{
42+
functors::FunctorApp,
4243
language_features::LanguageFeatures,
4344
line_column::{Encoding, Range},
4445
span::Span,
@@ -83,18 +84,19 @@ pub enum Error {
8384
#[error("runtime error")]
8485
#[diagnostic(transparent)]
8586
Eval(#[from] WithStack<WithSource<qsc_eval::Error>>),
87+
#[error("circuit error")]
88+
#[diagnostic(transparent)]
89+
Circuit(#[from] qsc_circuit::Error),
8690
#[error("entry point not found")]
8791
#[diagnostic(code("Qsc.Interpret.NoEntryPoint"))]
8892
NoEntryPoint,
8993
#[error("unsupported runtime capabilities for code generation")]
9094
#[diagnostic(code("Qsc.Interpret.UnsupportedRuntimeCapabilities"))]
9195
UnsupportedRuntimeCapabilities,
92-
#[error("expression does not evaluate to an operation that takes qubit parameters")]
93-
#[diagnostic(code("Qsc.Interpret.NoCircuitForOperation"))]
94-
#[diagnostic(help(
95-
"provide the name of a callable or a lambda expression that only takes qubits as parameters"
96-
))]
97-
NoCircuitForOperation,
96+
#[error("expression does not evaluate to an operation")]
97+
#[diagnostic(code("Qsc.Interpret.NotAnOperation"))]
98+
#[diagnostic(help("provide the name of a callable or a lambda expression"))]
99+
NotAnOperation,
98100
}
99101

100102
/// A Q# interpreter.
@@ -338,52 +340,22 @@ impl Interpreter {
338340

339341
let entry_expr = match entry {
340342
CircuitEntryPoint::Operation(operation_expr) => {
341-
// To determine whether the passed in expression is a valid callable name
342-
// or lambda, we evaluate it and inspect the runtime value.
343-
let maybe_operation = match self.eval_fragments(&mut out, &operation_expr)? {
344-
Value::Closure(b) => Some((b.id, b.functor)),
345-
Value::Global(item_id, functor_app) => Some((item_id, functor_app)),
346-
_ => None,
347-
};
348-
349-
let maybe_invoke_expr = if let Some((item_id, functor_app)) = maybe_operation {
350-
// Controlled operations are not supported at the moment.
351-
if functor_app.controlled > 0 {
352-
return Err(vec![Error::NoCircuitForOperation]);
353-
}
354-
355-
// Find the item in the HIR
356-
let package = map_fir_package_to_hir(item_id.package);
357-
let local_item_id = crate::hir::LocalItemId::from(usize::from(item_id.item));
358-
let package_store = self.compiler.package_store();
359-
360-
let item = package_store
361-
.get(package)
362-
.and_then(|unit| unit.package.items.get(local_item_id));
363-
364-
// Generate the entry expression to invoke the operation.
365-
// Will return `None` if item is not a valid callable that takes qubits.
366-
item.and_then(|item| entry_expr_for_qubit_operation(item, &operation_expr))
367-
} else {
368-
return Err(vec![Error::NoCircuitForOperation]);
369-
};
370-
371-
if maybe_invoke_expr.is_none() {
372-
return Err(vec![Error::NoCircuitForOperation]);
373-
}
374-
maybe_invoke_expr
343+
let (item, functor_app) = self.eval_to_operation(&operation_expr)?;
344+
let expr = entry_expr_for_qubit_operation(item, functor_app, &operation_expr)
345+
.map_err(|e| vec![e.into()])?;
346+
Some(expr)
375347
}
376348
CircuitEntryPoint::EntryExpr(expr) => Some(expr),
377349
CircuitEntryPoint::EntryPoint => None,
378350
};
379351

380-
let val = if let Some(entry_expr) = entry_expr {
352+
if let Some(entry_expr) = entry_expr {
381353
self.run_with_sim(&mut sim, &mut out, &entry_expr)?
382354
} else {
383355
self.eval_entry_with_sim(&mut sim, &mut out)
384356
}?;
385357

386-
Ok(sim.finish(&val))
358+
Ok(sim.finish())
387359
}
388360

389361
/// Runs the given entry expression on the given simulator with a new instance of the environment
@@ -459,6 +431,36 @@ impl Interpreter {
459431
self.lines += 1;
460432
label
461433
}
434+
435+
/// Evaluate the name of an operation, or any expression that evaluates to a callable,
436+
/// and return the Item ID and function application for the callable.
437+
/// Examples: "Microsoft.Quantum.Diagnostics.DumpMachine", "(qs: Qubit[]) => H(qs[0])",
438+
/// "Controlled SWAP"
439+
fn eval_to_operation(
440+
&mut self,
441+
operation_expr: &str,
442+
) -> std::result::Result<(&qsc_hir::hir::Item, FunctorApp), Vec<Error>> {
443+
let mut sink = std::io::sink();
444+
let mut out = GenericReceiver::new(&mut sink);
445+
let (store_item_id, functor_app) = match self.eval_fragments(&mut out, operation_expr)? {
446+
Value::Closure(b) => (b.id, b.functor),
447+
Value::Global(item_id, functor_app) => (item_id, functor_app),
448+
_ => return Err(vec![Error::NotAnOperation]),
449+
};
450+
let package = map_fir_package_to_hir(store_item_id.package);
451+
let local_item_id = crate::hir::LocalItemId::from(usize::from(store_item_id.item));
452+
let unit = self
453+
.compiler
454+
.package_store()
455+
.get(package)
456+
.expect("package should exist in the package store");
457+
let item = unit
458+
.package
459+
.items
460+
.get(local_item_id)
461+
.expect("item should exist in the package");
462+
Ok((item, functor_app))
463+
}
462464
}
463465

464466
/// Describes the entry point for circuit generation.

compiler/qsc/src/interpret/circuit_tests.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -656,7 +656,9 @@ fn controlled_operation() {
656656
// We don't generate an accurate call signature with the tuple arguments.
657657
expect![[r"
658658
[
659-
NoCircuitForOperation,
659+
Circuit(
660+
ControlledUnsupported,
661+
),
660662
]
661663
"]]
662664
.assert_debug_eq(&circ_err);
@@ -734,7 +736,9 @@ fn operation_with_non_qubit_args() {
734736

735737
expect![[r"
736738
[
737-
NoCircuitForOperation,
739+
Circuit(
740+
NoQubitParameters,
741+
),
738742
]
739743
"]]
740744
.assert_debug_eq(&circ_err);

compiler/qsc_circuit/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ license.workspace = true
1010

1111
[dependencies]
1212
log = { workspace = true }
13+
miette = { workspace = true }
1314
num-bigint = { workspace = true }
1415
num-complex = { workspace = true }
1516
qsc_codegen = { path = "../qsc_codegen" }
@@ -21,6 +22,7 @@ qsc_hir = { path = "../qsc_hir" }
2122
rustc-hash = { workspace = true }
2223
serde = { workspace = true, features = ["derive"] }
2324
serde_json = { workspace = true }
25+
thiserror = { workspace = true }
2426

2527
[dev-dependencies]
2628
expect-test = { workspace = true }

compiler/qsc_circuit/src/builder.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ impl Builder {
227227
}
228228

229229
#[must_use]
230-
pub fn finish(mut self, _val: &Value) -> Circuit {
230+
pub fn finish(mut self) -> Circuit {
231231
let circuit = take(&mut self.circuit);
232232
self.finish_circuit(circuit)
233233
}

compiler/qsc_circuit/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ pub mod operations;
77

88
pub use builder::Builder;
99
pub use circuit::{Circuit, Config, Operation};
10+
pub use operations::Error;

compiler/qsc_circuit/src/operations.rs

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,28 @@
44
#[cfg(test)]
55
mod tests;
66

7+
use miette::Diagnostic;
78
use qsc_hir::{
89
hir::{Item, ItemKind},
910
ty::{Prim, Ty},
1011
};
12+
use thiserror::Error;
13+
14+
#[derive(Clone, Debug, Diagnostic, Error)]
15+
pub enum Error {
16+
#[error("expression does not evaluate to an operation that takes qubit parameters")]
17+
#[diagnostic(code("Qsc.Circuit.NoCircuitForOperation"))]
18+
#[diagnostic(help(
19+
"provide the name of a callable or a lambda expression that only takes qubits as parameters"
20+
))]
21+
NoQubitParameters,
22+
#[error("cannot generate circuit for controlled invocation")]
23+
#[diagnostic(code("Qsc.Circuit.ControlledUnsupported"))]
24+
#[diagnostic(help(
25+
"controlled invocations are not currently supported. consider wrapping the invocation in a lambda expression"
26+
))]
27+
ControlledUnsupported,
28+
}
1129

1230
/// If the item is a callable, returns the information that would
1331
/// be needed to generate a circuit for it.
@@ -46,16 +64,24 @@ pub fn qubit_param_info(item: &Item) -> Option<(Vec<u32>, u32)> {
4664
///
4765
/// If the item is not a callable, returns `None`.
4866
/// If the callable takes any non-qubit parameters, returns `None`.
49-
#[must_use]
50-
pub fn entry_expr_for_qubit_operation(item: &Item, operation_expr: &str) -> Option<String> {
67+
pub fn entry_expr_for_qubit_operation(
68+
item: &Item,
69+
functor_app: qsc_data_structures::functors::FunctorApp,
70+
operation_expr: &str,
71+
) -> Result<String, Error> {
72+
if functor_app.controlled > 0 {
73+
return Err(Error::ControlledUnsupported);
74+
}
75+
5176
if let Some((qubit_param_dimensions, total_num_qubits)) = qubit_param_info(item) {
52-
return Some(operation_circuit_entry_expr(
77+
return Ok(operation_circuit_entry_expr(
5378
operation_expr,
5479
&qubit_param_dimensions,
5580
total_num_qubits,
5681
));
5782
}
58-
None
83+
84+
Err(Error::NoQubitParameters)
5985
}
6086

6187
/// Generates the entry expression to call the operation described by `params`.

compiler/qsc_circuit/src/operations/tests.rs

Lines changed: 23 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use super::*;
55
use expect_test::expect;
6-
use qsc_data_structures::language_features::LanguageFeatures;
6+
use qsc_data_structures::{functors::FunctorApp, language_features::LanguageFeatures};
77
use qsc_frontend::compile::{compile, core, std, PackageStore, RuntimeCapabilityFlags, SourceMap};
88
use qsc_hir::hir::{Item, ItemKind};
99

@@ -58,10 +58,12 @@ fn no_params() {
5858
}
5959
",
6060
);
61-
let expr = entry_expr_for_qubit_operation(&item, &operation);
62-
expect![[r"
63-
None
64-
"]]
61+
let expr = entry_expr_for_qubit_operation(&item, FunctorApp::default(), &operation);
62+
expect![[r#"
63+
Err(
64+
NoQubitParameters,
65+
)
66+
"#]]
6567
.assert_debug_eq(&expr);
6668
}
6769

@@ -75,10 +77,12 @@ fn non_qubit_params() {
7577
}
7678
",
7779
);
78-
let expr = entry_expr_for_qubit_operation(&item, &operation);
79-
expect![[r"
80-
None
81-
"]]
80+
let expr = entry_expr_for_qubit_operation(&item, FunctorApp::default(), &operation);
81+
expect![[r#"
82+
Err(
83+
NoQubitParameters,
84+
)
85+
"#]]
8286
.assert_debug_eq(&expr);
8387
}
8488

@@ -92,10 +96,12 @@ fn non_qubit_array_param() {
9296
}
9397
",
9498
);
95-
let expr = entry_expr_for_qubit_operation(&item, &operation);
96-
expect![[r"
97-
None
98-
"]]
99+
let expr = entry_expr_for_qubit_operation(&item, FunctorApp::default(), &operation);
100+
expect![[r#"
101+
Err(
102+
NoQubitParameters,
103+
)
104+
"#]]
99105
.assert_debug_eq(&expr);
100106
}
101107

@@ -110,7 +116,8 @@ fn qubit_params() {
110116
",
111117
);
112118

113-
let expr = entry_expr_for_qubit_operation(&item, &operation).expect("expression expected");
119+
let expr = entry_expr_for_qubit_operation(&item, FunctorApp::default(), &operation)
120+
.expect("expression expected");
114121

115122
expect![[r"
116123
{
@@ -133,7 +140,8 @@ fn qubit_array_params() {
133140
",
134141
);
135142

136-
let expr = entry_expr_for_qubit_operation(&item, &operation).expect("expression expected");
143+
let expr = entry_expr_for_qubit_operation(&item, FunctorApp::default(), &operation)
144+
.expect("expression expected");
137145

138146
expect![[r"
139147
{

compiler/qsc_eval/src/lib.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,8 +115,9 @@ pub enum Error {
115115
#[diagnostic(code("Qsc.Eval.ReleasedQubitNotZero"))]
116116
ReleasedQubitNotZero(usize, #[label("Qubit{0}")] PackageSpan),
117117

118-
#[error("Result comparison is unsupported for this backend")]
118+
#[error("cannot compare measurement results")]
119119
#[diagnostic(code("Qsc.Eval.ResultComparisonUnsupported"))]
120+
#[diagnostic(help("comparing measurement results is not supported when performing circuit synthesis or base profile QIR generation"))]
120121
ResultComparisonUnsupported(#[label("cannot compare to result")] PackageSpan),
121122

122123
#[error("name is not bound")]

0 commit comments

Comments
 (0)