Module swiplserver.prologmqi

Allows using SWI Prolog as an embedded part of an application, "like a library".

swiplserver enables SWI Prolog queries to be executed from within your Python application as if Python had a Prolog engine running inside of it. Queries are sent as strings like "atom(foo)" and the response is JSON.

swiplserver provides:

  • The PrologMQI class that automatically manages starting and stopping a SWI Prolog instance and starts the Machine Query Interface ('MQI') using the mqi_start/1 predicate to allow running Prolog queries.
  • The PrologThread class is used to run queries on the created process. Queries are run exactly as they would be if you were interacting with the SWI Prolog "top level" (i.e. the Prolog command line).

Installation

  1. Install SWI Prolog (www.swi-prolog.org) and ensure that "swipl" is on the system path.
  2. Either "pip install swiplserver" or copy the "swiplserver" library (the whole directory) from the "libs" directory of your SWI Prolog installation to be a subdirectory of your Python project.
  3. Check if your SWI Prolog version includes the Machine Query Interface by launching it and typing ?- mqi_start([]). If it can't find it, see below for how to install it.

If your SWI Prolog doesn't yet include the Machine Query Interface:

  1. Download the mqi.pl file from the GitHub repository.
  2. Open an operating system command prompt and go to the directory where you downloaded mqi.pl.
  3. Run the below command. On Windows the command prompt must be run as an administrator. On Mac or Linux, start the command with sudo as in sudo swipl -s ....
swipl -s mqi.pl -g "mqi:install_to_library('mqi.pl')" -t halt

Usage

PrologThread represents a thread in Prolog (it is not a Python thread!). A given PrologThread instance will always run queries on the same Prolog thread (i.e. it is single threaded within Prolog).

To run a query and wait until all results are returned:

from swiplserver import PrologMQI, PrologThread

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread:
        result = prolog_thread.query("atom(a)")
        print(result)

True

To run a query that returns multiple results and retrieve them as they are available:

from swiplserver import PrologMQI, PrologThread

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread:
        prolog_thread.query_async("member(X, [first, second, third])",
                                  find_all=False)
        while True:
            result = prolog_thread.query_async_result()
            if result is None:
                break
            else:
                print(result)
first
second
third

Creating two PrologThread instances allows queries to be run on multiple threads in Prolog:

from swiplserver import PrologMQI, PrologThread

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread1:
        with mqi.create_thread() as prolog_thread2:
            prolog_thread1.query_async("sleep(2), writeln(first_thread(true))")
            prolog_thread2.query_async("sleep(1), writeln(second_thread(true))")
            thread1_answer = prolog_thread1.query_async_result()
            thread2_answer = prolog_thread2.query_async_result()

Prolog: second_thread(true)
Prolog: first_thread(true)

Output printed in Prolog using writeln/1 or errors output by Prolog itself are written to Python's logging facility using the swiplserver log and shown prefixed with "Prolog:" as above.

Answers to Prolog queries that are not simply True or False are converted to JSON using the json_to_prolog/2 predicate in Prolog. They are returned as a Python dict with query variables as the keys and standard JSON as the values. If there is more than one answer, it is returned as a list:

from swiplserver import PrologMQI, PrologThread

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread:
        result = prolog_thread.query("member(X, [color(blue), color(red)])")
        print(result)

[{'X': {'functor': 'color', 'args': ['blue']}},
 {'X': {'functor': 'color', 'args': ['red']}}]

Exceptions in Prolog code are raised using Python's native exception facilities.

Debugging

When using swiplserver, debugging the Prolog code itself can often be done by viewing traces from the Prolog native writeln/1 or debug/3 predicates and viewing their output in the debugger output window. Sometimes an issue occurs deep in an application and it would be easier to set breakpoints and view traces in Prolog itself. Running SWI Prolog manually and launching the Machine Query Interface in "Standalone mode" is designed for this scenario.

