Skip to content

Ontology Syntax

pynmms.onto.syntax

Ontology-style concept/role assertion parsing for NMMS.

Extends the propositional parser with concept assertions (C(a)) and role assertions (R(a,b)) for ontology-style reasoning.

Grammar additions (beyond propositional)::

concept_atom  ::= CONCEPT '(' INDIVIDUAL ')'
role_atom     ::= ROLE '(' INDIVIDUAL ',' INDIVIDUAL ')'

The parser tries binary connectives first (at depth-0), then ontology-specific patterns (role assertions, concept assertions). Bare propositional atoms are rejected -- use concept assertions C(a) or role assertions R(a,b) instead.

OntoSentence dataclass

Immutable AST node for an ontology-style sentence.

Attributes:

Name Type Description
type str

One of ATOM_CONCEPT, ATOM_ROLE.

concept str | None

Concept name (for concept assertions).

individual str | None

Individual name (for concept assertions).

role str | None

Role name (for role assertions).

arg1 str | None

First argument of role assertion.

arg2 str | None

Second argument of role assertion.

Source code in pynmms/onto/syntax.py
@dataclass(frozen=True, slots=True)
class OntoSentence:
    """Immutable AST node for an ontology-style sentence.

    Attributes:
        type: One of ATOM_CONCEPT, ATOM_ROLE.
        concept: Concept name (for concept assertions).
        individual: Individual name (for concept assertions).
        role: Role name (for role assertions).
        arg1: First argument of role assertion.
        arg2: Second argument of role assertion.
    """

    type: str
    concept: str | None = None
    individual: str | None = None
    role: str | None = None
    arg1: str | None = None
    arg2: str | None = None

    def __str__(self) -> str:
        if self.type == ATOM_CONCEPT:
            return f"{self.concept}({self.individual})"
        if self.type == ATOM_ROLE:
            return f"{self.role}({self.arg1},{self.arg2})"
        return f"OntoSentence({self.type})"  # pragma: no cover

parse_onto_sentence(s)

Parse a string into a propositional Sentence or OntoSentence AST.

Tries binary connectives first (at depth-0), then ontology-specific patterns (role assertions, concept assertions), then falls through to propositional negation.

Source code in pynmms/onto/syntax.py
def parse_onto_sentence(s: str) -> Sentence | OntoSentence:
    """Parse a string into a propositional Sentence or OntoSentence AST.

    Tries binary connectives first (at depth-0), then ontology-specific patterns
    (role assertions, concept assertions), then falls through to propositional
    negation.
    """
    s = s.strip()
    if not s:
        raise ValueError("Cannot parse empty sentence")

    # Strip outer parens if they wrap the entire expression
    if s.startswith("(") and s.endswith(")"):
        depth = 0
        all_wrapped = True
        for i, c in enumerate(s):
            if c == "(":
                depth += 1
            elif c == ")":
                depth -= 1
            if depth == 0 and i < len(s) - 1:
                all_wrapped = False
                break
        if all_wrapped:
            return parse_onto_sentence(s[1:-1])

    # --- Binary connectives at depth 0, lowest precedence first ---

    # Implication (right-associative, lowest precedence)
    depth = 0
    for i in range(len(s)):
        c = s[i]
        if c == "(":
            depth += 1
        elif c == ")":
            depth -= 1
        elif depth == 0 and s[i : i + 2] == "->":
            left_str = s[:i].strip()
            right_str = s[i + 2 :].strip()
            if not left_str or not right_str:
                raise ValueError(f"Malformed implication in: {s!r}")
            return Sentence(
                type=IMPL,
                left=parse_sentence(left_str),
                right=parse_sentence(right_str),
            )

    # Disjunction (left-associative) -- find last '|' at depth 0
    depth = 0
    last_disj = -1
    for i, c in enumerate(s):
        if c == "(":
            depth += 1
        elif c == ")":
            depth -= 1
        elif depth == 0 and c == "|":
            last_disj = i
    if last_disj >= 0:
        left_str = s[:last_disj].strip()
        right_str = s[last_disj + 1 :].strip()
        if not left_str or not right_str:
            raise ValueError(f"Malformed disjunction in: {s!r}")
        return Sentence(
            type=DISJ,
            left=parse_sentence(left_str),
            right=parse_sentence(right_str),
        )

    # Conjunction (left-associative) -- find last '&' at depth 0
    depth = 0
    last_conj = -1
    for i, c in enumerate(s):
        if c == "(":
            depth += 1
        elif c == ")":
            depth -= 1
        elif depth == 0 and c == "&":
            last_conj = i
    if last_conj >= 0:
        left_str = s[:last_conj].strip()
        right_str = s[last_conj + 1 :].strip()
        if not left_str or not right_str:
            raise ValueError(f"Malformed conjunction in: {s!r}")
        return Sentence(
            type=CONJ,
            left=parse_sentence(left_str),
            right=parse_sentence(right_str),
        )

    # Negation
    if s.startswith("~"):
        sub_str = s[1:].strip()
        if not sub_str:
            raise ValueError("Negation with no operand")
        return Sentence(type=NEG, sub=parse_sentence(sub_str))

    # --- Ontology-specific atomic patterns ---

    # Role assertion: R(a,b)
    m = _ROLE_RE.match(s)
    if m:
        return OntoSentence(
            type=ATOM_ROLE,
            role=m.group(1),
            arg1=m.group(2),
            arg2=m.group(3),
        )

    # Concept assertion: C(a)
    m = _CONCEPT_RE.match(s)
    if m:
        return OntoSentence(
            type=ATOM_CONCEPT,
            concept=m.group(1),
            individual=m.group(2),
        )

    # Bare propositional atoms are not valid in NMMS_Onto.
    raise ValueError(
        f"Bare atom {s!r} is not valid in NMMS_Onto. "
        f"Use concept assertions C(a) or role assertions R(a,b)."
    )

is_onto_atomic(s)

Return True if s is a concept assertion or role assertion.

Source code in pynmms/onto/syntax.py
def is_onto_atomic(s: str) -> bool:
    """Return True if *s* is a concept assertion or role assertion."""
    try:
        parsed = parse_onto_sentence(s)
    except ValueError:
        return False
    if isinstance(parsed, OntoSentence):
        return parsed.type in (ATOM_CONCEPT, ATOM_ROLE)
    return False

all_onto_atomic(sentences)

Return True if every sentence in sentences is onto-atomic.

Source code in pynmms/onto/syntax.py
def all_onto_atomic(sentences: frozenset[str]) -> bool:
    """Return True if every sentence in *sentences* is onto-atomic."""
    return all(is_onto_atomic(s) for s in sentences)

make_concept_assertion(concept, individual)

Construct C(a) string.

Source code in pynmms/onto/syntax.py
def make_concept_assertion(concept: str, individual: str) -> str:
    """Construct ``C(a)`` string."""
    return f"{concept}({individual})"

make_role_assertion(role, arg1, arg2)

Construct R(a,b) string.

Source code in pynmms/onto/syntax.py
def make_role_assertion(role: str, arg1: str, arg2: str) -> str:
    """Construct ``R(a,b)`` string."""
    return f"{role}({arg1},{arg2})"