diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs index 7d8fa92029..8ced56d973 100644 --- a/compiler/qsc_eval/src/intrinsic/tests.rs +++ b/compiler/qsc_eval/src/intrinsic/tests.rs @@ -1502,3 +1502,51 @@ fn two_qubit_rotation_neg_inf_error() { &expect!["invalid rotation angle: -inf"], ); } + +#[test] +fn stop_counting_operation_before_start_fails() { + check_intrinsic_output( + "", + indoc! {"{ + Std.Diagnostics.StopCountingOperation(I); + }"}, + &expect!["callable not counted"], + ); +} + +#[test] +fn stop_counting_function_before_start_fails() { + check_intrinsic_output( + "", + indoc! {"{ + function Foo() : Unit {} + Std.Diagnostics.StopCountingFunction(Foo); + }"}, + &expect!["callable not counted"], + ); +} + +#[test] +fn start_counting_operation_called_twice_before_stop_fails() { + check_intrinsic_output( + "", + indoc! {"{ + Std.Diagnostics.StartCountingOperation(I); + Std.Diagnostics.StartCountingOperation(I); + }"}, + &expect!["callable already counted"], + ); +} + +#[test] +fn start_counting_function_called_twice_before_stop_fails() { + check_intrinsic_output( + "", + indoc! {"{ + function Foo() : Unit {} + Std.Diagnostics.StartCountingFunction(Foo); + Std.Diagnostics.StartCountingFunction(Foo); + }"}, + &expect!["callable already counted"], + ); +} diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs index 1ddedd904f..b876f251fa 100644 --- a/compiler/qsc_eval/src/lib.rs +++ b/compiler/qsc_eval/src/lib.rs @@ -43,7 +43,7 @@ use qsc_fir::fir::{ use qsc_fir::ty::Ty; use qsc_lowerer::map_fir_package_to_hir; use rand::{rngs::StdRng, SeedableRng}; -use rustc_hash::FxHashSet; +use rustc_hash::{FxHashMap, FxHashSet}; use std::ops; use std::{ cell::RefCell, @@ -61,6 +61,18 @@ pub enum Error { #[diagnostic(code("Qsc.Eval.ArrayTooLarge"))] ArrayTooLarge(#[label("this array has too many items")] PackageSpan), + #[error("callable already counted")] + #[diagnostic(help( + "counting for a given callable must be stopped before it can be started again" + ))] + #[diagnostic(code("Qsc.Eval.CallableAlreadyCounted"))] + CallableAlreadyCounted(#[label] PackageSpan), + + #[error("callable not counted")] + #[diagnostic(help("counting for a given callable must be started before it can be stopped"))] + #[diagnostic(code("Qsc.Eval.CallableNotCounted"))] + CallableNotCounted(#[label] PackageSpan), + #[error("invalid array length: {0}")] #[diagnostic(code("Qsc.Eval.InvalidArrayLength"))] InvalidArrayLength(i64, #[label("cannot be used as a length")] PackageSpan), @@ -150,6 +162,8 @@ impl Error { pub fn span(&self) -> &PackageSpan { match self { Error::ArrayTooLarge(span) + | Error::CallableAlreadyCounted(span) + | Error::CallableNotCounted(span) | Error::DivZero(span) | Error::EmptyRange(span) | Error::IndexOutOfRange(_, span) @@ -435,6 +449,8 @@ struct Scope { frame_id: usize, } +type CallableCountKey = (StoreItemId, bool, bool); + pub struct State { exec_graph_stack: Vec, idx: u32, @@ -446,6 +462,7 @@ pub struct State { call_stack: CallStack, current_span: Span, rng: RefCell, + call_counts: FxHashMap, } impl State { @@ -466,6 +483,7 @@ impl State { call_stack: CallStack::default(), current_span: Span::default(), rng, + call_counts: FxHashMap::default(), } } @@ -962,9 +980,20 @@ impl State { let spec = spec_from_functor_app(functor); match &callee.implementation { + CallableImpl::Intrinsic if is_counting_call(&callee.name.name) => { + self.push_frame(Vec::new().into(), callee_id, functor); + + let val = self.counting_call(&callee.name.name, arg, arg_span)?; + + self.set_val_register(val); + self.leave_frame(); + Ok(()) + } CallableImpl::Intrinsic => { self.push_frame(Vec::new().into(), callee_id, functor); + self.increment_call_count(callee_id, functor); + let name = &callee.name.name; let val = intrinsic::call( name, @@ -995,6 +1024,7 @@ impl State { .expect("missing specialization should be a compilation error"); self.push_frame(spec_decl.exec_graph.clone(), callee_id, functor); self.push_scope(env); + self.increment_call_count(callee_id, functor); self.bind_args_for_spec( env, @@ -1436,6 +1466,41 @@ impl State { span, } } + + fn counting_call(&mut self, name: &str, arg: Value, span: PackageSpan) -> Result { + let callable = if let Value::Closure(closure) = arg { + make_counting_key(closure.id, closure.functor) + } else { + let callable = arg.unwrap_global(); + make_counting_key(callable.0, callable.1) + }; + match name { + "StartCountingOperation" | "StartCountingFunction" => { + if self.call_counts.insert(callable, 0).is_some() { + Err(Error::CallableAlreadyCounted(span)) + } else { + Ok(Value::unit()) + } + } + "StopCountingOperation" | "StopCountingFunction" => { + if let Some(count) = self.call_counts.remove(&callable) { + Ok(Value::Int(count)) + } else { + Err(Error::CallableNotCounted(span)) + } + } + _ => panic!("unknown counting call"), + } + } + + fn increment_call_count(&mut self, callee_id: StoreItemId, functor: FunctorApp) { + if let Some(count) = self + .call_counts + .get_mut(&make_counting_key(callee_id, functor)) + { + *count += 1; + } + } } pub fn are_ctls_unique(ctls: &[Value], tup: &Value) -> bool { @@ -1925,3 +1990,17 @@ fn is_updatable_in_place(env: &Env, expr: &Expr) -> (bool, bool) { _ => (false, false), } } + +fn is_counting_call(name: &str) -> bool { + matches!( + name, + "StartCountingOperation" + | "StopCountingOperation" + | "StartCountingFunction" + | "StopCountingFunction" + ) +} + +fn make_counting_key(id: StoreItemId, functor: FunctorApp) -> CallableCountKey { + (id, functor.adjoint, functor.controlled > 0) +} diff --git a/compiler/qsc_eval/src/tests.rs b/compiler/qsc_eval/src/tests.rs index da44e16db0..c8df135ac9 100644 --- a/compiler/qsc_eval/src/tests.rs +++ b/compiler/qsc_eval/src/tests.rs @@ -3724,7 +3724,7 @@ fn controlled_operation_with_duplicate_controls_fails() { 1, ), item: LocalItemId( - 124, + 128, ), }, caller: PackageId( @@ -3774,7 +3774,7 @@ fn controlled_operation_with_target_in_controls_fails() { 1, ), item: LocalItemId( - 124, + 128, ), }, caller: PackageId( diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs index d07e327fbe..c8bac5f512 100644 --- a/compiler/qsc_fir/src/fir.rs +++ b/compiler/qsc_fir/src/fir.rs @@ -324,7 +324,7 @@ pub enum Global<'a> { } /// A unique identifier for an item within a package store. -#[derive(Clone, Copy, Debug, PartialEq)] +#[derive(Clone, Copy, Debug, PartialEq, Hash, Eq)] pub struct StoreItemId { /// The package ID. pub package: PackageId, diff --git a/library/src/tests/diagnostics.rs b/library/src/tests/diagnostics.rs index 7aed5a9029..f7d6d113a6 100644 --- a/library/src/tests/diagnostics.rs +++ b/library/src/tests/diagnostics.rs @@ -40,3 +40,133 @@ fn check_operations_are_equal() { ), ); } + +#[test] +fn check_start_stop_counting_operation_called_3_times() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingOperation; + import Microsoft.Quantum.Diagnostics.StopCountingOperation; + + operation op1() : Unit {} + operation op2() : Unit { op1(); } + StartCountingOperation(op1); + StartCountingOperation(op2); + op1(); op1(); op2(); + (StopCountingOperation(op1), StopCountingOperation(op2)) + }", + &Value::Tuple([Value::Int(3), Value::Int(1)].into()), + ); +} + +#[test] +fn check_start_stop_counting_operation_called_0_times() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingOperation; + import Microsoft.Quantum.Diagnostics.StopCountingOperation; + + operation op1() : Unit {} + operation op2() : Unit { op1(); } + StartCountingOperation(op1); + StartCountingOperation(op2); + (StopCountingOperation(op1), StopCountingOperation(op2)) + }", + &Value::Tuple([Value::Int(0), Value::Int(0)].into()), + ); +} + +#[test] +fn check_lambda_counted_separately_from_operation() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingOperation; + import Microsoft.Quantum.Diagnostics.StopCountingOperation; + + operation op1() : Unit {} + StartCountingOperation(op1); + let lambda = () => op1(); + StartCountingOperation(lambda); + op1(); + lambda(); + (StopCountingOperation(op1), StopCountingOperation(lambda)) + }", + &Value::Tuple([Value::Int(2), Value::Int(1)].into()), + ); +} + +#[test] +fn check_multiple_controls_counted_together() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingOperation; + import Microsoft.Quantum.Diagnostics.StopCountingOperation; + + operation op1() : Unit is Adj + Ctl {} + StartCountingOperation(Controlled op1); + Controlled op1([], ()); + Controlled Controlled op1([], ([], ())); + Controlled Controlled Controlled op1([], ([], ([], ()))); + (StopCountingOperation(Controlled op1)) + }", + &Value::Int(3), + ); +} + +#[test] +fn check_counting_operation_differentiates_between_body_adj_ctl() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingOperation; + import Microsoft.Quantum.Diagnostics.StopCountingOperation; + + operation op1() : Unit is Adj + Ctl {} + StartCountingOperation(op1); + StartCountingOperation(Adjoint op1); + StartCountingOperation(Controlled op1); + StartCountingOperation(Adjoint Controlled op1); + op1(); + Adjoint op1(); Adjoint op1(); + Controlled op1([], ()); Controlled op1([], ()); Controlled op1([], ()); + Adjoint Controlled op1([], ()); Adjoint Controlled op1([], ()); + Controlled Adjoint op1([], ()); Controlled Adjoint op1([], ()); + (StopCountingOperation(op1), StopCountingOperation(Adjoint op1), StopCountingOperation(Controlled op1), StopCountingOperation(Adjoint Controlled op1)) + }", + &Value::Tuple([Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)].into()), + ); +} + +#[test] +fn check_start_stop_counting_function_called_3_times() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingFunction; + import Microsoft.Quantum.Diagnostics.StopCountingFunction; + + function f1() : Unit {} + function f2() : Unit { f1(); } + StartCountingFunction(f1); + StartCountingFunction(f2); + f1(); f1(); f2(); + (StopCountingFunction(f1), StopCountingFunction(f2)) + }", + &Value::Tuple([Value::Int(3), Value::Int(1)].into()), + ); +} + +#[test] +fn check_start_stop_counting_function_called_0_times() { + test_expression( + "{ + import Microsoft.Quantum.Diagnostics.StartCountingFunction; + import Microsoft.Quantum.Diagnostics.StopCountingFunction; + + function f1() : Unit {} + function f2() : Unit { f1(); } + StartCountingFunction(f1); + StartCountingFunction(f2); + (StopCountingFunction(f1), StopCountingFunction(f2)) + }", + &Value::Tuple([Value::Int(0), Value::Int(0)].into()), + ); +} diff --git a/library/std/src/diagnostics.qs b/library/std/src/diagnostics.qs index 7d67e2c80b..86f17b44dd 100644 --- a/library/std/src/diagnostics.qs +++ b/library/std/src/diagnostics.qs @@ -194,5 +194,106 @@ namespace Microsoft.Quantum.Diagnostics { areEqual } - export DumpMachine, DumpRegister, CheckZero, CheckAllZero, Fact, CheckOperationsAreEqual; + /// # Summary + /// Starts counting the number of times the given operation is called. Fails if the operation is already being counted. + /// + /// # Description + /// This operation allows you to count the number of times a given operation is called. If the given operation is already + /// being counted, calling `StartCountingOperation` again will trigger a runtime failure. Counting is based on the specific + /// specialization of the operation invoked, so `X` and `Adjoint X` are counted separately. + /// Likewise `Controlled X`, `CNOT`, and `CX` are independent operations that are counted separately, as are `Controlled X` + /// and `Controlled Adjoint X`. + /// + /// # Input + /// ## callable + /// The operation to be counted. + /// + /// # Remarks + /// Counting operation calls requires specific care in what operation is passed as input. For example, `StartCountingOperation(H)` will + /// count only the number of times `H` is called, while `StartCountingOperation(Adjoint H)` will count only the number of times `Adjoint H` is called, even + /// though `H` is self-adjoint. This is due to how the execution treats the invocation of these operations as distinct by their specialization. + /// In the same way, `StartCountingOperation(Controlled X)` will count only the number of times `Controlled X` is called, while + /// `StartCountingOperation(CNOT)` will count only the number of times `CNOT` is called. + /// + /// When counting lambdas, the symbol the lambda is bound to is used to identify the operation and it is counted as a separate operation. For example, + /// ```qsharp + /// let myOp = q => H(q); + /// StartCountingOperation(myOp); + /// ``` + /// Will count specifically calls to `myOp` and not `H`. By contrast, the following code will count calls to `H` itself: + /// ```qsharp + /// let myOp = H; + /// StartCountingOperation(myOp); + /// ``` + /// This is because this code does not define a lambda and instead just creates a binding to `H` directly. + @Config(Unrestricted) + operation StartCountingOperation<'In, 'Out>(callable : 'In => 'Out) : Unit { + body intrinsic; + } + + /// # Summary + /// Stops counting the number of times the given operation is called and returns the count. Fails + /// if the operation was not being counted. + /// + /// # Description + /// This operation allows you to stop counting the number of times a given operation is called and returns the count. + /// If the operation was not being counted, it triggers a runtime failure. + /// + /// # Input + /// ## callable + /// The operation whose count will be returned. + /// # Output + /// The number of times the operation was called since the last call to `StartCountingOperation`. + @Config(Unrestricted) + operation StopCountingOperation<'In, 'Out>(callable : 'In => 'Out) : Int { + body intrinsic; + } + + /// # Summary + /// Starts counting the number of times the given function is called. Fails if the function is already being counted. + /// + /// # Description + /// This operation allows you to count the number of times a given function is called. If the given function is already + /// being counted, calling `StartCountingFunction` again will trigger a runtime failure. + /// + /// # Input + /// ## callable + /// The function to be counted. + /// + /// # Remarks + /// When counting lambdas, the symbol the lambda is bound to is used to identify the function and it is counted as a separate function. For example, + /// ```qsharp + /// let myFunc = i -> AbsI(i); + /// StartCountingFunction(myFunc); + /// ``` + /// Will count specifically calls to `myFunc` and not `AbsI`. By contrast, the following code will count calls to `AbsI` itself: + /// ```qsharp + /// let myFunc = AbsI; + /// StartCountingFunction(myFunc); + /// ``` + /// This is because this code does not define a lambda and instead just creates a binding to `AbsI` directly. + @Config(Unrestricted) + operation StartCountingFunction<'In, 'Out>(callable : 'In -> 'Out) : Unit { + body intrinsic; + } + + /// # Summary + /// Stops counting the number of times the given function is called and returns the count. Fails + /// if the function was not being counted. + /// + /// # Description + /// This operation allows you to stop counting the number of times a given function is called and returns the count. + /// If the function was not being counted, it triggers a runtime failure. + /// + /// # Input + /// ## callable + /// The function whose count will be returned. + /// # Output + /// The number of times the function was called since the last call to `StartCountingFunction`. + @Config(Unrestricted) + operation StopCountingFunction<'In, 'Out>(callable : 'In -> 'Out) : Int { + body intrinsic; + } + + export DumpMachine, DumpRegister, CheckZero, CheckAllZero, Fact, CheckOperationsAreEqual, StartCountingOperation, StopCountingOperation, StartCountingFunction, StopCountingFunction; }