La trace en Prolog

Jean-François Perrot

  1. L'ordre de trace
  2. Le littéral-but est considéré comme placé dans une boîte
  3. Dans le début de trace ci-dessus, on peut lire
  4. Interaction avec l'utilisateur
  5. Observation du retour-arrière (Redo)
  6. Effet du coupe-choix
  7. Arrêter la trace

Dans tout système de programmation interactif, l'idée de trace correspond à un mode de fonctionnement de l'exécuteur
qui visualise les différentes phases de l'exécution, en général dans un perspective de mise au point.
Notamment, dans le cas d'un programme récursif, la trace fait apparaître l'arbre des appels.
En raison de son schéma d'exécution complexe (en zig-zag), Prolog a un mode trace particulier,
qui a été formalisé dans le livre classique de Clocksin & Mellish.
[W. F. Clocksin and C. S. Mellish. Programming in Prolog. Springer-Verlag, New York, Third, Revised and Extended edition, 1987]

Nous prendrons comme exemple le programme arbgen1, avec les données familiales initiales.

  1. L'ordre de trace

    s'applique à la démonstration d'un littéral-but.
    Nous choisissons comme exemple le littéral arbgen1(cesar), dont la résolution donne normalement

    ?- arbgen1(cesar).
    cesar
              jules
              silvia
    false.


    Lancement de la trace [pour l'arrêter, voir in fine]:

    ?- trace, arbgen1(cesar).
       Call: (8) arbgen1(cesar) ? creep
       Call: (9) ecrire(cesar, 0) ? creep
       Call: (10) tab(0) ? creep
       Exit: (10) tab(0) ? creep
       Call: (10) write(cesar) ? creep
    cesar
       Exit: (10) write(cesar) ? creep
       Call: (10) nl ? creep

       Exit: (10) nl ? creep
       Exit: (9) ecrire(cesar, 0) ?
       ...........................

    Lorsque le calcul se termine normalement (et non par "abort", commandé par l'option 'a', voir plus loin  sect. 4),
    Prolog este en "mode trace", et le manifeste en demandant
    [trace]  ?-
    au lieu de
    ?-
    Pour revenir à la boucle d'interaction ordinaire, dites
    [trace]  ?- notrace, nodebug.
    true.

  2. Le littéral-but est considéré comme placé dans une boîte

    munie de 2 entrées et de 2 sorties (globalement appelées les 4 ports)
    Box
    [figure extraite des notes de cours de Jacques Tisseau et Fred Mesnard, pl. 52-64.]

    Dans le cas d'un prédicat évaluable comme tab, write ou nl, Call est immédiatement suivi de Exit.
    Dans celui d'un prédicat défini par un paquet de clauses, chaque Call vise une clause du paquet,
    et il est suivi d'une séquence de Calls aux différents littéraux qui composent le corps de la clause.
    Ces appels correspondent à une descente dans l'arbre des appels récursifs à l'exécuteur, et la profondeur courante est affichée entre parenthèses.

  3. Dans le début de trace ci-dessus, on peut lire

    que l'appel initial à arbgen1(cesar) s'est produit à la profondeur 8,
    qu'il a déclenché un appel en profondeur 9 à ecrire(cesar, 0),
    Ce que nous avons vu correspond à l'exécution du fragment de code Prolog :
     arbgen1(X) :- ecrire(X, 0), ...
     ecrire(X, N) :- tab(N), write(X), nl.

    On s'attend donc à ce que la suite commence par    Call: (9) genarb1(cesar, 10)...

  4. Interaction avec l'utilisateur

    Reste à expliquer les indications "? creep".
    Elles sont produites par l'interaction avec l'utilisateur :
    C'est l'option 's' la plus intéressante : dans notre exemple, le prédicat ecrire étant sans mystère est avantageusement skipped :
    ?- trace, arbgen1(cesar).
       Call: (9) arbgen1(cesar) ? creep
       Call: (10) ecrire(cesar, 0) ? skip
    cesar
       Exit: (10) ecrire(cesar, 0) ? creep
       Call: (10) genarb1(cesar, 10) ? creep
       ...........................


  5. Observation du retour-arrière (Redo)

    Suite de l'exemple :
       Call: (10) genarb1(cesar, 10) ? creep  ---------|
       Call: (11) genp1(cesar, 10) ? creep    ---------| -- première clause de genarb1
       Call: (12) pere(_L198, cesar) ? creep  ----|    |
       Exit: (12) pere(jules, cesar) ? creep  ----|    |
       Call: (12) ecrire(jules, 10) ? skip        |    |
              jules                               |    |
       Exit: (12) ecrire(jules, 10) ? creep       |    |
       Call: (12) sgenarb1(jules, 10) ? skip      |    |
       Fail: (12) sgenarb1(jules, 10) ? creep     |    |
       Redo: (12) pere(_L198, cesar) ? creep  ----|    |
       Fail: (12) pere(_L198, cesar) ? creep  ----|----| -- un seul père
       Fail: (11) genp1(cesar, 10) ? creep             |
       Redo: (10) genarb1(cesar, 10) ? creep  ---------|
       Call: (11) genm1(cesar, 10) ? creep    ------------- seconde clause de genarb1
       Call: (12) mere(_L191, cesar) ? creep  ----|
       Exit: (12) mere(silvia, cesar) ? creep ----|
       Call: (12) ecrire(silvia, 10) ? skip       |
              silvia                              |
       Exit: (12) ecrire(silvia, 10) ? skip       |
       Call: (12) sgenarb1(silvia, 10) ? skip     |
       Fail: (12) sgenarb1(silvia, 10) ? creep    |
       Redo: (12) mere(_L191, cesar) ? creep  ----|
       Fail: (12) mere(_L191, cesar) ? creep  ----| -- une seule mère
       Fail: (11) genm1(cesar, 10) ? creep
       Fail: (9) arbgen1(cesar) ? creep
    false.

  6. Effet du coupe-choix

    Même examen sur le programme arbgen2 : les retours-arrière ont disparu.
    C'est dû au changement de la définition dans genarb2, qui est séquentiel,
    et aux coupe-choix dans genp2 et dans genm2.

    ?- trace, arbgen2(cesar).
       Call: (9) arbgen2(cesar) ? creep
       Call: (10) ecrire(cesar, 0) ? skip
    cesar
       Exit: (10) ecrire(cesar, 0) ? creep
       Call: (10) genarb2(cesar, 10) ? creep
       Call: (11) genp2(cesar, 10) ? creep
       Call: (12) pere(_L198, cesar) ? creep
       Exit: (12) pere(jules, cesar) ? creep
       Call: (12) ecrire(jules, 10) ? skip
              jules
       Exit: (12) ecrire(jules, 10) ? creep
       Call: (12) sgenarb2(jules, 10) ? skip
                        Pere inconnu
                        Mere inconnue
       Exit: (12) sgenarb2(jules, 10) ? creep
       Exit: (11) genp2(cesar, 10) ? creep 
       Call: (11) genm2(cesar, 10) ? creep -- en séquence
       Call: (12) mere(_L198, cesar) ? creep
       Exit: (12) mere(silvia, cesar) ? creep
       Call: (12) ecrire(silvia, 10) ? skip
              silvia
       Exit: (12) ecrire(silvia, 10) ? creep
       Call: (12) sgenarb2(silvia, 10) ? skip
                        Pere inconnu
                        Mere inconnue
       Exit: (12) sgenarb2(silvia, 10) ? creep
       Exit: (11) genm2(cesar, 10) ? creep
       Exit: (10) genarb2(cesar, 10) ? creep
       Exit: (9) arbgen2(cesar) ? creep
    true.


  7. Arrêter la trace

    Lorsqu'on a fini d'examiner le comportement d'un prédicat, y compris par l'option abort,
    il suffit que le calcul ait réussi une fois (annonce true .) pour que la trace reste active,
    comme le montre l'invite de la boucle top-level :
    [trace]  ?-

    Pour revenir à une interaction normale, comment s'en débarrasser ?

    Il faut demander nodebug.

    % Execution Aborted
    [trace]  ?- nodebug.
    true.

    ?-



    N.B. Si vous demandez seulement notrace, vous aurez ensuite à demander nodebug.

    [trace]  ?- notrace.
    true.

    [debug]  ?- nodebug.
    true.

    ?-