Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
131 changes: 122 additions & 9 deletions src/parser/ast/parse_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@ enum Delim {
#[derive(Default)]
struct DelimStack(Vec<Delim>);

/// 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,
Expand All @@ -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 => ")",
Expand All @@ -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);
Expand Down Expand Up @@ -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<DdlogLanguage>` named `token`:
/// let mut buf = String::new();
/// push(&token, &mut buf);
/// assert!(buf.contains(token.text()));
/// ```
///
/// Note: Constructing a `SyntaxToken<DdlogLanguage>` requires a parsed syntax tree,
/// so a fully self-contained example is not practical here.
fn push(token: &rowan::SyntaxToken<DdlogLanguage>, 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<DelimiterError>,
expected: Delim,
Expand All @@ -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<SyntaxElement<DdlogLanguage>> = /* token stream for "(x: int, y: string)" */;
/// let (pairs, errors): (Vec<(String, String)>, Vec<DelimiterError>) = 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<I>(mut iter: I) -> (Vec<(String, String)>, Vec<DelimiterError>)
where
Expand Down Expand Up @@ -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::<DelimiterError>::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<DdlogLanguage>,
buf: &mut String,
Expand Down Expand Up @@ -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<string, u64>) {}", vec![("m".into(), "Map<string, u64>".into())])]
#[case(
"function nested(p: Vec<Map<string, Vec<u8>>>) {}",
vec![("p".into(), "Vec<Map<string, Vec<u8>>>".into())]
"function nested(p: Vec<Map<string, Vec<u8>>>) {}",
vec![("p".into(), "Vec<Map<string, Vec<u8>>>".into())]
)]
#[case("function array(a: [Vec<u32>]) {}", vec![("a".into(), "[Vec<u32>]".into())])]
#[case("function nested_vec(v: Vec<Vec<u8>>) {}", vec![("v".into(), "Vec<Vec<u8>>".into())])]
Expand Down
61 changes: 53 additions & 8 deletions src/parser/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1300,18 +1300,40 @@ 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
.children_with_tokens()
.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());
Expand Down Expand Up @@ -1614,18 +1636,41 @@ 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
.children_with_tokens()
.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());
Expand Down