use std::collections::HashSet;
use crate::action::{ActionValidationError, IrAction};
use crate::clause::IrClause;
use crate::query::IrQuery;
#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct IrProgram {
pub clauses: Vec<IrClause>,
pub queries: Vec<IrQuery>,
#[serde(default)]
pub tabled_predicates: HashSet<(String, usize)>,
#[serde(default)]
pub diff_neural_predicates: HashSet<(String, usize, String)>,
#[serde(default)]
pub neural_gen_predicates: HashSet<(String, usize)>,
#[serde(default)]
pub neural_models: std::collections::HashMap<String, String>,
#[serde(default)]
pub actions: Vec<IrAction>,
#[serde(default = "default_neural_unify_threshold")]
pub neural_unify_threshold: f64,
#[serde(default)]
pub type_hierarchy: Vec<(Vec<String>, Option<String>)>,
}
impl IrProgram {
pub fn new() -> Self {
Self {
clauses: vec![],
queries: vec![],
tabled_predicates: HashSet::new(),
diff_neural_predicates: HashSet::new(),
neural_gen_predicates: HashSet::new(),
neural_models: std::collections::HashMap::new(),
actions: vec![],
neural_unify_threshold: default_neural_unify_threshold(),
type_hierarchy: vec![],
}
}
}
fn default_neural_unify_threshold() -> f64 {
0.85
}
impl IrProgram {
pub fn validate_actions(&self) -> Result<(), (usize, ActionValidationError)> {
for (i, action) in self.actions.iter().enumerate() {
if let Err(e) = action.validate() {
return Err((i, e));
}
}
Ok(())
}
}
impl Default for IrProgram {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::action::IrAction;
use crate::clause::IrClause;
use crate::metadata::IrMetadata;
use crate::query::IrQuery;
use crate::term::IrTerm;
fn atom(s: &str) -> IrTerm {
IrTerm::Atom(s.into())
}
fn var(s: &str) -> IrTerm {
IrTerm::Var(s.into())
}
fn structure(name: &str, args: Vec<IrTerm>) -> IrTerm {
IrTerm::Structure { name: name.into(), args }
}
fn fact_clause(name: &str, args: Vec<IrTerm>) -> IrClause {
IrClause { head: structure(name, args), body: vec![], metadata: None }
}
fn query(name: &str, args: Vec<IrTerm>) -> IrQuery {
IrQuery { goal: structure(name, args), metadata: None }
}
fn move_action() -> IrAction {
IrAction {
name: "move".into(),
parameters: vec!["X".into(), "Y".into()],
preconditions: vec![
structure("at", vec![var("X")]),
structure("connected", vec![var("X"), var("Y")]),
],
effects: vec![
structure("at", vec![var("Y")]),
structure("not", vec![structure("at", vec![var("X")])]),
],
metadata: None,
}
}
#[test]
fn test_new_program_is_empty() {
let prog = IrProgram::new();
assert!(prog.clauses.is_empty());
assert!(prog.queries.is_empty());
assert!(prog.actions.is_empty());
assert!(prog.tabled_predicates.is_empty());
assert!(prog.diff_neural_predicates.is_empty());
}
#[test]
fn test_push_action_to_program() {
let mut prog = IrProgram::new();
prog.actions.push(move_action());
assert_eq!(prog.actions.len(), 1);
assert_eq!(prog.actions[0].name, "move");
assert_eq!(prog.actions[0].parameters, vec!["X", "Y"]);
}
#[test]
fn test_program_with_clauses_queries_and_actions() {
let mut prog = IrProgram::new();
prog.clauses.push(fact_clause("at", vec![atom("a")]));
prog.clauses.push(fact_clause("connected", vec![atom("a"), atom("b")]));
prog.queries.push(query("at", vec![var("X")]));
prog.actions.push(move_action());
assert_eq!(prog.clauses.len(), 2);
assert_eq!(prog.queries.len(), 1);
assert_eq!(prog.actions.len(), 1);
}
#[test]
fn test_multiple_actions_in_program() {
let mut prog = IrProgram::new();
prog.actions.push(move_action());
prog.actions.push(IrAction {
name: "pickup".into(),
parameters: vec!["X".into()],
preconditions: vec![structure("clear", vec![var("X")])],
effects: vec![structure("holding", vec![var("X")])],
metadata: None,
});
assert_eq!(prog.actions.len(), 2);
assert_eq!(prog.actions[1].name, "pickup");
}
#[test]
fn test_action_with_probability_metadata() {
let mut prog = IrProgram::new();
prog.actions.push(IrAction {
name: "risky_move".into(),
parameters: vec!["X".into()],
preconditions: vec![structure("at", vec![var("X")])],
effects: vec![],
metadata: Some(IrMetadata {
probability: Some(0.75),
..IrMetadata::default()
}),
});
let prob = prog.actions[0]
.metadata
.as_ref()
.and_then(|m| m.probability);
assert_eq!(prob, Some(0.75));
}
#[test]
fn test_serde_roundtrip_empty() {
let prog = IrProgram::new();
let s = ron::to_string(&prog).unwrap();
let back: IrProgram = ron::from_str(&s).unwrap();
assert_eq!(prog, back);
}
#[test]
fn test_serde_roundtrip_with_actions() {
let mut prog = IrProgram::new();
prog.clauses.push(fact_clause("at", vec![atom("a")]));
prog.actions.push(move_action());
let s = ron::to_string(&prog).unwrap();
let back: IrProgram = ron::from_str(&s).unwrap();
assert_eq!(prog.clauses, back.clauses);
assert_eq!(prog.actions, back.actions);
}
#[test]
fn test_clone_and_eq() {
let mut prog = IrProgram::new();
prog.clauses.push(fact_clause("at", vec![atom("a")]));
prog.actions.push(move_action());
let copy = prog.clone();
assert_eq!(prog, copy);
}
#[test]
fn test_validate_actions_empty_program_is_ok() {
assert!(IrProgram::new().validate_actions().is_ok());
}
#[test]
fn test_validate_actions_all_valid_is_ok() {
let mut prog = IrProgram::new();
prog.actions.push(move_action());
prog.actions.push(IrAction {
name: "pickup".into(),
parameters: vec!["X".into()],
preconditions: vec![structure("clear", vec![var("X")])],
effects: vec![structure("holding", vec![var("X")])],
metadata: None,
});
assert!(prog.validate_actions().is_ok());
}
#[test]
fn test_validate_actions_first_invalid_is_reported() {
use crate::action::ActionValidationError;
let mut prog = IrProgram::new();
prog.actions.push(move_action());
prog.actions.push(IrAction {
name: "".into(),
parameters: vec![],
preconditions: vec![],
effects: vec![],
metadata: None,
});
prog.actions.push(IrAction {
name: "bad".into(),
parameters: vec![],
preconditions: vec![var("Z")], effects: vec![],
metadata: None,
});
let result = prog.validate_actions();
assert!(result.is_err());
let (idx, err) = result.unwrap_err();
assert_eq!(idx, 1);
assert_eq!(err, ActionValidationError::EmptyName);
}
#[test]
fn test_validate_actions_second_invalid_index_correct() {
use crate::action::ActionValidationError;
let mut prog = IrProgram::new();
prog.actions.push(move_action()); prog.actions.push(IrAction { name: "bad_effect".into(),
parameters: vec!["X".into()],
preconditions: vec![],
effects: vec![structure("at", vec![var("Z")])], metadata: None,
});
let (idx, err) = prog.validate_actions().unwrap_err();
assert_eq!(idx, 1);
assert_eq!(
err,
ActionValidationError::UndeclaredVariable("Z".into(), "effects")
);
}
#[test]
fn test_lower_populates_actions_automatically() {
let src = "precond(move(X,Y), at(X)). effect(move(X,Y), at(Y)).";
let items = gollum_parser::parse(src).expect("parse failed");
let prog = crate::lower::lower(&items);
assert_eq!(prog.actions.len(), 1);
assert_eq!(prog.actions[0].name, "move");
assert_eq!(prog.actions[0].parameters, vec!["X", "Y"]);
assert_eq!(prog.actions[0].preconditions.len(), 1);
assert_eq!(prog.actions[0].effects.len(), 1);
}
#[test]
fn test_lower_actions_and_clauses_coexist() {
let src = "at(room1). precond(move(X,Y), at(X)). effect(move(X,Y), at(Y)).";
let items = gollum_parser::parse(src).expect("parse failed");
let prog = crate::lower::lower(&items);
assert_eq!(prog.clauses.len(), 3);
assert_eq!(prog.actions.len(), 1);
}
#[test]
fn test_lower_no_precond_effect_gives_empty_actions() {
let src = "parent(alice, bob). mortal(X) :- human(X).";
let items = gollum_parser::parse(src).expect("parse failed");
let prog = crate::lower::lower(&items);
assert!(prog.actions.is_empty());
}
#[test]
fn test_lower_multiple_actions_populated() {
let src = concat!(
"precond(move(X,Y), at(X)). effect(move(X,Y), at(Y)). ",
"precond(pickup(X), clear(X)). effect(pickup(X), holding(X))."
);
let items = gollum_parser::parse(src).expect("parse failed");
let prog = crate::lower::lower(&items);
assert_eq!(prog.actions.len(), 2);
let names: Vec<&str> = prog.actions.iter().map(|a| a.name.as_str()).collect();
assert!(names.contains(&"move"));
assert!(names.contains(&"pickup"));
}
#[test]
fn test_validate_actions_after_lower() {
let src = "precond(move(X,Y), at(X)). effect(move(X,Y), at(Y)).";
let items = gollum_parser::parse(src).expect("parse failed");
let prog = crate::lower::lower(&items);
assert!(prog.validate_actions().is_ok());
}
}