Skip to content

Prolog DSL

2P-Kt offers a Kotlin DSL for building terms, theories and queries without going through the textual Prolog parser. It is layered across four modules, each adding capabilities to a LogicProgrammingScope:

Module Adds
:dsl-core term construction (LogicProgrammingScope)
:dsl-unify unification operators on top of :dsl-core
:dsl-theory theory/clause management on top of :dsl-unify
:dsl-solve goal resolution on top of :dsl-theory

Each module exposes the same three entry-point functions — logicProgramming { ... }, its short alias lp { ... }, and (module-specific) prolog { ... } — that open a scope, run the given lambda as its receiver, and return the lambda's result. :dsl-solve's prolog { } is the most complete one, since LogicProgrammingScope there extends all the others:

/**
 * Entry point of the resolution-aware Prolog DSL: builds a fresh [LogicProgrammingScope] backed by a
 * [it.unibo.tuprolog.solve.MutableSolver] obtained from [solverFactory] (see
 * [LogicProgrammingScopeWithResolution.defaultSolver]), and runs [function] against it as the receiver. Everything
 * built or asserted inside [function] (facts, rules, queries via `solve`/`staticKb`/`dynamicKb`) shares that one
 * scope and that one underlying solver.
 *
 * ```kotlin
 * logicProgramming(SolverFactory) {
 *     staticKb(fact { "parent"("abraham", "isaac") })
 *     solve("parent"("abraham", "X")).forEach { println(it) }
 * }
 * ```
 *
 * @param solverFactory the [SolverFactory] used both to build the scope's default solver and, unless overridden,
 * as the source of [unificator].
 * @param unificator the [Unificator] shared by the scope's term-building helpers and its default solver; must be
 * compatible with [solverFactory] (see [LogicProgrammingScope.of]).
 */

Term construction (:dsl-core)

LogicProgrammingScope (it.unibo.tuprolog.dsl) composes several mixins: MinimalLogicProgrammingScope (structs, lists, clauses), LogicProgrammingScopeWithVariables (fresh variables), LogicProgrammingScopeWithOperators (infix arithmetic/relational/logical operators), LogicProgrammingScopeWithSubstitutions, and LogicProgrammingScopeWithPrologStandardLibrary.

Inside a scope, any String can be turned into a Struct by "calling" it with arguments, and Any (numbers, strings, terms) is auto-converted to a Term via Termificator:

"parent"("abraham", "isaac")     // Struct: parent(abraham, isaac)
"ancestor"("X", "Y")             // "X"/"Y" become Vars in the right position, atoms otherwise

Other builders: structOf(functor, *args), logicListOf(*items)/logicList(*items, tail = ...), tupleOf(*items), blockOf(*items), consOf(head, tail), factOf(term), directiveOf(term, *terms), and rule { } / fact { } / directive { } / clause { }, which build the corresponding Clause from the lambda's result.

LogicProgrammingScopeWithOperators supplies infix builders mirroring Prolog operators, e.g. a equalsTo b (=), a greaterThan b (>), a and b (,), a or b (;), a then b (->), xisy, and headifbody (builds a Rule, i.e. head :- body).

Unification (:dsl-unify)

LogicProgrammingScopeWithUnification adds the infix/functional unification operators documented in Unification API directly to the scope: term1 matches term2, match(term1, term2), term1 unifyWith term2, unify(term1, term2), term1 mguWith term2, mgu(term1, term2) — all backed by a configurable Unificator (LogicProgrammingScope.defaultUnificator).

    @Test
    fun testMatches() =
        logicProgramming {
            assertTrue(term1 matches term2)
        }

    @Test
    fun testMatch() =
        logicProgramming {
            assertTrue(match(term1, term2))
        }

    @Test
    fun testUnify() =
        logicProgramming {
            assertEquals(unified, unify(term1, term2))
        }

Theories (:dsl-theory)

LogicProgrammingScopeWithTheories layers a TheoryFactory (IndexedTheoryFactory by default) onto the scope, so rule { }/fact { } results can be assembled directly into a Theory — used by :dsl-solve to populate a solver's static knowledge base without touching Theory's own API (see Solver API).

Resolution (:dsl-solve)

LogicProgrammingScopeWithResolution wraps a Solver (a defaultSolver, built from the SolverFactory passed to logicProgramming(solverFactory) { }/prolog { }), exposing staticKb(vararg clauses) / staticKb(theory) to load facts/rules, and solve(goal, options = ...) /solveOnce/solveList mirroring Solver's own methods. prolog { } specifically defaults to Solver.prolog (the classic SLD solver).

Putting it together — the full DSL surface, in one query:

    fun testAbrahamFamilyTree() {
        prolog {
            staticKb(
                rule {
                    "ancestor"("X", "Y") `if` "parent"("X", "Y")
                },
                rule {
                    "ancestor"("X", "Y") `if` (
                        "parent"("X", "Z") and "ancestor"("Z", "Y")
                    )
                },
                fact { "parent"("abraham", "isaac") },
                fact { "parent"("isaac", "jacob") },
                fact { "parent"("jacob", "joseph") },
            )

            val actual = mutableListOf<String>()

            for (sol in solve("ancestor"("abraham", "X"))) {
                if (sol is Solution.Yes) {
                    actual.add(sol.substitution["X"].toString())
                }
            }

            assertEquals(
                mutableListOf("isaac", "jacob", "joseph"),
                actual,
            )
        }
    }

See Kotlin DSL background for the rationale behind layering the scope interfaces this way.