Debugging aggregations in place

Hi Jan,

Most of my programs contain many instances of aggregation (eg aggregate_all, findall, etc) .
When I debug them using gtrace, I would like to see the aggregate’s contained goal and visually step through it just like any other multi-step predicate call. E.g. given the following,

findall(X,(
    a(X),
    b(X),
    c(X) ),
    Xs),

I would like to step through a, then b, then c, seeing the variable bindings after each step and without stepping into the innards of call/1 for every call. I am guessing there is no built-in way to do this at present. My question is, would this be hard to implement? To me, it would be so useful that I am contemplating trying to add it to gtrace myself.

Thanks!
Carl

1 Like

If you type “s” (for “skip”), doesn’t that do what you want?

Also, you might want to combine your multiple goals into a single predicate, e.g.

abc(X) :- a(X), b(X), c(X).

This lets you put a spypoint on abc/1 to distinguish that call of a/1 from other calls.

Thanks Peter. If I step into the aggregation, I am confronted with the inner workings of the aggregation operator. This eventually involves a call to call(Goal), where Goal in my example would be (a(X),b(X),c(X)) . One can use the usual commands to step into/skip through a, b, and c.
But the problem here is that the debugger constantly shifts from predicate to predicate and backtracks, and it’s hard to follow.
By contrast, your suggestion to make a predicate abc() would definitely get me the nice simple debugging experience that I want, because the debugger would stay in predicate abc throughout its execution as one steps over a, b, and c. But it’s a lot of trouble to take every Goal and create a predicate for it. That’s why I wish this kind of experience was built into the debugger for aggregation predicates.

Hmmm … I’m not much of a debugger user; I prefer putting in “print” statements. :wink:

Anyway, you might try putting a spypoint on a/1 (using either spy/1 or trace/1 or trace/2), and when things get to the inner parts of the aggregation, do a “leap”?

Or maybe add :- set_prolog_flag(generate_debug_info, false). to library/aggregate.pl ?

Hehe, funny, I do this all the time too :slight_smile:

This is a rather complicated topic. Yes, I agree the gtrace/0 experience on complex meta goals is poor. There are a number of issues and (partial solutions).

  • First of all, the debugger stops at calls from user code to system code and from user code to user code. As both the aggregate predicate and builtins are system code, we cannot trace in e.g.

    findall(X, between(1,5,X), Xs).
    
  • Second, complex meta-goals, i.e., meta goals that include a control structure like ,/2, ;/2, etc. are compiled to an temporary clause allocated on the stack before execution. The usual stuff for relating clauses to source locations does not work for this. This is based on the file and line stored with the clause, re-reading the clause’s source code with complete layout details nd redoing the compilation steps to figure out the relation between VM code and source layout as well as the relation between variable names and where they are allocated.

What to do? One improvement is to use the Prolog flag compile_meta_arguments to control. This does what @peter.ludemann suggests: create an intermediate predicate. This predicate doesn’t have source code information and thus you will be tracing through decompiled code. For example:

:- set_prolog_flag(compile_meta_arguments, control).

d2(Xs) :- findall(X, (p(X), q(X)), Xs).

p(X) :- between(1, 5, X).
q(X) :- between(3, 7, X).

Creates:

103 ?- listing(d2).
d2(A) :-
    findall(B,
            '__aux_meta_call_5332574478a2482db49c3383c71a995fd352c0b7'(B),
            A).

true.

104 ?- listing('__aux_meta_call_5332574478a2482db49c3383c71a995fd352c0b7').
'__aux_meta_call_5332574478a2482db49c3383c71a995fd352c0b7'(A) :-
    p(A),
    q(A).

Note this flag was once used to simplify quick cross-referencing. Since a long time list_undefined/0 works differently and this flag was no longer needed. It was also broken. Fixed and updated the docs.

I don’t know what the best way would be to get the user experience one would assume. Possibly it is to deal with the above translation. Possibly it is to deal with the intermediate clause.

2 Likes

@jan - wouldn’t it make sense to turn off generate_debug_info in library(aggregate), so that tracing behaves more like the builtin setof/bagof/findall?

Compare these:

?- trace, setof(X, a(X), Xs).
   Call: (11) setof(_848, a(_848), _856) ? creep
   Call: (17) a(_848) ? creep
   Exit: (17) a(1) ? creep
   Redo: (17) a(_848) ? creep
   Exit: (17) a(2) ? creep
   Redo: (17) a(_848) ? creep
   Exit: (17) a(3) ? creep
   Exit: (11) setof(_848, user:a(_848), [1, 2, 3]) ? creep