swiplserver normally launches SWI Prolog and starts the Machine Query Interface so that it can connect and run queries. To debug your code using Prolog itself, you can do this manually and connect your application to it. A typical flow for standalone mode is:

  1. Launch SWI Prolog and call the mqi_start/1 predicate, specifying a port and password (documentation is here). Use the tdebug/0 predicate to set all threads to debugging mode like this: tdebug, mqi_start([port(4242), password(debugnow)]).
  2. Optionally run the predicate debug(mqi(_)). in Prolog to turn on tracing for the Machine Query Interface.
  3. Set the selected port and password when you call PrologMQI.
  4. Launch the application and go through the steps to reproduce the issue.

(As the Machine Query Interface is multithreaded, debugging the running code requires using the multithreaded debugging features of SWI Prolog as described in the section on "Debugging Threads" in the SWI Prolog documentation.)

At this point, all of the multi-threaded debugging tools in SWI Prolog are available for debugging the problem. If the issue is an unhandled or unexpected exception, the exception debugging features of SWI Prolog can be used to break on the exception and examine the state of the application. If it is a logic error, breakpoints can be set to halt at the point where the problem appears, etc.

Note that, while using a library to access Prolog will normally end and restart the process between runs of the code, running the Machine Query Interface standalone won't. You'll either need to relaunch between runs or build your application so that it does the initialization at startup.

Functions

def create_posix_path(os_path)

Convert a file path in whatever the current OS path format is to be a posix path so Prolog can understand it.

This is useful for Prolog predicates like consult which need a Posix path to be passed in on any platform.

def is_prolog_atom(json_term)

True if json_term is Prolog JSON representing a Prolog atom. See PrologThread.query() for documentation on the Prolog JSON format.

def is_prolog_functor(json_term)

True if json_term is Prolog JSON representing a Prolog functor (i.e. a term with zero or more arguments). See PrologThread.query() for documentation on the Prolog JSON format.

def is_prolog_list(json_term)

True if json_term is Prolog JSON representing a Prolog list. See PrologThread.query() for documentation on the Prolog JSON format.

def is_prolog_variable(json_term)

True if json_term is Prolog JSON representing a Prolog variable. See PrologThread.query() for documentation on the Prolog JSON format.

def json_to_prolog(json_term)

Convert json_term from the Prolog JSON format to a string that represents the term in the Prolog language. See PrologThread.query() for documentation on the Prolog JSON format.

def prolog_args(json_term)

Return the arguments from json_term if json_term is in the Prolog JSON format. See PrologThread.query() for documentation on the Prolog JSON format.

def prolog_name(json_term)

Return the atom (if json_term is an atom), variable (if a variable) or functor name of json_term. json_term must be in the Prolog JSON format. See PrologThread.query() for documentation on the Prolog JSON format.

def quote_prolog_identifier(identifier: str)

Surround a Prolog identifier with '' if Prolog rules require it.

Classes

class PrologConnectionFailedError (exception_json)

Raised when the connection used by a PrologThread fails. Indicates that the Machine Query Interface will no longer respond.

Ancestors

  • PrologError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class PrologError (exception_json)

Base class used for all exceptions raised by swiplserver.PrologMQI except for PrologLaunchError. Used directly when an exception is thrown by Prolog code itself, otherwise the subclass exceptions are used.

Ancestors

  • builtins.Exception
  • builtins.BaseException

Subclasses

Methods

def is_prolog_exception(self, term_name)

True if the exception thrown by Prolog code has the term name specified by term_name.

Args

term_name
The name of the Prolog term to test for.
def json(self)

Returns

A string that represents the Prolog exception in Prolog json form. See PrologThread.query() for documentation on the Prolog json format.

def prolog(self)

Returns

A string that represents the Prolog exception in the Prolog native form.

class PrologLaunchError (*args, **kwargs)

Raised when the SWI Prolog process was unable to be launched for any reason.

Ancestors

  • builtins.Exception
  • builtins.BaseException
class PrologMQI (launch_mqi: bool = True, port: int = None, password: str = None, unix_domain_socket: str = None, query_timeout_seconds: float = None, pending_connection_count: int = None, output_file_name: str = None, mqi_traces: str = None, prolog_path: str = None, prolog_path_args: list = None)

Initialize a PrologMQI class that manages a SWI Prolog process associated with your application process. PrologMQI.start() actually launches the process if launch_mqi is True.

