Skip to content

Commit

Permalink
Update test callable discovery to work within the language service (#…
Browse files Browse the repository at this point in the history
…2095)

This PR rewrites #2059 to trigger test discovery from within the
language server, as opposed to externally in an entirely separate
context. Please read the description in that PR for more detail.

It also adds tests for LS state in both the Rust-based LS tests and the
JS-based `basics.js` npm API tests.

---------

Co-authored-by: Mine Starks <[email protected]>
  • Loading branch information
2 people authored and idavis committed Jan 31, 2025
1 parent 64ac896 commit c2a3c56
Show file tree
Hide file tree
Showing 45 changed files with 1,461 additions and 138 deletions.
12 changes: 12 additions & 0 deletions compiler/qsc_frontend/src/lower.rs
Original file line number Diff line number Diff line change
Expand Up @@ -443,6 +443,18 @@ impl With<'_> {
None
}
},
Ok(hir::Attr::Test) => {
// verify that no args are passed to the attribute
match &*attr.arg.kind {
ast::ExprKind::Tuple(args) if args.is_empty() => {}
_ => {
self.lowerer
.errors
.push(Error::InvalidAttrArgs("()".to_string(), attr.arg.span));
}
}
Some(hir::Attr::Test)
}
Err(()) => {
self.lowerer.errors.push(Error::UnknownAttr(
attr.name.name.to_string(),
Expand Down
56 changes: 56 additions & 0 deletions compiler/qsc_hir/src/hir.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,58 @@ impl Display for Package {
}
}

/// The name of a test callable, including its parent namespace.
pub type TestCallableName = String;

impl Package {
/// Returns a collection of the fully qualified names of any callables annotated with `@Test()`
pub fn get_test_callables(&self) -> Vec<(TestCallableName, Span)> {
let items_with_test_attribute = self
.items
.iter()
.filter(|(_, item)| item.attrs.iter().any(|attr| *attr == Attr::Test));

let callables = items_with_test_attribute
.filter(|(_, item)| matches!(item.kind, ItemKind::Callable(_)));

let callable_names = callables
.filter_map(|(_, item)| -> Option<_> {
if let ItemKind::Callable(callable) = &item.kind {
if !callable.generics.is_empty()
|| callable.input.kind != PatKind::Tuple(vec![])
{
return None;
}

// this is indeed a test callable, so let's grab its parent name
let (name, span) = match item.parent {
None => Default::default(),
Some(parent_id) => {
let parent_item = self
.items
.get(parent_id)
.expect("Parent item did not exist in package");
let name = if let ItemKind::Namespace(ns, _) = &parent_item.kind {
format!("{}.{}", ns.name(), callable.name.name)
} else {
callable.name.name.to_string()
};
let span = callable.name.span;
(name, span)
}
};

Some((name, span))
} else {
None
}
})
.collect::<Vec<_>>();

callable_names
}
}

/// An item.
#[derive(Clone, Debug, PartialEq)]
pub struct Item {
Expand Down Expand Up @@ -1365,6 +1417,8 @@ pub enum Attr {
/// Indicates that an intrinsic callable is a reset. This means that the operation will be marked as
/// "irreversible" in the generated QIR.
Reset,
/// Indicates that a callable is a test case.
Test,
}

impl Attr {
Expand All @@ -1382,6 +1436,7 @@ The `not` operator is also supported to negate the attribute, e.g. `not Adaptive
Attr::SimulatableIntrinsic => "Indicates that an item should be treated as an intrinsic callable for QIR code generation and any implementation should only be used during simulation.",
Attr::Measurement => "Indicates that an intrinsic callable is a measurement. This means that the operation will be marked as \"irreversible\" in the generated QIR, and output Result types will be moved to the arguments.",
Attr::Reset => "Indicates that an intrinsic callable is a reset. This means that the operation will be marked as \"irreversible\" in the generated QIR.",
Attr::Test => "Indicates that a callable is a test case.",
}
}
}
Expand All @@ -1397,6 +1452,7 @@ impl FromStr for Attr {
"SimulatableIntrinsic" => Ok(Self::SimulatableIntrinsic),
"Measurement" => Ok(Self::Measurement),
"Reset" => Ok(Self::Reset),
"Test" => Ok(Self::Test),
_ => Err(()),
}
}
Expand Down
5 changes: 4 additions & 1 deletion compiler/qsc_lowerer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -943,7 +943,10 @@ fn lower_attrs(attrs: &[hir::Attr]) -> Vec<fir::Attr> {
hir::Attr::EntryPoint => Some(fir::Attr::EntryPoint),
hir::Attr::Measurement => Some(fir::Attr::Measurement),
hir::Attr::Reset => Some(fir::Attr::Reset),
hir::Attr::SimulatableIntrinsic | hir::Attr::Unimplemented | hir::Attr::Config => None,
hir::Attr::SimulatableIntrinsic
| hir::Attr::Unimplemented
| hir::Attr::Config
| hir::Attr::Test => None,
})
.collect()
}
Expand Down
17 changes: 17 additions & 0 deletions compiler/qsc_parse/src/item/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2396,3 +2396,20 @@ fn top_level_nodes_error_recovery() {
]"#]],
);
}

#[test]
fn test_attribute() {
check(
parse,
"@Test() function Foo() : Unit {}",
&expect![[r#"
Item _id_ [0-32]:
Attr _id_ [0-7] (Ident _id_ [1-5] "Test"):
Expr _id_ [5-7]: Unit
Callable _id_ [8-32] (Function):
name: Ident _id_ [17-20] "Foo"
input: Pat _id_ [20-22]: Unit
output: Type _id_ [25-29]: Path: Path _id_ [25-29] (Ident _id_ [25-29] "Unit")
body: Block: Block _id_ [30-32]: <empty>"#]],
);
}
6 changes: 6 additions & 0 deletions compiler/qsc_passes/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ mod measurement;
mod replace_qubit_allocation;
mod reset;
mod spec_gen;
mod test_attribute;

use callable_limits::CallableLimits;
use capabilitiesck::{check_supported_capabilities, lower_store, run_rca_pass};
Expand Down Expand Up @@ -52,6 +53,7 @@ pub enum Error {
Measurement(measurement::Error),
Reset(reset::Error),
SpecGen(spec_gen::Error),
TestAttribute(test_attribute::TestAttributeError),
}

#[derive(Clone, Copy, Debug, PartialEq)]
Expand Down Expand Up @@ -121,6 +123,9 @@ impl PassContext {
ReplaceQubitAllocation::new(core, assigner).visit_package(package);
Validator::default().visit_package(package);

let test_attribute_errors = test_attribute::validate_test_attributes(package);
Validator::default().visit_package(package);

callable_errors
.into_iter()
.map(Error::CallableLimits)
Expand All @@ -130,6 +135,7 @@ impl PassContext {
.chain(entry_point_errors)
.chain(measurement_decl_errors.into_iter().map(Error::Measurement))
.chain(reset_decl_errors.into_iter().map(Error::Reset))
.chain(test_attribute_errors.into_iter().map(Error::TestAttribute))
.collect()
}

Expand Down
47 changes: 47 additions & 0 deletions compiler/qsc_passes/src/test_attribute.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use miette::Diagnostic;
use qsc_data_structures::span::Span;
use qsc_hir::{hir::Attr, visit::Visitor};
use thiserror::Error;

#[cfg(test)]
mod tests;

#[derive(Clone, Debug, Diagnostic, Error)]
pub enum TestAttributeError {
#[error("test callables cannot take arguments")]
CallableHasParameters(#[label] Span),
#[error("test callables cannot have type parameters")]
CallableHasTypeParameters(#[label] Span),
}

pub(crate) fn validate_test_attributes(
package: &mut qsc_hir::hir::Package,
) -> Vec<TestAttributeError> {
let mut validator = TestAttributeValidator { errors: Vec::new() };
validator.visit_package(package);
validator.errors
}

struct TestAttributeValidator {
errors: Vec<TestAttributeError>,
}

impl<'a> Visitor<'a> for TestAttributeValidator {
fn visit_callable_decl(&mut self, decl: &'a qsc_hir::hir::CallableDecl) {
if decl.attrs.iter().any(|attr| matches!(attr, Attr::Test)) {
if !decl.generics.is_empty() {
self.errors
.push(TestAttributeError::CallableHasTypeParameters(
decl.name.span,
));
}
if decl.input.ty != qsc_hir::ty::Ty::UNIT {
self.errors
.push(TestAttributeError::CallableHasParameters(decl.span));
}
}
}
}
129 changes: 129 additions & 0 deletions compiler/qsc_passes/src/test_attribute/tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

use expect_test::{expect, Expect};
use indoc::indoc;
use qsc_data_structures::{language_features::LanguageFeatures, target::TargetCapabilityFlags};
use qsc_frontend::compile::{self, compile, PackageStore, SourceMap};
use qsc_hir::{validate::Validator, visit::Visitor};

use crate::test_attribute::validate_test_attributes;

fn check(file: &str, expect: &Expect) {
let store = PackageStore::new(compile::core());
let sources = SourceMap::new([("test".into(), file.into())], None);
let mut unit = compile(
&store,
&[],
sources,
TargetCapabilityFlags::all(),
LanguageFeatures::default(),
);
assert!(unit.errors.is_empty(), "{:?}", unit.errors);

let errors = validate_test_attributes(&mut unit.package);
Validator::default().visit_package(&unit.package);
if errors.is_empty() {
expect.assert_eq(&unit.package.to_string());
} else {
expect.assert_debug_eq(&errors);
}
}

#[test]
fn callable_cant_have_params() {
check(
indoc! {"
namespace test {
@Test()
operation A(q : Qubit) : Unit {
}
}
"},
&expect![[r#"
[
CallableHasParameters(
Span {
lo: 33,
hi: 71,
},
),
]
"#]],
);
}

#[test]
fn callable_cant_have_type_params() {
check(
indoc! {"
namespace test {
@Test()
operation A<'T>() : Unit {
}
}
"},
&expect![[r#"
[
CallableHasTypeParameters(
Span {
lo: 43,
hi: 44,
},
),
]
"#]],
);
}

#[test]
fn conditionally_compile_out_test() {
check(
indoc! {"
namespace test {
@Test()
@Config(Base)
operation A<'T>() : Unit {
}
}
"},
&expect![[r#"
Package:
Item 0 [0-86] (Public):
Namespace (Ident 0 [10-14] "test"): <empty>"#]],
);
}

#[test]
fn callable_is_valid_test_callable() {
check(
indoc! {"
namespace test {
@Test()
operation A() : Unit {
}
}
"},
&expect![[r#"
Package:
Item 0 [0-64] (Public):
Namespace (Ident 5 [10-14] "test"): Item 1
Item 1 [21-62] (Internal):
Parent: 0
Test
Callable 0 [33-62] (operation):
name: Ident 1 [43-44] "A"
input: Pat 2 [44-46] [Type Unit]: Unit
output: Unit
functors: empty set
body: SpecDecl 3 [33-62]: Impl:
Block 4 [54-62]: <empty>
adj: <none>
ctl: <none>
ctl-adj: <none>"#]],
);
}
Loading

0 comments on commit c2a3c56

Please sign in to comment.