The SWI Prolog Language Server is designed to enable embedding SWI Prolog into just about any programming language (Python, Go, C#, etc) in a straightforward way. It is designed for scenarios that need to use SWI Prolog as a local implementation detail of another language. Think of it as running SWI Prolog "like a library". It can support any programming language that can launch processes, read their STDOUT pipe, and send and receive JSON over TCP/IP. A Python 3 library is included as a part of SWI Prolog, see .
Key features of the server:
The server can be used in two different modes:
Note that the language server is related to the pengines library, but where the pengines library is focused on a client/server, multi-tenet, sandboxed environment, the language server is local, single tenet and unconstrained. Thus, when the requirement is to embed Prolog within another programming language "like a library", it can be a good solution for exposing the full power of Prolog with low integration overhead.
A Python 3.x library that integrates Python with SWI Prolog using the language server is included with in the libs
directory of the SWI Prolog installation. It is also available using pip install swiplserver
. See the Python swiplserver library documentation for more information on how to use and install it from either location.
In general, to use the language server with any programming language:
?- language_server([]).
If it can't find it, see below for how to install it.swipl
executable from that installation.If your SWI Prolog version doesn't yet include the language server:
language_server.pl
file from the GitHub repository.language_server.pl
.sudo
as in `sudo swipl -s ...`.swipl -s language_server.pl -g "language_server:install_to_library('language_server.pl')" -t halt
The language server is designed to act like using the "top level" prompt of SWI Prolog itself (i.e. the "?-" prompt). If you've built the Prolog part of your application by loading code, running it and debugging it using the normal SWI Prolog top level, integrating it with your native language should be straightforward: simply run the commands you'd normally run on the top level, but now run them using the query APIs provided by the library built for your target language. Those APIs will allow you to send the exact same text to Prolog and they should execute the same way. Here's an example using the Python swiplserver
library:
% Prolog Top Level ?- member(X, [first, second, third]). X = first ; X = second ; X = third.
# Python using the swiplserver library from swiplserver import PrologServer, PrologThread with PrologServer() as server: with server.create_thread() as prolog_thread: result = prolog_thread.query("member(X, [first, second, third]).") print(result) first second third
While the query functionality of the language server does run on a thread, it will always be the same thread, and, if you use a single connection, it will only allow queries to be run one at a time, just like the top level. Of course, the queries you send can launch threads, just like the top level, so you are not limited to a single threaded application. There are a few differences from the top level, however:
The basic rule to remember is: any predicates designed to interact with or change the default behavior of the top level itself probably won't have any effect.
The most common way to use the language server is to find a library that wraps and exposes it as a native part of another programming language such as the Python swiplserver
library. This section describes how to build one if there isn't yet a library for your language. To do this, you'll need to familiarize yourself with the server protocol as described in the language_server/1 documentation. However, to give an idea of the scope of work required, below is a typical interaction done (invisibly to the user) in the implementation of any programming language library:
swipl --quiet -g language_server -t halt -- --write_connection_values=true
. To work, the swipl
Prolog executable will need to be on the path or specified in the command. This launches the server and writes the chosen port and password to STDOUT. This way of launching invokes the language_server/0 predicate that turns off the int
(i.e. Interrupt/SIGINT) signal to Prolog. This is because some languages (such as Python) use that signal during debugging and it would be otherwise passed to the client Prolog process and switch it into the debugger. See the language_server/0 predicate for more information on other command line options.$ swipl --quiet -g language_server -t halt -- --write_connection_values=true 54501 185786669688147744015809740744888120144
Now the server is started. To create a connection:
<stringByteLength>.\n<stringBytes>.\n
where stringByteLength
includes the .\n
from the string. For example: 7.\nhello.\n
More information on the message format is below.true([[threads(Comm_Thread_ID, Goal_Thread_ID)]])
(which will be in JSON form) indicating successful creation of the connection. Comm_Thread_ID and Goal_Thread_ID are the internal Prolog IDs of the two threads that are used for the connection. They are sent solely for monitoring and debugging purposes.
We can try all of this using the Unix tool netcat
(also available for Windows) to interactively connect to the server. In netcat
hitting enter
sends \n
which is what the message format requires. The server responses are show indented inline.
We'll use the port and password that were sent to STDOUT above:
$ nc 127.0.0.1 54501 41. 185786669688147744015809740744888120144. 173. { "args": [ [ [ { "args": ["language_server1_conn2_comm", "language_server1_conn2_goal" ], "functor":"threads" } ] ] ], "functor":"true" }
Now the connection is established. To run queries and shutdown:
run(atom(a), -1)
to run the synchronous query atom(a)
with no timeout and wait for the response message. It will be true([[]])
(in JSON form).close
, waiting for the response message of true([[]])
(in JSON form), and then closing the socket using the socket API of the language. If the socket is closed (or fails) before the close
message is sent, the default behavior of the server is to exit the SWI Prolog process to avoid leaving the process around. This is to support scenarios where the user is running and halting their language debugger without cleanly exiting.quit
message and waiting for the response message of true([[]])
(in JSON form). This will cause an orderly shutdown and exit of the process.
Continuing with the netcat
session (the quit
message isn't shown since the close
message closes the connection):
18. run(atom(a), -1). 39. {"args": [ [ [] ] ], "functor":"true"} 7. close. 39. {"args": [ [ [] ] ], "functor":"true"}
Note that Unix Domain Sockets can be used instead of a TCP/IP port. How to do this is described in the language server Options documentation.
Here's the same example running in the R language. Note that this is not an example of how to use the language server from R, it just shows the first code a developer would write as they begin to build a nice library to connect R to Prolog using the language server:
# Server run with: swipl language_server.pl --port=40001 --password=123 # R Source print("# Establish connection") sck = make.socket('localhost', 40001) print("# Send password") write.socket(sck, '5.\n') # message length write.socket(sck, '123.\n') # password print(read.socket(sck)) print("# Run query") query = 'run(member(X, [1, 2, 3]), -1).\n' write.socket(sck, paste(nchar(query), '.\n', sep='')) # message length write.socket(sck, query) # query print(read.socket(sck)) print("# Close session") close.socket(sck)
And here's the output:
[1] "# Establish connection" [1] "# Send password" [1] "172.\n{\n "args": [\n [\n [\n\t{\n\t "args": ["language_server1_conn1_comm", "language_server1_conn1_goal" ],\n\t "functor":"threads"\n\t}\n ]\n ]\n ],\n "functor":"true"\n}" [1] "# Run query" [1] "188.\n{\n "args": [\n [\n [ {"args": ["X", 1 ], "functor":"="} ],\n [ {"args": ["X", 2 ], "functor":"="} ],\n [ {"args": ["X", 3 ], "functor":"="} ]\n ]\n ],\n "functor":"true"\n}" [1] "# Close session"
Other notes about creating a new library to communicate with the language server:
When using the language server from another language, debugging the Prolog code itself can often be done by viewing traces from the Prolog native writeln/1 or debug/3 predicates. Their output will be shown in the debugger of the native language used. Sometimes an issue surfaces deep in an application and a way to run the application in the native language while setting breakpoints and viewing traces in Prolog itself is the best approach. Standalone mode is designed for this scenario.
As the language server is a multithreaded application, 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. A typical flow for Standalone Mode is:
tdebug, language_server([port(4242), password(debugnow)])
.In Python this would look like:
% From the SWI Prolog top level ?- tdebug, language_server([port(4242), password(debugnow)]). % The graphical front-end will be used for subsequent tracing true.
# Python using the swiplserver library from swiplserver import PrologServer, PrologThread with PrologServer(4242, "debugnow") as server: with server.create_thread() as prolog_thread: # Your code to be debugged here
At this point, all of the multi-threaded debugging tools in SWI Prolog are available for debugging the problem. If the issue is an 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 server standalone doesn't clear state between launches of the application. You'll either need to relaunch between runs or build your application so that it does the initialization at startup.