Repository: microsoft/qsharp
Pull request #1024 (2024-01-16 23:27:51)

Remove shots from statful interpreter `run` API
This change removes the shots argument to the `run` API in stateful interpreter now that all callers pass 1. This avoids the need to wrap interpreter results in an additional vector and simplifies calling patterns. All consumers already passed 1 and had a shot loop around the call to `run` or utilized other APIs.

This also removes the need for a `reinit` function on the `Backend` trait, since now a simulator is only used once before being recreated.

github-actions[bot]: ## Benchmark for a2f3187
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | 870.6±10.54µs | 867.5±9.47µs | -0.36% |
| Array update evaluation | 942.8±12.67µs | 937.4±7.66µs | -0.57% |
| Deutsch-Jozsa evaluation | 31.2±0.36ms | 31.2±0.30ms | 0.00% |
| Large file parity evaluation | **36.1±0.09ms** | 36.2±1.13ms | **+0.28%** |
| Large input file | 38.5±1.41ms | 39.4±2.31ms | +2.34% |
| Standard library | 21.0±0.54ms | 21.0±0.76ms | 0.00% |
| Teleport evaluation | 104.7±2.24µs | 104.8±2.03µs | +0.10% |

  </details>
  
github-actions[bot]: ## Benchmark for c017e78
  <details>
    <summary>Click to view benchmark</summary>

| Test | Base         | PR               | % |
|------|--------------|------------------|---|
| Array append evaluation | **866.7±5.65µs** | 877.7±10.06µs | **+1.27%** |
| Array update evaluation | **939.5±5.69µs** | 946.7±7.79µs | **+0.77%** |
| Deutsch-Jozsa evaluation | 31.3±0.22ms | 31.3±0.31ms | 0.00% |
| Large file parity evaluation | 36.1±0.18ms | 36.2±0.57ms | +0.28% |
| Large input file | 37.5±1.08ms | 37.4±0.63ms | -0.27% |
| Standard library | 20.7±0.18ms | 20.7±0.34ms | 0.00% |
| Teleport evaluation | 105.1±1.61µs | 106.1±1.70µs | +0.95% |

  </details>
  

Patch:
diff --git a/.ado/publish.yml b/.ado/publish.yml
index a908b996..7b213815 100644
--- a/.ado/publish.yml
+++ b/.ado/publish.yml
@@ -5,7 +5,7 @@ pr: none
 
 variables:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
 
 # variables set by pipeline
 # - BASE_IMAGE
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 12156e60..7a28dfcd 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -20,7 +20,7 @@ env:
   CARGO_TERM_COLOR: always
   NODE_VERSION: "18.17.1"
   PYTHON_VERSION: "3.11"
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 jobs:
diff --git a/.github/workflows/publish-playground.yml b/.github/workflows/publish-playground.yml
index 4e5d6d98..9e757498 100644
--- a/.github/workflows/publish-playground.yml
+++ b/.github/workflows/publish-playground.yml
@@ -11,7 +11,7 @@ on:
 
 env:
   CARGO_TERM_COLOR: always
-  RUST_TOOLCHAIN_VERSION: "1.73"
+  RUST_TOOLCHAIN_VERSION: "1.75"
   RUST_TOOLCHAIN_COMPONENTS: rustfmt clippy
 
 # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
diff --git a/compiler/qsc/src/interpret/stateful.rs b/compiler/qsc/src/interpret/stateful.rs
index 9ebaf982..0e31dc8a 100644
--- a/compiler/qsc/src/interpret/stateful.rs
+++ b/compiler/qsc/src/interpret/stateful.rs
@@ -295,9 +295,8 @@ impl Interpreter {
         &mut self,
         receiver: &mut impl Receiver,
         expr: &str,
-        shots: u32,
-    ) -> Result<Vec<InterpretResult>, Vec<Error>> {
-        self.run_with_sim(&mut SparseSim::new(), receiver, expr, shots)
+    ) -> Result<InterpretResult, Vec<Error>> {
+        self.run_with_sim(&mut SparseSim::new(), receiver, expr)
     }
 
     /// Gets the current quantum state of the simulator.
