Skip to content

Primitives and functions

2P-Kt implements built-in predicates and arithmetic operators in Kotlin, rather than as Prolog clauses, through two fun interfaces in the :solve module: Primitive (predicates, called during resolution) and LogicFunction (arithmetic functions, called while evaluating is/2 and friends). Both are indexed by Signature inside a Library — see Libraries.

Primitives

fun interface Primitive {
    @JsName("solve")
    fun solve(request: Solve.Request<ExecutionContext>): Sequence<Solve.Response>

A Primitive takes a Solve.Request and returns a Sequence<Solve.Response> — one response per solution the primitive contributes (a deterministic predicate returns a one-element sequence, between/3 returns as many responses as integers in the range, repeat/0 returns an infinite sequence, and so on).

Requests and immutability

Solve.Request is an immutable data class:

    data class Request<out C : ExecutionContext>(
        /** Signature of the goal to be solved in this [Request] */
        @JsName("signature")
        val signature: Signature,
        /** Arguments with which the goal is invoked in this [Request] */
        @JsName("arguments")
        val arguments: List<Term>,
        /** The context that's current at Request making */
        @JsName("context")
        val context: C,
        /** The time instant when the request was submitted for resolution */
        override val startTime: TimeInstant = currentTimeInstant(),
        /** The execution max duration after which the computation should end, because no more useful */
        override val maxDuration: TimeDuration = context.endTime - startTime,
    ) : Solve(),
        Durable {
  • signature / arguments — what is being called and with what; validated at construction against arity/vararg;
  • context: C — the ExecutionContext (an immutable snapshot of the solver's state — see Solver API) current when the request was made;
  • startTime / maxDuration (from Durable) — used to enforce timeouts;
  • query: Struct — the request rebuilt as a Struct, lazily.

Because everything is val, a primitive cannot mutate its own request or the context it was given: subSolver() spins up a fresh Solver sharing the request's context to recurse into sub-goals (used by e.g. findall/3), and any change to global-looking state (the theory, flags, libraries, channels...) is expressed declaratively as data, not performed in place — see below.

Responses and side effects

A primitive replies by building a Solve.Response out of its Request, via one of the replyWith*/replySuccess/ replyFail/replyException helpers:

    /** Class representing a Response, from the Solver, to a [Solve.Request] */
    data class Response(
        /** The solution attached to the response */
        @JsName("solution")
        val solution: Solution,
        /** The Prolog flow modification manager after request execution (use `null` in case nothing changed) */
        @JsName("sideEffectManager")
        val sideEffectManager: SideEffectManager? = null,
        /** The (possibly empty) [List] of [SideEffect]s to be applied to the execution context after a primitive has been
         * executed */
        @JsName("sideEffects")
        val sideEffects: List<SideEffect>,
    ) : Solve() {
  • solution: Solution — the Yes/No/Halt produced for this response (see Solver API);
  • sideEffectManager: SideEffectManager? — an optional low-level hook into the engine's control flow (used internally, e.g. by cut);
  • sideEffects: List<SideEffect> — a description of state changes the engine should apply to the next context after this response, since the primitive itself cannot mutate its (immutable) ExecutionContext.

SideEffect (it.unibo.tuprolog.solve.sideffects) is a sealed hierarchy of such descriptions, grouped by what they touch: knowledge bases (AddStaticClauses, RemoveDynamicClauses, ResetDynamicKb, ...), flags (SetFlags, ResetFlags, ClearFlags), the library runtime (LoadLibrary, UnloadLibraries, AddLibraries, ResetRuntime), operators (SetOperators, RemoveOperators), channels (OpenInputChannels, CloseOutputChannels, ...), and custom per-request data at three lifetimes (SetEphemeralData, SetPersistentData, SetDurableData — backing the get_ephemeral/2/get_persistent/2/get_durable/2 primitives). Each knows how to applyTo(context) to produce the next ExecutionContext.

PrimitiveWrapper

Concrete primitives extend PrimitiveWrapper<C>, implementing uncheckedImplementation(request): Sequence<Solve.Response>; the base class wraps it with Primitive.enforcingSignature so a mismatched signature throws instead of running:

object Between : TernaryRelation.WithoutSideEffects<ExecutionContext>("between") {
    override fun Solve.Request<ExecutionContext>.computeAllSubstitutions(
        first: Term,
        second: Term,
        third: Term,
    ): Sequence<Substitution> {
        ensuringArgumentIsInstantiated(0)
        ensuringArgumentIsInstantiated(1)

PrimitiveWrapper's companion also carries a library of Solve.Request extension functions used for argument validation, each throwing the appropriate LogicError (see Errors and exceptions) on failure and returning this otherwise, so they chain fluently: ensuringArgumentIsInstantiated, ensuringArgumentIsInteger, ensuringArgumentIsAtom, ensuringArgumentIsList, ensuringArgumentIsCallable, ensuringArgumentIsWellFormedIndicator, ensuringProcedureHasPermission, and more.

Functions

fun interface LogicFunction {
    @JsName("compute")
    fun compute(request: Compute.Request<ExecutionContext>): Compute.Response

A LogicFunction takes a Compute.Request and returns a single Compute.Response — functions are pure and deterministic (no side effects, no multiple solutions), matching how Prolog arithmetic expressions behave:

    /** Class representing a Request to be full-filled by the Expression evaluator */
    data class Request<out C : ExecutionContext>(
        /** Signature of the function to be executed in this [Request] */
        @JsName("signature")
        val signature: Signature,
        /** Arguments with which the function is invoked in this [Request] */
        @JsName("arguments")
        val arguments: List<Term>,
        /** The context that's current at Request making */
        @JsName("context")
        val context: C,
        /** The time instant when the request was submitted for evaluation */
        @JsName("requestIssuingInstant")
        val requestIssuingInstant: TimeInstant = currentTimeInstant(),
        /** The execution max duration after which the computation should end, because no more useful */
        @JsName("executionMaxDuration")
        val executionMaxDuration: TimeDuration = TimeDuration.MAX_VALUE,
    ) : Compute() {

Compute.Request mirrors Solve.Request (signature, arguments, context, timing) minus anything related to non-determinism or side effects; replyWith(result: Term) builds the Response.

Concrete functions extend FunctionWrapper<C> (implementing uncheckedImplementation), most commonly through the narrower MathFunction base, which adds helpers to raise the right EvaluationError/TypeError (throwIntOverflowError, throwZeroDivisorError, throwUndefinedError, ...). Arity-specific subclasses (NullaryMathFunction, UnaryMathFunction, BinaryMathFunction, IntegersBinaryMathFunction) let implementers overload per numeric-type combination instead of pattern-matching manually:

object Addition : BinaryMathFunction("+") {
    override fun mathFunction(
        integer1: Integer,
        integer2: Integer,
        context: ExecutionContext,
    ): Numeric =
        Numeric.of(
            integer1.value + integer2.value,
        ) // TODO: 24/10/2019 "int_overflow" check missing (see the standard)

    override fun mathFunction(
        real: Real,
        integer: Integer,
        context: ExecutionContext,
    ): Numeric = commonBehaviour(real.value, integer.decimalValue)

See Default predicates for the full catalogue of built-in primitives, functions and rules, and Solver design for why side effects are represented as data instead of being applied directly.