Dans l’article #1, on avait laissé un trou. Le vérificateur détectait la récursion dans fact, notait que v était borné [0, 10], puis émettait un breadcrumb :

“La profondeur est bornée par la plage déclarée, mais ce n’est pas une preuve mathématique de terminaison. Cette preuve arrivera en Phase C.”

Phase C est là. Le compilateur verbose peut maintenant prouver que certains programmes récursifs terminent — pas juste le supposer. Et quand la preuve tient, le breadcrumb disparaît.


Le problème : pourquoi prouver la terminaison ?

Un programme récursif qui ne termine pas consomme de la pile indéfiniment et finit par crasher (le fameux SIGSEGV de l’article #2). Le bounds-check empêche les entrées invalides, mais il ne prouve pas que la récursion converge vers un cas de base.

Prenons un exemple volontairement cassé :

rule bad_fact
  input: n : N
  output: out : number
  logic:
    out = if n.v == 0 then 1 else n.v * bad_fact(N { v: n.v })

n.v est passé tel quel — pas n.v - 1. La récursion ne progresse jamais vers le cas de base. Avec le bounds-check, le programme ne crashera pas sur une entrée hors bornes. Mais pour v = 5, il appellera bad_fact(5) indéfiniment jusqu’au stack overflow.

Le bounds-check protège des entrées invalides. La preuve de terminaison protège de la logique invalide.


Les trois preuves de Phase C

Verbose propose trois manières de prouver la terminaison, selon la structure du programme :

flowchart TD
    R[Regle recursive] --> Q{Quel type input}
    Q -->|concept_group| S[structural : param]
    Q -->|champ qui decroit| D[decreasing : field]
    Q -->|champ qui croit| I[increasing : field]
    S --> OK[Breadcrumb supprime]
    D --> OK
    I --> OK

Preuve 1 — Structural : chaque appel traite un sous-arbre

Quand l’entrée est un type récursif (arbre, liste chaînée — déclaré via concept_group), la preuve structurelle vérifie que chaque appel récursif passe un sous-terme de l’entrée.

concept_group AST
  concept Expr
    variants:
      Int of (value : number)
      Add of (lhs : Expr, rhs : Expr)

rule eval
  input: e : Expr
  output: out : number
  logic:
    out = match e:
      Int(value) => value
      Add(lhs, rhs) => eval(lhs) + eval(rhs)
  proofs:
    termination:
      structural : e

Le vérificateur identifie que Add a deux champs lhs : Expr et rhs : Expr — des champs auto-référentiels (même type que l’input). Les appels eval(lhs) et eval(rhs) passent des binders issus du match qui lient ces champs. Un arbre est une structure finie ; chaque match descend d’un niveau. La récursion atteint forcément une feuille.

Preuve 2 — Decreasing : un champ numérique décroît

Pour factorial, N n’est pas un type récursif — c’est un simple record. La terminaison vient du fait que v décroît de 1 à chaque appel :

concept N
  fields:
    v : number [0, 10]

rule fact
  input: n : N
  output: out : number
  logic:
    out = if n.v == 0 then 1 else n.v * fact(N { v: n.v - 1 })
  proofs:
    termination:
      decreasing : v

Le vérificateur vérifie que :

  1. v est de type Number avec une plage déclarée [0, 10]
  2. L’appel récursif fact(N { v: n.v - 1 }) suit le pattern input.field - k avec k > 0

Si quelqu’un écrit fact(N { v: n.v }) (même valeur), le compilateur refuse avec un message explicite.

Preuve 3 — Increasing : un champ croît vers une borne

Le scanner scan_word avance dans un texte. La position croît au lieu de décroître :

concept ScanState
  fields:
    source : text [..256]
    pos : number [0, 256]

rule word_length
  input: s : ScanState
  output: out : number
  logic:
    out = if s.pos >= length(s.source) then 0
          else if byte_at(s.source, s.pos) >= 97
                  and byte_at(s.source, s.pos) <= 122
               then 1 + word_length(ScanState { source: s.source, pos: s.pos + 1 })
               else 0
  proofs:
    termination:
      increasing : pos

Le vérificateur confirme que pos suit le pattern input.field + k. La borne max 256 garantit la terminaison.


Ce qui change dans le binaire

Rien.

La preuve est un artefact purement compile-time. Zéro octet supplémentaire. Zéro cycle de CPU. Le vérificateur vérifie la preuve, le compilateur note que la preuve existe, le breadcrumb est supprimé.

Le bounds-check (38 octets, article #2) reste — il protège des entrées invalides. La preuve de terminaison protège de la logique invalide. Les deux sont indépendants :

MecanismeProtege contreQuandCout
Bounds-checkEntrees hors bornesRuntime, 38 octets~6 cycles
Preuve de terminaisonLogique qui ne converge pasCompile-time, 0 octet0 cycle

Le breadcrumb : avant et apres

Avant Phase C :

$ verbosec factorial.verbose --native fact
native: rule 'fact' is recursive (cycle through 'fact'); the declared
bound is NOT a termination proof for recursion.

Apres (avec decreasing : v) :

$ verbosec factorial.verbose --native fact
[silence — compilation reussie, aucun warning]

Le compilateur dit ce qu’il sait et ce qu’il ne sait pas. Quand le breadcrumb est la : “je ne sais pas prouver que ca termine.” Quand il disparait : “j’ai verifie mecaniquement.” Il n’y a pas d’etat intermediaire.


Ce qu’on a appris

  1. Trois preuves, trois patterns : structural (sous-termes d’un arbre), decreasing (champ qui decroit), increasing (champ qui croit). Chacune couvre un type de recursion different.

  2. La preuve est compile-time, pas runtime. Zero octet dans le binaire. Le compilateur verifie que la logique converge ; le bounds-check (separe) verifie que les entrees sont valides.

  3. Le verificateur est conservateur. Il ne reconnait que des patterns simples (field - k, field + k, binder de match). Si votre expression est plus complexe (n.v / 2), il refusera. Mieux vaut rejeter un programme valide que d’accepter un programme qui boucle.

  4. Le source n’a presque pas change. Pour factorial, la seule difference entre “pas de preuve” et “preuve verifiee” est une ligne : decreasing : v. Le programme est le meme. Le binaire est le meme. Ce qui change, c’est la confiance que le compilateur a dans le programme.


Verbose est open source : github.com/verbose-org/verbose Version utilisee dans cet article : v0.6.0 Serie : “Verbose — comprendre ce qui se passe vraiment” Article precedent : #2 — 38 octets de securite