Skip to content

Souffle Integrations

souffle logo

Bases: Solver

A solver that uses Soufflé.

Soufflé is a logic programming language inspired by Datalog. For more details, see The Souffle site.

Note that in order to use this integration, you need to install Souffle and have it on your path.

>>> from typedlogic.integrations.frameworks.pydantic import FactBaseModel
>>> class AncestorOf(FactBaseModel):
...     ancestor: str
...     descendant: str
>>> solver = SouffleSolver()
>>> from typedlogic import SentenceGroup, PredicateDefinition
>>> solver.add_predicate_definition(PredicateDefinition(predicate="AncestorOf", arguments={'ancestor': str, 'descendant': str}))
>>> solver.add_fact(AncestorOf(ancestor='p1', descendant='p1a'))
>>> solver.add_fact(AncestorOf(ancestor='p1a', descendant='p1aa'))
>>> aa = SentenceGroup(name="transitivity-of-ancestor-of")
>>> solver.add_sentence_group(aa)
>>> soln = solver.check()

This solver does not implement the open-world assumption.

>>> from typedlogic.profiles import OpenWorld
>>> solver.profile.impl(OpenWorld)
False
Source code in src/typedlogic/integrations/solvers/souffle/souffle_solver.py
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
@dataclass
class SouffleSolver(Solver):
    """
    A solver that uses Soufflé.

    Soufflé is a logic programming language inspired by Datalog. For more details,
    see [The Souffle site](https://souffle-lang.github.io/).

    Note that in order to use this integration, you need to [install Souffle](https://souffle-lang.github.io/install)
    and have it on your path.

        >>> from typedlogic.integrations.frameworks.pydantic import FactBaseModel
        >>> class AncestorOf(FactBaseModel):
        ...     ancestor: str
        ...     descendant: str
        >>> solver = SouffleSolver()
        >>> from typedlogic import SentenceGroup, PredicateDefinition
        >>> solver.add_predicate_definition(PredicateDefinition(predicate="AncestorOf", arguments={'ancestor': str, 'descendant': str}))
        >>> solver.add_fact(AncestorOf(ancestor='p1', descendant='p1a'))
        >>> solver.add_fact(AncestorOf(ancestor='p1a', descendant='p1aa'))
        >>> aa = SentenceGroup(name="transitivity-of-ancestor-of")
        >>> solver.add_sentence_group(aa)
        >>> soln = solver.check()

    This solver does not implement the open-world assumption.

        >>> from typedlogic.profiles import OpenWorld
        >>> solver.profile.impl(OpenWorld)
        False

    """

    exec_name: str = field(default="souffle")
    profile: ClassVar[Profile] = MixedProfile(
        ClassicDatalog(), SortedLogic(), AllowsComparisonTerms(), SingleModelSemantics()
    )

    def models(self) -> Iterator[Model]:
        compiler = SouffleCompiler()
        program = compiler.compile(self.base_theory)
        pdmap = {}

        facts = []
        with tempfile.TemporaryDirectory() as temp_dir:
            # Create output directives for each predicate
            output_files = {}
            input_files = {}
            for pd in self.base_theory.predicate_definitions:
                pred = pd.predicate
                pdmap[pred] = pd
                output_file = Path(temp_dir) / f"{pred}.csv"
                output_files[pred] = str(output_file)
                program += f'\n.output {pred}(IO=file, filename="{output_file}")\n'
                input_file = Path(temp_dir) / f"{pred}__in.csv"
                input_files[pred] = str(input_file)
                program += f'\n.input {pred}(IO=file, filename="{input_file}")\n'
                with open(input_file, "w", encoding="utf-8") as csvfile:
                    writer = csv.writer(csvfile, delimiter="\t")
                    for term in self.base_theory.ground_terms:
                        if term.predicate == pred:
                            writer.writerow(term.bindings.values())

            with tempfile.NamedTemporaryFile(suffix=".dl", mode="w") as fp:
                fp.write(program)
                fp.flush()
                res = subprocess.run([self.exec_name, fp.name], capture_output=True)
                if res.stderr:
                    msg = res.stderr.decode()
                    import re

                    if re.match(r".*Variable (\S+) only occurs once.*", msg):
                        logger.info(msg)
                    else:
                        logger.error(msg)

            for pred, filename in output_files.items():
                if not Path(filename).exists():
                    continue
                pd = pdmap[pred]
                rows = []
                with open(filename, "r") as csvfile:
                    reader = csv.reader(csvfile, delimiter="\t")
                    for row in reader:
                        rows.append(row)
                facts.extend(make_terms(rows, pd))

        model = Model(source_object=self, ground_terms=facts)
        yield model

    def check(self) -> Solution:
        return Solution(satisfiable=None)

    def dump(self) -> str:
        compiler = SouffleCompiler()
        return compiler.compile(self.base_theory)