This class is designed to allow Prolog to be used "like a normal Python library" using the Machine Query Interface of SWI Prolog. All communication is done using protocols that only work on the same machine as your application (localhost TCP/IP or Unix Domain Sockets), and the implementation is designed to make sure the process doesn't hang around even if the application is terminated unexpectedly (as with halting a debugger).

All arguments are optional and the defaults are set to the recommended settings that work best on all platforms during development. In production on Unix systems, consider using unix_domain_socket to further decrease security attack surface area.

For debugging scenarios, SWI Prolog can be launched manually and this class can be configured to (locally) connect to it using launch_mqi = False. This allows for inspection of the Prolog state and usage of the SWI Prolog debugging tools while your application is running. See the documentation for the Prolog mqi_start/1 predicate for more information on how to run the Machine Query Interface in "Standalone Mode".

Examples

To automatically launch a SWI Prolog process using TCP/IP localhost and an automatically chosen port and password (the default):

with PrologMQI() as mqi:
    # your code here

To connect to an existing SWI Prolog process that has already started the mqi_start/1 predicate and is using an automatically generated Unix Domain Socket (this value will be different for every launch) and a password of '8UIDSSDXLPOI':

with PrologMQI(launch_mqi = False,
                  unix_domain_socket = '/tmp/swipl_udsock_15609_1/swipl_15609_2',
                  password = '8UIDSSDXLPOI') as mqi:
    # your code here

Args

launch_mqi
True (default) launch a SWI Prolog process on PrologMQI.start() and shut it down automatically on PrologMQI.stop() (or after a resource manager like the Python "with" statement exits). False connects to an existing SWI Prolog process that is running the mqi_start/1 predicate (i.e. "Standalone Mode"). When False, password and one of port or unix_domain_socket must be specified to match the options provided to mqi_start/1 in the separate SWI Prolog process.
port

The TCP/IP localhost port to use for communication with the SWI Prolog process. Ignored if unix_domain_socket is not None.

  • When launch_mqi is True, None (default) automatically picks an open port that the Machine Query Interface and this class both use.
  • When launch_mqi is False, must be set to match the port specified in mqi_start/1 of the running SWI Prolog process.
password

The password to use for connecting to the SWI Prolog process. This is to prevent malicious users from connecting to the Machine Query Interface since it can run arbitrary code. Allowing the MQI to generate a strong password by using None is recommended.

  • When launch_mqi is True, None (default) automatically generates a strong password using a uuid. Other values specify the password to use.
  • When launch_mqi is False, must be set to match the password specified in mqi_start/1 of the running SWI Prolog process.
unix_domain_socket

None (default) use localhost TCP/IP for communication with the SWI Prolog process. Otherwise (only on Unix) is either a fully qualified path and filename of the Unix Domain Socket to use or an empty string (recommended). An empty string will cause a temporary directory to be created using Prolog's tmp_file/2 and a socket file will be created within that directory following the below requirements. If the directory and file are unable to be created for some reason, PrologMQI.start() with raise an exception. Specifying a file to use should follow the same guidelines as the generated file:

  • If the file exists when the Machine Query Interface is launched, it will be deleted.
  • The Prolog process will attempt to create and, if Prolog exits cleanly, delete this file when the Machine Query Interface closes. This means the directory must have the appropriate permissions to allow the Prolog process to do so.
  • For security reasons, the filename should not be predictable and the directory it is contained in should have permissions set so that files created are only accessible to the current user.
  • The path must be below 92 bytes long (including null terminator) to be portable according to the Linux documentation.
query_timeout_seconds
None (default) set the default timeout for all queries to be infinite (this can be changed on a per query basis). Other values set the default timeout in seconds.
pending_connection_count

Set the default number of pending connections allowed on the Machine Query Interface. Since the MQI is only connected to by your application and is not a server, this value should probably never be changed unless your application is creating new PrologThread objects at a very high rate.

  • When launch_mqi is True, None uses the default (5) and other values set the count.
  • When launch_mqi is False, ignored.
output_file_name
Provide the file name for a file to redirect all Prolog output (STDOUT and STDERR) to. Used for debugging or gathering a log of Prolog output. None outputs all Prolog output to the Python logging infrastructure using the 'swiplserver' log. If using multiple Machine Query Interfaces in one SWI Prolog instance, only set this on the first one. Each time it is set the output will be deleted and redirected.
mqi_traces

