microsoft/qdk

Public

mirrored from https://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
jan

Branches

Tags

  • No tags available.
0Branches0Tags
Go to file
Add file
Code

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1478lines · modecode

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4#![allow(
5 clippy::doc_markdown,
6 reason = "docstrings in this module conform to the python docstring format."
7)]
8
9pub(crate) mod data_interop;
10
11use crate::{
12 displayable_output::{DisplayableMatrix, DisplayableOutput, DisplayableState},
13 fs::file_system,
14 generic_estimator::register_generic_estimator_submodule,
15 interop::{
16 circuit_qasm_program, compile_qasm_program_to_qir, compile_qasm_to_qsharp,
17 create_filesystem_from_py, get_operation_name, get_output_semantics, get_program_type,
18 get_search_path, resource_estimate_qasm_program, run_qasm_program,
19 },
20 interpreter::data_interop::{
21 PrimitiveKind, TypeIR, TypeKind, UdtFields, UdtIR, UdtValue, collect_udt_fields,
22 pyobj_to_value, type_ir_from_qsharp_ty, value_to_pyobj,
23 },
24 noisy_simulator::register_noisy_simulator_submodule,
25 qir_simulation::{
26 IdleNoiseParams, NoiseConfig, NoiseTable, QirInstruction, QirInstructionId,
27 clifford::run_clifford,
28 cpu_full_state::run_cpu_full_state,
29 gpu_full_state::{run_parallel_shots, try_create_gpu_adapter},
30 },
31};
32use miette::{Diagnostic, Report};
33use num_bigint::BigUint;
34use num_complex::Complex64;
35use pyo3::{
36 IntoPyObjectExt, create_exception,
37 exceptions::{PyException, PyValueError},
38 prelude::*,
39 types::{PyDict, PyList, PyString, PyTuple, PyType},
40};
41use qsc::{
42 LanguageFeatures, PackageType, SourceMap,
43 error::WithSource,
44 fir::{self},
45 hir::ty::{Prim, Ty},
46 interpret::{
47 self, CircuitEntryPoint, PauliNoise, TaggedItem, Value,
48 output::{Error, Receiver},
49 },
50 packages::BuildableProgram,
51 project::{FileSystem, PackageCache, PackageGraphSources, ProjectType},
52 qasm::{CompilerConfig, QubitSemantics, compiler::compile_to_qsharp_ast_with_config},
53 target::Profile,
54};
55
56use resource_estimator::{
57 self as re, estimate_call, estimate_expr, logical_counts_call, logical_counts_expr,
58};
59use std::{cell::RefCell, fmt::Write, path::PathBuf, rc::Rc, str::FromStr, sync::Arc};
60
61/// If the classes are not Send, the Python interpreter
62/// will not be able to use them in a separate thread.
63///
64/// This function is used to verify that the classes are Send.
65/// The code will fail to compile if the classes are not Send.
66///
67/// ### Note
68/// `QSharpError`, and `QasmError` are not `Send`, *BUT*
69/// we return `QasmError::new_err` or `QSharpError::new_err` which
70/// actually returns a `PyErr` that is `Send` and the args passed
71/// into the `new_err` call must also impl `Send`.
72/// Because of this, we don't need to check the `Send`-ness of
73/// them. On the Python side, the `PyErr` is converted into the
74/// corresponding exception.
75fn verify_classes_are_sendable() {
76 fn is_send<T: Send>() {}
77 is_send::<OutputSemantics>();
78 is_send::<ProgramType>();
79 is_send::<TargetProfile>();
80 is_send::<Result>();
81 is_send::<Pauli>();
82 is_send::<Output>();
83 is_send::<StateDumpData>();
84 is_send::<CircuitConfig>();
85 is_send::<CircuitGenerationMethod>();
86 is_send::<Circuit>();
87 is_send::<UdtValue>();
88 is_send::<UdtFields>();
89 is_send::<TypeIR>();
90 is_send::<TypeKind>();
91 is_send::<PrimitiveKind>();
92 is_send::<UdtIR>();
93 is_send::<QirInstructionId>();
94 is_send::<QirInstruction>();
95 is_send::<NoiseConfig>();
96 is_send::<NoiseTable>();
97 is_send::<IdleNoiseParams>();
98}
99
100#[pymodule]
101fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> {
102 verify_classes_are_sendable();
103 m.add_class::<OutputSemantics>()?;
104 m.add_class::<ProgramType>()?;
105 m.add_class::<TargetProfile>()?;
106 m.add_class::<Interpreter>()?;
107 m.add_class::<Result>()?;
108 m.add_class::<Pauli>()?;
109 m.add_class::<Output>()?;
110 m.add_class::<StateDumpData>()?;
111 m.add_class::<CircuitConfig>()?;
112 m.add_class::<CircuitGenerationMethod>()?;
113 m.add_class::<Circuit>()?;
114 m.add_class::<GlobalCallable>()?;
115 m.add_class::<UdtValue>()?;
116 m.add_class::<TypeIR>()?;
117 m.add_class::<TypeKind>()?;
118 m.add_class::<PrimitiveKind>()?;
119 m.add_class::<UdtIR>()?;
120 m.add_class::<QirInstructionId>()?;
121 m.add_class::<QirInstruction>()?;
122 m.add_class::<NoiseConfig>()?;
123 m.add_class::<NoiseTable>()?;
124 m.add_class::<IdleNoiseParams>()?;
125 m.add_function(wrap_pyfunction!(physical_estimates, m)?)?;
126 m.add_function(wrap_pyfunction!(run_clifford, m)?)?;
127 m.add_function(wrap_pyfunction!(try_create_gpu_adapter, m)?)?;
128 m.add_function(wrap_pyfunction!(run_cpu_full_state, m)?)?;
129 m.add_function(wrap_pyfunction!(run_parallel_shots, m)?)?;
130 m.add("QSharpError", py.get_type::<QSharpError>())?;
131 register_noisy_simulator_submodule(py, m)?;
132 register_generic_estimator_submodule(m)?;
133 // QASM interop
134 m.add("QasmError", py.get_type::<QasmError>())?;
135 m.add_function(wrap_pyfunction!(resource_estimate_qasm_program, m)?)?;
136 m.add_function(wrap_pyfunction!(run_qasm_program, m)?)?;
137 m.add_function(wrap_pyfunction!(circuit_qasm_program, m)?)?;
138 m.add_function(wrap_pyfunction!(compile_qasm_program_to_qir, m)?)?;
139 m.add_function(wrap_pyfunction!(compile_qasm_to_qsharp, m)?)?;
140 Ok(())
141}
142
143// This ordering must match the _native.pyi file.
144#[derive(Clone, Copy, Default, PartialEq)]
145#[pyclass(eq, eq_int, module = "qsharp._native")]
146#[allow(non_camel_case_types)]
147/// A Q# target profile.
148///
149/// A target profile describes the capabilities of the hardware or simulator
150/// which will be used to run the Q# program.
151pub(crate) enum TargetProfile {
152 /// Target supports the minimal set of capabilities required to run a quantum program.
153 ///
154 /// This option maps to the Base Profile as defined by the QIR specification.
155 #[default]
156 Base,
157 /// Target supports the Adaptive profile with the integer computation extension.
158 ///
159 /// This profile includes all of the required Adaptive Profile
160 /// capabilities, as well as the optional integer computation
161 /// extension defined by the QIR specification.
162 Adaptive_RI,
163 /// Target supports the Adaptive profile with integer & floating-point
164 /// computation extensions.
165 ///
166 /// This profile includes all required Adaptive Profile and `Adaptive_RI`
167 /// capabilities, as well as the optional floating-point computation
168 /// extension defined by the QIR specification.
169 Adaptive_RIF,
170 /// Target supports the full set of capabilities required to run any Q# program.
171 ///
172 /// This option maps to the Full Profile as defined by the QIR specification.
173 Unrestricted,
174}
175
176#[pymethods]
177impl TargetProfile {
178 #[new]
179 // We need to define `new` so that instances of `TargetProfile` can be created by Python
180 pub(crate) fn new() -> Self {
181 Self::default()
182 }
183
184 // called and the returned object is pickled as the contents for the instance
185 #[allow(clippy::trivially_copy_pass_by_ref)]
186 fn __getstate__(&self) -> PyResult<isize> {
187 Ok(self.__pyo3__int__())
188 }
189
190 // called with the unpickled state and the instance is updated in place
191 // This is what requires `new` to be implemented as we can't hydrate an
192 // unininitialized instance in Python.
193 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
194 (*self) = match state {
195 0 => Self::Base,
196 1 => Self::Adaptive_RI,
197 2 => Self::Adaptive_RIF,
198 3 => Self::Unrestricted,
199 _ => return Err(PyValueError::new_err("invalid state")),
200 };
201 Ok(())
202 }
203
204 #[allow(clippy::trivially_copy_pass_by_ref)]
205 fn __str__(&self) -> String {
206 Into::<Profile>::into(*self).to_str().to_owned()
207 }
208
209 /// Creates a target profile from a string.
210 /// :param value: The string to parse.
211 /// :raises ValueError: If the string does not match any target profile.
212 #[classmethod]
213 #[allow(clippy::needless_pass_by_value)]
214 fn from_str(_cls: &Bound<'_, PyType>, key: String) -> pyo3::PyResult<Self> {
215 let profile = Profile::from_str(key.as_str())
216 .map_err(|()| PyValueError::new_err(format!("{key} is not a valid target profile")))?;
217 Ok(TargetProfile::from(profile))
218 }
219}
220
221impl From<Profile> for TargetProfile {
222 fn from(profile: Profile) -> Self {
223 match profile {
224 Profile::Base => TargetProfile::Base,
225 Profile::AdaptiveRI => TargetProfile::Adaptive_RI,
226 Profile::AdaptiveRIF => TargetProfile::Adaptive_RIF,
227 Profile::Unrestricted => TargetProfile::Unrestricted,
228 }
229 }
230}
231
232impl From<TargetProfile> for Profile {
233 fn from(profile: TargetProfile) -> Self {
234 match profile {
235 TargetProfile::Base => Profile::Base,
236 TargetProfile::Adaptive_RI => Profile::AdaptiveRI,
237 TargetProfile::Adaptive_RIF => Profile::AdaptiveRIF,
238 TargetProfile::Unrestricted => Profile::Unrestricted,
239 }
240 }
241}
242
243// This ordering must match the _native.pyi file.
244#[derive(Clone, Copy, Default, PartialEq)]
245#[pyclass(eq, eq_int, module = "qsharp._native")]
246#[allow(non_camel_case_types)]
247/// Represents the output semantics for OpenQASM 3 compilation.
248/// Each has implications on the output of the compilation
249/// and the semantic checks that are performed.
250pub(crate) enum OutputSemantics {
251 /// The output is in Qiskit format meaning that the output
252 /// is all of the classical registers, in reverse order
253 /// in which they were added to the circuit with each
254 /// bit within each register in reverse order.
255 #[default]
256 Qiskit,
257 /// [OpenQASM 3 has two output modes](https://openqasm.com/language/directives.html#input-output)
258 /// - If the programmer provides one or more `output` declarations, then
259 /// variables described as outputs will be returned as output.
260 /// The spec make no mention of endianness or order of the output.
261 /// - Otherwise, assume all of the declared variables are returned as output.
262 OpenQasm,
263 /// No output semantics are applied. The entry point returns `Unit`.
264 ResourceEstimation,
265}
266
267#[pymethods]
268impl OutputSemantics {
269 #[new]
270 // We need to define `new` so that instances of `TargetProfile` can be created by Python
271 pub(crate) fn new() -> Self {
272 Self::default()
273 }
274
275 // called and the returned object is pickled as the contents for the instance
276 #[allow(clippy::trivially_copy_pass_by_ref)]
277 fn __getstate__(&self) -> PyResult<isize> {
278 Ok(self.__pyo3__int__())
279 }
280
281 // called with the unpickled state and the instance is updated in place
282 // This is what requires `new` to be implemented as we can't hydrate an
283 // unininitialized instance in Python.
284 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
285 (*self) = match state {
286 0 => Self::Qiskit,
287 1 => Self::OpenQasm,
288 2 => Self::ResourceEstimation,
289 _ => return Err(PyValueError::new_err("invalid state")),
290 };
291 Ok(())
292 }
293}
294
295impl From<OutputSemantics> for qsc::qasm::OutputSemantics {
296 fn from(output_semantics: OutputSemantics) -> Self {
297 match output_semantics {
298 OutputSemantics::Qiskit => qsc::qasm::OutputSemantics::Qiskit,
299 OutputSemantics::OpenQasm => qsc::qasm::OutputSemantics::OpenQasm,
300 OutputSemantics::ResourceEstimation => qsc::qasm::OutputSemantics::ResourceEstimation,
301 }
302 }
303}
304
305// This ordering must match the _native.pyi file.
306#[derive(Clone, Copy, Default, PartialEq)]
307#[pyclass(eq, eq_int, module = "qsharp._native")]
308#[allow(non_camel_case_types)]
309/// Represents the type of compilation output to create
310pub enum ProgramType {
311 /// Creates an operation in a namespace as if the program is a standalone
312 /// file. Inputs are lifted to the operation params. Output are lifted to
313 /// the operation return type. The operation is marked as `@EntryPoint`
314 /// as long as there are no input parameters.
315 #[default]
316 File,
317 /// Programs are compiled to a standalone function. Inputs are lifted to
318 /// the operation params. Output are lifted to the operation return type.
319 Operation,
320 /// Creates a list of statements from the program. This is useful for
321 /// interactive environments where the program is a list of statements
322 /// imported into the current scope.
323 /// This is also useful for testing individual statements compilation.
324 Fragments,
325}
326
327#[pymethods]
328impl ProgramType {
329 #[new]
330 // We need to define `new` so that instances of `TargetProfile` can be created by Python
331 pub(crate) fn new() -> Self {
332 Self::default()
333 }
334
335 // called and the returned object is pickled as the contents for the instance
336 #[allow(clippy::trivially_copy_pass_by_ref)]
337 fn __getstate__(&self) -> PyResult<isize> {
338 Ok(self.__pyo3__int__())
339 }
340
341 // called with the unpickled state and the instance is updated in place
342 // This is what requires `new` to be implemented as we can't hydrate an
343 // unininitialized instance in Python.
344 fn __setstate__(&mut self, state: i32) -> PyResult<()> {
345 (*self) = match state {
346 0 => Self::File,
347 1 => Self::Operation,
348 2 => Self::Fragments,
349 _ => return Err(PyValueError::new_err("invalid state")),
350 };
351 Ok(())
352 }
353}
354
355impl From<ProgramType> for qsc::qasm::ProgramType {
356 fn from(output_semantics: ProgramType) -> Self {
357 match output_semantics {
358 ProgramType::File => qsc::qasm::ProgramType::File,
359 ProgramType::Operation => qsc::qasm::ProgramType::Operation,
360 ProgramType::Fragments => qsc::qasm::ProgramType::Fragments,
361 }
362 }
363}
364
365#[allow(clippy::struct_field_names)]
366#[pyclass(unsendable)]
367pub(crate) struct Interpreter {
368 pub(crate) interpreter: interpret::Interpreter,
369 /// The Python function to call to create a new function wrapping a callable invocation.
370 pub(crate) make_callable: Option<PyObject>,
371 /// The Python function to call to create a class representing a qsharp struct.
372 pub(crate) make_class: Option<PyObject>,
373 /// Whether circuit tracing was enabled.
374 trace_circuit: bool,
375}
376
377thread_local! { static PACKAGE_CACHE: Rc<RefCell<PackageCache>> = Rc::default(); }
378
379#[pymethods]
380/// A Q# interpreter.
381impl Interpreter {
382 #[allow(clippy::too_many_arguments)]
383 #[allow(clippy::needless_pass_by_value)]
384 #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, make_callable=None, make_class=None, trace_circuit=None))]
385 #[new]
386 /// Initializes a new Q# interpreter.
387 pub(crate) fn new(
388 py: Python,
389 target_profile: TargetProfile,
390 language_features: Option<Vec<String>>,
391 project_root: Option<String>,
392 read_file: Option<PyObject>,
393 list_directory: Option<PyObject>,
394 resolve_path: Option<PyObject>,
395 fetch_github: Option<PyObject>,
396 make_callable: Option<PyObject>,
397 make_class: Option<PyObject>,
398 trace_circuit: Option<bool>,
399 ) -> PyResult<Self> {
400 let target = Into::<Profile>::into(target_profile).into();
401
402 let language_features = LanguageFeatures::from_iter(language_features.unwrap_or_default());
403
404 let package_cache = PACKAGE_CACHE.with(Clone::clone);
405
406 let buildable_program = if let Some(project_root) = project_root {
407 if let (Some(read_file), Some(list_directory), Some(resolve_path), Some(fetch_github)) =
408 (read_file, list_directory, resolve_path, fetch_github)
409 {
410 let project =
411 file_system(py, read_file, list_directory, resolve_path, fetch_github)
412 .load_project(&PathBuf::from(project_root), Some(&package_cache))
413 .map_err(IntoPyErr::into_py_err)?;
414
415 if !project.errors.is_empty() {
416 return Err(project.errors.into_py_err());
417 }
418 let ProjectType::QSharp(package_graph_sources) = project.project_type else {
419 unreachable!("Project type should be Q#")
420 };
421 BuildableProgram::new(target, package_graph_sources)
422 } else {
423 panic!("file system hooks should have been passed in with a manifest descriptor")
424 }
425 } else {
426 let graph = PackageGraphSources::with_no_dependencies(
427 Vec::default(),
428 LanguageFeatures::from_iter(language_features),
429 None,
430 );
431 BuildableProgram::new(target, graph)
432 };
433
434 let trace_circuit = trace_circuit.unwrap_or(false);
435 let interpreter = if trace_circuit {
436 interpret::Interpreter::with_circuit_trace(
437 SourceMap::new(buildable_program.user_code.sources, None),
438 PackageType::Lib,
439 target,
440 buildable_program.user_code.language_features,
441 buildable_program.store,
442 &buildable_program.user_code_dependencies,
443 Default::default(),
444 )
445 } else {
446 interpret::Interpreter::new(
447 SourceMap::new(buildable_program.user_code.sources, None),
448 PackageType::Lib,
449 target,
450 buildable_program.user_code.language_features,
451 buildable_program.store,
452 &buildable_program.user_code_dependencies,
453 )
454 }
455 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
456
457 if let Some(make_callable) = &make_callable {
458 // Add any global callables from the user source as Python functions to the environment.
459 let exported_items = interpreter.source_globals();
460 for (namespace, name, val) in exported_items {
461 create_py_callable(py, make_callable, &namespace, &name, val)?;
462 }
463 }
464 if let Some(make_class) = &make_class {
465 // Add any global structs from the user source as Python classes to the environment.
466 let exported_items = interpreter.source_types();
467 for TaggedItem {
468 item_id,
469 name,
470 namespace,
471 } in exported_items
472 {
473 let ty = Ty::Udt(name.clone(), qsc::hir::Res::Item(item_id));
474 create_py_class(&interpreter, py, make_class, &namespace, &name, &ty)?;
475 }
476 }
477 Ok(Self {
478 interpreter,
479 make_callable,
480 make_class,
481 trace_circuit,
482 })
483 }
484
485 /// Interprets Q# source code.
486 ///
487 /// :param input: The Q# source code to interpret.
488 /// :param output_fn: A callback function that will be called with each output.
489 ///
490 /// :returns value: The value returned by the last statement in the input.
491 ///
492 /// :raises QSharpError: If there is an error interpreting the input.
493 #[pyo3(signature=(input, callback=None))]
494 fn interpret(
495 &mut self,
496 py: Python,
497 input: &str,
498 callback: Option<PyObject>,
499 ) -> PyResult<PyObject> {
500 let mut receiver = OptionalCallbackReceiver { callback, py };
501 match self.interpreter.eval_fragments(&mut receiver, input) {
502 Ok(value) => {
503 if let Some(make_callable) = &self.make_callable {
504 // Get any global callables from the evaluated input and add them to the environment. This will grab
505 // every callable that was defined in the input and by previous calls that added to the open package.
506 // This is safe because either the callable will be replaced with itself or a new callable with the
507 // same name will shadow the previous one, which is the expected behavior.
508 let new_items = self.interpreter.user_globals();
509 for (namespace, name, val) in new_items {
510 create_py_callable(py, make_callable, &namespace, &name, val)?;
511 }
512 }
513 if let Some(make_class) = &self.make_class {
514 // Get any global UDTs from the evaluated input and add them to the environment. This will grab
515 // every UDT that was defined in the input and by previous calls that added to the open package.
516 // This is safe because either the UDT will be replaced with itself or a new UDT with the
517 // same name will shadow the previous one, which is the expected behavior.
518 let new_items = self.interpreter.user_types();
519 for TaggedItem {
520 item_id,
521 name,
522 namespace,
523 } in new_items
524 {
525 let ty = Ty::Udt(name.clone(), qsc::hir::Res::Item(item_id));
526 create_py_class(&self.interpreter, py, make_class, &namespace, &name, &ty)?;
527 }
528 }
529 value_to_pyobj(&self.interpreter, py, &value)
530 }
531 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
532 }
533 }
534
535 /// Imports OpenQASM source code into the active Q# interpreter.
536 ///
537 /// Args:
538 /// source (str): An OpenQASM program or fragment.
539 /// output_fn: The function to handle the output of the execution.
540 /// read_file: A callable that reads a file and returns its content and path.
541 /// list_directory: A callable that lists the contents of a directory.
542 /// resolve_path: A callable that resolves a file path given a base path and a relative path.
543 /// fetch_github: A callable that fetches a file from GitHub.
544 /// **kwargs: Additional keyword arguments to pass to the execution.
545 /// - name (str): The name of the program. This is used as the entry point for the program.
546 /// - search_path (Optional[str]): The optional search path for resolving file references.
547 /// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
548 /// - program_type (ProgramType, optional): The type of program compilation to perform.
549 ///
550 /// Returns:
551 /// value: The value returned by the last statement in the source code.
552 ///
553 /// Raises:
554 /// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
555 /// QSharpError: If there is an error compiling the program.
556 /// QSharpError: If there is an error evaluating the source code.
557 #[pyo3(signature=(input, output_fn, read_file, list_directory, resolve_path, fetch_github, **kwargs))]
558 #[allow(clippy::needless_pass_by_value)]
559 #[allow(clippy::too_many_arguments)]
560 fn import_qasm(
561 &mut self,
562 py: Python,
563 input: &str,
564 output_fn: Option<PyObject>,
565 read_file: Option<PyObject>,
566 list_directory: Option<PyObject>,
567 resolve_path: Option<PyObject>,
568 fetch_github: Option<PyObject>,
569 kwargs: Option<Bound<'_, PyDict>>,
570 ) -> PyResult<PyObject> {
571 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
572
573 let operation_name = get_operation_name(&kwargs)?;
574 let search_path = get_search_path(&kwargs)?;
575 let program_ty = get_program_type(&kwargs, || ProgramType::Operation)?;
576 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
577
578 let fs =
579 create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
580 let file_path = PathBuf::from_str(&search_path)
581 .expect("from_str is infallible")
582 .join("program.qasm");
583 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(input)));
584 let ProjectType::OpenQASM(sources) = project.project_type else {
585 return Err(QasmError::new_err(
586 "Expected OpenQASM project, but got a different type".to_string(),
587 ));
588 };
589
590 let config = CompilerConfig::new(
591 QubitSemantics::Qiskit,
592 output_semantics.into(),
593 program_ty.into(),
594 Some(operation_name.into()),
595 None,
596 );
597 let res = qsc::qasm::semantic::parse_sources(&sources);
598 let unit = compile_to_qsharp_ast_with_config(res, config);
599 let (sources, errors, package, _, _) = unit.into_tuple();
600
601 if !errors.is_empty() {
602 let errors = errors
603 .iter()
604 .map(|e| {
605 use qsc::compile::ErrorKind;
606 use qsc::interpret::Error;
607 let error = e.error().clone();
608 let kind = ErrorKind::OpenQasm(error);
609 let v = WithSource::from_map(&sources, kind);
610 Error::Compile(v)
611 })
612 .collect();
613 return Err(QSharpError::new_err(format_errors(errors)));
614 }
615 let mut receiver = OptionalCallbackReceiver {
616 callback: output_fn,
617 py,
618 };
619
620 match self
621 .interpreter
622 .eval_ast_fragments(&mut receiver, input, package)
623 {
624 Ok(value) => {
625 if let Some(make_callable) = &self.make_callable {
626 // Get any global callables from the evaluated input and add them to the environment. This will grab
627 // every callable that was defined in the input and by previous calls that added to the open package.
628 // This is safe because either the callable will be replaced with itself or a new callable with the
629 // same name will shadow the previous one, which is the expected behavior.
630 let new_items = self.interpreter.user_globals();
631 for (namespace, name, val) in new_items {
632 create_py_callable(py, make_callable, &namespace, &name, val)?;
633 }
634 }
635 value_to_pyobj(&self.interpreter, py, &value)
636 }
637 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
638 }
639 }
640
641 /// Sets the quantum seed for the interpreter.
642 #[pyo3(signature=(seed=None))]
643 fn set_quantum_seed(&mut self, seed: Option<u64>) {
644 self.interpreter.set_quantum_seed(seed);
645 }
646
647 /// Sets the classical seed for the interpreter.
648 #[pyo3(signature=(seed=None))]
649 fn set_classical_seed(&mut self, seed: Option<u64>) {
650 self.interpreter.set_classical_seed(seed);
651 }
652
653 /// Dumps the quantum state of the interpreter.
654 /// Returns a tuple of (amplitudes, num_qubits), where amplitudes is a dictionary from integer indices to
655 /// pairs of real and imaginary amplitudes.
656 fn dump_machine(&mut self) -> StateDumpData {
657 let (state, qubit_count) = self.interpreter.get_quantum_state();
658 StateDumpData(DisplayableState(state, qubit_count))
659 }
660
661 /// Dumps a circuit showing the current state of the simulator.
662 ///
663 /// This circuit will contain the gates that have been applied
664 /// in the simulator up to the current point.
665 ///
666 /// Requires the interpreter to be initialized with `trace_circuit=True`.
667 fn dump_circuit(&mut self, py: Python) -> PyResult<PyObject> {
668 if !self.trace_circuit {
669 return Err(QSharpError::new_err(
670 "to enable circuit dumping, the interpreter must be created with trace_circuit=True",
671 ));
672 }
673 Circuit(self.interpreter.get_circuit()).into_py_any(py)
674 }
675
676 #[allow(clippy::too_many_arguments)]
677 #[pyo3(signature=(entry_expr=None, callback=None, noise=None, qubit_loss=None, callable=None, args=None))]
678 fn run(
679 &mut self,
680 py: Python,
681 entry_expr: Option<&str>,
682 callback: Option<PyObject>,
683 noise: Option<(f64, f64, f64)>,
684 qubit_loss: Option<f64>,
685 callable: Option<GlobalCallable>,
686 args: Option<PyObject>,
687 ) -> PyResult<PyObject> {
688 let mut receiver = OptionalCallbackReceiver { callback, py };
689
690 let noise = match noise {
691 None => None,
692 Some((px, py, pz)) => match PauliNoise::from_probabilities(px, py, pz) {
693 Ok(noise_struct) => Some(noise_struct),
694 Err(error_message) => return Err(PyException::new_err(error_message)),
695 },
696 };
697
698 let result = match callable {
699 Some(callable) => {
700 let (input_ty, output_ty) = self
701 .interpreter
702 .global_callable_ty(&callable.0)
703 .ok_or(QSharpError::new_err("callable not found"))?;
704 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
705
706 self.interpreter.invoke_with_noise(
707 &mut receiver,
708 callable.0,
709 args,
710 noise,
711 qubit_loss,
712 )
713 }
714 _ => self
715 .interpreter
716 .run(&mut receiver, entry_expr, noise, qubit_loss),
717 };
718
719 match result {
720 Ok(value) => value_to_pyobj(&self.interpreter, py, &value),
721 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
722 }
723 }
724
725 #[pyo3(signature=(callable, args=None, callback=None))]
726 fn invoke(
727 &mut self,
728 py: Python,
729 callable: GlobalCallable,
730 args: Option<PyObject>,
731 callback: Option<PyObject>,
732 ) -> PyResult<PyObject> {
733 let mut receiver = OptionalCallbackReceiver { callback, py };
734 let (input_ty, output_ty) = self
735 .interpreter
736 .global_callable_ty(&callable.0)
737 .ok_or(QSharpError::new_err("callable not found"))?;
738
739 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
740
741 match self.interpreter.invoke(&mut receiver, callable.0, args) {
742 Ok(value) => value_to_pyobj(&self.interpreter, py, &value),
743 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
744 }
745 }
746
747 #[pyo3(signature=(entry_expr=None, callable=None, args=None))]
748 fn qir(
749 &mut self,
750 py: Python,
751 entry_expr: Option<&str>,
752 callable: Option<GlobalCallable>,
753 args: Option<PyObject>,
754 ) -> PyResult<String> {
755 if let Some(entry_expr) = entry_expr {
756 match self.interpreter.qirgen(entry_expr) {
757 Ok(qir) => Ok(qir),
758 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
759 }
760 } else {
761 let callable = callable.ok_or_else(|| {
762 QSharpError::new_err("either entry_expr or callable must be specified")
763 })?;
764 let (input_ty, output_ty) = self
765 .interpreter
766 .global_callable_ty(&callable.0)
767 .ok_or(QSharpError::new_err("callable not found"))?;
768
769 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
770 match self.interpreter.qirgen_from_callable(&callable.0, args) {
771 Ok(qir) => Ok(qir),
772 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
773 }
774 }
775 }
776
777 /// Synthesizes a circuit for a Q# program. Either an entry
778 /// expression or an operation must be provided.
779 ///
780 /// :param config: Circuit generation options.
781 ///
782 /// :param entry_expr: An entry expression.
783 ///
784 /// :param operation: The operation to synthesize. This can be a name of
785 /// an operation of a lambda expression. The operation must take only
786 /// qubits or arrays of qubits as parameters.
787 ///
788 /// :param callable: A callable to synthesize.
789 ///
790 /// :param args: The arguments to pass to the callable.
791 ///
792 /// :raises QSharpError: If there is an error synthesizing the circuit.
793 #[pyo3(signature=(config, entry_expr=None,*, operation=None, callable=None, args=None))]
794 fn circuit(
795 &mut self,
796 py: Python,
797 config: &CircuitConfig,
798 entry_expr: Option<String>,
799 operation: Option<String>,
800 callable: Option<GlobalCallable>,
801 args: Option<PyObject>,
802 ) -> PyResult<PyObject> {
803 let entrypoint = match (entry_expr, operation, callable) {
804 (Some(entry_expr), None, None) => CircuitEntryPoint::EntryExpr(entry_expr),
805 (None, Some(operation), None) => CircuitEntryPoint::Operation(operation),
806 (None, None, Some(callable)) => {
807 let (input_ty, output_ty) = self
808 .interpreter
809 .global_callable_ty(&callable.0)
810 .ok_or(QSharpError::new_err("callable not found"))?;
811 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
812 CircuitEntryPoint::Callable(callable.0, args)
813 }
814 _ => {
815 return Err(PyException::new_err(
816 "either entry_expr or operation must be specified",
817 ));
818 }
819 };
820
821 let mut tracer_config = qsc::circuit::TracerConfig::default();
822 if let Some(max_ops) = config.max_operations {
823 tracer_config.max_operations = max_ops;
824 }
825 if let Some(locations) = config.source_locations {
826 tracer_config.source_locations = locations;
827 }
828 if let Some(group_by_scope) = config.group_by_scope {
829 tracer_config.group_by_scope = group_by_scope;
830 }
831
832 let generation_method = if let Some(generation_method) = config.generation_method {
833 generation_method.into()
834 } else {
835 qsc::interpret::CircuitGenerationMethod::ClassicalEval
836 };
837
838 match self
839 .interpreter
840 .circuit(entrypoint, generation_method, tracer_config)
841 {
842 Ok(circuit) => Circuit(circuit).into_py_any(py),
843 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
844 }
845 }
846
847 #[pyo3(signature=(job_params, entry_expr=None, callable=None, args=None))]
848 fn estimate(
849 &mut self,
850 py: Python,
851 job_params: &str,
852 entry_expr: Option<&str>,
853 callable: Option<GlobalCallable>,
854 args: Option<PyObject>,
855 ) -> PyResult<String> {
856 let results = if let Some(entry_expr) = entry_expr {
857 estimate_expr(&mut self.interpreter, entry_expr, job_params)
858 } else {
859 let callable = callable.ok_or_else(|| {
860 QSharpError::new_err("either entry_expr or callable must be specified")
861 })?;
862 let (input_ty, output_ty) = self
863 .interpreter
864 .global_callable_ty(&callable.0)
865 .ok_or(QSharpError::new_err("callable not found"))?;
866 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
867 estimate_call(&mut self.interpreter, callable.0, args, job_params)
868 };
869 match results {
870 Ok(estimate) => Ok(estimate),
871 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
872 Err(QSharpError::new_err(format_errors(
873 errors
874 .into_iter()
875 .map(|e| match e {
876 re::Error::Interpreter(e) => e,
877 re::Error::Estimation(_) => unreachable!(),
878 })
879 .collect::<Vec<_>>(),
880 )))
881 }
882 Err(errors) => Err(QSharpError::new_err(
883 errors
884 .into_iter()
885 .map(|e| match e {
886 re::Error::Estimation(e) => e.to_string(),
887 re::Error::Interpreter(_) => unreachable!(),
888 })
889 .collect::<Vec<_>>()
890 .join("\n"),
891 )),
892 }
893 }
894
895 #[pyo3(signature=(entry_expr=None, callable=None, args=None))]
896 fn logical_counts<'a>(
897 &mut self,
898 py: Python<'a>,
899 entry_expr: Option<&str>,
900 callable: Option<GlobalCallable>,
901 args: Option<PyObject>,
902 ) -> PyResult<Bound<'a, PyDict>> {
903 let results = if let Some(entry_expr) = entry_expr {
904 logical_counts_expr(&mut self.interpreter, entry_expr)
905 } else {
906 let callable = callable.ok_or_else(|| {
907 QSharpError::new_err("either entry_expr or callable must be specified")
908 })?;
909 let (input_ty, output_ty) = self
910 .interpreter
911 .global_callable_ty(&callable.0)
912 .ok_or(QSharpError::new_err("callable not found"))?;
913 let args = args_to_values(&self.interpreter, py, args, &input_ty, &output_ty)?;
914 logical_counts_call(&mut self.interpreter, callable.0, args)
915 };
916 match results {
917 Ok(counts) => {
918 let dict = PyDict::new(py);
919 dict.set_item("numQubits", counts.num_qubits)?;
920 dict.set_item("tCount", counts.t_count)?;
921 dict.set_item("rotationCount", counts.rotation_count)?;
922 dict.set_item("rotationDepth", counts.rotation_depth)?;
923 dict.set_item("cczCount", counts.ccz_count)?;
924 dict.set_item("ccixCount", counts.ccix_count)?;
925 dict.set_item("measurementCount", counts.measurement_count)?;
926 Ok(dict)
927 }
928 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
929 Err(QSharpError::new_err(format_errors(
930 errors
931 .into_iter()
932 .map(|e| match e {
933 re::Error::Interpreter(e) => e,
934 re::Error::Estimation(_) => unreachable!(),
935 })
936 .collect::<Vec<_>>(),
937 )))
938 }
939 Err(errors) => Err(QSharpError::new_err(
940 errors
941 .into_iter()
942 .map(|e| match e {
943 re::Error::Estimation(e) => e.to_string(),
944 re::Error::Interpreter(_) => unreachable!(),
945 })
946 .collect::<Vec<_>>()
947 .join("\n"),
948 )),
949 }
950 }
951}
952
953fn args_to_values(
954 ctx: &interpret::Interpreter,
955 py: Python,
956 args: Option<PyObject>,
957 input_ty: &Ty,
958 output_ty: &Ty,
959) -> PyResult<Value> {
960 // If the types are not supported, we can't convert the arguments or return value.
961 // Check this before trying to convert the arguments, and return an error if the types are not supported.
962 if let Some(ty) = first_unsupported_interop_ty(ctx, input_ty) {
963 return Err(QSharpError::new_err(format!(
964 "unsupported input type: `{ty}`"
965 )));
966 }
967 if let Some(ty) = first_unsupported_interop_ty(ctx, output_ty) {
968 return Err(QSharpError::new_err(format!(
969 "unsupported output type: `{ty}`"
970 )));
971 }
972
973 // Convert the Python arguments to Q# values, treating None as an empty tuple aka `Unit`.
974 if matches!(&input_ty, Ty::Tuple(tup) if tup.is_empty()) {
975 // Special case for unit, where args should be None
976 if args.is_some() {
977 return Err(QSharpError::new_err("expected no arguments"));
978 }
979 Ok(Value::unit())
980 } else {
981 let Some(args) = args else {
982 return Err(QSharpError::new_err(format!(
983 "expected arguments of type `{input_ty}`"
984 )));
985 };
986 // This conversion will produce errors if the types don't match or can't be converted.
987 Ok(pyobj_to_value(ctx, py, &args, input_ty)?)
988 }
989}
990
991/// Finds any Q# type recursively that does not support interop with Python, meaning our code cannot convert it back and forth
992/// across the interop boundary.
993fn first_unsupported_interop_ty<'ctx, 'ty>(
994 ctx: &'ctx interpret::Interpreter,
995 ty: &'ty Ty,
996) -> Option<&'ctx Ty>
997where
998 'ty: 'ctx,
999{
1000 match ty {
1001 Ty::Prim(prim_ty) => match prim_ty {
1002 Prim::Pauli
1003 | Prim::BigInt
1004 | Prim::Bool
1005 | Prim::Double
1006 | Prim::Int
1007 | Prim::String
1008 | Prim::Result => None,
1009 Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => {
1010 Some(ty)
1011 }
1012 },
1013 Ty::Tuple(tup) => tup
1014 .iter()
1015 .find(|t| first_unsupported_interop_ty(ctx, t).is_some()),
1016 Ty::Array(ty) => first_unsupported_interop_ty(ctx, ty),
1017 Ty::Udt(_, res) => {
1018 let qsc::hir::Res::Item(item_id) = res else {
1019 panic!("Udt should be an item");
1020 };
1021 let (udt, _) = ctx.udt_ty_from_item_id(item_id);
1022
1023 let Ok(fields) = collect_udt_fields(udt) else {
1024 return Some(ty);
1025 };
1026
1027 for field in fields {
1028 if let Some(ty) = first_unsupported_interop_ty(ctx, field.1) {
1029 return Some(ty);
1030 }
1031 }
1032
1033 None
1034 }
1035 Ty::Arrow(..) | Ty::Infer(..) | Ty::Param { .. } | Ty::Err => Some(ty),
1036 }
1037}
1038
1039#[pyfunction]
1040pub fn physical_estimates(logical_resources: &str, job_params: &str) -> PyResult<String> {
1041 match re::estimate_physical_resources_from_json(logical_resources, job_params) {
1042 Ok(estimates) => Ok(estimates),
1043 Err(error) => Err(QSharpError::new_err(error.to_string())),
1044 }
1045}
1046
1047create_exception!(
1048 module,
1049 QSharpError,
1050 pyo3::exceptions::PyException,
1051 "An error returned from the Q# interpreter."
1052);
1053
1054create_exception!(
1055 module,
1056 QasmError,
1057 pyo3::exceptions::PyException,
1058 "An error returned from the OpenQASM parser."
1059);
1060
1061pub(crate) fn format_errors(errors: Vec<interpret::Error>) -> String {
1062 errors
1063 .into_iter()
1064 .map(|e| format_error(&e))
1065 .collect::<Vec<_>>()
1066 .join("\n")
1067}
1068
1069pub(crate) fn format_error(e: &interpret::Error) -> String {
1070 let mut message = String::new();
1071 if let Some(stack_trace) = e.stack_trace() {
1072 write!(message, "{stack_trace}").expect("write should succeed");
1073 }
1074 let additional_help = python_help(e);
1075 let report = Report::new(e.clone());
1076 write!(message, "{report:?}")
1077 .unwrap_or_else(|err| panic!("writing error failed: {err} error was: {e:?}"));
1078 if let Some(additional_help) = additional_help {
1079 writeln!(message, "{additional_help}").expect("write should succeed");
1080 }
1081 message
1082}
1083
1084/// Additional help text for an error specific to the Python module
1085fn python_help(error: &interpret::Error) -> Option<String> {
1086 if matches!(error, interpret::Error::UnsupportedRuntimeCapabilities) {
1087 Some("Unsupported target profile. Initialize Q# by running `qsharp.init(target_profile=qsharp.TargetProfile.Base)` before performing code generation.".into())
1088 } else {
1089 None
1090 }
1091}
1092
1093#[pyclass]
1094pub(crate) struct Output(DisplayableOutput);
1095
1096#[pymethods]
1097/// An output returned from the Q# interpreter.
1098/// Outputs can be a state dumps or messages. These are normally printed to the console.
1099impl Output {
1100 fn __repr__(&self) -> String {
1101 match &self.0 {
1102 DisplayableOutput::State(state) => state.to_plain(),
1103 DisplayableOutput::Matrix(matrix) => matrix.to_plain(),
1104 DisplayableOutput::Message(msg) => msg.clone(),
1105 }
1106 }
1107
1108 fn __str__(&self) -> String {
1109 self.__repr__()
1110 }
1111
1112 fn _repr_markdown_(&self) -> Option<String> {
1113 match &self.0 {
1114 DisplayableOutput::State(state) => {
1115 let latex = if let Some(latex) = state.to_latex() {
1116 format!("\n\n{latex}")
1117 } else {
1118 String::default()
1119 };
1120 Some(format!("{}{latex}", state.to_html()))
1121 }
1122 DisplayableOutput::Message(_) => None,
1123 DisplayableOutput::Matrix(matrix) => Some(matrix.to_latex()),
1124 }
1125 }
1126
1127 fn state_dump(&self) -> Option<StateDumpData> {
1128 match &self.0 {
1129 DisplayableOutput::State(state) => Some(StateDumpData(state.clone())),
1130 DisplayableOutput::Matrix(_) | DisplayableOutput::Message(_) => None,
1131 }
1132 }
1133
1134 fn is_state_dump(&self) -> bool {
1135 matches!(&self.0, DisplayableOutput::State(_))
1136 }
1137
1138 fn is_matrix(&self) -> bool {
1139 matches!(&self.0, DisplayableOutput::Matrix(_))
1140 }
1141
1142 fn is_message(&self) -> bool {
1143 matches!(&self.0, DisplayableOutput::Message(_))
1144 }
1145}
1146
1147#[pyclass]
1148/// Captured simulation state dump.
1149pub(crate) struct StateDumpData(pub(crate) DisplayableState);
1150
1151#[pymethods]
1152impl StateDumpData {
1153 fn get_dict<'a>(&self, py: Python<'a>) -> PyResult<Bound<'a, PyDict>> {
1154 let dict = rustc_hash::FxHashMap::from_iter(self.0.0.clone());
1155 dict.into_pyobject(py)
1156 }
1157
1158 #[getter]
1159 fn get_qubit_count(&self) -> usize {
1160 self.0.1
1161 }
1162
1163 fn __len__(&self) -> usize {
1164 self.0.0.len()
1165 }
1166
1167 fn __repr__(&self) -> String {
1168 self.0.to_plain()
1169 }
1170
1171 fn __str__(&self) -> String {
1172 self.__repr__()
1173 }
1174
1175 fn _repr_markdown_(&self) -> String {
1176 let latex = if let Some(latex) = self.0.to_latex() {
1177 format!("\n\n{latex}")
1178 } else {
1179 String::default()
1180 };
1181 format!("{}{latex}", self.0.to_html())
1182 }
1183
1184 fn _repr_latex_(&self) -> Option<String> {
1185 self.0.to_latex()
1186 }
1187}
1188
1189#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
1190#[pyclass(eq, eq_int, ord)]
1191/// A Q# measurement result.
1192pub(crate) enum Result {
1193 Zero,
1194 One,
1195 Loss,
1196}
1197
1198impl From<Result> for qsc::interpret::Result {
1199 fn from(value: Result) -> Self {
1200 match value {
1201 Result::Loss => qsc::interpret::Result::Loss,
1202 Result::One | Result::Zero => qsc::interpret::Result::Val(value == Result::One),
1203 }
1204 }
1205}
1206
1207#[pymethods]
1208impl Result {
1209 #[allow(clippy::trivially_copy_pass_by_ref)]
1210 fn __repr__(&self) -> String {
1211 match self {
1212 Result::Zero => "Zero".to_owned(),
1213 Result::One => "One".to_owned(),
1214 Result::Loss => "Loss".to_owned(),
1215 }
1216 }
1217
1218 #[allow(clippy::trivially_copy_pass_by_ref)]
1219 fn __str__(&self) -> String {
1220 self.__repr__()
1221 }
1222
1223 #[allow(clippy::trivially_copy_pass_by_ref)]
1224 fn __hash__(&self) -> u32 {
1225 match self {
1226 Result::Zero => 0,
1227 Result::One => 1,
1228 Result::Loss => 2,
1229 }
1230 }
1231}
1232
1233#[derive(Clone, Copy, PartialEq)]
1234#[pyclass(eq, eq_int)]
1235/// A Q# Pauli operator.
1236pub(crate) enum Pauli {
1237 I,
1238 X,
1239 Y,
1240 Z,
1241}
1242
1243impl From<Pauli> for fir::Pauli {
1244 fn from(value: Pauli) -> Self {
1245 match value {
1246 Pauli::I => fir::Pauli::I,
1247 Pauli::X => fir::Pauli::X,
1248 Pauli::Y => fir::Pauli::Y,
1249 Pauli::Z => fir::Pauli::Z,
1250 }
1251 }
1252}
1253
1254pub(crate) struct OptionalCallbackReceiver<'a> {
1255 pub(crate) callback: Option<PyObject>,
1256 pub(crate) py: Python<'a>,
1257}
1258
1259impl Receiver for OptionalCallbackReceiver<'_> {
1260 fn state(
1261 &mut self,
1262 state: Vec<(BigUint, Complex64)>,
1263 qubit_count: usize,
1264 ) -> core::result::Result<(), Error> {
1265 if let Some(callback) = &self.callback {
1266 let out = DisplayableOutput::State(DisplayableState(state, qubit_count));
1267 callback
1268 .call1(
1269 self.py,
1270 PyTuple::new(
1271 self.py,
1272 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1273 )
1274 .map_err(|_| Error)?,
1275 )
1276 .map_err(|_| Error)?;
1277 }
1278 Ok(())
1279 }
1280
1281 fn matrix(&mut self, matrix: Vec<Vec<Complex64>>) -> std::result::Result<(), Error> {
1282 if let Some(callback) = &self.callback {
1283 let out = DisplayableOutput::Matrix(DisplayableMatrix(matrix));
1284 callback
1285 .call1(
1286 self.py,
1287 PyTuple::new(
1288 self.py,
1289 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1290 )
1291 .map_err(|_| Error)?,
1292 )
1293 .map_err(|_| Error)?;
1294 }
1295 Ok(())
1296 }
1297
1298 fn message(&mut self, msg: &str) -> core::result::Result<(), Error> {
1299 if let Some(callback) = &self.callback {
1300 let out = DisplayableOutput::Message(msg.to_owned());
1301 callback
1302 .call1(
1303 self.py,
1304 PyTuple::new(
1305 self.py,
1306 &[Py::new(self.py, Output(out)).expect("should be able to create output")],
1307 )
1308 .map_err(|_| Error)?,
1309 )
1310 .map_err(|_| Error)?;
1311 }
1312 Ok(())
1313 }
1314}
1315
1316#[pyclass]
1317pub(crate) struct CircuitConfig {
1318 #[pyo3(get, set)]
1319 pub(crate) max_operations: Option<usize>,
1320 #[pyo3(get, set)]
1321 pub(crate) generation_method: Option<CircuitGenerationMethod>,
1322 #[pyo3(get, set)]
1323 pub(crate) source_locations: Option<bool>,
1324 #[pyo3(get, set)]
1325 pub(crate) group_by_scope: Option<bool>,
1326}
1327
1328#[pymethods]
1329impl CircuitConfig {
1330 #[new]
1331 #[pyo3(signature=(*,max_operations=None, generation_method=None, source_locations=None, group_by_scope=None))]
1332 fn new(
1333 max_operations: Option<usize>,
1334 generation_method: Option<CircuitGenerationMethod>,
1335 source_locations: Option<bool>,
1336 group_by_scope: Option<bool>,
1337 ) -> Self {
1338 Self {
1339 max_operations,
1340 generation_method,
1341 source_locations,
1342 group_by_scope,
1343 }
1344 }
1345}
1346
1347#[pyclass]
1348#[derive(Clone, Copy)]
1349pub(crate) enum CircuitGenerationMethod {
1350 ClassicalEval,
1351 Simulate,
1352}
1353
1354impl From<CircuitGenerationMethod> for qsc::interpret::CircuitGenerationMethod {
1355 fn from(value: CircuitGenerationMethod) -> Self {
1356 match value {
1357 CircuitGenerationMethod::ClassicalEval => {
1358 qsc::interpret::CircuitGenerationMethod::ClassicalEval
1359 }
1360 CircuitGenerationMethod::Simulate => qsc::interpret::CircuitGenerationMethod::Simulate,
1361 }
1362 }
1363}
1364
1365#[pyclass]
1366pub(crate) struct Circuit(pub qsc::circuit::Circuit);
1367
1368#[pymethods]
1369impl Circuit {
1370 fn __repr__(&self) -> String {
1371 // Disable rendering source locations for user-facing string representation,
1372 // as they make the circuit look cluttered.
1373 self.0.display_no_locations().to_string()
1374 }
1375
1376 fn __str__(&self) -> String {
1377 self.__repr__()
1378 }
1379
1380 fn json(&self, _py: Python) -> PyResult<String> {
1381 serde_json::to_string(&self.0).map_err(|e| PyException::new_err(e.to_string()))
1382 }
1383}
1384
1385trait IntoPyErr {
1386 fn into_py_err(self) -> PyErr;
1387}
1388
1389impl IntoPyErr for Report {
1390 fn into_py_err(self) -> PyErr {
1391 PyException::new_err(format!("{self:?}"))
1392 }
1393}
1394
1395impl<E> IntoPyErr for Vec<E>
1396where
1397 E: Diagnostic + Send + Sync + 'static,
1398{
1399 fn into_py_err(self) -> PyErr {
1400 let mut message = String::new();
1401 for diag in self {
1402 let report = Report::new(diag);
1403 writeln!(message, "{report:?}").expect("string should be writable");
1404 }
1405 PyException::new_err(message)
1406 }
1407}
1408
1409#[pyclass(unsendable)]
1410#[derive(Clone)]
1411struct GlobalCallable(Value);
1412
1413impl From<Value> for GlobalCallable {
1414 fn from(val: Value) -> Self {
1415 match val {
1416 val @ Value::Global(..) => GlobalCallable(val),
1417 _ => panic!("expected global callable"),
1418 }
1419 }
1420}
1421
1422impl From<GlobalCallable> for Value {
1423 fn from(val: GlobalCallable) -> Self {
1424 val.0
1425 }
1426}
1427
1428/// Create a Python callable from a Q# callable and adds it to the given environment.
1429fn create_py_callable(
1430 py: Python,
1431 make_callable: &PyObject,
1432 namespace: &[Rc<str>],
1433 name: &str,
1434 val: Value,
1435) -> PyResult<()> {
1436 if namespace.is_empty() && name == "<lambda>" {
1437 // We don't want to bind auto-generated lambda callables.
1438 return Ok(());
1439 }
1440
1441 let args = (
1442 Py::new(py, GlobalCallable::from(val)).expect("should be able to create callable"), // callable id
1443 PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array
1444 PyString::new(py, name), // name of callable
1445 );
1446
1447 // Call into the Python layer to create the function wrapping the callable invocation.
1448 make_callable.call1(py, args)?;
1449
1450 Ok(())
1451}
1452
1453/// Create a Python class from a Q# type and adds it to the given environment.
1454fn create_py_class(
1455 ctx: &interpret::Interpreter,
1456 py: Python,
1457 make_class: &PyObject,
1458 namespace: &[Rc<str>],
1459 name: &str,
1460 ty: &Ty,
1461) -> PyResult<()> {
1462 let Some(type_ir) = type_ir_from_qsharp_ty(ctx, ty) else {
1463 // If the UDT can't be expressed in Python, we don't want to raise
1464 // an error, instead we just don't define that type in `qsharp.code.*`.
1465 return Ok(());
1466 };
1467
1468 let args = (
1469 Py::new(py, type_ir).expect("should be able to create callable"), // callable id
1470 PyList::new(py, namespace.iter().map(ToString::to_string))?, // namespace as string array
1471 PyString::new(py, name), // name of callable
1472 );
1473
1474 // Call into the Python layer to create the function wrapping the callable invocation.
1475 make_class.call1(py, args)?;
1476
1477 Ok(())
1478}
1479