Skip to content
Draft
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
132 changes: 54 additions & 78 deletions src/parser/ast/rule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,15 @@
//! let rule = first_rule("R(x) :- S(x), T(x).");
//! assert_eq!(rule.head().as_deref(), Some("R(x)"));
//! assert_eq!(rule.body_literals(), vec!["S(x)".into(), "T(x)".into()]);
//! let exprs = rule.body_expressions();
//! assert_eq!(exprs.len(), 2);
//! assert!(exprs.into_iter().all(|expr| expr.is_ok()));
//! ```

use super::AstNode;
use crate::{DdlogLanguage, SyntaxKind};
use chumsky::error::Simple;

use super::{AstNode, Expr};
use crate::{DdlogLanguage, SyntaxKind, parser::expression::parse_expression};

/// Typed wrapper for a rule declaration.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -64,94 +69,37 @@
/// Text of each body literal in order of appearance.
#[must_use]
pub fn body_literals(&self) -> Vec<String> {
let mut iter = self
.syntax
.children_with_tokens()
.skip_while(|e| e.kind() != SyntaxKind::T_IMPLIES);

if matches!(iter.next().map(|e| e.kind()), Some(SyntaxKind::T_IMPLIES)) {
Self::extract_literals_from_body(iter)
} else {
Vec::new()
}
}

/// Iterate over the body and collect literals.
fn extract_literals_from_body<I>(iter: I) -> Vec<String>
where
I: Iterator<Item = rowan::SyntaxElement<DdlogLanguage>>,
{
let mut buf = String::new();
let mut lits = Vec::new();

for e in iter {
if Self::process_body_element(e, &mut buf, &mut lits) {
break;
}
}

lits
}

/// Process a single syntax element of the rule body.
///
/// Returns `true` if the body has ended.
fn process_body_element(
element: rowan::SyntaxElement<DdlogLanguage>,
buf: &mut String,
lits: &mut Vec<String>,
) -> bool {
use rowan::NodeOrToken;

match element {
NodeOrToken::Token(t) => Self::process_token(&t, buf, lits),
NodeOrToken::Node(n) => {
buf.push_str(&n.text().to_string());
false
}
}
self.body_expression_texts()
}

/// Handle a token in the rule body.
///
/// Returns `true` when the terminating `.` is encountered.
fn process_token(
token: &rowan::SyntaxToken<DdlogLanguage>,
buf: &mut String,
lits: &mut Vec<String>,
) -> bool {
match token.kind() {
SyntaxKind::T_COMMA => {
Self::add_literal_if_not_empty(buf, lits);
buf.clear();
false
}
SyntaxKind::T_DOT => {
Self::add_literal_if_not_empty(buf, lits);
true
}
_ => {
buf.push_str(token.text());
false
}
}
/// Parse the rule body into structured expressions.
#[must_use]
pub fn body_expressions(&self) -> Vec<Result<Expr, Vec<Simple<SyntaxKind>>>> {
self.body_expression_texts()
.into_iter()
.map(|text| parse_expression(&text))
.collect()
}

/// Add a literal to the list if it contains non-whitespace text.
fn add_literal_if_not_empty(buf: &str, lits: &mut Vec<String>) {
let lit = buf.trim();
if !lit.is_empty() {
lits.push(lit.to_string());
}
fn body_expression_texts(&self) -> Vec<String> {
self.syntax
.children()
.filter(|node| node.kind() == SyntaxKind::N_EXPR_NODE)
.map(|node| {
let text = node.text().to_string();
text.trim().to_string()
})
.filter(|text| !text.is_empty())
.collect()
}
}

impl_ast_node!(Rule);

#[cfg(test)]

Check warning on line 99 in src/parser/ast/rule.rs

View workflow job for this annotation

GitHub Actions / build-test

Diff in /home/runner/work/ddlint/ddlint/src/parser/ast/rule.rs
mod tests {

use crate::parse;
use crate::{parse, parser::ast::{BinaryOp, Expr}};

#[expect(clippy::expect_used, reason = "Using expect for clearer test failures")]
#[test]
Expand All @@ -166,4 +114,32 @@
assert_eq!(rule.head().as_deref(), Some("A(x)"));
assert_eq!(rule.body_literals(), vec!["B(x)".to_string()]);
}

#[expect(clippy::expect_used, reason = "tests expect parsed rule bodies")]
#[test]
fn rule_body_expressions_parse_structures() {
let parsed = parse("R(x) :- 1 + 2 * 3, if (cond) Foo(cond) else Bar().");
let rule = parsed
.root()
.rules()
.first()
.cloned()
.expect("rule missing");
let exprs = rule.body_expressions();
assert_eq!(exprs.len(), 2);

Check warning on line 129 in src/parser/ast/rule.rs

View workflow job for this annotation

GitHub Actions / build-test

Diff in /home/runner/work/ddlint/ddlint/src/parser/ast/rule.rs
let first = exprs[0].as_ref().expect("first literal failed to parse");
match first {
Expr::Binary { op: BinaryOp::Add, lhs, rhs } => match rhs.as_ref() {
Expr::Binary { op: BinaryOp::Mul, .. } => {
assert!(matches!(lhs.as_ref(), Expr::Literal(_)));
}
other => panic!("expected multiplication RHS, got {other:?}"),
},

Check warning on line 137 in src/parser/ast/rule.rs

View workflow job for this annotation

GitHub Actions / build-test

Diff in /home/runner/work/ddlint/ddlint/src/parser/ast/rule.rs
other => panic!("unexpected expression: {other:?}"),
}
let second = exprs[1]
.as_ref()
.expect("second literal failed to parse");
assert!(matches!(second, Expr::IfElse { .. }));
}
}
37 changes: 0 additions & 37 deletions src/parser/expression_span.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,43 +11,6 @@ use thiserror::Error;
use crate::parser::expression::parse_expression;
use crate::{Span, SyntaxKind};

/// Find the span of a rule body expression within a slice of tokens.
///
/// The function scans the tokens starting at `start_idx` until `end`. It
/// returns the range of bytes between the token following `T_IMPLIES` and the
/// preceding `T_DOT`. `None` is returned if a well formed range cannot be
/// determined.
#[must_use]
pub(crate) fn rule_body_span(
tokens: &[(SyntaxKind, Span)],
start_idx: usize,
end: usize,
) -> Option<Span> {
let mut expr_start = None;
let mut expr_end = None;
let mut idx = start_idx;
while let Some(tok) = tokens.get(idx) {
if tok.1.start >= end {
break;
}
match tok.0 {
SyntaxKind::T_IMPLIES => {
expr_start = tokens.get(idx + 1).map(|t| t.1.start);
}
SyntaxKind::T_DOT => {
expr_end = Some(tok.1.start);
break;
}
_ => {}
}
idx += 1;
}
match (expr_start, expr_end) {
(Some(s), Some(e)) if s < e => Some(s..e),
_ => None,
}
}

/// Parse the text within `span` using the expression parser.
///
/// Any syntax errors reported by the parser are returned for the caller to
Expand Down
Loading
Loading