(Only used in unusual debugging circumstances) Since these are Prolog traces, where they go is determined by output_file_name.

  • None (the default) does not turn on mqi tracing
  • "_" turns on all tracing output from the Prolog Machine Query Interface (i.e. runs debug(mqi(_)). in Prolog).
  • "protocol" turns on only protocol level messages (which results in much less data in the trace for large queries)
  • "query" turns on only messages about the query.
prolog_path
(Only used for unusual testing situations) Set the path to where the swipl executable can be found.
prolog_path_args
(Only used for unusual testing situations) Set extra command line arguments to be sent to swipl when it is launched.

Raises

ValueError if the arguments don't make sense. For example: choosing Unix Domain Sockets on Windows or setting output_file with launch_mqi = False

Static methods

def unix_domain_socket_file(directory: str)

Creates a non-predictable Filename 36 bytes long suitable for using in the unix_domain_socket argument of the PrologMQI constructor. Appends it to directory.

Note that Python's gettempdir() function generates paths which are often quite large on some platforms and thus (at the time of this writing) is not suitable for use as the directory. The recommendation is to create a custom directory in a suitably short path (see notes below on length) in the filesystem and use that as directory. Ensure that the permissions for this folder are set as described below.

Args

directory

The fully qualified directory the file name will be appended to. Note that:

  • The directory containing the file must grant the user running the application (and ideally only that user) the ability to create and delete files created within it.
  • The total path (including the 36 bytes used by the file) must be below 92 bytes long (including null terminator) to be portable according to the Linux documentation.

Returns

A fully qualified path to a file in directory.

Methods

def create_thread(self)

Create a new PrologThread instance for this PrologMQI.

Examples

Using with the Python with statement is recommended:

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread:
        # Your code here

Returns

A PrologThread instance.

def process_id(self)

Retrieve the operating system process id of the SWI Prolog process that was launched by this class.

Returns

None if the value of launch_mqi passed to PrologMQI is False or if PrologMQI.start() has not yet been called. Otherwise return the operating system process ID.

def start(self)

Start a new SWI Prolog process associated with this class using the settings from PrologMQI and start the Machine Query Interface using the mqi_start Prolog predicate. If launch_mqi is False, does nothing.

To create the SWI Prolog process, 'swipl' must be on the system path. Manages the lifetime of the process it creates, ending it on PrologMQI.stop().

Raises

PrologLaunchError
The SWI Prolog process was unable to be launched. Often indicates that swipl is not in the system path.
def stop(self, kill=False)

Stop the SWI Prolog process and wait for it to exit if it has been launched by using launch_mqi = True on PrologMQI creation.

Does nothing if launch_mqi is False.

Args

kill
False (default) connect to the Machine Query Interface and ask it to perform an orderly shutdown of Prolog and exit the process. True uses the Python subprocess.kill() command which will terminate it immediately. Note that if PrologMQI.connection_failed is set to true (due to a failure that indicates the MQI will not respond), subprocess.kill() will be used regardless of this setting.
class PrologNoQueryError (exception_json)

Raised by PrologThread.cancel_query_async() and PrologThread.query_async_result() if there is no query running and no results to retrieve.

Ancestors

  • PrologError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class PrologQueryCancelledError (exception_json)

Raised by PrologThread.query_async_result() when the query has been cancelled.

Ancestors

  • PrologError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class PrologQueryTimeoutError (exception_json)

Raised when a Prolog query times out when calling PrologThread.query() or PrologThread.query_async() with a timeout.

Ancestors

  • PrologError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class PrologResultNotAvailableError (exception_json)

Raised by PrologThread.query_async_result() when the next result to a query is not yet available.

Ancestors

  • PrologError
  • builtins.Exception
  • builtins.BaseException

Inherited members

class PrologThread (prolog_mqi: PrologMQI)

Initialize a PrologThread instance for running Prolog queries on a single, consistent thread in the Machine Query Interface managed by prolog_mqi (does not create a thread in Python).

Each PrologThread class represents a single, consistent thread in prolog_mqi that can run queries using PrologThread.query() or PrologThread.query_async(). Queries on a single PrologThread will never run concurrently.

