use super::{
DatalogAtom, DatalogError, DatalogFact, DatalogProgram, DatalogRule, DatalogTerm, DatalogValue,
};
pub fn parse_program(input: &str) -> Result<DatalogProgram, DatalogError> {
let mut program = DatalogProgram::new();
let cleaned = strip_comments(input);
for clause in split_clauses(&cleaned) {
let clause = clause.trim();
if clause.is_empty() {
continue;
}
if clause.contains(":-") {
let rule = parse_rule(clause)?;
program.add_rule(rule);
} else {
let fact = parse_fact_str(clause)?;
program.add_fact(fact);
}
}
Ok(program)
}
pub fn parse_rule(input: &str) -> Result<DatalogRule, DatalogError> {
let input = input.trim().trim_end_matches('.');
let parts: Vec<&str> = input.splitn(2, ":-").collect();
if parts.len() != 2 {
return Err(DatalogError::ParseError(format!(
"expected ':-' in rule: {input}"
)));
}
let head = parse_atom(parts[0].trim())?;
let body_str = parts[1].trim();
let body = if body_str.is_empty() {
Vec::new()
} else {
split_atoms(body_str)
.iter()
.map(|s| parse_atom(s.trim()))
.collect::<Result<Vec<_>, _>>()?
};
Ok(DatalogRule { head, body })
}
pub fn parse_atom(input: &str) -> Result<DatalogAtom, DatalogError> {
let input = input.trim();
if let Some(paren_pos) = input.find('(') {
let predicate = input[..paren_pos].trim().to_string();
validate_predicate(&predicate)?;
let rest = &input[paren_pos + 1..];
let close_pos = rest.rfind(')').ok_or_else(|| {
DatalogError::ParseError(format!("missing closing ')' in atom: {input}"))
})?;
let args_str = &rest[..close_pos];
let terms = if args_str.trim().is_empty() {
Vec::new()
} else {
split_terms(args_str)
.iter()
.map(|s| parse_term(s.trim()))
.collect::<Result<Vec<_>, _>>()?
};
Ok(DatalogAtom { predicate, terms })
} else {
let predicate = input.to_string();
validate_predicate(&predicate)?;
Ok(DatalogAtom {
predicate,
terms: Vec::new(),
})
}
}
fn strip_comments(input: &str) -> String {
input
.lines()
.map(|line| {
if let Some(pos) = line.find('%') {
&line[..pos]
} else {
line
}
})
.collect::<Vec<_>>()
.join("\n")
}
fn split_clauses(input: &str) -> Vec<String> {
let mut clauses = Vec::new();
let mut current = String::new();
let mut in_quotes = false;
let mut paren_depth: usize = 0;
for ch in input.chars() {
match ch {
'"' => {
in_quotes = !in_quotes;
current.push(ch);
}
'(' if !in_quotes => {
paren_depth += 1;
current.push(ch);
}
')' if !in_quotes => {
paren_depth = paren_depth.saturating_sub(1);
current.push(ch);
}
'.' if !in_quotes && paren_depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
clauses.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
clauses.push(trimmed);
}
clauses
}
fn split_atoms(input: &str) -> Vec<String> {
split_on_comma_with_depth(input)
}
fn split_terms(input: &str) -> Vec<String> {
split_on_comma_with_depth(input)
}
fn split_on_comma_with_depth(input: &str) -> Vec<String> {
let mut items = Vec::new();
let mut current = String::new();
let mut depth: usize = 0;
let mut in_quotes = false;
for ch in input.chars() {
match ch {
'"' => {
in_quotes = !in_quotes;
current.push(ch);
}
'(' if !in_quotes => {
depth += 1;
current.push(ch);
}
')' if !in_quotes => {
depth = depth.saturating_sub(1);
current.push(ch);
}
',' if !in_quotes && depth == 0 => {
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
items.push(trimmed);
}
current.clear();
}
_ => {
current.push(ch);
}
}
}
let trimmed = current.trim().to_string();
if !trimmed.is_empty() {
items.push(trimmed);
}
items
}
fn parse_fact_str(input: &str) -> Result<DatalogFact, DatalogError> {
let atom = parse_atom(input)?;
let mut args = Vec::new();
for term in &atom.terms {
match term {
DatalogTerm::Constant(v) => args.push(v.clone()),
DatalogTerm::Variable(v) => {
return Err(DatalogError::ParseError(format!(
"facts cannot contain variables, found variable '{v}' in: {input}"
)));
}
}
}
Ok(DatalogFact {
predicate: atom.predicate,
args,
})
}
fn parse_term(input: &str) -> Result<DatalogTerm, DatalogError> {
let input = input.trim();
if input.is_empty() {
return Err(DatalogError::ParseError("empty term".to_string()));
}
if input.starts_with('"') && input.ends_with('"') && input.len() >= 2 {
let content = &input[1..input.len() - 1];
return Ok(DatalogTerm::Constant(DatalogValue::Str(
content.to_string(),
)));
}
let first = input
.chars()
.next()
.expect("non-empty input has a first char");
if first.is_uppercase() || first == '_' {
validate_identifier(input)?;
return Ok(DatalogTerm::Variable(input.to_string()));
}
if input == "true" {
return Ok(DatalogTerm::Constant(DatalogValue::Bool(true)));
}
if input == "false" {
return Ok(DatalogTerm::Constant(DatalogValue::Bool(false)));
}
if let Ok(i) = input.parse::<i64>() {
return Ok(DatalogTerm::Constant(DatalogValue::Int(i)));
}
if first.is_lowercase() || first == '_' {
validate_identifier(input)?;
return Ok(DatalogTerm::Constant(DatalogValue::Str(input.to_string())));
}
Err(DatalogError::ParseError(format!(
"cannot parse term: {input}"
)))
}
fn validate_identifier(s: &str) -> Result<(), DatalogError> {
for ch in s.chars() {
if !ch.is_alphanumeric() && ch != '_' {
return Err(DatalogError::ParseError(format!(
"invalid identifier character '{ch}' in: {s}"
)));
}
}
Ok(())
}
fn validate_predicate(name: &str) -> Result<(), DatalogError> {
if name.is_empty() {
return Err(DatalogError::ParseError(
"predicate name cannot be empty".to_string(),
));
}
let first = name
.chars()
.next()
.expect("non-empty name has a first char");
if first.is_uppercase() {
return Err(DatalogError::ParseError(format!(
"predicate name must start with lowercase: {name}"
)));
}
validate_identifier(name)
}
#[cfg(test)]
mod parser_unit_tests {
use super::*;
#[test]
fn test_strip_comments() {
let input = "parent(a, b). % this is a comment\nparent(b, c).";
let stripped = strip_comments(input);
assert!(!stripped.contains('%'));
assert!(stripped.contains("parent(a, b)"));
}
#[test]
fn test_split_clauses_basic() {
let input = "parent(a, b). parent(b, c).";
let clauses = split_clauses(input);
assert_eq!(clauses.len(), 2);
}
#[test]
fn test_parse_term_variable() {
let t = parse_term("X").expect("parse");
assert_eq!(t, DatalogTerm::Variable("X".to_string()));
}
#[test]
fn test_parse_term_string_constant() {
let t = parse_term("alice").expect("parse");
assert_eq!(
t,
DatalogTerm::Constant(DatalogValue::Str("alice".to_string()))
);
}
#[test]
fn test_parse_term_int() {
let t = parse_term("42").expect("parse");
assert_eq!(t, DatalogTerm::Constant(DatalogValue::Int(42)));
}
#[test]
fn test_parse_term_bool() {
let t = parse_term("true").expect("parse");
assert_eq!(t, DatalogTerm::Constant(DatalogValue::Bool(true)));
}
}