Skip to content

Commit

Permalink
Expose user-defined callables into Python
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
swernli committed Dec 6, 2024
1 parent 645243b commit 2ba5521
Show file tree
Hide file tree
Showing 15 changed files with 778 additions and 23 deletions.
95 changes: 94 additions & 1 deletion compiler/qsc/src/interpret.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ mod package_tests;
#[cfg(test)]
mod tests;

use std::rc::Rc;

pub use qsc_eval::{
debug::Frame,
noise::PauliNoise,
Expand All @@ -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;
Expand Down Expand Up @@ -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<str>>, Rc<str>, 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<str>>, Rc<str>, 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<str>>, Rc<str>, 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<u64>) {
self.quantum_seed = seed;
self.sim.set_seed(seed);
Expand Down Expand Up @@ -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<f64>)>, usize) {
self.sim.capture_quantum_state()
Expand Down Expand Up @@ -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;
Expand Down
215 changes: 215 additions & 0 deletions compiler/qsc/src/interpret/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<u8>::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;
Expand Down Expand Up @@ -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 =
Expand Down Expand Up @@ -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(
Expand Down
3 changes: 2 additions & 1 deletion compiler/qsc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down
Loading

0 comments on commit 2ba5521

Please sign in to comment.