Chromatic diagnostics

Hey all,
Last weekend I’ve written two little packages that may be useful for others in the audience:

diagnostics.pl and flymake-swi-prolog.el

diagnostics.pl is a SWI-Prolog packge providing a simple and
extensible interface for linting Prolog source code and emitting
diagnostic messages. It builds upon the comprehensive analysis
performed by the built-in prolog_colourise_stream/3 and
xref_source/1 to diagnose issues in a given source file.

This package is intended to further expose the detailed diagnosis that is available
to the user when working with the SWI-Prolog built-in IDE to be
consumed by different targets for different needs.

The default diagnosis provided can be extended and customized by defining the
multifile predicate diagnostics:diagnosis_hook/3 (refer to the documentation
for the specifics). Moreover, diagnostics are reported via
print_message/2 and can thus be intercepted and handled in arbitrary
manners via the built-in message_hook/3.

The two main use-cases of diagnostics.pl are:

Linting code in CI

diagnostics.pl can be executed from the command line to diagnose and
report on issues during CI. Let’s say we have the following code in a file called /tmp/spam.pl:

:- module(foo, [bar/2]).

bar(A, A) :-
    baz(A).

bar(_).

Note the typo in the body of bar/2 - we want to call bar/1 but we
accidentally typed baz instead of bar.
We can run diagnostics.pl to catch such issues by running:

➜ swipl -q -g "use_module(library(diagnostics))" -t halt -- /tmp/spam.pl
ERROR: /tmp/spam.pl:3:
ERROR:    /tmp/spam.pl:43:3: Undefined predicate baz/1
Warning: /tmp/spam.pl:6:
Warning:    /tmp/spam.pl:52:3: Unreferenced definition for bar/1

For an example of incorporating such a step in CI, check out the CI process of
ropes.pl
which
uses diagnostics.pl to check for errors whenever a new commit is
pushed to the package’s repository.
One can run swipl with the --on-error=status and --on-warning=status command line options set to have the process to fail (potentially failing the CI run) when diagnostics are
reported.

Diagnosing issues within an IDE during development

The other major use case for diagnostics.pl is allowing different user
interfaces to consume and display diagnostics on the fly in a way that
mirrors the SWI-Prolog built-in IDE, which brings us to the other
package mentioned above - flymake-swi-prolog.el.
flymake-swi-prolog is an Emacs Lisp package I’ve written along with
diagnostics.pl to integrate the Prolog diagnostic messages with
Flymake, the built-in diagnostics infrastructure in GNU Emacs.

Here’s what the aforementioned erroneous code looks like with flymake-swi-prolog enabled:
Screen Shot 2022-04-04 at 16.21.19

And here’s how it looks immediately after fixing the typo:
Screen Shot 2022-04-04 at 16.22.13

flymake-swi-prolog is available from the Emacs package archive
MELPA, so installation is trivial, check out the project’s README for more
information.

Important note: on-the-fly diagnostics for IDEs can also
be obtained from an LSP server, and some are indeed provided by
lsp_serverdiagnostics.pl does not aim to compete with such servers, instead, it aims to be a
layer that the LSP server and other programs can build upon.

Cheers

2 Likes

Nice! We might want to compare and merge this with library(check), which does something very similar and is used by make/0. Hopefully the set of diagnostics is complementary :slight_smile: One difference is that check works on the loaded program. The general way to make it run on code is

swipl -g check -t halt --on-error=status --on-warning=status -l file.pl

That would be great :slight_smile:
I found that indeed they somewhat complement each other, while there is also some overlap.
For instance, the approach taken in diagnostics.pl does not report on “discontiguous” predicate definitions, while check/0 does not report on e.g. undefined predicate options:

➜  ~ cat /tmp/spam.pl
:- module(foo, [bar/2]).

bar(A, A) :-
    bar(A), print_term(A, [foo(bar)]).

bar(_).

bar(_, _).

➜  ~ swipl -q -g check -t halt -l /tmp/spam.pl
Warning: /tmp/spam.pl:8:
Warning:    Clauses of foo:bar/2 are not together in the source-file
Warning:    Earlier definition at /tmp/spam.pl:3
Warning:    Current predicate: foo:bar/1
Warning:    Use :- discontiguous foo:bar/2. to suppress this message

➜  ~ swipl -q -g "use_module(library(diagnostics))" -t halt -- /tmp/spam.pl
Warning: /tmp/spam.pl:3:
Warning:    /tmp/spam.pl:66:3: No such option

Also, note that diagnostics.pl reports precise source spans (char-offset and length) for each diagnostic, which is crucial for facilitating the in-IDE use case (as done with the built-in IDE)

Note that the discontiguous warning comes from the compiler, not check/0. It should be possible to use mostly the same analysis code because at some point they both deal with a clause (-term) and its position information. Only, your library read this from the file and library(check) decompiles and uses clause_info/4 to get the layout info. The latter is not always guaranteed to work. The advantage of library(check) is that it also finds issues with generated code. But, it needs to compile and you may not want to do that continuously to support the IDE.