Using `format/2` with attributed variable on a pengine

It runs on a local SWISH installation :slight_smile: I has a first read through the code. These are some assorted comments

  • in ia_primitives.pl we find a rather misleading definition of non_empty/1 and non_empty/2. The second looks like a mis-typed version of the first, but it probably is not because it is not called due to the goal_expansion/2 rule above. There should not be a global goal expansion/2 rule for (I think) a local predicate. As is, that will upset any code that happens to use non_empty/2. If it is only local, simply define the goal expansion rule in the module (remove the system:). Expansion follows a pipeline: first expand in the module being compiles, then in user and finally in system. The last should normally not be used by application code.

  • The SWISH toplevel writes constraints for internal variables. I guess you can get rid of these using the SWISH post hook and remove the attributes from the variables that are not part of the bindings. Alternatively, leave them there, but extend the attribute on the variables in the bindings with an annotation that says they are part of the bindings.

  • simplesolveall_/2 doesnā€™t seem to need the meta-calling route. There are only three predicates choice_generator_/6 can create which you then call. Seems choice_generator_/6 can simply make the call, no?

  • Intercepting the tracer is something that should not be done by user libraries. Managing multiple interceptors is poorly defined and both the GUI debugger and SWISH use this hook (and cooperate poorly). It only seems to be used to wrap doNode_/7. That should be easy enough to wrap using normal Prolog. To achieve a zero-cost dynamic wrap you can use wrap_predicate/4. Doesnā€™t work very nice in the multi-threaded SWISH environment though.

  • The {} definition is a no-op. This is a different predicate from {...} and not exported. Note that empty {} is simply undefined in most other subsystems using it.

  • Iā€™m a bit unsure about the usage of debug/1 and debug/3 in the code. Should this type of library debug (trace?) code use the generic debug facilities or their own? I think there is something to be said for both.

  • What is the reason for set_min_free/1?

  • clpStatistics/0: cut on last call is a no-op. It is not clear why you want to redo the work wrt. Prologā€™s time and GC statistics anyway. Why not call call_time/2. Possibly time/1 should also print GC time?

I think that is enough for round one :slight_smile: It is a nice library to have, especially if we can make it blend into the system cleanly such that it can safely and transparently used in larger projects.

Thanks for testing on a local SWISH and for taking the time to review the code. Comments below:

There are two versions: non_empty/1 and non_empty/2; both are local to the module, and both are used (subject to goal expansion). So the basic idea was to test whether goal expansion was available on the system (I didnā€™t want to make the module dependent on that feature) using:
current_predicate(system:expand_goal/2). If so perform a goal expansion on non_empty/2, otherwise use non-empty/2 as defined. I didnā€™t think I was doing any ā€œglobal expansionā€ and this is only applied to the module. Why is this not the case?

If I understand you correctly, I currently do this for the normal top level using the user:expand_query and user:expand_answer that work with attribute_goals//1. It sounds like thereā€™s something similar for SWISH (post hook?) that can be used to do this in a SWISH environment. Any doc or examples for this?

I donā€™t think so. choice_generator_/6 tests if the interval can be sub-divided. If it fails, the second clause terminates the ā€œsearchā€. If it succeeds, a cut removes the second clause, but that cut must precede the meta call to Choices. So that call canā€™t be moved into choice_generator_/6.

Didnā€™t know about wrap_predicate/4 (it isnā€™t in the ā€œSummaryā€ list of predicates; prolog_trace_interception/4 is). But Iā€™ll put it on my TODO list if it can achieve a ā€œzero-cost dynamic wrapā€.

Not sure why thatā€™s there - probably an historical artifact and should be removed.

If youā€™re referring to the box model debugger, I found that was far too fine grained to be generally useful. Debug topics which enable additional messages on significant events, including interval watchpoints, and selectively enable noisy failures seem to work better for me and, I would think, most users who arenā€™t interested in the fine grained detail. I think it looks similar to the usage of debug topics in the sandbox module. But maybe Iā€™m missing your point.

The whole issue of how to debug CLP programs is still a bit of an open question to me.

To be honest, Iā€™m not quite sure anymore. I think the original strategy was to ensure, after garbage collection, there was sufficient stack space to run the fixed point iterator long enough to complete before hitting another gc. No guarantees of course, but seemed to be a useful heuristic and the number used was small enough that it was unlikely to have a significant global impact. Any recommendations?

I believe the cut was originally present because the clpStatistics/0 predicate is discontiguous so it leaves an unwanted choicepoint; the cut was on the last clause to remove it. Iā€™ve just tested it without the cut and it seems this is no longer the case (if it ever was :slightly_smiling_face:).

I did want a single ā€œstatisticsā€ predicate that combined a few relevant systems numbers (time, inferences, stack usage, ā€¦) with clpBNR numbers (primitive ops, primitive failures, constraint network size). If I had found a ā€œhookā€ to add my own operational measurements to the systems, I might have done it differently.

I see. I misread, assuming that if you test for it in system you also define the system version. Yes, it is ok. I donā€™t think Iā€™d go into all that trouble and instead would simply add a goal_expansion/2 rule. Systems that do not support it will just ignore it.

