diff --git a/Cargo.lock b/Cargo.lock index fd5c8a7e23..c787701008 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1436,6 +1436,7 @@ dependencies = [ name = "qsc_parse" version = "0.0.0" dependencies = [ + "bitflags 2.6.0", "enum-iterator", "expect-test", "indoc", diff --git a/compiler/qsc/src/interpret/tests.rs b/compiler/qsc/src/interpret/tests.rs index 5f5421568c..688a54dc05 100644 --- a/compiler/qsc/src/interpret/tests.rs +++ b/compiler/qsc/src/interpret/tests.rs @@ -1346,7 +1346,9 @@ mod given_interpreter { use expect_test::expect; use indoc::indoc; - use qsc_ast::ast::{Expr, ExprKind, NodeId, Package, Path, Stmt, StmtKind, TopLevelNode}; + use qsc_ast::ast::{ + Expr, ExprKind, NodeId, Package, Path, PathKind, Stmt, StmtKind, TopLevelNode, + }; use qsc_data_structures::span::Span; use qsc_frontend::compile::SourceMap; use qsc_passes::PackageType; @@ -1865,7 +1867,7 @@ mod given_interpreter { let path_expr = Expr { id: NodeId::default(), span: Span::default(), - kind: Box::new(ExprKind::Path(Box::new(path))), + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(path)))), }; let expr = Expr { id: NodeId::default(), diff --git a/compiler/qsc/src/lib.rs b/compiler/qsc/src/lib.rs index db9ee9ff4c..c52733f414 100644 --- a/compiler/qsc/src/lib.rs +++ b/compiler/qsc/src/lib.rs @@ -66,7 +66,7 @@ pub mod circuit { } pub mod parse { - pub use qsc_parse::top_level_nodes; + pub use qsc_parse::{completion, top_level_nodes}; } pub mod partial_eval { diff --git a/compiler/qsc/src/packages/tests.rs b/compiler/qsc/src/packages/tests.rs index 6bfcf44fd8..1bb60c264a 100644 --- a/compiler/qsc/src/packages/tests.rs +++ b/compiler/qsc/src/packages/tests.rs @@ -237,30 +237,6 @@ fn dependency_error() { ), ), }, - WithSource { - sources: [ - Source { - name: "librarymain", - contents: "broken_syntax", - offset: 0, - }, - ], - error: Frontend( - Error( - Parse( - Error( - ExpectedItem( - Eof, - Span { - lo: 0, - hi: 13, - }, - ), - ), - ), - ), - ), - }, ] "#]] .assert_debug_eq(&buildable_program.dependency_errors); diff --git a/compiler/qsc_ast/src/ast.rs b/compiler/qsc_ast/src/ast.rs index 8ffb9a6b46..616bba164d 100644 --- a/compiler/qsc_ast/src/ast.rs +++ b/compiler/qsc_ast/src/ast.rs @@ -12,6 +12,7 @@ use std::{ cmp::Ordering, fmt::{self, Display, Formatter, Write}, hash::{Hash, Hasher}, + iter::once, rc::Rc, }; @@ -164,7 +165,7 @@ pub struct Namespace { /// The documentation. pub doc: Rc, /// The namespace name. - pub name: Idents, + pub name: Box<[Ident]>, /// The items in the namespace. pub items: Box<[Box]>, } @@ -182,11 +183,21 @@ impl Namespace { impl Display for Namespace { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let mut indent = set_indentation(indented(f), 0); - write!( - indent, - "Namespace {} {} ({}):", - self.id, self.span, self.name - )?; + write!(indent, "Namespace {} {} (", self.id, self.span)?; + + let mut buf = Vec::with_capacity(self.name.len()); + + for ident in &self.name { + buf.push(format!("{ident}")); + } + if buf.len() > 1 { + // use square brackets only if there are more than one ident + write!(indent, "[{}]", buf.join(", "))?; + } else { + write!(indent, "{}", buf[0])?; + } + + write!(indent, "):",)?; indent = set_indentation(indent, 1); if !self.doc.is_empty() { @@ -262,7 +273,7 @@ pub enum ItemKind { #[default] Err, /// An `open` item for a namespace with an optional alias. - Open(Idents, Option>), + Open(PathKind, Option>), /// A `newtype` declaration. Ty(Box, Box), /// A `struct` declaration. @@ -661,7 +672,7 @@ pub enum TyKind { /// A type wrapped in parentheses. Paren(Box), /// A named type. - Path(Box), + Path(PathKind), /// A type parameter. Param(Box), /// A tuple type. @@ -873,7 +884,7 @@ pub enum ExprKind { /// Parentheses: `(a)`. Paren(Box), /// A path: `a` or `a.b`. - Path(Box), + Path(PathKind), /// A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. Range(Option>, Option>, Option>), /// A repeat-until loop with an optional fixup: `repeat { ... } until a fixup { ... }`. @@ -881,7 +892,7 @@ pub enum ExprKind { /// A return: `return a`. Return(Box), /// A struct constructor. - Struct(Box, Option>, Box<[Box]>), + Struct(PathKind, Option>, Box<[Box]>), /// A ternary operator. TernOp(TernOp, Box, Box, Box), /// A tuple: `(a, b, c)`. @@ -1136,7 +1147,7 @@ fn display_repeat( fn display_struct( mut indent: Indented, - name: &Path, + name: &PathKind, copy: &Option>, fields: &[Box], ) -> fmt::Result { @@ -1391,57 +1402,69 @@ impl Display for QubitInitKind { } } -/// A path to a declaration. -#[derive(Clone, Debug, Eq, Hash, PartialEq, Default)] -pub struct Path { - /// The node ID. - pub id: NodeId, - /// The span. - pub span: Span, - /// The segments that make up the front of the path before the final `.`. - pub segments: Option, - /// The declaration name. - pub name: Box, +/// A path that may or may not have been successfully parsed. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub enum PathKind { + /// A successfully parsed path. + Ok(Box), + + /// An invalid path. + Err(Option>), } -impl From for Vec { - fn from(val: Path) -> Self { - let mut buf = val.segments.unwrap_or_default().0.to_vec(); - buf.push(*val.name); - buf +impl Default for PathKind { + fn default() -> Self { + PathKind::Err(None) } } -impl From<&Path> for Vec { - fn from(val: &Path) -> Self { - let mut buf = match &val.segments { - Some(inner) => inner.0.to_vec(), - None => Vec::new(), - }; - buf.push(val.name.as_ref().clone()); - buf - } +/// A path that was successfully parsed up to a certain `.`, +/// but is missing its final identifier. +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct IncompletePath { + /// The whole span of the incomplete path, + /// including the final `.` and any whitespace or keyword + /// that follows it. + pub span: Span, + /// Any segments that were successfully parsed before the final `.`. + pub segments: Box<[Ident]>, + /// Whether a keyword exists after the final `.`. + /// This keyword can be presumed to be a partially typed identifier. + pub keyword: bool, } -impl From> for Path { - fn from(mut v: Vec) -> Self { - let name = v - .pop() - .expect("parser should never produce empty vector of idents"); - let segments: Option = if v.is_empty() { None } else { Some(v.into()) }; - let span = Span { - lo: segments.as_ref().map_or(name.span.lo, |ns| ns.span().lo), - hi: name.span.hi, - }; - Self { - id: NodeId::default(), - span, - segments, - name: name.into(), +impl Display for PathKind { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + PathKind::Ok(path) => write!(f, "{path}")?, + PathKind::Err(Some(incomplete_path)) => { + let mut indent = set_indentation(indented(f), 0); + write!(indent, "Err IncompletePath {}:", incomplete_path.span)?; + indent = set_indentation(indent, 1); + for part in &incomplete_path.segments { + write!(indent, "\n{part}")?; + } + } + PathKind::Err(None) => write!(f, "Err",)?, } + Ok(()) } } +/// A path to a declaration or a field access expression, +/// to be disambiguated during name resolution. +#[derive(Clone, Debug, Eq, Hash, PartialEq)] +pub struct Path { + /// The node ID. + pub id: NodeId, + /// The span. + pub span: Span, + /// The segments that make up the front of the path before the final `.`. + pub segments: Option>, + /// The declaration or field name. + pub name: Box, +} + impl Display for Path { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { if self.segments.is_none() { @@ -1478,161 +1501,122 @@ pub struct Ident { pub name: Rc, } -/// A [`Idents`] represents a sequence of idents. It provides a helpful abstraction -/// that is more powerful than a simple `Vec`, and is primarily used to represent -/// dot-separated paths. -#[derive(Clone, Debug, Eq, Hash, PartialEq, Default)] -pub struct Idents(pub Box<[Ident]>); - -impl From for Vec> { - fn from(v: Idents) -> Self { - v.0.iter().map(|i| i.name.clone()).collect() - } -} - -impl From<&Idents> for Vec> { - fn from(v: &Idents) -> Self { - v.0.iter().map(|i| i.name.clone()).collect() - } -} - -impl From> for Idents { - fn from(v: Vec) -> Self { - Idents(v.into_boxed_slice()) - } -} - -impl From for Vec { - fn from(v: Idents) -> Self { - v.0.into_vec() - } -} - -impl Display for Idents { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - let mut buf = Vec::with_capacity(self.0.len()); - - for ident in &self.0 { - buf.push(format!("{ident}")); - } - if buf.len() > 1 { - // use square brackets only if there are more than one ident - write!(f, "[{}]", buf.join(", ")) - } else { - write!(f, "{}", buf[0]) +impl Default for Ident { + fn default() -> Self { + Ident { + id: NodeId::default(), + span: Span::default(), + name: "".into(), } } } -impl<'a> IntoIterator for &'a Idents { - type IntoIter = std::slice::Iter<'a, Ident>; - type Item = &'a Ident; - fn into_iter(self) -> Self::IntoIter { - self.iter() +impl WithSpan for Ident { + fn with_span(self, span: Span) -> Self { + Self { span, ..self } } } -impl<'a> From<&'a Idents> for IdentsStrIter<'a> { - fn from(v: &'a Idents) -> Self { - IdentsStrIter(v) +impl Display for Ident { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + write!(f, "Ident {} {} \"{}\"", self.id, self.span, self.name) } } -/// An iterator which yields string slices of the names of the idents in a [`Idents`]. -/// Note that [`Idents`] itself only implements [`IntoIterator`] where the item is an [`Ident`]. -pub struct IdentsStrIter<'a>(pub &'a Idents); +/// Trait for working with dot-separated sequences of identifiers, +/// intended to unify the different representations that can appear +/// in the AST (`Path`s and `Ident` slices). +pub trait Idents { + /// Iterates over the [`Ident`]s in this sequence. + fn iter(&self) -> impl Iterator; -impl<'a> IntoIterator for IdentsStrIter<'a> { - type IntoIter = std::iter::Map, fn(&'a Ident) -> &'a str>; - type Item = &'a str; - fn into_iter(self) -> Self::IntoIter { - self.0.iter().map(|i| i.name.as_ref()) - } -} + /// The full dot-separated name represented by this [`Ident`] sequence. + /// E.g. `a.b.c` + fn full_name(&self) -> Rc { + let mut strs = self.rc_str_iter(); + let first = strs.next(); + let Some(first) = first else { + // No parts, empty string + return "".into(); + }; -impl FromIterator for Idents { - fn from_iter>(iter: T) -> Self { - Idents(iter.into_iter().collect()) - } -} + let next = strs.next(); + let Some(mut part) = next else { + // Only one ident, return it directly + return first.clone(); + }; -impl From for Idents { - fn from(p: Path) -> Self { - let mut buf = p.segments.unwrap_or_default().0.to_vec(); - buf.push(*p.name); - Self(buf.into_boxed_slice()) + // More than one ident, build up a dotted string + let mut buf = String::new(); + buf.push_str(first); + loop { + buf.push('.'); + buf.push_str(part); + part = match strs.next() { + Some(part) => part, + None => { + break; + } + }; + } + buf.into() } -} -impl Idents { - /// constructs an iterator over the [Ident]s that this contains. - /// see [`Self::str_iter`] for an iterator over the string slices of the [Ident]s. - pub fn iter(&self) -> std::slice::Iter<'_, Ident> { - self.0.iter() + /// Iterates over the identifier names as string slices. + fn str_iter(&self) -> impl Iterator { + self.iter().map(|ident| ident.name.as_ref()) } - /// constructs an iterator over the elements of `self` as string slices. - /// see [`Self::iter`] for an iterator over the [Ident]s. - #[must_use] - pub fn str_iter(&self) -> IdentsStrIter { - self.into() + /// Iterates over the identifier names as `Rc`s. + fn rc_str_iter(&self) -> impl Iterator> { + self.iter().map(|ident| &ident.name) } - /// the conjoined span of all idents in the `Idents` + /// Returns the conjoined span of all [`Ident`]s in this collection. #[must_use] - pub fn span(&self) -> Span { + fn full_span(&self) -> Span { + let mut idents = self.iter().peekable(); Span { - lo: self.0.first().map(|i| i.span.lo).unwrap_or_default(), - hi: self.0.last().map(|i| i.span.hi).unwrap_or_default(), + lo: idents.peek().map(|i| i.span.lo).unwrap_or_default(), + hi: idents.last().map(|i| i.span.hi).unwrap_or_default(), } } +} - /// The stringified dot-separated path of the idents in this [`Idents`] - /// E.g. `a.b.c` - #[must_use] - pub fn name(&self) -> Rc { - if self.0.len() == 1 { - return self.0[0].name.clone(); - } - let mut buf = String::new(); - for ident in &self.0 { - if !buf.is_empty() { - buf.push('.'); - } - buf.push_str(&ident.name); - } - Rc::from(buf) +impl Idents for Box<[Ident]> { + fn iter(&self) -> impl Iterator { + self.as_ref().iter() // invokes the slice iterator } +} - /// Appends another ident to this [`Idents`]. - /// Returns a new [`Idents`] with the appended ident. - #[must_use = "this method returns a new value and does not mutate the original value"] - pub fn push(&self, other: Ident) -> Self { - let mut buf = self.0.to_vec(); - buf.push(other); - Self(buf.into_boxed_slice()) +impl Idents for &[Ident] { + fn iter(&self) -> impl Iterator { + (*self).iter() // invokes the slice iterator } } -impl Default for Ident { - fn default() -> Self { - Ident { - id: NodeId::default(), - span: Span::default(), - name: "".into(), - } +impl Idents for (&T, &U) +where + T: Idents, + U: Idents, +{ + fn iter(&self) -> impl Iterator { + self.0.iter().chain(self.1.iter()) } } -impl WithSpan for Ident { - fn with_span(self, span: Span) -> Self { - Self { span, ..self } +impl Idents for Ident { + fn iter(&self) -> impl Iterator { + once(self) } } -impl Display for Ident { - fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { - write!(f, "Ident {} {} \"{}\"", self.id, self.span, self.name) +impl Idents for Path { + fn iter(&self) -> impl Iterator { + self.segments + .iter() + .flat_map(Idents::iter) + .chain(once(self.name.as_ref())) } } @@ -1919,11 +1903,13 @@ impl ImportOrExportDecl { } } -/// An individual item within an [`ExportDecl`]. This can be a path or a path with an alias. +/// An individual item within an [`ImportOrExportDecl`]. This can be a path or a path with an alias. #[derive(Clone, Debug, Eq, PartialEq, Default)] pub struct ImportOrExportItem { + /// The span of the import path including the glob and alias, if any. + pub span: Span, /// The path to the item being exported. - pub path: Path, + pub path: PathKind, /// An optional alias for the item being exported. pub alias: Option, /// Whether this is a glob import/export. @@ -1933,6 +1919,7 @@ pub struct ImportOrExportItem { impl Display for ImportOrExportItem { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { let ImportOrExportItem { + span: _, ref path, ref alias, is_glob, @@ -1947,36 +1934,24 @@ impl Display for ImportOrExportItem { impl WithSpan for ImportOrExportItem { fn with_span(self, span: Span) -> Self { - ImportOrExportItem { - path: self.path.with_span(span), - alias: self.alias.map(|x| x.with_span(span)), - is_glob: self.is_glob, - } + Self { span, ..self } } } impl ImportOrExportItem { - /// Returns the span of the export item. This includes the path and , if any exists, the alias. + /// Returns the alias ident, if any, or the name from the path if no alias is present. + /// Returns `None` if this the path has an error. #[must_use] - pub fn span(&self) -> Span { - match self.alias { - Some(ref alias) => { - // join the path and alias spans - Span { - lo: self.path.span.lo, - hi: alias.span.hi, + pub fn name(&self) -> Option<&Ident> { + match &self.alias { + Some(_) => self.alias.as_ref(), + None => { + if let PathKind::Ok(path) = &self.path { + Some(&path.name) + } else { + None } } - None => self.path.span, - } - } - - /// Returns the alias ident, if any, or the name from the path if no alias is present. - #[must_use] - pub fn name(&self) -> &Ident { - match self.alias { - Some(ref alias) => alias, - None => &self.path.name, } } } diff --git a/compiler/qsc_ast/src/mut_visit.rs b/compiler/qsc_ast/src/mut_visit.rs index cf2277c9b4..15664a6a95 100644 --- a/compiler/qsc_ast/src/mut_visit.rs +++ b/compiler/qsc_ast/src/mut_visit.rs @@ -3,9 +3,9 @@ use crate::ast::{ Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, - FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, QubitInit, - QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl, TopLevelNode, - Ty, TyDef, TyDefKind, TyKind, + FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, PathKind, + QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl, + TopLevelNode, Ty, TyDef, TyDefKind, TyKind, }; use qsc_data_structures::span::Span; @@ -82,11 +82,15 @@ pub trait MutVisitor: Sized { walk_path(self, path); } + fn visit_path_kind(&mut self, path: &mut PathKind) { + walk_path_kind(self, path); + } + fn visit_ident(&mut self, ident: &mut Ident) { walk_ident(self, ident); } - fn visit_idents(&mut self, ident: &mut crate::ast::Idents) { + fn visit_idents(&mut self, ident: &mut [Ident]) { walk_idents(self, ident); } @@ -116,7 +120,7 @@ pub fn walk_item(vis: &mut impl MutVisitor, item: &mut Item) { ItemKind::Callable(decl) => vis.visit_callable_decl(decl), ItemKind::Err => {} ItemKind::Open(ns, alias) => { - vis.visit_idents(ns); + vis.visit_path_kind(ns); alias.iter_mut().for_each(|a| vis.visit_ident(a)); } ItemKind::Ty(ident, def) => { @@ -127,7 +131,8 @@ pub fn walk_item(vis: &mut impl MutVisitor, item: &mut Item) { ItemKind::ImportOrExport(export) => { vis.visit_span(&mut export.span); for item in &mut *export.items { - vis.visit_path(&mut item.path); + vis.visit_span(&mut item.span); + vis.visit_path_kind(&mut item.path); if let Some(ref mut alias) = item.alias { vis.visit_ident(alias); } @@ -222,7 +227,7 @@ pub fn walk_ty(vis: &mut impl MutVisitor, ty: &mut Ty) { TyKind::Hole | TyKind::Err => {} TyKind::Paren(ty) => vis.visit_ty(ty), TyKind::Param(name) => vis.visit_ident(name), - TyKind::Path(path) => vis.visit_path(path), + TyKind::Path(path) => vis.visit_path_kind(path), TyKind::Tuple(tys) => tys.iter_mut().for_each(|t| vis.visit_ty(t)), } } @@ -314,7 +319,7 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } - ExprKind::Path(path) => vis.visit_path(path), + ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { start.iter_mut().for_each(|s| vis.visit_expr(s)); step.iter_mut().for_each(|s| vis.visit_expr(s)); @@ -326,7 +331,7 @@ pub fn walk_expr(vis: &mut impl MutVisitor, expr: &mut Expr) { fixup.iter_mut().for_each(|f| vis.visit_block(f)); } ExprKind::Struct(name, copy, fields) => { - vis.visit_path(name); + vis.visit_path_kind(name); copy.iter_mut().for_each(|c| vis.visit_expr(c)); fields.iter_mut().for_each(|f| vis.visit_field_assign(f)); } @@ -384,12 +389,26 @@ pub fn walk_path(vis: &mut impl MutVisitor, path: &mut Path) { vis.visit_ident(&mut path.name); } +pub fn walk_path_kind(vis: &mut impl MutVisitor, path: &mut PathKind) { + match path { + PathKind::Ok(path) => vis.visit_path(path), + PathKind::Err(Some(incomplete_path)) => { + vis.visit_span(&mut incomplete_path.span); + + for ref mut ident in &mut incomplete_path.segments { + vis.visit_ident(ident); + } + } + PathKind::Err(None) => {} + } +} + pub fn walk_ident(vis: &mut impl MutVisitor, ident: &mut Ident) { vis.visit_span(&mut ident.span); } -pub fn walk_idents(vis: &mut impl MutVisitor, ident: &mut crate::ast::Idents) { - for ref mut ident in &mut *ident.0 { +pub fn walk_idents(vis: &mut impl MutVisitor, idents: &mut [Ident]) { + for ref mut ident in idents { vis.visit_ident(ident); } } diff --git a/compiler/qsc_ast/src/visit.rs b/compiler/qsc_ast/src/visit.rs index 076d3e2092..09d3066774 100644 --- a/compiler/qsc_ast/src/visit.rs +++ b/compiler/qsc_ast/src/visit.rs @@ -3,7 +3,7 @@ use crate::ast::{ Attr, Block, CallableBody, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, - FunctorExprKind, Ident, Idents, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, + FunctorExprKind, Ident, Item, ItemKind, Namespace, Package, Pat, PatKind, Path, PathKind, QubitInit, QubitInitKind, SpecBody, SpecDecl, Stmt, StmtKind, StringComponent, StructDecl, TopLevelNode, Ty, TyDef, TyDefKind, TyKind, }; @@ -81,9 +81,13 @@ pub trait Visitor<'a>: Sized { walk_path(self, path); } + fn visit_path_kind(&mut self, path: &'a PathKind) { + walk_path_kind(self, path); + } + fn visit_ident(&mut self, _: &'a Ident) {} - fn visit_idents(&mut self, idents: &'a Idents) { + fn visit_idents(&mut self, idents: &'a [Ident]) { walk_idents(self, idents); } } @@ -107,7 +111,7 @@ pub fn walk_item<'a>(vis: &mut impl Visitor<'a>, item: &'a Item) { ItemKind::Err => {} ItemKind::Callable(decl) => vis.visit_callable_decl(decl), ItemKind::Open(ns, alias) => { - vis.visit_idents(ns); + vis.visit_path_kind(ns); alias.iter().for_each(|a| vis.visit_ident(a)); } ItemKind::Ty(ident, def) => { @@ -117,7 +121,7 @@ pub fn walk_item<'a>(vis: &mut impl Visitor<'a>, item: &'a Item) { ItemKind::Struct(decl) => vis.visit_struct_decl(decl), ItemKind::ImportOrExport(decl) => { for item in &decl.items { - vis.visit_path(&item.path); + vis.visit_path_kind(&item.path); if let Some(ref alias) = item.alias { vis.visit_ident(alias); } @@ -196,7 +200,7 @@ pub fn walk_ty<'a>(vis: &mut impl Visitor<'a>, ty: &'a Ty) { } TyKind::Hole | TyKind::Err => {} TyKind::Paren(ty) => vis.visit_ty(ty), - TyKind::Path(path) => vis.visit_path(path), + TyKind::Path(path) => vis.visit_path_kind(path), TyKind::Param(name) => vis.visit_ident(name), TyKind::Tuple(tys) => tys.iter().for_each(|t| vis.visit_ty(t)), } @@ -284,7 +288,7 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { ExprKind::Paren(expr) | ExprKind::Return(expr) | ExprKind::UnOp(_, expr) => { vis.visit_expr(expr); } - ExprKind::Path(path) => vis.visit_path(path), + ExprKind::Path(path) => vis.visit_path_kind(path), ExprKind::Range(start, step, end) => { start.iter().for_each(|s| vis.visit_expr(s)); step.iter().for_each(|s| vis.visit_expr(s)); @@ -296,7 +300,7 @@ pub fn walk_expr<'a>(vis: &mut impl Visitor<'a>, expr: &'a Expr) { fixup.iter().for_each(|f| vis.visit_block(f)); } ExprKind::Struct(name, copy, fields) => { - vis.visit_path(name); + vis.visit_path_kind(name); copy.iter().for_each(|c| vis.visit_expr(c)); fields.iter().for_each(|f| vis.visit_field_assign(f)); } @@ -348,6 +352,16 @@ pub fn walk_path<'a>(vis: &mut impl Visitor<'a>, path: &'a Path) { vis.visit_ident(&path.name); } -pub fn walk_idents<'a>(vis: &mut impl Visitor<'a>, idents: &'a Idents) { +pub fn walk_path_kind<'a>(vis: &mut impl Visitor<'a>, path: &'a PathKind) { + match path { + PathKind::Ok(path) => vis.visit_path(path), + PathKind::Err(Some(incomplete_path)) => { + vis.visit_idents(&incomplete_path.segments); + } + PathKind::Err(None) => {} + } +} + +pub fn walk_idents<'a>(vis: &mut impl Visitor<'a>, idents: &'a [Ident]) { idents.iter().for_each(|i| vis.visit_ident(i)); } diff --git a/compiler/qsc_codegen/src/qsharp.rs b/compiler/qsc_codegen/src/qsharp.rs index 3a07e96ef7..ffc212768d 100644 --- a/compiler/qsc_codegen/src/qsharp.rs +++ b/compiler/qsc_codegen/src/qsharp.rs @@ -16,9 +16,9 @@ use std::vec; use qsc_ast::ast::{ self, Attr, BinOp, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Functor, FunctorExpr, FunctorExprKind, Ident, Idents, ImportOrExportItem, Item, ItemKind, Lit, - Mutability, Pat, PatKind, Path, Pauli, QubitInit, QubitInitKind, QubitSource, SetOp, SpecBody, - SpecDecl, SpecGen, Stmt, StmtKind, StringComponent, TernOp, TopLevelNode, Ty, TyDef, TyDefKind, - TyKind, UnOp, + Mutability, Pat, PatKind, Path, PathKind, Pauli, QubitInit, QubitInitKind, QubitSource, SetOp, + SpecBody, SpecDecl, SpecGen, Stmt, StmtKind, StringComponent, TernOp, TopLevelNode, Ty, TyDef, + TyDefKind, TyKind, UnOp, }; use qsc_ast::ast::{Namespace, Package}; use qsc_ast::visit::Visitor; @@ -135,7 +135,7 @@ impl Visitor<'_> for QSharpGen { ItemKind::Callable(decl) => self.visit_callable_decl(decl), ItemKind::Open(ns, alias) => { self.write("open "); - self.visit_idents(ns); + self.visit_path_kind(ns); if let Some(alias) = alias { self.write(" as "); self.visit_ident(alias); @@ -160,6 +160,7 @@ impl Visitor<'_> for QSharpGen { for ( ix, ImportOrExportItem { + span: _, ref path, ref is_glob, ref alias, @@ -167,7 +168,7 @@ impl Visitor<'_> for QSharpGen { ) in decl.items.iter().enumerate() { let is_last = ix == decl.items.len() - 1; - self.visit_path(path); + self.visit_path_kind(path); if *is_glob { self.write(".*"); @@ -347,7 +348,7 @@ impl Visitor<'_> for QSharpGen { self.visit_ty(ty); self.write(")"); } - TyKind::Path(path) => self.visit_path(path), + TyKind::Path(path) => self.visit_path_kind(path), TyKind::Param(name) => self.visit_ident(name), TyKind::Tuple(tys) => { if tys.is_empty() { @@ -549,9 +550,9 @@ impl Visitor<'_> for QSharpGen { self.write("return "); self.visit_expr(expr); } - ExprKind::Struct(name, copy, assigns) => { + ExprKind::Struct(PathKind::Ok(path), copy, assigns) => { self.write("new "); - self.visit_path(name); + self.visit_path(path); self.writeln(" {"); if let Some(copy) = copy { self.write("..."); @@ -580,7 +581,7 @@ impl Visitor<'_> for QSharpGen { self.visit_expr(expr); } } - ExprKind::Path(path) => self.visit_path(path), + ExprKind::Path(PathKind::Ok(path)) => self.visit_path(path), ExprKind::Range(start, step, end) => { // A range: `start..step..end`, `start..end`, `start...`, `...end`, or `...`. match (start, step, end) { @@ -713,7 +714,9 @@ impl Visitor<'_> for QSharpGen { ExprKind::Hole => { self.write("_"); } - ExprKind::Err => { + ExprKind::Err + | ExprKind::Path(PathKind::Err(_)) + | ExprKind::Struct(PathKind::Err(_), ..) => { unreachable!(); } } @@ -807,8 +810,8 @@ impl Visitor<'_> for QSharpGen { self.write(&id.name); } - fn visit_idents(&mut self, idents: &'_ Idents) { - self.write(&idents.name()); + fn visit_idents(&mut self, idents: &'_ [Ident]) { + self.write(&idents.full_name()); } } diff --git a/compiler/qsc_doc_gen/src/display.rs b/compiler/qsc_doc_gen/src/display.rs index b0061c3f8d..f579b40d3d 100644 --- a/compiler/qsc_doc_gen/src/display.rs +++ b/compiler/qsc_doc_gen/src/display.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc_ast::ast; +use qsc_ast::ast::{self, Idents}; use qsc_frontend::resolve; use qsc_hir::{ hir::{self, PackageId}, @@ -516,7 +516,7 @@ impl<'a> Display for AstTy<'a> { } ast::TyKind::Hole => write!(f, "_"), ast::TyKind::Paren(ty) => write!(f, "{}", AstTy { ty }), - ast::TyKind::Path(path) => write!(f, "{}", AstPath { path }), + ast::TyKind::Path(path) => write!(f, "{}", AstPathKind { path }), ast::TyKind::Param(id) => write!(f, "{}", id.name), ast::TyKind::Tuple(tys) => fmt_tuple(f, tys, |ty| AstTy { ty }), ast::TyKind::Err => write!(f, "?"), @@ -540,15 +540,16 @@ impl<'a> Display for FunctorExpr<'a> { } } -struct AstPath<'a> { - path: &'a ast::Path, +struct AstPathKind<'a> { + path: &'a ast::PathKind, } -impl<'a> Display for AstPath<'a> { +impl<'a> Display for AstPathKind<'a> { fn fmt(&self, f: &mut Formatter<'_>) -> Result { - match self.path.segments.as_ref() { - Some(parts) => write!(f, "{parts}.{}", self.path.name.name), - None => write!(f, "{}", self.path.name.name), + if let ast::PathKind::Ok(path) = self.path { + write!(f, "{}", path.full_name()) + } else { + write!(f, "?") } } } diff --git a/compiler/qsc_frontend/src/compile.rs b/compiler/qsc_frontend/src/compile.rs index 259e8af63b..4c02a05110 100644 --- a/compiler/qsc_frontend/src/compile.rs +++ b/compiler/qsc_frontend/src/compile.rs @@ -154,7 +154,7 @@ impl SourceMap { /// Returns the sources as an iter, but with the project root directory subtracted /// from the individual source names. - pub(crate) fn relative_sources(&self) -> impl Iterator + '_ { + pub fn relative_sources(&self) -> impl Iterator + '_ { self.sources.iter().map(move |source| { let name = source.name.as_ref(); let relative_name = if let Some(common_prefix) = &self.common_prefix { diff --git a/compiler/qsc_frontend/src/compile/preprocess.rs b/compiler/qsc_frontend/src/compile/preprocess.rs index 8dcebe629d..f79ba7d7a9 100644 --- a/compiler/qsc_frontend/src/compile/preprocess.rs +++ b/compiler/qsc_frontend/src/compile/preprocess.rs @@ -3,7 +3,7 @@ use core::str::FromStr; use qsc_ast::{ - ast::{Attr, ExprKind, ItemKind, Namespace, Stmt, StmtKind, UnOp}, + ast::{Attr, ExprKind, Idents, ItemKind, Namespace, PathKind, Stmt, StmtKind, UnOp}, mut_visit::MutVisitor, }; use qsc_hir::hir; @@ -54,12 +54,12 @@ impl MutVisitor for Conditional { ItemKind::Callable(callable) => { self.included_names.push(TrackedName { name: callable.name.name.clone(), - namespace: namespace.name.name(), + namespace: namespace.name.full_name(), }); } ItemKind::Ty(ident, _) => self.included_names.push(TrackedName { name: ident.name.clone(), - namespace: namespace.name.name(), + namespace: namespace.name.full_name(), }), _ => {} } @@ -69,12 +69,12 @@ impl MutVisitor for Conditional { ItemKind::Callable(callable) => { self.dropped_names.push(TrackedName { name: callable.name.name.clone(), - namespace: namespace.name.name(), + namespace: namespace.name.full_name(), }); } ItemKind::Ty(ident, _) => self.dropped_names.push(TrackedName { name: ident.name.clone(), - namespace: namespace.name.name(), + namespace: namespace.name.full_name(), }), _ => {} } @@ -141,7 +141,7 @@ fn matches_config(attrs: &[Box], capabilities: TargetCapabilityFlags) -> b for attr in attrs { if let ExprKind::Paren(inner) = attr.arg.kind.as_ref() { match inner.kind.as_ref() { - ExprKind::Path(path) => { + ExprKind::Path(PathKind::Ok(path)) => { if let Ok(capability) = TargetCapabilityFlags::from_str(path.name.name.as_ref()) { if capability.is_empty() { @@ -153,7 +153,7 @@ fn matches_config(attrs: &[Box], capabilities: TargetCapabilityFlags) -> b } } ExprKind::UnOp(UnOp::NotL, inner) => { - if let ExprKind::Path(path) = inner.kind.as_ref() { + if let ExprKind::Path(PathKind::Ok(path)) = inner.kind.as_ref() { if let Ok(capability) = TargetCapabilityFlags::from_str(path.name.name.as_ref()) { diff --git a/compiler/qsc_frontend/src/compile/preprocess/tests.rs b/compiler/qsc_frontend/src/compile/preprocess/tests.rs index 55f52a2f4c..5991416482 100644 --- a/compiler/qsc_frontend/src/compile/preprocess/tests.rs +++ b/compiler/qsc_frontend/src/compile/preprocess/tests.rs @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use qsc_ast::ast::{Attr, Expr, ExprKind, Ident, NodeId, Path}; +use qsc_ast::ast::{Attr, Expr, ExprKind, Ident, NodeId, Path, PathKind}; use qsc_data_structures::span::Span; use crate::compile::{preprocess::matches_config, TargetCapabilityFlags}; @@ -36,7 +36,7 @@ fn name_value_attr(name: &str, value: &str) -> Attr { kind: Box::new(ExprKind::Paren(Box::new(Expr { id: NodeId::default(), span: Span::default(), - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { id: NodeId::default(), span: Span::default(), segments: None, @@ -45,7 +45,7 @@ fn name_value_attr(name: &str, value: &str) -> Attr { span: Span::default(), id: NodeId::default(), }), - }))), + })))), }))), }), span: Span::default(), diff --git a/compiler/qsc_frontend/src/lower.rs b/compiler/qsc_frontend/src/lower.rs index 8c2cca73c9..360c1e856b 100644 --- a/compiler/qsc_frontend/src/lower.rs +++ b/compiler/qsc_frontend/src/lower.rs @@ -10,7 +10,7 @@ use crate::{ typeck::{self, convert}, }; use miette::Diagnostic; -use qsc_ast::ast::{self, Ident}; +use qsc_ast::ast::{self, Ident, Idents, PathKind}; use qsc_data_structures::{index_map::IndexMap, span::Span, target::TargetCapabilityFlags}; use qsc_hir::{ assigner::Assigner, @@ -135,9 +135,10 @@ impl With<'_> { let exports: Vec<(_, Option)> = namespace .exports() .filter_map(|item| { - self.names - .get(item.path.id) - .map(|x| (x, item.alias.clone())) + let PathKind::Ok(path) = &item.path else { + return None; + }; + self.names.get(path.id).map(|x| (x, item.alias.clone())) }) .collect::>(); @@ -173,6 +174,7 @@ impl With<'_> { self.lowerer.parent = None; } + #[allow(clippy::too_many_lines)] fn lower_item( &mut self, item: &ast::Item, @@ -201,19 +203,22 @@ impl With<'_> { return None; } for item in &item.items { - let Some((id, alias)) = resolve_id(item.name().id) else { + let Some(item_name) = item.name() else { + continue; + }; + let Some((id, alias)) = resolve_id(item_name.id) else { continue; }; let is_reexport = id.package.is_some() || alias.is_some(); // if the package is Some, then this is a re-export and we // need to preserve the reference to the original `ItemId` if is_reexport { - let mut name = self.lower_ident(item.name()); + let mut name = self.lower_ident(item_name); name.id = self.assigner.next_node(); let kind = hir::ItemKind::Export(name, id); self.lowerer.items.push(hir::Item { id: self.assigner.next_item(), - span: item.span(), + span: item.span, parent: self.lowerer.parent, doc: "".into(), // attrs on exports not supported @@ -319,13 +324,13 @@ impl With<'_> { match &*attr.arg.kind { // @Config(Capability) ast::ExprKind::Paren(inner) - if matches!(inner.kind.as_ref(), ast::ExprKind::Path(path) + if matches!(inner.kind.as_ref(), ast::ExprKind::Path(PathKind::Ok(path)) if TargetCapabilityFlags::from_str(path.name.name.as_ref()).is_ok()) => {} // @Config(not Capability) ast::ExprKind::Paren(inner) if matches!(inner.kind.as_ref(), ast::ExprKind::UnOp(ast::UnOp::NotL, inner) - if matches!(inner.kind.as_ref(), ast::ExprKind::Path(path) + if matches!(inner.kind.as_ref(), ast::ExprKind::Path(PathKind::Ok(path)) if TargetCapabilityFlags::from_str(path.as_ref().name.name.as_ref()).is_ok())) => {} @@ -570,7 +575,6 @@ impl With<'_> { ast::ExprKind::Conjugate(within, apply) => { hir::ExprKind::Conjugate(self.lower_block(within), self.lower_block(apply)) } - ast::ExprKind::Err => hir::ExprKind::Err, ast::ExprKind::Fail(message) => hir::ExprKind::Fail(Box::new(self.lower_expr(message))), ast::ExprKind::Field(container, name) => { let container = self.lower_expr(container); @@ -615,7 +619,7 @@ impl With<'_> { } ast::ExprKind::Lit(lit) => lower_lit(lit), ast::ExprKind::Paren(_) => unreachable!("parentheses should be removed earlier"), - ast::ExprKind::Path(path) => { + ast::ExprKind::Path(PathKind::Ok(path)) => { let args = self .tys .generics @@ -634,8 +638,8 @@ impl With<'_> { fixup.as_ref().map(|f| self.lower_block(f)), ), ast::ExprKind::Return(expr) => hir::ExprKind::Return(Box::new(self.lower_expr(expr))), - ast::ExprKind::Struct(name, copy, fields) => hir::ExprKind::Struct( - self.node_id_to_res(name.id), + ast::ExprKind::Struct(PathKind::Ok(path), copy, fields) => hir::ExprKind::Struct( + self.node_id_to_res(path.id), copy.as_ref().map(|c| Box::new(self.lower_expr(c))), fields .iter() @@ -676,6 +680,9 @@ impl With<'_> { ast::ExprKind::While(cond, body) => { hir::ExprKind::While(Box::new(self.lower_expr(cond)), self.lower_block(body)) } + ast::ExprKind::Err + | &ast::ExprKind::Path(ast::PathKind::Err(_)) + | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => hir::ExprKind::Err, }; hir::Expr { @@ -866,7 +873,7 @@ impl With<'_> { fn path_parts_to_fields( &mut self, init_kind: hir::ExprKind, - parts: &[Ident], + parts: &[&Ident], lo: u32, ) -> hir::ExprKind { let (first, rest) = parts @@ -909,7 +916,7 @@ impl With<'_> { }) } - fn lower_idents(&mut self, name: &ast::Idents) -> hir::Idents { + fn lower_idents(&mut self, name: &impl Idents) -> hir::Idents { name.iter().map(|i| self.lower_ident(i)).collect() } } diff --git a/compiler/qsc_frontend/src/resolve.rs b/compiler/qsc_frontend/src/resolve.rs index 070c1da567..3d12aac36c 100644 --- a/compiler/qsc_frontend/src/resolve.rs +++ b/compiler/qsc_frontend/src/resolve.rs @@ -7,7 +7,8 @@ mod tests; use miette::Diagnostic; use qsc_ast::{ ast::{ - self, CallableBody, CallableDecl, Ident, Idents, NodeId, SpecBody, SpecGen, TopLevelNode, + self, CallableBody, CallableDecl, Ident, Idents, NodeId, PathKind, SpecBody, SpecGen, + TopLevelNode, }, visit::{self as ast_visit, walk_attr, Visitor as AstVisitor}, }; @@ -40,12 +41,12 @@ pub(super) type Names = IndexMap; // Otherwise, returns None. // Field accessor paths have their leading segment mapped as a local variable, whereas namespace paths have their path id mapped. #[must_use] -pub fn path_as_field_accessor( +pub fn path_as_field_accessor<'a>( names: &Names, - path: &ast::Path, -) -> Option<(NodeId, Vec)> { + path: &'a ast::Path, +) -> Option<(NodeId, Vec<&'a ast::Ident>)> { if path.segments.is_some() { - let parts: Vec = path.into(); + let parts: Vec<&Ident> = path.iter().collect(); let first = parts.first().expect("path should have at least one part"); if let Some(&Res::Local(node_id)) = names.get(first.id) { return Some((node_id, parts)); @@ -384,8 +385,8 @@ impl GlobalScope { /// Creates a namespace in the namespace mapping. Note that namespaces are tracked separately from their /// item contents. This returns a [`NamespaceId`] which you can use to add more tys and terms to the scope. - fn insert_or_find_namespace(&mut self, name: impl Into>>) -> NamespaceId { - self.namespaces.insert_or_find_namespace(name.into()) + fn insert_or_find_namespace(&mut self, name: impl IntoIterator>) -> NamespaceId { + self.namespaces.insert_or_find_namespace(name) } /// Given a starting namespace, search from that namespace. @@ -509,7 +510,7 @@ impl AstVisitor<'_> for ExportImportVisitor<'_> { .resolver .bind_import_or_export(decl, Some((ns, &namespace.name))); } - ItemKind::Open(name, alias) => { + ItemKind::Open(PathKind::Ok(path), alias) => { // we only need to bind opens that are in top-level namespaces, outside of callables. // this is because this is for the intermediate export-binding pass // and in the export-binding pass, we only need to know what symbols are available @@ -523,7 +524,7 @@ impl AstVisitor<'_> for ExportImportVisitor<'_> { // namespace A { callable foo() { open B; export { SomethingFromB }; } } // ^^^^^^ export from non-namespace scope is not allowed // ``` - visitor.resolver.bind_open(name, alias, ns); + visitor.resolver.bind_open(path.as_ref(), alias, ns); } _ => ast_visit::walk_item(visitor, item), } @@ -658,11 +659,10 @@ impl Resolver { // First we check if the the path can be resolved as a field accessor. // We do this by checking if the first part of the path is a local variable. - if let (NameKind::Term, Some(parts)) = (kind, segments) { - let parts: Vec = parts.clone().into(); - let first = parts + if let (NameKind::Term, Some(segments)) = (kind, segments) { + let first = segments .first() - .expect("path `parts` should have at least one element"); + .expect("path `segments` should have at least one element"); match resolve( NameKind::Term, &self.globals, @@ -754,7 +754,7 @@ impl Resolver { fn bind_open( &mut self, - name: &Idents, + name: &impl Idents, alias: &Option>, current_namespace: NamespaceId, ) { @@ -771,7 +771,7 @@ impl Resolver { } else if let Some(id) = self.globals.namespaces.get_namespace_id(name.str_iter()) { id } else { - let error = Error::NotFound(name.name().to_string(), name.span()); + let error = Error::NotFound(name.full_name().to_string(), name.full_span()); self.errors.push(error); return; }; @@ -786,7 +786,7 @@ impl Resolver { let open = Open { namespace: id, - span: name.span(), + span: name.full_span(), }; if !current_opens.contains(&open) { current_opens.push(open); @@ -802,11 +802,13 @@ impl Resolver { ) { match &*item.kind { ast::ItemKind::Open(name, alias) => { - self.bind_open( - name, - alias, - namespace.unwrap_or_else(|| self.globals.namespaces.root_id()), - ); + if let PathKind::Ok(path) = name { + self.bind_open( + path.as_ref(), + alias, + namespace.unwrap_or_else(|| self.globals.namespaces.root_id()), + ); + } } ast::ItemKind::Callable(decl) => { let id = intrapackage(assigner.next_item()); @@ -868,7 +870,7 @@ impl Resolver { fn bind_import_or_export( &mut self, decl: &ImportOrExportDecl, - current_namespace: Option<(NamespaceId, &Idents)>, + current_namespace: Option<(NamespaceId, &[Ident])>, ) { let (current_namespace, current_namespace_name) = if let Some((a, b)) = current_namespace { (Some(a), Some(b)) @@ -876,7 +878,8 @@ impl Resolver { (None, None) }; - let current_namespace_name: Option> = current_namespace_name.map(Idents::name); + let current_namespace_name: Option> = + current_namespace_name.map(|ns| ns.full_name()); let is_export = decl.is_export(); for decl_item in decl @@ -889,9 +892,10 @@ impl Resolver { // problem without upleveling the preprocessor into the resolver, so it can do resolution-aware // dropped_names population. .filter(|item| { - if let Some(ref current_namespace_name) = current_namespace_name { - let item_as_tracked_name = - path_as_tracked_name(&item.path, current_namespace_name); + if let (Some(ref current_namespace_name), PathKind::Ok(path)) = + (¤t_namespace_name, &item.path) + { + let item_as_tracked_name = path_as_tracked_name(path, current_namespace_name); !self.dropped_names.contains(&item_as_tracked_name) } else { true @@ -899,6 +903,12 @@ impl Resolver { }) .collect::>() { + let PathKind::Ok(path) = &decl_item.path else { + continue; + }; + let Some(decl_item_name) = decl_item.name() else { + continue; + }; let mut decl_alias = decl_item.alias.clone(); if decl_item.is_glob { self.bind_glob_import_or_export(decl_item, decl.is_export()); @@ -906,8 +916,8 @@ impl Resolver { } let (term_result, ty_result) = ( - self.resolve_path(NameKind::Term, &decl_item.path), - self.resolve_path(NameKind::Ty, &decl_item.path), + self.resolve_path(NameKind::Term, path), + self.resolve_path(NameKind::Ty, path), ); if let (Err(err), Err(_)) = (&term_result, &ty_result) { @@ -921,15 +931,15 @@ impl Resolver { Ok(()) => (), Err(_) => { self.errors.push(Error::ClobberedNamespace { - namespace_name: decl_item.name().name.to_string(), - span: decl_item.span(), + namespace_name: decl_item_name.name.to_string(), + span: decl_item.span, }); } }; continue; }; - let local_name = decl_item.name().name.clone(); + let local_name = decl_item_name.name.clone(); { let scope = self.current_scope_mut(); @@ -942,7 +952,7 @@ impl Resolver { if entry.source == ItemSource::Exported => { let err = - Error::DuplicateExport(local_name.to_string(), decl_item.name().span); + Error::DuplicateExport(local_name.to_string(), decl_item_name.span); self.errors.push(err); continue; } @@ -963,7 +973,7 @@ impl Resolver { _ => { let err = Error::ImportedDuplicate( local_name.to_string(), - decl_item.name().span, + decl_item_name.span, ); self.errors.push(err); continue; @@ -1056,7 +1066,7 @@ impl Resolver { } else { Error::ImportedNonItem }; - let err = err(decl_item.path.span); + let err = err(path.span); self.errors.push(err); continue; } @@ -1074,16 +1084,16 @@ impl Resolver { // the definition comes from. Res::Item(item_id, _) if item_id.package.is_some() && is_export => { self.names - .insert(decl_item.name().id, Res::ExportedItem(item_id, decl_alias)); + .insert(decl_item_name.id, Res::ExportedItem(item_id, decl_alias)); } Res::Item(underlying_item_id, _) if decl_alias.is_some() && is_export => { // insert the export's alias self.names.insert( - decl_item.name().id, + decl_item_name.id, Res::ExportedItem(underlying_item_id, decl_alias), ); } - _ => self.names.insert(decl_item.name().id, res), + _ => self.names.insert(decl_item_name.id, res), } } } @@ -1092,33 +1102,35 @@ impl Resolver { /// Globs can only be attached to namespaces, and /// they import all items from the namespace into the current scope. fn bind_glob_import_or_export(&mut self, item: &ImportOrExportItem, is_export: bool) { + let PathKind::Ok(path) = &item.path else { + return; + }; + if is_export { - self.errors - .push(Error::GlobExportNotSupported(item.path.span)); + self.errors.push(Error::GlobExportNotSupported(path.span)); return; } if let Some(alias) = &item.alias { self.errors.push(Error::GlobImportAliasNotSupported { - span: item.span(), - namespace_name: Into::::into(item.path.clone()).name().to_string(), + span: item.span, + namespace_name: path.full_name().to_string(), alias: alias.name.to_string(), }); return; } - let items = Into::::into(item.path.clone()); - let ns = self.globals.find_namespace(items.str_iter()); + let ns = self.globals.find_namespace(path.str_iter()); let Some(ns) = ns else { self.errors.push(Error::GlobImportNamespaceNotFound( - item.path.name.name.to_string(), - item.path.span, + path.name.name.to_string(), + path.span, )); return; }; if !is_export { - self.bind_open(&items, &None, ns); + self.bind_open(path.as_ref(), &None, ns); } } @@ -1159,24 +1171,28 @@ impl Resolver { current_namespace: Option, err: &Error, ) -> Result<(), ClobberedNamespace> { - let items = Into::::into(item.path.clone()); - let ns = self.globals.find_namespace(items.str_iter()); + let PathKind::Ok(path) = &item.path else { + return Ok(()); + }; + + let ns = self.globals.find_namespace(path.str_iter()); + let alias = item .alias .as_ref() .map(|x| Box::new(x.clone())) - .or(Some(item.path.name.clone())); + .or(Some(path.name.clone())); if let Some(ns) = ns { if is_export { // for exports, we update the namespace tree accordingly: // update the namespace tree to include the new namespace - let alias = alias.unwrap_or(item.path.name.clone()); + let alias = alias.unwrap_or(path.name.clone()); self.globals .namespaces .insert_with_id(current_namespace, ns, &alias.name)?; } else { // for imports, we just bind the namespace as an open - self.bind_open(&items, &alias, ns); + self.bind_open(path.as_ref(), &alias, ns); } } else { self.errors.push(err.clone()); @@ -1279,7 +1295,7 @@ impl AstVisitor<'_> for With<'_> { // Only locally scoped imports and exports are handled here. self.resolver.bind_import_or_export(decl, None); } - ItemKind::Open(name, alias) => { + ItemKind::Open(PathKind::Ok(path), alias) => { let scopes = self.resolver.curr_scope_chain.iter().rev(); let namespace = scopes .into_iter() @@ -1294,7 +1310,7 @@ impl AstVisitor<'_> for With<'_> { .unwrap_or_else(|| self.resolver.globals.namespaces.root_id()); // Only locally scoped opens are handled here. // There is only a namespace parent scope if we aren't executing incremental fragments. - self.resolver.bind_open(name, alias, namespace); + self.resolver.bind_open(path.as_ref(), alias, namespace); } _ => ast_visit::walk_item(self, item), } @@ -1345,7 +1361,7 @@ impl AstVisitor<'_> for With<'_> { fn visit_ty(&mut self, ty: &ast::Ty) { match &*ty.kind { - ast::TyKind::Path(path) => { + ast::TyKind::Path(PathKind::Ok(path)) => { if let Err(e) = self.resolver.resolve_path(NameKind::Ty, path) { self.resolver.errors.push(e); } @@ -1412,7 +1428,7 @@ impl AstVisitor<'_> for With<'_> { visitor.visit_expr(output); }); } - ast::ExprKind::Path(path) => { + ast::ExprKind::Path(PathKind::Ok(path)) => { if let Err(e) = self.resolver.resolve_path(NameKind::Term, path) { self.resolver.errors.push(e); }; @@ -1431,7 +1447,7 @@ impl AstVisitor<'_> for With<'_> { } self.visit_expr(replace); } - ast::ExprKind::Struct(path, copy, fields) => { + ast::ExprKind::Struct(PathKind::Ok(path), copy, fields) => { if let Err(e) = self.resolver.resolve_path(NameKind::Ty, path) { self.resolver.errors.push(e); }; @@ -1606,7 +1622,8 @@ impl GlobalTable { .insert(global.name.clone(), Res::ExportedItem(item_id, None)); } hir::ItemKind::Namespace(ns, _items) => { - self.scope.insert_or_find_namespace(ns); + self.scope + .insert_or_find_namespace(ns.iter().map(|s| s.name.clone())); } hir::ItemKind::Ty(..) => { self.scope @@ -1655,7 +1672,7 @@ fn bind_global_items( Res::Item(intrapackage(assigner.next_item()), ItemStatus::Available), ); - let namespace_id = scope.insert_or_find_namespace(&namespace.name); + let namespace_id = scope.insert_or_find_namespace(namespace.name.rc_str_iter().cloned()); for item in &namespace.items { match bind_global_item( @@ -1677,7 +1694,7 @@ fn bind_global_items( pub(super) fn extract_field_name<'a>(names: &Names, expr: &'a ast::Expr) -> Option<&'a Rc> { // Follow the same reasoning as `is_field_update`. match &*expr.kind { - ast::ExprKind::Path(path) + ast::ExprKind::Path(PathKind::Ok(path)) if path.segments.is_none() && !matches!(names.get(path.id), Some(Res::Local(_))) => { Some(&path.name.name) @@ -1694,7 +1711,7 @@ fn is_field_update<'a>( // Disambiguate the update operator by looking at the index expression. If it's an // unqualified path that doesn't resolve to a local, assume that it's meant to be a field name. match &*index.kind { - ast::ExprKind::Path(path) if path.segments.is_none() => !matches!( + ast::ExprKind::Path(PathKind::Ok(path)) if path.segments.is_none() => !matches!( { let name = &path.name; let namespace = &path.segments; @@ -1731,11 +1748,14 @@ fn bind_global_item( Ok(()) } else { for decl_item in &decl.items { + let PathKind::Ok(path) = &decl_item.path else { + continue; + }; + let Some(decl_item_name) = decl_item.name() else { + continue; + }; // if the item is a namespace, bind it here as an item - let Some(ns) = scope - .namespaces - .get_namespace_id(Into::::into(decl_item.path.clone()).str_iter()) - else { + let Some(ns) = scope.namespaces.get_namespace_id(path.str_iter()) else { continue; }; if ns == namespace { @@ -1745,11 +1765,11 @@ fn bind_global_item( } let item_id = next_id(); let res = Res::Item(item_id, ItemStatus::Available); - names.insert(decl_item.name().id, res.clone()); + names.insert(decl_item_name.id, res.clone()); match scope .terms .get_mut_or_default(namespace) - .entry(Rc::clone(&decl_item.name().name)) + .entry(Rc::clone(&decl_item_name.name)) { Entry::Occupied(_) => { let namespace_name = scope @@ -1758,9 +1778,9 @@ fn bind_global_item( .0 .join("."); return Err(vec![Error::Duplicate( - decl_item.name().name.to_string(), + decl_item_name.name.to_string(), namespace_name, - decl_item.name().span, + decl_item_name.span, )]); } Entry::Vacant(entry) => { @@ -1772,11 +1792,11 @@ fn bind_global_item( if let Err(ClobberedNamespace) = scope .namespaces - .insert_with_id(Some(namespace), ns, &decl_item.name().name) + .insert_with_id(Some(namespace), ns, &decl_item_name.name) { return Err(vec![Error::ClobberedNamespace { - namespace_name: decl_item.name().name.to_string(), - span: decl_item.name().span, + namespace_name: decl_item_name.name.to_string(), + span: decl_item_name.span, }]); } } @@ -1914,7 +1934,7 @@ fn resolve<'a>( globals: &GlobalScope, scopes: impl Iterator, provided_symbol_name: &Ident, - provided_namespace_name: &Option, + provided_namespace_name: &Option>, ) -> Result { if let Some(value) = check_all_scopes( kind, @@ -1986,13 +2006,10 @@ fn resolve<'a>( } Err(match provided_namespace_name { - Some(ns) => Error::NotFound( - ns.push(provided_symbol_name.clone()).name().to_string(), - Span { - lo: ns.span().lo, - hi: provided_symbol_name.span.hi, - }, - ), + Some(ns) => { + let full_name = (ns, provided_symbol_name); + Error::NotFound(full_name.full_name().to_string(), full_name.full_span()) + } None => Error::NotFound( provided_symbol_name.name.to_string(), provided_symbol_name.span, @@ -2006,7 +2023,7 @@ fn check_all_scopes<'a>( kind: NameKind, globals: &GlobalScope, provided_symbol_name: &Ident, - provided_namespace_name: &Option, + provided_namespace_name: &Option>, scopes: impl Iterator, ) -> Option> { let mut vars = true; @@ -2050,7 +2067,7 @@ fn check_scoped_resolutions( kind: NameKind, globals: &GlobalScope, provided_symbol_name: &Ident, - provided_namespace_name: &Option, + provided_namespace_name: &Option>, vars: &mut bool, scope: &Scope, ) -> Option> { @@ -2142,7 +2159,7 @@ fn ambiguous_symbol_error( fn find_symbol_in_namespaces( kind: NameKind, globals: &GlobalScope, - provided_namespace_name: &Option, + provided_namespace_name: &Option>, provided_symbol_name: &Ident, namespaces_to_search: T, aliases: &FxHashMap>, Vec<(NamespaceId, O)>>, @@ -2168,9 +2185,7 @@ where find_symbol_in_namespace( kind, globals, - &provided_namespace_name - .as_ref() - .map(|x| x.iter().skip(1).cloned().collect::>().into()), + &provided_namespace_name.as_ref().map(|x| &x[1..]), provided_symbol_name, &mut candidates, open.0, @@ -2213,7 +2228,7 @@ where fn find_symbol_in_namespace( kind: NameKind, globals: &GlobalScope, - provided_namespace_name: &Option, + provided_namespace_name: &Option, provided_symbol_name: &Ident, candidates: &mut FxHashMap, candidate_namespace_id: NamespaceId, @@ -2336,9 +2351,16 @@ fn get_scope_locals(scope: &Scope, offset: u32, vars: bool) -> Vec { // items // skip adding newtypes since they're already in the terms map - names.extend(scope.terms.iter().map(|term| Local { - name: term.0.clone(), - kind: LocalKind::Item(term.1.id), + names.extend(scope.terms.iter().filter_map(|(name, entry)| { + if entry.source == ItemSource::Declared { + Some(Local { + name: name.clone(), + kind: LocalKind::Item(entry.id), + }) + } else { + // Exclude imports as they are not local items + None + } })); names diff --git a/compiler/qsc_frontend/src/resolve/tests.rs b/compiler/qsc_frontend/src/resolve/tests.rs index 554d0ccb0f..2851b059ef 100644 --- a/compiler/qsc_frontend/src/resolve/tests.rs +++ b/compiler/qsc_frontend/src/resolve/tests.rs @@ -10,7 +10,7 @@ use crate::{ }; use expect_test::{expect, Expect}; use indoc::indoc; -use qsc_ast::ast::{Idents, Item, ItemKind}; +use qsc_ast::ast::{Idents, Item, ItemKind, PathKind}; use qsc_ast::{ assigner::Assigner as AstAssigner, ast::{Ident, NodeId, Package, Path, TopLevelNode}, @@ -26,8 +26,8 @@ use qsc_data_structures::{ }; use qsc_hir::assigner::Assigner as HirAssigner; use rustc_hash::FxHashMap; -use std::fmt::Write; use std::rc::Rc; +use std::{fmt::Write, vec}; #[derive(Debug)] enum Change { @@ -95,10 +95,38 @@ impl<'a> Renamer<'a> { impl Visitor<'_> for Renamer<'_> { fn visit_path(&mut self, path: &Path) { if let Some(res) = self.names.get(path.id) { + // The whole path node can be a resolved name self.changes.push((path.span, res.clone().into())); - } else { - visit::walk_path(self, path); + return; + } + + let ns_id = self.find_namespace_id(path); + if let Some(ns_id) = ns_id { + // The whole path can be a namespace + self.changes.push((path.span, ns_id.into())); + return; + } + + if let Some(segments) = &path.segments { + // The segments part can be a namespace + let ns_id = self.find_namespace_id(segments); + if let Some(ns_id) = ns_id { + self.changes.push((segments.full_span(), ns_id.into())); + return; + } + } + + // Individual ident nodes can be resolved names + visit::walk_path(self, path); + } + + fn visit_idents(&mut self, idents: &[Ident]) { + let ns_id = self.find_namespace_id(&idents); + if let Some(ns_id) = ns_id { + self.changes.push((idents.full_span(), ns_id.into())); + return; } + visit::walk_idents(self, idents); } fn visit_ident(&mut self, ident: &Ident) { @@ -109,21 +137,34 @@ impl Visitor<'_> for Renamer<'_> { fn visit_item(&mut self, item: &'_ Item) { match &*item.kind { - ItemKind::Open(namespace, Some(alias)) => { - let Some(ns_id) = self.namespaces.get_namespace_id(namespace.str_iter()) else { + ItemKind::Open(PathKind::Ok(namespace), Some(alias)) => { + if let Some(ns_id) = self.namespaces.get_namespace_id(namespace.str_iter()) { + // self.changes.push((item.span, ns_id.into())); + self.aliases.insert(vec![alias.name.clone()], ns_id); + } else { return; }; - self.aliases.insert(vec![alias.name.clone()], ns_id); } ItemKind::ImportOrExport(export) => { for item in export.items() { - if let Some(res) = self.names.get(item.path.id) { - self.changes.push((item.span(), (res.clone()).into())); - } else if let Some(namespace_id) = self - .namespaces - .get_namespace_id(Into::::into(item.clone().path).str_iter()) + let PathKind::Ok(path) = &item.path else { + continue; + }; + if let Some(res) = self.names.get(path.id) { + // Path node can be a resolved name + self.changes.push((item.span, (res.clone()).into())); + } else if let Some(namespace_id) = + self.namespaces.get_namespace_id(path.str_iter()) { - self.changes.push((item.span(), namespace_id.into())); + // Path can be a namespace + self.changes.push(( + if item.alias.is_some() { + item.span + } else { + path.span + }, + namespace_id.into(), + )); } } return; @@ -132,27 +173,19 @@ impl Visitor<'_> for Renamer<'_> { } visit::walk_item(self, item); } +} - fn visit_idents(&mut self, vec_ident: &Idents) { - let parts: Vec = vec_ident.clone().into(); - let first = parts.first().expect("should contain at least one item"); - if let Some(res) = self.names.get(first.id) { - self.changes.push((first.span, res.clone().into())); - return; - } - - let ns_id = match self.namespaces.get_namespace_id(vec_ident.str_iter()) { - Some(x) => x, - None => match self - .aliases - .get(&(Into::>>::into(vec_ident))) - .copied() - { - Some(x) => x, - None => return, - }, - }; - self.changes.push((vec_ident.span(), ns_id.into())); +impl<'a> Renamer<'a> { + fn find_namespace_id(&mut self, idents: &impl Idents) -> Option { + let ns_id = self + .namespaces + .get_namespace_id(idents.str_iter()) + .or_else(|| { + self.aliases + .get(&idents.rc_str_iter().cloned().collect::>()) + .copied() + }); + ns_id } } @@ -1374,7 +1407,7 @@ fn open_ambiguous_tys() { open namespace3; open namespace4; - function item5(local28 : A) : Unit {} + function item5(local30 : A) : Unit {} } // Ambiguous { name: "A", first_open: "Foo", second_open: "Bar", name_span: Span { lo: 146, hi: 147 }, first_open_span: Span { lo: 107, hi: 110 }, second_open_span: Span { lo: 121, hi: 124 } } @@ -1458,7 +1491,7 @@ fn merged_aliases_ambiguous_tys() { open namespace3 as Alias; open namespace4 as Alias; - function item5(local30 : namespace4.A) : Unit {} + function item5(local32 : namespace4.A) : Unit {} } // Ambiguous { name: "A", first_open: "Foo", second_open: "Bar", name_span: Span { lo: 170, hi: 171 }, first_open_span: Span { lo: 107, hi: 110 }, second_open_span: Span { lo: 130, hi: 133 } } @@ -4201,7 +4234,7 @@ fn export_udt_and_construct_it() { namespace namespace4 { open namespace3; operation item3() : Unit { - let local33: item1 = item1(1, 2); + let local34: item1 = item1(1, 2); } } "#]], diff --git a/compiler/qsc_frontend/src/typeck/convert.rs b/compiler/qsc_frontend/src/typeck/convert.rs index 2c2f56c51c..dc97f6493e 100644 --- a/compiler/qsc_frontend/src/typeck/convert.rs +++ b/compiler/qsc_frontend/src/typeck/convert.rs @@ -6,7 +6,7 @@ use std::rc::Rc; use crate::resolve::{self, Names}; use qsc_ast::ast::{ self, CallableBody, CallableDecl, CallableKind, FunctorExpr, FunctorExprKind, Ident, Pat, - PatKind, Path, SetOp, Spec, StructDecl, TyDef, TyDefKind, TyKind, + PatKind, Path, PathKind, SetOp, Spec, StructDecl, TyDef, TyDefKind, TyKind, }; use qsc_data_structures::span::Span; use qsc_hir::{ @@ -42,7 +42,7 @@ pub(crate) fn ty_from_ast(names: &Names, ty: &ast::Ty) -> (Ty, Vec (Ty::Err, vec![MissingTyError(ty.span)]), TyKind::Paren(inner) => ty_from_ast(names, inner), - TyKind::Path(path) => (ty_from_path(names, path), Vec::new()), + TyKind::Path(PathKind::Ok(path)) => (ty_from_path(names, path), Vec::new()), TyKind::Param(name) => match names.get(name.id) { Some(resolve::Res::Param(id)) => (Ty::Param(name.name.clone(), *id), Vec::new()), Some(_) => unreachable!( @@ -61,7 +61,7 @@ pub(crate) fn ty_from_ast(names: &Names, ty: &ast::Ty) -> (Ty, Vec (Ty::Err, Vec::new()), + TyKind::Err | TyKind::Path(PathKind::Err { .. }) => (Ty::Err, Vec::new()), } } diff --git a/compiler/qsc_frontend/src/typeck/rules.rs b/compiler/qsc_frontend/src/typeck/rules.rs index e809c172f3..59eac78b09 100644 --- a/compiler/qsc_frontend/src/typeck/rules.rs +++ b/compiler/qsc_frontend/src/typeck/rules.rs @@ -8,8 +8,8 @@ use super::{ }; use crate::resolve::{self, Names, Res}; use qsc_ast::ast::{ - self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, Path, QubitInit, - QubitInitKind, Spec, Stmt, StmtKind, StringComponent, TernOp, TyKind, UnOp, + self, BinOp, Block, Expr, ExprKind, Functor, Ident, Lit, NodeId, Pat, PatKind, Path, PathKind, + QubitInit, QubitInitKind, Spec, Stmt, StmtKind, StringComponent, TernOp, TyKind, UnOp, }; use qsc_data_structures::span::Span; use qsc_hir::{ @@ -105,7 +105,7 @@ impl<'a> Context<'a> { })), TyKind::Hole => self.inferrer.fresh_ty(TySource::not_divergent(ty.span)), TyKind::Paren(inner) => self.infer_ty(inner), - TyKind::Path(path) => match self.names.get(path.id) { + TyKind::Path(PathKind::Ok(path)) => match self.names.get(path.id) { Some(&Res::Item(item, _)) => Ty::Udt(path.name.name.clone(), hir::Res::Item(item)), Some(&Res::PrimTy(prim)) => Ty::Prim(prim), Some(Res::UnitTy) => Ty::Tuple(Vec::new()), @@ -136,7 +136,7 @@ impl<'a> Context<'a> { TyKind::Tuple(items) => { Ty::Tuple(items.iter().map(|item| self.infer_ty(item)).collect()) } - TyKind::Err => Ty::Err, + TyKind::Err | TyKind::Path(PathKind::Err { .. }) => Ty::Err, } } @@ -394,7 +394,7 @@ impl<'a> Context<'a> { Lit::String(_) => converge(Ty::Prim(Prim::String)), }, ExprKind::Paren(expr) => self.infer_expr(expr), - ExprKind::Path(path) => self.infer_path(expr, path), + ExprKind::Path(PathKind::Ok(path)) => self.infer_path(expr, path), ExprKind::Range(start, step, end) => { let mut diverges = false; for expr in start.iter().chain(step).chain(end) { @@ -444,7 +444,7 @@ impl<'a> Context<'a> { } self.diverge() } - ExprKind::Struct(name, copy, fields) => { + ExprKind::Struct(PathKind::Ok(name), copy, fields) => { let container = convert::ty_from_path(self.names, name); self.inferrer @@ -532,7 +532,9 @@ impl<'a> Context<'a> { self.typed_holes.push((expr.id, expr.span)); converge(self.inferrer.fresh_ty(TySource::not_divergent(expr.span))) } - ExprKind::Err => converge(Ty::Err), + ExprKind::Err + | ast::ExprKind::Path(ast::PathKind::Err(_)) + | ast::ExprKind::Struct(ast::PathKind::Err(_), ..) => converge(Ty::Err), }; self.record(expr.id, ty.ty.clone()); @@ -542,7 +544,7 @@ impl<'a> Context<'a> { fn infer_path_parts( &mut self, init_record: Partial, - rest: &[Ident], + rest: &[&Ident], lo: u32, ) -> Partial { let mut record = init_record; diff --git a/compiler/qsc_frontend/src/typeck/tests.rs b/compiler/qsc_frontend/src/typeck/tests.rs index 1c2dfffe80..d6494de5c3 100644 --- a/compiler/qsc_frontend/src/typeck/tests.rs +++ b/compiler/qsc_frontend/src/typeck/tests.rs @@ -12,7 +12,7 @@ use expect_test::{expect, Expect}; use indoc::indoc; use qsc_ast::{ assigner::Assigner as AstAssigner, - ast::{Block, Expr, Ident, NodeId, Package, Pat, Path, QubitInit, TopLevelNode}, + ast::{Block, Expr, Idents, NodeId, Package, Pat, Path, QubitInit, TopLevelNode}, mut_visit::MutVisitor, visit::{self, Visitor}, }; @@ -40,10 +40,10 @@ impl<'a> Visitor<'a> for TyCollector<'a> { fn visit_path(&mut self, path: &'a Path) { visit::walk_path(self, path); - let parts: Vec = path.into(); + let mut parts = path.iter().peekable(); if self .tys - .get(parts.first().expect("should contain at least one part").id) + .get(parts.peek().expect("should contain at least one part").id) .is_some() { for part in parts { @@ -3080,15 +3080,15 @@ fn local_open() { namespace B { function Bar() : () {} } "}, "", - &expect![[r#" + &expect![[r##" #6 26-28 "()" : Unit #8 34-52 "{ open B; Bar(); }" : Unit - #13 44-49 "Bar()" : Unit - #14 44-47 "Bar" : (Unit -> Unit) - #17 47-49 "()" : Unit - #23 81-83 "()" : Unit - #25 89-91 "{}" : Unit - "#]], + #14 44-49 "Bar()" : Unit + #15 44-47 "Bar" : (Unit -> Unit) + #18 47-49 "()" : Unit + #24 81-83 "()" : Unit + #26 89-91 "{}" : Unit + "##]], ); } diff --git a/compiler/qsc_linter/src/linter/ast.rs b/compiler/qsc_linter/src/linter/ast.rs index 1551fab9a7..697998ad4c 100644 --- a/compiler/qsc_linter/src/linter/ast.rs +++ b/compiler/qsc_linter/src/linter/ast.rs @@ -8,7 +8,7 @@ use crate::{ use qsc_ast::{ ast::{ Attr, Block, CallableDecl, Expr, FunctorExpr, Ident, Item, Namespace, Package, Pat, Path, - QubitInit, SpecDecl, Stmt, TopLevelNode, Ty, TyDef, + PathKind, QubitInit, SpecDecl, Stmt, TopLevelNode, Ty, TyDef, }, visit::Visitor, }; @@ -57,6 +57,7 @@ pub(crate) trait AstLintPass { fn check_package(&self, _package: &Package, _buffer: &mut Vec) {} fn check_pat(&self, _pat: &Pat, _buffer: &mut Vec) {} fn check_path(&self, _path: &Path, _buffer: &mut Vec) {} + fn check_path_kind(&self, _path: &PathKind, _buffer: &mut Vec) {} fn check_qubit_init(&self, _qubit_init: &QubitInit, _buffer: &mut Vec) {} fn check_spec_decl(&self, _spec_decl: &SpecDecl, _buffer: &mut Vec) {} fn check_stmt(&self, _stmt: &Stmt, _buffer: &mut Vec) {} @@ -84,7 +85,7 @@ macro_rules! declare_ast_lints { use crate::{linter::ast::{declare_ast_lints, AstLintPass}, Lint, LintLevel}; use qsc_ast::{ ast::{ - Attr, Block, CallableDecl, Expr, FunctorExpr, Ident, Item, Namespace, Package, Pat, Path, + Attr, Block, CallableDecl, Expr, FunctorExpr, Ident, Item, Namespace, Package, Pat, Path, PathKind, QubitInit, SpecDecl, Stmt, Ty, TyDef, }, visit::{self, Visitor}, @@ -204,6 +205,7 @@ macro_rules! declare_ast_lints { fn check_pat(&mut self, pat: &Pat) { $(self.$lint_name.check_pat(pat, &mut self.buffer));*; } fn check_qubit_init(&mut self, init: &QubitInit) { $(self.$lint_name.check_qubit_init(init, &mut self.buffer));*; } fn check_path(&mut self, path: &Path) { $(self.$lint_name.check_path(path, &mut self.buffer));*; } + fn check_path_kind(&mut self, path: &PathKind) { $(self.$lint_name.check_path_kind(path, &mut self.buffer));*; } fn check_ident(&mut self, ident: &Ident) { $(self.$lint_name.check_ident(ident, &mut self.buffer));*; } } @@ -283,6 +285,11 @@ macro_rules! declare_ast_lints { visit::walk_path(self, path); } + fn visit_path_kind(&mut self, path: &'a PathKind) { + self.check_path_kind(path); + visit::walk_path_kind(self, path); + } + fn visit_ident(&mut self, ident: &'a Ident) { self.check_ident(ident); } diff --git a/compiler/qsc_parse/Cargo.toml b/compiler/qsc_parse/Cargo.toml index f77b6215df..516be09635 100644 --- a/compiler/qsc_parse/Cargo.toml +++ b/compiler/qsc_parse/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true repository.workspace = true [dependencies] +bitflags = { workspace = true } enum-iterator = { workspace = true } miette = { workspace = true } num-bigint = { workspace = true } diff --git a/compiler/qsc_parse/src/completion.rs b/compiler/qsc_parse/src/completion.rs new file mode 100644 index 0000000000..da63ba6850 --- /dev/null +++ b/compiler/qsc_parse/src/completion.rs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +pub(crate) mod collector; +#[cfg(test)] +mod tests; +mod word_kinds; + +use crate::{item, scan::ParserContext}; +use collector::ValidWordCollector; +use qsc_data_structures::language_features::LanguageFeatures; +pub use word_kinds::*; + +/// Returns the words that would be valid syntax at a particular offset +/// in the given source file (using the source file parser). +/// +/// This is useful for providing completions in an editor. +#[must_use] +pub fn possible_words_at_offset_in_source( + input: &str, + source_name: Option<&str>, + language_features: LanguageFeatures, + at_offset: u32, +) -> WordKinds { + let mut collector = ValidWordCollector::new(at_offset); + let mut scanner = ParserContext::with_word_collector(input, language_features, &mut collector); + let _ = item::parse_namespaces_or_implicit(&mut scanner, source_name); + collector.into_words() +} + +/// Returns the words that would be valid syntax at a particular offset +/// in the given notebook cell (using the fragments parser). +/// +/// This is useful for providing completions in an editor. +#[must_use] +pub fn possible_words_at_offset_in_fragments( + input: &str, + language_features: LanguageFeatures, + at_offset: u32, +) -> WordKinds { + let mut collector = ValidWordCollector::new(at_offset); + let mut scanner = ParserContext::with_word_collector(input, language_features, &mut collector); + let _ = item::parse_top_level_nodes(&mut scanner); + collector.into_words() +} diff --git a/compiler/qsc_parse/src/completion/collector.rs b/compiler/qsc_parse/src/completion/collector.rs new file mode 100644 index 0000000000..50af494f4b --- /dev/null +++ b/compiler/qsc_parse/src/completion/collector.rs @@ -0,0 +1,186 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! The [`ValidWordCollector`] provides a mechanism to hook into the parser +//! to collect the possible valid words at a specific cursor location in the +//! code. It's meant to be used by the code completion feature in the +//! language service. +//! +//! Any time the parser is about to try parsing a word token, it records the +//! expected word(s) through a call to [`ValidWordCollector::expect()`]. +//! These are considered to be valid words for that location. +//! +//! If the parser is not at the cursor position yet, this call is ignored. +//! +//! Once the parser has reached the cursor position, the expected word(s) +//! are recorded into a list. +//! +//! At this point, the [`ValidWordCollector`] tricks the parser by +//! intercepting the lexer and returning an EOF token to the parser instead +//! of the real next token from the source. +//! +//! Since EOF will never match a token that the parser is looking for, this +//! causes the parser to keep trying all possible tokens at at this location, +//! recording the expected words in the process. Finally, it gives up. +//! +//! As soon as the parser reports a parse error at the cursor location, +//! the [`ValidWordCollector`] stops recording expected words. This +//! is to prevent the word list from getting polluted with words that are +//! expected after recovery occurs. +//! +//! For example, consider the code sample below, where `|` denotes the +//! cursor location: +//! +//! ```qsharp +//! operation Main() : Unit { let x: | +//! ``` +//! +//! When the parser gets to the cursor location, it looks for the words that are +//! applicable at a type position (paths, type parameters, etc). But it +//! keeps finding the EOF that was inserted by the [`ValidWordCollector`].As the +//! parser goes through each possible word, the word is recorded by the collector. +//! Finally, the parser gives up and reports a parse error. The parser then recovers, +//! and and starts looking for words that can start statements instead (`let`, etc). +//! These words are *not* recorded by the collector since they occur +//! after the parser has already reported an error. +//! +//! Note that returning EOF at the cursor means that the "manipulated" +//! parser will never run further than the cursor location, meaning the two +//! below code inputs are equivalent: +//! +//! ```qsharp +//! operation Foo() : | Unit {} +//! ``` +//! +//! ```qsharp +//! operation Foo() : | +//! ``` + +use super::WordKinds; +use crate::lex::{ClosedBinOp, Token, TokenKind}; +use qsc_data_structures::span::Span; + +pub(crate) struct ValidWordCollector { + cursor_offset: u32, + state: State, + collected: WordKinds, +} + +#[derive(Debug, PartialEq, Eq)] +enum State { + /// The parser has not reached the cursor location yet. + BeforeCursor, + /// The parser is at the cursor, i.e. the cursor touches the next + /// token the parser is about to consume. + /// + /// This is when we start collecting expected valid words from the parser. + AtCursor, + /// The parser has encountered an error at the cursor location. + /// Stop collecting expected valid words. + End, +} + +impl ValidWordCollector { + pub fn new(cursor_offset: u32) -> Self { + Self { + cursor_offset, + state: State::BeforeCursor, + collected: WordKinds::empty(), + } + } + + /// The parser expects the given word(s) at the next token. + pub fn expect(&mut self, expected: WordKinds) { + match self.state { + State::AtCursor => self.collected.extend(expected), + State::BeforeCursor | State::End => {} + } + } + + /// The parser has advanced. Update state. + pub fn did_advance(&mut self, next_token: &mut Token, scanner_offset: u32) { + match self.state { + State::BeforeCursor => { + if cursor_at_token(self.cursor_offset, *next_token, scanner_offset) { + self.state = State::AtCursor; + // Set the next token to be EOF. This will trick the parser into + // attempting to parse the token over and over again, + // collecting `WordKinds` in the process. + *next_token = eof(next_token.span.hi); + } + } + State::End | State::AtCursor => {} + } + } + + /// The parser reported an error. Update state. + pub fn did_error(&mut self) { + match self.state { + State::AtCursor => self.state = State::End, + State::BeforeCursor | State::End => {} + } + } + + /// Returns the collected valid words. + pub fn into_words(self) -> WordKinds { + self.collected + } +} + +/// Returns true if the cursor is at the given token. +/// +/// Cursor is considered to be at a token if it's just before +/// the token or in the middle of it. The only exception is when +/// the cursor is touching a word on the right side. In this +/// case, we want to count the cursor as being at that word. +/// +/// Touching the left side of a word: +/// operation Foo(x: |Int , y: String) : Unit {} +/// - at `Int` +/// +/// Touching the right side of a word: +/// `operation Foo(x: Int| , y: String) : Unit {}` +/// - at `Int` +/// +/// In the middle of a word: +/// `operation Foo(x: In|t , y: String) : Unit {}` +/// - at `Int` +/// +/// Touching the right side of a non-word: +/// `operation Foo(x:| Int , y: String) : Unit {}` +/// - at `Int` +/// +/// Between a word and a non-word: +/// `operation Foo(x:|Int , y: String) : Unit {}` +/// - at `Int` +/// +/// EOF: +/// `operation Foo(x: Int , y: String) : Unit {}|` +/// - at `EOF` +/// +fn cursor_at_token(cursor_offset: u32, next_token: Token, scanner_offset: u32) -> bool { + match next_token.kind { + // Order matters here as the cases overlap. + TokenKind::Ident + | TokenKind::Keyword(_) + | TokenKind::ClosedBinOp(ClosedBinOp::And | ClosedBinOp::Or) + | TokenKind::Eof => { + // next token is a word or eof, so count if cursor touches either side of the token + scanner_offset <= cursor_offset && cursor_offset <= next_token.span.hi + } + _ => { + // next token is not a word, so only count if cursor touches left side of token + scanner_offset <= cursor_offset && cursor_offset < next_token.span.hi + } + } +} + +fn eof(offset: u32) -> Token { + Token { + kind: TokenKind::Eof, + span: Span { + lo: offset, + hi: offset, + }, + } +} diff --git a/compiler/qsc_parse/src/completion/tests.rs b/compiler/qsc_parse/src/completion/tests.rs new file mode 100644 index 0000000000..b4a387e0f5 --- /dev/null +++ b/compiler/qsc_parse/src/completion/tests.rs @@ -0,0 +1,379 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::completion::possible_words_at_offset_in_source; +use expect_test::expect; +use qsc_data_structures::language_features::LanguageFeatures; + +fn get_source_and_cursor(input: &str) -> (String, u32) { + let mut cursor = -1; + let mut source = String::new(); + for c in input.chars() { + if c == '|' { + cursor = i32::try_from(source.len()).expect("input length should fit into u32"); + } else { + source.push(c); + } + } + let cursor = u32::try_from(cursor).expect("missing cursor marker in input"); + (source, cursor) +} + +fn check_valid_words(input: &str, expect: &expect_test::Expect) { + let (input, cursor) = get_source_and_cursor(input); + let w = possible_words_at_offset_in_source( + &input, + Some("test"), + LanguageFeatures::default(), + cursor, + ); + expect.assert_debug_eq(&w); +} + +fn check_valid_words_no_source_name(input: &str, expect: &expect_test::Expect) { + let (input, cursor) = get_source_and_cursor(input); + let w = possible_words_at_offset_in_source(&input, None, LanguageFeatures::default(), cursor); + expect.assert_debug_eq(&w); +} + +#[test] +fn end_of_keyword() { + check_valid_words( + "namespace Foo { open| ", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn after_open() { + check_valid_words( + "namespace Foo { open |", + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn begin_ident() { + check_valid_words( + "namespace Foo { open |X", + // right at the beginning of the namespace name. + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn middle_ident() { + check_valid_words( + "namespace Foo { open AB|CD", + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn end_ident() { + check_valid_words( + "namespace Foo { open ABCD| ", + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn middle() { + check_valid_words( + "namespace Foo { open AB|CD; open Foo; operation Main() : Unit {} }", + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn in_whitespace() { + check_valid_words( + "namespace MyQuantumApp { open Microsoft.Quantum.Diagnostics; | }", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn after_semicolon() { + check_valid_words( + "namespace MyQuantumApp { open Microsoft.Quantum.Diagnostics;| }", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn whitespace_at_end() { + check_valid_words( + "namespace Foo { open | ", + &expect![[r" + WordKinds( + PathNamespace, + ) + "]], + ); +} + +#[test] +fn path_part() { + check_valid_words( + "namespace Foo { operation Main() : Unit { Foo.| } }", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn namespace_part() { + check_valid_words( + "namespace Foo { open Foo.| }", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn type_position() { + check_valid_words( + "namespace Foo { operation Main() : Unit { let x:| ; } }", + &expect![[r" + WordKinds( + PathTy | TyParam | Underscore, + ) + "]], + ); +} + +#[test] +fn namespace_declaration() { + check_valid_words( + "namespace |", + &expect![[r" + WordKinds( + 0x0, + ) + "]], + ); +} + +#[test] +fn empty_source_no_source_name() { + check_valid_words_no_source_name( + "|", + &expect![[r" + WordKinds( + Namespace, + ) + "]], + ); +} + +#[test] +fn implicit_namespace_items_empty() { + check_valid_words( + "|", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Namespace | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn implicit_namespace_items_beginning() { + // Ideally, `Namespace` would not be in the list since we would gather from context + // that we're already in an implicit namespace. However, given the current design + // of the expected word collector, we don't have that context at the cursor location. + // (Note that the test cases is equivalent to the empty source file case, since we + // never look further than the cursor location). + check_valid_words( + "| operation Foo() : Unit {}", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Namespace | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn implicit_namespace_items_end() { + check_valid_words( + "operation Foo() : Unit {} |", + &expect![[r" + WordKinds( + Export | Function | Import | Internal | Newtype | Open | Operation | Struct, + ) + "]], + ); +} + +#[test] +fn import_missing_semi() { + check_valid_words( + "import | operation Foo()", + &expect![[r" + WordKinds( + PathImport, + ) + "]], + ); +} + +#[test] +fn import_with_semi() { + check_valid_words( + "import | ; operation Foo()", + &expect![[r" + WordKinds( + PathImport, + ) + "]], + ); +} + +#[test] +fn import_eof() { + check_valid_words( + "import |", + &expect![[r" + WordKinds( + PathImport, + ) + "]], + ); +} + +#[test] +fn import_end_of_namespace() { + check_valid_words( + "namespace Foo { import | }", + &expect![[r" + WordKinds( + PathImport, + ) + "]], + ); +} + +#[test] +fn export_part_missing_semi() { + check_valid_words( + "export Foo.| operation Foo()", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn export_part_with_semi() { + check_valid_words( + "export Foo.| ; operation Foo()", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn export_part_eof() { + check_valid_words( + "export Foo. |", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn export_part_end_of_namespace() { + check_valid_words( + "namespace Bar { export Foo.| }", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} + +#[test] +fn base_type() { + check_valid_words( + "newtype Foo = |", + &expect![[r" + WordKinds( + PathTy | TyParam | Underscore, + ) + "]], + ); +} + +#[test] +fn base_type_tuple() { + check_valid_words( + "newtype Foo = (|", + &expect![[r" + WordKinds( + PathTy | TyParam | Underscore, + ) + "]], + ); +} + +#[test] +fn keyword_after_incomplete_path() { + check_valid_words( + "import Foo.in|", + &expect![[r" + WordKinds( + PathSegment, + ) + "]], + ); +} diff --git a/compiler/qsc_parse/src/completion/word_kinds.rs b/compiler/qsc_parse/src/completion/word_kinds.rs new file mode 100644 index 0000000000..227b085e0f --- /dev/null +++ b/compiler/qsc_parse/src/completion/word_kinds.rs @@ -0,0 +1,222 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::keyword::Keyword; +use bitflags::bitflags; +use enum_iterator::all; + +bitflags! { + /// + /// Words can be of these kinds: + /// - Names + /// - Hardcoded words: + /// - Keywords + /// - Hardcoded identifiers + /// + /// Names are identifiers or paths that can be resolved to a definition + /// in the code, e.g. callable names, type names, namespaces. + /// + /// Keywords are known words that are not allowed as identifiers, e.g. `function`, `if`. + /// + /// Hardcoded identifiers are treated as identifiers by the parser, but the + /// possible names are hardcoded into the language, e.g. "EntryPoint", "Qubit". + /// + /// IF UPDATING: If new values are added before the keyword range, + /// [`KEYWORDS_START`] *must* be updated. + /// + #[repr(transparent)] + #[derive(Default, PartialEq, Debug, Clone, Copy)] + pub struct WordKinds: u128 { + + // + // Begin names. + // + + /// A path in an expression. Callables, UDT constructors, local variables. + const PathExpr = 1 << 0; + /// A path in a type. Builtins, type params (the leading '), UDTs, including structs. + const PathTy = 1 << 1; + /// A path to a struct UDT. + const PathStruct = 1 << 2; + /// A namespace. + const PathNamespace = 1 << 3; + /// A path to a name that can be imported. Items (callables, UDTs) and namespaces. + const PathImport = 1 << 4; + + /// A path segment that follows a `.` + /// A more specific name kind can be inferred from a recovered AST. + const PathSegment = 1 << 5; + /// A type parameter, without the leading `'`. + const TyParam = 1 << 6; + /// A field name that follows a `.` or `::` in a field access expression. + const Field = 1 << 7; + + // + // End names. + // + + // + // Begin hardcoded identifiers. + // + + /// An attribute, without the leading `@`. + const Attr = 1 << 8; + /// The word `Qubit`. + const Qubit = 1 << 9; + /// The word `size`. + const Size = 1 << 10; + + // + // End hardcoded identifiers. + // + + // + // Begin keywords. + // + + const Adj = keyword_bit(Keyword::Adj); + const Adjoint = keyword_bit(Keyword::Adjoint); + const AdjointUpper = keyword_bit(Keyword::AdjointUpper); + const And = keyword_bit(Keyword::And); + const Apply = keyword_bit(Keyword::Apply); + const As = keyword_bit(Keyword::As); + const Auto = keyword_bit(Keyword::Auto); + const Body = keyword_bit(Keyword::Body); + const Borrow = keyword_bit(Keyword::Borrow); + const Controlled = keyword_bit(Keyword::Controlled); + const ControlledUpper = keyword_bit(Keyword::ControlledUpper); + const Ctl = keyword_bit(Keyword::Ctl); + const Distribute = keyword_bit(Keyword::Distribute); + const Elif = keyword_bit(Keyword::Elif); + const Else = keyword_bit(Keyword::Else); + const Export = keyword_bit(Keyword::Export); + const Fail = keyword_bit(Keyword::Fail); + const False = keyword_bit(Keyword::False); + const Fixup = keyword_bit(Keyword::Fixup); + const For = keyword_bit(Keyword::For); + const Function = keyword_bit(Keyword::Function); + const If = keyword_bit(Keyword::If); + const Import = keyword_bit(Keyword::Import); + const In = keyword_bit(Keyword::In); + const Internal = keyword_bit(Keyword::Internal); + const Intrinsic = keyword_bit(Keyword::Intrinsic); + const Invert = keyword_bit(Keyword::Invert); + const Is = keyword_bit(Keyword::Is); + const Let = keyword_bit(Keyword::Let); + const Mutable = keyword_bit(Keyword::Mutable); + const Namespace = keyword_bit(Keyword::Namespace); + const Newtype = keyword_bit(Keyword::Newtype); + const New = keyword_bit(Keyword::New); + const Not = keyword_bit(Keyword::Not); + const One = keyword_bit(Keyword::One); + const Open = keyword_bit(Keyword::Open); + const Operation = keyword_bit(Keyword::Operation); + const Or = keyword_bit(Keyword::Or); + const PauliI = keyword_bit(Keyword::PauliI); + const PauliX = keyword_bit(Keyword::PauliX); + const PauliY = keyword_bit(Keyword::PauliY); + const PauliZ = keyword_bit(Keyword::PauliZ); + const Repeat = keyword_bit(Keyword::Repeat); + const Return = keyword_bit(Keyword::Return); + const Slf = keyword_bit(Keyword::Slf); + const Set = keyword_bit(Keyword::Set); + const Struct = keyword_bit(Keyword::Struct); + const True = keyword_bit(Keyword::True); + const Underscore = keyword_bit(Keyword::Underscore); + const Until = keyword_bit(Keyword::Until); + const Use = keyword_bit(Keyword::Use); + const While = keyword_bit(Keyword::While); + const Within = keyword_bit(Keyword::Within); + const Zero = keyword_bit(Keyword::Zero); + } +} + +const KEYWORDS_START: u8 = 11; +const fn keyword_bit(k: Keyword) -> u128 { + 1 << (k as u8 + KEYWORDS_START) +} + +impl From for WordKinds { + fn from(k: Keyword) -> Self { + Self::from_bits_truncate(keyword_bit(k)) + } +} + +impl WordKinds { + /// Returns only the name kinds that this prediction set contains. + pub fn iter_name_kinds(&self) -> impl Iterator + '_ { + self.iter().filter_map(|p| match p { + WordKinds::PathExpr => Some(NameKind::Path(PathKind::Expr)), + WordKinds::PathTy => Some(NameKind::Path(PathKind::Ty)), + WordKinds::PathStruct => Some(NameKind::Path(PathKind::Struct)), + WordKinds::PathNamespace => Some(NameKind::Path(PathKind::Namespace)), + WordKinds::PathImport => Some(NameKind::Path(PathKind::Import)), + WordKinds::PathSegment => Some(NameKind::PathSegment), + WordKinds::TyParam => Some(NameKind::TyParam), + WordKinds::Field => Some(NameKind::Field), + _ => None, + }) + } + + /// Returns only the hardcoded identifier kinds that this prediction set contains. + pub fn iter_hardcoded_ident_kinds(&self) -> impl Iterator + '_ { + self.iter().filter_map(|p| match p { + WordKinds::Attr => Some(HardcodedIdentKind::Attr), + WordKinds::Qubit => Some(HardcodedIdentKind::Qubit), + WordKinds::Size => Some(HardcodedIdentKind::Size), + _ => None, + }) + } + + /// Returns only the keywords that this prediction set contains. + pub fn iter_keywords(&self) -> impl Iterator + '_ { + all::().filter(|k| self.contains((*k).into())) + } +} + +/// A hardcoded identifier. +/// +/// Maps to a subset of values in [`Predictions`], but an enum +/// for friendly consumption. +pub enum HardcodedIdentKind { + /// An attribute, without the leading `@`. + Attr, + /// The word `Qubit`. + Qubit, + /// The word `size`. + Size, +} + +/// A name (see: [`Predictions`]) +/// +/// Maps to a subset of values in [`Predictions`], but an enum +/// for friendly consumption. +pub enum NameKind { + /// A path. + Path(PathKind), + /// A path segment that follows a `.` + /// A more specific name kind can only be inferred from a recovered AST. + PathSegment, + /// A type parameter, without the leading `'`. + TyParam, + /// A field name that follows a `.` or `::` in a field access expression. + Field, +} + +/// A path (see: [`Predictions`]) +/// +/// Maps to a subset of values in [`Predictions`], but an enum +/// for friendly consumption. +#[derive(Debug, Clone, Copy)] +pub enum PathKind { + /// A path in an expression. Callables, UDT constructors, local variables. + Expr, + /// A path in a type. Builtins, type params (the leading '), UDTs, including structs. + Ty, + /// A path to a struct UDT. + Struct, + /// A namespace. + Namespace, + /// A path to a name that can be imported. Items (callables, UDTs) and namespaces. + Import, +} diff --git a/compiler/qsc_parse/src/expr.rs b/compiler/qsc_parse/src/expr.rs index c4221fc847..9f37086244 100644 --- a/compiler/qsc_parse/src/expr.rs +++ b/compiler/qsc_parse/src/expr.rs @@ -8,12 +8,13 @@ mod tests; use crate::{ + completion::WordKinds, keyword::Keyword, lex::{ ClosedBinOp, Delim, InterpolatedEnding, InterpolatedStart, Radix, StringToken, Token, TokenKind, }, - prim::{ident, opt, pat, path, recovering_token, seq, shorten, single_ident_path, token}, + prim::{ident, opt, pat, recovering_path, recovering_token, seq, shorten, token}, scan::ParserContext, stmt, Error, ErrorKind, Result, }; @@ -21,7 +22,7 @@ use num_bigint::BigInt; use num_traits::Num; use qsc_ast::ast::{ self, BinOp, CallableKind, Expr, ExprKind, FieldAssign, Functor, Lit, NodeId, Pat, PatKind, - Path, Pauli, StringComponent, TernOp, UnOp, + PathKind, Pauli, StringComponent, TernOp, UnOp, }; use qsc_data_structures::span::Span; use std::{result, str::FromStr}; @@ -98,6 +99,8 @@ pub(super) fn is_stmt_final(kind: &ExprKind) -> bool { fn expr_op(s: &mut ParserContext, context: OpContext) -> Result> { let lo = s.peek().span.lo; + + s.expect(WordKinds::AdjointUpper | WordKinds::ControlledUpper | WordKinds::Not); let mut lhs = if let Some(op) = prefix_op(op_name(s)) { s.advance(); let rhs = expr_op(s, OpContext::Precedence(op.precedence))?; @@ -116,6 +119,7 @@ fn expr_op(s: &mut ParserContext, context: OpContext) -> Result> { OpContext::Stmt => 0, }; + s.expect(WordKinds::And | WordKinds::Or); while let Some(op) = mixfix_op(op_name(s)) { if op.precedence < min_precedence { break; @@ -214,14 +218,14 @@ fn expr_base(s: &mut ParserContext) -> Result> { let inner = stmt::parse_block(s)?; Ok(Box::new(ExprKind::Conjugate(outer, inner))) } else if token(s, TokenKind::Keyword(Keyword::New)).is_ok() { - parse_struct(s) + recovering_struct(s) } else if let Some(a) = opt(s, expr_array)? { Ok(a) } else if let Some(b) = opt(s, stmt::parse_block)? { Ok(Box::new(ExprKind::Block(b))) } else if let Some(l) = lit(s)? { Ok(Box::new(ExprKind::Lit(Box::new(l)))) - } else if let Some(p) = opt(s, single_ident_path)? { + } else if let Some(p) = opt(s, |s| recovering_path(s, WordKinds::PathExpr))? { Ok(Box::new(ExprKind::Path(p))) } else { Err(Error::new(ErrorKind::Rule( @@ -238,9 +242,27 @@ fn expr_base(s: &mut ParserContext) -> Result> { })) } -fn parse_struct(s: &mut ParserContext) -> Result> { - let name = path(s)?; - token(s, TokenKind::Open(Delim::Brace))?; +/// A struct expression excluding the `new` keyword, +/// e.g. `A { a = b, c = d }` +fn recovering_struct(s: &mut ParserContext) -> Result> { + let name = recovering_path(s, WordKinds::PathStruct)?; + + if let Err(e) = token(s, TokenKind::Open(Delim::Brace)) { + s.push_error(e); + return Ok(Box::new(ExprKind::Struct(name, None, Box::new([])))); + } + + let (copy, fields) = struct_fields(s)?; + recovering_token(s, TokenKind::Close(Delim::Brace)); + Ok(Box::new(ExprKind::Struct(name, copy, fields))) +} + +/// A sequence of field assignments and an optional base expression, +/// e.g. `...a, b = c, d = e` +#[allow(clippy::type_complexity)] +fn struct_fields( + s: &mut ParserContext<'_>, +) -> Result<(Option>, Box<[Box]>)> { let copy: Option> = opt(s, |s| { token(s, TokenKind::DotDotDot)?; expr(s) @@ -249,13 +271,7 @@ fn parse_struct(s: &mut ParserContext) -> Result> { if copy.is_none() || copy.is_some() && token(s, TokenKind::Comma).is_ok() { (fields, _) = seq(s, parse_field_assign)?; } - recovering_token(s, TokenKind::Close(Delim::Brace)); - - Ok(Box::new(ExprKind::Struct( - name, - copy, - fields.into_boxed_slice(), - ))) + Ok((copy, fields.into_boxed_slice())) } fn parse_field_assign(s: &mut ParserContext) -> Result> { @@ -333,6 +349,7 @@ fn expr_array_core(s: &mut ParserContext) -> Result> { return Ok(Box::new(ExprKind::Array(vec![first].into_boxed_slice()))); } + s.expect(WordKinds::Size); let second = expr(s)?; if is_ident("size", &second.kind) && token(s, TokenKind::Eq).is_ok() { let size = expr(s)?; @@ -347,7 +364,7 @@ fn expr_array_core(s: &mut ParserContext) -> Result> { } fn is_ident(name: &str, kind: &ExprKind) -> bool { - matches!(kind, ExprKind::Path(path) if path.segments.is_none() && path.name.name.as_ref() == name) + matches!(kind, ExprKind::Path(PathKind::Ok(path)) if path.segments.is_none() && path.name.name.as_ref() == name) } fn expr_range_prefix(s: &mut ParserContext) -> Result> { @@ -412,6 +429,18 @@ fn expr_interpolate(s: &mut ParserContext) -> Result> { fn lit(s: &mut ParserContext) -> Result> { let lexeme = s.read(); + + s.expect( + WordKinds::True + | WordKinds::False + | WordKinds::Zero + | WordKinds::One + | WordKinds::PauliX + | WordKinds::PauliY + | WordKinds::PauliZ + | WordKinds::PauliI, + ); + let token = s.peek(); match lit_token(lexeme, token) { Ok(Some(lit)) => { @@ -645,14 +674,10 @@ fn mixfix_op(name: OpName) -> Option { kind: OpKind::Postfix(UnOp::Unwrap), precedence: 15, }), - OpName::Token(TokenKind::ColonColon) => Some(MixfixOp { + OpName::Token(TokenKind::ColonColon | TokenKind::Dot) => Some(MixfixOp { kind: OpKind::Rich(field_op), precedence: 15, }), - OpName::Token(TokenKind::Dot) => Some(MixfixOp { - kind: OpKind::Rich(path_field_op), - precedence: 15, - }), OpName::Token(TokenKind::Open(Delim::Bracket)) => Some(MixfixOp { kind: OpKind::Rich(index_op), precedence: 15, @@ -686,36 +711,10 @@ fn lambda_op(s: &mut ParserContext, input: Expr, kind: CallableKind) -> Result) -> Result> { + s.expect(WordKinds::Field); Ok(Box::new(ExprKind::Field(lhs, ident(s)?))) } -fn path_field_op(s: &mut ParserContext, lhs: Box) -> Result> { - if let ExprKind::Path(leading_path) = *lhs.kind { - let rest = path(s)?; - let name = rest.name; - let span = Span { - lo: lhs.span.lo, - hi: rest.span.hi, - }; - let parts = { - let mut v = vec![*leading_path.name]; - if let Some(parts) = rest.segments { - v.append(&mut parts.into()); - } - v.into() - }; - let path = Path { - id: NodeId::default(), - span, - segments: Some(parts), - name, - }; - Ok(Box::new(ExprKind::Path(Box::new(path)))) - } else { - Ok(Box::new(ExprKind::Field(lhs, ident(s)?))) - } -} - fn index_op(s: &mut ParserContext, lhs: Box) -> Result> { let index = expr(s)?; token(s, TokenKind::Close(Delim::Bracket))?; @@ -762,7 +761,9 @@ fn next_precedence(precedence: u8, assoc: Assoc) -> u8 { fn expr_as_pat(expr: Expr) -> Result> { let kind = Box::new(match *expr.kind { - ExprKind::Path(path) if path.segments.is_none() => Ok(PatKind::Bind(path.name, None)), + ExprKind::Path(PathKind::Ok(path)) if path.segments.is_none() => { + Ok(PatKind::Bind(path.name, None)) + } ExprKind::Hole => Ok(PatKind::Discard(None)), ExprKind::Range(None, None, None) => Ok(PatKind::Elided), ExprKind::Paren(expr) => Ok(PatKind::Paren(expr_as_pat(*expr)?)), diff --git a/compiler/qsc_parse/src/item.rs b/compiler/qsc_parse/src/item.rs index a4e75711d7..0ebe54865b 100644 --- a/compiler/qsc_parse/src/item.rs +++ b/compiler/qsc_parse/src/item.rs @@ -7,6 +7,8 @@ #[cfg(test)] mod tests; +use std::rc::Rc; + use super::{ expr::expr, keyword::Keyword, @@ -17,10 +19,12 @@ use super::{ Error, Result, }; -use crate::lex::ClosedBinOp; use crate::{ - lex::{Delim, TokenKind}, - prim::{barrier, path, recovering, recovering_token, shorten}, + completion::WordKinds, + lex::{ClosedBinOp, Delim, TokenKind}, + prim::{ + barrier, path, recovering, recovering_path, recovering_semi, recovering_token, shorten, + }, stmt::check_semis, ty::array_or_arrow, ErrorKind, @@ -28,8 +32,8 @@ use crate::{ use qsc_ast::ast::{ Attr, Block, CallableBody, CallableDecl, CallableKind, FieldDef, Ident, Idents, ImportOrExportDecl, ImportOrExportItem, Item, ItemKind, Namespace, NodeId, Pat, PatKind, Path, - Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef, TyDefKind, - TyKind, + PathKind, Spec, SpecBody, SpecDecl, SpecGen, StmtKind, StructDecl, TopLevelNode, Ty, TyDef, + TyDefKind, TyKind, }; use qsc_data_structures::language_features::LanguageFeatures; use qsc_data_structures::span::Span; @@ -85,6 +89,8 @@ fn parse_many(s: &mut ParserContext) -> Result>> { TokenKind::Keyword(Keyword::Struct), TokenKind::Keyword(Keyword::Operation), TokenKind::Keyword(Keyword::Function), + TokenKind::Keyword(Keyword::Import), + TokenKind::Keyword(Keyword::Export), ]; const RECOVERY_TOKENS: &[TokenKind] = &[TokenKind::Semi, TokenKind::Close(Delim::Brace)]; @@ -139,25 +145,42 @@ fn parse_top_level_node(s: &mut ParserContext) -> Result { } } -pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Result { - if s.peek().kind == TokenKind::Eof { - return Ok(Namespace { - id: NodeId::default(), - span: s.span(0), - doc: "".into(), - name: source_name_to_namespace_name(source_name, s.span(0))?, - items: Vec::new().into_boxed_slice(), - }); +pub fn parse_namespaces_or_implicit( + s: &mut ParserContext<'_>, + source_name: Option<&str>, +) -> Result> { + let doc = parse_doc(s); + let doc = Rc::from(doc.unwrap_or_default()); + s.expect(WordKinds::Namespace); + #[allow(clippy::unnecessary_unwrap)] + if source_name.is_some() && s.peek().kind != TokenKind::Keyword(Keyword::Namespace) { + let mut ns = parse_implicit_namespace( + source_name.expect("invariant checked above via `.is_some()`"), + s, + ) + .map(|x| vec![x])?; + if let Some(ref mut ns) = ns.get_mut(0) { + if let Some(x) = ns.items.get_mut(0) { + x.span.lo = 0; + x.doc = doc; + }; + } + Ok(ns) + } else { + let mut ns = parse_namespaces(s)?; + if let Some(x) = ns.get_mut(0) { + x.span.lo = 0; + x.doc = doc; + }; + Ok(ns) } +} + +pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Result { let lo = s.peek().span.lo; let items = parse_namespace_block_contents(s)?; recovering_token(s, TokenKind::Eof); - if items.is_empty() || s.peek().kind != TokenKind::Eof { - return Err(Error::new(ErrorKind::ExpectedItem( - s.peek().kind, - s.span(lo), - ))); - } + let span = s.span(lo); let namespace_name = source_name_to_namespace_name(source_name, span)?; @@ -173,7 +196,7 @@ pub fn parse_implicit_namespace(source_name: &str, s: &mut ParserContext) -> Res /// Given a file name, convert it to a namespace name. /// For example, `foo/bar.qs` becomes `foo.bar`. /// Invalid or disallowed characters are cleaned up in a best effort manner. -fn source_name_to_namespace_name(raw: &str, span: Span) -> Result { +fn source_name_to_namespace_name(raw: &str, span: Span) -> Result> { let path = std::path::Path::new(raw); let mut namespace = Vec::new(); for component in path.components() { @@ -193,7 +216,7 @@ fn source_name_to_namespace_name(raw: &str, span: Span) -> Result { let mut ident = validate_namespace_name(span, name)?; ident.span = span; - namespace.push(ident); + namespace.push(*ident); } _ => { return Err(Error::new(ErrorKind::InvalidFileName( @@ -217,7 +240,7 @@ fn clean_namespace_name(name: &str) -> String { } /// Validates that a string could be a valid namespace name component -fn validate_namespace_name(error_span: Span, name: &str) -> Result { +fn validate_namespace_name(error_span: Span, name: &str) -> Result> { let name = clean_namespace_name(name); let mut s = ParserContext::new(&name, LanguageFeatures::default()); // if it could be a valid identifier, then it is a valid namespace name @@ -231,14 +254,19 @@ fn validate_namespace_name(error_span: Span, name: &str) -> Result { name.to_string(), ))); } - Ok(*ident) + Ok(ident) } fn parse_namespace(s: &mut ParserContext) -> Result { let lo = s.peek().span.lo; let doc = parse_doc(s).unwrap_or_default(); token(s, TokenKind::Keyword(Keyword::Namespace))?; - let name = path(s)?; + + let mut name = vec![*ident(s)?]; + while token(s, TokenKind::Dot).is_ok() { + name.push(*ident(s)?); + } + token(s, TokenKind::Open(Delim::Brace))?; let items = parse_namespace_block_contents(s)?; recovering_token(s, TokenKind::Close(Delim::Brace)); @@ -246,7 +274,7 @@ fn parse_namespace(s: &mut ParserContext) -> Result { id: NodeId::default(), span: s.span(lo), doc: doc.into(), - name: (*name).into(), + name: name.into_boxed_slice(), items: items.into_boxed_slice(), }) } @@ -290,6 +318,7 @@ pub(crate) fn parse_doc(s: &mut ParserContext) -> Option { fn parse_attr(s: &mut ParserContext) -> Result> { let lo = s.peek().span.lo; token(s, TokenKind::At)?; + s.expect(WordKinds::Attr); let name = ident(s)?; let arg = expr(s)?; Ok(Box::new(Attr { @@ -304,12 +333,12 @@ fn parse_visibility(s: &mut ParserContext) -> Result<()> { token(s, TokenKind::Keyword(Keyword::Internal))?; Ok(()) } + fn parse_open(s: &mut ParserContext) -> Result> { token(s, TokenKind::Keyword(Keyword::Open))?; - let mut name = vec![*(ident(s)?)]; - while let Ok(_dot) = token(s, TokenKind::Dot) { - name.push(*(ident(s)?)); - } + + let path = recovering_path(s, WordKinds::PathNamespace)?; + let alias = if token(s, TokenKind::Keyword(Keyword::As)).is_ok() { Some(ident(s)?) } else { @@ -322,8 +351,8 @@ fn parse_open(s: &mut ParserContext) -> Result> { return Err(Error::new(ErrorKind::DotIdentAlias(s.peek().span))); } - token(s, TokenKind::Semi)?; - Ok(Box::new(ItemKind::Open(name.into(), alias))) + recovering_semi(s); + Ok(Box::new(ItemKind::Open(path, alias))) } fn parse_newtype(s: &mut ParserContext) -> Result> { @@ -340,7 +369,7 @@ fn parse_newtype(s: &mut ParserContext) -> Result> { kind: Box::new(TyDefKind::Field(None, Box::new(ty))), }); } - token(s, TokenKind::Semi)?; + recovering_semi(s); Ok(Box::new(ItemKind::Ty(name, def))) } @@ -417,13 +446,14 @@ fn parse_ty_def(s: &mut ParserContext) -> Result> { } fn ty_as_ident(ty: Ty) -> Result> { - let TyKind::Path(path) = *ty.kind else { + let TyKind::Path(PathKind::Ok(path)) = *ty.kind else { return Err(Error::new(ErrorKind::Convert( "identifier", "type", ty.span, ))); }; + if let Path { segments: None, name, @@ -590,6 +620,7 @@ pub(super) fn check_input_parens(inputs: &Pat) -> Result<()> { fn parse_import_or_export(s: &mut ParserContext) -> Result { let lo = s.peek().span.lo; let _doc = parse_doc(s); + s.expect(WordKinds::Import | WordKinds::Export); let is_export = match s.peek().kind { TokenKind::Keyword(Keyword::Export) => true, TokenKind::Keyword(Keyword::Import) => false, @@ -603,7 +634,7 @@ fn parse_import_or_export(s: &mut ParserContext) -> Result { }; s.advance(); let (items, _) = seq(s, parse_import_or_export_item)?; - token(s, TokenKind::Semi)?; + recovering_semi(s); Ok(ImportOrExportDecl::new( s.span(lo), items.into_boxed_slice(), @@ -611,16 +642,44 @@ fn parse_import_or_export(s: &mut ParserContext) -> Result { )) } -fn parse_import_or_export_item(s: &mut ParserContext) -> Result { - let mut is_glob = false; - let mut parts = vec![*ident(s)?]; - while token(s, TokenKind::Dot).is_ok() { - if token(s, TokenKind::ClosedBinOp(ClosedBinOp::Star)).is_ok() { - is_glob = true; - break; +/// A path with an optional glob indicator at the end, e.g. `Foo.Bar.*` +fn path_import(s: &mut ParserContext) -> Result<(PathKind, bool)> { + match path(s, WordKinds::PathImport) { + Ok(path) => Ok((PathKind::Ok(path), false)), + Err((error, Some(incomplete_path))) => { + if !incomplete_path.keyword + && token(s, TokenKind::ClosedBinOp(ClosedBinOp::Star)).is_ok() + { + let (name, namespace) = incomplete_path + .segments + .split_last() + .expect("path should have at least one part"); + + Ok(( + PathKind::Ok(Box::new(Path { + id: NodeId::default(), + span: incomplete_path.segments.full_span(), + segments: if namespace.is_empty() { + None + } else { + Some(namespace.into()) + }, + name: Box::new(name.clone()), + })), + true, + )) + } else { + s.push_error(error); + Ok((PathKind::Err(Some(incomplete_path)), false)) + } } - parts.push(*ident(s)?); + Err((error, None)) => Err(error), } +} + +fn parse_import_or_export_item(s: &mut ParserContext) -> Result { + let lo = s.peek().span.lo; + let (path, is_glob) = path_import(s)?; let alias = if token(s, TokenKind::Keyword(Keyword::As)).is_ok() { Some(*(ident(s)?)) @@ -629,7 +688,8 @@ fn parse_import_or_export_item(s: &mut ParserContext) -> Result Result, crate::Error> { super::parse_namespaces(s) @@ -48,11 +48,10 @@ fn adjoint_invert() { #[test] fn file_name_to_namespace_name() { let raw = "foo/bar.qs"; - let error_span = Span::default(); check( - |_| source_name_to_namespace_name(raw, error_span), + |s| parse_implicit_namespace(raw, s), "", - &expect![[r#"[Ident _id_ [0-0] "foo", Ident _id_ [0-0] "bar"]"#]], + &expect![[r#"Namespace _id_ [0-0] ([Ident _id_ [0-0] "foo", Ident _id_ [0-0] "bar"]):"#]], ); } @@ -123,7 +122,10 @@ fn open_no_alias() { "open Foo.Bar.Baz;", &expect![[r#" Item _id_ [0-17]: - Open ([Ident _id_ [5-8] "Foo", Ident _id_ [9-12] "Bar", Ident _id_ [13-16] "Baz"])"#]], + Open (Path _id_ [5-16]: + Ident _id_ [5-8] "Foo" + Ident _id_ [9-12] "Bar" + Ident _id_ [13-16] "Baz")"#]], ); } @@ -134,7 +136,10 @@ fn open_alias() { "open Foo.Bar.Baz as Baz;", &expect![[r#" Item _id_ [0-24]: - Open ([Ident _id_ [5-8] "Foo", Ident _id_ [9-12] "Bar", Ident _id_ [13-16] "Baz"]) (Ident _id_ [20-23] "Baz")"#]], + Open (Path _id_ [5-16]: + Ident _id_ [5-8] "Foo" + Ident _id_ [9-12] "Bar" + Ident _id_ [13-16] "Baz") (Ident _id_ [20-23] "Baz")"#]], ); } @@ -1046,7 +1051,7 @@ fn open_attr() { Item _id_ [0-16]: Attr _id_ [0-6] (Ident _id_ [1-4] "Foo"): Expr _id_ [4-6]: Unit - Open (Ident _id_ [12-15] "Bar")"#]], + Open (Path _id_ [12-15] (Ident _id_ [12-15] "Bar"))"#]], ); } @@ -1283,9 +1288,9 @@ fn two_open_items() { &expect![[r#" Namespace _id_ [0-31] (Ident _id_ [10-11] "A"): Item _id_ [14-21]: - Open (Ident _id_ [19-20] "B") + Open (Path _id_ [19-20] (Ident _id_ [19-20] "B")) Item _id_ [22-29]: - Open (Ident _id_ [27-28] "C")"#]], + Open (Path _id_ [27-28] (Ident _id_ [27-28] "C"))"#]], ); } @@ -1977,19 +1982,22 @@ fn invalid_glob_syntax_extra_asterisk() { parse_import_or_export, "import Foo.**;", &expect![[r#" - Error( - Token( - Semi, - ClosedBinOp( - Star, + ImportOrExportDecl [0-12]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo").*] + + [ + Error( + Token( + Semi, + ClosedBinOp( + Star, + ), + Span { + lo: 12, + hi: 13, + }, ), - Span { - lo: 12, - hi: 13, - }, ), - ) - "#]], + ]"#]], ); } @@ -1999,19 +2007,24 @@ fn invalid_glob_syntax_missing_dot() { parse_import_or_export, "import Foo.Bar**;", &expect![[r#" - Error( - Token( - Semi, - ClosedBinOp( - Star, + ImportOrExportDecl [0-14]: [Path _id_ [7-14]: + Ident _id_ [7-10] "Foo" + Ident _id_ [11-14] "Bar"] + + [ + Error( + Token( + Semi, + ClosedBinOp( + Star, + ), + Span { + lo: 14, + hi: 15, + }, ), - Span { - lo: 14, - hi: 15, - }, ), - ) - "#]], + ]"#]], ); } @@ -2021,17 +2034,22 @@ fn invalid_glob_syntax_multiple_asterisks_in_path() { parse_import_or_export, "import Foo.Bar.*.*;", &expect![[r#" - Error( - Token( - Semi, - Dot, - Span { - lo: 16, - hi: 17, - }, + ImportOrExportDecl [0-16]: [Path _id_ [7-14]: + Ident _id_ [7-10] "Foo" + Ident _id_ [11-14] "Bar".*] + + [ + Error( + Token( + Semi, + Dot, + Span { + lo: 16, + hi: 17, + }, + ), ), - ) - "#]], + ]"#]], ); } @@ -2041,17 +2059,58 @@ fn invalid_glob_syntax_with_following_ident() { parse_import_or_export, "import Foo.*.Bar;", &expect![[r#" - Error( - Token( - Semi, - Dot, - Span { - lo: 12, - hi: 13, - }, + ImportOrExportDecl [0-12]: [Path _id_ [7-10] (Ident _id_ [7-10] "Foo").*] + + [ + Error( + Token( + Semi, + Dot, + Span { + lo: 12, + hi: 13, + }, + ), ), - ) - "#]], + ]"#]], + ); +} + +#[test] +fn invalid_glob_syntax_follows_keyword() { + check( + parse_import_or_export, + "import Foo.in*;", + &expect![[r#" + ImportOrExportDecl [0-13]: [Err IncompletePath [7-13]: + Ident _id_ [7-10] "Foo"] + + [ + Error( + Rule( + "identifier", + Keyword( + In, + ), + Span { + lo: 11, + hi: 13, + }, + ), + ), + Error( + Token( + Semi, + ClosedBinOp( + Star, + ), + Span { + lo: 13, + hi: 14, + }, + ), + ), + ]"#]], ); } @@ -2061,18 +2120,133 @@ fn disallow_top_level_recursive_glob() { parse_import_or_export, "import *;", &expect![[r#" - Error( - Token( - Semi, - ClosedBinOp( - Star, + ImportOrExportDecl [0-6]: [] + + [ + Error( + Token( + Semi, + ClosedBinOp( + Star, + ), + Span { + lo: 7, + hi: 8, + }, ), - Span { - lo: 7, - hi: 8, - }, ), - ) - "#]], + ]"#]], + ); +} + +#[test] +fn incomplete_open_with_semi() { + check( + parse_open, + "open Foo. ;", + &expect![[r#" + Open (Err IncompletePath [5-11]: + Ident _id_ [5-8] "Foo") + + [ + Error( + Rule( + "identifier", + Semi, + Span { + lo: 11, + hi: 12, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn incomplete_open_no_semi() { + check( + parse_open, + "open Foo. ", + &expect![[r#" + Open (Err IncompletePath [5-10]: + Ident _id_ [5-8] "Foo") + + [ + Error( + Rule( + "identifier", + Eof, + Span { + lo: 10, + hi: 10, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn missing_semi_between_items() { + check_vec( + parse_namespaces, + "namespace Foo { open Foo open Bar. open Baz }", + &expect![[r#" + Namespace _id_ [0-45] (Ident _id_ [10-13] "Foo"): + Item _id_ [16-24]: + Open (Path _id_ [21-24] (Ident _id_ [21-24] "Foo")) + Item _id_ [25-39]: + Open (Err IncompletePath [30-39]: + Ident _id_ [30-33] "Bar") + + [ + Error( + Token( + Semi, + Keyword( + Open, + ), + Span { + lo: 25, + hi: 29, + }, + ), + ), + Error( + Rule( + "identifier", + Keyword( + Open, + ), + Span { + lo: 35, + hi: 39, + }, + ), + ), + Error( + Token( + Semi, + Ident, + Span { + lo: 40, + hi: 43, + }, + ), + ), + Error( + Token( + Close( + Brace, + ), + Ident, + Span { + lo: 40, + hi: 43, + }, + ), + ), + ]"#]], ); } diff --git a/compiler/qsc_parse/src/lib.rs b/compiler/qsc_parse/src/lib.rs index 5ab2441498..4651a5ee16 100644 --- a/compiler/qsc_parse/src/lib.rs +++ b/compiler/qsc_parse/src/lib.rs @@ -5,6 +5,7 @@ //! The parser produces a tree with placeholder node identifiers that are expected to be replaced with //! unique identifiers by a later stage. +pub mod completion; mod expr; mod item; pub mod keyword; @@ -16,14 +17,11 @@ mod stmt; mod tests; mod ty; -use crate::item::parse_doc; -use crate::keyword::Keyword; use lex::TokenKind; use miette::Diagnostic; use qsc_ast::ast::{Expr, Namespace, TopLevelNode}; use qsc_data_structures::{language_features::LanguageFeatures, span::Span}; use scan::ParserContext; -use std::rc::Rc; use std::result; use thiserror::Error; @@ -185,32 +183,7 @@ pub fn namespaces( language_features: LanguageFeatures, ) -> (Vec, Vec) { let mut scanner = ParserContext::new(input, language_features); - let doc = parse_doc(&mut scanner); - let doc = Rc::from(doc.unwrap_or_default()); - #[allow(clippy::unnecessary_unwrap)] - let result: Result<_> = (|| { - if source_name.is_some() && scanner.peek().kind != TokenKind::Keyword(Keyword::Namespace) { - let mut ns = item::parse_implicit_namespace( - source_name.expect("invariant checked above via `.is_some()`"), - &mut scanner, - ) - .map(|x| vec![x])?; - if let Some(ref mut ns) = ns.get_mut(0) { - if let Some(x) = ns.items.get_mut(0) { - x.span.lo = 0; - x.doc = doc; - }; - } - Ok(ns) - } else { - let mut ns = item::parse_namespaces(&mut scanner)?; - if let Some(x) = ns.get_mut(0) { - x.span.lo = 0; - x.doc = doc; - }; - Ok(ns) - } - })(); + let result = item::parse_namespaces_or_implicit(&mut scanner, source_name); match result { Ok(namespaces) => (namespaces, scanner.into_errors()), diff --git a/compiler/qsc_parse/src/prim.rs b/compiler/qsc_parse/src/prim.rs index 6b0926e48d..89ca09a8d2 100644 --- a/compiler/qsc_parse/src/prim.rs +++ b/compiler/qsc_parse/src/prim.rs @@ -6,11 +6,12 @@ mod tests; use super::{keyword::Keyword, scan::ParserContext, ty::ty, Error, Parser, Result}; use crate::{ + completion::WordKinds, item::throw_away_doc, lex::{Delim, TokenKind}, ErrorKind, }; -use qsc_ast::ast::{Ident, NodeId, Pat, PatKind, Path}; +use qsc_ast::ast::{Ident, IncompletePath, NodeId, Pat, PatKind, Path, PathKind}; use qsc_data_structures::span::{Span, WithSpan}; #[derive(Clone, Copy, Debug, Eq, PartialEq)] @@ -35,6 +36,10 @@ impl FinalSep { } pub(super) fn token(s: &mut ParserContext, t: TokenKind) -> Result<()> { + if let TokenKind::Keyword(k) = t { + s.expect(k.into()); + } + if s.peek().kind == t { s.advance(); Ok(()) @@ -48,6 +53,7 @@ pub(super) fn token(s: &mut ParserContext, t: TokenKind) -> Result<()> { } pub(super) fn apos_ident(s: &mut ParserContext) -> Result> { + s.expect(WordKinds::TyParam); let peek = s.peek(); if peek.kind == TokenKind::AposIdent { let name = s.read().into(); @@ -85,52 +91,79 @@ pub(super) fn ident(s: &mut ParserContext) -> Result> { } } -pub fn single_ident_path(s: &mut ParserContext) -> Result> { - let lo = s.peek().span.lo; - let name = ident(s)?; - Ok(Box::new(Path { - id: NodeId::default(), - span: s.span(lo), - segments: None, - name, - })) -} - /// A `path` is a dot-separated list of idents like "Foo.Bar.Baz" -/// this can be either a namespace name (in an open statement or namespace declaration) or -/// it can be a direct reference to something in a namespace, like `Microsoft.Quantum.Diagnostics.DumpMachine()` -pub(super) fn path(s: &mut ParserContext) -> Result> { +/// this can be a namespace name (in an open statement or namespace declaration), +/// a reference to an item, like `Microsoft.Quantum.Diagnostics.DumpMachine`, +/// or a field access. +/// +/// Path parser. If parsing fails, also returns any valid segments +/// that were parsed up to the final `.` token. +pub(super) fn path( + s: &mut ParserContext, + kind: WordKinds, +) -> std::result::Result, (Error, Option>)> { + s.expect(kind); + let lo = s.peek().span.lo; - let mut parts = vec![ident(s)?]; + let i = ident(s).map_err(|e| (e, None))?; + + let mut parts = vec![*i]; while token(s, TokenKind::Dot).is_ok() { - parts.push(ident(s)?); + s.expect(WordKinds::PathSegment); + match ident(s) { + Ok(ident) => parts.push(*ident), + Err(error) => { + let _ = s.skip_trivia(); + let peek = s.peek(); + let keyword = matches!(peek.kind, TokenKind::Keyword(_)); + if keyword { + // Consume any keyword that comes after the final + // dot, assuming it was intended to be part of the path. + s.advance(); + } + + return Err(( + error, + Some(Box::new(IncompletePath { + span: s.span(lo), + segments: parts.into(), + keyword, + })), + )); + } + } } let name = parts.pop().expect("path should have at least one part"); let namespace = if parts.is_empty() { None } else { - Some( - parts - .iter() - .map(|part| Ident { - id: NodeId::default(), - span: part.span, - name: part.name.clone(), - }) - .collect::>() - .into(), - ) + Some(parts.into()) }; Ok(Box::new(Path { id: NodeId::default(), span: s.span(lo), segments: namespace, - name, + name: name.into(), })) } +/// Recovering [`Path`] parser. Parsing only fails if no segments +/// were successfully parsed. If any segments were successfully parsed, +/// returns a [`PathKind::Err`] containing the segments that were +/// successfully parsed up to the final `.` token. +pub(super) fn recovering_path(s: &mut ParserContext, kind: WordKinds) -> Result { + match path(s, kind) { + Ok(path) => Ok(PathKind::Ok(path)), + Err((error, Some(incomplete_path))) => { + s.push_error(error); + Ok(PathKind::Err(Some(incomplete_path))) + } + Err((error, None)) => Err(error), + } +} + pub(super) fn pat(s: &mut ParserContext) -> Result> { throw_away_doc(s); let lo = s.peek().span.lo; @@ -184,6 +217,7 @@ pub(super) fn many(s: &mut ParserContext, mut p: impl Parser) -> Result(s: &mut ParserContext, mut p: impl Parser) -> Result<(Vec, FinalSep)> diff --git a/compiler/qsc_parse/src/prim/tests.rs b/compiler/qsc_parse/src/prim/tests.rs index d11d034ccf..504007f599 100644 --- a/compiler/qsc_parse/src/prim/tests.rs +++ b/compiler/qsc_parse/src/prim/tests.rs @@ -1,8 +1,10 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use super::{ident, opt, pat, path, seq}; +use super::{ident, opt, pat, seq}; use crate::{ + completion::WordKinds, + expr::expr, keyword::Keyword, lex::{ClosedBinOp, TokenKind}, scan::ParserContext, @@ -10,8 +12,13 @@ use crate::{ Error, ErrorKind, }; use expect_test::expect; +use qsc_ast::ast::PathKind; use qsc_data_structures::{language_features::LanguageFeatures, span::Span}; +fn path(s: &mut ParserContext) -> Result { + super::recovering_path(s, WordKinds::empty()) +} + #[test] fn ident_basic() { check(ident, "foo", &expect![[r#"Ident _id_ [0-3] "foo""#]]); @@ -117,17 +124,49 @@ fn path_trailing_dot() { path, "Foo.Bar.", &expect![[r#" - Error( - Rule( - "identifier", - Eof, - Span { - lo: 8, - hi: 8, - }, + Err IncompletePath [0-8]: + Ident _id_ [0-3] "Foo" + Ident _id_ [4-7] "Bar" + + [ + Error( + Rule( + "identifier", + Eof, + Span { + lo: 8, + hi: 8, + }, + ), ), - ) - "#]], + ]"#]], + ); +} + +#[test] +fn path_followed_by_keyword() { + check( + path, + "Foo.Bar.in", + &expect![[r#" + Err IncompletePath [0-10]: + Ident _id_ [0-3] "Foo" + Ident _id_ [4-7] "Bar" + + [ + Error( + Rule( + "identifier", + Keyword( + In, + ), + Span { + lo: 8, + hi: 10, + }, + ), + ), + ]"#]], ); } @@ -285,16 +324,8 @@ fn opt_fail_consume() { |s| opt(s, path), "Foo.#", &expect![[r#" - Error( - Rule( - "identifier", - Eof, - Span { - lo: 5, - hi: 5, - }, - ), - ) + Err IncompletePath [0-5]: + Ident _id_ [0-3] "Foo" [ Error( @@ -308,6 +339,16 @@ fn opt_fail_consume() { ), ), ), + Error( + Rule( + "identifier", + Eof, + Span { + lo: 5, + hi: 5, + }, + ), + ), ]"#]], ); } @@ -360,12 +401,14 @@ fn seq_fail_no_consume() { #[test] fn seq_fail_consume() { check_seq( - |s| seq(s, path), - "foo, bar.", - &expect![[r#" + |s| seq(s, expr), + "foo, bar(", + &expect![[r" Error( - Rule( - "identifier", + Token( + Close( + Paren, + ), Eof, Span { lo: 9, @@ -373,6 +416,6 @@ fn seq_fail_consume() { }, ), ) - "#]], + "]], ); } diff --git a/compiler/qsc_parse/src/scan.rs b/compiler/qsc_parse/src/scan.rs index 628a35a6de..e19e8772d7 100644 --- a/compiler/qsc_parse/src/scan.rs +++ b/compiler/qsc_parse/src/scan.rs @@ -3,6 +3,7 @@ use super::Error; use crate::{ + completion::{collector::ValidWordCollector, WordKinds}, lex::{Lexer, Token, TokenKind}, ErrorKind, }; @@ -14,6 +15,7 @@ pub(super) struct NoBarrierError; pub(super) struct ParserContext<'a> { scanner: Scanner<'a>, language_features: LanguageFeatures, + word_collector: Option<&'a mut ValidWordCollector>, } /// Scans over the token stream. Notably enforces LL(1) parser behavior via @@ -35,6 +37,23 @@ impl<'a> ParserContext<'a> { Self { scanner: Scanner::new(input), language_features, + word_collector: None, + } + } + + pub fn with_word_collector( + input: &'a str, + language_features: LanguageFeatures, + word_collector: &'a mut ValidWordCollector, + ) -> Self { + let mut scanner = Scanner::new(input); + + word_collector.did_advance(&mut scanner.peek, scanner.offset); + + Self { + scanner, + language_features, + word_collector: Some(word_collector), } } @@ -50,8 +69,19 @@ impl<'a> ParserContext<'a> { self.scanner.span(from) } + /// Advances the scanner to start of the the next valid token. pub(super) fn advance(&mut self) { self.scanner.advance(); + + if let Some(e) = &mut self.word_collector { + e.did_advance(&mut self.scanner.peek, self.scanner.offset); + } + } + + /// Moves the scanner to the start of the current token, + /// returning the span of the skipped trivia. + pub(super) fn skip_trivia(&mut self) -> Span { + self.scanner.skip_trivia() } /// Pushes a recovery barrier. While the barrier is active, recovery will never advance past any @@ -74,6 +104,10 @@ impl<'a> ParserContext<'a> { pub(super) fn push_error(&mut self, error: Error) { self.scanner.push_error(error); + + if let Some(e) = &mut self.word_collector { + e.did_error(); + } } pub(super) fn into_errors(self) -> Vec { @@ -83,6 +117,12 @@ impl<'a> ParserContext<'a> { pub(crate) fn contains_language_feature(&self, feat: LanguageFeatures) -> bool { self.language_features.contains(feat) } + + pub fn expect(&mut self, expected: WordKinds) { + if let Some(e) = &mut self.word_collector { + e.expect(expected); + } + } } impl<'a> Scanner<'a> { @@ -93,13 +133,13 @@ impl<'a> Scanner<'a> { input, tokens, barriers: Vec::new(), + peek: peek.unwrap_or_else(|| eof(input.len())), errors: errors .into_iter() .map(|e| Error::new(ErrorKind::Lex(e))) .collect(), - recovered_eof: false, - peek: peek.unwrap_or_else(|| eof(input.len())), offset: 0, + recovered_eof: false, } } @@ -118,6 +158,15 @@ impl<'a> Scanner<'a> { } } + /// Moves the scanner to the start of the current token, + /// returning the span of the skipped trivia. + pub(super) fn skip_trivia(&mut self) -> Span { + let lo = self.offset; + self.offset = self.peek.span.lo; + let hi = self.offset; + Span { lo, hi } + } + pub(super) fn advance(&mut self) { if self.peek.kind != TokenKind::Eof { self.offset = self.peek.span.hi; @@ -155,6 +204,7 @@ impl<'a> Scanner<'a> { if peek == TokenKind::Eof || self.barriers.iter().any(|&b| contains(peek, b)) { break; } + self.advance(); } } diff --git a/compiler/qsc_parse/src/stmt.rs b/compiler/qsc_parse/src/stmt.rs index 5b11f70220..0d5f524f94 100644 --- a/compiler/qsc_parse/src/stmt.rs +++ b/compiler/qsc_parse/src/stmt.rs @@ -13,6 +13,7 @@ use super::{ Error, Result, }; use crate::{ + completion::WordKinds, lex::{Delim, TokenKind}, prim::{barrier, recovering, recovering_semi, recovering_token}, ErrorKind, @@ -90,10 +91,17 @@ fn parse_local(s: &mut ParserContext) -> Result> { }; let lhs = pat(s)?; - token(s, TokenKind::Eq)?; - let rhs = expr(s)?; - recovering_semi(s); - Ok(Box::new(StmtKind::Local(mutability, lhs, rhs))) + match token(s, TokenKind::Eq) { + Ok(()) => { + let rhs = expr(s)?; + recovering_semi(s); + Ok(Box::new(StmtKind::Local(mutability, lhs, rhs))) + } + Err(e) => { + s.push_error(e); + Ok(Box::new(StmtKind::Local(mutability, lhs, Box::default()))) + } + } } fn parse_qubit(s: &mut ParserContext) -> Result> { @@ -127,6 +135,7 @@ fn parse_qubit(s: &mut ParserContext) -> Result> { fn parse_qubit_init(s: &mut ParserContext) -> Result> { let lo = s.peek().span.lo; + s.expect(WordKinds::Qubit); let kind = if let Ok(name) = ident(s) { if name.name.as_ref() != "Qubit" { return Err(Error::new(ErrorKind::Convert( diff --git a/compiler/qsc_parse/src/tests/implicit_namespace.rs b/compiler/qsc_parse/src/tests/implicit_namespace.rs index a0816f9d83..ea734432e7 100644 --- a/compiler/qsc_parse/src/tests/implicit_namespace.rs +++ b/compiler/qsc_parse/src/tests/implicit_namespace.rs @@ -28,20 +28,18 @@ fn explicit_namespace_overrides_implicit() { hi: 21, }, doc: "", - name: Idents( - [ - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 10, - hi: 18, - }, - name: "Explicit", + name: [ + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 10, + hi: 18, }, - ], - ), + name: "Explicit", + }, + ], items: [], }, ], @@ -72,40 +70,38 @@ fn fixup_bad_namespace_name_with_dash() { hi: 26, }, doc: "", - name: Idents( - [ - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 0, - hi: 26, - }, - name: "code", + name: [ + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 0, + hi: 26, }, - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 0, - hi: 26, - }, - name: "src", + name: "code", + }, + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 0, + hi: 26, }, - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 0, - hi: 26, - }, - name: "Foo_Bar", + name: "src", + }, + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 0, + hi: 26, }, - ], - ), + name: "Foo_Bar", + }, + ], items: [ Item { id: NodeId( @@ -159,16 +155,8 @@ fn fixup_bad_namespace_name_with_dash() { hi: 23, }, kind: Path( - Path { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 19, - hi: 23, - }, - segments: None, - name: Ident { + Ok( + Path { id: NodeId( 4294967295, ), @@ -176,9 +164,19 @@ fn fixup_bad_namespace_name_with_dash() { lo: 19, hi: 23, }, - name: "Unit", + segments: None, + name: Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 19, + hi: 23, + }, + name: "Unit", + }, }, - }, + ), ), }, functors: None, @@ -257,40 +255,38 @@ oper", hi: 32, }, doc: "", - name: Idents( - [ - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 1, - hi: 32, - }, - name: "code", + name: [ + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 1, + hi: 32, }, - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 1, - hi: 32, - }, - name: "src", + name: "code", + }, + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 1, + hi: 32, }, - Ident { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 1, - hi: 32, - }, - name: "Foo", + name: "src", + }, + Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 1, + hi: 32, }, - ], - ), + name: "Foo", + }, + ], items: [ Item { id: NodeId( @@ -344,16 +340,8 @@ oper", hi: 24, }, kind: Path( - Path { - id: NodeId( - 4294967295, - ), - span: Span { - lo: 20, - hi: 24, - }, - segments: None, - name: Ident { + Ok( + Path { id: NodeId( 4294967295, ), @@ -361,9 +349,19 @@ oper", lo: 20, hi: 24, }, - name: "Unit", + segments: None, + name: Ident { + id: NodeId( + 4294967295, + ), + span: Span { + lo: 20, + hi: 24, + }, + name: "Unit", + }, }, - }, + ), ), }, functors: None, diff --git a/compiler/qsc_parse/src/ty.rs b/compiler/qsc_parse/src/ty.rs index b498a058ee..e5c82c7d3e 100644 --- a/compiler/qsc_parse/src/ty.rs +++ b/compiler/qsc_parse/src/ty.rs @@ -6,13 +6,15 @@ mod tests; use super::{ keyword::Keyword, - prim::{apos_ident, opt, path, seq, token}, + prim::{apos_ident, opt, seq, token}, scan::ParserContext, Error, Parser, Result, }; use crate::{ + completion::WordKinds, item::throw_away_doc, lex::{ClosedBinOp, Delim, TokenKind}, + prim::recovering_path, ErrorKind, }; use qsc_ast::ast::{ @@ -20,6 +22,7 @@ use qsc_ast::ast::{ }; pub(super) fn ty(s: &mut ParserContext) -> Result { + s.expect(WordKinds::PathTy); let lo = s.peek().span.lo; let lhs = base(s)?; array_or_arrow(s, lhs, lo) @@ -89,7 +92,7 @@ fn base(s: &mut ParserContext) -> Result { Ok(TyKind::Hole) } else if let Some(name) = opt(s, param)? { Ok(TyKind::Param(name)) - } else if let Some(path) = opt(s, path)? { + } else if let Some(path) = opt(s, |s| recovering_path(s, WordKinds::PathTy))? { Ok(TyKind::Path(path)) } else if token(s, TokenKind::Open(Delim::Paren)).is_ok() { let (tys, final_sep) = seq(s, ty)?; diff --git a/compiler/qsc_parse/src/ty/tests.rs b/compiler/qsc_parse/src/ty/tests.rs index 0ed05b84bd..8d38def154 100644 --- a/compiler/qsc_parse/src/ty/tests.rs +++ b/compiler/qsc_parse/src/ty/tests.rs @@ -372,3 +372,55 @@ fn op_ty_is_prec() { functors: Functor Expr _id_ [17-32]: BinOp Union: (Functor Expr _id_ [17-20]: Adj) (Functor Expr _id_ [23-32]: BinOp Intersect: (Functor Expr _id_ [23-26]: Adj) (Functor Expr _id_ [29-32]: Ctl))"#]], ); } + +#[test] +fn ty_incomplete() { + check( + ty, + "Foo. ", + &expect![[r#" + Type _id_ [0-5]: Path: Err IncompletePath [0-5]: + Ident _id_ [0-3] "Foo" + + [ + Error( + Rule( + "identifier", + Eof, + Span { + lo: 5, + hi: 5, + }, + ), + ), + ]"#]], + ); +} + +#[test] +fn ty_incomplete_in_tuple() { + check( + ty, + "(Foo, Bar. )", + &expect![[r#" + Type _id_ [0-12]: Tuple: + Type _id_ [1-4]: Path: Path _id_ [1-4] (Ident _id_ [1-4] "Foo") + Type _id_ [6-11]: Path: Err IncompletePath [6-11]: + Ident _id_ [6-9] "Bar" + + [ + Error( + Rule( + "identifier", + Close( + Paren, + ), + Span { + lo: 11, + hi: 12, + }, + ), + ), + ]"#]], + ); +} diff --git a/compiler/qsc_qasm3/src/ast_builder.rs b/compiler/qsc_qasm3/src/ast_builder.rs index a081ed967d..3eaa5e958e 100644 --- a/compiler/qsc_qasm3/src/ast_builder.rs +++ b/compiler/qsc_qasm3/src/ast_builder.rs @@ -7,9 +7,9 @@ use num_bigint::BigInt; use qsc::{ ast::{ - self, Attr, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Ident, Idents, - Item, Lit, Mutability, NodeId, Pat, PatKind, Path, QubitInit, QubitInitKind, QubitSource, - Stmt, StmtKind, TopLevelNode, Ty, TyKind, + self, Attr, Block, CallableBody, CallableDecl, CallableKind, Expr, ExprKind, Ident, Item, + Lit, Mutability, NodeId, Pat, PatKind, Path, PathKind, QubitInit, QubitInitKind, + QubitSource, Stmt, StmtKind, TopLevelNode, Ty, TyKind, }, Span, }; @@ -28,11 +28,12 @@ where ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments: build_idents(&["QIR", "Runtime"]), name: Box::new(alloc_ident), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call_expr = Expr { @@ -82,11 +83,12 @@ where }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments: build_idents(&["QIR", "Runtime"]), name: Box::new(alloc_ident), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call_expr = Expr { @@ -347,11 +349,12 @@ pub(crate) fn build_math_call_from_exprs(name: &str, exprs: Vec, span: Spa ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments: build_idents(&["Microsoft", "Quantum", "Math"]), name: Box::new(alloc_ident), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let exprs: Vec<_> = exprs.into_iter().map(Box::new).collect(); @@ -392,7 +395,7 @@ pub(crate) fn build_path_ident_expr>( segments: None, name: Box::new(ident), }; - let path_kind = ast::ExprKind::Path(Box::new(path)); + let path_kind = ast::ExprKind::Path(PathKind::Ok(Box::new(path))); ast::Expr { id: NodeId::default(), span: expr_span, @@ -416,12 +419,12 @@ pub(crate) fn build_indexed_assignment_statement>( let lhs = ast::Expr { id: NodeId::default(), span: name_span, - kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + kind: Box::new(ast::ExprKind::Path(PathKind::Ok(Box::new(ast::Path { id: NodeId::default(), span: name_span, segments: None, name: Box::new(ident.clone()), - }))), + })))), }; let assign_up = ast::StmtKind::Semi(Box::new(ast::Expr { @@ -460,7 +463,7 @@ pub(crate) fn build_assignment_statement>( let lhs = ast::Expr { id: NodeId::default(), span: name_span, - kind: Box::new(ast::ExprKind::Path(Box::new(path))), + kind: Box::new(ast::ExprKind::Path(PathKind::Ok(Box::new(path)))), }; let expr_kind = ast::ExprKind::Assign(Box::new(lhs), Box::new(rhs)); let expr = ast::Expr { @@ -483,11 +486,12 @@ pub(crate) fn build_convert_call_expr(expr: Expr, name: &str) -> Expr { ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments: build_idents(&["Microsoft", "Quantum", "Convert"]), name: Box::new(cast_ident), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call = ExprKind::Call( @@ -512,11 +516,12 @@ pub(crate) fn build_array_reverse_expr(expr: Expr) -> Expr { ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments: build_idents(&["Microsoft", "Quantum", "Arrays"]), name: Box::new(cast_ident), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call = ExprKind::Call( @@ -682,12 +687,12 @@ pub(crate) fn build_global_call_with_one_param>( let callee_expr = ast::Expr { id: NodeId::default(), span: name_span, - kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + kind: Box::new(ast::ExprKind::Path(PathKind::Ok(Box::new(ast::Path { id: NodeId::default(), span: Span::default(), segments: None, name: Box::new(ident), - }))), + })))), }; let param_expr_kind = ast::ExprKind::Paren(Box::new(expr)); @@ -719,12 +724,12 @@ pub(crate) fn build_global_call_with_two_params>( let callee_expr = ast::Expr { id: NodeId::default(), span: name_span, - kind: Box::new(ast::ExprKind::Path(Box::new(ast::Path { + kind: Box::new(ast::ExprKind::Path(PathKind::Ok(Box::new(ast::Path { id: NodeId::default(), span: Span::default(), segments: None, name: Box::new(ident), - }))), + })))), }; let param_expr_kind = ast::ExprKind::Tuple(Box::new([Box::new(fst), Box::new(snd)])); @@ -781,11 +786,12 @@ pub(crate) fn build_call_no_params(name: &str, idents: &[&str], span: Span) -> E ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments, name: Box::new(fn_name), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call = ExprKind::Call( @@ -815,11 +821,12 @@ pub(crate) fn build_call_with_param( ..Default::default() }; let path_expr = Expr { - kind: Box::new(ExprKind::Path(Box::new(Path { + kind: Box::new(ExprKind::Path(PathKind::Ok(Box::new(Path { segments, name: Box::new(fn_name), - ..Default::default() - }))), + id: NodeId::default(), + span: Span::default(), + })))), ..Default::default() }; let call = ExprKind::Call( @@ -906,11 +913,13 @@ where name: ty.to_string().into(), ..Default::default() }; - let result_ty_path = ast::Path { + let result_ty_path = ast::PathKind::Ok(Box::new(ast::Path { name: Box::new(result_ty_ident), - ..Default::default() - }; - let result_ty_kind = ast::TyKind::Path(Box::new(result_ty_path)); + segments: None, + id: NodeId::default(), + span: Span::default(), + })); + let result_ty_kind = ast::TyKind::Path(result_ty_path); let tydef = if USE_IMPLICIT_TYPE_DEF { None @@ -960,11 +969,13 @@ pub(crate) fn build_path_ident_ty>(name: S) -> Ty { name: Rc::from(name.as_ref()), ..Default::default() }; - let path = ast::Path { + let path = ast::PathKind::Ok(Box::new(ast::Path { name: Box::new(ident), - ..Default::default() - }; - let kind = TyKind::Path(Box::new(path)); + segments: Option::default(), + id: NodeId::default(), + span: Span::default(), + })); + let kind = TyKind::Path(path); Ty { kind: Box::new(kind), ..Default::default() @@ -976,12 +987,13 @@ pub(crate) fn build_complex_ty_ident() -> Ty { name: Rc::from("Complex"), ..Default::default() }; - let path = ast::Path { + let path = ast::PathKind::Ok(Box::new(ast::Path { name: Box::new(ident), segments: build_idents(&["Microsoft", "Quantum", "Math"]), - ..Default::default() - }; - let kind = TyKind::Path(Box::new(path)); + id: NodeId::default(), + span: Span::default(), + })); + let kind = TyKind::Path(path); Ty { kind: Box::new(kind), ..Default::default() @@ -996,11 +1008,12 @@ pub(crate) fn build_top_level_ns_with_item>( TopLevelNode::Namespace(qsc::ast::Namespace { id: NodeId::default(), span: whole_span, - name: qsc::ast::Idents(Box::new([Ident { + name: [Ident { name: Rc::from(ns.as_ref()), span: Span::default(), id: NodeId::default(), - }])), + }] + .into(), items: Box::new([Box::new(entry)]), doc: "".into(), }) @@ -1464,18 +1477,17 @@ pub(crate) fn build_gate_decl_lambda>( } } -fn build_idents(idents: &[&str]) -> Option { +fn build_idents(idents: &[&str]) -> Option> { let idents = idents .iter() .map(|name| Ident { name: Rc::from(*name), ..Default::default() }) - .collect::>() - .into_boxed_slice(); + .collect::>(); if idents.is_empty() { None } else { - Some(Idents(idents)) + Some(idents.into()) } } diff --git a/language_service/src/compilation.rs b/language_service/src/compilation.rs index 6cc2b28474..253eab560e 100644 --- a/language_service/src/compilation.rs +++ b/language_service/src/compilation.rs @@ -17,8 +17,8 @@ use qsc::{ use qsc_linter::{LintConfig, LintLevel}; use qsc_project::{PackageGraphSources, Project}; use rustc_hash::FxHashMap; -use std::mem::take; use std::sync::Arc; +use std::{iter::once, mem::take}; /// The alias that a project gives a dependency in its qsharp.json. /// In other words, this is the name that a given project uses to reference @@ -197,6 +197,7 @@ impl Compilation { compiler.update(increment); } + let source_package_id = compiler.source_package_id(); let (package_store, package_id) = compiler.into_package_store(); let unit = package_store .get(package_id) @@ -212,13 +213,18 @@ impl Compilation { run_linter_passes(&mut errors, &package_store, unit, lints_config); + let dependencies = dependencies + .into_iter() + .chain(once((source_package_id, None))) + .collect(); + Self { package_store, user_package_id: package_id, compile_errors: errors, project_errors: project.as_ref().map_or_else(Vec::new, |p| p.errors.clone()), kind: CompilationKind::Notebook { project }, - dependencies: dependencies.into_iter().collect(), + dependencies, } } diff --git a/language_service/src/completion.rs b/language_service/src/completion.rs index b0d2270c71..ec61ad7cac 100644 --- a/language_service/src/completion.rs +++ b/language_service/src/completion.rs @@ -1,809 +1,342 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +mod global_items; +mod locals; +mod path_context; #[cfg(test)] mod tests; +mod text_edits; -use crate::compilation::{Compilation, CompilationKind}; -use crate::protocol::{CompletionItem, CompletionItemKind, CompletionList, TextEdit}; -use crate::qsc_utils::into_range; - +use crate::{ + compilation::{Compilation, CompilationKind}, + protocol::{CompletionItem, CompletionItemKind, CompletionList, TextEdit}, +}; +use global_items::Globals; +use locals::Locals; use log::{log_enabled, trace, Level::Trace}; -use qsc::ast::visit::{self, Visitor}; -use qsc::display::{CodeDisplay, Lookup}; - -use qsc::hir::{ItemKind, Package, PackageId, Visibility}; -use qsc::line_column::{Encoding, Position, Range}; +use path_context::IncompletePath; use qsc::{ - resolve::{Local, LocalKind}, - PRELUDE, + line_column::{Encoding, Position}, + parse::completion::{ + possible_words_at_offset_in_fragments, possible_words_at_offset_in_source, + HardcodedIdentKind, NameKind, PathKind, WordKinds, + }, + LanguageFeatures, }; use rustc_hash::FxHashSet; -use std::rc::Rc; -use std::sync::Arc; +use std::iter::once; +use text_edits::TextEditRange; type SortPriority = u32; -#[derive(Debug)] -/// Used to represent pre-existing imports in the completion context -struct ImportItem { - path: Vec>, - alias: Option>, - is_glob: bool, -} - -impl ImportItem { - fn from_import_or_export_item(decl: &qsc::ast::ImportOrExportDecl) -> Vec { - if decl.is_export() { - return vec![]; - }; - let mut buf = Vec::with_capacity(decl.items.len()); - for item in &decl.items { - let alias = item.alias.as_ref().map(|x| x.name.clone()); - let is_glob = item.is_glob; - let path: qsc::ast::Idents = item.path.clone().into(); - let path = path.into_iter().map(|x| x.name.clone()).collect(); - - buf.push(ImportItem { - path, - alias, - is_glob, - }); - } - buf - } -} - -#[allow(clippy::too_many_lines)] pub(crate) fn get_completions( compilation: &Compilation, source_name: &str, position: Position, position_encoding: Encoding, ) -> CompletionList { - let offset = + let package_offset = compilation.source_position_to_package_offset(source_name, position, position_encoding); - let user_ast_package = &compilation.user_unit().ast.package; - - if log_enabled!(Trace) { - let last_char = compilation - .user_unit() - .sources - .find_by_offset(offset) - .map(|s| { - let offset = offset - s.offset; - if offset > 0 { - s.contents[(offset as usize - 1)..].chars().next() - } else { - None - } - }); - trace!("the character before the cursor is: {last_char:?}"); - } - - // Determine context for the offset - let mut context_finder = ContextFinder { - offset, - context: if user_ast_package.nodes.is_empty() { - // The parser failed entirely, no context to go on - Context::NoCompilation - } else { - // Starting context is top-level (i.e. outside a namespace block) - Context::TopLevel - }, - start_of_namespace: None, - current_namespace_name: None, - imports: vec![], - }; - context_finder.visit_package(user_ast_package); - - let insert_open_at = match compilation.kind { - CompilationKind::OpenProject { .. } => context_finder.start_of_namespace, - // Since notebooks don't typically contain namespace declarations, - // open statements should just get before the first non-whitespace - // character (i.e. at the top of the cell) - CompilationKind::Notebook { .. } => { - Some(get_first_non_whitespace_in_source(compilation, offset)) - } - }; - - let insert_open_range = insert_open_at.map(|o| { - into_range( - position_encoding, - qsc::Span { lo: o, hi: o }, - &compilation.user_unit().sources, - ) - }); - - let indent = match insert_open_at { - Some(start) => get_indent(compilation, start), - None => String::new(), - }; - - let mut prelude_ns_ids: Vec = PRELUDE - .iter() - .map(|ns| ImportItem { - path: ns.iter().map(|x| Rc::from(*x)).collect(), - alias: None, - is_glob: true, - }) - .collect(); - - // The PRELUDE namespaces are always implicitly opened. - context_finder.imports.append(&mut prelude_ns_ids); - - let mut builder = CompletionListBuilder::new(); - - match context_finder.context { - Context::Namespace => { - // Include "open", "operation", etc - builder.push_item_decl_keywords(); - builder.push_attributes(); - - // Typing into a callable decl sometimes breaks the - // parser and the context appears to be a namespace block, - // so just include everything that may be relevant - builder.push_stmt_keywords(); - builder.push_expr_keywords(); - builder.push_types(); - builder.push_globals( - compilation, - &context_finder.imports, - insert_open_range, - &context_finder.current_namespace_name, - &indent, - ); - } - - Context::CallableSignature => { - builder.push_types(); - builder.push_locals(compilation, offset, false, true); - } - Context::Block => { - // Pretty much anything goes in a block - builder.push_locals(compilation, offset, true, true); - builder.push_stmt_keywords(); - builder.push_expr_keywords(); - builder.push_types(); - builder.push_globals( - compilation, - &context_finder.imports, - insert_open_range, - &context_finder.current_namespace_name, - &indent, - ); - - // Item decl keywords last, unlike in a namespace - builder.push_item_decl_keywords(); - } - Context::NoCompilation | Context::TopLevel => match compilation.kind { - CompilationKind::OpenProject { .. } => builder.push_namespace_keyword(), - CompilationKind::Notebook { .. } => { - // For notebooks, the top-level allows for - // more syntax. - - builder.push_locals(compilation, offset, true, true); - - // Item declarations - builder.push_item_decl_keywords(); - - // Things that go in a block - builder.push_stmt_keywords(); - builder.push_expr_keywords(); - builder.push_types(); - builder.push_globals( - compilation, - &context_finder.imports, - insert_open_range, - &context_finder.current_namespace_name, - &indent, - ); - - // Namespace declarations - least likely to be used, so last - builder.push_namespace_keyword(); - } - }, - }; - - CompletionList { - items: builder.into_items(), - } -} - -fn get_first_non_whitespace_in_source(compilation: &Compilation, package_offset: u32) -> u32 { - const QSHARP_MAGIC: &str = "//qsharp"; let source = compilation .user_unit() .sources .find_by_offset(package_offset) - .expect("source should exist in the user source map"); - - // Skip the //qsharp magic if it exists (notebook cells) - let start = if let Some(qsharp_magic_start) = source.contents.find(QSHARP_MAGIC) { - qsharp_magic_start + QSHARP_MAGIC.len() - } else { - 0 - }; - - let source_after_magic = &source.contents[start..]; - - let first = start - + source_after_magic - .find(|c: char| !c.is_whitespace()) - .unwrap_or(source_after_magic.len()); - - let first = u32::try_from(first).expect("source length should fit into u32"); - - source.offset + first -} + .expect("source should exist"); + let source_offset: u32 = package_offset - source.offset; -fn get_indent(compilation: &Compilation, package_offset: u32) -> String { - let source = compilation + // The parser uses the relative source name to figure out the implicit namespace. + let source_name_relative = compilation .user_unit() .sources - .find_by_offset(package_offset) - .expect("source should exist in the user source map"); - let source_offset = (package_offset - source.offset) - .try_into() - .expect("offset can't be converted to uszie"); - let before_offset = &source.contents[..source_offset]; - let mut indent = match before_offset.rfind(|c| c == '{' || c == '\n') { - Some(begin) => { - let indent = &before_offset[begin..]; - indent.strip_prefix('{').unwrap_or(indent) - } - None => before_offset, - } - .to_string(); - if !indent.starts_with('\n') { - indent.insert(0, '\n'); + .relative_sources() + .find(|s| s.offset == source.offset) + .expect("source should exist in the user source map") + .name; + + if log_enabled!(Trace) { + let last_char = if source_offset > 0 { + source.contents[(package_offset as usize - 1)..] + .chars() + .next() + } else { + None + }; + trace!("the character before the cursor is: {last_char:?}"); } - indent -} -struct CompletionListBuilder { - current_sort_group: SortPriority, - items: FxHashSet, + // What kinds of words are expected at the cursor location? + let expected_words_at_cursor = expected_word_kinds( + compilation, + &source_name_relative, + &source.contents, + source_offset, + ); + + // Now that we have the information from the parser about what kinds of + // words are expected, gather the actual words (identifiers, keywords, etc) for each kind. + + // Keywords and other hardcoded words + let hardcoded_completions = collect_hardcoded_words(expected_words_at_cursor); + + // The tricky bit: globals, locals, names we need to gather from the compilation. + let name_completions = collect_names( + expected_words_at_cursor, + package_offset, + compilation, + position_encoding, + ); + + // We have all the data, put everything into a completion list. + into_completion_list(once(hardcoded_completions).chain(name_completions)) } -impl CompletionListBuilder { - fn new() -> Self { - CompletionListBuilder { - current_sort_group: 1, - items: FxHashSet::default(), - } +/// Invokes the parser to determine what kinds of words are expected at the cursor location. +fn expected_word_kinds( + compilation: &Compilation, + source_name_relative: &str, + source_contents: &str, + cursor_offset: u32, +) -> WordKinds { + match &compilation.kind { + CompilationKind::OpenProject { + package_graph_sources, + } => possible_words_at_offset_in_source( + source_contents, + Some(source_name_relative), + package_graph_sources.root.language_features, + cursor_offset, + ), + CompilationKind::Notebook { project } => possible_words_at_offset_in_fragments( + source_contents, + project.as_ref().map_or(LanguageFeatures::default(), |p| { + p.package_graph_sources.root.language_features + }), + cursor_offset, + ), } +} - fn into_items(self) -> Vec { - self.items.into_iter().collect() +/// Collects hardcoded completions from the given set of parser predictions. +/// +/// Hardcoded words are actual keywords (`let`, etc) as well as other words that are +/// hardcoded into the language (`Qubit`, `EntryPoint`, etc) +fn collect_hardcoded_words(expected: WordKinds) -> Vec { + let mut completions = Vec::new(); + for word_kind in expected.iter_hardcoded_ident_kinds() { + match word_kind { + HardcodedIdentKind::Qubit => { + completions.push(Completion::new( + "Qubit".to_string(), + CompletionItemKind::Interface, + )); + } + HardcodedIdentKind::Attr => { + completions.extend([ + Completion::new("EntryPoint".to_string(), CompletionItemKind::Interface), + Completion::new("Config".to_string(), CompletionItemKind::Interface), + ]); + } + HardcodedIdentKind::Size => { + completions.push(Completion::new( + "size".to_string(), + CompletionItemKind::Keyword, + )); + } + } } - fn push_item_decl_keywords(&mut self) { - static ITEM_KEYWORDS: [&str; 5] = ["operation", "open", "internal", "function", "newtype"]; - - self.push_completions( - ITEM_KEYWORDS - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Keyword)) - .into_iter(), - ); + for keyword in expected.iter_keywords() { + let keyword = keyword.to_string(); + // Skip adding the underscore keyword to the list, it's more confusing than helpful. + if keyword != "_" { + completions.push(Completion::new(keyword, CompletionItemKind::Keyword)); + } } - fn push_attributes(&mut self) { - static ATTRIBUTES: [&str; 2] = ["@EntryPoint()", "@Config()"]; - - self.push_completions( - ATTRIBUTES - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Property)) - .into_iter(), - ); - } + completions +} - fn push_namespace_keyword(&mut self) { - self.push_completions( - [CompletionItem::new( - "namespace".to_string(), - CompletionItemKind::Keyword, - )] - .into_iter(), - ); - } +/// Collects names from the compilation that match the expected word kinds. +fn collect_names( + expected: WordKinds, + cursor_offset: u32, + compilation: &Compilation, + position_encoding: Encoding, +) -> Vec> { + let mut groups = Vec::new(); - fn push_types(&mut self) { - static PRIMITIVE_TYPES: [&str; 10] = [ - "Qubit", "Int", "Unit", "Result", "Bool", "BigInt", "Double", "Pauli", "Range", - "String", - ]; - static FUNCTOR_KEYWORDS: [&str; 3] = ["Adj", "Ctl", "is"]; + for name_kind in expected.iter_name_kinds() { + match name_kind { + NameKind::Path(path_kind) => { + let globals = Globals::init(cursor_offset, compilation); + let edit_range = TextEditRange::init(cursor_offset, compilation, position_encoding); + let locals = Locals::new(cursor_offset, compilation); - self.push_completions( - PRIMITIVE_TYPES - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Interface)) - .into_iter(), - ); + groups.extend(collect_paths(path_kind, &globals, &locals, &edit_range)); + } + NameKind::PathSegment => { + let globals = Globals::init(cursor_offset, compilation); + let path = + IncompletePath::init(cursor_offset, &compilation.user_unit().ast.package); - self.push_completions( - FUNCTOR_KEYWORDS - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Keyword)) - .into_iter(), - ); + groups.extend(collect_path_segments(&globals, &path)); + } + NameKind::TyParam => { + let locals = Locals::new(cursor_offset, compilation); + groups.push(locals.type_names()); + } + NameKind::Field => { + // Not yet implemented + } + }; } + groups +} - /// Populates self's completion list with globals - fn push_globals( - &mut self, - compilation: &Compilation, - imports: &[ImportItem], - insert_open_range: Option, - current_namespace_name: &Option>>, - indent: &String, - ) { - for (package_id, _) in compilation.package_store.iter().rev() { - self.push_sorted_completions(Self::get_callables( - compilation, - package_id, - imports, - insert_open_range, - current_namespace_name.as_deref(), - indent, - )); +/// Collects paths that are applicable at the current cursor offset, +/// taking into account all the relevant name resolution context, +/// such as scopes and visibility at the cursor location. +/// +/// Note that the list will not contain full paths to items. It will typically +/// only include leading qualifier, or the item name along with an auto-import edit. +/// For example, the item `Microsoft.Quantum.Diagnostics.DumpMachine` will contribute +/// two completion items: the leading qualifier (namespace) `Microsoft` and the +/// callable name `DumpMachine` with an auto-import edit to add `Microsoft.Quantum.Diagnostics`. +fn collect_paths( + expected: PathKind, + globals: &Globals, + locals_at_cursor: &Locals, + text_edit_range: &TextEditRange, +) -> Vec> { + let mut global_names = Vec::new(); + let mut locals_and_builtins = Vec::new(); + match expected { + PathKind::Expr => { + locals_and_builtins.push(locals_at_cursor.expr_names()); + global_names.extend(globals.expr_names(text_edit_range)); } + PathKind::Ty => { + locals_and_builtins.push(locals_at_cursor.type_names()); + locals_and_builtins.push( + [ + "Qubit", "Int", "Unit", "Result", "Bool", "BigInt", "Double", "Pauli", "Range", + "String", + ] + .map(|s| Completion::new(s.to_string(), CompletionItemKind::Interface)) + .into(), + ); - for (id, unit) in compilation.package_store.iter().rev() { - let alias = compilation.dependencies.get(&id).cloned().flatten(); - self.push_completions(Self::get_namespaces(&unit.package, alias)); + global_names.extend(globals.type_names(text_edit_range)); + } + PathKind::Import => { + global_names.extend(globals.expr_names_in_scope_only()); + global_names.extend(globals.type_names_in_scope_only()); + global_names.push(globals.namespaces()); + } + PathKind::Struct => { + global_names.extend(globals.type_names(text_edit_range)); + } + PathKind::Namespace => { + global_names.push(globals.namespaces()); } } - fn push_locals( - &mut self, - compilation: &Compilation, - offset: u32, - include_terms: bool, - include_tys: bool, - ) { - self.push_sorted_completions( - compilation - .user_unit() - .ast - .locals - .get_all_at_offset(offset) - .iter() - .filter_map(|candidate| { - local_completion(candidate, compilation, include_terms, include_tys) - }) - .map(|item| (item, 0)), - ); - } - - fn push_stmt_keywords(&mut self) { - static STMT_KEYWORDS: [&str; 5] = ["let", "return", "use", "mutable", "borrow"]; - - self.push_completions( - STMT_KEYWORDS - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Keyword)) - .into_iter(), - ); - } - - fn push_expr_keywords(&mut self) { - static EXPR_KEYWORDS: [&str; 11] = [ - "if", "for", "in", "within", "apply", "repeat", "until", "fixup", "set", "while", - "fail", - ]; - - self.push_completions( - EXPR_KEYWORDS - .map(|key| CompletionItem::new(key.to_string(), CompletionItemKind::Keyword)) - .into_iter(), - ); - } - - /// Each invocation of this function increments the sort group so that - /// in the eventual completion list, the groups of items show up in the - /// order they were added. - /// The items are then sorted according to the input list order (not alphabetical) - fn push_completions(&mut self, iter: impl Iterator) { - let mut current_sort_prefix = 0; + // This order ensures that locals and builtins come before globals + // in the eventual completion list + locals_and_builtins.extend(global_names); + locals_and_builtins +} - self.items.extend(iter.map(|item| CompletionItem { - sort_text: { - current_sort_prefix += 1; - Some(format!( - "{:02}{:02}{}", - self.current_sort_group, current_sort_prefix, item.label - )) - }, - ..item - })); +/// Collects path segments that are applicable at the current cursor offset. +/// Assumes that the cursor is in the middle of a path, either immediately +/// following a `.` or in the middle of a path segment that follows a `.` . +/// +/// Narrows down the list based on the qualifier before the `.` as well +/// as the name kind expected at that syntax node. For example, +/// `let x : Microsoft.Quantum.Math.↘` should include `Complex` (a type) while +/// `let x = Microsoft.Quantum.Math.↘` should include `PI` (a callable). +fn collect_path_segments(globals: &Globals, path_context: &IncompletePath) -> Vec> { + let Some((path_kind, qualifier)) = path_context.context() else { + return Vec::new(); + }; - self.current_sort_group += 1; + match path_kind { + PathKind::Namespace => globals.namespaces_in(&qualifier), + PathKind::Expr => globals.expr_names_in(&qualifier), + PathKind::Ty | PathKind::Struct => globals.type_names_in(&qualifier), + PathKind::Import => [ + globals.expr_names_in(&qualifier), + globals.type_names_in(&qualifier), + globals.namespaces_in(&qualifier), + ] + .into_iter() + .flatten() + .collect(), } +} - /// Push a group of completions that are themselves sorted into subgroups - fn push_sorted_completions( - &mut self, - iter: impl Iterator, - ) { - self.items - .extend(iter.map(|(item, item_sort_group)| CompletionItem { +/// Builds the `CompletionList` from the ordered groups of completion items. +fn into_completion_list(groups: impl Iterator>) -> CompletionList { + // The HashSet serves to eliminate duplicates. + let mut items = FxHashSet::default(); + + // Build list one group at a time. The sort order + // is determined by the order in which the groups are pushed. + // Within each group, items are sorted by sort_priority. + for (current_sort_group, group) in groups.enumerate() { + items.extend(group.into_iter().map( + |Completion { + item, + sort_priority, + }| CompletionItem { + // The sort_text is what the editor will ultimately use to + // sort the items, so we're using the sort priority as a prefix. sort_text: Some(format!( "{:02}{:02}{}", - self.current_sort_group, item_sort_group, item.label + current_sort_group, sort_priority, item.label )), ..item - })); - - self.current_sort_group += 1; - } - - #[allow(clippy::too_many_lines)] - /// Get all callables in a package and return them as completion items, with a sort priority. - fn get_callables<'a>( - compilation: &'a Compilation, - package_id: PackageId, - // name and alias - imports: &'a [ImportItem], - // The range at which to insert an open statement if one is needed - insert_open_at: Option, - // The name of the current namespace, if any -- - current_namespace_name: Option<&'a [Rc]>, - indent: &'a String, - ) -> impl Iterator + 'a { - let package = &compilation - .package_store - .get(package_id) - .expect("package id should exist") - .package; - - // if an alias exists for this dependency from the manifest, - // this is used to prefix any access to items from this package with the alias - let package_alias_from_manifest = - compilation.dependencies.get(&package_id).cloned().flatten(); - - let display = CodeDisplay { compilation }; - - let is_user_package = compilation.user_package_id == package_id; - - // Given the package, get all completion items by iterating over its items - // and converting any that would be valid as completions into completions - package.items.values().filter_map(move |i| { - package_item_to_completion_item( - i, - package, - is_user_package, - current_namespace_name, - &display, - &package_alias_from_manifest, - imports, - insert_open_at, - indent, - ) - }) - } - - fn get_namespaces( - package: &'_ Package, - package_alias: Option>, - ) -> impl Iterator + '_ { - package.items.values().filter_map(move |i| match &i.kind { - ItemKind::Namespace(namespace, _) => { - let qualification = namespace - .str_iter() - .into_iter() - .map(Rc::from) - .collect::>(); - let label = format_external_name(&package_alias, &qualification[..], None); - Some(CompletionItem::new(label, CompletionItemKind::Module)) - } - _ => None, - }) + }, + )); } -} - -/// Format an external fully qualified name -/// This will prepend the package alias and remove `Main` if it is the first namespace -fn format_external_name( - package_alias_from_manifest: &Option>, - qualification: &[Rc], - name: Option<&str>, -) -> String { - let mut fully_qualified_name: Vec> = if let Some(alias) = package_alias_from_manifest { - vec![Rc::from(&*alias.clone())] - } else { - vec![] - }; - // if this comes from an external project's Main, then the path does not include Main - let item_comes_from_main_of_external_project = package_alias_from_manifest.is_some() - && qualification.len() == 1 - && qualification.first() == Some(&"Main".into()); - - // So, if it is _not_ from an external project's `Main`, we include the namespace in the fully - // qualified name. - if !(item_comes_from_main_of_external_project) { - fully_qualified_name.append(&mut qualification.to_vec()); - }; - - if let Some(name) = name { - fully_qualified_name.push(name.into()); + CompletionList { + items: items.into_iter().collect(), } - - fully_qualified_name.join(".") -} - -/// Convert a local into a completion item -fn local_completion( - candidate: &Local, - compilation: &Compilation, - include_terms: bool, - include_tys: bool, -) -> Option { - let display = CodeDisplay { compilation }; - let (kind, detail) = match &candidate.kind { - LocalKind::Item(item_id) => { - let item = compilation.resolve_item_relative_to_user_package(item_id); - let (detail, kind) = match &item.0.kind { - ItemKind::Callable(decl) => { - if !include_terms { - return None; - } - ( - Some(display.hir_callable_decl(decl).to_string()), - CompletionItemKind::Function, - ) - } - ItemKind::Namespace(_, _) => { - panic!("did not expect local namespace item") - } - ItemKind::Ty(_, udt) => { - if !include_terms && !include_tys { - return None; - } - ( - Some(display.hir_udt(udt).to_string()), - CompletionItemKind::Interface, - ) - } - // We don't want completions for items exported from the local scope - ItemKind::Export(_, _) => return None, - }; - (kind, detail) - } - LocalKind::Var(node_id) => { - if !include_terms { - return None; - } - let detail = Some(display.name_ty_id(&candidate.name, *node_id).to_string()); - (CompletionItemKind::Variable, detail) - } - LocalKind::TyParam(_) => { - if !include_tys { - return None; - } - (CompletionItemKind::TypeParameter, None) - } - }; - - Some(CompletionItem { - label: candidate.name.to_string(), - kind, - sort_text: None, - detail, - additional_text_edits: None, - }) } -struct ContextFinder { - offset: u32, - context: Context, - imports: Vec, - start_of_namespace: Option, - current_namespace_name: Option>>, +struct Completion { + item: CompletionItem, + sort_priority: SortPriority, } -#[derive(Debug, PartialEq)] -enum Context { - NoCompilation, - TopLevel, - Namespace, - CallableSignature, - Block, -} - -impl Visitor<'_> for ContextFinder { - fn visit_namespace(&mut self, namespace: &'_ qsc::ast::Namespace) { - if namespace.span.contains(self.offset) { - self.current_namespace_name = Some(namespace.name.clone().into()); - self.context = Context::Namespace; - self.imports = vec![]; - self.start_of_namespace = None; - visit::walk_namespace(self, namespace); - } +impl Completion { + fn new(label: String, kind: CompletionItemKind) -> Self { + Self::with_detail(label, kind, None) } - fn visit_item(&mut self, item: &'_ qsc::ast::Item) { - if self.start_of_namespace.is_none() { - self.start_of_namespace = Some(item.span.lo); - } - - match &*item.kind { - qsc::ast::ItemKind::Open(name, alias) => { - let open_as_import = ImportItem { - path: name.clone().into(), - alias: alias.as_ref().map(|x| x.name.clone()), - is_glob: true, - }; - self.imports.push(open_as_import); - } - qsc::ast::ItemKind::ImportOrExport(decl) => { - // if this is an import, populate self.imports - if decl.is_import() { - self.imports - .append(&mut ImportItem::from_import_or_export_item(decl)); - } - } - _ => (), - } - - if item.span.contains(self.offset) { - visit::walk_item(self, item); - } - } - - fn visit_callable_decl(&mut self, decl: &'_ qsc::ast::CallableDecl) { - if decl.span.contains(self.offset) { - // This span covers the body too, but the - // context will get overwritten by visit_block - // if the offset is inside the actual body - self.context = Context::CallableSignature; - visit::walk_callable_decl(self, decl); - } - } - - fn visit_block(&mut self, block: &'_ qsc::ast::Block) { - if block.span.contains(self.offset) { - self.context = Context::Block; - } + fn with_detail(label: String, kind: CompletionItemKind, detail: Option) -> Self { + Self::with_text_edits(label, kind, detail, None, 0) } -} - -#[allow(clippy::too_many_arguments)] -fn package_item_to_completion_item( - item: &qsc::hir::Item, - package: &qsc::hir::Package, - is_user_package: bool, - current_namespace_name: Option<&[Rc]>, - display: &CodeDisplay, - package_alias_from_manifest: &Option>, - imports: &[ImportItem], - insert_open_at: Option, - indent: &String, -) -> Option<(CompletionItem, SortPriority)> { - // We only want items whose parents are namespaces - if let Some(item_id) = item.parent { - if let Some(parent) = package.items.get(item_id) { - if let ItemKind::Namespace(callable_namespace, _) = &parent.kind { - // filter out internal packages that are not from the user's - // compilation - if matches!(item.visibility, Visibility::Internal) && !is_user_package { - return None; // ignore item if not in the user's package - } - match &item.kind { - ItemKind::Callable(callable_decl) => { - return Some(callable_decl_to_completion_item( - callable_decl, - current_namespace_name, - display, - package_alias_from_manifest, - callable_namespace, - imports, - insert_open_at, - indent, - )) - } - _ => return None, - } - } + fn with_text_edits( + label: String, + kind: CompletionItemKind, + detail: Option, + additional_text_edits: Option>, + sort_priority: u32, + ) -> Completion { + Completion { + item: CompletionItem { + label, + kind, + // This will be populated from sort_priority when the list gets built + sort_text: None, + detail, + additional_text_edits, + }, + sort_priority, } } - None -} - -#[allow(clippy::too_many_arguments)] -fn callable_decl_to_completion_item( - callable_decl: &qsc::hir::CallableDecl, - current_namespace_name: Option<&[Rc]>, - display: &CodeDisplay, - package_alias_from_manifest: &Option>, - callable_namespace: &qsc::hir::Idents, - imports: &[ImportItem], - insert_import_at: Option, - indent: &String, -) -> (CompletionItem, SortPriority) { - let name = callable_decl.name.name.as_ref(); - // details used when rendering the completion item - let detail = Some(display.hir_callable_decl(callable_decl).to_string()); - // Everything that starts with a __ goes last in the list - let sort_group = u32::from(name.starts_with("__")); - - let namespace_as_strs = Into::>::into(callable_namespace); - - // Now, we calculate the qualification that goes before the import - // item. - // if an exact import already exists, or if that namespace - // is glob imported, then there is no qualification. - // If there is no matching import or glob import, then the - // qualification is the full namespace name. - // - // An exact import is an import that matches the namespace - // and item name exactly - let preexisting_exact_import = imports.iter().any(|import_item| { - if import_item.is_glob { - return false; - } - let import_item_namespace = &import_item.path[..import_item.path.len() - 1]; - let import_item_name = import_item.path.last().map(|x| &**x); - *import_item_namespace == namespace_as_strs[..] && import_item_name == Some(name) - }); - - let preexisting_glob_import = imports - .iter() - .any(|import_item| import_item.path == namespace_as_strs[..] && import_item.is_glob); - - let preexisting_namespace_alias = imports.iter().find_map(|import_item| { - if import_item.path == namespace_as_strs[..] { - import_item.alias.as_ref().map(|x| vec![x.clone()]) - } else { - None - } - }); - - // this conditional is kind of gnarly, but it boils down to: - // do we need to add an import statement for this item, or is it already accessible? - // If so, what edit? - // The first condition is if we are in the same namespace, - // the second is if there is already an exact import or glob import of this item. In these - // cases, we do not need to add an import statement. - // The third condition is if there is no existing imports, and no existing package alias (a - // form of import), then we need to add an import statement. - let additional_text_edit = - // first condition - if current_namespace_name == Some(namespace_as_strs.as_slice()) || - // second condition - (preexisting_exact_import || preexisting_glob_import) { None } - // third condition - else if preexisting_namespace_alias.is_none() { - // if there is no place to insert an import, then we can't add an import. - if let Some(range) = insert_import_at { - let import_text = format_external_name( - package_alias_from_manifest, - &Into::>::into(callable_namespace), - Some(name), - ); - Some(TextEdit { - new_text: format!("import {import_text};{indent}",), - range, - }) - } else { None } - } else { - None - }; - - let label = if let Some(qualification) = preexisting_namespace_alias { - format_external_name(package_alias_from_manifest, &qualification, Some(name)) - } else { - name.to_owned() - }; - - ( - CompletionItem { - label, - kind: CompletionItemKind::Function, - sort_text: None, // This will get filled in during `push_sorted_completions` - detail, - additional_text_edits: additional_text_edit.map(|x| vec![x]), - }, - sort_group, - ) } diff --git a/language_service/src/completion/global_items.rs b/language_service/src/completion/global_items.rs new file mode 100644 index 0000000000..5956556ba0 --- /dev/null +++ b/language_service/src/completion/global_items.rs @@ -0,0 +1,813 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::{text_edits::TextEditRange, Completion}; +use crate::{ + compilation::Compilation, + protocol::{CompletionItemKind, TextEdit}, +}; +use qsc::{ + ast::{ + visit::{walk_block, walk_callable_decl, walk_item, walk_namespace, Visitor}, + Idents as _, Package as AstPackage, PathKind, + }, + display::CodeDisplay, + hir::{ty::Udt, CallableDecl, Idents, ItemKind, Package, PackageId, Visibility}, + PRELUDE, +}; +use std::{iter::once, rc::Rc}; + +/// Provides the globals that are visible or importable at the cursor offset. +pub(super) struct Globals<'a> { + compilation: &'a Compilation, + imports: Vec, +} + +impl<'a> Globals<'a> { + pub fn init(offset: u32, compilation: &'a Compilation) -> Self { + let import_finder = ImportFinder::init(offset, &compilation.user_unit().ast.package); + + Self { + compilation, + imports: import_finder.imports, + } + } + + /// Returns all names that are valid in an expression context, + /// and available at the current offset, + /// taking into account any imports that are in scope. + /// + /// If the item name is not in scope, + /// includes the item with text edits (auto-imports, etc.) to bring it into scope. + pub fn expr_names(&self, edit_range: &TextEditRange) -> Vec> { + // let mut completions = Vec::new(); + + // include UDTs as well since they can be constructors + let mut completions = self.items( + true, // include_callables + true, // include_udts + false, // in_scope_only + Some(edit_range), + ); + + completions.push(self.namespaces()); + + completions + } + + /// Returns all names that are valid in a type context, + /// and available at the current offset, + /// taking into account any imports that are in scope. + /// + /// If the item name is not in scope, and `in_scope_only` is false, + /// includes the item with text edits (auto-imports, etc.) to bring it into scope. + pub fn type_names(&self, edit_range: &TextEditRange) -> Vec> { + let mut completions = Vec::new(); + + completions.extend(self.items( + false, // include_callables + true, // include_udts + false, // in_scope_only + Some(edit_range), + )); + completions.push(self.namespaces()); + + completions + } + + /// Returns all names that are valid in an expression context, + /// and available at the current offset, + /// taking into account any imports that are in scope. + /// + /// Does not + pub fn expr_names_in_scope_only(&self) -> Vec> { + // let mut completions = Vec::new(); + + // include UDTs as well since they can be constructors + let mut completions = self.items( + true, // include_callables + true, // include_udts + true, // in_scope_only + None, + ); + + completions.push(self.namespaces()); + + completions + } + + /// Returns all names that are valid in a type context, + /// and available at the current offset, + /// taking into account any imports that are in scope. + /// + /// If the item name is not in scope, and `in_scope_only` is false, + /// includes the item with text edits (auto-imports, etc.) to bring the item into scope. + pub fn type_names_in_scope_only(&self) -> Vec> { + let mut completions = Vec::new(); + + completions.extend(self.items( + false, // include_callables + true, // include_udts + true, // in_scope_only + None, + )); + completions.push(self.namespaces()); + + completions + } + + /// Returns all namespaces in the compilation. + pub fn namespaces(&self) -> Vec { + let mut completions = Vec::new(); + + // Add all package aliases, and all top-level + // namespaces where the package does not have an alias + for (is_user_package, package_alias, package) in self.iter_all_packages() { + if let Some(package_alias) = package_alias { + completions.push(Completion::new( + (*package_alias).into(), + CompletionItemKind::Module, + )); + } else { + completions.extend(Self::namespaces_in_namespace(package, &[], is_user_package)); + } + } + completions + } + + /// Returns all namespaces that are valid completions at the current offset, + /// for the given qualifier. + pub fn namespaces_in(&self, qualifier: &[Rc]) -> Vec> { + let namespaces_in_packages = self.matching_namespaces_in_packages(qualifier); + + let mut groups = Vec::new(); + for (package, is_user_package, namespaces) in &namespaces_in_packages { + let mut completions = Vec::new(); + + for namespace in namespaces { + completions.extend(Self::namespaces_in_namespace( + package, + namespace, + *is_user_package, + )); + } + + groups.push(completions); + } + + groups + } + + /// Returns all names that are valid completions at the current offset, + /// in an expression context, for the given qualifier, + /// taking into account any imports that are in scope. + pub fn expr_names_in(&self, qualifier: &[Rc]) -> Vec> { + let mut groups = self.items_in( + qualifier, true, // include_callables + true, // include_udts + ); + + groups.extend(self.namespaces_in(qualifier)); + groups + } + + /// Returns all names that are valid completions at the current offset, + /// in a type context, for the given qualifier, + /// taking into account any imports that are in scope. + pub fn type_names_in(&self, qualifier: &[Rc]) -> Vec> { + let mut groups = self.items_in( + qualifier, false, // include_callables + true, // include_udts + ); + + groups.extend(self.namespaces_in(qualifier)); + groups + } + + /// Returns all item names that are available at the current offset, + /// taking into account any imports that are in scope. + /// + /// If the item name is not in scope, and `in_scope_only` is false, + /// includes the item with text edits (auto-imports, etc.) to bring the item into scope. + fn items( + &self, + include_callables: bool, + include_udts: bool, + in_scope_only: bool, + edit_range: Option<&TextEditRange>, + ) -> Vec> { + let mut groups = Vec::new(); + + for (is_user_package, package_alias, package) in self.iter_all_packages() { + // Given the package, get all completion items by iterating over its items + // and converting any that would be valid as completions into completions + let completions = package + .items + .values() + .filter_map(|item| { + Self::is_item_relevant( + package, + item, + include_callables, + include_udts, + is_user_package, + ) + }) + .filter_map(|item| { + let import_info = self.import_info(&item, package_alias); + if in_scope_only && !matches!(import_info, ImportInfo::InScope) { + return None; + } + Some(self.to_completion(&item, import_info, edit_range)) + }) + .collect(); + groups.push(completions); + } + groups + } + + /// Returns all item names that are valid completions at the current offset, + /// for the given qualifier, taking into account any imports that are in scope. + fn items_in( + &'a self, + qualifier: &[Rc], + include_callables: bool, + include_udts: bool, + ) -> Vec> { + let namespaces_in_packages = self.matching_namespaces_in_packages(qualifier); + + let mut groups = Vec::new(); + for (package, is_user_package, namespaces) in &namespaces_in_packages { + let mut completions = Vec::new(); + + for namespace in namespaces { + completions.extend( + Self::items_in_namespace( + package, + namespace, + include_callables, + include_udts, + *is_user_package, + ) + .into_iter() + .map(|item| self.to_completion(&item, ImportInfo::InScope, None)), + ); + } + + groups.push(completions); + } + + groups + } + + /// For a given package, returns all namespace names that are direct + /// children of the given namespace prefix. + /// + /// E.g. if the package contains `Foo.Bar.Baz` and `Foo.Qux` , and + /// the given prefix is `Foo` , this will return `Bar` and `Qux`. + fn namespaces_in_namespace( + package: &Package, + ns_prefix: &[Rc], + is_user_package: bool, + ) -> Vec { + package + .items + .values() + .filter_map(move |i| match &i.kind { + ItemKind::Namespace(namespace, _) => { + let candidate_ns: Vec> = namespace.into(); + + // Skip the `Main` namespace from dependency packages. + if !is_user_package && candidate_ns == ["Main".into()] { + return None; + } + + let prefix_stripped = candidate_ns.strip_prefix(ns_prefix); + if let Some(end) = prefix_stripped { + if let Some(first) = end.first() { + return Some(Completion::new( + first.to_string(), + CompletionItemKind::Module, + )); + } + } + None + } + _ => None, + }) + .collect() + } + + /// For a given package, returns all items that are in the given namespace. + fn items_in_namespace( + package: &'a Package, + namespace: &[Rc], + include_callables: bool, + include_udts: bool, + is_user_package: bool, + ) -> Vec> { + let ns_items = package.items.values().find_map(move |i| { + if let ItemKind::Namespace(candidate_ns, items) = &i.kind { + let candidate_ns: Vec> = candidate_ns.into(); + + // If the namespace matches exactly, include the items. + if candidate_ns == namespace { + return Some(items); + } + + // If we're being asked for the the top-level namespace in a dependency package, + // include items from the `Main` namespace. + if namespace.is_empty() && candidate_ns == ["Main".into()] && !is_user_package { + return Some(items); + } + } + None + }); + + ns_items + .into_iter() + .flatten() + .filter_map(|item_id| { + let item = package + .items + .get(*item_id) + .expect("item id should exist in package"); + + Self::is_item_relevant( + package, + item, + include_callables, + include_udts, + is_user_package, + ) + }) + .collect() + } + + /// Given a qualifier, and any imports that are in scope, + /// produces a list of `(package, is_user_package, namespaces)` + /// tuples that this qualifier could match. + #[allow(clippy::type_complexity)] + fn matching_namespaces_in_packages( + &self, + qualifier: &[Rc], + ) -> Vec<(&Package, bool, Vec>>)> { + let namespaces = self.matching_namespaces(qualifier); + + let mut packages_and_namespaces = Vec::new(); + + for (is_user_package, package_alias, package) in self.iter_all_packages() { + let mut namespaces_for_package = Vec::new(); + for namespace in &namespaces { + if let Some(package_alias) = package_alias { + // Only include the namespace if it starts with this package's alias + if !namespace.is_empty() && *namespace[0] == *package_alias { + namespaces_for_package.push(namespace[1..].to_vec()); + } + } else { + // No package alias, always include the namespace + namespaces_for_package.push(namespace.clone()); + } + } + packages_and_namespaces.push((package, is_user_package, namespaces_for_package)); + } + + packages_and_namespaces + } + + /// Given a qualifier, and any imports that are in scope, + /// produces a list of potential namespaces this qualifier could match. + fn matching_namespaces(&self, qualifier: &[Rc]) -> Vec>> { + let mut namespaces: Vec>> = Vec::new(); + // Add the qualifier as is + namespaces.push(qualifier.to_vec()); + + // Add qualifier prefixed with all opened namespaces + let opened_namespaces = self + .imports + .iter() + .filter_map(|import_item| { + if import_item.is_glob { + Some(import_item.path.clone()) + } else { + None + } + }) + .collect::>(); + for open_namespace in &opened_namespaces { + namespaces.push([open_namespace, qualifier].concat()); + } + + // Does `qualifier` start with a namespace alias? + let full_qualifier_for_aliased_ns = self + .imports + .iter() + .find_map(|import_item| { + if !qualifier.is_empty() + && import_item + .alias + .as_ref() + .is_some_and(|s| **s == *qualifier[0]) + { + Some(&import_item.path) + } else { + None + } + }) + .map(|full_ns_for_alias| { + let rest = &qualifier[1..]; + [full_ns_for_alias, rest].concat() + }); + + // Add any aliased namespaces with an alias that matches the qualifier + if let Some(full_qualifier_for_aliased_ns) = &full_qualifier_for_aliased_ns { + namespaces.push(full_qualifier_for_aliased_ns.clone()); + } + namespaces + } + + /// Returns the core package, then the dependencies in reverse order, + /// then finally the user package. + /// + /// Iterating in this order ensures that the user package items appear + /// first, and indirect dependencies are lower on the completion list. + /// + /// Returns the tuple of `(is_user_package, alias, package)` + fn iter_all_packages(&self) -> impl Iterator, &'a Package)> + 'a { + let packages = self + .compilation + .package_store + .iter() + .rev() + .filter_map(|(id, unit)| { + if self.compilation.user_package_id == id { + return Some((true, None, &unit.package)); + } + self.compilation + .dependencies + .get(&id) + .map(|alias| (false, alias.as_ref().map(AsRef::as_ref), &unit.package)) + }); + once(( + false, + None, + &self + .compilation + .package_store + .get(PackageId::CORE) + .expect("core package must exist") + .package, + )) + .chain(packages) + } + + /// Whether an exact import exists for the given path, along with its alias if one exists. + fn exact_import_exists(&self, path: &[Rc]) -> (bool, Option>) { + let exact_import = self.imports.iter().find_map(|import_item| { + if import_item.is_glob { + return None; + } + + if import_item.path == path { + Some(import_item.alias.clone()) + } else { + None + } + }); + (exact_import.is_some(), exact_import.unwrap_or_default()) + } + + /// An item is "relevant" if it's a callable or UDT that's visible to the user package. + fn is_item_relevant( + package: &'a qsc::hir::Package, + item: &'a qsc::hir::Item, + include_callables: bool, + include_udts: bool, + is_user_package: bool, + ) -> Option> { + // We only want items whose parents are namespaces + if let Some(item_id) = item.parent { + if let Some(parent) = package.items.get(item_id) { + if let ItemKind::Namespace(namespace, _) = &parent.kind { + // filter out internal packages that are not from the user's + // compilation + if matches!(item.visibility, Visibility::Internal) && !is_user_package { + return None; // ignore item if not in the user's package + } + + return match &item.kind { + ItemKind::Callable(callable_decl) if include_callables => { + Some(RelevantItem { + name: callable_decl.name.name.clone(), + namespace, + kind: RelevantItemKind::Callable(callable_decl), + }) + } + ItemKind::Ty(_, udt) if include_udts => Some(RelevantItem { + name: udt.name.clone(), + namespace, + kind: RelevantItemKind::Udt(udt), + }), + _ => None, + }; + } + } + } + None + } + + /// For a given item, produces any auto-imports, prefixes or aliases that would + /// make that item a valid completion in the current scope. + fn import_info(&self, item: &RelevantItem<'a>, package_alias: Option<&str>) -> ImportInfo { + let namespace_without_pkg_alias = Into::>::into(item.namespace); + let mut namespace = namespace_without_pkg_alias.clone(); + if let Some(package_alias) = package_alias { + namespace.insert(0, package_alias.into()); + } + + // Is there a glob import for the namespace, i.e. is the name already in scope? + let glob_import = self + .imports + .iter() + .any(|import_item| import_item.path == namespace && import_item.is_glob); + + if glob_import { + return ImportInfo::InScope; + } + + // An exact import is an import that matches the namespace and item name exactly + let (exact_import, item_alias) = + self.exact_import_exists(&[namespace.as_slice(), &[item.name.clone()]].concat()); + + if exact_import { + if let Some(alias) = item_alias { + return ImportInfo::Alias(alias); + } + return ImportInfo::InScope; + } + + // Does an alias for the namespace exist? + let namespace_alias = self.exact_import_exists(&namespace).1; + + if let Some(namespace_alias) = namespace_alias { + return ImportInfo::InAliasNamespace(namespace_alias); + } + + // If there are no existing exact or glob imports of the item, + // no open aliases for the namespace it's in, + // and we are not in the same namespace as the item, + // we need to add an import for it. + ImportInfo::NeedAutoImport(fully_qualify_name( + package_alias, + &namespace_without_pkg_alias, + Some(&item.name), + )) + } + + /// Creates a completion list entry for the given item, including + /// any text edits that would bring the item into scope, if requested. + fn to_completion( + &self, + item: &RelevantItem<'a>, + import_info: ImportInfo, + text_edits: Option<&TextEditRange>, + ) -> Completion { + let display = CodeDisplay { + compilation: self.compilation, + }; + let (kind, display) = match &item.kind { + RelevantItemKind::Callable(callable_decl) => ( + CompletionItemKind::Function, + display.hir_callable_decl(callable_decl).to_string(), + ), + RelevantItemKind::Udt(udt) => ( + CompletionItemKind::Interface, + display.hir_udt(udt).to_string(), + ), + }; + + // Deprioritize names starting with "__" in the completion list + let mut sort_priority = u32::from(item.name.starts_with("__")); + + match import_info { + ImportInfo::InScope => Completion::with_text_edits( + item.name.to_string(), + kind, + Some(display), + None, + sort_priority, + ), + ImportInfo::NeedAutoImport(import_path) => { + // Deprioritize auto-import items + sort_priority += 1; + + let text_edits = text_edits.expect( + "a text edit range should have been provided if `in_scope_only` is false", + ); + // if there is no place to insert an import, then we can't add an import. + let edits = text_edits.insert_import_at.as_ref().map(|range| { + vec![TextEdit { + new_text: format!("import {};{}", import_path, &text_edits.indent), + range: *range, + }] + }); + + Completion::with_text_edits( + item.name.to_string(), + kind, + Some(display), + edits, + sort_priority, + ) + } + ImportInfo::InAliasNamespace(prefix) => Completion::with_text_edits( + format!("{}.{}", prefix, item.name), + kind, + Some(display), + None, + sort_priority, + ), + ImportInfo::Alias(alias) => Completion::with_text_edits( + alias.to_string(), + kind, + Some(display), + None, + sort_priority, + ), + } + } +} + +enum ImportInfo { + /// Item name is already in scope, no edits necessary. + InScope, + /// The path that we should add an auto-import for, if any. + NeedAutoImport(String), + /// The item name should be prefixed with the namespace alias. + /// + /// e.g. `Foo` will appear as `Bar.Foo` if it's under an open + /// namespace that is aliased as `Bar`. + InAliasNamespace(Rc), + /// The item was imported under an alias. + Alias(Rc), +} + +/// A callable or UDT that's visible to the user package. +enum RelevantItemKind<'a> { + Callable(&'a CallableDecl), + Udt(&'a Udt), +} + +/// A callable or UDT that's visible to the user package. +struct RelevantItem<'a> { + name: Rc, + kind: RelevantItemKind<'a>, + namespace: &'a Idents, +} + +/// Format an external fully qualified name. +/// This will prepend the package alias and remove `Main` if it is the first namespace. +fn fully_qualify_name( + package_alias: Option<&str>, + namespace: &[Rc], + name: Option<&str>, +) -> String { + let mut fully_qualified_name: Vec> = if let Some(alias) = package_alias { + vec![Rc::from(alias)] + } else { + vec![] + }; + + // if this comes from an external project's Main, then the path does not include Main + let item_comes_from_main_of_external_project = package_alias.is_some() + && namespace.len() == 1 + && namespace.first() == Some(&"Main".into()); + + // So, if it is _not_ from an external project's `Main`, we include the namespace in the fully + // qualified name. + if !(item_comes_from_main_of_external_project) { + fully_qualified_name.append(&mut namespace.to_vec()); + }; + + if let Some(name) = name { + fully_qualified_name.push(name.into()); + } + + fully_qualified_name.join(".") +} + +#[derive(Default)] +struct ImportFinder { + offset: u32, + // The available imports at the current location + imports: Vec, +} + +impl ImportFinder { + fn init(offset: u32, package: &AstPackage) -> Self { + let mut context = Self { + offset, + ..Self::default() + }; + context.visit_package(package); + + let mut prelude_ns_ids: Vec = PRELUDE + .iter() + .map(|ns| ImportItem { + path: ns.iter().map(|x| Rc::from(*x)).collect(), + alias: None, + is_glob: true, + }) + .collect(); + + // The PRELUDE namespaces are always implicitly opened. + context.imports.append(&mut prelude_ns_ids); + + context + } +} + +impl<'a> Visitor<'a> for ImportFinder { + fn visit_namespace(&mut self, namespace: &'a qsc::ast::Namespace) { + if namespace.span.contains(self.offset) { + // the current namespace is implicitly opened. + self.imports = vec![ImportItem { + path: namespace.name.rc_str_iter().cloned().collect(), + alias: None, + is_glob: true, + }]; + walk_namespace(self, namespace); + } + } + + fn visit_item(&mut self, item: &'a qsc::ast::Item) { + match &*item.kind { + qsc::ast::ItemKind::Open(PathKind::Ok(name), alias) => { + let open_as_import = ImportItem { + path: name.rc_str_iter().cloned().collect(), + alias: alias.as_ref().map(|x| x.name.clone()), + is_glob: alias.is_none(), + }; + self.imports.push(open_as_import); + } + qsc::ast::ItemKind::ImportOrExport(decl) => { + // if this is an import, populate self.imports + if decl.is_import() { + self.imports + .append(&mut ImportItem::from_import_or_export_item(decl)); + } + } + _ => (), + } + + if item.span.contains(self.offset) { + walk_item(self, item); + } + } + + fn visit_callable_decl(&mut self, decl: &'a qsc::ast::CallableDecl) { + if decl.span.contains(self.offset) { + // This span covers the body too, but the + // context will get overwritten by visit_block + // if the offset is inside the actual body + walk_callable_decl(self, decl); + } + } + + fn visit_block(&mut self, block: &'a qsc::ast::Block) { + if block.span.contains(self.offset) { + walk_block(self, block); + } + } +} + +#[derive(Debug)] +/// Used to represent pre-existing imports in the completion context +struct ImportItem { + path: Vec>, + alias: Option>, + is_glob: bool, +} + +impl ImportItem { + fn from_import_or_export_item(decl: &qsc::ast::ImportOrExportDecl) -> Vec { + if decl.is_export() { + return vec![]; + }; + let mut buf = Vec::with_capacity(decl.items.len()); + for item in &decl.items { + let PathKind::Ok(path) = &item.path else { + continue; + }; + let alias = item.alias.as_ref().map(|x| x.name.clone()); + let is_glob = item.is_glob; + + buf.push(ImportItem { + path: path.rc_str_iter().cloned().collect(), + alias, + is_glob, + }); + } + buf + } +} diff --git a/language_service/src/completion/locals.rs b/language_service/src/completion/locals.rs new file mode 100644 index 0000000000..abc9536422 --- /dev/null +++ b/language_service/src/completion/locals.rs @@ -0,0 +1,111 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use super::Completion; +use crate::{compilation::Compilation, protocol::CompletionItemKind}; +use qsc::{ + display::{CodeDisplay, Lookup}, + hir::ItemKind, + resolve::{Local, LocalKind}, +}; + +/// Provides the locals that are visible at the cursor offset +pub(super) struct Locals<'a> { + compilation: &'a Compilation, + offset: u32, +} + +impl Locals<'_> { + pub fn new(offset: u32, compilation: &Compilation) -> Locals { + Locals { + compilation, + offset, + } + } + + pub fn expr_names(&self) -> Vec { + Self::local_completions(self.compilation, self.offset, true, false) + } + + pub fn type_names(&self) -> Vec { + Self::local_completions(self.compilation, self.offset, false, true) + } + + fn local_completions( + compilation: &Compilation, + offset: u32, + include_terms: bool, + include_tys: bool, + ) -> Vec { + compilation + .user_unit() + .ast + .locals + .get_all_at_offset(offset) + .iter() + .filter_map(|candidate| { + Self::local_completion(candidate, compilation, include_terms, include_tys) + }) + .collect::>() + } + + /// Convert a local into a completion item + fn local_completion( + candidate: &Local, + compilation: &Compilation, + include_terms: bool, + include_tys: bool, + ) -> Option { + let display = CodeDisplay { compilation }; + let (kind, detail) = match &candidate.kind { + LocalKind::Item(item_id) => { + let item = compilation.resolve_item_relative_to_user_package(item_id); + let (detail, kind) = match &item.0.kind { + ItemKind::Callable(decl) => { + if !include_terms { + return None; + } + ( + Some(display.hir_callable_decl(decl).to_string()), + CompletionItemKind::Function, + ) + } + ItemKind::Namespace(_, _) => { + panic!("did not expect local namespace item") + } + ItemKind::Ty(_, udt) => { + if !include_terms && !include_tys { + return None; + } + ( + Some(display.hir_udt(udt).to_string()), + CompletionItemKind::Interface, + ) + } + // We don't want completions for items exported from the local scope + ItemKind::Export(_, _) => return None, + }; + (kind, detail) + } + LocalKind::Var(node_id) => { + if !include_terms { + return None; + } + let detail = Some(display.name_ty_id(&candidate.name, *node_id).to_string()); + (CompletionItemKind::Variable, detail) + } + LocalKind::TyParam(_) => { + if !include_tys { + return None; + } + (CompletionItemKind::TypeParameter, None) + } + }; + + Some(Completion::with_detail( + candidate.name.to_string(), + kind, + detail, + )) + } +} diff --git a/language_service/src/completion/path_context.rs b/language_service/src/completion/path_context.rs new file mode 100644 index 0000000000..41928e66ae --- /dev/null +++ b/language_service/src/completion/path_context.rs @@ -0,0 +1,265 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use qsc::{ + ast::{ + visit::{self, Visitor}, + Attr, Block, CallableDecl, Expr, ExprKind, FieldAssign, FieldDef, FunctorExpr, Ident, + Idents, Item, ItemKind, Namespace, Package, Pat, Path, QubitInit, SpecDecl, Stmt, + StructDecl, Ty, TyDef, TyKind, + }, + parse::completion::PathKind, +}; +use std::rc::Rc; + +/// Provides the qualifier and the expected name kind for the +/// incomplete path (e.g. `foo.bar.`) at the cursor offset. +/// +/// Methods may panic if the offset does not fall within an incomplete path. +#[derive(Debug)] +pub(super) struct IncompletePath<'a> { + qualifier: Option>, + context: Option, + offset: u32, +} + +impl<'a> IncompletePath<'a> { + pub fn init(offset: u32, package: &'a Package) -> Self { + let mut offset_visitor = OffsetVisitor { + offset, + visitor: IncompletePath { + offset, + context: None, + qualifier: None, + }, + }; + + offset_visitor.visit_package(package); + + offset_visitor.visitor + } +} + +impl<'a> Visitor<'a> for IncompletePath<'a> { + fn visit_item(&mut self, item: &'a Item) { + match &*item.kind { + ItemKind::Open(..) => self.context = Some(PathKind::Namespace), + ItemKind::ImportOrExport(decl) => { + self.context = Some(PathKind::Import); + for item in &decl.items { + if item.is_glob + && item.span.touches(self.offset) + && item + .alias + .as_ref() + .map_or(true, |a| !a.span.touches(self.offset)) + { + // Special case when the cursor falls *between* the + // `Path` and the glob asterisk, + // e.g. `foo.bar.|*` . In that case, the visitor + // will not visit the path since the cursor technically + // is not within the path. + self.visit_path_kind(&item.path); + } + } + } + _ => {} + } + } + + fn visit_ty(&mut self, ty: &Ty) { + if let TyKind::Path(..) = *ty.kind { + self.context = Some(PathKind::Ty); + } + } + + fn visit_expr(&mut self, expr: &Expr) { + if let ExprKind::Path(..) = *expr.kind { + self.context = Some(PathKind::Expr); + } else if let ExprKind::Struct(..) = *expr.kind { + self.context = Some(PathKind::Struct); + } + } + + fn visit_path_kind(&mut self, path: &'a qsc::ast::PathKind) { + self.qualifier = match path { + qsc::ast::PathKind::Ok(path) => Some(path.iter().collect()), + qsc::ast::PathKind::Err(Some(incomplete_path)) => { + Some(incomplete_path.segments.iter().collect()) + } + qsc::ast::PathKind::Err(None) => None, + }; + } +} + +impl IncompletePath<'_> { + pub fn context(&self) -> Option<(PathKind, Vec>)> { + let context = self.context?; + let qualifier = self.segments_before_offset(); + + if qualifier.is_empty() { + return None; + } + + Some((context, qualifier)) + } + + fn segments_before_offset(&self) -> Vec> { + self.qualifier + .iter() + .flatten() + .take_while(|i| i.span.hi < self.offset) + .map(|i| i.name.clone()) + .collect::>() + } +} + +/// A [`Visitor`] wrapper that only descends into a node +/// if the given offset falls within that node. +struct OffsetVisitor { + offset: u32, + visitor: T, +} + +impl<'a, T> Visitor<'a> for OffsetVisitor +where + T: Visitor<'a>, +{ + fn visit_namespace(&mut self, namespace: &'a Namespace) { + if namespace.span.touches(self.offset) { + self.visitor.visit_namespace(namespace); + visit::walk_namespace(self, namespace); + } + } + + fn visit_item(&mut self, item: &'a Item) { + if item.span.touches(self.offset) { + self.visitor.visit_item(item); + visit::walk_item(self, item); + } + } + + fn visit_attr(&mut self, attr: &'a Attr) { + if attr.span.touches(self.offset) { + self.visitor.visit_attr(attr); + visit::walk_attr(self, attr); + } + } + + fn visit_ty_def(&mut self, def: &'a TyDef) { + if def.span.touches(self.offset) { + self.visitor.visit_ty_def(def); + visit::walk_ty_def(self, def); + } + } + + fn visit_callable_decl(&mut self, decl: &'a CallableDecl) { + if decl.span.touches(self.offset) { + self.visitor.visit_callable_decl(decl); + visit::walk_callable_decl(self, decl); + } + } + + fn visit_struct_decl(&mut self, decl: &'a StructDecl) { + if decl.span.touches(self.offset) { + self.visitor.visit_struct_decl(decl); + visit::walk_struct_decl(self, decl); + } + } + + fn visit_field_def(&mut self, def: &'a FieldDef) { + if def.span.touches(self.offset) { + self.visitor.visit_field_def(def); + visit::walk_field_def(self, def); + } + } + + fn visit_spec_decl(&mut self, decl: &'a SpecDecl) { + if decl.span.touches(self.offset) { + self.visitor.visit_spec_decl(decl); + visit::walk_spec_decl(self, decl); + } + } + + fn visit_functor_expr(&mut self, expr: &'a FunctorExpr) { + if expr.span.touches(self.offset) { + self.visitor.visit_functor_expr(expr); + visit::walk_functor_expr(self, expr); + } + } + + fn visit_ty(&mut self, ty: &'a Ty) { + if ty.span.touches(self.offset) { + self.visitor.visit_ty(ty); + visit::walk_ty(self, ty); + } + } + + fn visit_block(&mut self, block: &'a Block) { + if block.span.touches(self.offset) { + self.visitor.visit_block(block); + visit::walk_block(self, block); + } + } + + fn visit_stmt(&mut self, stmt: &'a Stmt) { + if stmt.span.touches(self.offset) { + self.visitor.visit_stmt(stmt); + visit::walk_stmt(self, stmt); + } + } + + fn visit_expr(&mut self, expr: &'a Expr) { + if expr.span.touches(self.offset) { + self.visitor.visit_expr(expr); + visit::walk_expr(self, expr); + } + } + + fn visit_field_assign(&mut self, assign: &'a FieldAssign) { + if assign.span.touches(self.offset) { + self.visitor.visit_field_assign(assign); + visit::walk_field_assign(self, assign); + } + } + + fn visit_pat(&mut self, pat: &'a Pat) { + if pat.span.touches(self.offset) { + self.visitor.visit_pat(pat); + visit::walk_pat(self, pat); + } + } + + fn visit_qubit_init(&mut self, init: &'a QubitInit) { + if init.span.touches(self.offset) { + self.visitor.visit_qubit_init(init); + visit::walk_qubit_init(self, init); + } + } + + fn visit_path(&mut self, path: &'a Path) { + if path.span.touches(self.offset) { + self.visitor.visit_path(path); + visit::walk_path(self, path); + } + } + + fn visit_path_kind(&mut self, path: &'a qsc::ast::PathKind) { + let span = match path { + qsc::ast::PathKind::Ok(path) => &path.span, + qsc::ast::PathKind::Err(Some(incomplete_path)) => &incomplete_path.span, + qsc::ast::PathKind::Err(None) => return, + }; + + if span.touches(self.offset) { + self.visitor.visit_path_kind(path); + visit::walk_path_kind(self, path); + } + } + + fn visit_ident(&mut self, ident: &'a Ident) { + if ident.span.touches(self.offset) { + self.visitor.visit_ident(ident); + } + } +} diff --git a/language_service/src/completion/tests.rs b/language_service/src/completion/tests.rs index 18da0fb2e4..d0f7ceda7b 100644 --- a/language_service/src/completion/tests.rs +++ b/language_service/src/completion/tests.rs @@ -3,16 +3,16 @@ #![allow(clippy::needless_raw_string_hashes)] -use expect_test::{expect, Expect}; - use super::{get_completions, CompletionItem}; use crate::{ protocol::CompletionList, test_utils::{ - compile_notebook_with_markers, compile_project_with_markers, compile_with_markers, + compile_notebook_with_markers, compile_project_with_markers, + compile_with_dependency_with_markers, compile_with_markers, }, Encoding, }; +use expect_test::{expect, Expect}; use indoc::indoc; fn check(source_with_cursor: &str, completions_to_check: &[&str], expect: &Expect) { @@ -95,6 +95,34 @@ fn check_notebook( assert_no_duplicates(actual_completions); } +fn check_with_dependency( + source_with_cursor: &str, + dependency_alias: &str, + dependency_source: &str, + completions_to_check: &[&str], + expect: &Expect, +) { + let (compilation, cursor_uri, cursor_position, _) = compile_with_dependency_with_markers( + &[("", source_with_cursor)], + dependency_alias, + &[("", dependency_source)], + ); + let actual_completions = + get_completions(&compilation, &cursor_uri, cursor_position, Encoding::Utf8); + let checked_completions: Vec> = completions_to_check + .iter() + .map(|comp| { + actual_completions + .items + .iter() + .find(|item| item.label == **comp) + }) + .collect(); + + expect.assert_debug_eq(&checked_completions); + assert_no_duplicates(actual_completions); +} + fn assert_no_duplicates(mut actual_completions: CompletionList) { actual_completions .items @@ -129,7 +157,7 @@ fn ignore_unstable_namespace() { label: "FakeStdLib", kind: Module, sort_text: Some( - "1101FakeStdLib", + "0100FakeStdLib", ), detail: None, additional_text_edits: None, @@ -159,7 +187,59 @@ fn ignore_unstable_callable() { label: "Fake", kind: Function, sort_text: Some( - "0700Fake", + "0401Fake", + ), + detail: Some( + "operation Fake() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Fake;\n ", + range: Range { + start: Position { + line: 2, + column: 12, + }, + end: Position { + line: 2, + column: 12, + }, + }, + }, + ], + ), + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn ignore_internal_callable() { + check( + r#" + namespace Test { + internal operation Foo() : Unit {} + operation Bar() : Unit { + ↘ + } + } + + namespace Test { + internal operation Baz() : Unit {} + }"#, + &["Fake", "Foo", "Baz", "Hidden"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Fake", + kind: Function, + sort_text: Some( + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -183,6 +263,32 @@ fn ignore_unstable_callable() { ), }, ), + Some( + CompletionItem { + label: "Foo", + kind: Function, + sort_text: Some( + "0300Foo", + ), + detail: Some( + "operation Foo() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Baz", + kind: Function, + sort_text: Some( + "0300Baz", + ), + detail: Some( + "operation Baz() : Unit", + ), + additional_text_edits: None, + }, + ), None, ] "#]], @@ -207,7 +313,7 @@ fn in_block_contains_std_functions_from_open_namespace() { label: "Fake", kind: Function, sort_text: Some( - "0700Fake", + "0400Fake", ), detail: Some( "operation Fake() : Unit", @@ -220,7 +326,7 @@ fn in_block_contains_std_functions_from_open_namespace() { label: "FakeWithParam", kind: Function, sort_text: Some( - "0700FakeWithParam", + "0400FakeWithParam", ), detail: Some( "operation FakeWithParam(x : Int) : Unit", @@ -233,7 +339,7 @@ fn in_block_contains_std_functions_from_open_namespace() { label: "FakeCtlAdj", kind: Function, sort_text: Some( - "0700FakeCtlAdj", + "0400FakeCtlAdj", ), detail: Some( "operation FakeCtlAdj() : Unit is Adj + Ctl", @@ -264,7 +370,7 @@ fn in_block_contains_std_functions() { label: "Fake", kind: Function, sort_text: Some( - "0700Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -293,7 +399,7 @@ fn in_block_contains_std_functions() { label: "FakeWithParam", kind: Function, sort_text: Some( - "0700FakeWithParam", + "0401FakeWithParam", ), detail: Some( "operation FakeWithParam(x : Int) : Unit", @@ -322,7 +428,7 @@ fn in_block_contains_std_functions() { label: "FakeCtlAdj", kind: Function, sort_text: Some( - "0700FakeCtlAdj", + "0401FakeCtlAdj", ), detail: Some( "operation FakeCtlAdj() : Unit is Adj + Ctl", @@ -351,28 +457,66 @@ fn in_block_contains_std_functions() { ); } -#[ignore = "need to implement newtypes"] #[test] fn in_block_contains_newtypes() { check( r#" namespace Test { - newtype Custom; + newtype Custom = String; operation Foo() : Unit { - ↘ + let x: ↘ } }"#, &["Custom", "Udt"], &expect![[r#" [ - some_valid_completion, - some_valid_completion, + Some( + CompletionItem { + label: "Custom", + kind: Interface, + sort_text: Some( + "0400Custom", + ), + detail: Some( + "newtype Custom = String", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0501Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Udt;\n ", + range: Range { + start: Position { + line: 2, + column: 8, + }, + end: Position { + line: 2, + column: 8, + }, + }, + }, + ], + ), + }, + ), ] "#]], ); } -#[ignore = "need more error recovery in parser to narrow down context in parameter list"] #[test] fn types_only_in_signature() { check( @@ -391,7 +535,7 @@ fn types_only_in_signature() { label: "Int", kind: Interface, sort_text: Some( - "0102Int", + "0200Int", ), detail: None, additional_text_edits: None, @@ -402,7 +546,7 @@ fn types_only_in_signature() { label: "String", kind: Interface, sort_text: Some( - "0110String", + "0200String", ), detail: None, additional_text_edits: None, @@ -432,7 +576,7 @@ fn in_block_no_auto_open() { label: "Fake", kind: Function, sort_text: Some( - "0700Fake", + "0400Fake", ), detail: Some( "operation Fake() : Unit", @@ -463,7 +607,7 @@ fn in_block_with_alias() { label: "Alias.Fake", kind: Function, sort_text: Some( - "0700Alias.Fake", + "0400Alias.Fake", ), detail: Some( "operation Fake() : Unit", @@ -477,101 +621,75 @@ fn in_block_with_alias() { } #[test] -fn in_block_from_other_namespace() { +fn members_of_aliased_namespace() { check( indoc! {r#" namespace Test { - operation Bar() : Unit { - ↘ + open FakeStdLib as Alias; + operation Foo() : Unit { + Alias.↘ } - export Bar; - } - namespace Other { - operation Foo() : Unit {} - export Foo; }"#}, - &["Foo"], + &["Fake", "Alias.Fake", "Library", "Alias.Library"], &expect![[r#" [ Some( CompletionItem { - label: "Foo", + label: "Fake", kind: Function, sort_text: Some( - "0600Foo", + "0300Fake", ), detail: Some( - "operation Foo() : Unit", + "operation Fake() : Unit", ), - additional_text_edits: Some( - [ - TextEdit { - new_text: "import Other.Foo;\n ", - range: Range { - start: Position { - line: 1, - column: 4, - }, - end: Position { - line: 1, - column: 4, - }, - }, - }, - ], + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0600Library", ), + detail: None, + additional_text_edits: None, }, ), + None, ] "#]], ); } #[test] -fn auto_open_multiple_files() { - check_project( - &[ - ( - "foo.qs", - indoc! {r#"namespace Foo { operation FooOperation() : Unit {} export FooOperation; } - "#}, - ), - ( - "bar.qs", - indoc! {r#"namespace Bar { operation BarOperation() : Unit { ↘ } export BarOperation; } - "#}, - ), - ], - &["FooOperation"], +fn aliased_exact_import() { + check( + indoc! {r#" + namespace Test { + import FakeStdLib.Fake as Alias; + operation Foo() : Unit { + ↘ + } + }"#}, + &["Fake", "Alias.Fake", "Alias"], &expect![[r#" [ + None, + None, Some( CompletionItem { - label: "FooOperation", + label: "Alias", kind: Function, sort_text: Some( - "0600FooOperation", + "0400Alias", ), detail: Some( - "operation FooOperation() : Unit", - ), - additional_text_edits: Some( - [ - TextEdit { - new_text: "import Foo.FooOperation;\n ", - range: Range { - start: Position { - line: 0, - column: 16, - }, - end: Position { - line: 0, - column: 16, - }, - }, - }, - ], + "operation Fake() : Unit", ), + additional_text_edits: None, }, ), ] @@ -580,27 +698,29 @@ fn auto_open_multiple_files() { } #[test] -fn in_block_nested_op() { - check( - indoc! {r#" - namespace Test { - operation Bar() : Unit { - operation Foo() : Unit {} - ↘ - } - }"#}, - &["Foo"], +fn open_from_dependency() { + check_with_dependency( + r" + namespace Test { + open MyDep.Dependency; + operation Foo() : Unit { + ↘ + } + }", + "MyDep", + "namespace Dependency { operation Baz() : Unit {} export Baz; }", + &["Baz"], &expect![[r#" [ Some( CompletionItem { - label: "Foo", + label: "Baz", kind: Function, sort_text: Some( - "0100Foo", + "0400Baz", ), detail: Some( - "operation Foo() : Unit", + "operation Baz() : Unit", ), additional_text_edits: None, }, @@ -611,28 +731,295 @@ fn in_block_nested_op() { } #[test] -fn in_block_hidden_nested_op() { - check( - indoc! {r#" - namespace Test { - operation Baz() : Unit { - ↘ - } - operation Foo() : Unit { - operation Bar() : Unit {} +fn open_with_alias_from_dependency() { + check_with_dependency( + r" + namespace Test { + open MyDep.Dependency as Alias; + open SamePackage as Alias1; + operation Foo() : Unit { + ↘ + } } - }"#}, - &["Bar"], + namespace SamePackage { operation Bar() : Unit {} }", + "MyDep", + "namespace Dependency { operation Baz() : Unit {} export Baz; }", + &["Alias.Baz", "Baz", "Alias1.Bar", "Bar"], &expect![[r#" [ - None, - ] - "#]], - ); -} - -#[test] -fn in_namespace_contains_open() { + Some( + CompletionItem { + label: "Alias.Baz", + kind: Function, + sort_text: Some( + "0400Alias.Baz", + ), + detail: Some( + "operation Baz() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "Alias1.Bar", + kind: Function, + sort_text: Some( + "0300Alias1.Bar", + ), + detail: Some( + "operation Bar() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn import_ns_with_alias_from_dependency() { + check_with_dependency( + r" + namespace Test { + import MyDep.Dependency as Alias; + import SamePackage as Alias1; + operation Foo() : Unit { + ↘ + } + } + namespace SamePackage { operation Bar() : Unit {} }", + "MyDep", + "namespace Dependency { operation Baz() : Unit {} export Baz; }", + &["Alias.Baz", "Baz", "Alias1.Bar", "Bar"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Alias.Baz", + kind: Function, + sort_text: Some( + "0400Alias.Baz", + ), + detail: Some( + "operation Baz() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "Alias1.Bar", + kind: Function, + sort_text: Some( + "0300Alias1.Bar", + ), + detail: Some( + "operation Bar() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn exact_import_from_dependency() { + check_with_dependency( + r" + namespace Test { + import MyDep.Dependency.Baz; + operation Foo() : Unit { + ↘ + } + }", + "MyDep", + "namespace Dependency { operation Baz() : Unit {} export Baz; }", + &["Baz"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Baz", + kind: Function, + sort_text: Some( + "0400Baz", + ), + detail: Some( + "operation Baz() : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn in_block_from_other_namespace() { + check( + indoc! {r#" + namespace Test { + operation Bar() : Unit { + ↘ + } + export Bar; + } + namespace Other { + operation Foo() : Unit {} + export Foo; + }"#}, + &["Foo"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Foo", + kind: Function, + sort_text: Some( + "0301Foo", + ), + detail: Some( + "operation Foo() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import Other.Foo;\n ", + range: Range { + start: Position { + line: 1, + column: 4, + }, + end: Position { + line: 1, + column: 4, + }, + }, + }, + ], + ), + }, + ), + ] + "#]], + ); +} + +#[test] +fn auto_open_multiple_files() { + check_project( + &[ + ( + "foo.qs", + indoc! {r#"namespace Foo { operation FooOperation() : Unit {} export FooOperation; } + "#}, + ), + ( + "bar.qs", + indoc! {r#"namespace Bar { operation BarOperation() : Unit { ↘ } export BarOperation; } + "#}, + ), + ], + &["FooOperation"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "FooOperation", + kind: Function, + sort_text: Some( + "0301FooOperation", + ), + detail: Some( + "operation FooOperation() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import Foo.FooOperation;\n ", + range: Range { + start: Position { + line: 0, + column: 16, + }, + end: Position { + line: 0, + column: 16, + }, + }, + }, + ], + ), + }, + ), + ] + "#]], + ); +} + +#[test] +fn in_block_nested_op() { + check( + indoc! {r#" + namespace Test { + operation Bar() : Unit { + operation Foo() : Unit {} + ↘ + } + }"#}, + &["Foo"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Foo", + kind: Function, + sort_text: Some( + "0100Foo", + ), + detail: Some( + "operation Foo() : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn in_block_hidden_nested_op() { + check( + indoc! {r#" + namespace Test { + operation Baz() : Unit { + ↘ + } + operation Foo() : Unit { + operation Bar() : Unit {} + } + }"#}, + &["Bar"], + &expect![[r#" + [ + None, + ] + "#]], + ); +} + +#[test] +fn in_namespace_contains_open() { check( indoc! {r#" namespace Test { @@ -648,7 +1035,7 @@ fn in_namespace_contains_open() { label: "open", kind: Keyword, sort_text: Some( - "0102open", + "0000open", ), detail: None, additional_text_edits: None, @@ -674,7 +1061,7 @@ fn top_level_contains_namespace() { label: "namespace", kind: Keyword, sort_text: Some( - "0101namespace", + "0000namespace", ), detail: None, additional_text_edits: None, @@ -690,18 +1077,18 @@ fn attributes() { check( indoc! {r#" namespace Test { - ↘ + @↘ } "#}, - &["@EntryPoint()"], + &["EntryPoint"], &expect![[r#" [ Some( CompletionItem { - label: "@EntryPoint()", - kind: Property, + label: "EntryPoint", + kind: Interface, sort_text: Some( - "0201@EntryPoint()", + "0000EntryPoint", ), detail: None, additional_text_edits: None, @@ -729,7 +1116,7 @@ fn stdlib_udt() { label: "TakesUdt", kind: Function, sort_text: Some( - "0700TakesUdt", + "0401TakesUdt", ), detail: Some( "function TakesUdt(input : Udt) : Udt", @@ -775,7 +1162,7 @@ fn notebook_top_level() { label: "operation", kind: Keyword, sort_text: Some( - "0201operation", + "0000operation", ), detail: None, additional_text_edits: None, @@ -786,7 +1173,7 @@ fn notebook_top_level() { label: "namespace", kind: Keyword, sort_text: Some( - "1301namespace", + "0000namespace", ), detail: None, additional_text_edits: None, @@ -797,7 +1184,7 @@ fn notebook_top_level() { label: "let", kind: Keyword, sort_text: Some( - "0301let", + "0000let", ), detail: None, additional_text_edits: None, @@ -808,7 +1195,7 @@ fn notebook_top_level() { label: "Fake", kind: Function, sort_text: Some( - "0800Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -854,7 +1241,7 @@ fn notebook_top_level_global() { label: "Fake", kind: Function, sort_text: Some( - "0800Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -902,7 +1289,7 @@ fn notebook_top_level_namespace_already_open_for_global() { label: "Fake", kind: Function, sort_text: Some( - "0800Fake", + "0400Fake", ), detail: Some( "operation Fake() : Unit", @@ -933,7 +1320,7 @@ fn notebook_block() { label: "Fake", kind: Function, sort_text: Some( - "0700Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -962,7 +1349,7 @@ fn notebook_block() { label: "let", kind: Keyword, sort_text: Some( - "0201let", + "0000let", ), detail: None, additional_text_edits: None, @@ -1000,7 +1387,7 @@ fn notebook_auto_open_start_of_cell_empty() { label: "Fake", kind: Function, sort_text: Some( - "0800Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -1057,7 +1444,7 @@ fn notebook_auto_open_start_of_cell() { label: "Fake", kind: Function, sort_text: Some( - "0800Fake", + "0401Fake", ), detail: Some( "operation Fake() : Unit", @@ -1184,7 +1571,7 @@ fn type_params() { r#" namespace Test { operation Foo<'T>() : Unit { - ↘ + let x: ↘ } }"#, &["'T", "Bar"], @@ -1379,7 +1766,7 @@ fn dont_import_if_already_glob_imported() { label: "Foo", kind: Function, sort_text: Some( - "0600Foo", + "0300Foo", ), detail: Some( "operation Foo() : Unit", @@ -1392,7 +1779,7 @@ fn dont_import_if_already_glob_imported() { label: "Bar", kind: Function, sort_text: Some( - "0600Bar", + "0300Bar", ), detail: Some( "operation Bar() : Unit", @@ -1432,7 +1819,7 @@ fn glob_import_item_with_same_name() { label: "Bar", kind: Function, sort_text: Some( - "0600Bar", + "0301Bar", ), detail: Some( "operation Bar() : Unit", @@ -1486,7 +1873,7 @@ fn dont_import_if_already_directly_imported() { label: "Foo", kind: Function, sort_text: Some( - "0100Foo", + "0300Foo", ), detail: Some( "operation Foo() : Unit", @@ -1499,7 +1886,7 @@ fn dont_import_if_already_directly_imported() { label: "Bar", kind: Function, sort_text: Some( - "0600Bar", + "0301Bar", ), detail: Some( "operation Bar() : Unit", @@ -1545,7 +1932,7 @@ fn auto_import_from_qir_runtime() { label: "AllocateQubitArray", kind: Function, sort_text: Some( - "0800AllocateQubitArray", + "0201AllocateQubitArray", ), detail: Some( "operation AllocateQubitArray(size : Int) : Qubit[]", @@ -1592,7 +1979,7 @@ fn dont_generate_import_for_core_prelude() { label: "Length", kind: Function, sort_text: Some( - "0800Length", + "0200Length", ), detail: Some( "function Length<'T>(a : 'T[]) : Int", @@ -1624,7 +2011,7 @@ fn dont_generate_import_for_stdlib_prelude() { label: "MResetZ", kind: Function, sort_text: Some( - "0700MResetZ", + "0400MResetZ", ), detail: Some( "operation MResetZ(target : Qubit) : Result", @@ -1655,7 +2042,7 @@ fn callable_from_same_file() { label: "MyCallable", kind: Function, sort_text: Some( - "0600MyCallable", + "0300MyCallable", ), detail: Some( "function MyCallable() : Unit", @@ -1667,3 +2054,1459 @@ fn callable_from_same_file() { "#]], ); } + +#[test] +fn member_completion() { + check( + r#" + namespace Test { + function MyCallable() : Unit {} + } + + namespace Main { + operation Main() : Unit { + Test.↘ + } + } + + "#, + &["MyCallable"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "MyCallable", + kind: Function, + sort_text: Some( + "0200MyCallable", + ), + detail: Some( + "function MyCallable() : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn member_completion_in_imported_namespace() { + check( + r#" + namespace Test.Foo { + function MyCallable() : Unit {} + } + + namespace Test.Foo.Bar { + function CallableInBar() : Unit {} + } + + namespace Main { + open Test; + operation Main() : Unit { + Foo.↘ + } + } + + "#, + &["MyCallable", "Bar"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "MyCallable", + kind: Function, + sort_text: Some( + "0200MyCallable", + ), + detail: Some( + "function MyCallable() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Bar", + kind: Module, + sort_text: Some( + "0500Bar", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn namespace_completion() { + check( + r#" + namespace Test.Foo { + function MyCallable() : Unit {} + } + + namespace Main { + operation Main() : Unit { + Test.↘ + } + } + + "#, + &["Foo"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Foo", + kind: Module, + sort_text: Some( + "0500Foo", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn nested_namespace() { + check( + r#" + namespace Test.Foo { + function MyCallable() : Unit {} + } + + namespace Test { + function MyCallable2() : Unit { + Foo.↘ + } + }"#, + &["MyCallable", "MyCallable2"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "MyCallable", + kind: Function, + sort_text: Some( + "0200MyCallable", + ), + detail: Some( + "function MyCallable() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn std_member() { + check( + r#" + namespace Test { + function MyCallable2() : Unit { + FakeStdLib.↘ + } + }"#, + &["Fake", "Library"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Fake", + kind: Function, + sort_text: Some( + "0300Fake", + ), + detail: Some( + "operation Fake() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0600Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn open_namespace() { + check( + r#" + namespace Test { + open FakeStdLib.↘; + }"#, + &["Fake", "Library"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0300Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn open_namespace_no_semi() { + check( + r#" + namespace Test { + open FakeStdLib.↘ + }"#, + &["Fake", "Library"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0300Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn open_namespace_no_semi_followed_by_decl() { + check( + r#" + namespace Test { + open FakeStdLib.↘ + operation Foo() : Unit {} + }"#, + &["Fake", "Library"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0300Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn open_namespace_partial_path_part() { + check( + r#" + namespace Test { + open FakeStdLib.↘F + operation Foo() : Unit {} + }"#, + &["Fake", "Library"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0300Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn let_stmt_type() { + check( + r#" + namespace Test { + function Main() : Unit { + let x: ↘ + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0501Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Udt;\n ", + range: Range { + start: Position { + line: 2, + column: 12, + }, + end: Position { + line: 2, + column: 12, + }, + }, + }, + ], + ), + }, + ), + Some( + CompletionItem { + label: "Qubit", + kind: Interface, + sort_text: Some( + "0200Qubit", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Int", + kind: Interface, + sort_text: Some( + "0200Int", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + None, + ] + "#]], + ); +} + +#[test] +fn let_stmt_type_before_next_stmt() { + check( + r#" + namespace Test { + function Main() : Unit { + use q = Qubit(); + let x: ↘ + H(q); + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0501Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Udt;\n ", + range: Range { + start: Position { + line: 2, + column: 12, + }, + end: Position { + line: 2, + column: 12, + }, + }, + }, + ], + ), + }, + ), + Some( + CompletionItem { + label: "Qubit", + kind: Interface, + sort_text: Some( + "0200Qubit", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Int", + kind: Interface, + sort_text: Some( + "0200Int", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + None, + ] + "#]], + ); +} + +#[test] +fn type_position_namespace() { + check( + r#" + namespace Test { + function Main() : Unit { + let x: FakeStdLib.↘ ; + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + None, + None, + None, + None, + ] + "#]], + ); +} + +#[test] +fn udt_base_type_part() { + check( + r#" + namespace Test { + newtype Foo = FakeStdLib.↘ + }"#, + &["Udt", "Qubit", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + None, + None, + ] + "#]], + ); +} + +#[test] +fn struct_init() { + check( + r#" + namespace Test { + function Main() : Unit { + let x = new ↘ ; + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0301Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import FakeStdLib.Udt;\n ", + range: Range { + start: Position { + line: 2, + column: 12, + }, + end: Position { + line: 2, + column: 12, + }, + }, + }, + ], + ), + }, + ), + None, + None, + None, + None, + ] + "#]], + ); +} + +#[test] +fn struct_init_path_part() { + check( + r#" + namespace Test { + function Main() : Unit { + let x = new FakeStdLib.↘ ; + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + None, + None, + None, + None, + ] + "#]], + ); +} + +#[test] +fn struct_init_path_part_in_field_assigment() { + check( + r#" + namespace Test { + function Main() : Unit { + let x = new FakeStdLib.Udt { x = FakeStdLib.↘ } ; + } + }"#, + &["Udt", "Qubit", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "FakeWithParam", + kind: Function, + sort_text: Some( + "0300FakeWithParam", + ), + detail: Some( + "operation FakeWithParam(x : Int) : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn export_path() { + check( + r#" + namespace Test { + export ↘ ; + function Main() : Unit { + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam", "FakeStdLib"], + &expect![[r#" + [ + None, + None, + None, + Some( + CompletionItem { + label: "Main", + kind: Function, + sort_text: Some( + "0200Main", + ), + detail: Some( + "function Main() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "FakeStdLib", + kind: Module, + sort_text: Some( + "0400FakeStdLib", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn export_path_part() { + check( + r#" + namespace Test { + export FakeStdLib.↘ ; + function Main() : Unit { + } + }"#, + &["Udt", "Qubit", "Int", "Main", "FakeWithParam", "FakeStdLib"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + None, + None, + None, + Some( + CompletionItem { + label: "FakeWithParam", + kind: Function, + sort_text: Some( + "0300FakeWithParam", + ), + detail: Some( + "operation FakeWithParam(x : Int) : Unit", + ), + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn partially_typed_name() { + check( + r#" + namespace Test { + export Fo↘ + function Foo() : Unit { + } + }"#, + &["Foo"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Foo", + kind: Function, + sort_text: Some( + "0200Foo", + ), + detail: Some( + "function Foo() : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn from_dependency_main() { + check_with_dependency( + "namespace Test { function Foo() : Unit { ↘ } }", + "MyDep", + "namespace Main { export MainFunc; function MainFunc() : Unit {} } + namespace Other { export OtherFunc; function OtherFunc() : Unit {} } + ", + &["MainFunc", "OtherFunc"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "MainFunc", + kind: Function, + sort_text: Some( + "0401MainFunc", + ), + detail: Some( + "function MainFunc() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import MyDep.MainFunc;\n ", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 17, + }, + }, + }, + ], + ), + }, + ), + Some( + CompletionItem { + label: "OtherFunc", + kind: Function, + sort_text: Some( + "0401OtherFunc", + ), + detail: Some( + "function OtherFunc() : Unit", + ), + additional_text_edits: Some( + [ + TextEdit { + new_text: "import MyDep.Other.OtherFunc;\n ", + range: Range { + start: Position { + line: 0, + column: 17, + }, + end: Position { + line: 0, + column: 17, + }, + }, + }, + ], + ), + }, + ), + ] + "#]], + ); +} + +#[test] +fn package_aliases() { + check_with_dependency( + "namespace Test { function Foo() : Unit { ↘ } }", + "MyDep", + "namespace Main { export MainFunc; function MainFunc() : Unit {} }", + &["MyDep", "Main"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "MyDep", + kind: Module, + sort_text: Some( + "0600MyDep", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn package_alias_members() { + check_with_dependency( + "namespace Test { function Foo() : Unit { MyDep.↘ } }", + "MyDep", + "namespace Main { export MainFunc; function MainFunc() : Unit {} } + namespace Other { export OtherFunc; function OtherFunc() : Unit {} } + namespace Other.Sub { export OtherFunc; function OtherFunc() : Unit {} } + ", + &["Main", "Other", "MainFunc", "Other.Sub", "Sub"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Other", + kind: Module, + sort_text: Some( + "0700Other", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "MainFunc", + kind: Function, + sort_text: Some( + "0300MainFunc", + ), + detail: Some( + "function MainFunc() : Unit", + ), + additional_text_edits: None, + }, + ), + None, + None, + ] + "#]], + ); +} + +#[test] +fn dependency_namespace_members() { + check_with_dependency( + "namespace Test { function Foo() : Unit { MyDep.Other.↘ } }", + "MyDep", + "namespace Main { export MainFunc; function MainFunc() : Unit {} } + namespace Other { export OtherFunc; function OtherFunc() : Unit {} } + namespace Other.Sub { export OtherFunc; function OtherFunc() : Unit {} } + ", + &["Main", "Other", "MainFunc", "Other.Sub", "Sub", "OtherFunc"], + &expect![[r#" + [ + None, + None, + None, + None, + Some( + CompletionItem { + label: "Sub", + kind: Module, + sort_text: Some( + "0700Sub", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "OtherFunc", + kind: Function, + sort_text: Some( + "0300OtherFunc", + ), + detail: Some( + "function OtherFunc() : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn package_alias_members_in_open() { + check_with_dependency( + "namespace Test { open MyDep.↘ }", + "MyDep", + "namespace Main { export MainFunc; function MainFunc() : Unit {} } + namespace Other { export OtherFunc; function OtherFunc() : Unit {} } + namespace Other.Sub { export OtherFunc; function OtherFunc() : Unit {} } + ", + &["Main", "Other", "MainFunc", "Other.Sub", "Sub"], + &expect![[r#" + [ + None, + Some( + CompletionItem { + label: "Other", + kind: Module, + sort_text: Some( + "0300Other", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + None, + None, + ] + "#]], + ); +} + +#[test] +fn member_completion_in_imported_namespace_from_dependency() { + check_with_dependency( + "namespace Main { + open MyDep.Test; + operation Main() : Unit { + Foo.↘ + } + }", + "MyDep", + " + namespace Test.Foo { + function CallableInFoo() : Unit {} + export CallableInFoo; + } + + namespace Test.Foo.Bar { + function CallableInBar() : Unit {} + export CallableInBar; + } + ", + &["CallableInFoo", "Bar"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "CallableInFoo", + kind: Function, + sort_text: Some( + "0300CallableInFoo", + ), + detail: Some( + "function CallableInFoo() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Bar", + kind: Module, + sort_text: Some( + "0700Bar", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn aliased_namespace_in_dependency() { + check_with_dependency( + "namespace Main { + open MyDep.Test.Foo as Alias; + operation Main() : Unit { + Alias.↘ + } + }", + "MyDep", + " + namespace Test.Foo { + function CallableInFoo() : Unit {} + export CallableInFoo; + } + + namespace Test.Foo.Bar { + function CallableInBar() : Unit {} + export CallableInBar; + } + ", + &["CallableInFoo", "Bar"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "CallableInFoo", + kind: Function, + sort_text: Some( + "0300CallableInFoo", + ), + detail: Some( + "function CallableInFoo() : Unit", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Bar", + kind: Module, + sort_text: Some( + "0700Bar", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn open_does_not_match_pkg_alias() { + check_with_dependency( + "namespace Main { + open Test.Foo as Alias; + operation Main() : Unit { + Alias.↘ + } + }", + "MyDep", + " + namespace Test.Foo { + function CallableInFoo() : Unit {} + export CallableInFoo; + } + + namespace Test.Foo.Bar { + function CallableInBar() : Unit {} + export CallableInBar; + } + ", + &["CallableInFoo", "Bar"], + &expect![[r#" + [ + None, + None, + ] + "#]], + ); +} + +#[test] +fn input_type_missing() { + check( + "namespace Test { function Foo(x : FakeStdLib.↘ ) : Unit { body intrinsic; } }", + &["Udt", "Library"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0600Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn notebook_top_level_path_part() { + check_notebook( + &[( + "cell1", + (" + FakeStdLib.↘ + "), + )], + &["Udt", "Library", "FakeStdLib", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0600Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + Some( + CompletionItem { + label: "FakeWithParam", + kind: Function, + sort_text: Some( + "0300FakeWithParam", + ), + detail: Some( + "operation FakeWithParam(x : Int) : Unit", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn notebook_top_level_path_part_in_type() { + check_notebook( + &[( + "cell1", + (" + let x : FakeStdLib.↘ + "), + )], + &["Udt", "Library", "FakeStdLib", "FakeWithParam"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "Udt", + kind: Interface, + sort_text: Some( + "0300Udt", + ), + detail: Some( + "struct Udt { x : Int, y : Int }", + ), + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Library", + kind: Module, + sort_text: Some( + "0600Library", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + None, + ] + "#]], + ); +} + +#[test] +fn prefix_ops() { + check( + "namespace Test { function Main() : Unit { let x = ↘ ; } }", + &["and", "or", "not", "Adjoint"], + &expect![[r#" + [ + None, + None, + Some( + CompletionItem { + label: "not", + kind: Keyword, + sort_text: Some( + "0000not", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "Adjoint", + kind: Keyword, + sort_text: Some( + "0000Adjoint", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn binary_ops() { + check( + "namespace Test { function Main() : Unit { let x = 1 ↘ ; } }", + &["and", "or", "not"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "and", + kind: Keyword, + sort_text: Some( + "0000and", + ), + detail: None, + additional_text_edits: None, + }, + ), + Some( + CompletionItem { + label: "or", + kind: Keyword, + sort_text: Some( + "0000or", + ), + detail: None, + additional_text_edits: None, + }, + ), + None, + ] + "#]], + ); +} + +#[test] +fn array_size() { + check( + "namespace Test { function Main() : Unit { let x = [0, ↘] ; } }", + &["size"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "size", + kind: Keyword, + sort_text: Some( + "0000size", + ), + detail: None, + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn path_segment_partial_ident_is_keyword() { + check( + "namespace Test { import FakeStdLib.struct↘ }", + &["StructFn"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "StructFn", + kind: Interface, + sort_text: Some( + "0300StructFn", + ), + detail: Some( + "struct StructFn { inner : (Int -> Int) }", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn path_segment_followed_by_wslash() { + // `w/` is a single token, so it gets tricky + // to separate out the `w` and treat it as an identifier. + // We're just not going to worry about doing anything clever here. + check( + "namespace Test { import FakeStdLib.w↘/ }", + &["StructFn"], + &expect![[r#" + [ + None, + ] + "#]], + ); +} + +#[test] +fn path_segment_followed_by_op_token() { + // Invoking in the middle of a multi-character op token + // shouldn't break anything. + check( + "namespace Test { import FakeStdLib.<↘<< }", + &["StructFn"], + &expect![[r#" + [ + None, + ] + "#]], + ); +} + +#[test] +fn path_segment_before_glob() { + check( + "namespace Test { import FakeStdLib.↘* }", + &["StructFn"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "StructFn", + kind: Interface, + sort_text: Some( + "0300StructFn", + ), + detail: Some( + "struct StructFn { inner : (Int -> Int) }", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} + +#[test] +fn path_segment_before_glob_with_alias() { + check( + "namespace Test { import FakeStdLib.↘* as Alias }", + &["StructFn"], + &expect![[r#" + [ + Some( + CompletionItem { + label: "StructFn", + kind: Interface, + sort_text: Some( + "0300StructFn", + ), + detail: Some( + "struct StructFn { inner : (Int -> Int) }", + ), + additional_text_edits: None, + }, + ), + ] + "#]], + ); +} diff --git a/language_service/src/completion/text_edits.rs b/language_service/src/completion/text_edits.rs new file mode 100644 index 0000000000..f21230c586 --- /dev/null +++ b/language_service/src/completion/text_edits.rs @@ -0,0 +1,131 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +use crate::{ + compilation::{Compilation, CompilationKind}, + qsc_utils::into_range, +}; +use qsc::{ + ast::visit::{walk_namespace, Visitor}, + line_column::{Encoding, Range}, +}; + +/// Provides information about where auto-imports should be inserted +/// in the document based on the cursor offset. +pub(super) struct TextEditRange { + /// Location to insert any auto-import text edits at. + pub insert_import_at: Option, + /// The indentation level for the auto-import text edits. + pub indent: String, +} + +impl TextEditRange { + pub fn init(offset: u32, compilation: &Compilation, position_encoding: Encoding) -> Self { + let mut finder = StartOfNamespace { + offset, + start_of_namespace: None, + }; + finder.visit_package(&compilation.user_unit().ast.package); + + let insert_open_at = match compilation.kind { + CompilationKind::OpenProject { .. } => finder.start_of_namespace, + // Since notebooks don't typically contain namespace declarations, + // open statements should just get before the first non-whitespace + // character (i.e. at the top of the cell) + CompilationKind::Notebook { .. } => Some(Self::get_first_non_whitespace_in_source( + compilation, + offset, + )), + }; + + let indent = match insert_open_at { + Some(start) => Self::get_indent(compilation, start), + None => String::new(), + }; + + let insert_open_range = insert_open_at.map(|o| { + into_range( + position_encoding, + qsc::Span { lo: o, hi: o }, + &compilation.user_unit().sources, + ) + }); + + TextEditRange { + insert_import_at: insert_open_range, + indent, + } + } + + fn get_first_non_whitespace_in_source(compilation: &Compilation, package_offset: u32) -> u32 { + const QSHARP_MAGIC: &str = "//qsharp"; + let source = compilation + .user_unit() + .sources + .find_by_offset(package_offset) + .expect("source should exist in the user source map"); + + // Skip the //qsharp magic if it exists (notebook cells) + let start = if let Some(qsharp_magic_start) = source.contents.find(QSHARP_MAGIC) { + qsharp_magic_start + QSHARP_MAGIC.len() + } else { + 0 + }; + + let source_after_magic = &source.contents[start..]; + + let first = start + + source_after_magic + .find(|c: char| !c.is_whitespace()) + .unwrap_or(source_after_magic.len()); + + let first = u32::try_from(first).expect("source length should fit into u32"); + + source.offset + first + } + + fn get_indent(compilation: &Compilation, package_offset: u32) -> String { + let source = compilation + .user_unit() + .sources + .find_by_offset(package_offset) + .expect("source should exist in the user source map"); + let source_offset = (package_offset - source.offset) + .try_into() + .expect("offset can't be converted to uszie"); + let before_offset = &source.contents[..source_offset]; + let mut indent = match before_offset.rfind(['{', '\n']) { + Some(begin) => { + let indent = &before_offset[begin..]; + indent.strip_prefix('{').unwrap_or(indent) + } + None => before_offset, + } + .to_string(); + if !indent.starts_with('\n') { + indent.insert(0, '\n'); + } + indent + } +} + +/// Find the start of the namespace that contains the offset. +struct StartOfNamespace { + offset: u32, + start_of_namespace: Option, +} + +impl<'a> Visitor<'a> for StartOfNamespace { + fn visit_namespace(&mut self, namespace: &'a qsc::ast::Namespace) { + if namespace.span.contains(self.offset) { + self.start_of_namespace = None; + walk_namespace(self, namespace); + } + } + + fn visit_item(&mut self, item: &'a qsc::ast::Item) { + if self.start_of_namespace.is_none() { + self.start_of_namespace = Some(item.span.lo); + } + } +} diff --git a/language_service/src/definition/tests.rs b/language_service/src/definition/tests.rs index 704f1535bb..38dc60c6d6 100644 --- a/language_service/src/definition/tests.rs +++ b/language_service/src/definition/tests.rs @@ -478,12 +478,12 @@ fn std_call() { source: "qsharp-library-source:", range: Range { start: Position { - line: 1, - column: 26, + line: 2, + column: 18, }, end: Position { - line: 1, - column: 30, + line: 2, + column: 22, }, }, }, @@ -586,12 +586,12 @@ fn std_udt() { source: "qsharp-library-source:", range: Range { start: Position { - line: 4, - column: 24, + line: 5, + column: 16, }, end: Position { - line: 4, - column: 27, + line: 5, + column: 19, }, }, }, @@ -618,12 +618,12 @@ fn std_udt_udt_field() { source: "qsharp-library-source:", range: Range { start: Position { - line: 4, - column: 31, + line: 5, + column: 23, }, end: Position { - line: 4, - column: 32, + line: 5, + column: 24, }, }, }, diff --git a/language_service/src/name_locator.rs b/language_service/src/name_locator.rs index 56e7df6011..371cafc9c2 100644 --- a/language_service/src/name_locator.rs +++ b/language_service/src/name_locator.rs @@ -6,6 +6,7 @@ use std::rc::Rc; use crate::compilation::Compilation; use qsc::ast::visit::{walk_expr, walk_namespace, walk_pat, walk_ty, walk_ty_def, Visitor}; +use qsc::ast::{Idents, PathKind}; use qsc::display::Lookup; use qsc::{ast, hir, resolve}; @@ -167,7 +168,7 @@ impl<'inner, 'package, T: Handler<'package>> Locator<'inner, 'package, T> { impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inner, 'package, T> { fn visit_namespace(&mut self, namespace: &'package ast::Namespace) { if namespace.span.contains(self.offset) { - self.context.current_namespace = namespace.name.name(); + self.context.current_namespace = namespace.name.full_name(); walk_namespace(self, namespace); } } @@ -348,7 +349,7 @@ impl<'inner, 'package, T: Handler<'package>> Visitor<'package> for Locator<'inne } } } - ast::ExprKind::Struct(ty_name, copy, fields) => { + ast::ExprKind::Struct(PathKind::Ok(ty_name), copy, fields) => { if ty_name.span.touches(self.offset) { self.visit_path(ty_name); return; diff --git a/language_service/src/references.rs b/language_service/src/references.rs index 221bfd1124..23c3dfe822 100644 --- a/language_service/src/references.rs +++ b/language_service/src/references.rs @@ -10,6 +10,7 @@ use crate::compilation::Compilation; use crate::name_locator::{Handler, Locator, LocatorContext}; use crate::qsc_utils::into_location; use qsc::ast::visit::{walk_callable_decl, walk_expr, walk_ty, Visitor}; +use qsc::ast::PathKind; use qsc::display::Lookup; use qsc::hir::ty::Ty; use qsc::hir::{PackageId, Res}; @@ -346,7 +347,7 @@ impl<'a> Visitor<'_> for FindItemRefs<'a> { } fn visit_ty(&mut self, ty: &ast::Ty) { - if let ast::TyKind::Path(ty_path) = &*ty.kind { + if let ast::TyKind::Path(PathKind::Ok(ty_path)) = &*ty.kind { let res = self.compilation.get_res(ty_path.id); if let Some(resolve::Res::Item(item_id, _)) = res { if self.eq(item_id) { @@ -409,7 +410,7 @@ impl<'a> Visitor<'_> for FindFieldRefs<'a> { } } } - ast::ExprKind::Struct(struct_name, copy, fields) => { + ast::ExprKind::Struct(PathKind::Ok(struct_name), copy, fields) => { self.visit_path(struct_name); if let Some(copy) = copy { self.visit_expr(copy); diff --git a/language_service/src/references/tests.rs b/language_service/src/references/tests.rs index 9a9d28be5b..2fa1fe1a8e 100644 --- a/language_service/src/references/tests.rs +++ b/language_service/src/references/tests.rs @@ -93,12 +93,12 @@ fn std_callable_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 1, - column: 26, + line: 2, + column: 18, }, end: Position { - line: 1, - column: 30, + line: 2, + column: 22, }, }, }, @@ -250,12 +250,12 @@ fn std_udt_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 4, - column: 24, + line: 5, + column: 16, }, end: Position { - line: 4, - column: 27, + line: 5, + column: 19, }, }, }, @@ -340,12 +340,12 @@ fn std_field_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 4, - column: 31, + line: 5, + column: 23, }, end: Position { - line: 4, - column: 32, + line: 5, + column: 24, }, }, }, @@ -446,12 +446,12 @@ fn std_struct_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 16, - column: 23, + line: 17, + column: 15, }, end: Position { - line: 16, - column: 33, + line: 17, + column: 25, }, }, }, @@ -551,12 +551,12 @@ fn std_struct_field_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 16, - column: 36, + line: 17, + column: 28, }, end: Position { - line: 16, - column: 37, + line: 17, + column: 29, }, }, }, @@ -660,12 +660,12 @@ fn std_struct_field_path_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 16, - column: 36, + line: 17, + column: 28, }, end: Position { - line: 16, - column: 37, + line: 17, + column: 29, }, }, }, @@ -718,12 +718,12 @@ fn std_struct_field_path_with_expr_ref() { source: "qsharp-library-source:", range: Range { start: Position { - line: 16, - column: 36, + line: 17, + column: 28, }, end: Position { - line: 16, - column: 37, + line: 17, + column: 29, }, }, }, diff --git a/language_service/src/signature_help.rs b/language_service/src/signature_help.rs index f1eb2e994f..2eb39ecf33 100644 --- a/language_service/src/signature_help.rs +++ b/language_service/src/signature_help.rs @@ -13,6 +13,7 @@ use qsc::{ ast::{ self, visit::{walk_expr, walk_item, Visitor}, + PathKind, }, display::{parse_doc_for_param, parse_doc_for_summary, CodeDisplay, Lookup}, hir, @@ -321,7 +322,7 @@ fn try_get_direct_callee<'a>( compilation: &'a Compilation, callee: &ast::Expr, ) -> Option<(hir::PackageId, &'a hir::CallableDecl, &'a str)> { - if let ast::ExprKind::Path(path) = &*callee.kind { + if let ast::ExprKind::Path(PathKind::Ok(path)) = &*callee.kind { if let Some(resolve::Res::Item(item_id, _)) = compilation.get_res(path.id) { let (item, _, resolved_item_id) = compilation.resolve_item_relative_to_user_package(item_id); diff --git a/language_service/src/state/tests.rs b/language_service/src/state/tests.rs index 53acec1f36..2cf949e2c0 100644 --- a/language_service/src/state/tests.rs +++ b/language_service/src/state/tests.rs @@ -185,21 +185,6 @@ async fn close_last_doc_in_project() { ), ), ), - Frontend( - Error( - Parse( - Error( - ExpectedItem( - Eof, - Span { - lo: 59, - hi: 140, - }, - ), - ), - ), - ), - ), ], [], ), @@ -324,21 +309,6 @@ async fn compile_error() { ), ), ), - Frontend( - Error( - Parse( - Error( - ExpectedItem( - Eof, - Span { - lo: 0, - hi: 9, - }, - ), - ), - ), - ), - ), ], [], ), @@ -1408,21 +1378,6 @@ async fn close_doc_prioritizes_fs() { ), ), ), - Frontend( - Error( - Parse( - Error( - ExpectedItem( - Eof, - Span { - lo: 59, - hi: 140, - }, - ), - ), - ), - ), - ), ], [], ), @@ -2180,21 +2135,6 @@ async fn error_from_dependency_reported() { ), ), ), - Frontend( - Error( - Parse( - Error( - ExpectedItem( - Eof, - Span { - lo: 0, - hi: 13, - }, - ), - ), - ), - ), - ), ], [], ), diff --git a/language_service/src/test_utils.rs b/language_service/src/test_utils.rs index b08aaaaff4..5667e455d1 100644 --- a/language_service/src/test_utils.rs +++ b/language_service/src/test_utils.rs @@ -10,12 +10,49 @@ use qsc::{ incremental::Compiler, line_column::{Encoding, Position, Range}, location::Location, + packages::prepare_package_store, target::Profile, LanguageFeatures, PackageStore, PackageType, SourceMap, Span, }; use qsc_project::{PackageGraphSources, PackageInfo}; use rustc_hash::FxHashMap; +const FAKE_STDLIB_CONTENTS: &str = r#" + namespace FakeStdLib { + operation Fake() : Unit {} + operation FakeWithParam(x : Int) : Unit {} + operation FakeCtlAdj() : Unit is Ctl + Adj {} + newtype Udt = (x : Int, y : Int); + newtype UdtWrapper = (inner : Udt); + newtype UdtFn = (Int -> Int); + newtype UdtFnWithUdtParams = (Udt -> Udt); + function TakesUdt(input : Udt) : Udt { + fail "not implemented" + } + operation RefFake() : Unit { + Fake(); + } + operation FakeWithTypeParam<'A>(a : 'A) : 'A { a } + internal operation Hidden() : Unit {} + struct FakeStruct { x : Int, y : Int } + struct StructWrapper { inner : FakeStruct } + struct StructFn { inner : Int -> Int } + struct StructFnWithStructParams { inner : FakeStruct -> FakeStruct } + function TakesStruct(input : FakeStruct) : FakeStruct { + fail "not implemented" + } + export Fake, FakeWithParam, FakeCtlAdj, Udt, UdtWrapper, UdtFn, UdtFnWithUdtParams, TakesUdt, RefFake, FakeWithTypeParam; + export FakeStruct, StructWrapper, StructFn, StructFnWithStructParams, TakesStruct; + } + + namespace FakeStdLib.Library { + operation OperationInLibrary() : Unit {} + export OperationInLibrary; + } + "#; + +const FAKE_STDLIB_NAME: &str = "qsharp-library-source:"; + pub(crate) fn compile_with_markers( source_with_markers: &str, use_fake_stdlib: bool, @@ -45,7 +82,27 @@ pub(crate) fn compile_project_with_markers( use_fake_stdlib: bool, ) -> (Compilation, String, Position, Vec) { let (compilation, cursor_location, target_spans) = - compile_project_with_markers_cursor_optional(sources_with_markers, use_fake_stdlib); + compile_project_with_markers_cursor_optional(sources_with_markers, None, use_fake_stdlib); + + let (cursor_uri, cursor_offset) = + cursor_location.expect("input string should have a cursor marker"); + + (compilation, cursor_uri, cursor_offset, target_spans) +} + +pub(crate) fn compile_with_dependency_with_markers( + sources_with_markers: &[(&str, &str)], + dependency_alias: &str, + dependency_sources: &[(&str, &str)], +) -> (Compilation, String, Position, Vec) { + let (compilation, cursor_location, target_spans) = compile_project_with_markers_cursor_optional( + sources_with_markers, + Some(Dependency { + sources: dependency_sources, + alias: dependency_alias, + }), + true, + ); let (cursor_uri, cursor_offset) = cursor_location.expect("input string should have a cursor marker"); @@ -58,7 +115,7 @@ pub(crate) fn compile_project_with_markers_no_cursor( use_fake_stdlib: bool, ) -> (Compilation, Vec) { let (compilation, cursor_location, target_spans) = - compile_project_with_markers_cursor_optional(sources_with_markers, use_fake_stdlib); + compile_project_with_markers_cursor_optional(sources_with_markers, None, use_fake_stdlib); assert!( cursor_location.is_none(), @@ -68,21 +125,98 @@ pub(crate) fn compile_project_with_markers_no_cursor( (compilation, target_spans) } +struct Dependency<'a> { + sources: &'a [(&'a str, &'a str)], + alias: &'a str, +} + fn compile_project_with_markers_cursor_optional( sources_with_markers: &[(&str, &str)], + dependency: Option, use_fake_stdlib: bool, ) -> (Compilation, Option<(String, Position)>, Vec) { let (sources, cursor_location, target_spans) = get_sources_and_markers(sources_with_markers); - let source_map = SourceMap::new(sources.clone(), None); - let (std_package_id, mut package_store) = if use_fake_stdlib { - compile_fake_stdlib() + let mut package_graph_sources = PackageGraphSources { + root: PackageInfo { + sources: sources.clone(), + language_features: LanguageFeatures::default(), + dependencies: FxHashMap::default(), + package_type: None, + }, + packages: FxHashMap::default(), + }; + + if use_fake_stdlib { + package_graph_sources.packages.insert( + "".into(), + PackageInfo { + sources: vec![(FAKE_STDLIB_NAME.into(), FAKE_STDLIB_CONTENTS.into())], + language_features: LanguageFeatures::default(), + dependencies: FxHashMap::default(), + package_type: None, + }, + ); + + package_graph_sources + .root + .dependencies + .insert("FakeStdLib".into(), "".into()); + } + + let (mut package_store, dependencies) = if let Some(dependency) = dependency { + package_graph_sources + .root + .dependencies + .insert(dependency.alias.into(), "".into()); + + package_graph_sources.packages.insert( + "".into(), + PackageInfo { + sources: dependency + .sources + .iter() + .map(|(n, s)| (Arc::from(*n), Arc::from(*s))) + .collect(), + language_features: LanguageFeatures::default(), + dependencies: FxHashMap::default(), + package_type: Some(qsc_project::PackageType::Lib), + }, + ); + + let buildable_program = prepare_package_store( + qsc::TargetCapabilityFlags::all(), + package_graph_sources.clone(), + ); + let mut dependencies = buildable_program.user_code_dependencies; + + if use_fake_stdlib { + // We still paid the cost of building the stdlib above, + // that's ok, but we'll remove it from the dependencies now. + + // Remove the real stdlib + dependencies.retain(|(_, alias)| alias.is_some()); + + // Erase the alias for the fake stdlib to make it act like the real stdlib + dependencies + .iter_mut() + .find(|(_, alias)| alias.as_deref() == Some("FakeStdLib")) + .expect("expected to find the fake stdlib") + .1 = None; + } + (buildable_program.store, dependencies) } else { - qsc::compile::package_store_with_stdlib(qsc::TargetCapabilityFlags::all()) + let (std_package_id, package_store) = if use_fake_stdlib { + compile_fake_stdlib() + } else { + qsc::compile::package_store_with_stdlib(qsc::TargetCapabilityFlags::all()) + }; + (package_store, vec![(std_package_id, None)]) }; + let source_map = SourceMap::new(sources, None); let (unit, errors) = compile::compile( &package_store, - &[(std_package_id, None)], + &dependencies, source_map, PackageType::Exe, Profile::Unrestricted.into(), @@ -96,19 +230,11 @@ fn compile_project_with_markers_cursor_optional( package_store, user_package_id: package_id, kind: CompilationKind::OpenProject { - package_graph_sources: PackageGraphSources { - root: PackageInfo { - sources, - language_features: LanguageFeatures::default(), - dependencies: FxHashMap::default(), - package_type: None, - }, - packages: FxHashMap::default(), - }, + package_graph_sources, }, compile_errors: errors, project_errors: Vec::new(), - dependencies: FxHashMap::default(), + dependencies: dependencies.into_iter().collect(), }, cursor_location, target_spans, @@ -131,18 +257,7 @@ where I: Iterator, { let std_source_map = SourceMap::new( - [( - "qsharp-library-source:".into(), - "namespace FakeStdLib { - operation Fake() : Unit {} - operation FakeWithParam(x: Int) : Unit {} - operation FakeCtlAdj() : Unit is Ctl + Adj {} - newtype Complex = (Real: Double, Imag: Double); - function TakesComplex(input : Complex) : Unit {} - export Fake, FakeWithParam, FakeCtlAdj, Complex, TakesComplex; - }" - .into(), - )], + [(FAKE_STDLIB_NAME.into(), FAKE_STDLIB_CONTENTS.into())], None, ); @@ -169,6 +284,7 @@ where compiler.update(increment); } + let source_package_id = compiler.source_package_id(); let (package_store, package_id) = compiler.into_package_store(); Compilation { @@ -177,43 +293,15 @@ where compile_errors: errors, kind: CompilationKind::Notebook { project: None }, project_errors: Vec::new(), - dependencies: FxHashMap::default(), + dependencies: [(source_package_id, None)].into_iter().collect(), } } fn compile_fake_stdlib() -> (PackageId, PackageStore) { let mut package_store = PackageStore::new(compile::core()); + let std_source_map = SourceMap::new( - [( - "qsharp-library-source:".into(), - r#"namespace FakeStdLib { - operation Fake() : Unit {} - operation FakeWithParam(x : Int) : Unit {} - operation FakeCtlAdj() : Unit is Ctl + Adj {} - newtype Udt = (x : Int, y : Int); - newtype UdtWrapper = (inner : Udt); - newtype UdtFn = (Int -> Int); - newtype UdtFnWithUdtParams = (Udt -> Udt); - function TakesUdt(input : Udt) : Udt { - fail "not implemented" - } - operation RefFake() : Unit { - Fake(); - } - operation FakeWithTypeParam<'A>(a : 'A) : 'A { a } - internal operation Hidden() : Unit {} - struct FakeStruct { x : Int, y : Int } - struct StructWrapper { inner : FakeStruct } - struct StructFn { inner : Int -> Int } - struct StructFnWithStructParams { inner : FakeStruct -> FakeStruct } - function TakesStruct(input : FakeStruct) : FakeStruct { - fail "not implemented" - } - export Fake, FakeWithParam, FakeCtlAdj, Udt, UdtWrapper, UdtFn, UdtFnWithUdtParams, TakesUdt, RefFake, FakeWithTypeParam; - export FakeStruct, StructWrapper, StructFn, StructFnWithStructParams, TakesStruct; - }"# - .into(), - )], + [(FAKE_STDLIB_NAME.into(), FAKE_STDLIB_CONTENTS.into())], None, ); let (std_compile_unit, std_errors) = compile::compile( diff --git a/language_service/src/tests.rs b/language_service/src/tests.rs index fde96a5f39..3fd52d3fc9 100644 --- a/language_service/src/tests.rs +++ b/language_service/src/tests.rs @@ -210,18 +210,19 @@ async fn completions_requested_after_document_load() { worker.apply_pending().await; - // this should be empty, because the doc hasn't loaded - assert_eq!( - ls.get_completions( + expect![[r#" + 337 + "#]] + .assert_debug_eq( + &ls.get_completions( "foo.qs", Position { line: 0, - column: 76 - } + column: 92, + }, ) .items .len(), - 13 ); } diff --git a/playground/src/main.tsx b/playground/src/main.tsx index bd989b9122..0a8b581b3d 100644 --- a/playground/src/main.tsx +++ b/playground/src/main.tsx @@ -248,12 +248,6 @@ async function loaded() { function registerMonacoLanguageServiceProviders( languageService: ILanguageService, ) { - monaco.languages.setLanguageConfiguration("qsharp", { - // This pattern is duplicated in /vscode/language-configuration.json . Please keep them in sync. - wordPattern: new RegExp( - "(-?\\d*\\.\\d\\w*)|(@\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\.\\'\\\"\\,\\<\\>\\/\\?\\s]+)", - ), - }); monaco.languages.registerCompletionItemProvider("qsharp", { // @ts-expect-error - Monaco's types expect range to be defined, // but it's actually optional and the default behavior is better @@ -310,7 +304,7 @@ function registerMonacoLanguageServiceProviders( }), }; }, - triggerCharacters: ["@"], // for attribute completions + triggerCharacters: ["@", "."], }); monaco.languages.registerHoverProvider("qsharp", { diff --git a/vscode/language-configuration.json b/vscode/language-configuration.json index 1f3ef07c76..a3b458c049 100644 --- a/vscode/language-configuration.json +++ b/vscode/language-configuration.json @@ -21,20 +21,5 @@ ["(", ")"], ["{", "}"], ["\"", "\""] - ], - // This is the default word pattern from https://code.visualstudio.com/api/language-extensions/language-configuration-guide - // with the following modifications: - // @foo - Attributes, including the leading '@', are treated as one word. - // foo.bar - Qualified names, including the dots, are treated as one word. - // - // This setting influences how completions are committed when a partial word has been typed. - // Some completions contain non-word characters (e.g. '@' and '.'), and when these completions are - // committed, they must replace the entire word that has been typed until the cursor, including any special - // characters. - // - // If you change this word pattern, please verify that Monaco and VS Code completions are working as intended - // for attributes and namespaces, by explicitly invoking completions after typing '@', '@Entr', 'Microsoft.Q', etc. - // - // This setting is duplicated for the Monaco setting for the playground: playground/src/main.tsx - "wordPattern": "(-?\\d*\\.\\d\\w*)|(@\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\.\\'\\\"\\,\\<\\>\\/\\?\\s]+)" + ] } diff --git a/vscode/src/completion.ts b/vscode/src/completion.ts index 827c191c0e..f12b26d72c 100644 --- a/vscode/src/completion.ts +++ b/vscode/src/completion.ts @@ -82,6 +82,8 @@ class QSharpCompletionItemProvider implements vscode.CompletionItemProvider { }); return item; }); - return results.concat(this.samples); + + // Include the samples list for empty documents + return document.lineCount === 0 ? results.concat(this.samples) : results; } } diff --git a/vscode/src/extension.ts b/vscode/src/extension.ts index 9f70588ef8..c0c70f361d 100644 --- a/vscode/src/extension.ts +++ b/vscode/src/extension.ts @@ -267,7 +267,8 @@ async function activateLanguageService(extensionUri: vscode.Uri) { vscode.languages.registerCompletionItemProvider( qsharpLanguageId, createCompletionItemProvider(languageService), - "@", // for attribute completion + "@", + ".", ), ); diff --git a/vscode/test/suites/language-service/language-service.test.ts b/vscode/test/suites/language-service/language-service.test.ts index 97eae0027e..8ad2580349 100644 --- a/vscode/test/suites/language-service/language-service.test.ts +++ b/vscode/test/suites/language-service/language-service.test.ts @@ -72,7 +72,7 @@ suite("Q# Language Service Tests", function suite() { const actualCompletionList = (await vscode.commands.executeCommand( "vscode.executeCompletionItemProvider", testQs, - new vscode.Position(0, 0), + new vscode.Position(1, 0), )) as vscode.CompletionList; assert.include(