diff --git a/src/parser/ast/parse_utils.rs b/src/parser/ast/parse_utils.rs index 43cadb4a..de3e58ab 100644 --- a/src/parser/ast/parse_utils.rs +++ b/src/parser/ast/parse_utils.rs @@ -23,6 +23,14 @@ enum Delim { #[derive(Default)] struct DelimStack(Vec); +/// An error emitted when a closing token does not match the expected +/// delimiter. +/// +/// During parameter parsing the parser maintains a stack of opening +/// delimiters as described in `docs/function-parsing-design.md`. If a +/// closing token arrives before the matching delimiter, it is recorded as a +/// `DelimiterError` so callers can report the unexpected character along with +/// its span. #[derive(Debug, Clone, PartialEq, Eq)] pub(super) struct DelimiterError { expected: Delim, @@ -31,6 +39,29 @@ pub(super) struct DelimiterError { } impl std::fmt::Display for DelimiterError { + /// Formats a `DelimiterError` for display, indicating the expected delimiter, + /// the actual token found, and the location of the error. + /// + /// This is used to provide clear error messages when delimiter mismatches + /// occur during parsing. + /// + /// # Examples + /// + /// ```no_run + /// use crate::parser::ast::parse_utils::{DelimiterError, Delim}; + /// use rowan::TextRange; + /// use crate::parser::SyntaxKind; + /// + /// let err = DelimiterError { + /// expected: Delim::Angle, + /// found: SyntaxKind::T_SHR, + /// span: TextRange::new(0.into(), 2.into()), + /// }; + /// assert_eq!( + /// format!("{}", err), + /// "expected '>' before '>>' at 0..2" + /// ); + /// ``` fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let expected = match self.expected { Delim::Paren => ")", @@ -57,6 +88,17 @@ impl std::fmt::Display for DelimiterError { impl std::error::Error for DelimiterError {} impl DelimStack { + /// Pushes one or more delimiters of the specified type onto the stack. + /// + /// This increases the nesting level for the given delimiter by the specified count. + /// + /// # Examples + /// + /// ```no_run + /// let mut stack = DelimStack::default(); + /// stack.open(Delim::Paren, 2); + /// assert_eq!(stack.0, vec![Delim::Paren, Delim::Paren]); + /// ``` fn open(&mut self, delim: Delim, count: usize) { for _ in 0..count { self.0.push(delim); @@ -110,10 +152,26 @@ fn close_and_push( closed } +/// Appends the text of a syntax token to the provided buffer. +/// +/// # Example +/// +/// ```ignore +/// // Assuming you have a `SyntaxToken` named `token`: +/// let mut buf = String::new(); +/// push(&token, &mut buf); +/// assert!(buf.contains(token.text())); +/// ``` +/// +/// Note: Constructing a `SyntaxToken` requires a parsed syntax tree, +/// so a fully self-contained example is not practical here. fn push(token: &rowan::SyntaxToken, buf: &mut String) { buf.push_str(token.text()); } +/// Appends a `DelimiterError` to the error list for an unexpected delimiter token. +/// +/// Records the expected delimiter, the actual token kind found, and the token's text range. fn push_error( errors: &mut Vec, expected: Delim, @@ -126,11 +184,29 @@ fn push_error( }); } -/// Consume `(name: type)` pairs from the provided iterator. +/// Parses `(name: type)` pairs from a parameter or column list, returning both the pairs and any delimiter errors encountered. /// -/// The iterator should yield the tokens of a parameter or column list -/// starting at the opening parenthesis. The returned vector contains -/// each name and its associated type text. +/// The iterator should yield syntax elements starting at the opening parenthesis of the list. The function tracks delimiter nesting and collects errors for unmatched or unexpected delimiters. Each returned pair consists of the parameter or column name and its associated type as strings. +/// +/// # Returns +/// +/// A tuple containing: +/// - A vector of `(name, type)` pairs extracted from the list. +/// - A vector of `DelimiterError`s for any unmatched or unexpected delimiters found during parsing. +/// +/// # Examples +/// +/// ```no_run +/// use parser::ast::parse_utils::{parse_name_type_pairs, DelimiterError}; +/// use parser::ast::DdlogLanguage; +/// use parser::syntax::{SyntaxElement, SyntaxKind}; +/// +/// // Example: parsing a parameter list "(x: int, y: string)" +/// let tokens: Vec> = /* token stream for "(x: int, y: string)" */; +/// let (pairs, errors): (Vec<(String, String)>, Vec) = parse_name_type_pairs(tokens.into_iter()); +/// assert_eq!(pairs, vec![("x".to_string(), "int".to_string()), ("y".to_string(), "string".to_string())]); +/// assert!(errors.is_empty()); +/// ``` #[must_use] pub(super) fn parse_name_type_pairs(mut iter: I) -> (Vec<(String, String)>, Vec) where @@ -189,7 +265,27 @@ where (pairs, errors) } -/// Handle a single token during name-type pair parsing. +/// Processes a single token while parsing name-type pairs, updating buffers, delimiter +/// stack, and error tracking as needed. +/// +/// Handles delimiter nesting, finalises pairs on commas, and records delimiter errors. +/// Returns `true` if the outermost parentheses have closed and parsing should stop. +/// +/// # Examples +/// +/// ```no_run +/// # use ddlog_parser::ast::parse_utils::{handle_token, DelimStack, DelimiterError}; +/// # use ddlog_parser::DdlogLanguage; +/// # use rowan::{SyntaxKind, SyntaxToken, TextRange}; +/// let mut buf = String::new(); +/// let mut name = None; +/// let mut pairs = Vec::new(); +/// let mut depth = DelimStack::default(); +/// let mut outer_parens = 1; +/// let mut errors = Vec::::new(); +/// // Suppose `token` is a colon at the outermost level: +/// // handle_token(&token, &mut buf, &mut name, &mut pairs, &mut depth, &mut outer_parens, &mut errors); +/// ``` fn handle_token( token: &rowan::SyntaxToken, buf: &mut String, @@ -339,17 +435,34 @@ mod tests { parse_type_after_colon(&mut iter) } + /// Tests the extraction of name-type pairs from function and relation parameter lists. + /// + /// This parameterised test verifies that `parse_name_type_pairs` correctly parses + /// parameter declarations of the form `(name: type)` from various function and relation + /// signatures, including cases with nested delimiters, missing colons, and empty parameter + /// lists. It asserts that the extracted pairs match the expected output and that no + /// delimiter errors are reported. + /// + /// # Examples + /// + /// ```no_run + /// name_type_pairs( + /// "function f(a: u32, b: string) {}", + /// vec![("a".into(), "u32".into()), ("b".into(), "string".into())], + /// tokens_for, + /// ); + /// ``` #[rstest] #[case("function f(a: u32, b: string) {}", vec![("a".into(), "u32".into()), ("b".into(), "string".into())])] #[case( - "input relation R(id: u32, name: string)", - vec![("id".into(), "u32".into()), ("name".into(), "string".into())] + "input relation R(id: u32, name: string)", + vec![("id".into(), "u32".into()), ("name".into(), "string".into())] )] #[case("function wrap(t: Option<(u32, string)>) {}", vec![("t".into(), "Option<(u32, string)>".into())])] #[case("function g(m: Map) {}", vec![("m".into(), "Map".into())])] #[case( - "function nested(p: Vec>>) {}", - vec![("p".into(), "Vec>>".into())] + "function nested(p: Vec>>) {}", + vec![("p".into(), "Vec>>".into())] )] #[case("function array(a: [Vec]) {}", vec![("a".into(), "[Vec]".into())])] #[case("function nested_vec(v: Vec>) {}", vec![("v".into(), "Vec>".into())])] diff --git a/src/parser/mod.rs b/src/parser/mod.rs index c359364e..30233b41 100644 --- a/src/parser/mod.rs +++ b/src/parser/mod.rs @@ -1300,7 +1300,17 @@ pub mod ast { .any(|e| e.kind() == SyntaxKind::K_INPUT) } - /// Whether the relation is declared as `output`. + /// Returns `true` if the relation is declared with the `output` keyword. + /// + /// # Examples + /// + /// ```no_run + /// # use crate::parser::ast::{Relation, Root}; + /// # let src = "output MyRel(x: u32)"; + /// # let parsed = crate::parser::parse(src); + /// # let rel = parsed.root.relations().next().unwrap(); + /// assert!(rel.is_output()); + /// ``` #[must_use] pub fn is_output(&self) -> bool { self.syntax @@ -1308,10 +1318,22 @@ pub mod ast { .any(|e| e.kind() == SyntaxKind::K_OUTPUT) } - /// Columns declared for the relation. + /// Returns the columns declared for the relation as name/type pairs. + /// + /// Delimiter errors encountered during parsing are currently ignored, + /// but this behaviour may change in future versions to surface such diagnostics. + /// + /// # Examples /// - /// Delimiter errors detected during parsing are ignored. - /// This may change in future to surface these diagnostics. + /// ```no_run + /// # use your_crate::parser::ast::{Relation, Root}; + /// # let src = "relation Person(name: string, age: integer)"; + /// # let parsed = your_crate::parser::parse(src); + /// # let root = parsed.root(); + /// # let relation = root.relations().next().unwrap(); + /// let columns = relation.columns(); + /// assert_eq!(columns, vec![("name".to_string(), "string".to_string()), ("age".to_string(), "integer".to_string())]); + /// ``` #[must_use] pub fn columns(&self) -> Vec<(String, String)> { let (pairs, errors) = parse_name_type_pairs(self.syntax.children_with_tokens()); @@ -1614,7 +1636,18 @@ pub mod ast { }) } - /// Whether the function is declared as `extern`. + /// Returns `true` if the function is declared with the `extern` keyword. + /// + /// # Examples + /// + /// ```no_run + /// # use crate::parser::ast::{Function, Root}; + /// # let src = "extern function foo(x: i32): i32;"; + /// # let parsed = crate::parser::parse(src); + /// # let root = parsed.root(); + /// # let func = root.functions().next().unwrap(); + /// assert!(func.is_extern()); + /// ``` #[must_use] pub fn is_extern(&self) -> bool { self.syntax @@ -1622,10 +1655,22 @@ pub mod ast { .any(|e| e.kind() == SyntaxKind::K_EXTERN) } - /// Function parameters as name/type pairs. + /// Returns the function parameters as pairs of parameter name and type. + /// + /// Delimiter errors encountered during parsing are currently ignored, + /// but this behaviour may change in future versions to surface such diagnostics. + /// + /// # Examples /// - /// Delimiter errors detected during parsing are ignored. - /// This may change in future to surface these diagnostics. + /// ```no_run + /// # use crate::parser::ast::{Function, Root}; + /// # let src = "function foo(x: i32, y: string): bool { ... }"; + /// # let parsed = crate::parser::parse(src); + /// # let root = parsed.root(); + /// # let func = root.functions().next().unwrap(); + /// let params = func.parameters(); + /// assert_eq!(params, vec![("x".to_string(), "i32".to_string()), ("y".to_string(), "string".to_string())]); + /// ``` #[must_use] pub fn parameters(&self) -> Vec<(String, String)> { let (pairs, errors) = parse_name_type_pairs(self.syntax.children_with_tokens());