So, it works differently. Inside the Pengine thread there is a call to swish_trace:post_context/1 that is normally not defined. The attribute_goals//1 hook is called in the HTTP thread that communicates between the Pengine and the HTTP client. The post_context hook receives a dict with quite a bit of context, among which the Bindings, a list of Name=Term for the answer. The hook could combine this answer to rewrite the attributes such that attribute_goals//1 in the HTTP thread does the right thing. Note that you can safely modify the attributes because the modifications are subject to backtracking anyway, so all modifications are undone in case the user asks for another solution.

To play, Iā€™d make a local installation. That is quite simple:

 git clone https://github.com/SWI-Prolog/swish.git
 cd swish
 make yarn-zip
 swipl run.pl

That does not include clpBNR. I just added the config-available for that, so

 mkdir config-enabled
 cd config-enabled
 ln -s ../config-available/clpBNR.pl

(and restart swipl run.pl)

You are probably right. You can easily remove the meta call though by having a new predicate with three clauses. that calls the right continuation. That is probably as fast and avoids the need for sandbox declarations.

Great. As is, I donā€™t feel comfortable with this on a public server.

No. I am wondering whether a library that presents some form of debug messages to its user that are not there to debug the library, but to debug user code should use debug/1 and debug/3. Alternatively it could use its own debug statements and conditions. But, surely one could also argue this is the right way to go. Iā€™ll have a look at handling debug messages from libraries in SWISH. There is not only the threading issue, but also whether or not this would allow to get access to information that should remain hidden. For example, library(pengines) uses debug messages, some of which reveal data about other Pengines.

Remove it :slight_smile: If it leads to significant performance degradation there is something wrong with the garbage collector scheduling that should be fixed elsewhere. Such problems did exist, but several have been resolved.

Nope. There is never an open choicepoint for a last call. There used to be one in old systems for dynamic predicates, but since we have the logical update view, there is not. Discontiguous would not be a good reason anyway. Multifile could be, as one could load a new file, but for SWI-Prolog. mulfile predicates also use the logical update view (in fact, all predicates do that, including static predciates).

I managed to follow this and get a local copy running - I guess I
can handle copy/paste even when I donā€™t know what Iā€™m doing. My problem seems to be how to define the hook. I tried:

:- use_module(library(swish_trace, [swish_trace:post_context/1])).

swish_trace:post_context(Dict) :-  %%% for swish testing
    Body...

but got an error:

% Updating GIT version stamps in the background.
ERROR: /Users/rworkman/.local/share/swi-prolog/pack/clpBNR/prolog/clpBNR/ia_utilities.pl:86:
ERROR:    source_sink `library(swish_trace,[swish_trace:post_context/1])' does not exist
Warning: /Users/rworkman/.local/share/swi-prolog/pack/clpBNR/prolog/clpBNR/ia_utilities.pl:86:
Warning:    Goal (directive) failed: clpBNR:use_module(library(swish_trace,[swish_trace:post_context/1]))

and if I donā€™t try to use it, it looks like the hook is 'abolished` later in the server startup:

Warning: /Volumes/Mac Pro HD/Developer/swish-devel/swish/lib/trace.pl:36:
Warning:    Loading module swish_trace abolished: [swish_trace:post_context/1]

Remedy ??

I like the wrapper solution more, but I have the impression itā€™s not safe because wrappers are not localized to the thread. I believe debug state is localized to a thread so in a sense, it would seem the debugger solution is safer. Itā€™s a bit moot since the tracing feature itself is not permitted anyway.

You cannot import a non-exported predicate. You also do not want to import it, you want to define (some) rules for it. You do this (from any module) using

:- multifle swish_trace:post_context/1.

swish_trace:post_context(Dict) :-
   ...

No need for a use_module/1 of the library. The above creates a rule in the module. When the library is loaded it notices the predicate is multifile and it leaves the externally provided clauses alone. According to the ISO rules, any file using or defining clauses for a multifile predicate must declare it. ISO doesnā€™t talk about modules :frowning: . Same, b.t.w. holds for your imports of library(sandbox). There is no need to do that. As in this case you do not explicitly import the multifile rules, nothing goes wrong. Not importing minimizes the footprint when your library is not used with Pengines.

That is true. Considering the complexity of the thing you want to wrap, Iā€™d simply define a wrapper at the Prolog source level that is conditional on whether or not you want to trace the clpBNR execution. I doubt youā€™ll be able to measure the additional overhead. If that is an issue you can still use conditional compilation to allow for debugging or not. Unlike old versions, tail calls that pass all arguments are cheap these days, regardless of the number of arguments. Effectively, p(A,B,C.,,,,) :- q(A,B,C,...) is little more than a jump.

Thanks for the reminder - I should have known based on the added definitions of prolog:message//1. So Iā€™ll clean all that up in the next release.

In any case, all the machinery now appears to be working, but fixing my problem is a little trickier than I thought. I need an additional piece of information that canā€™t be easily passed in the attributes between the post_context call and the attribute_goals call. This is a bit difficult to implement when they execute in different threads. Basically, I need to distinguish between the case when answers are being produced (abbreviated version) and the general case required by a call to copy_term (ā€œkitchen sinkā€ version). So more thought required.

1 Like

Got stuck so opened a new topic: