Prolog events

I have pushed a number of commits to arrive at a generic mechanism to handle kernel events that applications may be interested in. Before that, most was handled using an undocumented hook user:prolog_event_hook/1 that was implemented for several modules around the (graphical) debugger. The debugger needs to know about reclaimed clauses as it needs to update its caches that keep information about clauses. It also needs to know about breakpoints being set and possibly being cleared because the code is removed and Prolog environment frames that are finished as it maintains a view of these.

The new mechanism is implemented by prolog_listen/2,3 and friends. In addition to the old stuff it allows for tracking changes to (dynamic) predicates. If you build the current git source you can run

?- help(prolog_listen).

to get an idea of the API. The plan is to use this API for more hooks in the future. Nothing is fixed yet, so remarks to making it better, more compatible, etc. are welcome.

3 Likes

Any reason to have:

meta_predicate prolog_listen(+, :)
meta_predicate prolog_listen(+, :, +)

Instead of the much more informative and accurate:

meta_predicate prolog_listen(+, 2)
meta_predicate prolog_listen(+, 2, +)

Or am I missing something in my quick glance to the documentation?

The annoying reason is that the number of additional arguments depends in the channel (first argument). Some take none (e.g., abort). The only somewhat sensible solution I see is to have always exactly one argument and make that a term. But then, it is used for thread_at_exit/1 and the at_exit option of threads and these are used quite a bit and that event passes no arguments :frowning:

From an API point of view I think the current approach makes sense, but from a uniform code analysis point of view not :frowning: If you have a suggestion that satisfies compatibility and analysis requirements I’m interested!

Maybe pass those additional arguments as a (possibly empty) list of options? E.g.

:- dynamic p/1.
:- prolog_listen(p/1, updated(p/1)).
            
updated(Pred, [action(Action), context(Context)]) :-
    format('Updated ~p: ~p ~p~n', [Pred, Action, Context]).

That still breaks the compatibility with old hooks that pass no context arguments for abort (not so important as it is rarely used), thread exit (is frequently used). It is in this sense the same as my suggestion to pass a term as the always present one and only context. I also want to keep this as simple as possible as some of these hook may be used in rather time critical places.

Not sure I follow. For abort or thread_exit we would use e.g.

:- prolog_listen(abort, called(abort)).
:- prolog_listen(thread_exit, called(thread_exit)).

called(abort, _) :-    % an empty list would be passed
    ...
called(thread_exit, [thread(Thread)]) :-
    ...

In all cases, the closure argument would take two arguments, the channal (typo in documentation? Shouldn’t it be channel?) and a list of context arguments.

As is, we can do

?- thread_create(run, _, [at_exit(done)]).

done :-
   ...

This uses the same mechanism now as the option list argument is simple passed to the internal equivalent of prolog_listen(this_thread_exit, done). There is no context and I do not want to break compatibility. The various channels (thanks; I’ll fix the spelling) simply require zero or more context arguments. As they are handled by call/N, you can pass as many additional context as you want.

One alternative I see is that you write e.g.

?- prolog_listen(p/1, updated(_Action, _ClauseRef)).
?- prolog_listen(p/1, updated(p/1, _Action, _ClauseRef)).

Now the rule would be that the last N arguments of the term you pass are filled with the context arguments. In this way the declaration can be prolog_listen(+,0).

It requires some extra work during the registration (verifying the last N arguments are variables and remove them), but does not need to have any impact on the execution.