A library(prolog_xref) limitation or bug?

Hi,

As you may be aware, Logtak’s diagrams tool can generate diagrams for Prolog module libraries and applications (I have a couple of examples online at Tools - Logtalk).

When running on SWI-Prolog, it uses the library(prolog_xref) for most of the required information on the application modules and predicates. In order to illustrate the issue, consider as an example Anne’s Talespin application:

As typical in SWI-Prolog applications, some module dependencies are explicitly declared while others are omitted with the autoloading mechanism filing the gaps. Take for example this application planner module with implicit dependencies on the apply and lists modules. Let’s generate diagrams for the application plus a predicate cross-diagram for the planner module:

$ swilgt
...

% load the application
?- [run].
true.

% load Logtalk's diagrams tool
?- {diagrams(loader)}.
...
true.

% generate file loading, file dependency, module xref, ... diagrams
?- diagrams::directory('.').
true.

% generate predicate xref diagram for the planner module
?- xref_diagram::entity(planner).
true.

Converting the generated .dot file for the planner module to .svg:

$ dot -Tsvg planner_module_xref_diagram.dot > planner_module_xref_diagram.svg

you will noticed that predicates such as member/2 or reverse/2 are displayed in the diagram as local predicates. I.e. SWI-Prolog runtime knows that these predicates come from the lists module but library(prolog_xref) doesn’t seem to return that information. The diagrams tool is making the following calls (enumerating callees; here I’m just instantiating the callee argument to member(_,_) to illustrate the issue):

?- module_property(planner, file(File)), xref_source(File), xref_called(File, member(_,_), Caller).

File = '/Users/pmoura/talespin-annie-master/planner.pl',

Caller = plan(_13422, _13424, _13426, _13428).

?- module_property(planner, file(File)), xref_source(File), xref_called(File, member(_,_), Caller), xref_defined(File, member(_,_), imported(FromFile)).

false.

However, if I modify the planner.pl file to include the missing directive and reload the application:

:- use_module(library(lists)).

I will now get:

?- module_property(planner, file(File)), xref_source(File), xref_called(File, member(_,_), Caller), xref_defined(File, member(_,_), imported(FromFile)).

File = '/Users/pmoura/talespin-annie-master/planner.pl',

Caller = plan(_26232, _26234, _26236, _26238),

FromFile = '/Users/pmoura/lib/swipl/library/lists.pl'.

which allows the xref diagram (and other diagrams to) correctly show the module dependencies as intended.

This issue, which is not clear to me if it is a library(prolog_xref) limitation or bug (or something that I’m missing in its usage!), defeats one of the main purpose of generating the diagrams in the first place: to provide a comprehensive view of an application at file, entity, and predicate levels.

Feedback most appreciated.

Thanks,
Paulo

1 Like

Follow up. Found (and committed) a workaround, based on predicate_property/2 property autoload/1, that allows the tool to explicitly load the files providing the implicit dependencies and thus resolve the predicate calls to the module providing the predicates. This works nicely for all the generated diagrams. Still, I would very much prefer a solution based on a fixed or improved library(prolog_xref).

Not really sure. library(prolog_xref) is not supposed to load anything. It could claim otherwise undefined predicates to be defined as autoload(from). I don’t know whether that makes things much better. As is, it has a clear responsibility to read the file and the header of the dependencies and decide what is called from where.

I assume that would translate to xref_defined(File, Predicate, autoload(FromFile)) being true for auto-loaded predicates. I think is better than the xref_defined/3 goals simply failing for those predicates and thus a worthwhile change/improvement.

Btw, the workaround I’m actually using is:

predicate_property(Callee0, autoload(FromFile)),
absolute_file_name(FromFile, FromFilePath, [file_type(prolog)]),
catch(load_files([FromFilePath]), _, fail),
once(module_property(FromModule, file(FromFilePath))) ->
functor(Callee0, CalleeFunctor, CalleeArity),
Callee = ':'(FromModule,CalleeFunctor/CalleeArity)

with the change you suggest, the call to predicate_property/2 would be replaced by a call to xref_defined/3.

Looking at the code and comment for xref_defined/3, I think this is a bad idea. It also doesn’t report builtin predicates. The comment is a bit ambiguous as it says (1) “Test if Goal is accessible in Source” and (2) “Note that this predicate does not deal with built-in or global predicates, just locally defined and imported ones”.

I think figuring out what is defined in a file is what a cross-referencer is about. What is callable (accessible) is what is defined + what is accessible anywhere. You can use the predicate property visible for that, or the others if you want to know why it is visible.

Note that the catch should not be needed. This can only fail if the installation is broken. Also no need fpr absolute_file_name/3, just load the file reported by predicate_property. Finally, use

predicate_property(Callee0, autoload(FromFile)),
load_files(FromFile, [imports([])])

do avoid importing possibly conflicting stuff into the calling context. Using use_module/2 should also be fine as the autoloader only supports modules. If you do this just to find out the module, you can also use

xref_public_list(FromFile, -, [module(Module)])

The xref_public_list/3 can be used to read the declarations in a file and report the module and other meta data about the file.

I would say that auto-loaded predicates are implicitly imported into the module calling them.

A common use of a cross-referencer is not only to know what is defined in an entity (file, module, …) but also what is called from that entity. In this case, we have xref_called/3 to know what is being called. But having xref_defined/3 failing for called auto-loaded predicates means we need extra steps to find if the called predicate is auto-loaded (or built-in or unknown. I don’t see any downside on having xref_defined/3 succeeding with xref_defined(File, Predicate, autoload(FromFile)).

Noted.

The call to absolute_file_name/3 is required in my workaround in order for the call to module_property/2 to succeed as predicate_property/2 returns a file without extension but the file/1 property returned by module_property/2 have an extension.

That’s a better workaround. Thanks!

First I had just some gut feeling that this isn’t right, but I think there is also a more practical reason: this would also require adding xref_defined(File, Predicate, built_in) or something like that. Both are all fine in mode (+,+,-), but results in a large number of not-so-interesting answers if Predicate is unbound. This mode is used to answer “what is defined but not called?”