diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index e1560bac14..2c736ee701 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -11,6 +11,8 @@ mod package_tests; #[cfg(test)] mod tests; +use std::rc::Rc; + pub use qsc_eval::{ debug::Frame, noise::PauliNoise, @@ -21,6 +23,7 @@ pub use qsc_eval::{ val::Value, StepAction, StepResult, }; +use qsc_hir::{global, ty}; use qsc_linter::{HirLint, Lint, LintKind, LintLevel}; use qsc_lowerer::{map_fir_package_to_hir, map_hir_package_to_fir}; use qsc_partial_eval::ProgramEntry; @@ -315,6 +318,72 @@ impl Interpreter { }) } + /// Given a package ID, returns all the global items in the package. + /// Note this does not currently include re-exports. + fn package_globals(&self, package_id: PackageId) -> Vec<(Vec>, Rc, Value)> { + let mut exported_items = Vec::new(); + let package = &self + .compiler + .package_store() + .get(map_fir_package_to_hir(package_id)) + .expect("package should exist in the package store") + .package; + for global in global::iter_package(Some(map_fir_package_to_hir(package_id)), package) { + if let global::Kind::Term(term) = global.kind { + let store_item_id = fir::StoreItemId { + package: package_id, + item: fir::LocalItemId::from(usize::from(term.id.item)), + }; + exported_items.push(( + global.namespace, + global.name, + Value::Global(store_item_id, FunctorApp::default()), + )); + } + } + exported_items + } + + /// Get the global callables defined in the user source passed into initialization of the interpreter as `Value` instances. + pub fn user_globals(&self) -> Vec<(Vec>, Rc, Value)> { + self.package_globals(self.source_package) + } + + /// Get the global callables defined in the open package being interpreted as `Value` instances, which will include any items + /// defined by calls to `eval_fragments` and the like. + pub fn source_globals(&self) -> Vec<(Vec>, Rc, Value)> { + self.package_globals(self.package) + } + + /// Get the input and output types of a given value representing a global item. + /// # Panics + /// Panics if the item is not callable or a type that can be invoked as a callable. + pub fn global_tys(&self, item_id: &Value) -> Option<(ty::Ty, ty::Ty)> { + let Value::Global(item_id, _) = item_id else { + panic!("value is not a global callable"); + }; + let package_id = map_fir_package_to_hir(item_id.package); + let unit = self + .compiler + .package_store() + .get(package_id) + .expect("package should exist in the package store"); + let item = unit + .package + .items + .get(qsc_hir::hir::LocalItemId::from(usize::from(item_id.item)))?; + match &item.kind { + qsc_hir::hir::ItemKind::Callable(decl) => { + Some((decl.input.ty.clone(), decl.output.clone())) + } + qsc_hir::hir::ItemKind::Ty(_, udt) => { + // We don't handle UDTs, so we return an error type that prevents later code from processing this item. + Some((udt.get_pure_ty(), ty::Ty::Err)) + } + _ => panic!("item is not callable"), + } + } + pub fn set_quantum_seed(&mut self, seed: Option) { self.quantum_seed = seed; self.sim.set_seed(seed); @@ -467,6 +536,33 @@ impl Interpreter { ) } + /// Invokes the given callable with the given arguments using the current environment, simlator, and compilation. + pub fn invoke( + &mut self, + receiver: &mut impl Receiver, + callable: Value, + args: Value, + ) -> InterpretResult { + qsc_eval::invoke( + self.package, + self.classical_seed, + &self.fir_store, + &mut self.env, + &mut self.sim, + receiver, + callable, + args, + ) + .map_err(|(error, call_stack)| { + eval_error( + self.compiler.package_store(), + &self.fir_store, + call_stack, + error, + ) + }) + } + /// Runs the given entry expression on a new instance of the environment and simulator, /// but using the current compilation. pub fn run( @@ -1033,7 +1129,7 @@ impl<'a> BreakpointCollector<'a> { .expect("Couldn't find source file") } - fn add_stmt(&mut self, stmt: &qsc_fir::fir::Stmt) { + fn add_stmt(&mut self, stmt: &fir::Stmt) { let source: &Source = self.get_source(stmt.span.lo); if source.offset == self.offset { let span = stmt.span - source.offset; diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index ce37254484..2b82256ce3 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -46,6 +46,21 @@ mod given_interpreter { (result, receiver.dump()) } + fn invoke( + interpreter: &mut Interpreter, + callable: &str, + args: Value, + ) -> (InterpretResult, String) { + let mut cursor = Cursor::new(Vec::::new()); + let mut receiver = CursorReceiver::new(&mut cursor); + let callable = match interpreter.eval_fragments(&mut receiver, callable) { + Ok(val) => val, + Err(e) => return (Err(e), receiver.dump()), + }; + let result = interpreter.invoke(&mut receiver, callable, args); + (result, receiver.dump()) + } + mod without_sources { use expect_test::expect; use indoc::indoc; @@ -514,6 +529,166 @@ mod given_interpreter { is_only_value(&result, &output, &Value::unit()); } + #[test] + fn interpreter_without_sources_has_no_items() { + let interpreter = get_interpreter(); + let items = interpreter.user_globals(); + assert!(items.is_empty()); + } + + #[test] + fn fragment_without_items_has_no_items() { + let mut interpreter = get_interpreter(); + let (result, output) = line(&mut interpreter, "()"); + is_only_value(&result, &output, &Value::unit()); + let items = interpreter.source_globals(); + assert!(items.is_empty()); + } + + #[test] + fn fragment_defining_items_has_items() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + function Foo() : Int { 2 } + function Bar() : Int { 3 } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + let items = interpreter.source_globals(); + assert_eq!(items.len(), 2); + // No namespace for top-level items + assert!(items[0].0.is_empty()); + expect![[r#" + "Foo" + "#]] + .assert_debug_eq(&items[0].1); + // No namespace for top-level items + assert!(items[1].0.is_empty()); + expect![[r#" + "Bar" + "#]] + .assert_debug_eq(&items[1].1); + } + + #[test] + fn fragment_defining_items_with_namespace_has_items() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + namespace Foo { + function Bar() : Int { 3 } + } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + let items = interpreter.source_globals(); + assert_eq!(items.len(), 1); + expect![[r#" + [ + "Foo", + ] + "#]] + .assert_debug_eq(&items[0].0); + expect![[r#" + "Bar" + "#]] + .assert_debug_eq(&items[0].1); + } + + #[test] + fn fragments_defining_items_add_to_existing_items() { + let mut interpreter = get_interpreter(); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + function Foo() : Int { 2 } + function Bar() : Int { 3 } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + let items = interpreter.source_globals(); + assert_eq!(items.len(), 2); + let (result, output) = line( + &mut interpreter, + indoc! {r#" + function Baz() : Int { 4 } + function Qux() : Int { 5 } + "#}, + ); + is_only_value(&result, &output, &Value::unit()); + let items = interpreter.source_globals(); + assert_eq!(items.len(), 4); + // No namespace for top-level items + assert!(items[0].0.is_empty()); + expect![[r#" + "Foo" + "#]] + .assert_debug_eq(&items[0].1); + // No namespace for top-level items + assert!(items[1].0.is_empty()); + expect![[r#" + "Bar" + "#]] + .assert_debug_eq(&items[1].1); + // No namespace for top-level items + assert!(items[2].0.is_empty()); + expect![[r#" + "Baz" + "#]] + .assert_debug_eq(&items[2].1); + // No namespace for top-level items + assert!(items[3].0.is_empty()); + expect![[r#" + "Qux" + "#]] + .assert_debug_eq(&items[3].1); + } + + #[test] + fn invoke_callable_without_args_succeeds() { + let mut interpreter = get_interpreter(); + let (result, output) = invoke( + &mut interpreter, + "Std.Diagnostics.DumpMachine", + Value::unit(), + ); + is_unit_with_output(&result, &output, "STATE:\nNo qubits allocated"); + } + + #[test] + fn invoke_callable_with_args_succeeds() { + let mut interpreter = get_interpreter(); + let (result, output) = invoke( + &mut interpreter, + "Message", + Value::String("Hello, World!".into()), + ); + is_unit_with_output(&result, &output, "Hello, World!"); + } + + #[test] + fn invoke_lambda_with_capture_succeeds() { + let mut interpreter = get_interpreter(); + let (result, output) = line(&mut interpreter, "let x = 1; let f = y -> x + y;"); + is_only_value(&result, &output, &Value::unit()); + let (result, output) = invoke(&mut interpreter, "f", Value::Int(2)); + is_only_value(&result, &output, &Value::Int(3)); + } + + #[test] + fn invoke_lambda_with_capture_in_callable_expr_succeeds() { + let mut interpreter = get_interpreter(); + let (result, output) = invoke( + &mut interpreter, + "{let x = 1; let f = y -> x + y; f}", + Value::Int(2), + ); + is_only_value(&result, &output, &Value::Int(3)); + } + #[test] fn callables_failing_profile_validation_are_not_registered() { let mut interpreter = @@ -1698,6 +1873,46 @@ mod given_interpreter { ); } + #[test] + fn interpreter_returns_items_from_source() { + let sources = SourceMap::new( + [( + "test".into(), + "namespace A { + operation B(): Unit { } + } + " + .into(), + )], + Some("A.B()".into()), + ); + + let (std_id, store) = + crate::compile::package_store_with_stdlib(TargetCapabilityFlags::all()); + let interpreter = Interpreter::new( + sources, + PackageType::Lib, + TargetCapabilityFlags::all(), + LanguageFeatures::default(), + store, + &[(std_id, None)], + ) + .expect("interpreter should be created"); + + let items = interpreter.user_globals(); + assert_eq!(1, items.len()); + expect![[r#" + [ + "A", + ] + "#]] + .assert_debug_eq(&items[0].0); + expect![[r#" + "B" + "#]] + .assert_debug_eq(&items[0].1); + } + #[test] fn interpreter_can_be_created_from_ast() { let sources = SourceMap::new( diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index dc957bfc8e..5847ab4449 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -38,7 +38,8 @@ pub mod project { } pub use qsc_data_structures::{ - language_features::LanguageFeatures, namespaces::*, span::Span, target::TargetCapabilityFlags, + functors::FunctorApp, language_features::LanguageFeatures, namespaces::*, span::Span, + target::TargetCapabilityFlags, }; pub use qsc_passes::{lower_hir_to_fir, PackageType, PassContext}; diff --git a/compiler/qsc_eval/src/lib.rs b/compiler/qsc_eval/src/lib.rs index 8655e05685..e3daac3b37 100644 --- a/compiler/qsc_eval/src/lib.rs +++ b/compiler/qsc_eval/src/lib.rs @@ -282,6 +282,51 @@ pub fn eval( Ok(value) } +/// Evaluates the given callable with the given context. +/// # Errors +/// Returns the first error encountered during execution. +/// # Panics +/// On internal error where no result is returned. +#[allow(clippy::too_many_arguments)] +pub fn invoke( + package: PackageId, + seed: Option, + globals: &impl PackageStoreLookup, + env: &mut Env, + sim: &mut impl Backend>, + receiver: &mut impl Receiver, + callable: Value, + args: Value, +) -> Result)> { + let mut state = State::new(package, Vec::new().into(), seed); + // Push the callable value into the state stack and then the args value so they are ready for evaluation. + state.set_val_register(callable); + state.push_val(); + state.set_val_register(args); + + // Evaluate the call, which will pop the args and callable values from the stack and then either + // a) prepare the call stack for the execution of the callable, or + // b) invoke the callable directly if it is an intrinsic. + state + .eval_call( + env, + sim, + globals, + Span::default(), + Span::default(), + receiver, + ) + .map_err(|e| (e, state.get_stack_frames()))?; + + // Trigger evaluation of the state until the end of the stack is reached and a return value is obtained, which will be the final + // result of the invocation. + let res = state.eval(globals, env, sim, receiver, &[], StepAction::Continue)?; + let StepResult::Return(value) = res else { + panic!("eval should always return a value"); + }; + Ok(value) +} + /// The type of step action to take during evaluation #[derive(Debug, Copy, Clone, Eq, PartialEq)] pub enum StepAction { diff --git a/compiler/qsc_hir/src/global.rs b/compiler/qsc_hir/src/global.rs index 754daf4846..d4afe8fecc 100644 --- a/compiler/qsc_hir/src/global.rs +++ b/compiler/qsc_hir/src/global.rs @@ -157,6 +157,17 @@ impl PackageIter<'_> { intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), }), }), + (ItemKind::Callable(decl), None) => Some(Global { + namespace: Vec::new(), + name: alias.map_or_else(|| Rc::clone(&decl.name.name), |alias| alias.name.clone()), + visibility, + status, + kind: Kind::Term(Term { + id, + scheme: decl.scheme(), + intrinsic: decl.body.body == SpecBody::Gen(SpecGen::Intrinsic), + }), + }), (ItemKind::Ty(name, def), Some(ItemKind::Namespace(namespace, _))) => { self.next = Some(Global { namespace: namespace.into(), diff --git a/pip/qsharp/__init__.py b/pip/qsharp/__init__.py index 3b9984cfae..d588a68e77 100644 --- a/pip/qsharp/__init__.py +++ b/pip/qsharp/__init__.py @@ -56,5 +56,5 @@ "PauliNoise", "DepolarizingNoise", "BitFlipNoise", - "PhaseFlipNoise" + "PhaseFlipNoise", ] diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index adef263e44..328ccb0f01 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -103,6 +103,13 @@ class TargetProfile(Enum): Describes the unrestricted set of capabilities required to run any Q# program. """ +class GlobalCallable: + """ + A callable reference that can be invoked with arguments. + """ + + ... + class Interpreter: """A Q# interpreter.""" @@ -114,6 +121,7 @@ class Interpreter: read_file: Callable[[str], Tuple[str, str]], list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], + make_callable: Optional[Callable[[GlobalCallable], None]], ) -> None: """ Initializes the Q# interpreter. @@ -123,6 +131,7 @@ class Interpreter: :param read_file: A function that reads a file from the file system. :param list_directory: A function that lists the contents of a directory. :param resolve_path: A function that joins path segments and normalizes the resulting path. + :param make_callable: A function that registers a Q# callable in the in the environment module. """ ... @@ -159,6 +168,22 @@ class Interpreter: """ ... + def invoke( + self, + callable: GlobalCallable, + args: Any, + output_fn: Callable[[Output], None], + ) -> Any: + """ + Invokes the callable with the given arguments, converted into the appropriate Q# values. + :param callable: The callable to invoke. + :param args: The arguments to pass to the callable. + :param output_fn: A callback function that will be called with each output. + :returns values: A result or runtime errors. + :raises QSharpError: If there is an error interpreting the input. + """ + ... + def qir(self, entry_expr: str) -> str: """ Generates QIR from Q# source code. diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 5bf6ba4f1a..198b52db37 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -1,7 +1,7 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from . import telemetry_events +from . import telemetry_events, code from ._native import ( Interpreter, TargetProfile, @@ -9,6 +9,7 @@ QSharpError, Output, Circuit, + GlobalCallable, ) from typing import ( Any, @@ -24,6 +25,8 @@ from .estimator._estimator import EstimatorResult, EstimatorParams import json import os +import sys +import types from time import monotonic _interpreter = None @@ -190,6 +193,25 @@ def init( f"Error reading {qsharp_json}. qsharp.json should exist at the project root and be a valid JSON file." ) from e + # Loop through the environment module and remove any dynamically added attributes that represent + # Q# callables. This is necessary to avoid conflicts with the new interpreter instance. + keys_to_remove = [] + for key in code.__dict__: + if hasattr(code.__dict__[key], "__qs_gen") or isinstance( + code.__dict__[key], types.ModuleType + ): + keys_to_remove.append(key) + for key in keys_to_remove: + code.__delattr__(key) + + # Also remove any namespace modules dynamically added to the system. + keys_to_remove = [] + for key in sys.modules: + if key.startswith("qsharp.code."): + keys_to_remove.append(key) + for key in keys_to_remove: + sys.modules.__delitem__(key) + _interpreter = Interpreter( target_profile, language_features, @@ -198,6 +220,7 @@ def init( list_directory, resolve, fetch_github, + _make_callable, ) _config = Config(target_profile, language_features, manifest_contents, project_root) @@ -372,6 +395,56 @@ def callback(output: Output) -> None: return results["result"] +# Helper function that knows how to create a function that invokes a callable. This will be +# used by the underlying native code to create functions for callables on the fly that know +# how to get the currently intitialized global interpreter instance. +def _make_callable(callable: GlobalCallable, namespace: List[str], callable_name: str): + module = code + # Create a name that will be used to collect the hierachy of namespace identifiers if they exist and use that + # to register created modules with the system. + accumulated_namespace = "qsharp.code" + accumulated_namespace += "." + for name in namespace: + accumulated_namespace += name + # Use the existing entry, which should already be a module. + if hasattr(module, name): + module = module.__getattribute__(name) + else: + # This namespace entry doesn't exist as a module yet, so create it, add it to the environment, and + # add it to sys.modules so it supports import properly. + new_module = types.ModuleType(accumulated_namespace) + module.__setattr__(name, new_module) + sys.modules[accumulated_namespace] = new_module + module = new_module + accumulated_namespace += "." + + def _callable(*args): + ipython_helper() + + def callback(output: Output) -> None: + if _in_jupyter: + try: + display(output) + return + except: + # If IPython is not available, fall back to printing the output + pass + print(output, flush=True) + + if len(args) == 1: + args = args[0] + elif len(args) == 0: + args = None + + return get_interpreter().invoke(callable, args, callback) + + # Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. + _callable.__qs_gen = True + + # Add the callable to the module. + module.__setattr__(callable_name, _callable) + + def run( entry_expr: str, shots: int, diff --git a/pip/qsharp/code/__init__.py b/pip/qsharp/code/__init__.py new file mode 100644 index 0000000000..695b54fb63 --- /dev/null +++ b/pip/qsharp/code/__init__.py @@ -0,0 +1,6 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +""" +Code module that receives any user-defined Q# callables as Python functions. +""" diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index f540360f77..6f1c41a255 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -11,16 +11,17 @@ use crate::{ noisy_simulator::register_noisy_simulator_submodule, }; use miette::{Diagnostic, Report}; -use num_bigint::BigUint; +use num_bigint::{BigInt, BigUint}; use num_complex::Complex64; use pyo3::{ create_exception, exceptions::{PyException, PyValueError}, prelude::*, - types::{PyComplex, PyDict, PyList, PyTuple, PyType}, + types::{PyComplex, PyDict, PyList, PyString, PyTuple, PyType}, }; use qsc::{ - fir, + fir::{self}, + hir::ty::{Prim, Ty}, interpret::{ self, output::{Error, Receiver}, @@ -73,6 +74,7 @@ fn _native<'a>(py: Python<'a>, m: &Bound<'a, PyModule>) -> PyResult<()> { m.add_class::()?; m.add_class::()?; m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(physical_estimates, m)?)?; m.add("QSharpError", py.get_type_bound::())?; register_noisy_simulator_submodule(py, m)?; @@ -213,9 +215,12 @@ impl From for qsc_qasm3::ProgramType { } } +#[allow(clippy::struct_field_names)] #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, + /// The Python function to call to create a new function wrapping a callable invocation. + pub(crate) make_callable: Option, } thread_local! { static PACKAGE_CACHE: Rc> = Rc::default(); } @@ -225,7 +230,7 @@ thread_local! { static PACKAGE_CACHE: Rc> = Rc::default(); impl Interpreter { #[allow(clippy::too_many_arguments)] #[allow(clippy::needless_pass_by_value)] - #[pyo3(signature = (target_profile, language_features=None, project_root=None, read_file=None, list_directory=None, resolve_path=None, fetch_github=None))] + #[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))] #[new] /// Initializes a new Q# interpreter. pub(crate) fn new( @@ -237,6 +242,7 @@ impl Interpreter { list_directory: Option, resolve_path: Option, fetch_github: Option, + make_callable: Option, ) -> PyResult { let target = Into::::into(target_profile).into(); @@ -278,7 +284,19 @@ impl Interpreter { buildable_program.store, &buildable_program.user_code_dependencies, ) { - Ok(interpreter) => Ok(Self { interpreter }), + Ok(interpreter) => { + if let Some(make_callable) = &make_callable { + // Add any global callables from the user source as Python functions to the environment. + let exported_items = interpreter.user_globals(); + for (namespace, name, val) in exported_items { + create_py_callable(py, make_callable, &namespace, &name, val)?; + } + } + Ok(Self { + interpreter, + make_callable, + }) + } Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } } @@ -300,7 +318,19 @@ impl Interpreter { ) -> PyResult { let mut receiver = OptionalCallbackReceiver { callback, py }; match self.interpreter.eval_fragments(&mut receiver, input) { - Ok(value) => Ok(ValueWrapper(value).into_py(py)), + Ok(value) => { + if let Some(make_callable) = &self.make_callable { + // Get any global callables from the evaluated input and add them to the environment. This will grab + // every callable that was defined in the input and by previous calls that added to the open package. + // This is safe because either the callable will be replaced with itself or a new callable with the + // same name will shadow the previous one, which is the expected behavior. + let new_items = self.interpreter.source_globals(); + for (namespace, name, val) in new_items { + create_py_callable(py, make_callable, &namespace, &name, val)?; + } + } + Ok(ValueWrapper(value).into_py(py)) + } Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } } @@ -357,6 +387,56 @@ impl Interpreter { } } + #[pyo3(signature=(callable, args=None, callback=None))] + fn invoke( + &mut self, + py: Python, + callable: GlobalCallable, + args: Option, + callback: Option, + ) -> PyResult { + let mut receiver = OptionalCallbackReceiver { callback, py }; + let (input_ty, output_ty) = self + .interpreter + .global_tys(&callable.0) + .ok_or(QSharpError::new_err("callable not found"))?; + + // If the types are not supported, we can't convert the arguments or return value. + // Check this before trying to convert the arguments, and return an error if the types are not supported. + if let Some(ty) = first_unsupported_interop_ty(&input_ty) { + return Err(QSharpError::new_err(format!( + "unsupported input type: `{ty}`" + ))); + } + if let Some(ty) = first_unsupported_interop_ty(&output_ty) { + return Err(QSharpError::new_err(format!( + "unsupported output type: `{ty}`" + ))); + } + + // Conver the Python arguments to Q# values, treating None as an empty tuple aka `Unit`. + let args = if matches!(&input_ty, Ty::Tuple(tup) if tup.is_empty()) { + // Special case for unit, where args should be None + if args.is_some() { + return Err(QSharpError::new_err("expected no arguments")); + } + Value::unit() + } else { + let Some(args) = args else { + return Err(QSharpError::new_err(format!( + "expected arguments of type `{input_ty}`" + ))); + }; + // This conversion will produce errors if the types don't match or can't be converted. + convert_obj_with_ty(py, &args, &input_ty)? + }; + + match self.interpreter.invoke(&mut receiver, callable.0, args) { + Ok(value) => Ok(ValueWrapper(value).into_py(py)), + Err(errors) => Err(QSharpError::new_err(format_errors(errors))), + } + } + fn qir(&mut self, _py: Python, entry_expr: &str) -> PyResult { match self.interpreter.qirgen(entry_expr) { Ok(qir) => Ok(qir), @@ -494,6 +574,85 @@ impl Interpreter { } } +/// Finds any Q# type recursively that does not support interop with Python, meaning our code cannot convert it back and forth +/// across the interop boundary. +fn first_unsupported_interop_ty(ty: &Ty) -> Option<&Ty> { + match ty { + Ty::Prim(prim_ty) => match prim_ty { + Prim::Pauli + | Prim::BigInt + | Prim::Bool + | Prim::Double + | Prim::Int + | Prim::String + | Prim::Result => None, + Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => { + Some(ty) + } + }, + Ty::Tuple(tup) => tup + .iter() + .find(|t| first_unsupported_interop_ty(t).is_some()), + Ty::Array(ty) => first_unsupported_interop_ty(ty), + _ => Some(ty), + } +} + +/// Given a type, convert a Python object into a Q# value of that type. This will recur through tuples and arrays, +/// and will return an error if the type is not supported or the object cannot be converted. +fn convert_obj_with_ty(py: Python, obj: &PyObject, ty: &Ty) -> PyResult { + match ty { + Ty::Prim(prim_ty) => match prim_ty { + Prim::BigInt => Ok(Value::BigInt(obj.extract::(py)?)), + Prim::Bool => Ok(Value::Bool(obj.extract::(py)?)), + Prim::Double => Ok(Value::Double(obj.extract::(py)?)), + Prim::Int => Ok(Value::Int(obj.extract::(py)?)), + Prim::String => Ok(Value::String(obj.extract::(py)?.into())), + Prim::Result => Ok(Value::Result(qsc::interpret::Result::Val( + obj.extract::(py)? == Result::One, + ))), + Prim::Pauli => Ok(Value::Pauli(match obj.extract::(py)? { + Pauli::I => fir::Pauli::I, + Pauli::X => fir::Pauli::X, + Pauli::Y => fir::Pauli::Y, + Pauli::Z => fir::Pauli::Z, + })), + Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => { + unimplemented!("primitive input type: {prim_ty:?}") + } + }, + Ty::Tuple(tup) => { + if tup.len() == 1 { + let value = convert_obj_with_ty(py, obj, &tup[0]); + Ok(Value::Tuple(vec![value?].into())) + } else { + let obj = obj.extract::>(py)?; + if obj.len() != tup.len() { + return Err(QSharpError::new_err(format!( + "mismatched tuple arity: expected {}, got {}", + tup.len(), + obj.len() + ))); + } + let mut values = Vec::with_capacity(obj.len()); + for (i, ty) in tup.iter().enumerate() { + values.push(convert_obj_with_ty(py, &obj[i], ty)?); + } + Ok(Value::Tuple(values.into())) + } + } + Ty::Array(ty) => { + let obj = obj.extract::>(py)?; + let mut values = Vec::with_capacity(obj.len()); + for item in &obj { + values.push(convert_obj_with_ty(py, item, ty)?); + } + Ok(Value::Array(values.into())) + } + _ => unimplemented!("input type: {ty}"), + } +} + #[pyfunction] pub fn physical_estimates(logical_resources: &str, job_params: &str) -> PyResult { match re::estimate_physical_resources_from_json(logical_resources, job_params) { @@ -840,3 +999,47 @@ where PyException::new_err(message) } } + +#[pyclass(unsendable)] +#[derive(Clone)] +struct GlobalCallable(Value); + +impl From for GlobalCallable { + fn from(val: Value) -> Self { + match val { + val @ Value::Global(..) => GlobalCallable(val), + _ => panic!("expected global callable"), + } + } +} + +impl From for Value { + fn from(val: GlobalCallable) -> Self { + val.0 + } +} + +/// Create a Python callable from a Q# callable and adds it to the given environment. +fn create_py_callable( + py: Python, + make_callable: &PyObject, + namespace: &[Rc], + name: &str, + val: Value, +) -> PyResult<()> { + if namespace.is_empty() && name == "" { + // We don't want to bind auto-generated lambda callables. + return Ok(()); + } + + let args = ( + Py::new(py, GlobalCallable::from(val)).expect("should be able to create callable"), // callable id + PyList::new_bound(py, namespace.iter().map(ToString::to_string)), // namespace as string array + PyString::new_bound(py, name), // name of callable + ); + + // Call into the Python layer to create the function wrapping the callable invocation. + make_callable.call1(py, args)?; + + Ok(()) +} diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index a1f9892587..f79541b070 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -3,6 +3,7 @@ import pytest import qsharp +import qsharp.code import qsharp.utils from contextlib import redirect_stdout import io @@ -404,3 +405,207 @@ def test_target_profile_from_str_match_enum_values() -> None: assert qsharp.TargetProfile.from_str(str_value) == target_profile with pytest.raises(ValueError): qsharp.TargetProfile.from_str("Invalid") + + +def test_callables_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("function Four() : Int { 4 }") + assert qsharp.code.Four() == 4, "callable should be available" + qsharp.eval("function Add(a : Int, b : Int) : Int { a + b }") + assert qsharp.code.Four() == 4, "first callable should still be available" + assert qsharp.code.Add(2, 3) == 5, "second callable should be available" + # After init, the callables should be cleared and no longer available + qsharp.init() + with pytest.raises(AttributeError): + qsharp.code.Four() + + +def test_callable_exposed_into_env_complex_types() -> None: + qsharp.eval( + "function Complicated(a : Int, b : (Double, BigInt)) : ((Double, BigInt), Int) { (b, a) }" + ) + assert qsharp.code.Complicated(2, (3.0, 4000000000000000000000)) == ( + (3.0, 4000000000000000000000), + 2, + ), "callables that take complex types should marshall them correctly" + + +def test_callable_exposed_into_env_with_array() -> None: + qsharp.init() + qsharp.eval("function Smallest(a : Int[]) : Int { Std.Math.Min(a)}") + assert ( + qsharp.code.Smallest([1, 2, 3, 0, 4, 5]) == 0 + ), "callable that takes array should work" + + +def test_callable_with_int_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : Int) : Int { a }") + assert qsharp.code.Identity(4) == 4 + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + with pytest.raises(OverflowError): + qsharp.code.Identity(4000000000000000000000) + with pytest.raises(TypeError): + qsharp.code.Identity([4]) + + +def test_callable_with_double_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : Double) : Double { a }") + assert qsharp.code.Identity(4.0) == 4.0 + assert qsharp.code.Identity(4) == 4.0 + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity([4]) + + +def test_callable_with_bigint_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : BigInt) : BigInt { a }") + assert qsharp.code.Identity(4000000000000000000000) == 4000000000000000000000 + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + + +def test_callable_with_string_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : String) : String { a }") + assert qsharp.code.Identity("4") == "4" + with pytest.raises(TypeError): + qsharp.code.Identity(4) + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + with pytest.raises(TypeError): + qsharp.code.Identity([4]) + + +def test_callable_with_bool_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : Bool) : Bool { a }") + assert qsharp.code.Identity(True) == True + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity(4) + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + with pytest.raises(TypeError): + qsharp.code.Identity([4]) + + +def test_callable_with_array_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : Int[]) : Int[] { a }") + assert qsharp.code.Identity([4, 5, 6]) == [4, 5, 6] + assert qsharp.code.Identity([]) == [] + assert qsharp.code.Identity((4, 5, 6)) == [4, 5, 6] + with pytest.raises(TypeError): + qsharp.code.Identity(4) + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + with pytest.raises(TypeError): + qsharp.code.Identity([1, 2, 3.0]) + + +def test_callable_with_tuple_exposed_into_env_fails_incorrect_types() -> None: + qsharp.init() + qsharp.eval("function Identity(a : (Int, Double)) : (Int, Double) { a }") + assert qsharp.code.Identity((4, 5.0)) == (4, 5.0) + assert qsharp.code.Identity((4, 5)) == (4, 5.0) + assert qsharp.code.Identity([4, 5.0]) == (4, 5.0) + with pytest.raises(qsharp.QSharpError): + qsharp.code.Identity((4, 5, 6)) + with pytest.raises(TypeError): + qsharp.code.Identity(4) + with pytest.raises(TypeError): + qsharp.code.Identity("4") + with pytest.raises(TypeError): + qsharp.code.Identity(4.0) + with pytest.raises(TypeError): + qsharp.code.Identity([4.0, 5]) + + +def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_reinit() -> ( + None +): + qsharp.init() + # callables should be created with their namespaces + qsharp.eval("namespace Test { function Four() : Int { 4 } }") + qsharp.eval("function Identity(a : Int) : Int { a }") + # should be able to import callables from env and namespace submodule + from qsharp.code import Identity + from qsharp.code.Test import Four + + assert Identity(4) == 4 + assert Four() == 4 + qsharp.init() + # namespaces should be removed + with pytest.raises(AttributeError): + qsharp.code.Test + with pytest.raises(AttributeError): + qsharp.code.Identity() + # imported callables should fail gracefully + with pytest.raises(qsharp.QSharpError): + Four() + + +def test_callables_with_unsupported_types_raise_errors_on_call() -> None: + qsharp.init() + qsharp.eval("function Unsupported(a : Int, q : Qubit) : Unit { }") + with pytest.raises(qsharp.QSharpError, match="unsupported input type: `Qubit`"): + qsharp.code.Unsupported() + + +def test_callables_with_unsupported_types_in_tuples_raise_errors_on_call() -> None: + qsharp.init() + qsharp.eval("function Unsupported(q : (Int, Qubit)[]) : Unit { }") + with pytest.raises(qsharp.QSharpError, match="unsupported input type: `Qubit`"): + qsharp.code.Unsupported() + + +def test_callables_with_unsupported_return_types_raise_errors_on_call() -> None: + qsharp.init() + qsharp.eval('function Unsupported() : Qubit { fail "won\'t be called" }') + with pytest.raises(qsharp.QSharpError, match="unsupported output type: `Qubit`"): + qsharp.code.Unsupported() + + +def test_callables_with_unsupported_udt_types_raise_errors_on_call() -> None: + qsharp.init() + qsharp.eval("function Unsupported(a : Std.Math.Complex) : Unit { }") + with pytest.raises( + qsharp.QSharpError, match='unsupported input type: `UDT<"Complex":' + ): + qsharp.code.Unsupported() + + +def test_callable_with_unsupported_udt_return_types_raise_errors_on_call() -> None: + qsharp.init() + qsharp.eval('function Unsupported() : Std.Math.Complex { fail "won\'t be called" }') + with pytest.raises( + qsharp.QSharpError, match='unsupported output type: `UDT<"Complex":' + ): + qsharp.code.Unsupported() + + +def test_struct_call_constructor_not_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("struct CustomUDT { a : Int }") + with pytest.raises(AttributeError): + qsharp.code.CustomUDT + + +def test_lambdas_not_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("a -> a + 1") + assert not hasattr(qsharp.code, "") + qsharp.eval("q => I(q)") + assert not hasattr(qsharp.code, "")