Xalg / Fix / La terminaison : une garantie, et son prix
Navigation
Apprendre
Recettes
Référence
Comprendre
Bonnes pratiques
En pratique
Explications

La terminaison : une garantie, et son prix

« Le calcul s'arrête toujours » est la première promesse de Fix. Ce n'est pas un vœu : c'est une propriété vérifiée à la compilation. Cette page explique ce que le vérificateur regarde, pourquoi il refuse parfois un programme qui aurait pourtant terminé, et comment réécrire quand ça arrive.


D'où vient la garantie

Fix sature : il applique tes règles jusqu'au point fixe. Ça termine si l'univers des faits dérivables est fini. Pour la logique pure, c'est acquis — une règle recombine des valeurs qui existent déjà. Le seul danger vient du calcul : une règle qui fabrique des valeurs nouvelles à chaque tour peut grossir sans fin.

compteur(0).
compteur(M) :- compteur(N), N < 5, add(N, 1, M).   % ✗ refusé

add(N, 1, M) produit un M qui n'existait pas. Le vérificateur suit ce flux dans un graphe de positions : chaque argument de chaque prédicat est un nœud, chaque règle trace des arêtes, et une arête est marquée croissante quand la valeur qui la traverse grandit. Une arête croissante qui boucle sur elle-même → refus :

fix: terminaison non prouvable : règle croissante compteur (position 0)
    → :help termination

Le refus est conservateur — et c'est voulu

Tu l'as remarqué : le compteur ci-dessus termine (la garde N < 5 le borne). Le vérificateur le refuse quand même, parce qu'il ignore les gardes : prouver qu'une garde arithmétique borne une croissance est indécidable en général, et Fix préfère un critère simple et sûr à une analyse savante et faillible.

La règle du jeu est donc asymétrique, et honnête :

Une nuance connue côté « accepte » : l'accumulation récursive à travers une jointure (cout(X, Z, C) :- cout(X, Y, C1), arc(Y, Z, C2), add(C1, C2, C).) passe la garde alors que sa terminaison dépend des données (une infinité de chemins si arc a un cycle). Le moteur la rattrape à l'exécution : budget de travail borné + certificat Bellman-Ford ⟹ erreur explicite en temps borné (« évaluation incomplète »), jamais un calcul sans fin (BUG-044, volets runtime fermés). La forme recommandée reste la relaxation (voir Recettes) : elle termine, cycles compris (poids ≥ 0).

Il n'y a pas de drapeau pour passer outre : la réponse à un refus est une réécriture, pas un contournement.


La réécriture : descendre au lieu de monter

La forme décroissante est toujours acceptée : pars de la borne et descends. La descente s'arrête d'elle-même à 0 — plus besoin que le vérificateur fasse confiance à une garde.

compteur(5).
compteur(M) :- compteur(N), N > 0, sub(N, 1, M).   % ✓ accepté
?- compteur(X).
X = 0
X = 1
...
X = 5

Même relation, mêmes six faits — la borne (5) est simplement devenue le point de départ au lieu d'un plafond à surveiller. C'est le réflexe à acquérir : quand une règle croissante est refusée, cherche la borne qu'elle visait, et fais-en l'origine d'une descente.

Le même besoin se couvre aussi, souvent, par un générateur de domaine — un prédicat de faits qui énumère les valeurs licites, testé ensuite par la garde (voir Bonnes pratiques).


Ce qui ne risque rien — et le piège du terme construit

Les règles qui recombinent des valeurs existantes passent sans y penser : chemins, fermetures transitives, jointures — aucune valeur neuve n'y naît.

impacte(S, T) :- depend(S, T).
impacte(S, T) :- impacte(S, U), impacte(U, T).   % ✓ recombine, ne fabrique pas

Le piège, c'est qu'une règle peut fabriquer autre chose que des nombres : un terme. Une tête récursive qui construit un terme plus gros que ce que le corps fournit est croissante au même titre :

has_type(cons(H, T), list(int)) :- has_type(H, int), has_type(T, list(int)).
% ✗ refusé : règle croissante has_type (position 0)

La sortie est la même que pour le compteur : donner un nom aux objets et relier leurs types, au lieu de construire le terme en tête. C'est le geste des types dépendants (matmul(A, B, matrix(T, R, C)) :- …) — le terme dérivé vit dans son propre prédicat, les objets sont nommés par des faits. Voir Les types dépendants et, pour les données, un ADT déclaré (type list(T) = nil | cons(T, list(T)).) dont les faits ground sont vérifiés sans qu'aucune règle croissante soit nécessaire.


Les bords honnêtes


À côté