1 points par GN⁺ 2024-08-26 | 1 commentaires | Partager sur WhatsApp
  • Dozer est un compilateur Rust en C pur qui vise à rendre Rust utilisable plus tôt dans les étapes de bootstrap ; il est développé sans C++, flex, yacc ni Makefile
  • Le compilateur officiel rustc est écrit en Rust : chaque nouvelle version est construite avec la version précédente de rustc, et cette chaîne remonte jusqu’au compilateur Rust initial écrit en OCaml, puis aux couches Guile et C
  • Le bootstrap Linux de Bootstrappable Builds part d’une graine binaire de 512 octets et s’étend progressivement vers un compilateur simple, un shell, un sous-ensemble de C, TinyCC, yacc, coreutils, Bash, autotools, GCC puis Linux
  • Aujourd’hui, Rust apparaît tard dans la chaîne, via mrustc, écrit en C++, qui compile rustc 1.56 ; il est donc difficile d’utiliser Rust avant l’introduction de C++
  • Dozer vise à devenir un compilateur Rust bootstrappable avec TinyCC, puis à permettre d’enchaîner vers libcore, le backend Cranelift de rustc, un outil remplaçant cargo, et enfin la reconstruction des rustc/cargo canoniques

Objectifs et contraintes de Dozer

  • Dozer est un compilateur Rust en cours d’écriture en C pur
  • Il n’utilise pas C++, ni flex, yacc ou Makefile
  • Son objectif principal est de créer un compilateur permettant de bootstrapper Rust depuis C
  • Il doit notamment être bootstrappable avec TinyCC, en supposant que le système ne dispose d’aucun outil utile en dehors d’un compilateur C et d’un shell très basique

Le problème d’un compilateur Rust qui se construit lui-même

  • Pour exécuter du code Rust, il faut le compiler, et en général cargo build appelle en interne rustc
  • rustc lui-même est un compilateur Rust écrit en Rust, si bien qu’un nouveau rustc est compilé avec une version précédente de rustc
    • rustc 1.80.0 est compilé avec rustc 1.79.0
    • Cette chaîne continue avec rustc 1.78.0 et d’autres versions encore plus anciennes
  • Les premières étapes remontent jusqu’à Rust 0.7, époque à laquelle le compilateur était écrit en OCaml
  • Comme un compilateur OCaml est également nécessaire, la chaîne de bootstrap se poursuit à nouveau vers d’autres implémentations de langages
    • camlboot peut compiler le compilateur OCaml en utilisant Guile
    • L’interpréteur Guile est écrit en C

Les maillons inférieurs de Bootstrappable Builds

  • Bootstrappable Builds s’intéresse au processus permettant de bootstrapper un système complet à partir d’une petite graine binaire
  • Le processus de bootstrap Linux commence avec une graine binaire de 512 octets
    • Cette graine contient un compilateur très simple qui reçoit des nombres hexadécimaux et émet les octets bruts correspondants
    • Une suite d’octets hexadécimaux, hors commentaires et espaces, est aussi considérée techniquement comme du code source analysable
  • Les étapes suivantes construisent progressivement des outils de plus haut niveau
    • Un système d’exploitation très simple
    • Un shell de base
    • Un compilateur un peu plus évolué
    • Une étape qui ressemble à du code assembleur
    • Un sous-ensemble très basique de C
    • Un compilateur C plus avancé écrit dans ce sous-ensemble de C
  • Quelques étapes plus tard, il devient possible de compiler TinyCC, puis la chaîne se poursuit avec yacc, les coreutils de base, Bash, autotools, GCC, puis Linux
  • Chaque étape est listée dans live-bootstrap parts.rst

Rust arrive trop tard dans la chaîne de bootstrap

  • Aujourd’hui, Rust apparaît très tard dans ce processus
  • L’implémentation utilisée est mrustc, une implémentation alternative de Rust écrite en C++
  • mrustc peut compiler rustc 1.56, puis la compilation se poursuit jusqu’au code Rust moderne
  • Cependant, au moment où C++ est introduit dans la chaîne de bootstrap, celui-ci est en pratique presque terminé
  • Pour utiliser Rust avant l’arrivée de C++, il faut un compilateur Rust bootstrappable depuis C

