microsoft/qdk

Public

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

CodeCommitsIssuesPull requestsActionsInsightsSecurity
v1.21.0

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interpreter.rs

1353lines · modecode

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