Faites une photocopie d’une photocopie d’une photocopie. À chaque génération, ça se dégrade : le grain monte, le texte bave. Sauf qu’il existe un cas magique où la copie est parfaite — où la photocopie de la photocopie est rigoureusement identique à l’originale. On appelle ça un point fixe : appliquer l’opération une fois de plus ne change plus rien.

Tout langage dont le compilateur est écrit dans ce même langage finit devant une seule question : la version de moi que je viens de produire, quand je lui demande de me produire, produit-elle exactement moi ?

Le 11 juillet, verbose a répondu oui, à l’octet près. C’est le bout de l’arc self-hosting qu’on suit depuis des semaines. Et c’est, dans un compilateur, la validation la plus forte qui existe.

Où on en était

Dans cette série, on a suivi les deux moitiés d’un compilateur. La moitié qui lit : l’arbre, le tokenizer. La moitié qui écrit : une règle verbose qui émet du code machine.

Il restait à boucler la boucle : que l’émetteur écrit en verbose ne compile plus une petite factorielle, mais le compilateur verbose tout entier — ses propres 855 KB de source — et que le binaire produit soit le compilateur verbose. C’est fait, et cette fois c’est sur main, pas sur une branche : 18 commits en une semaine ont fermé l’arc ouvert le 13 juin.

Trois générations

Le mot « point fixe » se démonte en trois personnages :

  gen0  ← l'émetteur verbose, compilé par le compilateur Rust hôte.
          (ce qu'un checkout tout frais construit)
            │  on lui donne les 855 KB de source verbose

  gen1  ← l'ELF émis : c'est l'émetteur verbose, mais compilé PAR verbose.
            │  on lui donne les MÊMES 855 KB de source

  gen2  ← l'ELF que gen1 émet à son tour.

  La porte :   gen1  ==  gen2   (octet pour octet)

gen0, c’est l’amorce écrite en Rust — le pied-de-biche qui démarre tout. gen1, c’est verbose qui s’est compilé lui-même une fois. gen2, c’est verbose-compilé-par-verbose qui se compile encore une fois.

Et le verdict :

gen1 == gen2 == 1 302 980 octets, même sha256 001481eb….

Pas « ça marche ». Pas « ça produit un binaire qui tourne ». Le même binaire, au bit près, deux générations de suite. Vérifié par un test de régression (two_generation_bootstrap_fixed_point — il lui faut ~9 GB de RAM et tourne en ~30 s).

Pourquoi l’octet-pour-octet, et pas juste « ça compile »

Parce que « ça compile » se contente d’un binaire qui fonctionne. Le point fixe demande plus : que le compilateur, en se reproduisant, ne dérive pas d’un seul octet. S’il dérivait — ne serait-ce que d’un bit — ça voudrait dire que gen1 et gen0 ne sont pas le même compilateur : l’un mettrait quelque chose que l’autre ne met pas. L’égalité byte-à-byte prouve que le langage tient sur son propre sol : à partir d’ici, verbose n’a plus besoin de Rust pour se reconstruire à l’identique. C’est ça, « se tenir debout tout seul ».

Ce qui a failli l’arrêter — et pourquoi le diagnostic est la vraie histoire

Une première tentative a planté. L’hypothèse de départ : un débordement d’entier 32 bits quelque part dans les compteurs de l’émetteur. Plausible, vu la taille de l’entrée.

C’était faux. Et dans le commit, c’est écrit comme faux — pas discrètement effacé. Le ptrace + objdump à l’adresse exacte du crash ont donné la vraie cause : épuisement d’arène. L’instruction fautive écrivait un nœud à 40 octets au-delà du 1 GiB d’arène. 10,3 millions de nœuds × 104 octets = pile la limite, franchie de 40 octets. Pas un pointeur fou : un débordement propre, à la frontière.

Pourquoi gen1 déborde quand gen0 tient, sur la même entrée ? Deux modèles mémoire différents.

  gen0 (backend Rust)          gen1 (émetteur auto-hébergé)
  passe les records par pile   stocke CHAQUE record ET variante en arène
  réutilise l'espace           ne libère jamais
  ~1,5 M nœuds au pic          ~84 M nœuds, ~8,7 GB de RSS au pic

L’analogie : gen0 travaille sur un tableau blanc qu’il efface au fur et à mesure ; gen1 écrit sur un carnet dont il n’arrache jamais une page. Pour une factorielle, le carnet suffit large. Pour compiler le compilateur entier, il déborde.

Le fix — et le fix qui attend, assumé

Le correctif de PR #102 est petit : le prologue ELF de l’émetteur mmap-e désormais 16 GiB en MAP_NORESERVE. Réserver est gratuit — c’est comme réserver tout un parking mais ne payer que les places où on se gare réellement ; seuls les ~8,7 GB effectivement touchés coûtent de la RAM, et les petits programmes n’en touchent presque aucune. Les petits compilateurs restent petits ; le cas lourd cesse de crasher.

Mais le vrai correctif mémoire (dédupliquer un recalcul en O(sites × programme), et recycler l’arène entre procédures) est documenté et explicitement reporté. Il ferait fondre le pic de 8,7 GB vers l’empreinte de gen0. Il ne changerait pas la sortie byte-identique.

Et c’est le point qui compte le plus. Un piège classique des jalons de compilateurs auto-hébergés, c’est de coupler la preuve de correction à une optimisation précise — si bien qu’on ne sait plus si le résultat tient par principe ou par chance. Ici les deux sont découplés : l’identité byte-à-byte est une propriété sémantique de l’émetteur, et elle tient aujourd’hui. L’empreinte mémoire est une propriété de ressource, et elle s’améliorera plus tard. La preuve ne dépend pas de la perf.

C’est la même discipline qu’ailleurs dans ces projets : nommer la limite au lieu de la cacher. Le pic de 8,7 GB, le recalcul O(sites × programme), l’équivalence pas-encore-structurelle des blocs input: — tout est écrit sur le même commit qui livre le point fixe. Aucun n’est un bloqueur de la propriété prouvée. Tous sont sur la carte.

Ce que ça veut dire

La chaîne complète que le projet construisait depuis son ouverture est désormais démontrée en elle-même : parse → vérifie (quatre preuves : lints, types sound, pureté, terminaison) → interprète → émet → se compile byte-identique → se reproduit tout entier. Le checker auto-hébergé qui vérifie ses propres preuves, l’émetteur auto-hébergé qui émet son propre ELF, et la porte d’identité entre générations — trois affirmations qui tiennent en même temps.

Le trigger de release v0.10.0 s’est armé : 18 commits au-dessus de v0.9.0, fermant un jalon de langage. Et après ça — le langage se sert de son propre sol pour la suite.

Une photocopie parfaite de soi-même, deux fois de suite. verbose tient debout tout seul.