use std::collections::{HashMap, HashSet};
use std::fmt;
use anyhow::Result;
use crate::{RuleAtom, Term};
#[derive(Debug, Clone)]
pub struct AspConfig {
pub max_answer_sets: usize,
pub max_iterations: usize,
pub enable_optimization: bool,
pub random_seed: Option<u64>,
pub optimize_grounding: bool,
}
impl Default for AspConfig {
fn default() -> Self {
Self {
max_answer_sets: 10,
max_iterations: 10000,
enable_optimization: true,
random_seed: None,
optimize_grounding: true,
}
}
}
impl AspConfig {
pub fn with_max_answer_sets(mut self, max: usize) -> Self {
self.max_answer_sets = max;
self
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.random_seed = Some(seed);
self
}
pub fn with_optimization(mut self, enabled: bool) -> Self {
self.enable_optimization = enabled;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AspLiteral {
Positive(Atom),
Negated(Atom),
DefaultNegated(Atom),
}
impl AspLiteral {
pub fn positive(atom: Atom) -> Self {
AspLiteral::Positive(atom)
}
pub fn negated(atom: Atom) -> Self {
AspLiteral::Negated(atom)
}
pub fn not(atom: Atom) -> Self {
AspLiteral::DefaultNegated(atom)
}
pub fn atom(&self) -> &Atom {
match self {
AspLiteral::Positive(a) | AspLiteral::Negated(a) | AspLiteral::DefaultNegated(a) => a,
}
}
pub fn is_positive(&self) -> bool {
matches!(self, AspLiteral::Positive(_))
}
pub fn is_default_negated(&self) -> bool {
matches!(self, AspLiteral::DefaultNegated(_))
}
}
impl fmt::Display for AspLiteral {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AspLiteral::Positive(a) => write!(f, "{a}"),
AspLiteral::Negated(a) => write!(f, "-{a}"),
AspLiteral::DefaultNegated(a) => write!(f, "not {a}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Atom {
pub predicate: String,
pub args: Vec<AspTerm>,
}
impl Atom {
pub fn new(predicate: &str, args: Vec<AspTerm>) -> Self {
Self {
predicate: predicate.to_string(),
args,
}
}
pub fn nullary(predicate: &str) -> Self {
Self::new(predicate, Vec::new())
}
pub fn is_ground(&self) -> bool {
self.args.iter().all(|a| a.is_ground())
}
pub fn apply(&self, subst: &Substitution) -> Atom {
Atom {
predicate: self.predicate.clone(),
args: self.args.iter().map(|a| a.apply(subst)).collect(),
}
}
pub fn variables(&self) -> HashSet<String> {
let mut vars = HashSet::new();
for arg in &self.args {
if let AspTerm::Variable(v) = arg {
vars.insert(v.clone());
}
}
vars
}
}
impl fmt::Display for Atom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.args.is_empty() {
write!(f, "{}", self.predicate)
} else {
write!(
f,
"{}({})",
self.predicate,
self.args
.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AspTerm {
Constant(String),
Variable(String),
Integer(i64),
Function { name: String, args: Vec<AspTerm> },
}
impl AspTerm {
pub fn constant(value: &str) -> Self {
AspTerm::Constant(value.to_string())
}
pub fn variable(name: &str) -> Self {
AspTerm::Variable(name.to_string())
}
pub fn integer(value: i64) -> Self {
AspTerm::Integer(value)
}
pub fn is_ground(&self) -> bool {
match self {
AspTerm::Constant(_) | AspTerm::Integer(_) => true,
AspTerm::Variable(_) => false,
AspTerm::Function { args, .. } => args.iter().all(|a| a.is_ground()),
}
}
pub fn apply(&self, subst: &Substitution) -> AspTerm {
match self {
AspTerm::Variable(v) => subst.get(v).cloned().unwrap_or_else(|| self.clone()),
AspTerm::Function { name, args } => AspTerm::Function {
name: name.clone(),
args: args.iter().map(|a| a.apply(subst)).collect(),
},
_ => self.clone(),
}
}
}
impl fmt::Display for AspTerm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AspTerm::Constant(c) => write!(f, "{c}"),
AspTerm::Variable(v) => write!(f, "{v}"),
AspTerm::Integer(i) => write!(f, "{i}"),
AspTerm::Function { name, args } => {
write!(
f,
"{}({})",
name,
args.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ")
)
}
}
}
}
pub type Substitution = HashMap<String, AspTerm>;
#[derive(Debug, Clone)]
pub enum AspRule {
Normal {
head: Option<AspLiteral>,
body: Vec<AspLiteral>,
},
Choice {
choices: Vec<Atom>,
lower_bound: Option<usize>,
upper_bound: Option<usize>,
body: Vec<AspLiteral>,
},
Constraint { body: Vec<AspLiteral> },
Weak {
body: Vec<AspLiteral>,
weight: i64,
level: i32,
},
}
impl AspRule {
pub fn normal(head: Atom, body: Vec<AspLiteral>) -> Self {
AspRule::Normal {
head: Some(AspLiteral::Positive(head)),
body,
}
}
pub fn fact(head: Atom) -> Self {
AspRule::Normal {
head: Some(AspLiteral::Positive(head)),
body: Vec::new(),
}
}
pub fn constraint(body: Vec<AspLiteral>) -> Self {
AspRule::Constraint { body }
}
pub fn choice(
choices: Vec<Atom>,
lower: Option<usize>,
upper: Option<usize>,
body: Vec<AspLiteral>,
) -> Self {
AspRule::Choice {
choices,
lower_bound: lower,
upper_bound: upper,
body,
}
}
pub fn weak(body: Vec<AspLiteral>, weight: i64, level: i32) -> Self {
AspRule::Weak {
body,
weight,
level,
}
}
pub fn variables(&self) -> HashSet<String> {
let mut vars = HashSet::new();
match self {
AspRule::Normal { head, body } => {
if let Some(h) = head {
vars.extend(h.atom().variables());
}
for lit in body {
vars.extend(lit.atom().variables());
}
}
AspRule::Choice { choices, body, .. } => {
for c in choices {
vars.extend(c.variables());
}
for lit in body {
vars.extend(lit.atom().variables());
}
}
AspRule::Constraint { body } | AspRule::Weak { body, .. } => {
for lit in body {
vars.extend(lit.atom().variables());
}
}
}
vars
}
pub fn is_ground(&self) -> bool {
self.variables().is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AnswerSet {
pub atoms: HashSet<Atom>,
pub cost: Option<i64>,
pub optimality: Option<i32>,
}
impl AnswerSet {
pub fn new(atoms: HashSet<Atom>) -> Self {
Self {
atoms,
cost: None,
optimality: None,
}
}
pub fn contains(&self, atom: &Atom) -> bool {
self.atoms.contains(atom)
}
pub fn len(&self) -> usize {
self.atoms.len()
}
pub fn is_empty(&self) -> bool {
self.atoms.is_empty()
}
pub fn atoms_with_predicate(&self, predicate: &str) -> Vec<&Atom> {
self.atoms
.iter()
.filter(|a| a.predicate == predicate)
.collect()
}
}
impl fmt::Display for AnswerSet {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let atoms: Vec<String> = self.atoms.iter().map(|a| a.to_string()).collect();
write!(f, "{{ {} }}", atoms.join(", "))
}
}
#[derive(Debug)]
struct GroundProgram {
rules: Vec<AspRule>,
atoms: HashSet<Atom>,
}
pub struct AspSolver {
config: AspConfig,
rules: Vec<AspRule>,
facts: HashSet<Atom>,
domain: HashSet<AspTerm>,
}
impl AspSolver {
pub fn new() -> Self {
Self::with_config(AspConfig::default())
}
pub fn with_config(config: AspConfig) -> Self {
Self {
config,
rules: Vec::new(),
facts: HashSet::new(),
domain: HashSet::new(),
}
}
pub fn add_fact(&mut self, fact_str: &str) -> Result<()> {
let atom = Self::parse_atom(fact_str)?;
for arg in &atom.args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
self.facts.insert(atom);
Ok(())
}
pub fn add_rule(&mut self, rule: AspRule) {
self.extract_domain(&rule);
self.rules.push(rule);
}
pub fn add_constraint(&mut self, body: Vec<AspLiteral>) {
self.add_rule(AspRule::constraint(body));
}
pub fn add_choice_rule(
&mut self,
choices: Vec<Atom>,
lower: Option<usize>,
upper: Option<usize>,
body: Vec<AspLiteral>,
) {
self.add_rule(AspRule::choice(choices, lower, upper, body));
}
pub fn solve(&mut self) -> Result<Vec<AnswerSet>> {
let ground_program = self.ground()?;
let answer_sets = self.compute_stable_models(&ground_program)?;
if self.config.enable_optimization {
return Ok(self.optimize(answer_sets));
}
Ok(answer_sets)
}
fn ground(&self) -> Result<GroundProgram> {
let mut ground_rules = Vec::new();
let mut ground_atoms = HashSet::new();
for fact in &self.facts {
ground_atoms.insert(fact.clone());
ground_rules.push(AspRule::fact(fact.clone()));
}
for rule in &self.rules {
let vars = rule.variables();
if vars.is_empty() {
ground_rules.push(rule.clone());
} else {
let substitutions = self.generate_substitutions(&vars);
for subst in substitutions {
let ground_rule = self.apply_substitution_to_rule(rule, &subst);
ground_rules.push(ground_rule);
}
}
}
Ok(GroundProgram {
rules: ground_rules,
atoms: ground_atoms,
})
}
fn generate_substitutions(&self, vars: &HashSet<String>) -> Vec<Substitution> {
let vars_vec: Vec<&String> = vars.iter().collect();
if vars_vec.is_empty() {
return vec![HashMap::new()];
}
let domain_vec: Vec<&AspTerm> = self.domain.iter().collect();
if domain_vec.is_empty() {
return Vec::new();
}
let mut result = vec![HashMap::new()];
for var in vars_vec {
let mut new_result = Vec::new();
for subst in &result {
for term in &domain_vec {
let mut new_subst = subst.clone();
new_subst.insert(var.clone(), (*term).clone());
new_result.push(new_subst);
}
}
result = new_result;
}
result
}
fn apply_substitution_to_rule(&self, rule: &AspRule, subst: &Substitution) -> AspRule {
match rule {
AspRule::Normal { head, body } => AspRule::Normal {
head: head
.as_ref()
.map(|h| self.apply_substitution_to_literal(h, subst)),
body: body
.iter()
.map(|l| self.apply_substitution_to_literal(l, subst))
.collect(),
},
AspRule::Choice {
choices,
lower_bound,
upper_bound,
body,
} => AspRule::Choice {
choices: choices.iter().map(|c| c.apply(subst)).collect(),
lower_bound: *lower_bound,
upper_bound: *upper_bound,
body: body
.iter()
.map(|l| self.apply_substitution_to_literal(l, subst))
.collect(),
},
AspRule::Constraint { body } => AspRule::Constraint {
body: body
.iter()
.map(|l| self.apply_substitution_to_literal(l, subst))
.collect(),
},
AspRule::Weak {
body,
weight,
level,
} => AspRule::Weak {
body: body
.iter()
.map(|l| self.apply_substitution_to_literal(l, subst))
.collect(),
weight: *weight,
level: *level,
},
}
}
fn apply_substitution_to_literal(&self, lit: &AspLiteral, subst: &Substitution) -> AspLiteral {
match lit {
AspLiteral::Positive(a) => AspLiteral::Positive(a.apply(subst)),
AspLiteral::Negated(a) => AspLiteral::Negated(a.apply(subst)),
AspLiteral::DefaultNegated(a) => AspLiteral::DefaultNegated(a.apply(subst)),
}
}
fn compute_stable_models(&mut self, program: &GroundProgram) -> Result<Vec<AnswerSet>> {
let mut answer_sets = Vec::new();
let mut potential_atoms: HashSet<Atom> = program.atoms.clone();
for rule in &program.rules {
match rule {
AspRule::Normal {
head: Some(AspLiteral::Positive(a)),
..
} => {
potential_atoms.insert(a.clone());
}
AspRule::Choice { choices, .. } => {
for c in choices {
potential_atoms.insert(c.clone());
}
}
_ => {}
}
}
let mut base_model: HashSet<Atom> = self.facts.clone();
let mut changed = true;
let mut iterations = 0;
while changed && iterations < self.config.max_iterations {
changed = false;
iterations += 1;
for rule in &program.rules {
if let AspRule::Normal {
head: Some(AspLiteral::Positive(head_atom)),
body,
} = rule
{
if !base_model.contains(head_atom) && self.body_satisfied(body, &base_model) {
base_model.insert(head_atom.clone());
changed = true;
}
}
}
}
if self.check_constraints(&program.rules, &base_model) {
answer_sets.push(AnswerSet::new(base_model.clone()));
}
let choice_rules: Vec<_> = program
.rules
.iter()
.filter(|r| matches!(r, AspRule::Choice { .. }))
.collect();
if !choice_rules.is_empty() {
let additional =
self.generate_choice_models(&base_model, &choice_rules, &program.rules)?;
for model in additional {
if answer_sets.len() >= self.config.max_answer_sets {
break;
}
if !answer_sets.iter().any(|as_| as_.atoms == model.atoms) {
answer_sets.push(model);
}
}
}
Ok(answer_sets)
}
fn body_satisfied(&self, body: &[AspLiteral], model: &HashSet<Atom>) -> bool {
for lit in body {
match lit {
AspLiteral::Positive(a) => {
if !model.contains(a) {
return false;
}
}
AspLiteral::Negated(a) => {
if model.contains(a) {
return false;
}
}
AspLiteral::DefaultNegated(a) => {
if model.contains(a) {
return false;
}
}
}
}
true
}
fn check_constraints(&self, rules: &[AspRule], model: &HashSet<Atom>) -> bool {
for rule in rules {
if let AspRule::Constraint { body } = rule {
if self.body_satisfied(body, model) {
return false;
}
}
}
true
}
fn generate_choice_models(
&mut self,
base: &HashSet<Atom>,
choice_rules: &[&AspRule],
all_rules: &[AspRule],
) -> Result<Vec<AnswerSet>> {
let mut models = Vec::new();
for rule in choice_rules {
if let AspRule::Choice {
choices,
lower_bound,
upper_bound,
body,
} = rule
{
if !self.body_satisfied(body, base) {
continue;
}
let lower = lower_bound.unwrap_or(0);
let upper = upper_bound.unwrap_or(choices.len());
let subsets = self.generate_subsets(choices, lower, upper);
for subset in subsets {
let mut model = base.clone();
model.extend(subset);
model = self.apply_rules_until_fixpoint(model, all_rules);
if self.check_constraints(all_rules, &model) {
models.push(AnswerSet::new(model));
if models.len() >= self.config.max_answer_sets {
return Ok(models);
}
}
}
}
}
Ok(models)
}
fn generate_subsets(&self, items: &[Atom], min: usize, max: usize) -> Vec<Vec<Atom>> {
let mut result = Vec::new();
for size in min..=max.min(items.len()) {
let combinations = Self::generate_combinations(items, size);
result.extend(combinations);
}
result
}
fn generate_combinations(items: &[Atom], k: usize) -> Vec<Vec<Atom>> {
if k == 0 {
return vec![Vec::new()];
}
if items.len() < k {
return Vec::new();
}
let mut result = Vec::new();
for (i, item) in items.iter().enumerate() {
let rest = &items[i + 1..];
for mut combo in Self::generate_combinations(rest, k - 1) {
combo.insert(0, item.clone());
result.push(combo);
}
}
result
}
fn apply_rules_until_fixpoint(
&self,
mut model: HashSet<Atom>,
rules: &[AspRule],
) -> HashSet<Atom> {
let mut changed = true;
let mut iterations = 0;
while changed && iterations < self.config.max_iterations {
changed = false;
iterations += 1;
for rule in rules {
if let AspRule::Normal {
head: Some(AspLiteral::Positive(head_atom)),
body,
} = rule
{
if !model.contains(head_atom) && self.body_satisfied(body, &model) {
model.insert(head_atom.clone());
changed = true;
}
}
}
}
model
}
fn optimize(&self, mut answer_sets: Vec<AnswerSet>) -> Vec<AnswerSet> {
answer_sets.sort_by(|a, b| match (a.cost, b.cost) {
(Some(ca), Some(cb)) => ca.cmp(&cb),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
answer_sets.truncate(self.config.max_answer_sets);
answer_sets
}
fn extract_domain(&mut self, rule: &AspRule) {
match rule {
AspRule::Normal { head, body } => {
if let Some(h) = head {
for arg in &h.atom().args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
}
for lit in body {
for arg in &lit.atom().args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
}
}
AspRule::Choice { choices, body, .. } => {
for c in choices {
for arg in &c.args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
}
for lit in body {
for arg in &lit.atom().args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
}
}
AspRule::Constraint { body } | AspRule::Weak { body, .. } => {
for lit in body {
for arg in &lit.atom().args {
if arg.is_ground() {
self.domain.insert(arg.clone());
}
}
}
}
}
}
fn parse_atom(atom_str: &str) -> Result<Atom> {
let atom_str = atom_str.trim();
if let Some(paren_pos) = atom_str.find('(') {
let predicate = atom_str[..paren_pos].trim();
let args_str = atom_str[paren_pos + 1..].trim_end_matches(')');
let args: Vec<AspTerm> = if args_str.is_empty() {
Vec::new()
} else {
args_str
.split(',')
.map(|s| Self::parse_term(s.trim()))
.collect()
};
Ok(Atom::new(predicate, args))
} else {
Ok(Atom::nullary(atom_str))
}
}
fn parse_term(term_str: &str) -> AspTerm {
let term_str = term_str.trim();
if let Ok(i) = term_str.parse::<i64>() {
return AspTerm::Integer(i);
}
if term_str
.chars()
.next()
.is_some_and(|c| c.is_uppercase() || c == '_')
{
return AspTerm::Variable(term_str.to_string());
}
AspTerm::Constant(term_str.to_string())
}
pub fn rule_count(&self) -> usize {
self.rules.len()
}
pub fn fact_count(&self) -> usize {
self.facts.len()
}
pub fn clear(&mut self) {
self.rules.clear();
self.facts.clear();
self.domain.clear();
}
}
impl Default for AspSolver {
fn default() -> Self {
Self::new()
}
}
pub mod conversion {
use super::*;
pub fn rule_atom_to_asp_literal(atom: &RuleAtom) -> Option<AspLiteral> {
match atom {
RuleAtom::Triple {
subject,
predicate,
object,
} => {
let pred = match predicate {
Term::Constant(c) => c.clone(),
_ => return None,
};
let args = vec![term_to_asp_term(subject), term_to_asp_term(object)];
Some(AspLiteral::positive(Atom::new(&pred, args)))
}
RuleAtom::Builtin { name, args } => {
let asp_args: Vec<AspTerm> = args.iter().map(term_to_asp_term).collect();
Some(AspLiteral::positive(Atom::new(name, asp_args)))
}
_ => None,
}
}
pub fn term_to_asp_term(term: &Term) -> AspTerm {
match term {
Term::Variable(v) => AspTerm::Variable(v.clone()),
Term::Constant(c) => AspTerm::Constant(c.clone()),
Term::Literal(l) => AspTerm::Constant(l.clone()),
Term::Function { name, args } => AspTerm::Function {
name: name.clone(),
args: args.iter().map(term_to_asp_term).collect(),
},
}
}
pub fn asp_literal_to_rule_atom(lit: &AspLiteral) -> Option<RuleAtom> {
if let AspLiteral::Positive(atom) = lit {
if atom.args.len() >= 2 {
Some(RuleAtom::Triple {
subject: asp_term_to_term(&atom.args[0]),
predicate: Term::Constant(atom.predicate.clone()),
object: asp_term_to_term(&atom.args[1]),
})
} else {
None
}
} else {
None
}
}
pub fn asp_term_to_term(term: &AspTerm) -> Term {
match term {
AspTerm::Variable(v) => Term::Variable(v.clone()),
AspTerm::Constant(c) => Term::Constant(c.clone()),
AspTerm::Integer(i) => Term::Literal(i.to_string()),
AspTerm::Function { name, args } => Term::Function {
name: name.clone(),
args: args.iter().map(asp_term_to_term).collect(),
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_atom_creation() {
let atom = Atom::new(
"parent",
vec![AspTerm::constant("john"), AspTerm::constant("mary")],
);
assert_eq!(atom.predicate, "parent");
assert_eq!(atom.args.len(), 2);
assert!(atom.is_ground());
}
#[test]
fn test_atom_with_variables() {
let atom = Atom::new(
"parent",
vec![AspTerm::variable("X"), AspTerm::constant("mary")],
);
assert!(!atom.is_ground());
assert!(atom.variables().contains("X"));
}
#[test]
fn test_literal_types() {
let atom = Atom::nullary("rain");
let pos = AspLiteral::positive(atom.clone());
let neg = AspLiteral::negated(atom.clone());
let naf = AspLiteral::not(atom);
assert!(pos.is_positive());
assert!(!neg.is_positive());
assert!(naf.is_default_negated());
}
#[test]
fn test_solver_add_fact() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("node(a)")?;
solver.add_fact("node(b)")?;
assert_eq!(solver.fact_count(), 2);
Ok(())
}
#[test]
fn test_solver_simple_inference() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("parent(john, mary)")?;
let rule = AspRule::normal(
Atom::new(
"ancestor",
vec![AspTerm::variable("X"), AspTerm::variable("Y")],
),
vec![AspLiteral::positive(Atom::new(
"parent",
vec![AspTerm::variable("X"), AspTerm::variable("Y")],
))],
);
solver.add_rule(rule);
let answer_sets = solver.solve()?;
assert!(!answer_sets.is_empty());
Ok(())
}
#[test]
fn test_integrity_constraint() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("node(a)")?;
solver.add_fact("node(b)")?;
solver.add_constraint(vec![
AspLiteral::positive(Atom::new(
"color",
vec![AspTerm::constant("a"), AspTerm::constant("red")],
)),
AspLiteral::positive(Atom::new(
"color",
vec![AspTerm::constant("b"), AspTerm::constant("red")],
)),
]);
let answer_sets = solver.solve()?;
for as_ in &answer_sets {
let a_red = as_.contains(&Atom::new(
"color",
vec![AspTerm::constant("a"), AspTerm::constant("red")],
));
let b_red = as_.contains(&Atom::new(
"color",
vec![AspTerm::constant("b"), AspTerm::constant("red")],
));
assert!(!(a_red && b_red), "Constraint violated");
}
Ok(())
}
#[test]
fn test_choice_rule() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("option(a)")?;
solver.add_fact("option(b)")?;
solver.add_choice_rule(
vec![
Atom::new("selected", vec![AspTerm::constant("a")]),
Atom::new("selected", vec![AspTerm::constant("b")]),
],
Some(1), Some(1),
Vec::new(),
);
let answer_sets = solver.solve()?;
assert!(!answer_sets.is_empty());
Ok(())
}
#[test]
fn test_default_negation() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("bird(tweety)")?;
solver.add_rule(AspRule::normal(
Atom::new("flies", vec![AspTerm::variable("X")]),
vec![
AspLiteral::positive(Atom::new("bird", vec![AspTerm::variable("X")])),
AspLiteral::not(Atom::new("penguin", vec![AspTerm::variable("X")])),
],
));
let answer_sets = solver.solve()?;
assert!(!answer_sets.is_empty());
let flies = Atom::new("flies", vec![AspTerm::constant("tweety")]);
assert!(
answer_sets[0].contains(&flies) || answer_sets.iter().any(|as_| as_.contains(&flies))
);
Ok(())
}
#[test]
fn test_atom_display() {
let atom = Atom::new(
"parent",
vec![AspTerm::constant("john"), AspTerm::constant("mary")],
);
assert_eq!(atom.to_string(), "parent(john, mary)");
let nullary = Atom::nullary("rain");
assert_eq!(nullary.to_string(), "rain");
}
#[test]
fn test_substitution() {
let atom = Atom::new(
"parent",
vec![AspTerm::variable("X"), AspTerm::variable("Y")],
);
let mut subst = Substitution::new();
subst.insert("X".to_string(), AspTerm::constant("john"));
subst.insert("Y".to_string(), AspTerm::constant("mary"));
let grounded = atom.apply(&subst);
assert!(grounded.is_ground());
assert_eq!(grounded.args[0], AspTerm::constant("john"));
assert_eq!(grounded.args[1], AspTerm::constant("mary"));
}
#[test]
fn test_answer_set_display() {
let mut atoms = HashSet::new();
atoms.insert(Atom::new("a", vec![AspTerm::constant("x")]));
atoms.insert(Atom::new("b", vec![AspTerm::constant("y")]));
let as_ = AnswerSet::new(atoms);
let display = as_.to_string();
assert!(display.contains("a(x)") || display.contains("b(y)"));
}
#[test]
fn test_config_builder() {
let config = AspConfig::default()
.with_max_answer_sets(5)
.with_seed(42)
.with_optimization(false);
assert_eq!(config.max_answer_sets, 5);
assert_eq!(config.random_seed, Some(42));
assert!(!config.enable_optimization);
}
#[test]
fn test_solver_clear() -> Result<(), Box<dyn std::error::Error>> {
let mut solver = AspSolver::new();
solver.add_fact("test(a)")?;
solver.add_rule(AspRule::fact(Atom::nullary("b")));
assert_eq!(solver.fact_count(), 1);
assert_eq!(solver.rule_count(), 1);
solver.clear();
assert_eq!(solver.fact_count(), 0);
assert_eq!(solver.rule_count(), 0);
Ok(())
}
#[test]
fn test_weak_constraint() {
let weak = AspRule::weak(
vec![AspLiteral::positive(Atom::new(
"cost",
vec![AspTerm::variable("X"), AspTerm::integer(10)],
))],
10,
1,
);
assert!(matches!(
weak,
AspRule::Weak {
weight: 10,
level: 1,
..
}
));
}
#[test]
fn test_answer_set_predicates() {
let mut atoms = HashSet::new();
atoms.insert(Atom::new(
"color",
vec![AspTerm::constant("a"), AspTerm::constant("red")],
));
atoms.insert(Atom::new(
"color",
vec![AspTerm::constant("b"), AspTerm::constant("blue")],
));
atoms.insert(Atom::new("node", vec![AspTerm::constant("a")]));
let as_ = AnswerSet::new(atoms);
let colors = as_.atoms_with_predicate("color");
assert_eq!(colors.len(), 2);
let nodes = as_.atoms_with_predicate("node");
assert_eq!(nodes.len(), 1);
}
#[test]
fn test_combinations() {
let items = vec![Atom::nullary("a"), Atom::nullary("b"), Atom::nullary("c")];
let combos = AspSolver::generate_combinations(&items, 2);
assert_eq!(combos.len(), 3); }
#[test]
fn test_conversion_term_to_asp() {
let term = Term::Variable("X".to_string());
let asp_term = conversion::term_to_asp_term(&term);
assert!(matches!(asp_term, AspTerm::Variable(v) if v == "X"));
let const_term = Term::Constant("john".to_string());
let asp_const = conversion::term_to_asp_term(&const_term);
assert!(matches!(asp_const, AspTerm::Constant(c) if c == "john"));
}
}