However, running queries on more than one PrologThread instance will run concurrent Prolog queries and all the multithreading considerations that that implies.

Usage

All of these are equivalent and automatically start SWI Prolog and the Machine Query Interface:

PrologThread instances can be created and started manually:

mqi = PrologMQI()
prolog_thread = PrologThread(mqi)
prolog_thread.start()
# Your code here

Or (recommended) started automatically using the Python with statement:

with PrologMQI() as mqi:
    with PrologThread(mqi) as prolog_thread:
        # Your code here

Or using the handy helper function:

with PrologMQI() as mqi:
    with mqi.create_thread() as prolog_thread:
        # Your code here

Methods

def cancel_query_async(self)

Attempt to cancel a query started with PrologThread.query_async() in a way that allows further queries to be run on this PrologThread afterwards.

If there is a query running, injects a Prolog throw(cancel_goal) into the query's thread. Does not inject Prolog abort/0 because this would kill the thread and we want to keep the thread alive for future queries. This means it is a "best effort" cancel since the exception can be caught by your Prolog code. cancel_query_async() is guaranteed to either raise an exception (if there is no query or pending results from the last query), or safely attempt to stop the last executed query.

To guaranteed that a query is cancelled, call PrologThread.stop() instead.

It is not necessary to determine the outcome of cancel_query_async() after calling it. Further queries can be immediately run after calling cancel_query_async(). They will be run after the current query stops for whatever reason.

If you do need to determine the outcome or determine when the query stops, call PrologThread.query_async_result(wait_timeout_seconds = 0). Using wait_timeout_seconds = 0 is recommended since the query might have caught the exception or still be running. Calling PrologThread.query_async_result() will return the "natural" result of the goal's execution. The "natural" result depends on the particulars of what the code actually did. The return value could be one of:

  • Raise PrologQueryCancelledError if the goal was running and did not catch the exception. I.e. the goal was successfully cancelled.
  • Raise PrologQueryTimeoutError if the query timed out before getting cancelled.
  • Raise PrologError (i.e. an arbitrary exception) if query hits another exception before it has a chance to be cancelled.
  • A valid answer if the query finished before being cancelled.

Note that you will need to continue calling PrologThread.query_async_result() until you receive None or an exception to be sure the query is finished (see documentation for PrologThread.query_async_result()).

Raises

PrologNoQueryError if there was no query running and no results that haven't been retrieved yet from the last query.

PrologConnectionFailedError if the query thread has unexpectedly exited. The MQI will no longer be listening after this exception.

Returns

True. Note that this does not mean the query was successfully cancelled (see notes above).

def halt_server(self)

Perform an orderly shutdown of the Machine Query Interface and end the Prolog process.

This is called automatically by PrologMQI.stop() and when a PrologMQI instance is used in a Python with statement.

def query(self, value: str, query_timeout_seconds: float = None)

Run a Prolog query and wait to return all results (as if run using Prolog findall/3) or optionally time out.

Calls PrologMQI.start() and PrologThread.start() if either is not already started.

The query is run on the same Prolog thread every time, emulating the Prolog top level. There is no way to cancel the goal using this method, so using a timeout is recommended. To run a cancellable goal, use PrologThread.query_async().

Args

value
A Prolog query to execute as a string, just like you would run on the Prolog top level. e.g. "member(X, [1, 2]), X = 2".
query_timeout_seconds
None uses the query_timeout_seconds set in the prolog_mqi object passed to PrologThread.

Raises

PrologQueryTimeoutError if the query timed out.

PrologError for all other exceptions that occurred when running the query in Prolog.

PrologConnectionFailedError if the query thread has unexpectedly exited. The MQI will no longer be listening after this exception.

Returns

False
The query failed.
True
The query succeeded once with no free variables.
list

The query succeeded once with free variables or more than once with no free variables. There will be an item in the list for every answer. Each item will be:

  • True if there were no free variables
  • A dict if there were free variables. Each key will be the name of a variable, each value will be the JSON representing the term it was unified with.
def query_async(self, value: str, find_all: bool = True, query_timeout_seconds: float = None)

