// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use core::str::FromStr; use qsc_ast::{ ast::{ Attr, ExprKind, Idents, ItemKind, Namespace, Package, PathKind, Stmt, StmtKind, TopLevelNode, UnOp, }, mut_visit::{MutVisitor, walk_stmt}, visit::Visitor, }; use qsc_data_structures::{span::Span, target::Profile}; use qsc_hir::hir; use std::rc::Rc; use super::{SourceMap, TargetCapabilityFlags}; #[cfg(test)] mod tests; /// Transformation to detect `@EntryPoint` attribute in the AST. #[derive(Default)] pub struct DetectEntryPointProfile { pub profile: Option<(Profile, Span)>, } impl DetectEntryPointProfile { #[must_use] pub fn new() -> Self { Self { profile: None } } } impl Visitor<'_> for DetectEntryPointProfile { fn visit_attr(&mut self, attr: &Attr) { if hir::Attr::from_str(attr.name.name.as_ref()) == Ok(hir::Attr::EntryPoint) { // Try to parse the argument as a profile name if let ExprKind::Paren(inner) = attr.arg.kind.as_ref() && let ExprKind::Path(PathKind::Ok(path)) = inner.kind.as_ref() && let Ok(profile) = Profile::from_str(path.name.name.as_ref()) { self.profile = Some((profile, path.span)); } } } } #[derive(PartialEq, Hash, Clone, Debug)] pub struct TrackedName { pub name: Rc, pub namespace: Rc, } pub(crate) struct Conditional { capabilities: TargetCapabilityFlags, dropped_names: Vec, included_names: Vec, dropped_spans: Vec, } impl Conditional { pub(crate) fn new(capabilities: TargetCapabilityFlags) -> Self { Self { capabilities, dropped_names: Vec::new(), included_names: Vec::new(), dropped_spans: Vec::new(), } } /// Takes the spans of any items that were dropped from the compilation /// because they did not match the current target capabilities. These are /// reported to the editor so that the excluded code can be greyed out. pub(crate) fn take_dropped_spans(&mut self) -> Vec { std::mem::take(&mut self.dropped_spans) } pub(crate) fn into_names(self) -> Vec { self.dropped_names .into_iter() .filter(|n| !self.included_names.contains(n)) .collect() } } impl MutVisitor for Conditional { fn visit_namespace(&mut self, namespace: &mut Namespace) { namespace.items = namespace .items .iter() .filter_map(|item| { if matches_config(&item.attrs, self.capabilities) { match item.kind.as_ref() { ItemKind::Callable(callable) => { self.included_names.push(TrackedName { name: callable.name.name.clone(), namespace: namespace.name.full_name(), }); } ItemKind::Ty(ident, _) => self.included_names.push(TrackedName { name: ident.name.clone(), namespace: namespace.name.full_name(), }), _ => {} } Some(item.clone()) } else { self.dropped_spans.push(item.span); match item.kind.as_ref() { ItemKind::Callable(callable) => { self.dropped_names.push(TrackedName { name: callable.name.name.clone(), namespace: namespace.name.full_name(), }); } ItemKind::Ty(ident, _) => self.dropped_names.push(TrackedName { name: ident.name.clone(), namespace: namespace.name.full_name(), }), _ => {} } None } }) .collect::>() .into_boxed_slice(); } fn visit_stmt(&mut self, stmt: &mut Stmt) { if let StmtKind::Item(item) = stmt.kind.as_mut() { if matches_config(&item.attrs, self.capabilities) { match item.kind.as_ref() { ItemKind::Callable(callable) => { self.included_names.push(TrackedName { name: callable.name.name.clone(), namespace: Rc::from(""), }); } ItemKind::Ty(ident, _) => self.included_names.push(TrackedName { name: ident.name.clone(), namespace: Rc::from(""), }), _ => {} } } else { self.dropped_spans.push(item.span); match item.kind.as_ref() { ItemKind::Callable(callable) => { self.dropped_names.push(TrackedName { name: callable.name.name.clone(), namespace: Rc::from(""), }); } ItemKind::Ty(ident, _) => self.dropped_names.push(TrackedName { name: ident.name.clone(), namespace: Rc::from(""), }), _ => {} } *stmt.kind = StmtKind::Empty; } } } } fn matches_config(attrs: &[Box], capabilities: TargetCapabilityFlags) -> bool { let attrs: Vec<_> = attrs .iter() .filter(|attr| hir::Attr::from_str(attr.name.name.as_ref()) == Ok(hir::Attr::Config)) .collect(); if attrs.is_empty() { return true; } let mut found_capabilities = TargetCapabilityFlags::empty(); let mut disallowed_capabilities = TargetCapabilityFlags::empty(); let mut base = false; let mut not_base = false; // When checking attributes, anything we don't recognize (invalid form or invalid capability) gets // left in the compilation by returning true. This ensures that later compilation steps, specifically lowering // from AST to HIR, can check the attributes and return errors as appropriate. for attr in attrs { if let ExprKind::Paren(inner) = attr.arg.kind.as_ref() { match inner.kind.as_ref() { ExprKind::Path(PathKind::Ok(path)) => { if let Ok(capability) = TargetCapabilityFlags::from_str(path.name.name.as_ref()) { if capability.is_empty() { base = true; } found_capabilities |= capability; } else { return true; // Unknown capability, so we assume it matches } } ExprKind::UnOp(UnOp::NotL, inner) => { if let ExprKind::Path(PathKind::Ok(path)) = inner.kind.as_ref() { if let Ok(capability) = TargetCapabilityFlags::from_str(path.name.name.as_ref()) { if capability.is_empty() { not_base = true; } disallowed_capabilities |= capability; } else { return true; // Unknown capability, so we assume it matches } } else { return true; // Unknown config attribute, so we assume it matches } } _ => return true, // Unknown config attribute, so we assume it matches } } else { // Something other than a parenthesized expression, so we assume it matches return true; } } if found_capabilities.is_empty() && disallowed_capabilities.is_empty() { if not_base && !base { // There was at least one config attribute, but it was "not Base" so // ensure that the capabilities are not empty. return capabilities != TargetCapabilityFlags::empty(); } else if base && !not_base { // There was at least one config attribute, but it was Base // Therefore, we only match if there are no capabilities return capabilities == TargetCapabilityFlags::empty(); } // The config specified both "Base" and "not Base" which is a contradiction, but we // drop the item in this case. return false; } capabilities.contains(found_capabilities) && (disallowed_capabilities.is_empty() || !capabilities.contains(disallowed_capabilities)) } // Visitor to remove spans from circuit callables defined in QSC files. // This will remove the spans for the contents of these circuit callables, // but it is important for the language server that the span for the callable itself // is preserved, so that the user can navigate to the definition of the callable. pub(crate) struct RemoveCircuitSpans { qsc_spans: Vec, } impl RemoveCircuitSpans { pub(crate) fn new(sources: &SourceMap) -> Self { let qsc_spans = sources .iter() .filter(|source| { std::path::Path::new(source.name.as_ref()) .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("qsc")) }) .map(|source| { let start = source.offset; let end = start + u32::try_from(source.contents.len()).expect("source length exceeds u32::MAX"); Span { lo: start, hi: end } }) .collect(); Self { qsc_spans } } } impl MutVisitor for RemoveCircuitSpans { fn visit_package(&mut self, package: &mut Package) { // We only want to visit namespaces package.nodes.iter_mut().for_each(|n| match n { TopLevelNode::Namespace(ns) => self.visit_namespace(ns), TopLevelNode::Stmt(_) => {} }); } fn visit_namespace(&mut self, namespace: &mut Namespace) { // We only want to visit circuit callables namespace.items.iter_mut().for_each(|item| { if let ItemKind::Callable(callable) = item.kind.as_mut() { // Check if the callable's span is inside a QSC file's span self.qsc_spans .iter() .any(|s| s.lo <= callable.span.lo && s.hi >= callable.span.hi) .then(|| { self.visit_callable_decl(callable); }); } }); } fn visit_stmt(&mut self, stmt: &mut Stmt) { stmt.span = Span::default(); // Clear the span for the statement walk_stmt(self, stmt); } }