État actuel de l’implémentation de Dozer

  • Dozer est en développement depuis environ deux mois et est écrit sans extensions
  • Il se compile actuellement sans problème avec TinyCC comme avec cproc
  • Le backend utilise QBE
  • L’implémentation en est encore à ses débuts
    • Le lexer est terminé
    • Le parser est implémenté en grande partie
    • L’expansion des macros et des modules est repoussée autant que possible
    • La vérification de types ne prend actuellement en charge que i32
    • La génération de code reste encore rudimentaire
  • Il peut actuellement compiler avec succès le code Rust suivant
fn rust_main() -> i32 {
    (2 - 1) * 6 + 3
}

Le plan pour aller jusqu’à rustc

  • L’objectif est de faire évoluer progressivement Dozer afin de compiler des exemples de base utilisant libc, puis libcore, et enfin rustc
  • Pour compiler rustc, le plan est d’utiliser le backend Cranelift
    • Le backend Cranelift est entièrement écrit en Rust
    • Comme l’absence de C++ est supposée, LLVM ne peut pas être compilé
  • Il est également prévu de créer un outil remplaçant cargo capable de compiler des packages Rust avec Dozer
  • Il faudra trouver et supprimer les fichiers générés automatiquement dans les sources de rustc
    • Les règles du projet Bootstrappable n’autorisent pas le code généré automatiquement
  • L’objectif final est de compiler rustc et cargo, puis d’utiliser ces rustc/cargo compilés directement pour recompiler les rustc/cargo canoniques
  • Le projet est le plus difficile que son auteur ait entrepris jusqu’ici, et même s’il existe des doutes sur ses chances d’aboutir, il entend continuer à essayer