@@ -316,11 +315,7 @@ impl Interpreter {
         let mut stdout = std::io::sink();
         let mut out = GenericReceiver::new(&mut stdout);
 
-        let val = self
-            .run_with_sim(&mut sim, &mut out, expr, 1)?
-            .into_iter()
-            .last()
-            .expect("execution should have at least one result")?;
+        let val = self.run_with_sim(&mut sim, &mut out, expr)??;
 
         Ok(sim.finish(&val))
     }
@@ -332,11 +327,10 @@ impl Interpreter {
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
         expr: &str,
-        shots: u32,
-    ) -> Result<Vec<InterpretResult>, Vec<Error>> {
+    ) -> Result<InterpretResult, Vec<Error>> {
         let stmt_id = self.compile_expr_to_stmt(expr)?;
 
-        Ok(self.run_internal(sim, receiver, stmt_id, shots))
+        Ok(self.run_internal(sim, receiver, stmt_id))
     }
 
     fn run_internal(
@@ -344,37 +338,23 @@ impl Interpreter {
         sim: &mut impl Backend<ResultType = impl Into<val::Result>>,
         receiver: &mut impl Receiver,
         stmt_id: StmtId,
-        shots: u32,
-    ) -> Vec<InterpretResult> {
-        let mut results: Vec<InterpretResult> = Vec::new();
-        for i in 0..shots {
-            results.push(
-                match eval_stmt(
-                    stmt_id,
-                    &self.fir_store,
-                    &mut Env::with_empty_scope(),
-                    sim,
-                    self.package,
-                    receiver,
-                ) {
-                    Ok(value) => Ok(value),
-                    Err((error, call_stack)) => Err(eval_error(
-                        self.compiler.package_store(),
-                        &self.fir_store,
-                        call_stack,
-                        error,
-                    )),
-                },
-            );
-
-            if i != 0 {
-                // If running more than one shot, re-initialize the simulator to start the next shot
-                // from a clean state.
-                sim.reinit();
-            }
+    ) -> InterpretResult {
+        match eval_stmt(
+            stmt_id,
+            &self.fir_store,
+            &mut Env::with_empty_scope(),
+            sim,
+            self.package,
+            receiver,
+        ) {
+            Ok(value) => Ok(value),
+            Err((error, call_stack)) => Err(eval_error(
+                self.compiler.package_store(),
+                &self.fir_store,
+                call_stack,
+                error,
+            )),
         }
-
-        results
     }
 
     fn compile_expr_to_stmt(&mut self, expr: &str) -> Result<StmtId, Vec<Error>> {
diff --git a/compiler/qsc_codegen/src/qir_base.rs b/compiler/qsc_codegen/src/qir_base.rs
index dbc0671c..82591cb7 100644
--- a/compiler/qsc_codegen/src/qir_base.rs
+++ b/compiler/qsc_codegen/src/qir_base.rs
@@ -437,10 +437,6 @@ impl Backend for BaseProfSim {
         // true to avoid a panic.
         true
     }
-
-    fn reinit(&mut self) {
-        *self = Self::default();
-    }
 }
 
 struct Qubit(HardwareId);
diff --git a/compiler/qsc_eval/src/backend.rs b/compiler/qsc_eval/src/backend.rs
index 22c7bb84..2cc80d31 100644
--- a/compiler/qsc_eval/src/backend.rs
+++ b/compiler/qsc_eval/src/backend.rs
@@ -38,7 +38,6 @@ pub trait Backend {
     fn qubit_release(&mut self, q: usize);
     fn capture_quantum_state(&mut self) -> (Vec<(BigUint, Complex<f64>)>, usize);
     fn qubit_is_zero(&mut self, q: usize) -> bool;
-    fn reinit(&mut self);
 
     fn custom_intrinsic(&mut self, _name: &str, _arg: Value) -> Option<Result<Value, String>> {
         None
@@ -194,10 +193,6 @@ impl Backend for SparseSim {
         self.sim.qubit_is_zero(q)
     }
 
-    fn reinit(&mut self) {
-        *self = Self::new();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, _arg: Value) -> Option<Result<Value, String>> {
         match name {
             "BeginEstimateCaching" => Some(Ok(Value::Bool(true))),
diff --git a/compiler/qsc_fir/src/fir.rs b/compiler/qsc_fir/src/fir.rs
index 4390dc1a..c3b721d7 100644
--- a/compiler/qsc_fir/src/fir.rs
+++ b/compiler/qsc_fir/src/fir.rs
@@ -521,6 +521,15 @@ impl PackageStore {
     }
 }
 
+impl<'a> IntoIterator for &'a PackageStore {
+    type IntoIter = qsc_data_structures::index_map::Iter<'a, PackageId, Package>;
+    type Item = (PackageId, &'a Package);
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.iter()
+    }
+}
+
 /// A trait to find elements in a package.
 pub trait PackageLookup {
     /// Gets a block.
diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi
index 92e8b4b5..d96b72e3 100644
--- a/pip/qsharp/_native.pyi
+++ b/pip/qsharp/_native.pyi
@@ -57,17 +57,15 @@ class Interpreter:
         """
         ...
     def run(
-        self, entry_expr: str, shots: int, output_fn: Callable[[Output], None]
+        self, entry_expr: str, output_fn: Callable[[Output], None]
     ) -> Any:
         """
-        Runs the given Q# expressin for the given number of shots.
-        Each shot uses an independent instance of the simulator.
+        Runs the given Q# expression with an independent instance of the simulator.
 
         :param entry_expr: The entry expression.
-        :param shots: The number of shots to run.
         :param output_fn: A callback function that will be called with each output.
 
-        :returns values: A list of results or runtime errors.
+        :returns values: A result or runtime errors.
 
         :raises QSharpError: If there is an error interpreting the input.
         """
diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py
index 4573ffd0..9b279ca5 100644
--- a/pip/qsharp/_qsharp.py
+++ b/pip/qsharp/_qsharp.py
@@ -163,9 +163,9 @@ def run(
     for _ in range(shots):
         results.append({"result": None, "events": []})
         run_results = get_interpreter().run(
-            entry_expr, 1, on_save_events if save_events else print_output
+            entry_expr, on_save_events if save_events else print_output
         )
-        results[-1]["result"] = run_results[0]
+        results[-1]["result"] = run_results
         if on_result:
             on_result(results[-1])
 
diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs
index 1f3c8fde..1e0d4c73 100644
--- a/pip/src/interpreter.rs
+++ b/pip/src/interpreter.rs
@@ -167,19 +167,14 @@ impl Interpreter {
         &mut self,
         py: Python,
         entry_expr: &str,
-        shots: u32,
         callback: Option<PyObject>,
-    ) -> PyResult<Py<PyList>> {
+    ) -> PyResult<PyObject> {
         let mut receiver = OptionalCallbackReceiver { callback, py };
-        match self.interpreter.run(&mut receiver, entry_expr, shots) {
-            Ok(results) => Ok(PyList::new(
-                py,
-                results.into_iter().map(|res| match res {
-                    Ok(v) => ValueWrapper(v).into_py(py),
-                    Err(errors) => QSharpError::new_err(format_errors(errors)).into_py(py),
-                }),
-            )
-            .into_py(py)),
+        match self.interpreter.run(&mut receiver, entry_expr) {
+            Ok(result) => match result {
+                Ok(v) => Ok(ValueWrapper(v).into_py(py)),
+                Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
+            },
             Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
         }
     }
diff --git a/prereqs.py b/prereqs.py
index 0eee7fc0..c7f101e6 100755
--- a/prereqs.py
+++ b/prereqs.py
@@ -13,14 +13,14 @@ import tempfile
 import functools
 
 python_ver = (3, 11)  # Python support for Windows on ARM64 requires v3.11 or later
-rust_ver = (1, 73)  # Ensure Rust version 1.73 or later is installed
+rust_ver = (1, 75)  # Ensure Rust version 1.75 or later is installed
 node_ver = (
     18,
     17,
 )
 wasmpack_ver = (0, 12, 1)  # Latest tested wasm-pack version
-rust_fmt_ver = (1, 6, 0)  # Current version when Rust 1.73 shipped
-clippy_ver = (0, 1, 69)
+rust_fmt_ver = (1, 7, 0)  # Current version when Rust 1.75 shipped
+clippy_ver = (0, 1, 75)
 
 # Disable buffered output so that the log statements and subprocess output get interleaved in proper order
 print = functools.partial(print, flush=True)
diff --git a/resource_estimator/src/counts.rs b/resource_estimator/src/counts.rs
index c956b1a3..0557b7a3 100644
--- a/resource_estimator/src/counts.rs
+++ b/resource_estimator/src/counts.rs
@@ -522,10 +522,6 @@ impl Backend for LogicalCounter {
         true
     }
 
-    fn reinit(&mut self) {
-        *self = Self::default();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
         match name {
             "BeginEstimateCaching" => {
diff --git a/resource_estimator/src/lib.rs b/resource_estimator/src/lib.rs
index d82b8cf0..a18199c6 100644
--- a/resource_estimator/src/lib.rs
+++ b/resource_estimator/src/lib.rs
@@ -53,11 +53,8 @@ pub fn estimate_expr(
     let mut stdout = std::io::sink();
     let mut out = GenericReceiver::new(&mut stdout);
     interpreter
-        .run_with_sim(&mut counter, &mut out, expr, 1)
+        .run_with_sim(&mut counter, &mut out, expr)
         .map_err(|e| e.into_iter().map(Error::Interpreter).collect::<Vec<_>>())?
-        .into_iter()
-        .last()
-        .expect("execution should have at least one result")
         .map_err(|e| vec![Error::Interpreter(e[0].clone())])?;
     estimate_physical_resources(&counter.logical_resources(), params)
         .map_err(|e| vec![Error::Estimation(e)])


Test patch:
diff --git a/compiler/qsc/src/interpret/stateful/tests.rs b/compiler/qsc/src/interpret/stateful/tests.rs
index a885c800..2da9196b 100644
--- a/compiler/qsc/src/interpret/stateful/tests.rs
+++ b/compiler/qsc/src/interpret/stateful/tests.rs
@@ -24,11 +24,10 @@ mod given_interpreter {
     fn run(
         interpreter: &mut Interpreter,
         expr: &str,
-        shots: u32,
-    ) -> (Result<Vec<InterpretResult>, Vec<Error>>, String) {
+    ) -> (Result<InterpretResult, Vec<Error>>, String) {
         let mut cursor = Cursor::new(Vec::<u8>::new());
         let mut receiver = CursorReceiver::new(&mut cursor);
-        (interpreter.run(&mut receiver, expr, shots), receiver.dump())
+        (interpreter.run(&mut receiver, expr), receiver.dump())
     }
 
     fn entry(
@@ -960,47 +959,45 @@ mod given_interpreter {
         #[test]
         fn run_with_shots() {
             let mut interpreter = get_interpreter();
-            let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
+            let (result, output) = line(&mut interpreter, "operation Foo(qs : Qubit[]) : Unit { Microsoft.Quantum.Diagnostics.DumpMachine(); }");
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::Int(1));
+            for _ in 0..4 {
+                let (results, output) = run(&mut interpreter, "{use qs = Qubit[2]; Foo(qs)}");
+                is_unit_with_output(
+                    &results.expect("compilation should succeed"),
+                    &output,
+                    "STATE:\n|00⟩: 1+0i",
+                );
             }
         }
 
         #[test]
         fn run_parse_error() {
             let mut interpreter = get_interpreter();
-            let (results, _) = run(&mut interpreter, "Foo)", 5);
+            let (results, _) = run(&mut interpreter, "Foo)");
             results.expect_err("run() should fail");
         }
 
         #[test]
         fn run_compile_error() {
             let mut interpreter = get_interpreter();
-            let (results, _) = run(&mut interpreter, "Foo()", 5);
+            let (results, _) = run(&mut interpreter, "Foo()");
             results.expect_err("run() should fail");
         }
 
         #[test]
-        fn run_multiple_statements() {
+        fn run_multiple_statements_with_return_value() {
             let mut interpreter = get_interpreter();
             let (result, output) = line(&mut interpreter, "operation Foo() : Int { 1 }");
             is_only_value(&result, &output, &Value::unit());
             let (result, output) = line(&mut interpreter, "operation Bar() : Int { 2 }");
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "{ Foo(); Bar() }", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::Int(2));
-            }
+            let (result, output) = run(&mut interpreter, "{ Foo(); Bar() }");
+            is_only_value(
+                &result.expect("compilation should succeed"),
+                &output,
+                &Value::Int(2),
+            );
         }
 
         #[test]
@@ -1011,12 +1008,16 @@ mod given_interpreter {
                 r#"operation Foo() : Int { fail "failed" }"#,
             );
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            assert_eq!(output, String::new());
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                r.expect_err("individual run should fail");
+            for _ in 0..1 {
+                let (result, output) = run(&mut interpreter, "Foo()");
+                is_only_error(
+                    &result.expect("compilation should succeed"),
+                    &output,
+                    &expect![[r#"
+                        runtime error: program failed: failed
+                          explicit fail [line_0] [fail "failed"]
+                    "#]],
+                );
             }
         }
 
@@ -1028,19 +1029,13 @@ mod given_interpreter {
                 r#"operation Foo() : Unit { Message("hello!") }"#,
             );
             is_only_value(&result, &output, &Value::unit());
-            let (results, output) = run(&mut interpreter, "Foo()", 5);
-            expect![[r#"
-                hello!
-                hello!
-                hello!
-                hello!
-                hello!"#]]
-            .assert_eq(&output);
-            let results = results.expect("run() should succeed");
-            assert_eq!(results.len(), 5);
-            for r in results {
-                let val = r.expect("individual run should succeed");
-                assert_eq!(val, Value::unit());
+            for _ in 0..4 {
+                let (result, output) = run(&mut interpreter, "Foo()");
+                is_unit_with_output(
+                    &result.expect("compilation should succeed"),
+                    &output,
+                    "hello!",
+                );
             }
         }
     }
diff --git a/compiler/qsc_eval/src/intrinsic/tests.rs b/compiler/qsc_eval/src/intrinsic/tests.rs
index 2b3c7f1e..4c05191d 100644
--- a/compiler/qsc_eval/src/intrinsic/tests.rs
+++ b/compiler/qsc_eval/src/intrinsic/tests.rs
@@ -134,10 +134,6 @@ impl Backend for CustomSim {
         self.sim.qubit_is_zero(q)
     }
 
-    fn reinit(&mut self) {
-        self.sim.reinit();
-    }
-
     fn custom_intrinsic(&mut self, name: &str, arg: Value) -> Option<Result<Value, String>> {
         match name {
             "Add1" => Some(Ok(Value::Int(arg.unwrap_int() + 1))),
diff --git a/pip/tests/test_interpreter.py b/pip/tests/test_interpreter.py
index e3ef98ae..87709a2a 100644
--- a/pip/tests/test_interpreter.py
+++ b/pip/tests/test_interpreter.py
@@ -199,7 +199,9 @@ def test_run_with_shots() -> None:
     e.interpret('operation Foo() : Unit { Message("Hello, world!"); }', callback)
     assert called == 0
 
-    value = e.run("Foo()", 5, callback)
+    value = []
+    for _ in range(5):
+        value.append(e.run("Foo()", callback))
     assert called == 5
 
     assert value == [None, None, None, None, None]