Start a Prolog query and return immediately unless a previous query is still running. In that case, wait until the previous query finishes before returning.

Calls PrologMQI.start() and PrologThread.start() if either is not already started.

Answers are retrieved using PrologThread.query_async_result(). The query can be cancelled by calling PrologThread.cancel_query_async(). The query is run on the same Prolog thread every time, emulating the Prolog top level.

Args

value
A Prolog query to execute as a string, just like you would run on the Prolog top level. e.g. "member(X, [1, 2]), X = 2".
find_all
True (default) will run the query using Prolog's findall/3 to return all answers with one call to PrologThread.query_async_result(). False will return one answer per PrologThread.query_async_result() call.
query_timeout_seconds
None uses the query_timeout_seconds set in the prolog_mqi object passed to PrologThread.

Raises

PrologError if an exception occurs in Prolog when parsing the goal.

PrologConnectionFailedError if the query thread has unexpectedly exited. The MQI will no longer be listening after this exception.

Any exception that happens when running the query is raised when calling PrologThread.query_async_result()

Returns

True

def query_async_result(self, wait_timeout_seconds: float = None)

Get results from a query that was run using PrologThread.query_async().

Used to get results for all cases: if the query terminates normally, is cancelled by PrologThread.cancel_query_async(), or times out. Each call to query_async_result() returns one result and either None or raises an exception when there are no more results. Any raised exception except for PrologResultNotAvailableError indicates there are no more results. If PrologThread.query_async() was run with find_all == False, multiple query_async_result() calls may be required before receiving the final None or raised exception.

Examples

  • If the query succeeds with N answers: query_async_result() calls 1 to N will receive each answer, in order, and query_async_result() call N+1 will return None.
  • If the query fails (i.e. has no answers): query_async_result() call 1 will return False and query_async_result() call 2 will return None`.
  • If the query times out after one answer, query_async_result() call 1 will return the first answer and query_async_result() call 2 will raise PrologQueryTimeoutError.
  • If the query is cancelled after it had a chance to get 3 answers: query_async_result() calls 1 to 3 will receive each answer, in order, and query_async_result() call 4 will raise PrologQueryCancelledError.
  • If the query throws an exception before returning any results, query_async_result() call 1 will raise PrologError.

Note that, after calling PrologThread.cancel_query_async(), calling query_async_result() will return the "natural" result of the goal's execution. See documentation for PrologThread.cancel_query_async() for more information.

Args

wait_timeout_seconds
Wait wait_timeout_seconds seconds for a result, or forever if None. If the wait timeout is exceeded before a result is available, raises PrologResultNotAvailableError.

Raises

PrologNoQueryError if there is no query in progress.

PrologResultNotAvailableError if there is a running query and no results were available in wait_timeout_seconds.

PrologQueryCancelledError if the next answer was the exception caused by PrologThread.cancel_query_async(). Indicates no more answers.

PrologQueryTimeoutError if the query timed out generating the next answer (possibly in a race condition before getting cancelled). Indicates no more answers.

PrologError if the next answer is an arbitrary exception thrown when the query was generating the next answer. This can happen after PrologThread.cancel_query_async() is called if the exception for cancelling the query is caught or the code hits another exception first. Indicates no more answers.

PrologConnectionFailedError if the query thread unexpectedly exited. The MQI will no longer be listening after this exception.

Returns

False
The query failed.
True
The next answer is success with no free variables.
list

The query succeeded once with free variables or more than once with no free variables. There will be an item in the list for every answer. Each item will be:

  • True if there were no free variables
  • A dict if there were free variables. Each key will be the name of a variable, each value will be the JSON representing the term it was unified with.
def start(self)

Connect to the prolog_mqi specified in PrologThread and start a new thread in it. Launch SWI Prolog and start the Machine Query Interface using the mqi/1 predicate if launch_mqi is True on that object. Does not start a Python thread.

Does nothing if the thread is already started.

Raises

PrologLaunchError if launch_mqi is False and the password does not match the server.

Various socket errors if the server is not running or responding.

def stop(self)

Do an orderly stop of the thread running in the Prolog process associated with this object and close the connection to the prolog_mqi specified in PrologThread.

If an asynchronous query is running on that thread, it is halted using Prolog's abort.