Xs = [1, 2, 3].

?- trace, aggregate(set(X), a(X), Xs).
   Call: (11) aggregate:aggregate(set(_3064), a(_3064), _3076) ? creep
   Call: (12) aggregate:template_to_pattern(bag, set(_3064), _3710, user:a(_3064), _3712, _3714) ? creep
   Call: (13) aggregate:template_to_pattern(set(_3064), _3710, _3762, _3764, _3714) ? creep
   Call: (14) aggregate:templ_to_pattern(set(_3064), _3710, _3762, _3764, _3714) ? creep
   Exit: (14) aggregate:templ_to_pattern(set(_3064), _3064, true, [], set) ? creep
   Exit: (13) aggregate:template_to_pattern(set(_3064), _3064, true, [], set) ? creep
   Call: (13) aggregate:existential_vars(user:a(_3064), _3950, _3952, []) ? creep
   Call: (14) var(user:a(_3064)) ? creep
   Fail: (14) var(user:a(_3064)) ? creep
   Redo: (13) aggregate:existential_vars(user:a(_3064), _3950, _3952, []) ? creep
   Call: (14) _4144=_3952 ? creep
   Exit: (14) _3952=_3952 ? creep
   Call: (14) aggregate:existential_vars(a(_3064), _4142, _3952, []) ? creep
   Call: (15) var(a(_3064)) ? creep
   Fail: (15) var(a(_3064)) ? creep
   Redo: (14) aggregate:existential_vars(a(_3064), _4142, _3952, []) ? creep
   Exit: (14) aggregate:existential_vars(a(_3064), a(_3064), [], []) ? creep
   Exit: (13) aggregate:existential_vars(user:a(_3064), user:a(_3064), [], []) ? creep
   Call: (13) aggregate:clean_body((user:a(_3064), true), _4520) ? creep
   Call: (14) aggregate:clean_body(user:a(_3064), _4568) ? creep
   Exit: (14) aggregate:clean_body(user:a(_3064), user:a(_3064)) ? creep
   Call: (14) aggregate:clean_body(true, _4662) ? creep
   Exit: (14) aggregate:clean_body(true, true) ? creep
   Call: (14) user:a(_3064)==true ? creep
   Fail: (14) user:a(_3064)==true ? creep
   Redo: (13) aggregate:clean_body((user:a(_3064), true), _4520) ? creep
   Call: (14) true==true ? creep
   Exit: (14) true==true ? creep
   Call: (14) _4520=user:a(_3064) ? creep
   Exit: (14) user:a(_3064)=user:a(_3064) ? creep
   Exit: (13) aggregate:clean_body((user:a(_3064), true), user:a(_3064)) ? creep
   Call: (13) bag==bag ? creep
   Exit: (13) bag==bag ? creep
   Call: (13) aggregate:add_existential_vars([], user:a(_3064), _3712) ? creep
   Exit: (13) aggregate:add_existential_vars([], user:a(_3064), user:a(_3064)) ? creep
   Exit: (12) aggregate:template_to_pattern(bag, set(_3064), _3064, user:a(_3064), user:a(_3064), set) ? creep
   Call: (12) bagof(_3064, user:a(_3064), _5354) ? creep
   Call: (18) a(_3064) ? creep
   Exit: (18) a(1) ? creep
   Redo: (18) a(_3064) ? creep
   Exit: (18) a(2) ? creep
   Redo: (18) a(_3064) ? creep
   Exit: (18) a(3) ? creep
   Exit: (12) bagof(_3064, user:a(_3064), [1, 2, 3]) ? creep
   Call: (12) aggregate:aggregate_list(set, [1, 2, 3], _3076) ? creep
   Call: (13) sort([1, 2, 3], _3076) ? creep
   Exit: (13) sort([1, 2, 3], [1, 2, 3]) ? creep
   Exit: (12) aggregate:aggregate_list(set, [1, 2, 3], [1, 2, 3]) ? creep
   Exit: (11) aggregate:aggregate(set(_3064), user:a(_3064), [1, 2, 3]) ? creep
Xs = [1, 2, 3].
3 Likes

Pushed 96f65b30253bac200c0b903bb396d2f0a22bcf28 to address this. Thanks.