Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes and improvements in the parsing of typed lists #82

Merged
merged 51 commits into from
Jun 13, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
e3a249e
fix: remove constants.py module and use pddl/parser/symbols.py
marcofavorito Jun 6, 2023
658dc62
fix: add missing __invert__ method for True/FalseFormula classes
marcofavorito Jun 7, 2023
d7d0534
test: fix blocksworld_fond/p01.pddl goal
marcofavorito Jun 7, 2023
d5b509c
chore: add 'check' function to generalize assert_ for any exception type
marcofavorito Jun 7, 2023
3c53e3f
feat: add TypesIndex class
marcofavorito Jun 7, 2023
3cf8cb4
feat: changing domain parser behaviour on typed_list_name (backward c…
marcofavorito Jun 7, 2023
cf25df5
test: add problem parsing in test_formatter.py
marcofavorito Jun 7, 2023
ccb5097
test: fix expected error messages when duplicated names/types occur
marcofavorito Jun 7, 2023
7a7836c
fix: minor fix to object-supertypes-error
marcofavorito Jun 7, 2023
f1e3316
fix: use 'name' type to validate tokens of typed list
marcofavorito Jun 7, 2023
48ffa56
chore: sort Symbols in alphanumerical order
marcofavorito Jun 7, 2023
976f481
fix: add keyword validation for typed list names
marcofavorito Jun 7, 2023
889b3fc
fix: renaming variables to avoid shadowing from outer scope
marcofavorito Jun 7, 2023
73e4fac
feat: use TypesIndex to parse and validate typed_list_variables
marcofavorito Jun 7, 2023
d2dded9
test: split domain parser tests from problem parser tests
marcofavorito Jun 7, 2023
67d3b1d
docs: fix types argument passed to Domain in README
marcofavorito Jun 7, 2023
124a60a
fix: problem optionally accepts requirements
marcofavorito Jun 7, 2023
03abf7d
lint: fix vulture's whitelist
marcofavorito Jun 7, 2023
04fe2f8
feat: add internal class to manage and handle types
marcofavorito Jun 7, 2023
1a35939
refactor: more Requirements to its own module pddl.requirements
marcofavorito Jun 7, 2023
c3ab1aa
chore: move _Types in pddl._validation
marcofavorito Jun 7, 2023
35f333b
chore: return 'name' instead of str in domain/problem names property …
marcofavorito Jun 7, 2023
529d35d
chore: used 'validate' instead of 'assert_'
marcofavorito Jun 7, 2023
0d264a8
chore: add Problem.check method stub
marcofavorito Jun 7, 2023
03e632b
chore: change the way requirements are set in a Problem object
marcofavorito Jun 7, 2023
f232127
fix: update domain setter of Problem
marcofavorito Jun 7, 2023
1586db1
feat: add TypeChecker utility class and use it for checking (both dom…
marcofavorito Jun 7, 2023
e879efb
fix: improve Problem initialize regarding requirements and domain_nam…
marcofavorito Jun 7, 2023
771c6fc
test: refactor parametrized tests
marcofavorito Jun 7, 2023
2d35e8c
build: bump minimum Python interpreter supported to 3.8
marcofavorito Jun 7, 2023
190a7c5
fix: minor fixes to requirements handling in core module
marcofavorito Jun 7, 2023
4376e54
feat: add validation of domain/problem predicates
marcofavorito Jun 7, 2023
a511022
feat: add type validation of domain actions
marcofavorito Jun 7, 2023
3d02538
refactor: move Action class in its own module core.action
marcofavorito Jun 7, 2023
617cfd7
test: fix validation exception message
marcofavorito Jun 7, 2023
bdddfbf
feat: add keyword check
marcofavorito Jun 7, 2023
7b3e7ad
test: add tests on wrong variable typed lists
marcofavorito Jun 7, 2023
a12e34b
fix: move keyword validation inside class contructors
marcofavorito Jun 8, 2023
f0e79af
fix: make find_cycle to handle arbitrary graphs
marcofavorito Jun 9, 2023
8c41830
chore: rename TypesIndex to TypedListParser
marcofavorito Jun 9, 2023
eb2429d
feat: allow repetition in variable list
marcofavorito Jun 9, 2023
9a16858
test: improve 'test_variables_repetition_allowed_if_same_type'
marcofavorito Jun 9, 2023
a6367ba
Update .github/workflows/docs.yml
marcofavorito Jun 9, 2023
2c67fde
feat: add validation of no-duplicates in type-tags of a Term
marcofavorito Jun 9, 2023
b8e0a2b
feat: make Term non-instantiatable
marcofavorito Jun 9, 2023
3661f64
feat: check terms consistency wrt type tag
marcofavorito Jun 9, 2023
924c88c
feat: add term type checks in EqualTo class
marcofavorito Jun 9, 2023
f7996d1
test: add more tests for problem parsing
marcofavorito Jun 9, 2023
f5858b3
ci: change GH action version from master to main
marcofavorito Jun 9, 2023
edcc295
fix formatting of typed lists
marcofavorito Jun 13, 2023
97b379f
fix: update error message in case a name inherits from multiple types
marcofavorito Jun 13, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ a1 = Action(
requirements = [Requirements.STRIPS, Requirements.TYPING]
domain = Domain("my_domain",
requirements=requirements,
types={"type_1": []},
types={"type_1": None},
constants=[a, b, c],
predicates=[p1, p2],
actions=[a1])
Expand Down
16 changes: 11 additions & 5 deletions pddl/_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@

from typing import Collection, Dict, Optional, Set, Tuple

from pddl.constants import OBJECT
from pddl.custom_types import name, to_names # noqa: F401
from pddl.exceptions import PDDLValidationError
from pddl.helpers.base import find_cycle
from pddl.helpers.base import ensure_set, find_cycle
from pddl.logic import Constant, Predicate
from pddl.logic.terms import Term
from pddl.parser.symbols import ALL_SYMBOLS, Symbols


def _check_types_dictionary(type_dict: Dict[name, Optional[name]]) -> None:
Expand All @@ -34,7 +34,7 @@ def _check_types_dictionary(type_dict: Dict[name, Optional[name]]) -> None:
>>> _check_types_dictionary({name("object"): a})
Traceback (most recent call last):
...
pddl.exceptions.PDDLValidationError: object must not have supertypes, but got object is a subtype of a
pddl.exceptions.PDDLValidationError: object must not have supertypes, but got 'object' is a subtype of 'a'

3) If cycles in the type hierarchy graph are present, an error is raised:
>>> a, b, c = to_names(["a", "b", "c"])
Expand All @@ -49,11 +49,11 @@ def _check_types_dictionary(type_dict: Dict[name, Optional[name]]) -> None:
return

# check `object` type
object_name = name(OBJECT)
object_name = name(Symbols.OBJECT.value)
if object_name in type_dict and type_dict[object_name] is not None:
object_supertype = type_dict[object_name]
raise PDDLValidationError(
f"object must not have supertypes, but got object is a subtype of {object_supertype}"
f"object must not have supertypes, but got 'object' is a subtype of '{object_supertype}'"
)

# check cycles
Expand Down Expand Up @@ -110,3 +110,9 @@ def _check_types_in_has_terms_objects(
f"type {repr(type_tag)} of term {repr(term)} in atomic expression "
f"{repr(has_terms)} is not in available types {all_types}"
)


def _is_a_keyword(word: str, ignore: Optional[Set[str]] = None) -> bool:
"""Check that the word is not a keyword."""
ignore_set = ensure_set(ignore)
return word not in ignore_set and word in ALL_SYMBOLS
9 changes: 8 additions & 1 deletion pddl/helpers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ def assert_(condition: bool, message: str = "") -> None:
when the code is compiled in optimized mode. For more information, see
https://bandit.readthedocs.io/en/1.7.5/plugins/b101_assert_used.html
"""
check(condition, message=message, exception_cls=AssertionError)


def check(
condition: bool, message: str = "", exception_cls: Type[Exception] = AssertionError
) -> None:
"""Check a condition, and if false, raise exception."""
if not condition:
raise AssertionError(message)
raise exception_cls(message)


def ensure(arg: Optional[Any], default: Any):
Expand Down
8 changes: 8 additions & 0 deletions pddl/logic/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,10 @@ def __hash__(self):
"""Hash the object."""
return hash(TrueFormula)

def __invert__(self) -> Formula:
"""Negate the formula."""
return FALSE

def __neg__(self) -> Formula:
"""Negate."""
return FALSE
Expand All @@ -158,6 +162,10 @@ def __hash__(self):
"""Hash the object."""
return hash(FalseFormula)

def __invert__(self) -> Formula:
"""Negate the formula."""
return TRUE

def __neg__(self) -> Formula:
"""Negate."""
return TRUE
Expand Down
4 changes: 2 additions & 2 deletions pddl/parser/domain.lark
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@ atomic_formula_term: LPAR predicate term* RPAR
constant: NAME

typed_list_variable: variable*
| variable+ TYPE_SEP type_def (typed_list_variable)
| (variable+ TYPE_SEP type_def)+ variable*
?variable: "?" NAME

typed_list_name: NAME*
| NAME+ TYPE_SEP primitive_type (typed_list_name)
| (NAME+ TYPE_SEP primitive_type)+ NAME*
type_def: LPAR EITHER primitive_type+ RPAR
| primitive_type
?primitive_type: NAME
Expand Down
168 changes: 50 additions & 118 deletions pddl/parser/domain.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@

"""Implementation of the PDDL domain parser."""
import sys
from typing import AbstractSet, Dict, List, Mapping, Optional, Sequence, Set, Tuple
from typing import Dict, Optional
from typing import OrderedDict as OrderedDictType
from typing import Set

from lark import Lark, ParseError, Transformer

from pddl.constants import EITHER
from pddl.core import Action, Domain, Requirements
from pddl.custom_types import name
from pddl.exceptions import PDDLMissingRequirementError, PDDLParsingError
from pddl.helpers.base import assert_, safe_index
from pddl.helpers.base import assert_
from pddl.logic.base import (
And,
ExistsCondition,
Expand All @@ -35,6 +37,7 @@
from pddl.logic.terms import Constant, Variable
from pddl.parser import DOMAIN_GRAMMAR_FILE, PARSERS_DIRECTORY
from pddl.parser.symbols import Symbols
from pddl.parser.types_index import TypesIndex


class DomainTransformer(Transformer):
Expand Down Expand Up @@ -88,11 +91,11 @@ def requirements(self, args):
def types(self, args):
"""Parse the 'types' rule."""
has_typing_requirement = self._has_requirement(Requirements.TYPING)
type_definition = args[2]
have_type_hierarchy = any(type_definition.values())
types_definition = args[2]
have_type_hierarchy = any(types_definition.values())
if have_type_hierarchy and not has_typing_requirement:
raise PDDLMissingRequirementError(Requirements.TYPING)
return dict(types=args[2])
return dict(types=types_definition)

def constants(self, args):
"""Process the 'constant_def' rule."""
Expand All @@ -109,15 +112,15 @@ def predicates(self, args):

def action_def(self, args):
"""Process the 'action_def' rule."""
name = args[2]
action_name = args[2]
variables = args[4]

# process action body
_children = args[5].children
action_body = {
_children[i][1:]: _children[i + 1] for i in range(0, len(_children), 2)
}
return Action(name, variables, **action_body)
return Action(action_name, variables, **action_body)

def derived_predicates(self, args):
"""Process the 'derived_predicates' rule."""
Expand All @@ -128,7 +131,7 @@ def derived_predicates(self, args):
def action_parameters(self, args):
"""Process the 'action_parameters' rule."""
self._current_parameters_by_name = {
name: Variable(name, tags) for name, tags in args[1].items()
var_name: Variable(var_name, tags) for var_name, tags in args[1].items()
}
return list(self._current_parameters_by_name.values())

Expand Down Expand Up @@ -192,7 +195,7 @@ def gd_quantifiers(self, args):
& self._extended_requirements
):
raise PDDLMissingRequirementError(req)
variables = [Variable(name, tags) for name, tags in args[3].items()]
variables = [Variable(var_name, tags) for var_name, tags in args[3].items()]
condition = args[5]
return cond_class(cond=condition, variables=variables)

Expand Down Expand Up @@ -231,7 +234,7 @@ def c_effect(self, args):
if len(args) == 1:
return args[0]
if args[1] == Symbols.FORALL.value:
variables = [Variable(name, tags) for name, tags in args[3].items()]
variables = [Variable(var_name, tags) for var_name, tags in args[3].items()]
return Forall(effect=args[-2], variables=variables)
if args[1] == Symbols.WHEN.value:
return When(args[2], args[3])
Expand Down Expand Up @@ -275,9 +278,9 @@ def constant_or_variable(t):
right = constant_or_variable(args[3])
return EqualTo(left, right)
else:
name = args[1]
predicate_name = args[1]
terms = list(map(constant_or_variable, args[2:-1]))
return Predicate(name, *terms)
return Predicate(predicate_name, *terms)

def constant(self, args):
"""Process the 'constant' rule."""
Expand All @@ -289,47 +292,24 @@ def constant(self, args):

def atomic_formula_skeleton(self, args):
"""Process the 'atomic_formula_skeleton' rule."""
name = args[1]
predicate_name = args[1]
variable_data: Dict[str, Set[str]] = args[2]
variables = [Variable(name, tags) for name, tags in variable_data.items()]
return Predicate(name, *variables)

def typed_list_name(self, args) -> Dict[str, Optional[str]]:
"""
Process the 'typed_list_name' rule.

Return a dictionary with as keys the names and as value the type of the name.

Steps:
- if the '-' symbol is not present, then return the list of names
- if the '-' symbol is present, parse the names with their type tags

:param args: the argument of this grammar rule
:return: a typed list (name)
"""
type_sep_index = safe_index(args, Symbols.TYPE_SEP.value)

if type_sep_index is None:
# simple list of names
return self._parse_simple_typed_list(args, check_for_duplicates=True)

# if we are here, the matched pattern is: [name_1, ..., name_n], "-", parent_name, other_typed_list_dict
# make sure there are only two tokens after "-"
assert_(len(args[type_sep_index:]) == 3, "unexpected parser state")

names: Tuple[str, ...] = tuple(args[:type_sep_index])
parent_name: str = str(args[type_sep_index + 1])
other_typed_list_dict: Mapping[str, Optional[str]] = args[type_sep_index + 2]
new_typed_list_dict: Mapping[str, Optional[str]] = {
obj: parent_name for obj in names
}

# check type conflicts
self._check_duplicates(other_typed_list_dict.keys(), new_typed_list_dict.keys())

return {**new_typed_list_dict, **other_typed_list_dict}
variables = [
Variable(var_name, tags) for var_name, tags in variable_data.items()
]
return Predicate(predicate_name, *variables)

def typed_list_name(self, args) -> Dict[name, Optional[name]]:
"""Process the 'typed_list_name' rule."""
try:
types_index = TypesIndex.parse_typed_list(args)
return types_index.get_typed_list_of_names()
except ValueError as e:
raise PDDLParsingError(
f"error while parsing tokens {list(map(str, args))}: {str(e)}"
) from None

def typed_list_variable(self, args) -> Dict[str, Set[str]]:
def typed_list_variable(self, args) -> OrderedDictType[name, Set[name]]:
Copy link
Member Author

@marcofavorito marcofavorito Jun 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OrderedDict is essential, predicates variables and action parameters require an ordered (possibly typed) list of variables, and the ordering in the PDDL input file must be remembered.

The same cannot be said for forall/exists expressions, but having them ordered does not harm.

"""
Process the 'typed_list_variable' rule.

Expand All @@ -338,79 +318,31 @@ def typed_list_variable(self, args) -> Dict[str, Set[str]]:
:param args: the argument of this grammar rule
:return: a typed list (variable), i.e. a mapping from variables to the supported types
"""
type_sep_index = safe_index(args, Symbols.TYPE_SEP.value)
if type_sep_index is None:
result = self._parse_simple_typed_list(args, check_for_duplicates=False)
return {var: set() for var in result}

# if we are here, the matched pattern is: [name_1 ... name_n], "-", type_def, other_typed_list_dict # noqa
# make sure there are only two tokens after "-"
assert_(len(args[type_sep_index:]) == 3, "unexpected parser state")

variables: Tuple[str, ...] = tuple(args[:type_sep_index])
type_def: Set[str] = self._process_type_def(args[type_sep_index + 1])
other_typed_list_dict: Mapping[str, Set[str]] = args[type_sep_index + 2]
new_typed_list_dict: Mapping[str, Set[str]] = {v: type_def for v in variables}

# check type conflicts
self._check_duplicates(other_typed_list_dict.keys(), new_typed_list_dict.keys())

return {**new_typed_list_dict, **other_typed_list_dict}
try:
types_index = TypesIndex.parse_typed_list(args)
return types_index.get_typed_list_of_variables()
except ValueError as e:
raise PDDLParsingError(
f"error while parsing tokens {list(map(str, args))}: {str(e)}"
) from None

def type_def(self, args):
"""Parse the 'type_def' rule."""
return args if len(args) == 1 else args[1:-1]
assert_(len(args) != 0, "unexpected parser state: empty type_def")

def _has_requirement(self, requirement: Requirements) -> bool:
"""Check whether a requirement is satisfied by the current state of the domain parsing."""
return requirement in self._extended_requirements

def _check_duplicates(
self,
other_names: AbstractSet[str],
new_names: AbstractSet[str],
) -> None:
names_intersection = new_names & other_names
if len(names_intersection) != 0:
names_list_as_strings = map(repr, map(str, names_intersection))
names_list_str = ", ".join(sorted(names_list_as_strings))
raise PDDLParsingError(
f"detected conflicting items in a typed list: items occurred twice: [{names_list_str}]"
)

def _parse_simple_typed_list(
self, args: Sequence[str], check_for_duplicates: bool = True
) -> Dict[str, Optional[str]]:
"""
Parse a 'simple' typed list.

In this simple case, there are no type specifications, i.e. just a list of items.

If check_for_duplicates is True, a check for duplicates is performed.
"""
# check for duplicates
if check_for_duplicates and len(set(args)) != len(args):
# find duplicates
seen = set()
dupes = [str(x) for x in args if x in seen or seen.add(x)] # type: ignore
raise PDDLParsingError(
f"duplicate items {dupes} found in the typed list: {list(map(str, args))}'"
)

return {arg: None for arg in args}

def _process_type_def(self, type_def: List[str]) -> Set[str]:
"""Process a raw type_def and return a set of types."""
assert_(len(type_def) != 0, "unexpected parser state: empty type_def")

if len(type_def) == 1:
if len(args) == 1:
# single-typed type-def, return
return set(type_def)
return args

# if we are here, type_def is of the form (either t1 ... tn)
either_keyword, types = type_def[0], type_def[1:]
assert_(str(either_keyword) == EITHER)
return set(types)
# ignore first and last tokens since they are brackets.
either_keyword, types = args[1], args[2:-1]
assert_(str(either_keyword) == Symbols.EITHER.value)
return types

def _has_requirement(self, requirement: Requirements) -> bool:
"""Check whether a requirement is satisfied by the current state of the domain parsing."""
return requirement in self._extended_requirements


_domain_parser_lark = DOMAIN_GRAMMAR_FILE.read_text()
Expand Down
7 changes: 5 additions & 2 deletions pddl/parser/problem.lark
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
start: problem

problem: LPAR DEFINE problem_def problem_domain [requirements] [objects] init goal RPAR
problem: LPAR DEFINE problem_def problem_domain [problem_requirements] [objects] init goal RPAR
problem_def: LPAR PROBLEM NAME RPAR
problem_domain: LPAR DOMAIN_P NAME RPAR

problem_requirements: LPAR REQUIREMENTS require_key+ RPAR


objects: LPAR OBJECTS typed_list_name RPAR

init: LPAR INIT init_el* RPAR
Expand All @@ -29,7 +32,7 @@ GOAL: ":goal"
%ignore COMMENT

%import .common.COMMENT -> COMMENT
%import .domain.requirements -> requirements
%import .domain.require_key -> require_key
%import .domain.typed_list_name -> typed_list_name
%import .domain.predicate -> predicate
%import .common.NAME -> NAME
Expand Down
Loading
Loading