1 commentaires

 
GN⁺ 2024-08-26
Avis de Hacker News
  • Si je devais bootstrapper Rust, je pense que je créerais en C un proto-Rust avec moins de fonctionnalités que Rust complet, puis que j’écrirais un compilateur Rust complet avec ce proto-Rust
    Par exemple, proto-Rust n’aurait pas de borrow checker, aurait une prise en charge limitée ou inexistante des macros, pourrait ne jamais libérer la mémoire, et n’aurait pas besoin de générer du bon code
    En pratique, ce serait proche de C avec une syntaxe Rust, mais du point de vue d’un amateur de Rust, cela semble préférable à l’écriture d’un compilateur Rust en “C à syntaxe C”, qui est l’objectif de ce projet
    Je me demande pourquoi ils n’ont pas choisi cette voie

    • À noter que mrustc, le principal compilateur Rust non écrit en Rust existant, n’a déjà pas de borrow checker
      Supprimer le borrow checker ne casse pas les programmes corrects, cela permet seulement à une grande quantité de programmes incorrects de compiler
      L’usage principal de mrustc est de compiler rustc, et comme on sait déjà que rustc peut se compiler lui-même sans erreurs du borrow checker, ce n’est pas un problème
    • Dans Mozart/Oz, ils ont effectivement fait cela. Il existe un compilateur proto-Oz écrit en Scala, qui sert à compiler le vrai compilateur écrit en Oz
      Comme le compilateur Scala produit du code inefficace, le vrai compilateur est ensuite recompilé par lui-même
      Au final, on obtient ainsi un vrai compilateur efficace qui produit du bon code, et ce processus fait partie du build standard du langage
      https://github.com/mozart/mozart2
    • Dans ce cas, on finit simplement par utiliser deux compilateurs ; je ne vois pas ce qu’on y gagne réellement, à part du travail supplémentaire
  • Je développe pour le plaisir un compilateur C en Rust, et je l’appelle Small C Compiler pour plaisanter sur le fait que Rust est évidemment plus lourd que C. C’est une parodie de “Tiny C Compiler”
    J’utilise Cranelift comme backend, mais toute l’architecture du compilateur est conçue pour être interchangeable via de nombreux traits et facile à hacker
    Je ne compte pas le publier en open source tant qu’il ne fonctionnera pas au moins assez pour gérer printf("%s", "Hello World!")
    J’ai essayé d’implémenter le préprocesseur et le parseur, et à cause du tristement célèbre problème de typedef, j’ai aussi contribué à rust-peg et HimeCC
    Je sais que dans l’industrie on utilise une table des symboles pour conserver le contexte des typedef, mais cela a pour limite de ne pas pouvoir lire le type plus bas. Je me demande quelle est la solution académique ; la seule chose qui me vient à l’esprit est la mémoire transactionnelle
    Si quelque chose en sort d’utile, je finirai probablement par le publier

  • C’est vraiment génial, et ce qui est intéressant, c’est que le même type de problème de bootstrapping existe aussi dans le matériel
    Qu’est-ce qui fabrique les ordinateurs ? Des ordinateurs fabriqués auparavant et les logiciels qui tournent dessus. Plus on y pense, plus c’est fascinant

    • Le même problème de bootstrapping existe partout. Qu’est-ce qui construit les routes ? Des engins de chantier. Mais s’il n’y a pas encore de route, comment amener ces engins jusqu’au chantier ?
      Il y a quelques mois, j’ai rencontré quelqu’un qui travaille dans une startup faisant de la livraison/fulfillment de matériaux pour des projets de construction
      Ce type d’activité demande une expertise différente de la livraison généraliste à la Amazon, non seulement parce que les matériaux ont souvent des propriétés physiques particulières ou dangereuses, mais aussi parce que le lieu de livraison n’a souvent pas encore d’adresse
      C’est solvable, mais cela semble exiger une expertise qui dépasse les capacités habituelles des entreprises de livraison modernes
    • J’ai travaillé dans une entreprise qui construisait des datacenters, et nous voulions développer les logiciels jusqu’au point où l’on pourrait démarrer tout un datacenter avec un seul ordinateur portable
      La raison était que, dans le cadre de collaborations avec des entreprises européennes, il fallait prouver aux régulateurs qu’il n’y avait pas de backdoor
      C’était un problème extrêmement intéressant mais très difficile ; notre équipe n’était impliquée qu’indirectement, mais nous travaillions à faire transiter les données via un proxy garantissant que toutes les données étaient auditables et que rien qui ne devait pas être envoyé ne l’était
      J’ai quitté l’entreprise avant la fin, et j’ai appris plus tard que le projet avait été abandonné parce qu’il était trop difficile
    • Quand on regarde les opcodes assembleur en octal de l’ancien Cray-1 ou les opcodes word de l’IBM System/360, on constate qu’ils avaient été conçus avec une simplicité étonnante, au point qu’un humain pouvait écrire directement les octets d’opcode et assembler à la main
      Ensuite, x86 est arrivé sans budgets énormes ni gros acheteurs, et son assembleur a été conçu pour être aussi efficace et dense que possible
      Résultat : il a perdu des propriétés que d’autres machines pouvaient avoir confortablement
    • C’est l’un des aspects les plus cool de ce genre de projets de bootstrapping et des builds reproductibles
      En théorie, on peut fabriquer soi-même un ordinateur très simple à partir de composants individuels
      Il serait gros, inefficace et incroyablement lent, mais on pourrait le faire respecter une architecture de jeu d’instructions donnée, et construire dessus un programme de bootstrapping
      On pourrait alors affirmer que le résultat obtenu sur un mauvais ordinateur entièrement compréhensible est identique à celui obtenu sur du matériel moderne auquel on ne fait pas totalement confiance
    • C’est aussi une réflexion intéressante à l’échelle de la civilisation humaine. Si l’humanité revenait somehow, à partir d’aujourd’hui, à l’âge de pierre, pourrait-elle reconstruire jusqu’au niveau actuel ?
      C’est une sorte de problème de bootstrapping. Par exemple, les gisements de pétrole actuels sont plus difficiles à exploiter qu’il y a 100 ans ; je me demande si l’on pourrait bootstrapper à nouveau jusqu’à ce point
  • J’ai trouvé ça un peu agaçant de devoir suivre 4 liens pour trouver une justification de haut niveau expliquant les avantages du bootstrapping
    Je m’attendais à ce que la partie « Why » du titre traite de cela
    https://bootstrappable.org/benefits.html

    • Il peut être difficile d’expliquer pourquoi le bootstrapping est important. C’est pourquoi j’ai aussi ajouté une section « Why? » dans le README de mon compilateur bootstrap
      La sécurité est une raison majeure, et c’est le point que l’équipe bootstrappable met surtout en avant
      Pour éviter le problème de trusting trust et des attaques comme la récente backdoor xz, il faut pouvoir tout bootstrapper à partir de pur code source
      Ils suppriment même tous les fichiers prégénérés afin de ne dépendre que de choses écrites à la main et auditables. Par exemple, le bootstrapping de Python devient assez complexe, car le code source contient du code généré par des scripts Python
      De mon côté, je m’intéresse davantage à l’aspect préservation culturelle. J’aimerais préserver des médias modernes dans des lieux comme l’Arctic World Archive pour les archéologues du futur, mais cela n’a aucun sens s’il n’existe aucun moyen de les décoder
      On peut préserver les spécifications, mais on ne peut pas s’attendre à ce qu’ils réimplémentent x265 et tout le nécessaire depuis zéro. Si l’on préserve les binaires, il faudra faire fonctionner du matériel vieux de mille ans, ou virtualiser un CPU vieux de mille ans
      On pourrait leur donner une définition simple de Lisp et du code qui tourne dessus, mais qui va implémenter x265 en Lisp de base ? Ce n’est pas réaliste
      Dans mon projet, j’ai donc créé une machine virtuelle simple, puis j’ai bootstrappé C dessus
      Elle peut être portée très facilement non seulement sur les architectures actuelles, mais aussi sur des architectures futures, voire extraterrestres. Des archéologues du futur ou une civilisation extraterrestre pourraient implémenter la VM en une journée, exécuter le bootstrap de C dessus, puis compiler ffmpeg, etc., afin de décoder nos médias
      Il n’y a pas de boîte noire : tout est débogable, auditable et constitué de code source ouvert écrit à la main
      https://github.com/ludocode/onramp?tab=readme-ov-file#why-bo...
      https://en.wikipedia.org/wiki/Arctic_World_Archive
  • Je suis un peu perplexe. Ce n’est qu’au milieu de l’article qu’apparaît la raison du parcours évoqué dans le titre, et l’idée centrale est qu’au moment où C++ entre dans la chaîne de bootstrapping, le bootstrap est en fait pratiquement terminé ; avant cela, même si l’on voulait utiliser Rust, il n’y aurait aucun moyen de le faire
    Il semble donc que l’objectif soit de disposer d’un compilateur Rust écrit en C — plus précisément bootstrappable à partir de TinyCC sur un système où l’on suppose ne pas encore disposer d’outils utiles
    Mais cela contredit les prémisses du début. rustc 1.80.0 est compilé par 1.79.0, 1.79.0 par 1.78.0, et ainsi de suite jusqu’à 0.7, dont le compilateur était écrit en OCaml
    Il est également dit qu’il existe un projet qui compile avec succès un compilateur OCaml en Guile, et que l’interpréteur Guile est écrit en C
    Dans ce cas, le chemin sans C++ souhaité par l’auteur existe déjà ; ce n’est simplement pas le chemin utilisé au quotidien par l’équipe rustc
    Au final, la motivation n’est pas claire. Veut-il créer un meilleur processus de bootstrapping basé sur C, en faire la méthode de bootstrapping quotidienne de rustc, supprimer l’étape C++ pour une raison particulière, ou préférer l’étape C pour une raison donnée ? Je ne comprends pas
    Si c’est juste parce qu’il en a envie, très bien, mais même après avoir lu un article assez long, je ne vois pas vraiment d’autre objectif

    • Bootstrapper Rust à partir de Guile et du compilateur Rust 0.7 est techniquement possible, mais il faut recompiler le compilateur Rust environ 100 fois
      Chaque étape prend plusieurs heures, et comme 1.80 exige 1.79, 1.79 exige 1.78, etc., il est impossible de sauter une étape jusqu’à 0.7
      Même entièrement automatisé, ce bootstrap pourrait prendre des mois
      En plus, il me semble que les premières versions de rustc ne produisaient que du LLVM ; il faut donc de toute façon bootstrapper un compilateur C++ pour compiler LLVM
      Si l’on a un compilateur C++, autant compiler directement mrustc. Actuellement, mrustc ne prend en charge que jusqu’à rustc 1.54, donc il faudrait tout de même passer par environ 35 versions à compiler
      Tout ce processus n’est pas pratique. L’objectif de Dozer est de bootstrapper un petit compilateur C, de compiler Dozer, puis de compiler directement un rustc récent
      On obtient ainsi Rust directement, sans bootstrapper C++ ni passer par des étapes intermédiaires
  • Si l’on pouvait séparer GCC 4 et binutils de leurs scripts de build d’origine, on pourrait probablement couper environ la moitié de la liste
    Une bonne partie des éléments qui s’y trouvent ne fait que reconstruire à répétition des outils de type autoconf et leurs dépendances
    https://github.com/fosslinux/live-bootstrap/blob/master/part...

  • Je ne comprends pas bien l’intérêt. Pour produire de nouveaux binaires fonctionnant sur la machine cible, rustc doit prendre en charge l’architecture cible
    Si ce support a été ajouté à rustc, il suffit de laisser rustc se compiler lui-même

    • L’enjeu n’est pas tant la prise en charge de nouvelles architectures que le fait d’avoir un processus de bootstrapping beaucoup plus court et auditable
  • J’imagine parfois écrire un interpréteur ou compilateur C++ en Scheme
    Passer directement de Scheme au GCC actuel pourrait être un énorme raccourci
    Mais l’idée reçue veut qu’écrire un compilateur C++ soit quasiment impossible. Cela dit, ce serait sans doute utile pour apprendre

  • Si l’on considère toute la pile à partir du sous-assembleur, est-ce que cela pourrait être une façon de contourner le problème du trusting trust ?
    https://www.cs.cmu.edu/~rdriley/487/papers/Thompson_1984_Ref...

    • Ce n’est possible qu’en auditant tout et en exécutant soi-même l’ensemble du processus.
      Cela dit, il existe quand même des choses comme https://en.m.wikipedia.org/wiki/Underhanded_C_Contest, et je pense que j’aurais raté certaines des soumissions même si je les avais auditées.
    • N’était-ce pas justement l’idée centrale ?
  • Quand j’apprenais un peu le C, j’ai cherché comment les gens faisaient en C des choses à la C++, et j’ai vu des implémentations d’objets, d’exceptions, de concurrence, etc.
    Si mrustc est écrit en C++, ne serait-il pas plus simple de porter en C du code C++ fonctionnel en utilisant ce genre de primitives C de style C++ ?
    Il semble aussi possible de s’appuyer sur la forte interopérabilité entre C++ et C pour migrer petit à petit.
    Bien sûr, je sais que c’est un travail de portage difficile, avec beaucoup de pièges. Mais il faut garder en tête que le point de comparaison, c’est réécrire un compilateur Rust en C.
    Cela me fait aussi penser aux compilateurs C++ vers C qui existaient autrefois. Je ne sais pas s’il en existe encore.
    Même aujourd’hui, des compilateurs Rust vers C/C++ et C++ vers du C lisible par des humains me sembleraient utiles, parce qu’ils permettraient de combiner les avantages de sécurité de l’un avec l’écosystème d’outils de l’autre.