microsoft/qdk

Public

mirrored fromhttps://github.com/microsoft/qdkAvailable

CodeCommitsIssuesPull requestsActionsInsightsSecurity
iadavis/pipeline-issue-debugging

Branches

Tags

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

Clone

HTTPS

Download ZIP

source/pip/src/interop.rs

819lines · 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
9use std::fmt::Write;
10use std::path::PathBuf;
11use std::str::FromStr;
12use std::sync::Arc;
13
14use pyo3::IntoPyObjectExt;
15use pyo3::exceptions::PyException;
16use pyo3::prelude::*;
17use pyo3::types::{PyDict, PyList};
18use qsc::circuit::TracerConfig;
19use qsc::hir::PackageId;
20use qsc::interpret::output::Receiver;
21use qsc::interpret::{CircuitEntryPoint, Interpreter, into_errors};
22use qsc::openqasm::compiler::compile_to_qsharp_ast_with_config;
23use qsc::openqasm::semantic::QasmSemanticParseResult;
24use qsc::openqasm::{OperationSignature, QubitSemantics};
25use qsc::project::ProjectType;
26use qsc::target::Profile;
27use qsc::{Backend, PackageType, PauliNoise, SparseSim};
28use qsc::{
29 LanguageFeatures, SourceMap, ast::Package, error::WithSource, interpret, project::FileSystem,
30};
31
32use crate::fs::file_system;
33use crate::interpreter::data_interop::value_to_pyobj;
34use crate::interpreter::{
35 CircuitConfig, OptionalCallbackReceiver, OutputSemantics, ProgramType, QSharpError, QasmError,
36 TargetProfile, format_error, format_errors,
37};
38
39use resource_estimator as re;
40
41/// Runs the given OpenQASM program for the given number of shots.
42/// Each shot uses an independent instance of the simulator.
43///
44/// Note:
45/// This call while exported is not intended to be used directly by the user.
46/// It is intended to be used by the Python wrapper which will handle the
47/// callbacks and other Python specific details.
48///
49/// Args:
50/// source (str): The OpenQASM source code to execute.
51/// output_fn (Callable[[Output], None]): The function to handle the output of the execution.
52/// noise: The noise to use in simulation.
53/// read_file (Callable[[str], Tuple[str, str]]): The function to read a file and return its contents.
54/// list_directory (Callable[[str], List[Dict[str, str]]]): The function to list the contents of a directory.
55/// resolve_path (Callable[[str, str], str]): The function to resolve a path given a base path and a relative path.
56/// fetch_github (Callable[[str, str, str, str], str]): The function to fetch a file from GitHub.
57/// **kwargs: Additional keyword arguments to pass to the execution.
58/// - target_profile (TargetProfile): The target profile to use for execution.
59/// - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'.
60/// - search_path (str): The optional search path for resolving imports.
61/// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
62/// - shots (int): The number of shots to run the program for. Defaults to 1.
63/// - seed (int): The seed to use for the random number generator.
64///
65/// Returns:
66/// Any: The result of the execution.
67///
68/// Raises:
69/// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
70/// QSharpError: If there is an error interpreting the input.
71#[pyfunction]
72#[allow(clippy::too_many_arguments)]
73#[pyo3(
74 signature = (source, callback=None, noise=None, qubit_loss=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None, **kwargs)
75)]
76pub(crate) fn run_qasm_program(
77 py: Python,
78 source: &str,
79 callback: Option<Py<PyAny>>,
80 noise: Option<(f64, f64, f64)>,
81 qubit_loss: Option<f64>,
82 read_file: Option<Py<PyAny>>,
83 list_directory: Option<Py<PyAny>>,
84 resolve_path: Option<Py<PyAny>>,
85 fetch_github: Option<Py<PyAny>>,
86 kwargs: Option<Bound<'_, PyDict>>,
87) -> PyResult<Py<PyAny>> {
88 let mut receiver = OptionalCallbackReceiver { callback, py };
89
90 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
91
92 let target = get_target_profile(&kwargs)?;
93 let operation_name = get_operation_name(&kwargs)?;
94 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::OpenQasm)?;
95 let seed = get_seed(&kwargs);
96 let shots = get_shots(&kwargs)?;
97 let search_path = get_search_path(&kwargs)?;
98
99 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
100 let file_path = PathBuf::from_str(&search_path)
101 .expect("from_str is infallible")
102 .join("program.qasm");
103 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
104 let ProjectType::OpenQASM(sources) = project.project_type else {
105 return Err(QasmError::new_err(
106 "Expected OpenQASM project, but got a different type".to_string(),
107 ));
108 };
109 let res = qsc::openqasm::semantic::parse_sources(&sources);
110 let (package, source_map, signature) = compile_qasm_enriching_errors(
111 res,
112 &operation_name,
113 ProgramType::File,
114 output_semantics,
115 false,
116 )?;
117
118 let package_type = PackageType::Exe;
119 let language_features = LanguageFeatures::default();
120 let mut interpreter =
121 create_interpreter_from_ast(package, source_map, target, language_features, package_type)
122 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
123
124 let entry_expr = signature.create_entry_expr_from_params(String::new());
125 interpreter
126 .set_entry_expr(&entry_expr)
127 .map_err(|errors| map_entry_compilation_errors(errors, &signature))?;
128
129 let noise = match noise {
130 None => None,
131 Some((px, py, pz)) => match PauliNoise::from_probabilities(px, py, pz) {
132 Ok(noise_struct) => Some(noise_struct),
133 Err(error_message) => return Err(PyException::new_err(error_message)),
134 },
135 };
136 let loss = qubit_loss.unwrap_or(0.0);
137 let result = run_ast(&mut interpreter, &mut receiver, shots, seed, noise, loss);
138 match result {
139 Ok(result) => {
140 let list: Result<Vec<_>, _> = result
141 .iter()
142 .map(|v| value_to_pyobj(&interpreter, py, v))
143 .collect();
144 Ok(PyList::new(py, list?)?.into())
145 }
146 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
147 }
148}
149
150pub(crate) fn run_ast(
151 interpreter: &mut Interpreter,
152 receiver: &mut impl Receiver,
153 shots: usize,
154 seed: Option<u64>,
155 noise: Option<PauliNoise>,
156 loss: f64,
157) -> Result<Vec<qsc::interpret::Value>, Vec<interpret::Error>> {
158 let mut results = Vec::with_capacity(shots);
159 for i in 0..shots {
160 let mut sim = if let Some(noise) = noise {
161 SparseSim::new_with_noise(&noise)
162 } else {
163 SparseSim::new()
164 };
165 sim.set_loss(loss);
166 // If seed is provided, we want to use a different seed for each shot
167 // so that the results are different for each shot, but still deterministic
168 sim.set_seed(seed.map(|s| s + i as u64));
169 let result = interpreter.run_with_sim(&mut sim, receiver, None)?;
170 results.push(result);
171 }
172
173 Ok(results)
174}
175
176/// Estimates the resource requirements for executing OpenQASM source code.
177///
178/// Note:
179/// This call while exported is not intended to be used directly by the user.
180/// It is intended to be used by the Python wrapper which will handle the
181/// callbacks and other Python specific details.
182///
183/// Args:
184/// source (str): The OpenQASM source code to estimate the resource requirements for.
185/// job_params (str): The parameters for the job.
186/// read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
187/// list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
188/// resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
189/// fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
190/// **kwargs: Additional keyword arguments to pass to the execution.
191/// - name (str): The name of the circuit. This is used as the entry point for the program. Defaults to 'program'.
192/// - search_path (str): The optional search path for resolving imports.
193/// Returns:
194/// str: The estimated resource requirements for executing the OpenQASM source code.
195#[pyfunction]
196#[allow(clippy::too_many_arguments)]
197#[pyo3(
198 signature = (source, job_params, read_file, list_directory, resolve_path, fetch_github, **kwargs)
199)]
200pub(crate) fn resource_estimate_qasm_program(
201 py: Python,
202 source: &str,
203 job_params: &str,
204 read_file: Option<Py<PyAny>>,
205 list_directory: Option<Py<PyAny>>,
206 resolve_path: Option<Py<PyAny>>,
207 fetch_github: Option<Py<PyAny>>,
208 kwargs: Option<Bound<'_, PyDict>>,
209) -> PyResult<String> {
210 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
211
212 let operation_name = get_operation_name(&kwargs)?;
213 let search_path = get_search_path(&kwargs)?;
214
215 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
216 let file_path = PathBuf::from_str(&search_path)
217 .expect("from_str is infallible")
218 .join("program.qasm");
219 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
220 let ProjectType::OpenQASM(sources) = project.project_type else {
221 return Err(QasmError::new_err(
222 "Expected OpenQASM project, but got a different type".to_string(),
223 ));
224 };
225 let res = qsc::openqasm::semantic::parse_sources(&sources);
226
227 let program_type = ProgramType::File;
228 let output_semantics = OutputSemantics::ResourceEstimation;
229 let (package, source_map, _) =
230 compile_qasm_enriching_errors(res, &operation_name, program_type, output_semantics, false)?;
231
232 match crate::interop::estimate_qasm(package, source_map, job_params) {
233 Ok(estimate) => Ok(estimate),
234 Err(errors) if matches!(errors[0], re::Error::Interpreter(_)) => {
235 Err(QSharpError::new_err(format_errors(
236 errors
237 .into_iter()
238 .map(|e| match e {
239 re::Error::Interpreter(e) => e,
240 re::Error::Estimation(_) => unreachable!(),
241 })
242 .collect::<Vec<_>>(),
243 )))
244 }
245 Err(errors) => Err(QSharpError::new_err(
246 errors
247 .into_iter()
248 .map(|e| match e {
249 re::Error::Estimation(e) => e.to_string(),
250 re::Error::Interpreter(_) => unreachable!(),
251 })
252 .collect::<Vec<_>>()
253 .join("\n"),
254 )),
255 }
256}
257
258/// Compiles the OpenQASM source code into a program that can be submitted to a
259/// target as QIR (Quantum Intermediate Representation).
260///
261/// Note:
262/// This call while exported is not intended to be used directly by the user.
263/// It is intended to be used by the Python wrapper which will handle the
264/// callbacks and other Python specific details.
265///
266/// Args:
267/// source (str): The OpenQASM source code to estimate the resource requirements for.
268/// read_file (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
269/// list_directory (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
270/// resolve_path (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
271/// fetch_github (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
272/// **kwargs: Additional keyword arguments to pass to the compilation when source program is provided.
273/// - name (str): The name of the circuit. This is used as the entry point for the program.
274/// - target_profile (TargetProfile): The target profile to use for code generation.
275/// - search_path (Optional[str]): The optional search path for resolving file references.
276/// - output_semantics (OutputSemantics, optional): The output semantics for the compilation.
277///
278/// Returns:
279/// str: The converted QIR code as a string.
280///
281/// Raises:
282/// QasmError: If there is an error generating, parsing, or analyzing the OpenQASM source.
283/// QSharpError: If there is an error compiling the program.
284#[pyfunction]
285#[allow(clippy::too_many_arguments)]
286#[pyo3(
287 signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs)
288)]
289pub(crate) fn compile_qasm_program_to_qir(
290 py: Python,
291 source: &str,
292 read_file: Option<Py<PyAny>>,
293 list_directory: Option<Py<PyAny>>,
294 resolve_path: Option<Py<PyAny>>,
295 fetch_github: Option<Py<PyAny>>,
296 kwargs: Option<Bound<'_, PyDict>>,
297) -> PyResult<String> {
298 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
299
300 let target = get_target_profile(&kwargs)?;
301 let operation_name = get_operation_name(&kwargs)?;
302 let search_path = get_search_path(&kwargs)?;
303
304 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
305 let file_path = PathBuf::from_str(&search_path)
306 .expect("from_str is infallible")
307 .join("program.qasm");
308 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
309 let ProjectType::OpenQASM(sources) = project.project_type else {
310 return Err(QasmError::new_err(
311 "Expected OpenQASM project, but got a different type".to_string(),
312 ));
313 };
314 let res = qsc::openqasm::semantic::parse_sources(&sources);
315
316 let program_ty = ProgramType::File;
317 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::Qiskit)?;
318 let (package, source_map, signature) =
319 compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, false)?;
320
321 let package_type = PackageType::Lib;
322 let language_features = LanguageFeatures::default();
323 let mut interpreter =
324 create_interpreter_from_ast(package, source_map, target, language_features, package_type)
325 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
326 let entry_expr = signature.create_entry_expr_from_params(String::new());
327
328 generate_qir_from_ast(entry_expr, &mut interpreter)
329}
330
331pub(crate) fn compile_qasm_enriching_errors<S: AsRef<str>>(
332 semantic_parse_result: QasmSemanticParseResult,
333 operation_name: S,
334 program_ty: ProgramType,
335 output_semantics: OutputSemantics,
336 allow_input_params: bool,
337) -> PyResult<(Package, SourceMap, OperationSignature)> {
338 let config = qsc::openqasm::CompilerConfig::new(
339 QubitSemantics::Qiskit,
340 output_semantics.into(),
341 program_ty.into(),
342 Some(operation_name.as_ref().into()),
343 None,
344 );
345
346 let unit = compile_to_qsharp_ast_with_config(semantic_parse_result, config);
347
348 let (source_map, errors, package, sig, _) = unit.into_tuple();
349 if !errors.is_empty() {
350 return Err(QasmError::new_err(format_qasm_errors(errors)));
351 }
352
353 let Some(signature) = sig else {
354 return Err(QasmError::new_err(
355 "signature should have had value. This is a bug",
356 ));
357 };
358
359 if !signature.input.is_empty() && !allow_input_params {
360 // no entry expression is provided, but the signature has input parameters.
361 let mut message = String::new();
362 message += "Circuit has unbound input parameters\n";
363 write!(message, " help: Parameters: {}", signature.input_params())
364 .expect("writing to string should succeed");
365
366 return Err(QSharpError::new_err(message));
367 }
368
369 Ok((package, source_map, signature))
370}
371
372fn generate_qir_from_ast<S: AsRef<str>>(
373 entry_expr: S,
374 interpreter: &mut Interpreter,
375) -> PyResult<String> {
376 interpreter
377 .qirgen(entry_expr.as_ref())
378 .map_err(map_qirgen_errors)
379}
380
381/// This call while exported is not intended to be used directly by the user.
382/// It is intended to be used by the Python wrapper which will handle the
383/// callbacks and other Python specific details.
384#[pyfunction]
385#[allow(clippy::too_many_arguments)]
386#[pyo3(
387 signature = (source, read_file, list_directory, resolve_path, fetch_github, **kwargs)
388)]
389pub(crate) fn compile_qasm_to_qsharp(
390 py: Python,
391 source: &str,
392 read_file: Option<Py<PyAny>>,
393 list_directory: Option<Py<PyAny>>,
394 resolve_path: Option<Py<PyAny>>,
395 fetch_github: Option<Py<PyAny>>,
396 kwargs: Option<Bound<'_, PyDict>>,
397) -> PyResult<String> {
398 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
399
400 let operation_name = get_operation_name(&kwargs)?;
401 let search_path = get_search_path(&kwargs)?;
402
403 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
404 let file_path = PathBuf::from_str(&search_path)
405 .expect("from_str is infallible")
406 .join("program.qasm");
407 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
408 let ProjectType::OpenQASM(sources) = project.project_type else {
409 return Err(QasmError::new_err(
410 "Expected OpenQASM project, but got a different type".to_string(),
411 ));
412 };
413 let res = qsc::openqasm::semantic::parse_sources(&sources);
414
415 let program_ty = get_program_type(&kwargs, || ProgramType::File)?;
416 let output_semantics = get_output_semantics(&kwargs, || OutputSemantics::Qiskit)?;
417 let (package, _, _) =
418 compile_qasm_enriching_errors(res, &operation_name, program_ty, output_semantics, true)?;
419
420 let qsharp = qsc::codegen::qsharp::write_package_string(&package);
421 Ok(qsharp)
422}
423
424/// Enriches the compilation errors to provide more helpful messages
425/// as we know that we are compiling the entry expression.
426pub(crate) fn map_entry_compilation_errors(
427 errors: Vec<interpret::Error>,
428 sig: &OperationSignature,
429) -> PyErr {
430 let mut semantic = vec![];
431 for error in errors {
432 match &error {
433 interpret::Error::Compile(_) => {
434 // The entry expression is invalid. This is likely due to a type mismatch
435 // or missing parameter(s). We should provide a more helpful error message.
436 let mut message = format_error(&error);
437 writeln!(message).expect("write should succeed");
438 writeln!(message, "failed to compile entry point.").expect("write should succeed");
439 writeln!(
440 message,
441 " help: check that the parameter types match the supplied parameters"
442 )
443 .expect("write should succeed");
444
445 write!(message, " help: Parameters: {}", sig.input_params())
446 .expect("writing to string should succeed");
447
448 semantic.push(message);
449 }
450 _ => {
451 semantic.push(format_error(&error));
452 }
453 }
454 }
455 let message = semantic.into_iter().collect::<String>();
456 QSharpError::new_err(message)
457}
458
459/// Adds additional information to interpreter errors to make them more user-friendly.
460/// when QIR generation fails.
461fn map_qirgen_errors(errors: Vec<interpret::Error>) -> PyErr {
462 let mut semantic = vec![];
463 for error in errors {
464 match &error {
465 interpret::Error::Compile(_) => {
466 // We've gotten this far with no compilation errors, so if we get one here
467 // then the entry expression is invalid.
468 let mut message = format_error(&error);
469 writeln!(message).expect("write should succeed");
470 writeln!(message, "failed to compile entry point.").expect("write should succeed");
471 writeln!(
472 message,
473 " help: check that the parameter types match the entry point signature"
474 )
475 .expect("write should succeed");
476
477 semantic.push(message);
478 }
479 interpret::Error::PartialEvaluation(pe) => match pe.error() {
480 qsc::partial_eval::Error::OutputResultLiteral(..) => {
481 let mut message = format_error(&error);
482 writeln!(message).expect("write should succeed");
483 writeln!(
484 message,
485 " help: ensure all output registers have been measured into."
486 )
487 .expect("write should succeed");
488
489 semantic.push(message);
490 }
491 _ => {
492 semantic.push(format_error(&error));
493 }
494 },
495 _ => {
496 semantic.push(format_error(&error));
497 }
498 }
499 }
500 let message = semantic.into_iter().collect::<String>();
501 QSharpError::new_err(message)
502}
503
504/// Estimates the resources required to run a QASM program
505/// represented by the provided AST. The source map is used for
506/// error reporting during compilation or runtime.
507fn estimate_qasm(
508 ast_package: Package,
509 source_map: SourceMap,
510 params: &str,
511) -> Result<String, Vec<resource_estimator::Error>> {
512 let mut interpreter = create_interpreter_from_ast(
513 ast_package,
514 source_map,
515 Profile::Unrestricted,
516 LanguageFeatures::default(),
517 PackageType::Exe,
518 )
519 .map_err(into_estimation_errors)?;
520
521 resource_estimator::estimate_entry(&mut interpreter, params)
522}
523
524/// Synthesizes a circuit for an `OpenQASM` program.
525///
526/// Note:
527/// This call while exported is not intended to be used directly by the user.
528/// It is intended to be used by the Python wrapper which will handle the
529/// callbacks and other Python specific details.
530///
531/// Args:
532/// source (str): An `OpenQASM` program. Alternatively, a callable can be provided,
533/// which must be an already imported global callable.
534/// `read_file` (Callable[[str], Tuple[str, str]]): A callable that reads a file and returns its content and path.
535/// `list_directory` (Callable[[str], List[Dict[str, str]]]): A callable that lists the contents of a directory.
536/// `resolve_path` (Callable[[str, str], str]): A callable that resolves a file path given a base path and a relative path.
537/// `fetch_github` (Callable[[str, str, str, str], str]): A callable that fetches a file from GitHub.
538/// **kwargs: Additional keyword arguments to pass to the execution.
539/// - name (str): The name of the program. This is used as the entry point for the program.
540/// - `search_path` (Optional[str]): The optional search path for resolving file references.
541/// Returns:
542/// Circuit: The synthesized circuit.
543///
544/// Raises:
545/// `QasmError`: If there is an error generating, parsing, or analyzing the `OpenQASM` source.
546/// `QSharpError`: If there is an error evaluating the program.
547/// `QSharpError`: If there is an error synthesizing the circuit.
548#[pyfunction]
549#[allow(clippy::too_many_arguments)]
550#[pyo3(
551 signature = (source, config, read_file, list_directory, resolve_path, fetch_github, **kwargs)
552)]
553pub(crate) fn circuit_qasm_program(
554 py: Python,
555 source: &str,
556 config: &CircuitConfig,
557 read_file: Option<Py<PyAny>>,
558 list_directory: Option<Py<PyAny>>,
559 resolve_path: Option<Py<PyAny>>,
560 fetch_github: Option<Py<PyAny>>,
561 kwargs: Option<Bound<'_, PyDict>>,
562) -> PyResult<Py<PyAny>> {
563 let kwargs = kwargs.unwrap_or_else(|| PyDict::new(py));
564
565 let operation_name = get_operation_name(&kwargs)?;
566 let search_path = get_search_path(&kwargs)?;
567
568 let fs = create_filesystem_from_py(py, read_file, list_directory, resolve_path, fetch_github);
569 let file_path = PathBuf::from_str(&search_path)
570 .expect("from_str is infallible")
571 .join("program.qasm");
572 let project = fs.load_openqasm_project(&file_path, Some(Arc::<str>::from(source)));
573 let ProjectType::OpenQASM(sources) = project.project_type else {
574 return Err(QasmError::new_err(
575 "Expected OpenQASM project, but got a different type".to_string(),
576 ));
577 };
578 let res = qsc::openqasm::semantic::parse_sources(&sources);
579
580 let (package, source_map, signature) = compile_qasm_enriching_errors(
581 res,
582 &operation_name,
583 ProgramType::File,
584 OutputSemantics::ResourceEstimation,
585 false,
586 )?;
587
588 let package_type = PackageType::Exe;
589 let language_features = LanguageFeatures::default();
590 let mut interpreter = create_interpreter_from_ast(
591 package,
592 source_map,
593 TargetProfile::Unrestricted.into(),
594 language_features,
595 package_type,
596 )
597 .map_err(|errors| QSharpError::new_err(format_errors(errors)))?;
598
599 let entry_expr = signature.create_entry_expr_from_params(String::new());
600 interpreter
601 .set_entry_expr(&entry_expr)
602 .map_err(|errors| map_entry_compilation_errors(errors, &signature))?;
603
604 let tracer_config = qsc::circuit::TracerConfig {
605 max_operations: config
606 .max_operations
607 .unwrap_or(TracerConfig::DEFAULT_MAX_OPERATIONS),
608 source_locations: config.source_locations,
609 group_by_scope: config.group_by_scope,
610 prune_classical_qubits: config.prune_classical_qubits,
611 };
612
613 let generation_method = if let Some(generation_method) = config.generation_method {
614 generation_method.into()
615 } else {
616 qsc::interpret::CircuitGenerationMethod::ClassicalEval
617 };
618
619 match interpreter.circuit(
620 CircuitEntryPoint::EntryExpr(entry_expr),
621 generation_method,
622 tracer_config,
623 ) {
624 Ok(circuit) => crate::interpreter::Circuit(circuit).into_py_any(py),
625 Err(errors) => Err(QSharpError::new_err(format_errors(errors))),
626 }
627}
628
629/// Converts a list of Q# errors into a list of resource estimator errors.
630fn into_estimation_errors(errors: Vec<interpret::Error>) -> Vec<resource_estimator::Error> {
631 errors
632 .into_iter()
633 .map(|error| resource_estimator::Error::Interpreter(error.clone()))
634 .collect::<Vec<_>>()
635}
636
637/// Formats a list of QASM errors into a single string.
638pub(crate) fn format_qasm_errors(errors: Vec<WithSource<qsc::openqasm::error::Error>>) -> String {
639 errors
640 .into_iter()
641 .map(|e| {
642 let mut message = String::new();
643 let report = miette::Report::new(e);
644 write!(message, "{report:?}").expect("write should succeed");
645 message
646 })
647 .collect::<String>()
648}
649
650/// Creates a `FileSystem` from the provided Python callbacks.
651/// If any of the callbacks are missing, this will panic.
652pub(crate) fn create_filesystem_from_py(
653 py: Python,
654 read_file: Option<Py<PyAny>>,
655 list_directory: Option<Py<PyAny>>,
656 resolve_path: Option<Py<PyAny>>,
657 fetch_github: Option<Py<PyAny>>,
658) -> impl FileSystem {
659 file_system(
660 py,
661 read_file.expect("file system hooks should have been passed in with a read file callback"),
662 list_directory
663 .expect("file system hooks should have been passed in with a list directory callback"),
664 resolve_path
665 .expect("file system hooks should have been passed in with a resolve path callback"),
666 fetch_github
667 .expect("file system hooks should have been passed in with a fetch github callback"),
668 )
669}
670
671/// Creates an `Interpreter` from the provided AST package and configuration.
672fn create_interpreter_from_ast(
673 ast_package: Package,
674 source_map: SourceMap,
675 profile: Profile,
676 language_features: LanguageFeatures,
677 package_type: PackageType,
678) -> Result<Interpreter, Vec<interpret::Error>> {
679 let capabilities = profile.into();
680 let (stdid, mut store) = qsc::compile::package_store_with_stdlib(capabilities);
681 let dependencies = vec![(PackageId::CORE, None), (stdid, None)];
682
683 let (mut unit, errors) = qsc::compile::compile_ast(
684 &store,
685 &dependencies,
686 ast_package,
687 source_map,
688 package_type,
689 capabilities,
690 );
691
692 if !errors.is_empty() {
693 return Err(into_errors(errors));
694 }
695
696 unit.expose();
697 let source_package_id = store.insert(unit);
698
699 interpret::Interpreter::with_package_store(
700 false,
701 store,
702 source_package_id,
703 capabilities,
704 language_features,
705 &dependencies,
706 )
707}
708
709/// Sanitizes the name to ensure it is a valid identifier according
710/// to the Q# specification. If the name is empty, returns "circuit".
711pub(crate) fn sanitize_name<S: AsRef<str>>(name: S) -> String {
712 let name = name.as_ref();
713 if name.is_empty() {
714 return "circuit".to_string();
715 }
716
717 let mut output = String::with_capacity(name.len());
718 let c = name.chars().next().expect("name should not be empty");
719 if c == '_' || c.is_alphabetic() {
720 output.push(c);
721 } else {
722 // invalid first character, replace with '_'
723 output.push('_');
724 }
725 output.extend(name.chars().skip(1).filter_map(|c| {
726 if c == '-' {
727 Some('_')
728 } else if c == '_' || c.is_alphanumeric() {
729 Some(c)
730 } else {
731 None
732 }
733 }));
734 output
735}
736
737/// Extracts the search path from the kwargs dictionary.
738/// If the search path is not present, returns an error.
739/// Otherwise, returns the search path as a string.
740pub(crate) fn get_search_path(kwargs: &Bound<'_, PyDict>) -> PyResult<String> {
741 kwargs.get_item("search_path")?.map_or_else(
742 || {
743 Err(PyException::new_err(
744 "Could not parse search path".to_string(),
745 ))
746 },
747 |x| x.extract::<String>(),
748 )
749}
750
751/// Extracts the program type from the kwargs dictionary.
752pub(crate) fn get_program_type<D>(kwargs: &Bound<'_, PyDict>, default: D) -> PyResult<ProgramType>
753where
754 D: FnOnce() -> ProgramType,
755{
756 match kwargs.get_item("program_type")? {
757 Some(obj) => Ok(obj.extract::<ProgramType>()?),
758 None => Ok(default()),
759 }
760}
761
762/// Extracts the output semantics from the kwargs dictionary.
763pub(crate) fn get_output_semantics<D>(
764 kwargs: &Bound<'_, PyDict>,
765 default: D,
766) -> PyResult<OutputSemantics>
767where
768 D: FnOnce() -> OutputSemantics,
769{
770 match kwargs.get_item("output_semantics")? {
771 Some(obj) => Ok(obj.extract::<OutputSemantics>()?),
772 None => Ok(default()),
773 }
774}
775
776/// Extracts the name from the kwargs dictionary.
777/// If the name is not present, returns "program".
778/// Otherwise, returns the name after sanitizing it.
779pub(crate) fn get_operation_name(kwargs: &Bound<'_, PyDict>) -> PyResult<String> {
780 let name = kwargs
781 .get_item("name")?
782 .map_or_else(|| Ok("program".to_string()), |x| x.extract::<String>())?;
783
784 // sanitize the name to ensure it is a valid identifier
785 // When creating operation, we'll throw an error if the name is not a valid identifier
786 // so that the user gets the exact name they expect, but here it's better to sanitize.
787 Ok(sanitize_name(name))
788}
789
790/// Extracts the target profile from the kwargs dictionary.
791/// If the target profile is not present, returns `TargetProfile::Unrestricted`.
792/// Otherwise if not a valid `TargetProfile`, returns an error.
793///
794/// This also maps the `TargetProfile` exposed to Python to a `Profile`
795/// used by the interpreter.
796pub(crate) fn get_target_profile(kwargs: &Bound<'_, PyDict>) -> PyResult<Profile> {
797 match kwargs.get_item("target_profile")? {
798 Some(obj) => Ok(obj.extract::<TargetProfile>()?.into()),
799 None => Ok(TargetProfile::Unrestricted.into()),
800 }
801}
802
803/// Extracts the shots from the kwargs dictionary.
804/// If the shots are not present, or are not a valid usize, returns an error.
805pub(crate) fn get_shots(kwargs: &Bound<'_, PyDict>) -> PyResult<usize> {
806 kwargs.get_item("shots")?.map_or_else(
807 || Err(PyException::new_err("Could not parse shots".to_string())),
808 |x| x.extract::<usize>(),
809 )
810}
811
812/// Extracts the seed from the kwargs dictionary.
813/// If the seed is not present, or is not a valid u64, returns None.
814pub(crate) fn get_seed(kwargs: &Bound<'_, PyDict>) -> Option<u64> {
815 kwargs
816 .get_item("seed")
817 .ok()?
818 .map_or_else(|| None::<u64>, |x| x.extract::<u64>().ok())
819}
820