use std::collections::HashSet;
use crate::{IrAction, IrClause, IrProgram, IrState, IrTerm};
use gollum_pddl::*;
use super::error::PddlIrError;
pub fn ir_to_pddl(
program: &IrProgram,
initial_state: &IrState,
goals: &[IrTerm],
domain_name: &str,
problem_name: &str,
) -> Result<(Domain, Problem), PddlIrError> {
let domain = ir_to_domain(program, domain_name)?;
let init = convert_state_to_init(initial_state)?;
let goal = convert_goals_to_goal_desc(goals)?;
let objects = infer_objects(initial_state);
let problem = Problem {
name: problem_name.into(),
domain_name: domain_name.into(),
requirements: vec![],
objects,
init,
goal,
constraints: None,
metric: None,
};
Ok((domain, problem))
}
pub fn ir_to_domain(
program: &IrProgram,
domain_name: &str,
) -> Result<Domain, PddlIrError> {
let mut actions = Vec::new();
let mut durative_actions = Vec::new();
let mut processes = Vec::new();
let mut events = Vec::new();
for ir_action in &program.actions {
if is_process_ir_action(ir_action) {
processes.push(convert_ir_process(ir_action)?);
} else if is_event_ir_action(ir_action) {
events.push(convert_ir_event(ir_action)?);
} else if is_durative_ir_action(ir_action) {
durative_actions.push(convert_ir_durative_action(ir_action)?);
} else {
actions.push(convert_ir_action(ir_action)?);
}
}
let derived_predicates: Result<Vec<_>, _> = program
.clauses
.iter()
.filter(|c| !c.body.is_empty())
.filter(|c| !is_type_fact(c))
.map(convert_clause_to_derived)
.collect();
let derived_predicates = derived_predicates?;
let predicates = infer_predicates(&actions, &derived_predicates);
let functions = infer_functions_from_both(&actions, &durative_actions);
let types = reconstruct_type_hierarchy(program, &actions, &durative_actions);
let requirements = infer_requirements_full(&actions, &durative_actions, &derived_predicates, &types);
Ok(Domain {
name: domain_name.into(),
requirements,
types,
constants: vec![],
predicates,
functions,
constraints: None,
actions,
durative_actions,
derived_predicates,
axioms: vec![],
processes,
events,
})
}
pub(crate) fn ir_term_to_pddl_term(term: &IrTerm) -> Result<Term, PddlIrError> {
match term {
IrTerm::Atom(s) => Ok(Term::Name(s.clone())),
IrTerm::Var(s) => Ok(Term::Variable(decapitalize_var(s))),
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to PDDL term: {term:?}"),
}),
}
}
pub(crate) fn ir_term_to_goal_desc(term: &IrTerm) -> Result<GoalDesc, PddlIrError> {
match term {
IrTerm::Structure { name, args } if name == "not" && args.len() == 1 => {
Ok(GoalDesc::Not(Box::new(ir_term_to_goal_desc(&args[0])?)))
}
IrTerm::Structure { name, args } if name == "=" && args.len() == 2 => {
Ok(GoalDesc::Equal(
ir_term_to_pddl_term(&args[0])?,
ir_term_to_pddl_term(&args[1])?,
))
}
IrTerm::Structure { name, args } if name == "and" => {
let gds: Result<Vec<_>, _> = args.iter().map(ir_term_to_goal_desc).collect();
Ok(GoalDesc::And(gds?))
}
IrTerm::Structure { name, args } if name == "or" => {
let gds: Result<Vec<_>, _> = args.iter().map(ir_term_to_goal_desc).collect();
Ok(GoalDesc::Or(gds?))
}
IrTerm::Structure { name, args } if name == "fcomp" && args.len() == 3 => {
let comp = match &args[0] {
IrTerm::Atom(s) => match s.as_str() {
"=" => Comparator::Eq,
"<" => Comparator::Lt,
">" => Comparator::Gt,
"<=" => Comparator::LtEq,
">=" => Comparator::GtEq,
other => {
return Err(PddlIrError::ReconstructionError {
reason: format!("unknown comparator: {other}"),
})
}
},
_ => {
return Err(PddlIrError::ReconstructionError {
reason: "fcomp first arg must be comparator atom".into(),
})
}
};
Ok(GoalDesc::FComp(
comp,
ir_term_to_fexp(&args[1])?,
ir_term_to_fexp(&args[2])?,
))
}
IrTerm::Structure { name, args } if name == "preference" && args.len() == 2 => {
let pref_name = match &args[0] {
IrTerm::Atom(s) if s == "_anon" => None,
IrTerm::Atom(s) => Some(s.clone()),
_ => None,
};
let inner = ir_term_to_goal_desc(&args[1])?;
Ok(GoalDesc::Preference(pref_name, Box::new(inner)))
}
IrTerm::Structure { name, args } => {
let terms: Result<Vec<_>, _> = args.iter().map(ir_term_to_pddl_term).collect();
Ok(GoalDesc::Atom(name.clone(), terms?))
}
IrTerm::Atom(s) if s == "true" => Ok(GoalDesc::Empty),
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to PDDL goal: {term:?}"),
}),
}
}
pub(crate) fn ir_term_to_effect(term: &IrTerm) -> Result<Effect, PddlIrError> {
match term {
IrTerm::Structure { name, args }
if name == "when" && args.len() == 2 =>
{
let cond = ir_term_to_goal_desc(&args[0])?;
let eff = match &args[1] {
IrTerm::Structure { name: n, args: inner } if n == "and" => {
let effs: Result<Vec<_>, _> =
inner.iter().map(ir_term_to_effect).collect();
Effect::And(effs?)
}
other => ir_term_to_effect(other)?,
};
Ok(Effect::When(cond, Box::new(eff)))
}
IrTerm::Structure { name, args }
if name == "not" && args.len() == 1 =>
{
match &args[0] {
IrTerm::Structure {
name: pred,
args: inner_args,
} => {
let terms: Result<Vec<_>, _> =
inner_args.iter().map(ir_term_to_pddl_term).collect();
Ok(Effect::Delete(pred.clone(), terms?))
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("expected structure inside not-effect: {:?}", args[0]),
}),
}
}
IrTerm::Structure { name, args }
if is_numeric_effect_name(name) && args.len() == 2 =>
{
let fh = ir_term_to_fhead(&args[0])?;
let fe = ir_term_to_fexp(&args[1])?;
match name.as_str() {
"assign" => Ok(Effect::Assign(fh, fe)),
"increase" => Ok(Effect::Increase(fh, fe)),
"decrease" => Ok(Effect::Decrease(fh, fe)),
"scale-up" => Ok(Effect::ScaleUp(fh, fe)),
"scale-down" => Ok(Effect::ScaleDown(fh, fe)),
_ => unreachable!(),
}
}
IrTerm::Structure { name, args } => {
let terms: Result<Vec<_>, _> = args.iter().map(ir_term_to_pddl_term).collect();
Ok(Effect::Add(name.clone(), terms?))
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to PDDL effect: {term:?}"),
}),
}
}
fn convert_ir_action(action: &IrAction) -> Result<Action, PddlIrError> {
let parameters = reconstruct_typed_lists(
&action.parameters,
action.metadata.as_ref().and_then(|m| m.types.as_ref()),
);
let precondition = if action.preconditions.is_empty() {
None
} else if action.preconditions.len() == 1 {
Some(ir_term_to_goal_desc(&action.preconditions[0])?)
} else {
let gds: Result<Vec<_>, _> = action
.preconditions
.iter()
.map(ir_term_to_goal_desc)
.collect();
Some(GoalDesc::And(gds?))
};
let effect = if action.effects.is_empty() {
None
} else if action.effects.len() == 1 {
Some(ir_term_to_effect(&action.effects[0])?)
} else {
let effs: Result<Vec<_>, _> =
action.effects.iter().map(ir_term_to_effect).collect();
Some(Effect::And(effs?))
};
Ok(Action {
name: action.name.clone(),
parameters,
precondition,
effect,
})
}
fn is_process_ir_action(action: &IrAction) -> bool {
action
.preconditions
.first()
.is_some_and(|p| matches!(p, IrTerm::Atom(s) if s == "__process"))
}
fn is_event_ir_action(action: &IrAction) -> bool {
action
.preconditions
.first()
.is_some_and(|p| matches!(p, IrTerm::Atom(s) if s == "__event"))
}
fn convert_ir_process(action: &IrAction) -> Result<Process, PddlIrError> {
let parameters = reconstruct_typed_lists(
&action.parameters,
action.metadata.as_ref().and_then(|m| m.types.as_ref()),
);
let prec_terms = &action.preconditions[1..];
let precondition = if prec_terms.is_empty() {
GoalDesc::Empty
} else if prec_terms.len() == 1 {
ir_term_to_goal_desc(&prec_terms[0])?
} else {
let gds: Result<Vec<_>, _> = prec_terms.iter().map(ir_term_to_goal_desc).collect();
GoalDesc::And(gds?)
};
let mut effects = Vec::new();
for eff_term in &action.effects {
match eff_term {
IrTerm::Structure { name, args }
if name == "continuous-effect" && args.len() == 1 =>
{
effects.push(ir_term_to_effect(&args[0])?);
}
other => effects.push(ir_term_to_effect(other)?),
}
}
Ok(Process {
name: action.name.clone(),
parameters,
precondition,
effect: effects,
})
}
fn convert_ir_event(action: &IrAction) -> Result<Event, PddlIrError> {
let parameters = reconstruct_typed_lists(
&action.parameters,
action.metadata.as_ref().and_then(|m| m.types.as_ref()),
);
let prec_terms = &action.preconditions[1..];
let precondition = if prec_terms.is_empty() {
GoalDesc::Empty
} else if prec_terms.len() == 1 {
ir_term_to_goal_desc(&prec_terms[0])?
} else {
let gds: Result<Vec<_>, _> = prec_terms.iter().map(ir_term_to_goal_desc).collect();
GoalDesc::And(gds?)
};
let effects: Result<Vec<_>, _> = action
.effects
.iter()
.map(ir_term_to_effect)
.collect();
Ok(Event {
name: action.name.clone(),
parameters,
precondition,
effect: effects?,
})
}
fn is_durative_ir_action(action: &IrAction) -> bool {
let is_temporal = |name: &str| {
matches!(
name,
"duration"
| "at-start"
| "at-end"
| "over-all"
| "continuous-increase"
| "continuous-decrease"
)
};
action.preconditions.iter().any(|p| matches!(p, IrTerm::Structure { name, .. } if is_temporal(name)))
|| action.effects.iter().any(|e| matches!(e, IrTerm::Structure { name, .. } if is_temporal(name)))
}
fn convert_ir_durative_action(action: &IrAction) -> Result<DurativeAction, PddlIrError> {
let parameters = reconstruct_typed_lists(
&action.parameters,
action.metadata.as_ref().and_then(|m| m.types.as_ref()),
);
let mut duration_parts = Vec::new();
let mut condition_parts = Vec::new();
for pre in &action.preconditions {
match pre {
IrTerm::Structure { name, args } if name == "duration" && args.len() == 2 => {
duration_parts.push(ir_term_to_duration_constraint(&args[0], &args[1])?);
}
IrTerm::Structure { name, args }
if (name == "at-start" || name == "at-end" || name == "over-all")
&& args.len() == 1 =>
{
let gd = ir_term_to_goal_desc(&args[0])?;
let dagd = match name.as_str() {
"at-start" => DaGd::AtStart(gd),
"at-end" => DaGd::AtEnd(gd),
"over-all" => DaGd::OverAll(gd),
_ => unreachable!(),
};
condition_parts.push(dagd);
}
_ => {
let gd = ir_term_to_goal_desc(pre)?;
condition_parts.push(DaGd::AtStart(gd));
}
}
}
let mut effect_parts = Vec::new();
for eff in &action.effects {
match eff {
IrTerm::Structure { name, args }
if (name == "at-start" || name == "at-end") && args.len() == 1 =>
{
let e = ir_term_to_effect(&args[0])?;
let dae = match name.as_str() {
"at-start" => DaEffect::AtStart(e),
"at-end" => DaEffect::AtEnd(e),
_ => unreachable!(),
};
effect_parts.push(dae);
}
IrTerm::Structure { name, args }
if name == "continuous-increase" && args.len() == 2 =>
{
let fh = ir_term_to_fhead(&args[0])?;
let fe = ir_term_to_fexp(&args[1])?;
effect_parts.push(DaEffect::ContinuousIncrease(fh, fe));
}
IrTerm::Structure { name, args }
if name == "continuous-decrease" && args.len() == 2 =>
{
let fh = ir_term_to_fhead(&args[0])?;
let fe = ir_term_to_fexp(&args[1])?;
effect_parts.push(DaEffect::ContinuousDecrease(fh, fe));
}
IrTerm::Structure { name, args } if name == "when" && args.len() == 2 => {
let cond = ir_da_gd_from_term(&args[0])?;
let eff = ir_da_effect_from_term(&args[1])?;
effect_parts.push(DaEffect::When(cond, Box::new(eff)));
}
_ => {
let e = ir_term_to_effect(eff)?;
effect_parts.push(DaEffect::AtEnd(e));
}
}
}
let duration = if duration_parts.len() == 1 {
duration_parts.into_iter().next().unwrap()
} else if duration_parts.is_empty() {
DurationConstraint::Cmp(Comparator::Eq, FExp::Number(1.0))
} else {
DurationConstraint::And(duration_parts)
};
let condition = if condition_parts.is_empty() {
None
} else if condition_parts.len() == 1 {
Some(condition_parts.into_iter().next().unwrap())
} else {
Some(DaGd::And(condition_parts))
};
let effect = if effect_parts.is_empty() {
None
} else if effect_parts.len() == 1 {
Some(effect_parts.into_iter().next().unwrap())
} else {
Some(DaEffect::And(effect_parts))
};
Ok(DurativeAction {
name: action.name.clone(),
parameters,
duration,
condition,
effect,
})
}
fn ir_term_to_duration_constraint(
comp_term: &IrTerm,
value_term: &IrTerm,
) -> Result<DurationConstraint, PddlIrError> {
let comp = match comp_term {
IrTerm::Atom(s) => match s.as_str() {
"=" => Comparator::Eq,
"<" => Comparator::Lt,
">" => Comparator::Gt,
"<=" => Comparator::LtEq,
">=" => Comparator::GtEq,
other => {
return Err(PddlIrError::ReconstructionError {
reason: format!("unknown duration comparator: {other}"),
})
}
},
_ => {
return Err(PddlIrError::ReconstructionError {
reason: "duration comparator must be atom".into(),
})
}
};
let fe = ir_term_to_fexp(value_term)?;
Ok(DurationConstraint::Cmp(comp, fe))
}
fn ir_da_gd_from_term(term: &IrTerm) -> Result<DaGd, PddlIrError> {
match term {
IrTerm::Structure { name, args }
if (name == "at-start" || name == "at-end" || name == "over-all")
&& args.len() == 1 =>
{
let gd = ir_term_to_goal_desc(&args[0])?;
Ok(match name.as_str() {
"at-start" => DaGd::AtStart(gd),
"at-end" => DaGd::AtEnd(gd),
"over-all" => DaGd::OverAll(gd),
_ => unreachable!(),
})
}
IrTerm::Structure { name, args } if name == "and" => {
let parts: Result<Vec<_>, _> = args.iter().map(ir_da_gd_from_term).collect();
Ok(DaGd::And(parts?))
}
other => {
let gd = ir_term_to_goal_desc(other)?;
Ok(DaGd::AtStart(gd))
}
}
}
fn ir_da_effect_from_term(term: &IrTerm) -> Result<DaEffect, PddlIrError> {
match term {
IrTerm::Structure { name, args }
if (name == "at-start" || name == "at-end") && args.len() == 1 =>
{
let e = ir_term_to_effect(&args[0])?;
Ok(match name.as_str() {
"at-start" => DaEffect::AtStart(e),
"at-end" => DaEffect::AtEnd(e),
_ => unreachable!(),
})
}
IrTerm::Structure { name, args } if name == "and" => {
let parts: Result<Vec<_>, _> = args.iter().map(ir_da_effect_from_term).collect();
Ok(DaEffect::And(parts?))
}
IrTerm::Structure { name, args }
if name == "continuous-increase" && args.len() == 2 =>
{
let fh = ir_term_to_fhead(&args[0])?;
let fe = ir_term_to_fexp(&args[1])?;
Ok(DaEffect::ContinuousIncrease(fh, fe))
}
IrTerm::Structure { name, args }
if name == "continuous-decrease" && args.len() == 2 =>
{
let fh = ir_term_to_fhead(&args[0])?;
let fe = ir_term_to_fexp(&args[1])?;
Ok(DaEffect::ContinuousDecrease(fh, fe))
}
other => {
let e = ir_term_to_effect(other)?;
Ok(DaEffect::AtEnd(e))
}
}
}
fn convert_clause_to_derived(clause: &IrClause) -> Result<DerivedPredicate, PddlIrError> {
let (name, params) = match &clause.head {
IrTerm::Structure { name, args } => {
let params: Vec<TypedList> = vec![TypedList {
items: args
.iter()
.filter_map(|a| match a {
IrTerm::Var(v) => Some(decapitalize_var(v)),
_ => None,
})
.collect(),
type_name: None,
}];
(name.clone(), params)
}
_ => {
return Err(PddlIrError::ReconstructionError {
reason: "derived predicate head must be a structure".into(),
});
}
};
let body = if clause.body.len() == 1 {
ir_term_to_goal_desc(&clause.body[0])?
} else {
let gds: Result<Vec<_>, _> = clause.body.iter().map(ir_term_to_goal_desc).collect();
GoalDesc::And(gds?)
};
Ok(DerivedPredicate {
predicate: AtomicFormulaSkeleton {
name,
parameters: params,
},
body,
})
}
fn convert_state_to_init(state: &IrState) -> Result<Vec<Init>, PddlIrError> {
state
.facts
.iter()
.map(|fact| match fact {
IrTerm::Structure { name, args } if name == "not" && args.len() == 1 => {
match &args[0] {
IrTerm::Structure {
name: pred,
args: inner_args,
} => {
let names = extract_atom_names(inner_args)?;
Ok(Init::Negative(pred.clone(), names))
}
_ => Err(PddlIrError::ReconstructionError {
reason: "expected structure inside not-init".into(),
}),
}
}
IrTerm::Structure { name, args }
if name == "=" && args.len() == 2 && is_function_value_init(&args[1]) =>
{
match (&args[0], &args[1]) {
(
IrTerm::Structure {
name: fname,
args: fargs,
},
IrTerm::Float(val),
) => {
let obj_names = extract_atom_names(fargs)?;
Ok(Init::FunctionValue(fname.clone(), obj_names, *val))
}
(
IrTerm::Structure {
name: fname,
args: fargs,
},
IrTerm::Number(val),
) => {
let obj_names = extract_atom_names(fargs)?;
Ok(Init::FunctionValue(fname.clone(), obj_names, *val as f64))
}
_ => Err(PddlIrError::ReconstructionError {
reason: "invalid function value init structure".into(),
}),
}
}
IrTerm::Structure { name, args } => {
let names = extract_atom_names(args)?;
Ok(Init::Positive(name.clone(), names))
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("init fact must be a structure: {fact:?}"),
}),
})
.collect()
}
fn convert_goals_to_goal_desc(goals: &[IrTerm]) -> Result<GoalDesc, PddlIrError> {
if goals.is_empty() {
Ok(GoalDesc::Empty)
} else if goals.len() == 1 {
ir_term_to_goal_desc(&goals[0])
} else {
let gds: Result<Vec<_>, _> = goals.iter().map(ir_term_to_goal_desc).collect();
Ok(GoalDesc::And(gds?))
}
}
fn parse_type_str(s: &str) -> Type {
if s.contains('|') {
Type::Either(s.split('|').map(|n| n.to_string()).collect())
} else {
Type::Simple(s.to_string())
}
}
fn reconstruct_typed_lists(
param_names: &[String],
types: Option<&Vec<String>>,
) -> Vec<TypedList> {
match types {
None => {
if param_names.is_empty() {
vec![]
} else {
vec![TypedList {
items: param_names.iter().map(|n| decapitalize_var(n)).collect(),
type_name: None,
}]
}
}
Some(type_list) => {
let mut result = Vec::new();
let mut current_type: Option<&str> = None;
let mut current_items = Vec::new();
for (name, ty) in param_names.iter().zip(type_list.iter()) {
if current_type == Some(ty.as_str()) {
current_items.push(decapitalize_var(name));
} else {
if !current_items.is_empty() {
result.push(TypedList {
items: std::mem::take(&mut current_items),
type_name: current_type.map(parse_type_str),
});
}
current_type = Some(ty);
current_items.push(decapitalize_var(name));
}
}
if !current_items.is_empty() {
result.push(TypedList {
items: current_items,
type_name: current_type.map(parse_type_str),
});
}
result
}
}
}
fn infer_requirements_full(
actions: &[Action],
durative_actions: &[DurativeAction],
derived: &[DerivedPredicate],
types: &[TypeDecl],
) -> Vec<Requirement> {
let mut reqs = infer_requirements(actions, derived, types);
if !durative_actions.is_empty() && !reqs.contains(&Requirement::DurativeActions) {
reqs.push(Requirement::DurativeActions);
}
reqs
}
fn infer_requirements(
actions: &[Action],
derived: &[DerivedPredicate],
types: &[TypeDecl],
) -> Vec<Requirement> {
let mut reqs = vec![Requirement::Strips];
if !types.is_empty() {
reqs.push(Requirement::Typing);
}
let has_neg_prec = actions.iter().any(|a| {
a.precondition
.as_ref()
.is_some_and(goal_desc_contains_not)
});
if has_neg_prec {
reqs.push(Requirement::NegativePreconditions);
}
if !derived.is_empty() {
reqs.push(Requirement::DerivedPredicates);
}
let has_conditional = actions.iter().any(|a| {
a.effect
.as_ref()
.is_some_and(effect_contains_when)
});
if has_conditional {
reqs.push(Requirement::ConditionalEffects);
}
let has_disjunctive = actions.iter().any(|a| {
a.precondition
.as_ref()
.is_some_and(goal_desc_contains_or)
});
if has_disjunctive {
reqs.push(Requirement::DisjunctivePreconditions);
}
let has_equality = actions.iter().any(|a| {
a.precondition
.as_ref()
.is_some_and(goal_desc_contains_eq)
});
if has_equality {
reqs.push(Requirement::Equality);
}
let has_numeric = actions.iter().any(|a| {
let in_prec = a
.precondition
.as_ref()
.is_some_and(goal_desc_contains_fcomp);
let in_eff = a.effect.as_ref().is_some_and(effect_contains_numeric);
in_prec || in_eff
});
if has_numeric {
reqs.push(Requirement::NumericFluents);
}
reqs
}
fn infer_predicates(
actions: &[Action],
derived: &[DerivedPredicate],
) -> Vec<AtomicFormulaSkeleton> {
let mut seen: HashSet<(String, usize)> = HashSet::new();
let mut predicates = Vec::new();
let mut record = |name: &str, arity: usize| {
if seen.insert((name.to_string(), arity)) {
predicates.push(AtomicFormulaSkeleton {
name: name.to_string(),
parameters: vec![TypedList {
items: (0..arity).map(|i| format!("x{i}")).collect(),
type_name: None,
}],
});
}
};
for action in actions {
if let Some(ref pre) = action.precondition {
collect_predicate_signatures(pre, &mut record);
}
if let Some(ref eff) = action.effect {
collect_effect_signatures(eff, &mut record);
}
}
for dp in derived {
record(&dp.predicate.name, dp.predicate.parameters.iter().map(|tl| tl.items.len()).sum());
}
predicates
}
fn infer_functions_from_both(
actions: &[Action],
durative_actions: &[DurativeAction],
) -> Vec<FunctionDef> {
let mut fns = infer_functions(actions);
let mut seen: HashSet<(String, usize)> = HashSet::new();
for fd in &fns {
for f in &fd.functions {
seen.insert((f.name.clone(), f.parameters.iter().map(|tl| tl.items.len()).sum()));
}
}
let mut extra = Vec::new();
let mut record = |name: &str, arity: usize| {
if seen.insert((name.to_string(), arity)) {
extra.push(AtomicFormulaSkeleton {
name: name.to_string(),
parameters: vec![TypedList {
items: (0..arity).map(|i| format!("x{i}")).collect(),
type_name: None,
}],
});
}
};
for da in durative_actions {
if let Some(ref cond) = da.condition {
collect_dagd_function_heads(cond, &mut record);
}
if let Some(ref eff) = da.effect {
collect_da_effect_function_heads(eff, &mut record);
}
}
if !extra.is_empty() {
if fns.is_empty() {
fns.push(FunctionDef {
functions: extra,
return_type: None,
});
} else {
fns[0].functions.extend(extra);
}
}
fns
}
fn collect_dagd_function_heads(dagd: &DaGd, record: &mut impl FnMut(&str, usize)) {
match dagd {
DaGd::AtStart(gd) | DaGd::AtEnd(gd) | DaGd::OverAll(gd) => {
collect_goal_function_heads(gd, record);
}
DaGd::And(gds) => {
for g in gds {
collect_dagd_function_heads(g, record);
}
}
DaGd::Forall(_, inner) => collect_dagd_function_heads(inner, record),
DaGd::Preference(_, inner) => collect_dagd_function_heads(inner, record),
}
}
fn collect_da_effect_function_heads(daeff: &DaEffect, record: &mut impl FnMut(&str, usize)) {
match daeff {
DaEffect::AtStart(eff) | DaEffect::AtEnd(eff) => {
collect_effect_function_heads(eff, record);
}
DaEffect::And(effs) => {
for e in effs {
collect_da_effect_function_heads(e, record);
}
}
DaEffect::Forall(_, inner) => collect_da_effect_function_heads(inner, record),
DaEffect::When(cond, eff) => {
collect_dagd_function_heads(cond, record);
collect_da_effect_function_heads(eff, record);
}
DaEffect::ContinuousIncrease(fh, _) | DaEffect::ContinuousDecrease(fh, _) => {
record(&fh.name, fh.args.len());
}
}
}
fn infer_functions(actions: &[Action]) -> Vec<FunctionDef> {
let mut seen: HashSet<(String, usize)> = HashSet::new();
let mut functions = Vec::new();
let mut record = |name: &str, arity: usize| {
if seen.insert((name.to_string(), arity)) {
functions.push(AtomicFormulaSkeleton {
name: name.to_string(),
parameters: vec![TypedList {
items: (0..arity).map(|i| format!("x{i}")).collect(),
type_name: None,
}],
});
}
};
for action in actions {
if let Some(ref pre) = action.precondition {
collect_goal_function_heads(pre, &mut record);
}
if let Some(ref eff) = action.effect {
collect_effect_function_heads(eff, &mut record);
}
}
if functions.is_empty() {
vec![]
} else {
vec![FunctionDef {
functions,
return_type: None, }]
}
}
fn collect_goal_function_heads(gd: &GoalDesc, record: &mut impl FnMut(&str, usize)) {
match gd {
GoalDesc::FComp(_, lhs, rhs) => {
collect_fexp_function_heads(lhs, record);
collect_fexp_function_heads(rhs, record);
}
GoalDesc::Not(inner) => collect_goal_function_heads(inner, record),
GoalDesc::And(gds) | GoalDesc::Or(gds) => {
for g in gds {
collect_goal_function_heads(g, record);
}
}
GoalDesc::Imply(a, b) => {
collect_goal_function_heads(a, record);
collect_goal_function_heads(b, record);
}
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => {
collect_goal_function_heads(inner, record)
}
_ => {}
}
}
fn collect_effect_function_heads(eff: &Effect, record: &mut impl FnMut(&str, usize)) {
match eff {
Effect::Assign(fh, _)
| Effect::Increase(fh, _)
| Effect::Decrease(fh, _)
| Effect::ScaleUp(fh, _)
| Effect::ScaleDown(fh, _) => record(&fh.name, fh.args.len()),
Effect::And(effs) => {
for e in effs {
collect_effect_function_heads(e, record);
}
}
Effect::When(gd, eff) => {
collect_goal_function_heads(gd, record);
collect_effect_function_heads(eff, record);
}
Effect::Forall(_, eff) => collect_effect_function_heads(eff, record),
_ => {}
}
}
fn collect_fexp_function_heads(fe: &FExp, record: &mut impl FnMut(&str, usize)) {
match fe {
FExp::FHead(fh) => record(&fh.name, fh.args.len()),
FExp::Negate(inner) => collect_fexp_function_heads(inner, record),
FExp::BinaryOp(_, lhs, rhs) => {
collect_fexp_function_heads(lhs, record);
collect_fexp_function_heads(rhs, record);
}
_ => {}
}
}
fn reconstruct_type_hierarchy(
program: &IrProgram,
actions: &[Action],
durative_actions: &[DurativeAction],
) -> Vec<TypeDecl> {
if !program.type_hierarchy.is_empty() {
return program
.type_hierarchy
.iter()
.map(|(names, parent)| TypeDecl {
names: names.clone(),
parent: parent.clone(),
})
.collect();
}
infer_types_from_both(actions, durative_actions)
}
fn infer_types_from_both(
actions: &[Action],
durative_actions: &[DurativeAction],
) -> Vec<TypeDecl> {
let mut type_names: HashSet<String> = HashSet::new();
for action in actions {
for tl in &action.parameters {
if let Some(Type::Simple(t)) = &tl.type_name {
type_names.insert(t.clone());
}
}
}
for da in durative_actions {
for tl in &da.parameters {
if let Some(Type::Simple(t)) = &tl.type_name {
type_names.insert(t.clone());
}
}
}
if type_names.is_empty() {
return vec![];
}
let mut names: Vec<_> = type_names.into_iter().collect();
names.sort();
vec![TypeDecl {
names,
parent: Some("object".into()),
}]
}
#[allow(dead_code)]
fn infer_types(actions: &[Action]) -> Vec<TypeDecl> {
let mut type_names: HashSet<String> = HashSet::new();
for action in actions {
for tl in &action.parameters {
if let Some(Type::Simple(t)) = &tl.type_name {
type_names.insert(t.clone());
}
}
}
if type_names.is_empty() {
return vec![];
}
let mut names: Vec<_> = type_names.into_iter().collect();
names.sort();
vec![TypeDecl {
names,
parent: Some("object".into()),
}]
}
fn infer_objects(state: &IrState) -> Vec<TypedList> {
let mut objects: HashSet<String> = HashSet::new();
for fact in &state.facts {
collect_atoms(fact, &mut objects);
}
if objects.is_empty() {
return vec![];
}
let mut names: Vec<_> = objects.into_iter().collect();
names.sort();
vec![TypedList {
items: names,
type_name: None,
}]
}
#[allow(clippy::too_many_arguments)]
pub fn ir_to_pddl_full(
program: &IrProgram,
initial_state: &IrState,
goals: &[IrTerm],
domain_name: &str,
problem_name: &str,
domain_constraints: Option<&IrTerm>,
problem_constraints: &[IrTerm],
metric: Option<&IrTerm>,
) -> Result<(Domain, Problem), PddlIrError> {
let mut domain = ir_to_domain(program, domain_name)?;
if let Some(con_term) = domain_constraints {
domain.constraints = Some(ir_term_to_con_gd(con_term)?);
}
let init = convert_state_to_init(initial_state)?;
let goal = convert_goals_to_goal_desc(goals)?;
let objects = infer_objects(initial_state);
let p_constraints = if problem_constraints.is_empty() {
None
} else {
let parts: Result<Vec<_>, _> =
problem_constraints.iter().map(ir_term_to_pref_con_gd_item).collect();
let parts = parts?;
if parts.len() == 1 {
Some(parts.into_iter().next().unwrap())
} else {
Some(PrefConGd::And(parts))
}
};
let p_metric = match metric {
Some(m) => Some(ir_term_to_metric(m)?),
None => None,
};
if domain.constraints.is_some()
&& !domain.requirements.contains(&Requirement::Constraints)
{
domain.requirements.push(Requirement::Constraints);
}
let has_preferences = goals.iter().any(is_preference_term)
|| problem_constraints.iter().any(is_preference_term)
|| domain.actions.iter().any(|a| {
a.precondition
.as_ref()
.is_some_and(goal_desc_contains_preference)
});
if has_preferences && !domain.requirements.contains(&Requirement::Preferences) {
domain.requirements.push(Requirement::Preferences);
}
let problem = Problem {
name: problem_name.into(),
domain_name: domain_name.into(),
requirements: vec![],
objects,
init,
goal,
constraints: p_constraints,
metric: p_metric,
};
Ok((domain, problem))
}
pub(crate) fn ir_term_to_con_gd(term: &IrTerm) -> Result<ConGd, PddlIrError> {
match term {
IrTerm::Structure { name, args } if name == "and" => {
let gds: Result<Vec<_>, _> = args.iter().map(ir_term_to_con_gd).collect();
Ok(ConGd::And(gds?))
}
IrTerm::Structure { name, args } if name == "at-end" && args.len() == 1 => {
Ok(ConGd::AtEnd(ir_term_to_goal_desc(&args[0])?))
}
IrTerm::Structure { name, args } if name == "always" && args.len() == 1 => {
Ok(ConGd::Always(ir_term_to_goal_desc(&args[0])?))
}
IrTerm::Structure { name, args } if name == "sometime" && args.len() == 1 => {
Ok(ConGd::Sometime(ir_term_to_goal_desc(&args[0])?))
}
IrTerm::Structure { name, args } if name == "within" && args.len() == 2 => {
let t = extract_float(&args[0])?;
Ok(ConGd::Within(t, ir_term_to_goal_desc(&args[1])?))
}
IrTerm::Structure { name, args } if name == "at-most-once" && args.len() == 1 => {
Ok(ConGd::AtMostOnce(ir_term_to_goal_desc(&args[0])?))
}
IrTerm::Structure { name, args } if name == "sometime-after" && args.len() == 2 => {
Ok(ConGd::SometimeAfter(
ir_term_to_goal_desc(&args[0])?,
ir_term_to_goal_desc(&args[1])?,
))
}
IrTerm::Structure { name, args } if name == "sometime-before" && args.len() == 2 => {
Ok(ConGd::SometimeBefore(
ir_term_to_goal_desc(&args[0])?,
ir_term_to_goal_desc(&args[1])?,
))
}
IrTerm::Structure { name, args } if name == "always-within" && args.len() == 3 => {
let t = extract_float(&args[0])?;
Ok(ConGd::AlwaysWithin(
t,
ir_term_to_goal_desc(&args[1])?,
ir_term_to_goal_desc(&args[2])?,
))
}
IrTerm::Structure { name, args } if name == "hold-during" && args.len() == 3 => {
let t1 = extract_float(&args[0])?;
let t2 = extract_float(&args[1])?;
Ok(ConGd::HoldDuring(t1, t2, ir_term_to_goal_desc(&args[2])?))
}
IrTerm::Structure { name, args } if name == "hold-after" && args.len() == 2 => {
let t = extract_float(&args[0])?;
Ok(ConGd::HoldAfter(t, ir_term_to_goal_desc(&args[1])?))
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to ConGd: {term:?}"),
}),
}
}
fn ir_term_to_pref_con_gd_item(term: &IrTerm) -> Result<PrefConGd, PddlIrError> {
match term {
IrTerm::Structure { name, args } if name == "preference" && args.len() == 2 => {
let pref_name = match &args[0] {
IrTerm::Atom(s) if s == "_anon" => None,
IrTerm::Atom(s) => Some(s.clone()),
_ => None,
};
let con = ir_term_to_con_gd(&args[1])?;
Ok(PrefConGd::Preference(pref_name, con))
}
_ => {
let gd = ir_term_to_goal_desc(term)?;
Ok(PrefConGd::Goal(gd))
}
}
}
pub(crate) fn ir_term_to_metric(term: &IrTerm) -> Result<MetricSpec, PddlIrError> {
match term {
IrTerm::Structure { name, args } if name == "metric" && args.len() == 2 => {
let optimization = match &args[0] {
IrTerm::Atom(s) if s == "minimize" => Optimization::Minimize,
IrTerm::Atom(s) if s == "maximize" => Optimization::Maximize,
_ => {
return Err(PddlIrError::ReconstructionError {
reason: "metric optimization must be minimize or maximize".into(),
})
}
};
let expression = ir_term_to_fexp(&args[1])?;
Ok(MetricSpec {
optimization,
expression,
})
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to MetricSpec: {term:?}"),
}),
}
}
fn extract_float(term: &IrTerm) -> Result<f64, PddlIrError> {
match term {
IrTerm::Float(v) => Ok(*v),
IrTerm::Number(v) => Ok(*v as f64),
_ => Err(PddlIrError::ReconstructionError {
reason: format!("expected float, got: {term:?}"),
}),
}
}
fn is_preference_term(term: &IrTerm) -> bool {
matches!(term, IrTerm::Structure { name, .. } if name == "preference")
}
fn goal_desc_contains_preference(gd: &GoalDesc) -> bool {
match gd {
GoalDesc::Preference(..) => true,
GoalDesc::And(gds) | GoalDesc::Or(gds) => gds.iter().any(goal_desc_contains_preference),
GoalDesc::Not(inner) => goal_desc_contains_preference(inner),
GoalDesc::Imply(a, b) => {
goal_desc_contains_preference(a) || goal_desc_contains_preference(b)
}
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => {
goal_desc_contains_preference(inner)
}
_ => false,
}
}
pub(crate) fn ir_term_to_fexp(term: &IrTerm) -> Result<FExp, PddlIrError> {
match term {
IrTerm::Float(f) => Ok(FExp::Number(*f)),
IrTerm::Number(n) => Ok(FExp::Number(*n as f64)),
IrTerm::Structure { name, args } if name == "negate" && args.len() == 1 => {
Ok(FExp::Negate(Box::new(ir_term_to_fexp(&args[0])?)))
}
IrTerm::Structure { name, args }
if is_binary_op(name) && args.len() == 2 =>
{
let op = match name.as_str() {
"+" => BinaryOp::Add,
"-" => BinaryOp::Sub,
"*" => BinaryOp::Mul,
"/" => BinaryOp::Div,
_ => unreachable!(),
};
Ok(FExp::BinaryOp(
op,
Box::new(ir_term_to_fexp(&args[0])?),
Box::new(ir_term_to_fexp(&args[1])?),
))
}
IrTerm::Structure { name, args } if name == "is-violated" && args.len() == 1 => {
match &args[0] {
IrTerm::Atom(s) => Ok(FExp::IsViolated(s.clone())),
_ => Err(PddlIrError::ReconstructionError {
reason: "is-violated arg must be atom".into(),
}),
}
}
IrTerm::Atom(s) if s == "?duration" => Ok(FExp::Duration),
IrTerm::Atom(s) if s == "#t" => Ok(FExp::Time),
IrTerm::Atom(s) if s == "total-time" => Ok(FExp::TotalTime),
IrTerm::Structure { name, args } => {
Ok(FExp::FHead(ir_term_to_fhead_inner(name, args)?))
}
_ => Err(PddlIrError::ReconstructionError {
reason: format!("cannot convert IR term to FExp: {term:?}"),
}),
}
}
pub(crate) fn ir_term_to_fhead(term: &IrTerm) -> Result<FHead, PddlIrError> {
match term {
IrTerm::Structure { name, args } => ir_term_to_fhead_inner(name, args),
_ => Err(PddlIrError::ReconstructionError {
reason: format!("expected structure for FHead: {term:?}"),
}),
}
}
fn ir_term_to_fhead_inner(name: &str, args: &[IrTerm]) -> Result<FHead, PddlIrError> {
let pddl_args: Result<Vec<_>, _> = args.iter().map(ir_term_to_pddl_term).collect();
Ok(FHead {
name: name.to_string(),
args: pddl_args?,
})
}
fn is_binary_op(name: &str) -> bool {
matches!(name, "+" | "-" | "*" | "/")
}
fn is_numeric_effect_name(name: &str) -> bool {
matches!(
name,
"assign" | "increase" | "decrease" | "scale-up" | "scale-down"
)
}
fn is_function_value_init(term: &IrTerm) -> bool {
matches!(term, IrTerm::Float(_) | IrTerm::Number(_))
}
pub(crate) fn decapitalize_var(ir_name: &str) -> String {
let mut chars = ir_name.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_lowercase().collect::<String>() + chars.as_str(),
}
}
fn extract_atom_names(args: &[IrTerm]) -> Result<Vec<String>, PddlIrError> {
args.iter()
.map(|a| match a {
IrTerm::Atom(s) => Ok(s.clone()),
_ => Err(PddlIrError::ReconstructionError {
reason: format!("expected atom in init, got {a:?}"),
}),
})
.collect()
}
fn collect_atoms(term: &IrTerm, set: &mut HashSet<String>) {
match term {
IrTerm::Atom(s) => {
set.insert(s.clone());
}
IrTerm::Structure { args, .. } => {
for a in args {
collect_atoms(a, set);
}
}
_ => {}
}
}
fn is_type_fact(_clause: &IrClause) -> bool {
false
}
fn effect_contains_when(eff: &Effect) -> bool {
match eff {
Effect::When(..) => true,
Effect::And(effs) => effs.iter().any(effect_contains_when),
Effect::Forall(_, inner) => effect_contains_when(inner),
_ => false,
}
}
fn goal_desc_contains_not(gd: &GoalDesc) -> bool {
match gd {
GoalDesc::Not(_) => true,
GoalDesc::And(gds) | GoalDesc::Or(gds) => gds.iter().any(goal_desc_contains_not),
GoalDesc::Imply(a, b) => goal_desc_contains_not(a) || goal_desc_contains_not(b),
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => goal_desc_contains_not(inner),
_ => false,
}
}
fn goal_desc_contains_or(gd: &GoalDesc) -> bool {
match gd {
GoalDesc::Or(_) => true,
GoalDesc::And(gds) => gds.iter().any(goal_desc_contains_or),
GoalDesc::Not(inner) => goal_desc_contains_or(inner),
GoalDesc::Imply(a, b) => goal_desc_contains_or(a) || goal_desc_contains_or(b),
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => goal_desc_contains_or(inner),
_ => false,
}
}
fn goal_desc_contains_fcomp(gd: &GoalDesc) -> bool {
match gd {
GoalDesc::FComp(..) => true,
GoalDesc::And(gds) | GoalDesc::Or(gds) => gds.iter().any(goal_desc_contains_fcomp),
GoalDesc::Not(inner) => goal_desc_contains_fcomp(inner),
GoalDesc::Imply(a, b) => goal_desc_contains_fcomp(a) || goal_desc_contains_fcomp(b),
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => {
goal_desc_contains_fcomp(inner)
}
_ => false,
}
}
fn effect_contains_numeric(eff: &Effect) -> bool {
match eff {
Effect::Assign(..)
| Effect::Increase(..)
| Effect::Decrease(..)
| Effect::ScaleUp(..)
| Effect::ScaleDown(..) => true,
Effect::And(effs) => effs.iter().any(effect_contains_numeric),
Effect::When(_, inner) | Effect::Forall(_, inner) => effect_contains_numeric(inner),
_ => false,
}
}
fn goal_desc_contains_eq(gd: &GoalDesc) -> bool {
match gd {
GoalDesc::Equal(..) => true,
GoalDesc::And(gds) | GoalDesc::Or(gds) => gds.iter().any(goal_desc_contains_eq),
GoalDesc::Not(inner) => goal_desc_contains_eq(inner),
GoalDesc::Imply(a, b) => goal_desc_contains_eq(a) || goal_desc_contains_eq(b),
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => goal_desc_contains_eq(inner),
_ => false,
}
}
fn collect_predicate_signatures(gd: &GoalDesc, record: &mut impl FnMut(&str, usize)) {
match gd {
GoalDesc::Atom(name, args) => record(name, args.len()),
GoalDesc::Not(inner) => collect_predicate_signatures(inner, record),
GoalDesc::And(gds) | GoalDesc::Or(gds) => {
for g in gds {
collect_predicate_signatures(g, record);
}
}
GoalDesc::Imply(a, b) => {
collect_predicate_signatures(a, record);
collect_predicate_signatures(b, record);
}
GoalDesc::Exists(_, inner) | GoalDesc::Forall(_, inner) => {
collect_predicate_signatures(inner, record);
}
_ => {}
}
}
fn collect_effect_signatures(eff: &Effect, record: &mut impl FnMut(&str, usize)) {
match eff {
Effect::Add(name, args) => record(name, args.len()),
Effect::Delete(name, args) => record(name, args.len()),
Effect::And(effs) => {
for e in effs {
collect_effect_signatures(e, record);
}
}
Effect::When(gd, eff) => {
collect_predicate_signatures(gd, record);
collect_effect_signatures(eff, record);
}
Effect::Forall(_, eff) => collect_effect_signatures(eff, record),
_ => {}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{IrAction, IrMetadata};
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,
}
}
#[test]
fn test_ir_atom_to_pddl_name() {
assert_eq!(
ir_term_to_pddl_term(&atom("room1")).unwrap(),
Term::Name("room1".into())
);
}
#[test]
fn test_ir_var_to_pddl_variable() {
assert_eq!(
ir_term_to_pddl_term(&var("X")).unwrap(),
Term::Variable("x".into())
);
assert_eq!(
ir_term_to_pddl_term(&var("From")).unwrap(),
Term::Variable("from".into())
);
}
#[test]
fn test_ir_structure_to_goal_desc() {
let term = structure("at", vec![atom("a"), atom("b")]);
let gd = ir_term_to_goal_desc(&term).unwrap();
assert_eq!(
gd,
GoalDesc::Atom("at".into(), vec![Term::Name("a".into()), Term::Name("b".into())])
);
}
#[test]
fn test_ir_not_to_goal_desc() {
let term = structure("not", vec![structure("clear", vec![var("X")])]);
let gd = ir_term_to_goal_desc(&term).unwrap();
match gd {
GoalDesc::Not(inner) => match *inner {
GoalDesc::Atom(name, args) => {
assert_eq!(name, "clear");
assert_eq!(args, vec![Term::Variable("x".into())]);
}
_ => panic!("expected Atom"),
},
_ => panic!("expected Not"),
}
}
#[test]
fn test_ir_action_to_pddl_action() {
let ir = IrAction {
name: "move".into(),
parameters: vec!["V".into(), "From".into(), "To".into()],
preconditions: vec![structure("at", vec![var("V"), var("From")])],
effects: vec![
structure("at", vec![var("V"), var("To")]),
structure("not", vec![structure("at", vec![var("V"), var("From")])]),
],
metadata: Some(IrMetadata {
types: Some(vec![
"vehicle".into(),
"location".into(),
"location".into(),
]),
..IrMetadata::default()
}),
};
let action = convert_ir_action(&ir).unwrap();
assert_eq!(action.name, "move");
assert_eq!(action.parameters.len(), 2); assert_eq!(action.parameters[0].items, vec!["v"]);
assert_eq!(
action.parameters[0].type_name,
Some(Type::Simple("vehicle".into()))
);
assert_eq!(action.parameters[1].items, vec!["from", "to"]);
assert_eq!(
action.parameters[1].type_name,
Some(Type::Simple("location".into()))
);
assert!(action.precondition.is_some());
assert!(action.effect.is_some());
}
#[test]
fn test_reconstruct_typed_lists_with_types() {
let params = vec!["V".into(), "From".into(), "To".into()];
let types = vec!["vehicle".into(), "location".into(), "location".into()];
let tls = reconstruct_typed_lists(¶ms, Some(&types));
assert_eq!(tls.len(), 2);
assert_eq!(tls[0].items, vec!["v"]);
assert_eq!(tls[1].items, vec!["from", "to"]);
}
#[test]
fn test_reconstruct_typed_lists_no_types() {
let params = vec!["X".into(), "Y".into()];
let tls = reconstruct_typed_lists(¶ms, None);
assert_eq!(tls.len(), 1);
assert_eq!(tls[0].items, vec!["x", "y"]);
assert!(tls[0].type_name.is_none());
}
#[test]
fn test_infer_requirements_strips_only() {
let reqs = infer_requirements(&[], &[], &[]);
assert_eq!(reqs, vec![Requirement::Strips]);
}
#[test]
fn test_infer_requirements_typing() {
let types = vec![TypeDecl {
names: vec!["location".into()],
parent: Some("object".into()),
}];
let reqs = infer_requirements(&[], &[], &types);
assert!(reqs.contains(&Requirement::Strips));
assert!(reqs.contains(&Requirement::Typing));
}
#[test]
fn test_state_to_init() {
let state = IrState::with_facts(vec![
structure("on", vec![atom("a"), atom("b")]),
structure("clear", vec![atom("a")]),
]);
let inits = convert_state_to_init(&state).unwrap();
assert_eq!(inits.len(), 2);
assert_eq!(
inits[0],
Init::Positive("on".into(), vec!["a".into(), "b".into()])
);
}
#[test]
fn test_goals_to_goal_desc() {
let goals = vec![
structure("on", vec![atom("a"), atom("b")]),
structure("clear", vec![atom("a")]),
];
let gd = convert_goals_to_goal_desc(&goals).unwrap();
match gd {
GoalDesc::And(gds) => assert_eq!(gds.len(), 2),
_ => panic!("expected And"),
}
}
#[test]
fn test_decapitalize_var() {
assert_eq!(decapitalize_var("X"), "x");
assert_eq!(decapitalize_var("From"), "from");
assert_eq!(decapitalize_var("From-waypoint"), "from-waypoint");
}
#[test]
fn test_roundtrip_action() {
use crate::pddl::to_ir;
let pddl_action = Action {
name: "pick".into(),
parameters: vec![TypedList {
items: vec!["x".into()],
type_name: Some(Type::Simple("block".into())),
}],
precondition: Some(GoalDesc::And(vec![
GoalDesc::Atom("clear".into(), vec![Term::Variable("x".into())]),
GoalDesc::Atom("arm-empty".into(), vec![]),
])),
effect: Some(Effect::And(vec![
Effect::Add("holding".into(), vec![Term::Variable("x".into())]),
Effect::Delete("clear".into(), vec![Term::Variable("x".into())]),
])),
};
let ir_action = to_ir::domain_to_ir(&Domain {
name: "test".into(),
actions: vec![pddl_action.clone()],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_action.actions;
let domain = ir_to_domain(&program, "test").unwrap();
let rt_action = &domain.actions[0];
assert_eq!(rt_action.name, "pick");
assert_eq!(rt_action.parameters[0].items, vec!["x"]);
assert_eq!(
rt_action.parameters[0].type_name,
Some(Type::Simple("block".into()))
);
match rt_action.precondition.as_ref().unwrap() {
GoalDesc::And(gds) => assert_eq!(gds.len(), 2),
_ => panic!("expected And precondition"),
}
match rt_action.effect.as_ref().unwrap() {
Effect::And(effs) => assert_eq!(effs.len(), 2),
_ => panic!("expected And effect"),
}
}
#[test]
fn test_when_effect_to_pddl() {
let term = structure(
"when",
vec![
structure("fragile", vec![var("X")]),
structure("broken", vec![var("X")]),
],
);
let eff = ir_term_to_effect(&term).unwrap();
match eff {
Effect::When(cond, inner) => {
assert_eq!(
cond,
GoalDesc::Atom("fragile".into(), vec![Term::Variable("x".into())])
);
assert_eq!(
*inner,
Effect::Add("broken".into(), vec![Term::Variable("x".into())])
);
}
_ => panic!("expected When effect"),
}
}
#[test]
fn test_when_effect_with_and_body() {
let term = structure(
"when",
vec![
structure("heavy", vec![]),
structure(
"and",
vec![
structure("damaged", vec![]),
structure("not", vec![structure("intact", vec![])]),
],
),
],
);
let eff = ir_term_to_effect(&term).unwrap();
match eff {
Effect::When(_, inner) => match *inner {
Effect::And(effs) => {
assert_eq!(effs.len(), 2);
assert_eq!(effs[0], Effect::Add("damaged".into(), vec![]));
assert_eq!(effs[1], Effect::Delete("intact".into(), vec![]));
}
_ => panic!("expected And inside When"),
},
_ => panic!("expected When effect"),
}
}
#[test]
fn test_infer_requirements_conditional_effects() {
let action = Action {
name: "push".into(),
parameters: vec![],
precondition: None,
effect: Some(Effect::When(
GoalDesc::Atom("fragile".into(), vec![]),
Box::new(Effect::Add("broken".into(), vec![])),
)),
};
let reqs = infer_requirements(&[action], &[], &[]);
assert!(reqs.contains(&Requirement::Strips));
assert!(reqs.contains(&Requirement::ConditionalEffects));
}
#[test]
fn test_roundtrip_conditional_effect() {
use crate::pddl::to_ir;
let pddl_action = Action {
name: "push".into(),
parameters: vec![TypedList {
items: vec!["x".into()],
type_name: None,
}],
precondition: Some(GoalDesc::Atom(
"pushable".into(),
vec![Term::Variable("x".into())],
)),
effect: Some(Effect::And(vec![
Effect::Add(
"moved".into(),
vec![Term::Variable("x".into())],
),
Effect::When(
GoalDesc::Atom(
"fragile".into(),
vec![Term::Variable("x".into())],
),
Box::new(Effect::Add(
"broken".into(),
vec![Term::Variable("x".into())],
)),
),
])),
};
let ir_program = to_ir::domain_to_ir(&Domain {
name: "test".into(),
actions: vec![pddl_action],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_program.actions;
let domain = ir_to_domain(&program, "test").unwrap();
let rt = &domain.actions[0];
assert_eq!(rt.name, "push");
assert!(domain.requirements.contains(&Requirement::ConditionalEffects));
match rt.effect.as_ref().unwrap() {
Effect::And(effs) => {
assert_eq!(effs.len(), 2);
match &effs[1] {
Effect::When(cond, inner) => {
assert_eq!(
*cond,
GoalDesc::Atom(
"fragile".into(),
vec![Term::Variable("x".into())]
)
);
assert_eq!(
**inner,
Effect::Add(
"broken".into(),
vec![Term::Variable("x".into())]
)
);
}
_ => panic!("expected When effect"),
}
}
_ => panic!("expected And effect"),
}
}
#[test]
fn test_or_to_goal_desc() {
let term = structure(
"or",
vec![
structure("at", vec![atom("a"), atom("b")]),
structure("at", vec![atom("a"), atom("c")]),
],
);
let gd = ir_term_to_goal_desc(&term).unwrap();
match gd {
GoalDesc::Or(gds) => {
assert_eq!(gds.len(), 2);
assert_eq!(
gds[0],
GoalDesc::Atom(
"at".into(),
vec![Term::Name("a".into()), Term::Name("b".into())]
)
);
}
_ => panic!("expected Or"),
}
}
#[test]
fn test_infer_requirements_disjunctive_preconditions() {
let action = Action {
name: "go".into(),
parameters: vec![],
precondition: Some(GoalDesc::Or(vec![
GoalDesc::Atom("path-a".into(), vec![]),
GoalDesc::Atom("path-b".into(), vec![]),
])),
effect: None,
};
let reqs = infer_requirements(&[action], &[], &[]);
assert!(reqs.contains(&Requirement::Strips));
assert!(reqs.contains(&Requirement::DisjunctivePreconditions));
}
#[test]
fn test_roundtrip_or_precondition() {
use crate::pddl::to_ir;
let pddl_action = Action {
name: "go".into(),
parameters: vec![TypedList {
items: vec!["from".into(), "to".into()],
type_name: Some(Type::Simple("location".into())),
}],
precondition: Some(GoalDesc::And(vec![
GoalDesc::Atom("at".into(), vec![Term::Variable("from".into())]),
GoalDesc::Or(vec![
GoalDesc::Atom(
"road".into(),
vec![Term::Variable("from".into()), Term::Variable("to".into())],
),
GoalDesc::Atom(
"bridge".into(),
vec![Term::Variable("from".into()), Term::Variable("to".into())],
),
]),
])),
effect: Some(Effect::And(vec![
Effect::Add("at".into(), vec![Term::Variable("to".into())]),
Effect::Delete("at".into(), vec![Term::Variable("from".into())]),
])),
};
let ir_program = to_ir::domain_to_ir(&Domain {
name: "test".into(),
actions: vec![pddl_action],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_program.actions;
let domain = ir_to_domain(&program, "test").unwrap();
let rt = &domain.actions[0];
assert_eq!(rt.name, "go");
assert!(domain.requirements.contains(&Requirement::DisjunctivePreconditions));
match rt.precondition.as_ref().unwrap() {
GoalDesc::And(gds) => {
assert_eq!(gds.len(), 2);
match &gds[1] {
GoalDesc::Or(disj) => {
assert_eq!(disj.len(), 2);
assert_eq!(
disj[0],
GoalDesc::Atom(
"road".into(),
vec![
Term::Variable("from".into()),
Term::Variable("to".into()),
]
)
);
}
_ => panic!("expected Or"),
}
}
_ => panic!("expected And"),
}
}
#[test]
fn test_roundtrip_imply_becomes_or_not() {
use crate::pddl::to_ir;
let pddl_action = Action {
name: "check".into(),
parameters: vec![],
precondition: Some(GoalDesc::Imply(
Box::new(GoalDesc::Atom("armed".into(), vec![])),
Box::new(GoalDesc::Atom("dangerous".into(), vec![])),
)),
effect: Some(Effect::Add("checked".into(), vec![])),
};
let ir_program = to_ir::domain_to_ir(&Domain {
name: "test".into(),
actions: vec![pddl_action],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_program.actions;
let domain = ir_to_domain(&program, "test").unwrap();
let rt = &domain.actions[0];
match rt.precondition.as_ref().unwrap() {
GoalDesc::Or(disj) => {
assert_eq!(disj.len(), 2);
match &disj[0] {
GoalDesc::Not(inner) => {
assert_eq!(
**inner,
GoalDesc::Atom("armed".into(), vec![])
);
}
_ => panic!("expected Not"),
}
assert_eq!(disj[1], GoalDesc::Atom("dangerous".into(), vec![]));
}
_ => panic!("expected Or from imply normalization"),
}
}
#[test]
fn test_fcomp_to_goal_desc() {
let term = structure(
"fcomp",
vec![
atom(">="),
structure("fuel", vec![var("R")]),
IrTerm::Float(10.0),
],
);
let gd = ir_term_to_goal_desc(&term).unwrap();
match gd {
GoalDesc::FComp(comp, lhs, rhs) => {
assert_eq!(comp, Comparator::GtEq);
match lhs {
FExp::FHead(fh) => {
assert_eq!(fh.name, "fuel");
assert_eq!(fh.args, vec![Term::Variable("r".into())]);
}
_ => panic!("expected FHead"),
}
match rhs {
FExp::Number(n) => assert!((n - 10.0).abs() < f64::EPSILON),
_ => panic!("expected Number"),
}
}
_ => panic!("expected FComp"),
}
}
#[test]
fn test_numeric_effect_to_pddl() {
let term = structure(
"decrease",
vec![
structure("fuel", vec![atom("rover1")]),
IrTerm::Float(5.0),
],
);
let eff = ir_term_to_effect(&term).unwrap();
match eff {
Effect::Decrease(fh, fe) => {
assert_eq!(fh.name, "fuel");
assert_eq!(fh.args, vec![Term::Name("rover1".into())]);
match fe {
FExp::Number(n) => assert!((n - 5.0).abs() < f64::EPSILON),
_ => panic!("expected Number"),
}
}
_ => panic!("expected Decrease"),
}
}
#[test]
fn test_assign_effect_to_pddl() {
let term = structure(
"assign",
vec![
structure("cost", vec![]),
IrTerm::Float(0.0),
],
);
let eff = ir_term_to_effect(&term).unwrap();
match eff {
Effect::Assign(fh, _fe) => assert_eq!(fh.name, "cost"),
_ => panic!("expected Assign"),
}
}
#[test]
fn test_function_value_init_roundtrip() {
let state = IrState::with_facts(vec![
structure("at", vec![atom("rover1"), atom("base")]),
IrTerm::Structure {
name: "=".into(),
args: vec![
structure("fuel", vec![atom("rover1")]),
IrTerm::Float(100.0),
],
},
]);
let inits = convert_state_to_init(&state).unwrap();
assert_eq!(inits.len(), 2);
assert_eq!(
inits[0],
Init::Positive("at".into(), vec!["rover1".into(), "base".into()])
);
assert_eq!(
inits[1],
Init::FunctionValue("fuel".into(), vec!["rover1".into()], 100.0)
);
}
#[test]
fn test_infer_requirements_numeric_fluents() {
let action = Action {
name: "drive".into(),
parameters: vec![],
precondition: Some(GoalDesc::FComp(
Comparator::GtEq,
FExp::FHead(FHead { name: "fuel".into(), args: vec![] }),
FExp::Number(10.0),
)),
effect: Some(Effect::Decrease(
FHead { name: "fuel".into(), args: vec![] },
FExp::Number(10.0),
)),
};
let reqs = infer_requirements(&[action], &[], &[]);
assert!(reqs.contains(&Requirement::NumericFluents));
}
#[test]
fn test_infer_functions() {
let action = Action {
name: "drive".into(),
parameters: vec![],
precondition: Some(GoalDesc::FComp(
Comparator::GtEq,
FExp::FHead(FHead {
name: "fuel".into(),
args: vec![Term::Variable("r".into())],
}),
FExp::Number(10.0),
)),
effect: Some(Effect::Decrease(
FHead {
name: "fuel".into(),
args: vec![Term::Variable("r".into())],
},
FExp::Number(10.0),
)),
};
let fns = infer_functions(&[action]);
assert_eq!(fns.len(), 1);
assert_eq!(fns[0].functions.len(), 1);
assert_eq!(fns[0].functions[0].name, "fuel");
}
#[test]
fn test_is_durative_detection() {
let durative = IrAction {
name: "move".into(),
parameters: vec!["V".into()],
preconditions: vec![
structure("duration", vec![atom("="), IrTerm::Float(5.0)]),
structure("at-start", vec![structure("at", vec![var("V")])]),
],
effects: vec![
structure("at-end", vec![structure("arrived", vec![var("V")])]),
],
metadata: None,
};
assert!(is_durative_ir_action(&durative));
let non_durative = IrAction {
name: "pick".into(),
parameters: vec![],
preconditions: vec![structure("clear", vec![atom("a")])],
effects: vec![structure("holding", vec![atom("a")])],
metadata: None,
};
assert!(!is_durative_ir_action(&non_durative));
}
#[test]
fn test_durative_action_reconstruction() {
let ir = IrAction {
name: "fly".into(),
parameters: vec!["A".into()],
preconditions: vec![
structure("duration", vec![atom("="), IrTerm::Float(10.0)]),
structure("at-start", vec![structure("at-airport", vec![var("A")])]),
structure("over-all", vec![structure("fuel-ok", vec![var("A")])]),
],
effects: vec![
structure("at-start", vec![structure("not", vec![structure("at-airport", vec![var("A")])])]),
structure("at-end", vec![structure("arrived", vec![var("A")])]),
],
metadata: Some(IrMetadata {
types: Some(vec!["aircraft".into()]),
..IrMetadata::default()
}),
};
let da = convert_ir_durative_action(&ir).unwrap();
assert_eq!(da.name, "fly");
assert_eq!(da.parameters.len(), 1);
assert_eq!(da.parameters[0].items, vec!["a"]);
match &da.duration {
DurationConstraint::Cmp(comp, fe) => {
assert_eq!(*comp, Comparator::Eq);
match fe {
FExp::Number(n) => assert!((n - 10.0).abs() < f64::EPSILON),
_ => panic!("expected Number"),
}
}
_ => panic!("expected Cmp"),
}
assert!(da.condition.is_some());
assert!(da.effect.is_some());
}
#[test]
fn test_ir_to_domain_separates_durative() {
let mut program = IrProgram::new();
program.actions.push(IrAction {
name: "pick".into(),
parameters: vec![],
preconditions: vec![structure("clear", vec![atom("a")])],
effects: vec![structure("holding", vec![atom("a")])],
metadata: None,
});
program.actions.push(IrAction {
name: "fly".into(),
parameters: vec![],
preconditions: vec![
structure("duration", vec![atom("="), IrTerm::Float(5.0)]),
structure("at-start", vec![structure("ready", vec![])]),
],
effects: vec![
structure("at-end", vec![structure("arrived", vec![])]),
],
metadata: None,
});
let domain = ir_to_domain(&program, "test").unwrap();
assert_eq!(domain.actions.len(), 1);
assert_eq!(domain.actions[0].name, "pick");
assert_eq!(domain.durative_actions.len(), 1);
assert_eq!(domain.durative_actions[0].name, "fly");
assert!(domain.requirements.contains(&Requirement::DurativeActions));
}
#[test]
fn test_roundtrip_durative_action() {
use crate::pddl::to_ir;
let pddl_da = DurativeAction {
name: "fly".into(),
parameters: vec![TypedList {
items: vec!["a".into()],
type_name: Some(Type::Simple("aircraft".into())),
}],
duration: DurationConstraint::Cmp(Comparator::Eq, FExp::Number(10.0)),
condition: Some(DaGd::And(vec![
DaGd::AtStart(GoalDesc::Atom(
"at-airport".into(),
vec![Term::Variable("a".into())],
)),
DaGd::OverAll(GoalDesc::Atom(
"has-fuel".into(),
vec![Term::Variable("a".into())],
)),
])),
effect: Some(DaEffect::And(vec![
DaEffect::AtStart(Effect::Delete(
"at-airport".into(),
vec![Term::Variable("a".into())],
)),
DaEffect::AtEnd(Effect::Add(
"arrived".into(),
vec![Term::Variable("a".into())],
)),
])),
};
let ir_program = to_ir::domain_to_ir(&Domain {
name: "air".into(),
durative_actions: vec![pddl_da],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_program.actions;
let domain = ir_to_domain(&program, "air").unwrap();
assert!(domain.actions.is_empty());
assert_eq!(domain.durative_actions.len(), 1);
assert!(domain.requirements.contains(&Requirement::DurativeActions));
let rt = &domain.durative_actions[0];
assert_eq!(rt.name, "fly");
assert!(rt.condition.is_some());
assert!(rt.effect.is_some());
}
#[test]
fn test_roundtrip_numeric_action() {
use crate::pddl::to_ir;
let pddl_action = Action {
name: "drive".into(),
parameters: vec![TypedList {
items: vec!["r".into()],
type_name: Some(Type::Simple("rover".into())),
}],
precondition: Some(GoalDesc::FComp(
Comparator::GtEq,
FExp::FHead(FHead {
name: "fuel".into(),
args: vec![Term::Variable("r".into())],
}),
FExp::Number(10.0),
)),
effect: Some(Effect::Decrease(
FHead {
name: "fuel".into(),
args: vec![Term::Variable("r".into())],
},
FExp::Number(10.0),
)),
};
let ir_program = to_ir::domain_to_ir(&Domain {
name: "rover".into(),
actions: vec![pddl_action],
..Domain::default()
})
.unwrap();
let mut program = IrProgram::new();
program.actions = ir_program.actions;
let domain = ir_to_domain(&program, "rover").unwrap();
assert!(domain.requirements.contains(&Requirement::NumericFluents));
assert!(!domain.functions.is_empty());
let rt = &domain.actions[0];
assert_eq!(rt.name, "drive");
match rt.precondition.as_ref().unwrap() {
GoalDesc::FComp(comp, lhs, _rhs) => {
assert_eq!(*comp, Comparator::GtEq);
match lhs {
FExp::FHead(fh) => assert_eq!(fh.name, "fuel"),
_ => panic!("expected FHead"),
}
}
_ => panic!("expected FComp"),
}
match rt.effect.as_ref().unwrap() {
Effect::Decrease(fh, _) => assert_eq!(fh.name, "fuel"),
_ => panic!("expected Decrease"),
}
}
#[test]
fn test_preference_goal_desc_roundtrip() {
let term = structure(
"preference",
vec![atom("p1"), structure("tidy", vec![])],
);
let gd = ir_term_to_goal_desc(&term).unwrap();
match gd {
GoalDesc::Preference(name, inner) => {
assert_eq!(name, Some("p1".into()));
assert_eq!(*inner, GoalDesc::Atom("tidy".into(), vec![]));
}
_ => panic!("expected Preference"),
}
}
#[test]
fn test_preference_anonymous_roundtrip() {
let term = structure(
"preference",
vec![atom("_anon"), structure("clean", vec![])],
);
let gd = ir_term_to_goal_desc(&term).unwrap();
match gd {
GoalDesc::Preference(name, _) => assert!(name.is_none()),
_ => panic!("expected Preference"),
}
}
#[test]
fn test_con_gd_always_roundtrip() {
let term = structure("always", vec![structure("safe", vec![])]);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::Always(gd) => {
assert_eq!(gd, GoalDesc::Atom("safe".into(), vec![]));
}
_ => panic!("expected Always"),
}
}
#[test]
fn test_con_gd_sometime_roundtrip() {
let term = structure("sometime", vec![structure("visited", vec![])]);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::Sometime(_) => {}
_ => panic!("expected Sometime"),
}
}
#[test]
fn test_con_gd_sometime_after_roundtrip() {
let term = structure(
"sometime-after",
vec![structure("alarm", vec![]), structure("evacuated", vec![])],
);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::SometimeAfter(a, b) => {
assert_eq!(a, GoalDesc::Atom("alarm".into(), vec![]));
assert_eq!(b, GoalDesc::Atom("evacuated".into(), vec![]));
}
_ => panic!("expected SometimeAfter"),
}
}
#[test]
fn test_con_gd_within_roundtrip() {
let term = structure(
"within",
vec![IrTerm::Float(5.0), structure("goal", vec![])],
);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::Within(t, _) => assert!((t - 5.0).abs() < f64::EPSILON),
_ => panic!("expected Within"),
}
}
#[test]
fn test_con_gd_at_most_once_roundtrip() {
let term = structure("at-most-once", vec![structure("toggle", vec![])]);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::AtMostOnce(_) => {}
_ => panic!("expected AtMostOnce"),
}
}
#[test]
fn test_con_gd_hold_during_roundtrip() {
let term = structure(
"hold-during",
vec![
IrTerm::Float(10.0),
IrTerm::Float(20.0),
structure("stable", vec![]),
],
);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::HoldDuring(t1, t2, _) => {
assert!((t1 - 10.0).abs() < f64::EPSILON);
assert!((t2 - 20.0).abs() < f64::EPSILON);
}
_ => panic!("expected HoldDuring"),
}
}
#[test]
fn test_con_gd_hold_after_roundtrip() {
let term = structure(
"hold-after",
vec![IrTerm::Float(15.0), structure("done", vec![])],
);
let con = ir_term_to_con_gd(&term).unwrap();
match con {
ConGd::HoldAfter(t, _) => assert!((t - 15.0).abs() < f64::EPSILON),
_ => panic!("expected HoldAfter"),
}
}
#[test]
fn test_pref_con_gd_roundtrip() {
let term = structure(
"preference",
vec![
atom("soft1"),
structure("sometime", vec![structure("visited", vec![])]),
],
);
let pcg = ir_term_to_pref_con_gd_item(&term).unwrap();
match pcg {
PrefConGd::Preference(name, con) => {
assert_eq!(name, Some("soft1".into()));
match con {
ConGd::Sometime(_) => {}
_ => panic!("expected Sometime"),
}
}
_ => panic!("expected Preference"),
}
}
#[test]
fn test_metric_roundtrip() {
let term = structure(
"metric",
vec![
atom("minimize"),
structure(
"+",
vec![
structure("total-cost", vec![]),
structure(
"*",
vec![
IrTerm::Float(10.0),
structure("is-violated", vec![atom("p1")]),
],
),
],
),
],
);
let metric = ir_term_to_metric(&term).unwrap();
match metric.optimization {
Optimization::Minimize => {}
_ => panic!("expected Minimize"),
}
match metric.expression {
FExp::BinaryOp(BinaryOp::Add, _, _) => {}
_ => panic!("expected Add"),
}
}
#[test]
fn test_ir_to_pddl_full_roundtrip() {
use crate::pddl::to_ir;
let domain = Domain {
name: "test".into(),
actions: vec![Action {
name: "go".into(),
parameters: vec![],
precondition: Some(GoalDesc::Atom("ready".into(), vec![])),
effect: Some(Effect::Add("done".into(), vec![])),
}],
constraints: Some(ConGd::Always(GoalDesc::Atom("safe".into(), vec![]))),
..Domain::default()
};
let problem = Problem {
name: "p1".into(),
domain_name: "test".into(),
goal: GoalDesc::Atom("done".into(), vec![]),
constraints: Some(PrefConGd::Preference(
Some("soft1".into()),
ConGd::Sometime(GoalDesc::Atom("visited".into(), vec![])),
)),
metric: Some(MetricSpec {
optimization: Optimization::Minimize,
expression: FExp::IsViolated("soft1".into()),
}),
..Problem::default()
};
let ir = to_ir::pddl_to_ir_full(&domain, &problem).unwrap();
let (rt_domain, rt_problem) = ir_to_pddl_full(
&ir.program,
&ir.initial_state,
&ir.goals,
"test",
"p1",
ir.domain_constraints.as_ref(),
&ir.problem_constraints,
ir.metric.as_ref(),
)
.unwrap();
assert!(rt_domain.constraints.is_some());
match rt_domain.constraints.unwrap() {
ConGd::Always(gd) => {
assert_eq!(gd, GoalDesc::Atom("safe".into(), vec![]));
}
_ => panic!("expected Always"),
}
assert!(rt_domain.requirements.contains(&Requirement::Constraints));
assert!(rt_problem.constraints.is_some());
match rt_problem.constraints.unwrap() {
PrefConGd::Preference(name, con) => {
assert_eq!(name, Some("soft1".into()));
match con {
ConGd::Sometime(_) => {}
_ => panic!("expected Sometime"),
}
}
_ => panic!("expected Preference"),
}
assert!(rt_problem.metric.is_some());
let m = rt_problem.metric.unwrap();
match m.optimization {
Optimization::Minimize => {}
_ => panic!("expected Minimize"),
}
match m.expression {
FExp::IsViolated(name) => assert_eq!(name, "soft1"),
_ => panic!("expected IsViolated"),
}
}
#[test]
fn test_process_ir_roundtrip() {
let proc_ir = IrAction {
name: "gravity".into(),
parameters: vec!["Ball".into()],
preconditions: vec![
IrTerm::Atom("__process".into()),
IrTerm::Structure {
name: "airborne".into(),
args: vec![IrTerm::Var("Ball".into())],
},
],
effects: vec![IrTerm::Structure {
name: "continuous-effect".into(),
args: vec![IrTerm::Structure {
name: "increase".into(),
args: vec![
IrTerm::Structure { name: "velocity".into(), args: vec![IrTerm::Var("Ball".into())] },
IrTerm::Float(9.81),
],
}],
}],
metadata: Some(IrMetadata {
types: Some(vec!["object".into()]),
..IrMetadata::default()
}),
};
assert!(is_process_ir_action(&proc_ir));
assert!(!is_event_ir_action(&proc_ir));
let process = convert_ir_process(&proc_ir).unwrap();
assert_eq!(process.name, "gravity");
assert_eq!(process.parameters.len(), 1);
assert_eq!(process.parameters[0].items, vec!["ball".to_string()]);
match &process.precondition {
GoalDesc::Atom(name, args) => {
assert_eq!(name, "airborne");
assert_eq!(args.len(), 1);
}
_ => panic!("expected Atom precondition, got {:?}", process.precondition),
}
assert_eq!(process.effect.len(), 1);
match &process.effect[0] {
Effect::Increase(head, _) => {
assert_eq!(head.name, "velocity");
}
other => panic!("expected Increase effect, got {:?}", other),
}
}
#[test]
fn test_event_ir_roundtrip() {
let evt_ir = IrAction {
name: "bounce".into(),
parameters: vec![],
preconditions: vec![
IrTerm::Atom("__event".into()),
IrTerm::Structure {
name: "falling".into(),
args: vec![],
},
],
effects: vec![
IrTerm::Structure {
name: "not".into(),
args: vec![IrTerm::Structure { name: "falling".into(), args: vec![] }],
},
IrTerm::Structure { name: "rising".into(), args: vec![] },
],
metadata: None,
};
assert!(!is_process_ir_action(&evt_ir));
assert!(is_event_ir_action(&evt_ir));
let event = convert_ir_event(&evt_ir).unwrap();
assert_eq!(event.name, "bounce");
assert!(event.parameters.is_empty());
match &event.precondition {
GoalDesc::Atom(name, args) => {
assert_eq!(name, "falling");
assert!(args.is_empty());
}
_ => panic!("expected Atom precondition"),
}
assert_eq!(event.effect.len(), 2);
}
#[test]
fn test_domain_with_process_and_event_roundtrip() {
let domain = Domain {
name: "physics".into(),
processes: vec![Process {
name: "fall".into(),
parameters: vec![],
precondition: GoalDesc::Atom("airborne".into(), vec![]),
effect: vec![Effect::Increase(
FHead { name: "velocity".into(), args: vec![] },
FExp::Number(9.81),
)],
}],
events: vec![Event {
name: "land".into(),
parameters: vec![],
precondition: GoalDesc::Atom("at-ground".into(), vec![]),
effect: vec![
Effect::Delete("airborne".into(), vec![]),
Effect::Add("grounded".into(), vec![]),
],
}],
..Domain::default()
};
let program = crate::pddl::to_ir::domain_to_ir(&domain).unwrap();
assert_eq!(program.actions.len(), 2);
let rt_domain = ir_to_domain(&program, "physics").unwrap();
assert_eq!(rt_domain.name, "physics");
assert_eq!(rt_domain.processes.len(), 1);
assert_eq!(rt_domain.events.len(), 1);
assert_eq!(rt_domain.processes[0].name, "fall");
assert_eq!(rt_domain.events[0].name, "land");
}
#[test]
fn test_type_hierarchy_roundtrip_multi_level() {
let domain = Domain {
name: "logistics".into(),
types: vec![
TypeDecl {
names: vec!["city".into(), "location".into()],
parent: Some("object".into()),
},
TypeDecl {
names: vec!["vehicle".into(), "package".into()],
parent: Some("object".into()),
},
TypeDecl {
names: vec!["truck".into(), "airplane".into()],
parent: Some("vehicle".into()),
},
],
actions: vec![Action {
name: "drive".into(),
parameters: vec![
TypedList {
items: vec!["v".into()],
type_name: Some(Type::Simple("truck".into())),
},
TypedList {
items: vec!["from".into(), "to".into()],
type_name: Some(Type::Simple("location".into())),
},
TypedList {
items: vec!["c".into()],
type_name: Some(Type::Simple("city".into())),
},
],
precondition: Some(GoalDesc::And(vec![
GoalDesc::Atom("at".into(), vec![
Term::Variable("v".into()),
Term::Variable("from".into()),
]),
GoalDesc::Atom("in-city".into(), vec![
Term::Variable("from".into()),
Term::Variable("c".into()),
]),
GoalDesc::Atom("in-city".into(), vec![
Term::Variable("to".into()),
Term::Variable("c".into()),
]),
])),
effect: Some(Effect::And(vec![
Effect::Add("at".into(), vec![
Term::Variable("v".into()),
Term::Variable("to".into()),
]),
Effect::Delete("at".into(), vec![
Term::Variable("v".into()),
Term::Variable("from".into()),
]),
])),
}],
..Domain::default()
};
let program = crate::pddl::to_ir::domain_to_ir(&domain).unwrap();
assert_eq!(program.type_hierarchy.len(), 3);
assert_eq!(
program.type_hierarchy[0],
(vec!["city".into(), "location".into()], Some("object".into()))
);
assert_eq!(
program.type_hierarchy[2],
(vec!["truck".into(), "airplane".into()], Some("vehicle".into()))
);
let rt_domain = ir_to_domain(&program, "logistics").unwrap();
assert_eq!(rt_domain.types.len(), 3);
assert_eq!(rt_domain.types[0].names, vec!["city", "location"]);
assert_eq!(rt_domain.types[0].parent, Some("object".into()));
assert_eq!(rt_domain.types[1].names, vec!["vehicle", "package"]);
assert_eq!(rt_domain.types[1].parent, Some("object".into()));
assert_eq!(rt_domain.types[2].names, vec!["truck", "airplane"]);
assert_eq!(rt_domain.types[2].parent, Some("vehicle".into()));
assert_eq!(rt_domain.actions.len(), 1);
assert_eq!(rt_domain.actions[0].name, "drive");
}
#[test]
fn test_type_hierarchy_roundtrip_via_writer() {
let domain = Domain {
name: "typed".into(),
requirements: vec![Requirement::Strips, Requirement::Typing],
types: vec![
TypeDecl {
names: vec!["room".into(), "hall".into()],
parent: Some("location".into()),
},
TypeDecl {
names: vec!["location".into()],
parent: Some("object".into()),
},
],
actions: vec![Action {
name: "go".into(),
parameters: vec![TypedList {
items: vec!["from".into(), "to".into()],
type_name: Some(Type::Simple("location".into())),
}],
precondition: Some(GoalDesc::Atom("at".into(), vec![Term::Variable("from".into())])),
effect: Some(Effect::And(vec![
Effect::Add("at".into(), vec![Term::Variable("to".into())]),
Effect::Delete("at".into(), vec![Term::Variable("from".into())]),
])),
}],
predicates: vec![AtomicFormulaSkeleton {
name: "at".into(),
parameters: vec![TypedList {
items: vec!["l".into()],
type_name: Some(Type::Simple("location".into())),
}],
}],
..Domain::default()
};
let text = gollum_pddl::writer::domain_to_string(&domain);
let parsed = gollum_pddl::parse(&text).unwrap();
let parsed_domain = match parsed {
PddlFile::Domain(d) => d,
_ => panic!("expected domain"),
};
let program = crate::pddl::to_ir::domain_to_ir(&parsed_domain).unwrap();
let rt_domain = ir_to_domain(&program, "typed").unwrap();
assert_eq!(rt_domain.types.len(), 2);
assert_eq!(rt_domain.types[0].names, vec!["room", "hall"]);
assert_eq!(rt_domain.types[0].parent, Some("location".into()));
assert_eq!(rt_domain.types[1].names, vec!["location"]);
assert_eq!(rt_domain.types[1].parent, Some("object".into()));
}
#[test]
fn test_type_hierarchy_fallback_when_empty() {
let mut program = IrProgram::new();
program.actions.push(IrAction {
name: "move".into(),
parameters: vec!["X".into(), "Y".into()],
preconditions: vec![IrTerm::Structure {
name: "at".into(),
args: vec![IrTerm::Var("X".into())],
}],
effects: vec![IrTerm::Structure {
name: "at".into(),
args: vec![IrTerm::Var("Y".into())],
}],
metadata: Some(IrMetadata {
types: Some(vec!["location".into(), "location".into()]),
..IrMetadata::default()
}),
});
let domain = ir_to_domain(&program, "test").unwrap();
assert_eq!(domain.types.len(), 1);
assert_eq!(domain.types[0].names, vec!["location"]);
assert_eq!(domain.types[0].parent, Some("object".into()));
}
#[test]
fn test_type_hierarchy_with_either_type() {
let domain = Domain {
name: "either-test".into(),
types: vec![
TypeDecl {
names: vec!["bricks".into(), "windows".into()],
parent: Some("object".into()),
},
],
actions: vec![Action {
name: "clean".into(),
parameters: vec![TypedList {
items: vec!["x".into()],
type_name: Some(Type::Either(vec!["bricks".into(), "windows".into()])),
}],
precondition: Some(GoalDesc::Atom("dirty".into(), vec![Term::Variable("x".into())])),
effect: Some(Effect::And(vec![
Effect::Delete("dirty".into(), vec![Term::Variable("x".into())]),
Effect::Add("clean".into(), vec![Term::Variable("x".into())]),
])),
}],
..Domain::default()
};
let program = crate::pddl::to_ir::domain_to_ir(&domain).unwrap();
let rt_domain = ir_to_domain(&program, "either-test").unwrap();
assert_eq!(rt_domain.types.len(), 1);
assert_eq!(rt_domain.types[0].names, vec!["bricks", "windows"]);
assert_eq!(rt_domain.types[0].parent, Some("object".into()));
assert_eq!(rt_domain.actions[0].parameters.len(), 1);
let param_type = &rt_domain.actions[0].parameters[0].type_name;
match param_type {
Some(Type::Either(names)) => {
assert_eq!(names, &vec!["bricks".to_string(), "windows".to_string()]);
}
Some(Type::Simple(s)) if s == "bricks|windows" => {
}
other => panic!("expected Either or pipe-separated type, got {:?}", other),
}
}
}