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— theExecutionContext(an immutable snapshot of the solver's state — see Solver API) current when the request was made;startTime/maxDuration(fromDurable) — used to enforce timeouts;query: Struct— the request rebuilt as aStruct, 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— theYes/No/Haltproduced 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.