Search code examples
prolog

Automating my pet debugging strategy in SWI-Prolog


I have a very straightforward question I'd be happy to receive any guidance on.

I'm working on a Definite Clause Grammar, and I'm running spot checks on its output. If a parse tree is confusing to me, I want to trace it back to the predicate that generated that part of the tree. So what I do is insert numeric atoms into my predicates. Like so:

sentence(sentence(Subject, Verb, Object)) --> Subject, Verb, Object.

becomes

sentence(sentence(736, Subject, Verb, Object)) --> Subject, Verb, Object.

I can then search for the number 736 and examine that particular predicate to see why it was chosen by Prolog. This has become very handy as my grammar has ballooned in size. But it's inconvenient to have to make these text edits whenever I want to debug.

Is there some elegant Prolog rule I could add to the grammar when I want to debug in this way, something that would attach a unique i.d. to each predicate?


Solution

  • This is highly implementation-specific, but SWI-Prolog has a source_location/2 predicate that, called inside a term_expansion/2 rule, gives you the file name and line number of the clause being expanded.

    So you can use something like the following:

    term_expansion(Head --> Body, EnhancedHead --> Body) :-
        source_location(File, Line),
        format('~w --> ~w at ~w:~w~n', [Head, Body, File, Line]),
        Head =.. [Functor, Arg1 | Args],
        Arg1 =.. [ArgFunctor | ArgArgs],
        EnhancedArg1 =.. [ArgFunctor, File:Line | ArgArgs],
        EnhancedHead =.. [Functor, EnhancedArg1 | Args].
    
    hello -->
        [world].
        
    sentence(sentence(Subject, Verb, Object)) -->
        [Subject, Verb, Object].
    

    Note that this term_expansion/2 will print the log message for every -->/2 rule in the program:

    hello --> [world] at /home/isabelle/hello.pl:9
    sentence(sentence(_2976,_2978,_2980)) --> [_2976,_2978,_2980] at /home/isabelle/hello.pl:12
    

    But it will then fail if the rule's head doesn't have at least one argument, and the first argument doesn't have at least one argument of its own. This is fine, failure just means "don't rewrite this term":

    ?- listing(hello).
    hello([world|A], A).
    
    true.
    
    ?- phrase(hello, Hello).
    Hello = [world].
    

    But sentence//1 will be rewritten:

    ?- listing(sentence).
    sentence(sentence('/home/isabelle/hello.pl':12, A, B, C), [A, B, C|D], D).
    
    true.
    
    ?- phrase(sentence(sentence(Position, S, V, O)), [isabelle, likes, prolog]).
    Position = '/home/isabelle/hello.pl':12,
    S = isabelle,
    V = likes,
    O = prolog.
    

    You could build on this, maybe with a separate operator ---> to mark only those rules you really want rewritten. I think having this extra implicit argument is a recipe for lots of unexpected failures when you try to unify something with the actual underlying term, not the term as it appears in the source code.

    So maybe a better approach would be something like this:

    sentence(sentence(@position, Subject, Verb, Object)) -->
        [Subject, Verb, Object].
    

    and a corresponding term_expansion/2 rule that looks for these @position terms and replaces them accordingly.