def _standardize_quantified_variables(node: syntax.Node, state: syntax.WalkState) -> syntax.Node: """Standardizes quantified variables by giving them unique names. :param node: The node to rewrite. :param state: Rewriting state. :returns: Rewritten node. Rewrites expressions of type: - `*x: A(x)` into `*var_1: A(var_1)`, - `?x: A(x)` into `?var_1: A(var_1)`. """ seen: List[syntax.Node] = state.context.setdefault('seen', []) replaced: syntax.T_Substitution = state.context.setdefault('replaced', {}) # todo: replaced would not work with nested quantified formulas that # reuse the same symbol, but actually never mind - it's a corner case # I don't want to fiddle with now if node in seen: return node if node.is_quantified(): old = node.get_quantified_variable().value if old.startswith('_'): return node # already renamed new = _new_variable_name() qtype = node.get_quantifier_type() quant = syntax.make_quantifier(qtype, new) rv = syntax.make_formula(quant, node.children) replaced[new] = node.get_quantified_variable() state.stack.append((old, new)) seen.append(rv) return rv elif node.is_variable(): # reversed, because we want to rename symbol to the last seen value. # Example: We want to rewrite `?x, ?x: x` into `?a: ?b: b`. for old, new in reversed(state.stack): if old == node.value: rv = syntax.make_variable(new) seen.append(rv) return rv return node else: return node
def _standardize_free_variables(node: syntax.Node, state: syntax.WalkState) -> syntax.Node: """Standardizes free variables by giving them unique names. :param node: The node to rewrite. :param state: Rewriting state. :returns: Rewritten node. """ seen: List[syntax.Node] = state.context.setdefault('seen', []) replaced: syntax.T_Substitution = state.context.setdefault('replaced', {}) if node in seen: return node if node.is_variable(): old = node.value if old.startswith('_'): return node # already renamed try: new = next(k for k, v in replaced.items() if v.value == old) except StopIteration: new = _new_variable_name() replaced[new] = node rv = syntax.make_variable(new) seen.append(rv) return rv else: return node
def convert_to_cnf( node: syntax.Node) -> Tuple[syntax.Node, syntax.T_Substitution]: """Converts node to CNF representation. :param node: The node to convert. :returns: Node in CNF. """ if not node.is_formula(): raise ValueError() # todo: custom exc rv = {} node = node.denormalize() assert node.is_formula() for f in (_eliminate_biconditional, _eliminate_implication, _propagate_negation, _standardize_quantified_variables, _standardize_free_variables, _skolemize, _distribute_conjunction): state = syntax.WalkState.make() node = syntax.walk(node, f, state=state) replaced = state.context.get('replaced', {}) assert not (replaced.keys() & rv.keys()) rv = unification.compose(rv, replaced) node = node.normalize() assert node.is_formula() assert node.is_cnf() return node, rv
def unify(p: syntax.Node, q: syntax.Node) -> syntax.T_Substitution: """Unifies two expressions via Robinson's unification algorithm.""" if not p.is_term() and not p.is_atom(): raise TypeError(f"'{p}' is not a term and neither an atom") if not q.is_term() and not q.is_atom(): raise TypeError(f"'{q}' is not a term and neither an atom") if p.is_constant() and q.is_constant(): if p.value != q.value: raise NotUnifiable() else: return {} # already unified elif p.is_variable(): if p.occurs_in(q): raise NotUnifiable() else: return {p.value: q} elif q.is_variable(): if q.occurs_in(p): raise NotUnifiable() else: return {q.value: p} else: if (p.type_ != q.type_ or p.value != q.value or len(p.children) != len(q.children)): raise NotUnifiable() else: rv = {} for x, y in zip(p.children, q.children): x = x.apply(rv) y = y.apply(rv) rv = compose(rv, unify(x, y)) return rv
def _eliminate_implication(node: syntax.Node, *args) -> syntax.Node: """Eliminates implication. :param node: The node to rewrite. :returns: Rewritten node. Rewrites expressions of type `A => B` into `!A | B`. """ if not node.is_implication(): return node a, b = node.children children = [a.negate(), b] return syntax.make_formula(syntax.DISJUNCTION, children)
def _propagate_negation(node: syntax.Node, *args) -> syntax.Node: """Propagates negations down the syntax tree. :param node: The node to rewrite. :returns: Rewritten node. Rewrites expressions of type: - `!!A` into `A`, - `!(A & B)` into `!A | !B`, - `!(A | B)` into `!A & !B`, - `!(*x: A)` into `?x: !A`,` - `!(?x: A)` into `*x: !A`. """ if not node.is_negation(): return node rv = None child = node.children[0] # Double negation if child.is_negation(): return child.children[0] # De Morgan elif child.is_conjunction(): rv = syntax.DISJUNCTION # De Morgan elif child.is_disjunction(): rv = syntax.CONJUNCTION # Flip Quantifiers elif child.is_quantified(): qtype = next( k for k in [syntax.UNIVERSAL_QUANTIFIER, syntax.EXISTENTIAL_QUANTIFIER] if k != child.get_quantifier_type()) qname = child.get_quantified_variable().value rv = syntax.make_quantifier(qtype, qname) if rv is None: return node else: children = [x.negate() for x in child.children] return syntax.make_formula(rv, children)
def _eliminate_biconditional(node: syntax.Node, *args) -> syntax.Node: """Eliminates biconditional. :param node: The node to rewrite. :returns: Rewritten node. Rewrites expressions of type `A <=> B` into `(A => B) & (B => A)`. """ if not node.is_equivalence(): return node a, b = node.children child1 = syntax.make_formula(syntax.IMPLICATION, [a, b]) child2 = syntax.make_formula(syntax.IMPLICATION, [b, a]) children = [child1, child2] return syntax.make_formula(syntax.CONJUNCTION, children)
def _distribute_conjunction(node: syntax.Node, *args) -> syntax.Node: """Distributes conjunctions over disjunctions. :param node: The node to rewrite. :returns: Rewritten node. Rewrites expressions of type `(A & B) | C` into `(A | C) & (B | C)`. """ if not node.is_disjunction(): return node for child in node.children: other = next(x for x in node.children if x is not child) if child.is_conjunction(): a, b = child.children rv1 = syntax.make_formula(syntax.DISJUNCTION, [a, other]) rv2 = syntax.make_formula(syntax.DISJUNCTION, [b, other]) rv = syntax.make_formula(syntax.CONJUNCTION, [rv1, rv2]) return rv return node
def _skolemize(node: syntax.Node, state: syntax.WalkState) -> syntax.Node: """Skolemizes expressions and drops quantifiers. :param node: The node to rewrite. :param state: Rewriting state. :returns: Rewritten node. Rewrites expressions of type: - `?x: x` into `C1`, - `*a: a & ?x: x` into `a & F1(a)`, - `*a: a & ?x: x & *b: b & ?y: y` into `a & F1(a) & b & F2(a, b)`, """ if not state.stack: state.stack.extend([[], []]) # enclosing universally quantified variables universal: List[str] = state.stack[0] replacements: List[Tuple[str, syntax.Node]] = state.stack[1] replaced: syntax.T_Substitution = state.context.setdefault('replaced', {}) if node.is_quantified(): qtype = node.get_quantifier_type() if qtype == syntax.UNIVERSAL_QUANTIFIER: qv = node.get_quantified_variable() universal.append(qv.value) else: assert qtype == syntax.EXISTENTIAL_QUANTIFIER # store what to replace (actual replacement happens in the # elif branch below) qv = node.get_quantified_variable() old = qv.value if universal: name = _new_function_name() new = syntax.make_function(name, *universal) else: name = _new_constant_name() new = syntax.make_constant(name) replaced[new.value] = qv replacements.append((old, new)) # drop quantifiers children = node.children assert len(children) == 1 return children[0] elif node.is_variable(): for old, new in replacements: if old == node.value: return copy.deepcopy(new) return node else: return node
def _is_symbol(node: syntax.Node) -> bool: return (node.is_constant() or node.is_variable() or node.is_function() or (node.is_predicate() and not node.is_equality()))