Some SWI Prolog development tips

λ June 10, 2020
Tags: swi, prolog

I thought I would just share how I do my daily development with SWI Prolog at the moment. I am following the guidelines on project structure and to that end I have a file that contains the code that sets up my REPL environment and it’s now very useful and feels like an old pair of worn comfy slippers.

Extending the file search paths

Once you get to a certain size of project you’ll probably want to create a ./src folder to better arrange your files. For reasons I won’t explain here, SWI Prolog looks for test files in the same folder as the source file, so for example the file has its tests in the file foo.plt. T for test I guess.

This is the first time I have done this so my solution may not be the most elegant but it works. First we need to inform the compiler that the predicate user:file_search_path is one that is allowed to be defined across multiple files

:- multifile user:file_search_path/2.

:- prolog_load_context(directory, Dir),
    asserta(user:file_search_path(myapp, Dir)),
    format(atom(S), '~s/src', [Dir]),
    asserta(user:file_search_path(myapp, S)),
    format(atom(A), '~s/src/ast', [Dir]),
    asserta(user:file_search_path(myapp, A)),
    format(atom(T), '~s/src/tokeniser', [Dir]),
    asserta(user:file_search_path(myapp, T)),
    format(atom(C), '~s/src/coder', [Dir]),
    asserta(user:file_search_path(myapp, C)).

Note that :- is basically a “compile time” expression of a predicate. Code that runs when the file is loaded not when the actual application gets to run later. What it does as associate the alias myapp with these folder relative to the project root:


Firing it off

And next, we have these lines:-

:- initialization(setup).

setup :-

openfiles :-

Once a file has been loaded and compiled, if there is a registered initialisation goal (setup, in this case) then it is called. What I do here is ask that all test files be loaded, set the current REPL module to be the user module and finally open all my most used source files.

Notice that the typical format is:

:- use_module(myapp(foo)).

and this will then search all the paths registered above looking for a source file defines the module foo, typically the file, but not necessarily so and the first line of that file must be:

:- module(foo, [ ...export list ]).

(Looks like Erlang doesn’t it…I wonder why?! :O )

The next thing encountered is more compile time directives that cause the required modules to be loaded into the engine. Because this happens at compile time and the previous setup runs at run time, this means that the call to load_test_files([]) also loads in any file it can find with the same name as a loaded source file but that has the file extension .plt. This is very slick because there is a command, make that automatically reloads any files changed on disk and then re-runs any unit tests affected by those changed files.

In the built-in editor, there is a shortcut to run make and this leads to a very nice way of working. You open a normal terminal, start a session, open the files, then as you plough your way through the day all you do is:

    Ctrl-X s   % saves the current file
    Ctrl-c m   % pokes the REPL to reload and run tests

And immediately you can see if any tests failed. It’s really nice actually!

REPL Helpers

Once yo get into the swing of it you start to realise that you can use Prolog to extend the capabilities of the REPL both transparently and easily. The following are some things that I now find I can’t live without on the project that this code is from (a mother of a transpiler).

Clear Screen

So simple but very useful at times especially after running an AST run and wanting the see it all on the page.

cls :- format('\033[2J\033[H').

All it does is send standard ANSII escape sequences to cause the terminal to clear and “home” the cursor.

Abbreviated stock commands.

Call me lazy but typing help and apropos gets tiring after a while so I just shortened them!

a(X):- apropos(X).
h(X):- help(X).

Finding stuff in files

This one just shells a find command. Sure it’s easy to drop out with Ctrl-z, run any command and then type fg but this way I don’t have to be bothered.

find(X) :-
    format(string(C),'find . -name "*.pl" -exec grep -iHn "~s" {} \\;',[X]),

TODO listing

I absolutely litter files with %TODO: notices any time something pops into my head, even if the thought and the current file are in no way related. I just type it out and forget it. Then later on I can issue the todo command and see what needs doing next.

    shell('find . -name "*.pl" -exec grep -iHn "todo" {} \\;'),
    shell('find . -name "*.plt" -exec grep -iHn "todo" {} \\;').

edit/1 is your friend.

It’s taken me a while but I now know that once your source files are loaded, you can forget them! If you want to edit a particular piece of code, say a predicate called frobulator then you don’t need to know the file it lives in because the REPL already does, all you do is pass the name of the predicate,

?- edit(frobulator).

If there is only one file then it just opens in the editor and Voila, off you go. If however it was a multlifile predicate, or there is more than one file that matches, you might see something like this:

?- edit(ast).
Please select item to edit:

  1 <loaded file>               'src/ast/':1
  2 ast:ast/2                   'src/ast/':70
  3 ast:ast/3                   'src/ast/':74
  4 <loaded file>               'src/ast/ast.plt'

Your choice? 

All you do is press the number on the left and again, the relevant file just appears in the editor on the correct line. Note that 2 and 3 above would be the two implementations of that predicate.

I will definitely be writing a lot more about Prolog on the coming months as I think that for a language first launched in 1972 it has some mind bendingly powerful features that should be shared!