From 2ba55215ab9dc102abb2af28034235a02e590169 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 12:18:51 -0800 Subject: [PATCH 01/15] Expose user-defined callables into Python This change adds new interop capabilities for the qsharp Python module. Any user-defined global callables with types supported by interop will automatically be surfaced into the `qsharp.env` module and available for invocation or import directly in Python. --- compiler/qsc/src/interpret.rs | 95 ++++++++- compiler/qsc/src/interpret/tests.rs | 215 ++++++++++++++++++++ compiler/qsc/src/lib.rs | 3 +- compiler/qsc_eval/src/lib.rs | 45 +++++ compiler/qsc_frontend/src/error.rs | 17 +- compiler/qsc_frontend/src/error/tests.rs | 4 +- compiler/qsc_hir/src/global.rs | 11 ++ language_service/src/state.rs | 4 +- pip/qsharp/__init__.py | 5 +- pip/qsharp/_native.pyi | 27 +++ pip/qsharp/_qsharp.py | 52 ++++- pip/qsharp/env/__init__.py | 2 + pip/src/interpreter.rs | 241 ++++++++++++++++++++++- pip/tests/test_qsharp.py | 76 +++++++ wasm/src/diagnostic.rs | 4 +- 15 files changed, 778 insertions(+), 23 deletions(-) create mode 100644 pip/qsharp/env/__init__.py diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index e1560bac14..fd19fdb462 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,70 @@ impl Interpreter { }) } + /// Given a package ID, returns all the global items in the package. + /// Note this does not currently include re-exports. + pub fn get_global_items( + &self, + package_id: PackageId, + ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { + 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, store_item_id)); + } + } + exported_items + } + + /// Get the global items defined in the user source passed into initialization of the interpreter. + pub fn get_source_package_global_items( + &self, + ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { + self.get_global_items(self.source_package) + } + + /// Get the global items defined in the open package being interpreted, which will include any items + /// defined by calls to `eval_fragments` and the like. + pub fn get_open_package_global_items(&self) -> Vec<(Vec>, Rc, fir::StoreItemId)> { + self.get_global_items(self.package) + } + + /// Get the input and output types of a given callable item. + /// # Panics + /// Panics if the item is not callable or a type that can be invoked as a callable. + pub fn get_callable_tys(&self, item_id: fir::StoreItemId) -> Option<(ty::Ty, ty::Ty)> { + 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); @@ -482,6 +549,32 @@ impl Interpreter { self.run_with_sim(&mut sim, receiver, expr) } + /// 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, + ) + }) + } /// Gets the current quantum state of the simulator. pub fn get_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { self.sim.capture_quantum_state() @@ -1033,7 +1126,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..bdd3ee184f 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.get_source_package_global_items(); + 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.get_open_package_global_items(); + 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.get_open_package_global_items(); + 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.get_open_package_global_items(); + 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.get_open_package_global_items(); + 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.get_open_package_global_items(); + 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.get_source_package_global_items(); + 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_frontend/src/error.rs b/compiler/qsc_frontend/src/error.rs index 81e54a28a0..3da222b51c 100644 --- a/compiler/qsc_frontend/src/error.rs +++ b/compiler/qsc_frontend/src/error.rs @@ -40,9 +40,11 @@ impl WithSource { .flatten() .map(|label| u32::try_from(label.offset()).expect("offset should fit into u32")) { - let source = sources - .find_by_offset(offset) - .expect("expected to find source at offset"); + let Some(source) = sources.find_by_offset(offset) else { + // If the source is not found, skip it. This can happen when the originating line for this particular label + // comes from Python code, which is not included in the source map. + continue; + }; // Keep the vector sorted by source offsets match filtered.binary_search_by_key(&source.offset, |s| s.offset) { @@ -70,15 +72,16 @@ impl WithSource { /// Takes a span that uses `SourceMap` offsets, and returns /// a span that is relative to the `Source` that the span falls into, /// along with a reference to the `Source`. - pub fn resolve_span(&self, span: &SourceSpan) -> (&Source, SourceSpan) { + pub fn resolve_span(&self, span: &SourceSpan) -> Result<(&Source, SourceSpan), MietteError> { let offset = u32::try_from(span.offset()).expect("expected the offset to fit into u32"); + // Since some sources may be missing, treat any failure to find the source as "out of bounds" error. let source = self .sources .iter() .rev() .find(|source| offset >= source.offset) - .expect("expected to find source at span"); - (source, with_offset(span, |o| o - (source.offset as usize))) + .ok_or(MietteError::OutOfBounds)?; + Ok((source, with_offset(span, |o| o - (source.offset as usize)))) } } @@ -135,7 +138,7 @@ impl SourceCode for WithSource { context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { - let (source, source_relative_span) = self.resolve_span(span); + let (source, source_relative_span) = self.resolve_span(span)?; let contents = source.contents.read_span( &source_relative_span, diff --git a/compiler/qsc_frontend/src/error/tests.rs b/compiler/qsc_frontend/src/error/tests.rs index c643e68459..aa009e310a 100644 --- a/compiler/qsc_frontend/src/error/tests.rs +++ b/compiler/qsc_frontend/src/error/tests.rs @@ -136,7 +136,9 @@ fn resolve_spans() { .labels() .expect("expected labels to exist") .map(|l| { - let resolved = with_source.resolve_span(l.inner()); + let resolved = with_source + .resolve_span(l.inner()) + .expect("expected to resolve span"); ( resolved.0.name.to_string(), resolved.1.offset(), 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/language_service/src/state.rs b/language_service/src/state.rs index 3eaed8c13c..9e495d5d70 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -566,8 +566,8 @@ fn map_errors_to_docs( .flatten() .next() .map_or(compilation_uri, |l| { - let (source, _) = err.resolve_span(l.inner()); - &source.name + err.resolve_span(l.inner()) + .map_or(compilation_uri, |(s, _)| &s.name) }); map.entry(doc.clone()) diff --git a/pip/qsharp/__init__.py b/pip/qsharp/__init__.py index 3b9984cfae..a259762e9a 100644 --- a/pip/qsharp/__init__.py +++ b/pip/qsharp/__init__.py @@ -23,7 +23,7 @@ telemetry_events.on_import() -from ._native import Result, Pauli, QSharpError, TargetProfile +from ._native import Result, Pauli, QSharpError, TargetProfile, GlobalCallable # IPython notebook specific features try: @@ -56,5 +56,6 @@ "PauliNoise", "DepolarizingNoise", "BitFlipNoise", - "PhaseFlipNoise" + "PhaseFlipNoise", + "GlobalCallable", ] diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 0bf47a2d5b..5e9cc4ed0f 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,8 @@ class Interpreter: read_file: Callable[[str], Tuple[str, str]], list_directory: Callable[[str], List[Dict[str, str]]], resolve_path: Callable[[str, str], str], + env: Optional[Any], + make_callable: Optional[Callable[[GlobalCallable], Callable]], ) -> None: """ Initializes the Q# interpreter. @@ -123,6 +132,8 @@ 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 env: An object on which to add any globally accesible callables from the compiled source. + :param make_callable: A function that converts a GlobalCallable into a callable object. """ ... @@ -159,6 +170,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 84a083c770..80b77227fe 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, env from ._native import ( Interpreter, TargetProfile, @@ -24,6 +24,8 @@ from .estimator._estimator import EstimatorResult, EstimatorParams import json import os +import sys +import types from time import monotonic _interpreter = None @@ -190,6 +192,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 env.__dict__: + if hasattr(env.__dict__[key], "__qs_gen") or isinstance( + env.__dict__[key], types.ModuleType + ): + keys_to_remove.append(key) + for key in keys_to_remove: + env.__delattr__(key) + + # Also remove any namespace modules dynamically added to the system. + keys_to_remove = [] + for key in sys.modules: + if key.startswith("qsharp.env."): + keys_to_remove.append(key) + for key in keys_to_remove: + sys.modules.__delitem__(key) + _interpreter = Interpreter( target_profile, language_features, @@ -198,6 +219,8 @@ def init( list_directory, resolve, fetch_github, + env, + _make_callable, ) _config = Config(target_profile, language_features, manifest_contents, project_root) @@ -252,6 +275,33 @@ def callback(output: Output) -> None: return results +# 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): + 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) + + return _callable + + class ShotResult(TypedDict): """ A single result of a shot. diff --git a/pip/qsharp/env/__init__.py b/pip/qsharp/env/__init__.py new file mode 100644 index 0000000000..59e481eb93 --- /dev/null +++ b/pip/qsharp/env/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 78b4b00a10..12ab786a2f 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, StoreItemId}, + hir::ty::{Prim, Ty}, interpret::{ self, output::{Error, Receiver}, @@ -29,7 +30,7 @@ use qsc::{ packages::BuildableProgram, project::{FileSystem, PackageCache, PackageGraphSources}, target::Profile, - LanguageFeatures, PackageType, SourceMap, + FunctorApp, LanguageFeatures, PackageType, SourceMap, }; use resource_estimator::{self as re, estimate_expr}; @@ -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,14 @@ impl From for qsc_qasm3::ProgramType { } } +#[allow(clippy::struct_field_names)] #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, + /// The Python environment to which new callables will be added. + pub(crate) env: Option, + /// 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 +232,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, env=None, make_callable=None))] #[new] /// Initializes a new Q# interpreter. pub(crate) fn new( @@ -237,6 +244,8 @@ impl Interpreter { list_directory: Option, resolve_path: Option, fetch_github: Option, + env: Option, + make_callable: Option, ) -> PyResult { let target = Into::::into(target_profile).into(); @@ -278,7 +287,24 @@ impl Interpreter { buildable_program.store, &buildable_program.user_code_dependencies, ) { - Ok(interpreter) => Ok(Self { interpreter }), + Ok(interpreter) => { + if let (Some(env), Some(make_callable)) = (&env, &make_callable) { + // Add any global callables from the user source whose types are supported by interop as + // Python functions to the environment. + let exported_items = interpreter.get_source_package_global_items(); + for (namespace, name, item_id) in exported_items + .iter() + .filter(|(_, _, id)| item_supports_interop(&interpreter, *id)) + { + create_py_callable(py, env, make_callable, namespace, name, *item_id)?; + } + } + Ok(Self { + interpreter, + env, + make_callable, + }) + } Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } } @@ -300,7 +326,22 @@ 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(env), Some(make_callable)) = (&self.env, &self.make_callable) { + // Get any global callables from the evaluated input whose types are supported by interop 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.get_open_package_global_items(); + for (namespace, name, item_id) in new_items + .iter() + .filter(|(_, _, id)| item_supports_interop(&self.interpreter, *id)) + { + create_py_callable(py, env, make_callable, namespace, name, *item_id)?; + } + } + Ok(ValueWrapper(value).into_py(py)) + } Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } } @@ -357,6 +398,45 @@ 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 (ty, _) = self + .interpreter + .get_callable_tys(callable.id) + .ok_or(QSharpError::new_err("callable not found"))?; + + let callable = Value::Global(callable.id, FunctorApp::default()); + + // Conver the Python arguments to Q# values, treating None as an empty tuple aka `Unit`. + let args = if matches!(&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 `{ty}`" + ))); + }; + // This conversion will produce errors if the types don't match or can't be converted. + convert_obj_with_ty(py, &args, &ty)? + }; + + match self.interpreter.invoke(&mut receiver, callable, 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,89 @@ impl Interpreter { } } +/// Check if the given item supports interop with Python by verifying the input and output types. +fn item_supports_interop(interpreter: &interpret::Interpreter, id: StoreItemId) -> bool { + let (input_ty, output_ty) = interpreter + .get_callable_tys(id) + .expect("item should exist in package"); + type_supports_interop(&input_ty) && type_supports_interop(&output_ty) +} + +/// Confirm a Q# type supports with interop with Python, meaning our code knows how to convert it back and forth +/// across the interop boundary. +fn type_supports_interop(ty: &Ty) -> bool { + match ty { + Ty::Prim(prim_ty) => match prim_ty { + Prim::Pauli + | Prim::BigInt + | Prim::Bool + | Prim::Double + | Prim::Int + | Prim::String + | Prim::Result => true, + Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => false, + }, + Ty::Tuple(tup) => tup.iter().all(type_supports_interop), + Ty::Array(ty) => type_supports_interop(ty), + _ => false, + } +} + +/// 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 => Err( + PyException::new_err(format!("unhandled 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())) + } + _ => Err(PyException::new_err(format!("unhandled 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) { @@ -828,3 +991,67 @@ where PyException::new_err(message) } } + +#[pyclass] +#[derive(Clone, Copy)] +struct GlobalCallable { + id: StoreItemId, +} + +/// Create a Python callable from a Q# callable and adds it to the given environment. +fn create_py_callable( + py: Python, + env: &PyObject, + make_callable: &PyObject, + namespace: &[Rc], + name: &str, + item_id: StoreItemId, +) -> PyResult<()> { + if namespace.is_empty() && name == "lambda" { + // We don't want to bind auto-generated lambda callables. + return Ok(()); + } + + // env is expected to be a module, any other type will raise an error. + let mut module: Bound = env.extract(py)?; + + // Create a name that will be used to collect the hieirchy of namespace identifiers if they exist and use that + // to register created modules with the system. + let mut accumulated_namespace: String = module.name()?.extract()?; + accumulated_namespace.push('.'); + for name in namespace { + accumulated_namespace.push_str(name); + let py_name = PyString::new_bound(py, name); + module = if let Ok(module) = module.as_any().getattr(py_name.clone()) { + // Use the existing entry, which should already be a module. + module.extract()? + } 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 support import properly. + let new_module = PyModule::new_bound(py, &accumulated_namespace)?; + module.add(py_name, &new_module)?; + py.import_bound("sys")? + .getattr("modules")? + .set_item(accumulated_namespace.clone(), new_module.clone())?; + new_module + }; + accumulated_namespace.push('.'); + } + + // Call into the Python layer to create the function wrapping the callable invocation. + let callable = make_callable.call1( + py, + PyTuple::new_bound( + py, + &[Py::new(py, GlobalCallable { id: item_id }) + .expect("should be able to create callable")], + ), + )?; + + // Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. + callable.setattr(py, "__qs_gen", true.into_py(py))?; + + // Add the callable to the module. + module.add(name, callable)?; + Ok(()) +} diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 0779972fa5..2f0a65a9dd 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -257,6 +257,7 @@ def test_dump_operation() -> None: else: assert res[i][j] == complex(0.0, 0.0) + def test_run_with_noise_produces_noisy_results() -> None: qsharp.init() qsharp.set_quantum_seed(0) @@ -273,6 +274,7 @@ def test_run_with_noise_produces_noisy_results() -> None: ) assert result[0] > 5 + def test_compile_qir_input_data() -> None: qsharp.init(target_profile=qsharp.TargetProfile.Base) qsharp.eval("operation Program() : Result { use q = Qubit(); return M(q) }") @@ -373,3 +375,77 @@ 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.env.Four() == 4 + qsharp.eval("function Add(a : Int, b : Int) : Int { a + b }") + assert qsharp.env.Four() == 4 + assert qsharp.env.Add(2, 3) == 5 + qsharp.eval( + "function Complicated(a : Int, b : (Double, BigInt)) : ((Double, BigInt), Int) { (b, a) }" + ) + assert qsharp.env.Complicated(2, (3.0, 4000000000000000000)) == ( + (3.0, 4000000000000000000), + 2, + ) + qsharp.eval("function Smallest(a : Int[]) : Int { Std.Math.Min(a)}") + assert qsharp.env.Smallest([1, 2, 3, 0, 4, 5]) == 0 + qsharp.init() + # After init, the callables should be cleared and no longer available + with pytest.raises(AttributeError): + qsharp.env.Four() + qsharp.eval("function Identity(a : Int) : Int { a }") + assert qsharp.env.Identity(4) == 4 + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.Identity(4.0) + # callables should be created with their namespaces + qsharp.eval("namespace Test { function Four() : Int { 4 } }") + assert qsharp.env.Test.Four() == 4 + # should be able to import callables + from qsharp.env import Identity + from qsharp.env.Test import Four + + assert Identity(4) == 4 + assert Four() == 4 + qsharp.init() + # namespaces should be removed + with pytest.raises(AttributeError): + qsharp.env.Test + # imported callables should fail gracefully + with pytest.raises(qsharp.QSharpError): + Four() + + +def test_callables_with_unsupported_types_not_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("function Unsupported(q : Qubit) : Unit { }") + with pytest.raises(AttributeError): + qsharp.env.Unsupported + qsharp.eval("function Unsupported(q : Qubit[]) : Unit { }") + with pytest.raises(AttributeError): + qsharp.env.Unsupported + qsharp.eval('function Unsupported() : Qubit { fail "won\'t be called" }') + with pytest.raises(AttributeError): + qsharp.env.Unsupported + qsharp.eval("function Unsupported(a : Std.Math.Complex) : Unit { }") + with pytest.raises(AttributeError): + qsharp.env.Unsupported + qsharp.eval('function Unsupported() : Std.Math.Complex { fail "won\'t be called" }') + with pytest.raises(AttributeError): + qsharp.env.Unsupported + qsharp.eval("struct Unsupported { a : Int }") + with pytest.raises(AttributeError): + qsharp.env.Unsupported + + +def test_lambdas_not_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("a -> a + 1") + assert not hasattr(qsharp.env, "lambda") + qsharp.eval("q => I(q)") + assert not hasattr(qsharp.env, "lambda") diff --git a/wasm/src/diagnostic.rs b/wasm/src/diagnostic.rs index b5c2e6dc6b..48dbfd06bf 100644 --- a/wasm/src/diagnostic.rs +++ b/wasm/src/diagnostic.rs @@ -195,7 +195,9 @@ fn resolve_label(e: &WithSource, labeled_span: &LabeledSpan) -> Label where T: Diagnostic + Send + Sync, { - let (source, span) = e.resolve_span(labeled_span.inner()); + let (source, span) = e + .resolve_span(labeled_span.inner()) + .expect("expected span to resolve"); let start = u32::try_from(span.offset()).expect("offset should fit in u32"); let len = u32::try_from(span.len()).expect("length should fit in u32"); let range = qsc::line_column::Range::from_span( From d06cff477cfa69db83c6ad70abffc9c2b19f4156 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 13:49:44 -0800 Subject: [PATCH 02/15] Adjust lambda name --- pip/src/interpreter.rs | 2 +- pip/tests/test_qsharp.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 12ab786a2f..b8631e87cd 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -1007,7 +1007,7 @@ fn create_py_callable( name: &str, item_id: StoreItemId, ) -> PyResult<()> { - if namespace.is_empty() && name == "lambda" { + if namespace.is_empty() && name == "" { // We don't want to bind auto-generated lambda callables. return Ok(()); } diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 2f0a65a9dd..20bee1881d 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -446,6 +446,6 @@ def test_callables_with_unsupported_types_not_exposed_into_env() -> None: def test_lambdas_not_exposed_into_env() -> None: qsharp.init() qsharp.eval("a -> a + 1") - assert not hasattr(qsharp.env, "lambda") + assert not hasattr(qsharp.env, "") qsharp.eval("q => I(q)") - assert not hasattr(qsharp.env, "lambda") + assert not hasattr(qsharp.env, "") From 3dec0172b049c7166d8365a5cb320ad51f7399b6 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 16:34:52 -0800 Subject: [PATCH 03/15] Revert error span handling changes --- compiler/qsc_frontend/src/error.rs | 16 +++++++--------- compiler/qsc_frontend/src/error/tests.rs | 4 +--- language_service/src/state.rs | 4 ++-- wasm/src/diagnostic.rs | 4 +--- 4 files changed, 11 insertions(+), 17 deletions(-) diff --git a/compiler/qsc_frontend/src/error.rs b/compiler/qsc_frontend/src/error.rs index 3da222b51c..dab663309e 100644 --- a/compiler/qsc_frontend/src/error.rs +++ b/compiler/qsc_frontend/src/error.rs @@ -40,11 +40,9 @@ impl WithSource { .flatten() .map(|label| u32::try_from(label.offset()).expect("offset should fit into u32")) { - let Some(source) = sources.find_by_offset(offset) else { - // If the source is not found, skip it. This can happen when the originating line for this particular label - // comes from Python code, which is not included in the source map. - continue; - }; + let source = sources + .find_by_offset(offset) + .expect("expected to find source at offset"); // Keep the vector sorted by source offsets match filtered.binary_search_by_key(&source.offset, |s| s.offset) { @@ -72,7 +70,7 @@ impl WithSource { /// Takes a span that uses `SourceMap` offsets, and returns /// a span that is relative to the `Source` that the span falls into, /// along with a reference to the `Source`. - pub fn resolve_span(&self, span: &SourceSpan) -> Result<(&Source, SourceSpan), MietteError> { + pub fn resolve_span(&self, span: &SourceSpan) -> (&Source, SourceSpan) { let offset = u32::try_from(span.offset()).expect("expected the offset to fit into u32"); // Since some sources may be missing, treat any failure to find the source as "out of bounds" error. let source = self @@ -80,8 +78,8 @@ impl WithSource { .iter() .rev() .find(|source| offset >= source.offset) - .ok_or(MietteError::OutOfBounds)?; - Ok((source, with_offset(span, |o| o - (source.offset as usize)))) + .expect("expected to find source at span"); + (source, with_offset(span, |o| o - (source.offset as usize))) } } @@ -138,7 +136,7 @@ impl SourceCode for WithSource { context_lines_before: usize, context_lines_after: usize, ) -> Result + 'a>, MietteError> { - let (source, source_relative_span) = self.resolve_span(span)?; + let (source, source_relative_span) = self.resolve_span(span); let contents = source.contents.read_span( &source_relative_span, diff --git a/compiler/qsc_frontend/src/error/tests.rs b/compiler/qsc_frontend/src/error/tests.rs index aa009e310a..c643e68459 100644 --- a/compiler/qsc_frontend/src/error/tests.rs +++ b/compiler/qsc_frontend/src/error/tests.rs @@ -136,9 +136,7 @@ fn resolve_spans() { .labels() .expect("expected labels to exist") .map(|l| { - let resolved = with_source - .resolve_span(l.inner()) - .expect("expected to resolve span"); + let resolved = with_source.resolve_span(l.inner()); ( resolved.0.name.to_string(), resolved.1.offset(), diff --git a/language_service/src/state.rs b/language_service/src/state.rs index 9e495d5d70..3eaed8c13c 100644 --- a/language_service/src/state.rs +++ b/language_service/src/state.rs @@ -566,8 +566,8 @@ fn map_errors_to_docs( .flatten() .next() .map_or(compilation_uri, |l| { - err.resolve_span(l.inner()) - .map_or(compilation_uri, |(s, _)| &s.name) + let (source, _) = err.resolve_span(l.inner()); + &source.name }); map.entry(doc.clone()) diff --git a/wasm/src/diagnostic.rs b/wasm/src/diagnostic.rs index 48dbfd06bf..b5c2e6dc6b 100644 --- a/wasm/src/diagnostic.rs +++ b/wasm/src/diagnostic.rs @@ -195,9 +195,7 @@ fn resolve_label(e: &WithSource, labeled_span: &LabeledSpan) -> Label where T: Diagnostic + Send + Sync, { - let (source, span) = e - .resolve_span(labeled_span.inner()) - .expect("expected span to resolve"); + let (source, span) = e.resolve_span(labeled_span.inner()); let start = u32::try_from(span.offset()).expect("offset should fit in u32"); let len = u32::try_from(span.len()).expect("length should fit in u32"); let range = qsc::line_column::Range::from_span( From 4625c6e2a8a9a868efd674952c872f95fb5586cb Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 16:35:41 -0800 Subject: [PATCH 04/15] Remove old comment --- compiler/qsc_frontend/src/error.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/compiler/qsc_frontend/src/error.rs b/compiler/qsc_frontend/src/error.rs index dab663309e..81e54a28a0 100644 --- a/compiler/qsc_frontend/src/error.rs +++ b/compiler/qsc_frontend/src/error.rs @@ -72,7 +72,6 @@ impl WithSource { /// along with a reference to the `Source`. pub fn resolve_span(&self, span: &SourceSpan) -> (&Source, SourceSpan) { let offset = u32::try_from(span.offset()).expect("expected the offset to fit into u32"); - // Since some sources may be missing, treat any failure to find the source as "out of bounds" error. let source = self .sources .iter() From ca59f2c6c7f2d4caf8aa44b964556562dcd47654 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 21:17:54 -0800 Subject: [PATCH 05/15] Some PR feedback --- pip/src/interpreter.rs | 8 ++++---- pip/tests/test_qsharp.py | 20 +++++++++++++++++--- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index b8631e87cd..048485ea43 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -621,9 +621,9 @@ fn convert_obj_with_ty(py: Python, obj: &PyObject, ty: &Ty) -> PyResult { Pauli::Y => fir::Pauli::Y, Pauli::Z => fir::Pauli::Z, })), - Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => Err( - PyException::new_err(format!("unhandled primitive input type: {prim_ty:?}")), - ), + Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => { + unimplemented!("primitive input type: {prim_ty:?}") + } }, Ty::Tuple(tup) => { if tup.len() == 1 { @@ -653,7 +653,7 @@ fn convert_obj_with_ty(py: Python, obj: &PyObject, ty: &Ty) -> PyResult { } Ok(Value::Array(values.into())) } - _ => Err(PyException::new_err(format!("unhandled input type: {ty}"))), + _ => unimplemented!("input type: {ty}"), } } diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 20bee1881d..ec4ee9092c 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -384,6 +384,13 @@ def test_callables_exposed_into_env() -> None: qsharp.eval("function Add(a : Int, b : Int) : Int { a + b }") assert qsharp.env.Four() == 4 assert qsharp.env.Add(2, 3) == 5 + # After init, the callables should be cleared and no longer available + qsharp.init() + with pytest.raises(AttributeError): + qsharp.env.Four() + + +def test_callable_exposed_into_env_complex_types() -> None: qsharp.eval( "function Complicated(a : Int, b : (Double, BigInt)) : ((Double, BigInt), Int) { (b, a) }" ) @@ -393,18 +400,25 @@ def test_callables_exposed_into_env() -> None: ) qsharp.eval("function Smallest(a : Int[]) : Int { Std.Math.Min(a)}") assert qsharp.env.Smallest([1, 2, 3, 0, 4, 5]) == 0 + + +def test_callable_exposed_into_env_fails_incorrect_types() -> None: qsharp.init() - # After init, the callables should be cleared and no longer available - with pytest.raises(AttributeError): - qsharp.env.Four() qsharp.eval("function Identity(a : Int) : Int { a }") assert qsharp.env.Identity(4) == 4 with pytest.raises(TypeError): qsharp.env.Identity("4") with pytest.raises(TypeError): qsharp.env.Identity(4.0) + + +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 }") assert qsharp.env.Test.Four() == 4 # should be able to import callables from qsharp.env import Identity From e02a300b5270e0d3cfe4b0a23cc17ec250abcfb2 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Fri, 6 Dec 2024 22:13:18 -0800 Subject: [PATCH 06/15] Expose unsupported callables with error at runtime --- pip/src/interpreter.rs | 70 ++++++++++++++++++++-------------------- pip/tests/test_qsharp.py | 38 +++++++++++++--------- 2 files changed, 58 insertions(+), 50 deletions(-) diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 048485ea43..aadb611ac4 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -289,14 +289,10 @@ impl Interpreter { ) { Ok(interpreter) => { if let (Some(env), Some(make_callable)) = (&env, &make_callable) { - // Add any global callables from the user source whose types are supported by interop as - // Python functions to the environment. + // Add any global callables from the user source as Python functions to the environment. let exported_items = interpreter.get_source_package_global_items(); - for (namespace, name, item_id) in exported_items - .iter() - .filter(|(_, _, id)| item_supports_interop(&interpreter, *id)) - { - create_py_callable(py, env, make_callable, namespace, name, *item_id)?; + for (namespace, name, item_id) in exported_items { + create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; } } Ok(Self { @@ -328,16 +324,13 @@ impl Interpreter { match self.interpreter.eval_fragments(&mut receiver, input) { Ok(value) => { if let (Some(env), Some(make_callable)) = (&self.env, &self.make_callable) { - // Get any global callables from the evaluated input whose types are supported by interop 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. + // 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.get_open_package_global_items(); - for (namespace, name, item_id) in new_items - .iter() - .filter(|(_, _, id)| item_supports_interop(&self.interpreter, *id)) - { - create_py_callable(py, env, make_callable, namespace, name, *item_id)?; + for (namespace, name, item_id) in new_items { + create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; } } Ok(ValueWrapper(value).into_py(py)) @@ -407,15 +400,28 @@ impl Interpreter { callback: Option, ) -> PyResult { let mut receiver = OptionalCallbackReceiver { callback, py }; - let (ty, _) = self + let (input_ty, output_ty) = self .interpreter .get_callable_tys(callable.id) .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_unsupport_interop_ty(&input_ty) { + return Err(QSharpError::new_err(format!( + "unsupported input type: `{ty}`" + ))); + } + if let Some(ty) = first_unsupport_interop_ty(&output_ty) { + return Err(QSharpError::new_err(format!( + "unsupported output type: `{ty}`" + ))); + } + let callable = Value::Global(callable.id, FunctorApp::default()); // Conver the Python arguments to Q# values, treating None as an empty tuple aka `Unit`. - let args = if matches!(&ty, Ty::Tuple(tup) if tup.is_empty()) { + 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")); @@ -424,11 +430,11 @@ impl Interpreter { } else { let Some(args) = args else { return Err(QSharpError::new_err(format!( - "expected arguments of type `{ty}`" + "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, &ty)? + convert_obj_with_ty(py, &args, &input_ty)? }; match self.interpreter.invoke(&mut receiver, callable, args) { @@ -574,17 +580,9 @@ impl Interpreter { } } -/// Check if the given item supports interop with Python by verifying the input and output types. -fn item_supports_interop(interpreter: &interpret::Interpreter, id: StoreItemId) -> bool { - let (input_ty, output_ty) = interpreter - .get_callable_tys(id) - .expect("item should exist in package"); - type_supports_interop(&input_ty) && type_supports_interop(&output_ty) -} - -/// Confirm a Q# type supports with interop with Python, meaning our code knows how to convert it back and forth +/// 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 type_supports_interop(ty: &Ty) -> bool { +fn first_unsupport_interop_ty(ty: &Ty) -> Option<&Ty> { match ty { Ty::Prim(prim_ty) => match prim_ty { Prim::Pauli @@ -593,12 +591,14 @@ fn type_supports_interop(ty: &Ty) -> bool { | Prim::Double | Prim::Int | Prim::String - | Prim::Result => true, - Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => false, + | Prim::Result => None, + Prim::Qubit | Prim::Range | Prim::RangeTo | Prim::RangeFrom | Prim::RangeFull => { + Some(ty) + } }, - Ty::Tuple(tup) => tup.iter().all(type_supports_interop), - Ty::Array(ty) => type_supports_interop(ty), - _ => false, + Ty::Tuple(tup) => tup.iter().find(|t| first_unsupport_interop_ty(t).is_some()), + Ty::Array(ty) => first_unsupport_interop_ty(ty), + _ => Some(ty), } } diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index ec4ee9092c..2c025bfe5f 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -435,26 +435,34 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein Four() -def test_callables_with_unsupported_types_not_exposed_into_env() -> None: +def test_callables_with_unsupported_types_raise_errors_on_call() -> None: qsharp.init() - qsharp.eval("function Unsupported(q : Qubit) : Unit { }") - with pytest.raises(AttributeError): - qsharp.env.Unsupported - qsharp.eval("function Unsupported(q : Qubit[]) : Unit { }") - with pytest.raises(AttributeError): - qsharp.env.Unsupported + qsharp.eval("function Unsupported(a : Int, q : Qubit) : Unit { }") + with pytest.raises(qsharp.QSharpError, match="unsupported input type: `Qubit`"): + qsharp.env.Unsupported() + qsharp.eval("function Unsupported(q : (Int, Qubit)[]) : Unit { }") + with pytest.raises(qsharp.QSharpError, match="unsupported input type: `Qubit`"): + qsharp.env.Unsupported() qsharp.eval('function Unsupported() : Qubit { fail "won\'t be called" }') - with pytest.raises(AttributeError): - qsharp.env.Unsupported + with pytest.raises(qsharp.QSharpError, match="unsupported output type: `Qubit`"): + qsharp.env.Unsupported() qsharp.eval("function Unsupported(a : Std.Math.Complex) : Unit { }") - with pytest.raises(AttributeError): - qsharp.env.Unsupported + with pytest.raises( + qsharp.QSharpError, match='unsupported input type: `UDT<"Complex":' + ): + qsharp.env.Unsupported() qsharp.eval('function Unsupported() : Std.Math.Complex { fail "won\'t be called" }') + with pytest.raises( + qsharp.QSharpError, match='unsupported output type: `UDT<"Complex":' + ): + qsharp.env.Unsupported() + + +def test_struct_call_constructor_not_exposed_into_env() -> None: + qsharp.init() + qsharp.eval("struct CustomUDT { a : Int }") with pytest.raises(AttributeError): - qsharp.env.Unsupported - qsharp.eval("struct Unsupported { a : Int }") - with pytest.raises(AttributeError): - qsharp.env.Unsupported + qsharp.env.CustomUDT def test_lambdas_not_exposed_into_env() -> None: From f50aa9f080bd3574019219666af24cbabe9779c9 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Mon, 9 Dec 2024 13:37:10 -0800 Subject: [PATCH 07/15] Add comment to `env` module --- pip/qsharp/env/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pip/qsharp/env/__init__.py b/pip/qsharp/env/__init__.py index 59e481eb93..5ab98a9e5f 100644 --- a/pip/qsharp/env/__init__.py +++ b/pip/qsharp/env/__init__.py @@ -1,2 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. + +# Environment module that receives any user-defined Q# callables as Python functions. From 91b6c482fbef1369410129a59f2afa44f9004961 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 10 Dec 2024 12:50:55 -0800 Subject: [PATCH 08/15] Split up tests, add a few more --- pip/tests/test_qsharp.py | 127 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 117 insertions(+), 10 deletions(-) diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 2c025bfe5f..86caac8ceb 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -3,6 +3,7 @@ import pytest import qsharp +import qsharp.env import qsharp.utils from contextlib import redirect_stdout import io @@ -380,10 +381,10 @@ def test_target_profile_from_str_match_enum_values() -> None: def test_callables_exposed_into_env() -> None: qsharp.init() qsharp.eval("function Four() : Int { 4 }") - assert qsharp.env.Four() == 4 + assert qsharp.env.Four() == 4, "callable should be available" qsharp.eval("function Add(a : Int, b : Int) : Int { a + b }") - assert qsharp.env.Four() == 4 - assert qsharp.env.Add(2, 3) == 5 + assert qsharp.env.Four() == 4, "first callable should still be available" + assert qsharp.env.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): @@ -394,15 +395,21 @@ 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.env.Complicated(2, (3.0, 4000000000000000000)) == ( - (3.0, 4000000000000000000), + assert qsharp.env.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.env.Smallest([1, 2, 3, 0, 4, 5]) == 0 + assert ( + qsharp.env.Smallest([1, 2, 3, 0, 4, 5]) == 0 + ), "callable that takes array should work" -def test_callable_exposed_into_env_fails_incorrect_types() -> None: +def test_callable_with_int_exposed_into_env_fails_incorrect_types() -> None: qsharp.init() qsharp.eval("function Identity(a : Int) : Int { a }") assert qsharp.env.Identity(4) == 4 @@ -410,6 +417,91 @@ def test_callable_exposed_into_env_fails_incorrect_types() -> None: qsharp.env.Identity("4") with pytest.raises(TypeError): qsharp.env.Identity(4.0) + with pytest.raises(OverflowError): + qsharp.env.Identity(4000000000000000000000) + with pytest.raises(TypeError): + qsharp.env.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.env.Identity(4.0) == 4.0 + assert qsharp.env.Identity(4) == 4.0 + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.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.env.Identity(4000000000000000000000) == 4000000000000000000000 + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.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.env.Identity("4") == "4" + with pytest.raises(TypeError): + qsharp.env.Identity(4) + with pytest.raises(TypeError): + qsharp.env.Identity(4.0) + with pytest.raises(TypeError): + qsharp.env.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.env.Identity(True) == True + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.Identity(4) + with pytest.raises(TypeError): + qsharp.env.Identity(4.0) + with pytest.raises(TypeError): + qsharp.env.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.env.Identity([4, 5, 6]) == [4, 5, 6] + assert qsharp.env.Identity([]) == [] + assert qsharp.env.Identity((4, 5, 6)) == [4, 5, 6] + with pytest.raises(TypeError): + qsharp.env.Identity(4) + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.Identity(4.0) + with pytest.raises(TypeError): + qsharp.env.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.env.Identity((4, 5.0)) == (4, 5.0) + assert qsharp.env.Identity((4, 5)) == (4, 5.0) + assert qsharp.env.Identity([4, 5.0]) == (4, 5.0) + with pytest.raises(qsharp.QSharpError): + qsharp.env.Identity((4, 5, 6)) + with pytest.raises(TypeError): + qsharp.env.Identity(4) + with pytest.raises(TypeError): + qsharp.env.Identity("4") + with pytest.raises(TypeError): + qsharp.env.Identity(4.0) + with pytest.raises(TypeError): + qsharp.env.Identity([4.0, 5]) def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_reinit() -> ( @@ -419,8 +511,7 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein # callables should be created with their namespaces qsharp.eval("namespace Test { function Four() : Int { 4 } }") qsharp.eval("function Identity(a : Int) : Int { a }") - assert qsharp.env.Test.Four() == 4 - # should be able to import callables + # should be able to import callables from env and namespace submodule from qsharp.env import Identity from qsharp.env.Test import Four @@ -440,17 +531,33 @@ def test_callables_with_unsupported_types_raise_errors_on_call() -> None: qsharp.eval("function Unsupported(a : Int, q : Qubit) : Unit { }") with pytest.raises(qsharp.QSharpError, match="unsupported input type: `Qubit`"): qsharp.env.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.env.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.env.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.env.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":' From 301a38980dd0325287c967b0c4751ef5e758bdb3 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 10 Dec 2024 16:11:50 -0800 Subject: [PATCH 09/15] refactor to get rid of env argument to constructor --- pip/qsharp/_native.pyi | 6 ++-- pip/qsharp/_qsharp.py | 28 +++++++++++++++++-- pip/src/interpreter.rs | 59 ++++++++-------------------------------- pip/tests/test_qsharp.py | 2 ++ 4 files changed, 40 insertions(+), 55 deletions(-) diff --git a/pip/qsharp/_native.pyi b/pip/qsharp/_native.pyi index 5e9cc4ed0f..37a50b7f9b 100644 --- a/pip/qsharp/_native.pyi +++ b/pip/qsharp/_native.pyi @@ -121,8 +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], - env: Optional[Any], - make_callable: Optional[Callable[[GlobalCallable], Callable]], + make_callable: Optional[Callable[[GlobalCallable], None]], ) -> None: """ Initializes the Q# interpreter. @@ -132,8 +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 env: An object on which to add any globally accesible callables from the compiled source. - :param make_callable: A function that converts a GlobalCallable into a callable object. + :param make_callable: A function that registers a Q# callable in the in the environment module. """ ... diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 80b77227fe..84c457a07e 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -219,7 +219,6 @@ def init( list_directory, resolve, fetch_github, - env, _make_callable, ) @@ -278,7 +277,26 @@ def callback(output: Output) -> None: # 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): +def _make_callable(callable, namespace, callable_name): + module = env + # 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.env" + 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() @@ -299,7 +317,11 @@ def callback(output: Output) -> None: return get_interpreter().invoke(callable, args, callback) - return _callable + # 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) class ShotResult(TypedDict): diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index aadb611ac4..8cb886015e 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -219,8 +219,6 @@ impl From for qsc_qasm3::ProgramType { #[pyclass(unsendable)] pub(crate) struct Interpreter { pub(crate) interpreter: interpret::Interpreter, - /// The Python environment to which new callables will be added. - pub(crate) env: Option, /// The Python function to call to create a new function wrapping a callable invocation. pub(crate) make_callable: Option, } @@ -232,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, env=None, make_callable=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( @@ -244,7 +242,6 @@ impl Interpreter { list_directory: Option, resolve_path: Option, fetch_github: Option, - env: Option, make_callable: Option, ) -> PyResult { let target = Into::::into(target_profile).into(); @@ -288,16 +285,15 @@ impl Interpreter { &buildable_program.user_code_dependencies, ) { Ok(interpreter) => { - if let (Some(env), Some(make_callable)) = (&env, &make_callable) { + 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.get_source_package_global_items(); for (namespace, name, item_id) in exported_items { - create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; + create_py_callable(py, make_callable, &namespace, &name, item_id)?; } } Ok(Self { interpreter, - env, make_callable, }) } @@ -323,14 +319,14 @@ impl Interpreter { let mut receiver = OptionalCallbackReceiver { callback, py }; match self.interpreter.eval_fragments(&mut receiver, input) { Ok(value) => { - if let (Some(env), Some(make_callable)) = (&self.env, &self.make_callable) { + 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.get_open_package_global_items(); for (namespace, name, item_id) in new_items { - create_py_callable(py, env, make_callable, &namespace, &name, item_id)?; + create_py_callable(py, make_callable, &namespace, &name, item_id)?; } } Ok(ValueWrapper(value).into_py(py)) @@ -1001,7 +997,6 @@ struct GlobalCallable { /// Create a Python callable from a Q# callable and adds it to the given environment. fn create_py_callable( py: Python, - env: &PyObject, make_callable: &PyObject, namespace: &[Rc], name: &str, @@ -1012,46 +1007,14 @@ fn create_py_callable( return Ok(()); } - // env is expected to be a module, any other type will raise an error. - let mut module: Bound = env.extract(py)?; - - // Create a name that will be used to collect the hieirchy of namespace identifiers if they exist and use that - // to register created modules with the system. - let mut accumulated_namespace: String = module.name()?.extract()?; - accumulated_namespace.push('.'); - for name in namespace { - accumulated_namespace.push_str(name); - let py_name = PyString::new_bound(py, name); - module = if let Ok(module) = module.as_any().getattr(py_name.clone()) { - // Use the existing entry, which should already be a module. - module.extract()? - } 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 support import properly. - let new_module = PyModule::new_bound(py, &accumulated_namespace)?; - module.add(py_name, &new_module)?; - py.import_bound("sys")? - .getattr("modules")? - .set_item(accumulated_namespace.clone(), new_module.clone())?; - new_module - }; - accumulated_namespace.push('.'); - } + let args = ( + Py::new(py, GlobalCallable { id: item_id }).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. - let callable = make_callable.call1( - py, - PyTuple::new_bound( - py, - &[Py::new(py, GlobalCallable { id: item_id }) - .expect("should be able to create callable")], - ), - )?; - - // Each callable is annotated so that we know it is auto-generated and can be removed on a re-init of the interpreter. - callable.setattr(py, "__qs_gen", true.into_py(py))?; + make_callable.call1(py, args)?; - // Add the callable to the module. - module.add(name, callable)?; Ok(()) } diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 86caac8ceb..84d4434f0b 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -521,6 +521,8 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein # namespaces should be removed with pytest.raises(AttributeError): qsharp.env.Test + with pytest.raises(AttributeError): + qsharp.env.Identity() # imported callables should fail gracefully with pytest.raises(qsharp.QSharpError): Four() From bd50c864555b86ee22d0df6bae7a79783e43ba82 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 10 Dec 2024 16:39:30 -0800 Subject: [PATCH 10/15] Change name from `env` to `code` --- pip/qsharp/__init__.py | 3 +- pip/qsharp/_qsharp.py | 16 +++--- pip/qsharp/code/__init__.py | 6 ++ pip/qsharp/env/__init__.py | 4 -- pip/tests/test_qsharp.py | 110 ++++++++++++++++++------------------ 5 files changed, 70 insertions(+), 69 deletions(-) create mode 100644 pip/qsharp/code/__init__.py delete mode 100644 pip/qsharp/env/__init__.py diff --git a/pip/qsharp/__init__.py b/pip/qsharp/__init__.py index a259762e9a..d588a68e77 100644 --- a/pip/qsharp/__init__.py +++ b/pip/qsharp/__init__.py @@ -23,7 +23,7 @@ telemetry_events.on_import() -from ._native import Result, Pauli, QSharpError, TargetProfile, GlobalCallable +from ._native import Result, Pauli, QSharpError, TargetProfile # IPython notebook specific features try: @@ -57,5 +57,4 @@ "DepolarizingNoise", "BitFlipNoise", "PhaseFlipNoise", - "GlobalCallable", ] diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index 84c457a07e..c40f3d3086 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, env +from . import telemetry_events, code from ._native import ( Interpreter, TargetProfile, @@ -195,18 +195,18 @@ def init( # 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 env.__dict__: - if hasattr(env.__dict__[key], "__qs_gen") or isinstance( - env.__dict__[key], types.ModuleType + 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: - env.__delattr__(key) + 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.env."): + if key.startswith("qsharp.code."): keys_to_remove.append(key) for key in keys_to_remove: sys.modules.__delitem__(key) @@ -278,10 +278,10 @@ def callback(output: Output) -> None: # 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, namespace, callable_name): - module = env + 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.env" + accumulated_namespace = "qsharp.code" accumulated_namespace += "." for name in namespace: accumulated_namespace += name 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/qsharp/env/__init__.py b/pip/qsharp/env/__init__.py deleted file mode 100644 index 5ab98a9e5f..0000000000 --- a/pip/qsharp/env/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -# Copyright (c) Microsoft Corporation. -# Licensed under the MIT License. - -# Environment module that receives any user-defined Q# callables as Python functions. diff --git a/pip/tests/test_qsharp.py b/pip/tests/test_qsharp.py index 84d4434f0b..880927adfe 100644 --- a/pip/tests/test_qsharp.py +++ b/pip/tests/test_qsharp.py @@ -3,7 +3,7 @@ import pytest import qsharp -import qsharp.env +import qsharp.code import qsharp.utils from contextlib import redirect_stdout import io @@ -381,21 +381,21 @@ def test_target_profile_from_str_match_enum_values() -> None: def test_callables_exposed_into_env() -> None: qsharp.init() qsharp.eval("function Four() : Int { 4 }") - assert qsharp.env.Four() == 4, "callable should be available" + assert qsharp.code.Four() == 4, "callable should be available" qsharp.eval("function Add(a : Int, b : Int) : Int { a + b }") - assert qsharp.env.Four() == 4, "first callable should still be available" - assert qsharp.env.Add(2, 3) == 5, "second callable should be available" + 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.env.Four() + 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.env.Complicated(2, (3.0, 4000000000000000000000)) == ( + assert qsharp.code.Complicated(2, (3.0, 4000000000000000000000)) == ( (3.0, 4000000000000000000000), 2, ), "callables that take complex types should marshall them correctly" @@ -405,103 +405,103 @@ def test_callable_exposed_into_env_with_array() -> None: qsharp.init() qsharp.eval("function Smallest(a : Int[]) : Int { Std.Math.Min(a)}") assert ( - qsharp.env.Smallest([1, 2, 3, 0, 4, 5]) == 0 + 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.env.Identity(4) == 4 + assert qsharp.code.Identity(4) == 4 with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + qsharp.code.Identity(4.0) with pytest.raises(OverflowError): - qsharp.env.Identity(4000000000000000000000) + qsharp.code.Identity(4000000000000000000000) with pytest.raises(TypeError): - qsharp.env.Identity([4]) + 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.env.Identity(4.0) == 4.0 - assert qsharp.env.Identity(4) == 4.0 + assert qsharp.code.Identity(4.0) == 4.0 + assert qsharp.code.Identity(4) == 4.0 with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity([4]) + 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.env.Identity(4000000000000000000000) == 4000000000000000000000 + assert qsharp.code.Identity(4000000000000000000000) == 4000000000000000000000 with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + 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.env.Identity("4") == "4" + assert qsharp.code.Identity("4") == "4" with pytest.raises(TypeError): - qsharp.env.Identity(4) + qsharp.code.Identity(4) with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + qsharp.code.Identity(4.0) with pytest.raises(TypeError): - qsharp.env.Identity([4]) + 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.env.Identity(True) == True + assert qsharp.code.Identity(True) == True with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity(4) + qsharp.code.Identity(4) with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + qsharp.code.Identity(4.0) with pytest.raises(TypeError): - qsharp.env.Identity([4]) + 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.env.Identity([4, 5, 6]) == [4, 5, 6] - assert qsharp.env.Identity([]) == [] - assert qsharp.env.Identity((4, 5, 6)) == [4, 5, 6] + 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.env.Identity(4) + qsharp.code.Identity(4) with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + qsharp.code.Identity(4.0) with pytest.raises(TypeError): - qsharp.env.Identity([1, 2, 3.0]) + 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.env.Identity((4, 5.0)) == (4, 5.0) - assert qsharp.env.Identity((4, 5)) == (4, 5.0) - assert qsharp.env.Identity([4, 5.0]) == (4, 5.0) + 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.env.Identity((4, 5, 6)) + qsharp.code.Identity((4, 5, 6)) with pytest.raises(TypeError): - qsharp.env.Identity(4) + qsharp.code.Identity(4) with pytest.raises(TypeError): - qsharp.env.Identity("4") + qsharp.code.Identity("4") with pytest.raises(TypeError): - qsharp.env.Identity(4.0) + qsharp.code.Identity(4.0) with pytest.raises(TypeError): - qsharp.env.Identity([4.0, 5]) + qsharp.code.Identity([4.0, 5]) def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_reinit() -> ( @@ -512,17 +512,17 @@ def test_callables_in_namespaces_exposed_into_env_submodules_and_removed_on_rein 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.env import Identity - from qsharp.env.Test import Four + 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.env.Test + qsharp.code.Test with pytest.raises(AttributeError): - qsharp.env.Identity() + qsharp.code.Identity() # imported callables should fail gracefully with pytest.raises(qsharp.QSharpError): Four() @@ -532,21 +532,21 @@ 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.env.Unsupported() + 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.env.Unsupported() + 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.env.Unsupported() + qsharp.code.Unsupported() def test_callables_with_unsupported_udt_types_raise_errors_on_call() -> None: @@ -555,7 +555,7 @@ def test_callables_with_unsupported_udt_types_raise_errors_on_call() -> None: with pytest.raises( qsharp.QSharpError, match='unsupported input type: `UDT<"Complex":' ): - qsharp.env.Unsupported() + qsharp.code.Unsupported() def test_callable_with_unsupported_udt_return_types_raise_errors_on_call() -> None: @@ -564,19 +564,19 @@ def test_callable_with_unsupported_udt_return_types_raise_errors_on_call() -> No with pytest.raises( qsharp.QSharpError, match='unsupported output type: `UDT<"Complex":' ): - qsharp.env.Unsupported() + 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.env.CustomUDT + qsharp.code.CustomUDT def test_lambdas_not_exposed_into_env() -> None: qsharp.init() qsharp.eval("a -> a + 1") - assert not hasattr(qsharp.env, "") + assert not hasattr(qsharp.code, "") qsharp.eval("q => I(q)") - assert not hasattr(qsharp.env, "") + assert not hasattr(qsharp.code, "") From a960ee3045e1c02810cc781dcde0a00e15cbceb2 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 11 Dec 2024 12:48:02 -0800 Subject: [PATCH 11/15] Add types, fix name typo --- pip/qsharp/_qsharp.py | 3 ++- pip/src/interpreter.rs | 12 +++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/pip/qsharp/_qsharp.py b/pip/qsharp/_qsharp.py index c40f3d3086..24403bceff 100644 --- a/pip/qsharp/_qsharp.py +++ b/pip/qsharp/_qsharp.py @@ -9,6 +9,7 @@ QSharpError, Output, Circuit, + GlobalCallable, ) from typing import ( Any, @@ -277,7 +278,7 @@ def callback(output: Output) -> None: # 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, namespace, callable_name): +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. diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 8cb886015e..cce72606ab 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -403,12 +403,12 @@ impl Interpreter { // 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_unsupport_interop_ty(&input_ty) { + 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_unsupport_interop_ty(&output_ty) { + if let Some(ty) = first_unsupported_interop_ty(&output_ty) { return Err(QSharpError::new_err(format!( "unsupported output type: `{ty}`" ))); @@ -578,7 +578,7 @@ 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_unsupport_interop_ty(ty: &Ty) -> Option<&Ty> { +fn first_unsupported_interop_ty(ty: &Ty) -> Option<&Ty> { match ty { Ty::Prim(prim_ty) => match prim_ty { Prim::Pauli @@ -592,8 +592,10 @@ fn first_unsupport_interop_ty(ty: &Ty) -> Option<&Ty> { Some(ty) } }, - Ty::Tuple(tup) => tup.iter().find(|t| first_unsupport_interop_ty(t).is_some()), - Ty::Array(ty) => first_unsupport_interop_ty(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), } } From e41ecddf6c6ee7b2162a27f4982dcb5e48ee2378 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 11 Dec 2024 13:15:16 -0800 Subject: [PATCH 12/15] Change to "current_package" name --- compiler/qsc/src/interpret.rs | 4 +++- compiler/qsc/src/interpret/tests.rs | 10 +++++----- pip/src/interpreter.rs | 2 +- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index fd19fdb462..9618c29a16 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -352,7 +352,9 @@ impl Interpreter { /// Get the global items defined in the open package being interpreted, which will include any items /// defined by calls to `eval_fragments` and the like. - pub fn get_open_package_global_items(&self) -> Vec<(Vec>, Rc, fir::StoreItemId)> { + pub fn get_current_package_global_items( + &self, + ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { self.get_global_items(self.package) } diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index bdd3ee184f..e32993dbf2 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -541,7 +541,7 @@ mod given_interpreter { let mut interpreter = get_interpreter(); let (result, output) = line(&mut interpreter, "()"); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_open_package_global_items(); + let items = interpreter.get_current_package_global_items(); assert!(items.is_empty()); } @@ -556,7 +556,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_open_package_global_items(); + let items = interpreter.get_current_package_global_items(); assert_eq!(items.len(), 2); // No namespace for top-level items assert!(items[0].0.is_empty()); @@ -584,7 +584,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_open_package_global_items(); + let items = interpreter.get_current_package_global_items(); assert_eq!(items.len(), 1); expect![[r#" [ @@ -609,7 +609,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_open_package_global_items(); + let items = interpreter.get_current_package_global_items(); assert_eq!(items.len(), 2); let (result, output) = line( &mut interpreter, @@ -619,7 +619,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_open_package_global_items(); + let items = interpreter.get_current_package_global_items(); assert_eq!(items.len(), 4); // No namespace for top-level items assert!(items[0].0.is_empty()); diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index cce72606ab..433f998961 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -324,7 +324,7 @@ impl Interpreter { // 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.get_open_package_global_items(); + let new_items = self.interpreter.get_current_package_global_items(); for (namespace, name, item_id) in new_items { create_py_callable(py, make_callable, &namespace, &name, item_id)?; } From fad5d4a450851b9417c92cc55c60a0f6e1dec9f0 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 11 Dec 2024 13:51:59 -0800 Subject: [PATCH 13/15] Move `invoke` up so it's next to `eval_fragments` --- compiler/qsc/src/interpret.rs | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index 9618c29a16..5063b69871 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -536,21 +536,6 @@ impl Interpreter { ) } - /// Runs the given entry expression on a new instance of the environment and simulator, - /// but using the current compilation. - pub fn run( - &mut self, - receiver: &mut impl Receiver, - expr: Option<&str>, - noise: Option, - ) -> InterpretResult { - let mut sim = match noise { - Some(noise) => SparseSim::new_with_noise(&noise), - None => SparseSim::new(), - }; - self.run_with_sim(&mut sim, receiver, expr) - } - /// Invokes the given callable with the given arguments using the current environment, simlator, and compilation. pub fn invoke( &mut self, @@ -577,6 +562,22 @@ impl Interpreter { ) }) } + + /// Runs the given entry expression on a new instance of the environment and simulator, + /// but using the current compilation. + pub fn run( + &mut self, + receiver: &mut impl Receiver, + expr: Option<&str>, + noise: Option, + ) -> InterpretResult { + let mut sim = match noise { + Some(noise) => SparseSim::new_with_noise(&noise), + None => SparseSim::new(), + }; + self.run_with_sim(&mut sim, receiver, expr) + } + /// Gets the current quantum state of the simulator. pub fn get_quantum_state(&mut self) -> (Vec<(BigUint, Complex)>, usize) { self.sim.capture_quantum_state() From ba543dddcb51ac24bb82693bb0437742ad86ce7c Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Wed, 11 Dec 2024 14:17:40 -0800 Subject: [PATCH 14/15] Use `Value` as type exposed from interpreter --- compiler/qsc/src/interpret.rs | 34 +++++++++++------------ compiler/qsc/src/interpret/tests.rs | 14 +++++----- pip/src/interpreter.rs | 42 +++++++++++++++++++---------- 3 files changed, 52 insertions(+), 38 deletions(-) diff --git a/compiler/qsc/src/interpret.rs b/compiler/qsc/src/interpret.rs index 5063b69871..2c736ee701 100644 --- a/compiler/qsc/src/interpret.rs +++ b/compiler/qsc/src/interpret.rs @@ -320,10 +320,7 @@ impl Interpreter { /// Given a package ID, returns all the global items in the package. /// Note this does not currently include re-exports. - pub fn get_global_items( - &self, - package_id: PackageId, - ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { + fn package_globals(&self, package_id: PackageId) -> Vec<(Vec>, Rc, Value)> { let mut exported_items = Vec::new(); let package = &self .compiler @@ -337,31 +334,34 @@ impl Interpreter { package: package_id, item: fir::LocalItemId::from(usize::from(term.id.item)), }; - exported_items.push((global.namespace, global.name, store_item_id)); + exported_items.push(( + global.namespace, + global.name, + Value::Global(store_item_id, FunctorApp::default()), + )); } } exported_items } - /// Get the global items defined in the user source passed into initialization of the interpreter. - pub fn get_source_package_global_items( - &self, - ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { - self.get_global_items(self.source_package) + /// 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 items defined in the open package being interpreted, which will include any items + /// 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 get_current_package_global_items( - &self, - ) -> Vec<(Vec>, Rc, fir::StoreItemId)> { - self.get_global_items(self.package) + pub fn source_globals(&self) -> Vec<(Vec>, Rc, Value)> { + self.package_globals(self.package) } - /// Get the input and output types of a given callable item. + /// 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 get_callable_tys(&self, item_id: fir::StoreItemId) -> Option<(ty::Ty, ty::Ty)> { + 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 diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index e32993dbf2..2b82256ce3 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -532,7 +532,7 @@ mod given_interpreter { #[test] fn interpreter_without_sources_has_no_items() { let interpreter = get_interpreter(); - let items = interpreter.get_source_package_global_items(); + let items = interpreter.user_globals(); assert!(items.is_empty()); } @@ -541,7 +541,7 @@ mod given_interpreter { let mut interpreter = get_interpreter(); let (result, output) = line(&mut interpreter, "()"); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_current_package_global_items(); + let items = interpreter.source_globals(); assert!(items.is_empty()); } @@ -556,7 +556,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_current_package_global_items(); + let items = interpreter.source_globals(); assert_eq!(items.len(), 2); // No namespace for top-level items assert!(items[0].0.is_empty()); @@ -584,7 +584,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_current_package_global_items(); + let items = interpreter.source_globals(); assert_eq!(items.len(), 1); expect![[r#" [ @@ -609,7 +609,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_current_package_global_items(); + let items = interpreter.source_globals(); assert_eq!(items.len(), 2); let (result, output) = line( &mut interpreter, @@ -619,7 +619,7 @@ mod given_interpreter { "#}, ); is_only_value(&result, &output, &Value::unit()); - let items = interpreter.get_current_package_global_items(); + let items = interpreter.source_globals(); assert_eq!(items.len(), 4); // No namespace for top-level items assert!(items[0].0.is_empty()); @@ -1899,7 +1899,7 @@ mod given_interpreter { ) .expect("interpreter should be created"); - let items = interpreter.get_source_package_global_items(); + let items = interpreter.user_globals(); assert_eq!(1, items.len()); expect![[r#" [ diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 433f998961..36e7b8d1a8 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -287,9 +287,9 @@ impl 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.get_source_package_global_items(); - for (namespace, name, item_id) in exported_items { - create_py_callable(py, make_callable, &namespace, &name, item_id)?; + let exported_items = interpreter.user_globals(); + for (namespace, name, val) in exported_items { + create_py_callable(py, make_callable, &namespace, &name, val)?; } } Ok(Self { @@ -324,9 +324,9 @@ impl Interpreter { // 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.get_current_package_global_items(); - for (namespace, name, item_id) in new_items { - create_py_callable(py, make_callable, &namespace, &name, item_id)?; + 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)) @@ -398,7 +398,7 @@ impl Interpreter { let mut receiver = OptionalCallbackReceiver { callback, py }; let (input_ty, output_ty) = self .interpreter - .get_callable_tys(callable.id) + .global_tys(&callable.into()) .ok_or(QSharpError::new_err("callable not found"))?; // If the types are not supported, we can't convert the arguments or return value. @@ -414,8 +414,6 @@ impl Interpreter { ))); } - let callable = Value::Global(callable.id, FunctorApp::default()); - // 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 @@ -433,7 +431,10 @@ impl Interpreter { convert_obj_with_ty(py, &args, &input_ty)? }; - match self.interpreter.invoke(&mut receiver, callable, args) { + match self + .interpreter + .invoke(&mut receiver, callable.into(), args) + { Ok(value) => Ok(ValueWrapper(value).into_py(py)), Err(errors) => Err(QSharpError::new_err(format_errors(errors))), } @@ -992,8 +993,21 @@ where #[pyclass] #[derive(Clone, Copy)] -struct GlobalCallable { - id: StoreItemId, +struct GlobalCallable(StoreItemId, FunctorApp); + +impl From for GlobalCallable { + fn from(val: Value) -> Self { + match val { + Value::Global(id, app) => GlobalCallable(id, app), + _ => panic!("expected global callable"), + } + } +} + +impl From for Value { + fn from(val: GlobalCallable) -> Self { + Value::Global(val.0, val.1) + } } /// Create a Python callable from a Q# callable and adds it to the given environment. @@ -1002,7 +1016,7 @@ fn create_py_callable( make_callable: &PyObject, namespace: &[Rc], name: &str, - item_id: StoreItemId, + val: Value, ) -> PyResult<()> { if namespace.is_empty() && name == "" { // We don't want to bind auto-generated lambda callables. @@ -1010,7 +1024,7 @@ fn create_py_callable( } let args = ( - Py::new(py, GlobalCallable { id: item_id }).expect("should be able to create callable"), // callable id + 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 ); From 60093852d19338896addc40f47e0206058fd130e Mon Sep 17 00:00:00 2001 From: Mine Starks <16928427+minestarks@users.noreply.github.com> Date: Thu, 12 Dec 2024 18:19:52 +0000 Subject: [PATCH 15/15] don't unwrap and rewrap Value --- pip/src/interpreter.rs | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/pip/src/interpreter.rs b/pip/src/interpreter.rs index 36e7b8d1a8..e2a3a2f480 100644 --- a/pip/src/interpreter.rs +++ b/pip/src/interpreter.rs @@ -20,7 +20,7 @@ use pyo3::{ types::{PyComplex, PyDict, PyList, PyString, PyTuple, PyType}, }; use qsc::{ - fir::{self, StoreItemId}, + fir::{self}, hir::ty::{Prim, Ty}, interpret::{ self, @@ -30,7 +30,7 @@ use qsc::{ packages::BuildableProgram, project::{FileSystem, PackageCache, PackageGraphSources}, target::Profile, - FunctorApp, LanguageFeatures, PackageType, SourceMap, + LanguageFeatures, PackageType, SourceMap, }; use resource_estimator::{self as re, estimate_expr}; @@ -398,7 +398,7 @@ impl Interpreter { let mut receiver = OptionalCallbackReceiver { callback, py }; let (input_ty, output_ty) = self .interpreter - .global_tys(&callable.into()) + .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. @@ -431,10 +431,7 @@ impl Interpreter { convert_obj_with_ty(py, &args, &input_ty)? }; - match self - .interpreter - .invoke(&mut receiver, callable.into(), args) - { + 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))), } @@ -991,14 +988,14 @@ where } } -#[pyclass] -#[derive(Clone, Copy)] -struct GlobalCallable(StoreItemId, FunctorApp); +#[pyclass(unsendable)] +#[derive(Clone)] +struct GlobalCallable(Value); impl From for GlobalCallable { fn from(val: Value) -> Self { match val { - Value::Global(id, app) => GlobalCallable(id, app), + val @ Value::Global(..) => GlobalCallable(val), _ => panic!("expected global callable"), } } @@ -1006,7 +1003,7 @@ impl From for GlobalCallable { impl From for Value { fn from(val: GlobalCallable) -> Self { - Value::Global(val.0, val.1) + val.0 } }