1 points par GN⁺ 2 시간 전 | 1 commentaires | Partager sur WhatsApp
  • Un assert sert à exprimer dans le code des préconditions, postconditions et invariants, et lorsqu’une contrainte peut être imposée par le système de types, il est préférable de l’exprimer via les fonctionnalités du langage
  • Dans Zig, std.debug.assert n’est pas une macro mais une fonction ordinaire, qui s’appuie sur unreachable pour signaler un chemin impossible à atteindre et peut aussi servir à l’optimisation
  • En Debug et ReleaseSafe, un assert qui échoue provoque un panic et fait planter le programme, mais en ReleaseFast et ReleaseSmall, cela devient un comportement illégal non vérifié pouvant entraîner un fonctionnement erroné
  • Désactiver les asserts en production fait perdre l’occasion de découvrir rapidement des hypothèses erronées, puis le code peut finir par dépendre d’asserts faux, jusqu’à créer des vulnérabilités
  • Le choix entre ReleaseSafe et ReleaseFast dépend des priorités du programme, mais l’essentiel est de ne pas masquer le problème en désactivant les asserts : il faut corriger les asserts erronés

Rôle des asserts et comportement par défaut de Zig

  • Un assert permet d’exprimer dans le code qu’une condition doit toujours être vraie, comme « cet argument ne peut pas être null » ou « cet entier ne peut pas être pair »
    • Exemples : assert(my_arg != null);, assert(my_num % 2 != 0);
    • Si le système de types peut imposer la contrainte, mieux vaut utiliser une fonctionnalité du langage plutôt qu’un assert
    • En Zig, un pointeur ordinaire *Foo ne peut pas être null, tandis qu’un pointeur optionnel ?*Foo peut l’être, mais oblige à faire une vérification avant d’accéder à la valeur
  • Les asserts conviennent bien pour expliciter des préconditions, postconditions et invariants
    • Un bon assert peut être plus puissant que des tests unitaires pour attraper des erreurs de programmation
    • Leur efficacité peut encore augmenter lorsqu’ils sont utilisés avec du fuzzing

unreachable et les asserts dans Zig

  • Dans Zig, les asserts reposent sur unreachable, une fonctionnalité du langage qui indique qu’un chemin de code est invalide
    • Dans un switch, on peut marquer une branche inatteignable comme .a => unreachable
    • unreachable peut être utilisé comme instruction, mais aussi là où une expression de n’importe quel type est requise
    • Il n’est pas nécessaire de fabriquer artificiellement une valeur temporaire pour le cas impossible
  • Dans la bibliothèque standard de Zig, std.debug.assert est implémenté ainsi
    pub fn assert(ok: bool) void {
      if (!ok) unreachable; // assertion failure
    }
    
  • L’information portée par unreachable peut être exploitée pour l’optimisation
    • Le compilateur peut supprimer les chemins inatteignables, et cette information peut se propager pour permettre des optimisations non locales
    • Tous les asserts n’améliorent pas les performances, mais certains peuvent déboucher sur des optimisations difficiles à anticiper pour le programmeur

Modes de build et sûreté à l’exécution

  • Zig propose les modes de build Debug, ReleaseSafe, ReleaseFast et ReleaseSmall
    • Ce réglage n’a pas nécessairement à s’appliquer globalement à tout le programme
    • Chaque dépendance peut être compilée dans un mode différent, et @setRuntimeSafety permet aussi d’ajuster la sûreté à l’exécution à l’échelle d’un bloc dans une fonction
  • L’échec d’un assert est considéré comme un « illegal behavior » dans Zig
    • Dans les modes vérifiés, à savoir Debug, ReleaseSafe et @setRuntimeSafety(true), le programme panique et s’arrête
    • Dans les modes non vérifiés, à savoir ReleaseFast, ReleaseSmall et @setRuntimeSafety(false), cela devient un « unchecked illegal behavior » qui fait fonctionner le programme de travers
  • Les conséquences d’un unchecked illegal behavior ne sont pas garanties
    • Dans l’exemple avec switch, les caractéristiques du code machine généré peuvent actuellement donner l’impression que l’exécution tombe dans une autre branche
    • Avec une autre version du compilateur, le comportement erroné pourrait être totalement différent
    • On peut voir ce comportement dans cet exemple godbolt
  • La différence de comportement entre assert et switch en ReleaseSafe et en ReleaseFast peut être observée dans un autre exemple godbolt
    • En ReleaseFast, on voit apparaître une forme où la fonction saute toutes les comparaisons et retourne true
    • C’est le type d’optimisation dont dépendent fortement les jeux vidéo et d’autres applications multimédia temps réel

L’assert de Zig n’est pas une macro

  • std.debug.assert dans Zig est une fonction ordinaire, pas une macro
    • Zig n’a pas de macros
    • C’est souvent un point surprenant pour les développeurs C/C++ qui découvrent Zig
  • En C/C++, lorsqu’on désactive les asserts, il est courant que l’appel à assert entier, ainsi que l’expression passée en argument, se comporte comme s’il avait été commenté
    • C’est pourquoi il ne faut pas mettre d’expressions avec effets de bord dans un assert en C/C++
    • En cas de désactivation, l’opération elle-même peut disparaître
  • En Zig, selon les règles d’appel de fonction, les arguments sont évalués avant l’appel
    • L’expression d’argument est donc évaluée indépendamment de la logique interne de std.debug.assert
    • On peut ainsi mettre dans un assert une expression qui a des effets de bord, par exemple
    // assert that the remove operation is not a noop:
    assert(my_map.remove("expected-to-exist"));
    
  • En revanche, si calculer la condition de l’assert demande des opérations complexes, ce calcul n’est pas forcément éliminé en mode non vérifié
    • Dans ce cas, il faut protéger le code avec comptime if
    const builtin = @import("builtin");
    
    if (builtin.mode == .Debug) {
      var condition = ...;
      // whatever bookkeeping is necessary
      // to compute the condition
      assert(condition == .ok);
    }
    
  • Cela peut sembler inhabituel si l’on est habitué à la sémantique C/C++, mais Zig part du principe que les asserts ne sont généralement pas désactivés

Le problème de désactiver les asserts en production

  • Il existe grosso modo trois façons d’utiliser les asserts
    • Les conserver comme vérifications à l’exécution et faire planter le processus avec un panic en cas d’échec
    • Les utiliser comme outil d’optimisation des performances, en acceptant que le programme se comporte mal si l’assert est faux
    • Les désactiver complètement
  • std.debug.assert ne prend pas en charge nativement la désactivation totale des asserts
    • On peut obtenir un comportement plus proche de C/C++ en implémentant son propre assert qui consulte un drapeau au moment du build
  • Si l’on a envie de désactiver les asserts, c’est généralement à cause de la combinaison de deux facteurs
    • On ne veut pas garder des vérifications à l’exécution à cause de leur coût en performances ou du risque de crash de l’application
    • On a du mal à croire que les asserts sont toujours corrects, et l’on craint les dysfonctionnements qu’ils pourraient causer s’ils sont exploités pour l’optimisation
  • Comme l’a rappelé matklad dans une discussion liée, il existe des situations où éviter un crash est justifié pour des raisons d’ingénierie parfaitement valables
    • Mais, pour les logiciels ordinaires, prendre l’évitement des crashs comme choix par défaut est jugé mauvais
  • Désactiver les asserts permet au programme de continuer à tourner même lorsqu’une condition censée être impossible se produit réellement
    • Le programme continue alors sous une hypothèse fausse, ce qui constitue déjà une forme de dysfonctionnement, même sans unchecked illegal behavior
  • Si l’unchecked illegal behavior, ou l’undefined behavior en C, est dangereux, c’est notamment parce qu’il peut transformer un programme en weird machine
    • Dans un logiciel suffisamment complexe, le programme peut se tordre de manière imprévue même sans UIB
    • Le fait qu’un assert devienne faux à l’exécution constitue déjà une sortie hors spécification, et cela peut à lui seul conduire à des actions non intentionnelles
    • L’injection SQL est un exemple concret et répandu de dysfonctionnement de type weird machine sans UIB
  • Si le coût d’un dysfonctionnement est trop élevé, il vaut mieux laisser les asserts activés
    • Si les performances sont extrêmement importantes et que l’on peut accepter le risque d’un comportement erroné, il est alors logique d’utiliser les asserts comme opportunités d’optimisation
    • Désactiver les asserts risque de faire perdre les gains de performance tout en donnant une fausse impression de sûreté

Comment des asserts erronés trompent toute une base de code

  • Le risque central est qu’un assert faux puisse ne jamais apparaître dans les tests et n’échouer qu’en production
    • Si l’on pouvait garantir que tous les asserts sont toujours vrais, leur utilisation pour l’optimisation ne ferait pas débat
    • Si l’on pouvait garantir que les tests attrapent tous les asserts erronés, les optimisations en production seraient elles aussi sûres
    • En pratique, on peut écrire de mauvais asserts, et les tests ne les détectent pas forcément
  • Désactiver les asserts en production fait perdre la meilleure occasion de détecter le plus tôt possible des asserts erronés
    • Plus grave encore : le code ultérieur continue d’être écrit en s’appuyant sur ces asserts erronés
  • Dans l’exemple, processThing est censée n’être appelée que sur un thing déjà démarré
    fn processThing(thing: Thing) void {
       // this function must always be invoked on
       // a thing that has already been started
       assert(thing.is_started);
    
       // ...
    }
    
  • On peut ne pas voir que cet assert, jamais pris en défaut pendant les tests, est en réalité désactivé en production et peut donc être faux
    • S’il n’y a pas de dysfonctionnement visible côté utilisateur, tout peut sembler normal et le développement continue
  • Plus tard, quelqu’un peut ajouter du code en partant du principe que thing a déjà été démarré et qu’il est donc possible d’appeler baz sans préparation supplémentaire
    fn processThing(thing: Thing) void {
       // this function must always be invoked on
       // a thing that has already been started
       assert(thing.is_started);
    
       // ...
    
       // Since thing is already started, we don't
       // need to foo the bar before bazzing the qux.
       // It would be really bad to baz the qux otherwise,
       // so we add an assert for good measure.
       assert(thing.is_fooed);
       thing.baz(qux);
    }
    
  • Même si le second assert est logiquement correct en lui-même, le danger apparaît si le premier assert peut en réalité être faux
    • Dans les tests, comme le premier assert n’échoue pas, le second non plus
    • En production, avec des asserts désactivés, on peut ne pas remarquer le moment où une vulnérabilité entre dans la base de code
  • Quand les asserts du code se mettent à tromper les développeurs, écrire du code correct devient déraisonnablement difficile

Le choix dépend des priorités du programme

  • Chaque programme a ses propres priorités, et pour certains il peut être légitime de privilégier les performances plutôt que la réduction maximale du risque de dysfonctionnement
    • Dans ce cas, transformer les asserts en opportunités d’optimisation est un choix naturel
  • Désactiver machinalement les asserts en production est présenté comme un choix inférieur, aussi bien au fait de les laisser activés qu’à l’exploitation assumée des optimisations de performance
    • Être très critique envers ReleaseFast tout en acceptant sans recul la désactivation des asserts est contradictoire
  • Zine est un générateur de sites statiques, aujourd’hui surtout utilisé pour compiler des blogs personnels
    • Son modèle de menace n’est pas défini et ce n’est pas sa priorité principale
    • Il distribue des builds ReleaseFast, en privilégiant le fait d’être un ordre de grandeur plus rapide que Hugo
  • Awebo est une alternative à Discord auto-hébergeable encore en phase pre-alpha
    • Le fait qu’il traite des données personnelles et soit exposé à Internet est déjà clair
    • Il est prévu de fournir des builds ReleaseSafe au moment du déploiement
    • En revanche, certaines dépendances essentielles comme FFmpeg, Xiph Opus et SQLite seront compilées en ReleaseFast
    • Pour ces dépendances, le gain de performance est jugé nettement plus important qu’une réduction supplémentaire du risque de dysfonctionnement

Choix de vrais projets et exemples de sécurité

Les asserts implicites de Zig ne disparaissent jamais totalement

  • Même si l’on peut désactiver ses propres asserts, il est impossible de désactiver les asserts que le langage Zig ajoute implicitement au code
    • Cela concerne par exemple le dépassement d’entier, la division par zéro ou les accès hors limites d’un tableau
    • Ces conditions déclenchent soit un panic à l’exécution, soit sont exploitées à des fins d’optimisation
  • La pratique consistant à désactiver les asserts en production peut laisser des asserts erronés se dégrader et se multiplier dans la base de code
    • Cela peut renforcer une paranoïa vis-à-vis de l’UIB, au point que les développeurs finissent par craindre inconsciemment de réactiver les asserts et d’en voir les conséquences
  • La conclusion inévitable est qu’il ne faut pas masquer le problème en désactivant les asserts : il faut corriger les asserts erronés
    • La correction du programme doit être recherchée dans son ensemble, pas seulement sur un sous-ensemble

1 commentaires

 
GN⁺ 2 시간 전
Avis sur Lobste.rs
  • Je suis d’accord sur le fait que, pour assert, le mieux est généralement soit de simplement provoquer un crash, soit de ne faire planter que la tâche en cours, comme avec une panic en Rust. En revanche, j’ai du mal à accepter que transformer assert en indice d’optimisation soit toujours préférable à le supprimer purement et simplement
    Premièrement, un assert arbitraire aide souvent peu à l’optimisation, et de nombreuses conditions ne peuvent pas être exploitées directement par l’optimiseur. À moins d’introduire une hypothèse explicite du type « cette branche ne sera jamais prise », les gains de performance obtenus en disséminant des hypothèses aléatoires dans le code ont de fortes chances d’être modestes
    Deuxièmement, remplacer assert par une hypothèse élargit énormément le rayon d’impact d’une erreur. Par exemple, dans un système qui traite des données séparées par projet ou par utilisateur, imaginons un assert au milieu d’une fonction de calcul qui détecte un état censé être impossible. Si, dans un build de release, on le désactive parce qu’il coûte trop cher, un simple retrait peut limiter l’impact à un projet ou à un utilisateur, et le problème peut encore être détecté plus tard par d’autres vérifications. En revanche, si on en fait un cas de comportement indéfini, le calcul peut sauter vers un code aberrant, corrompre la mémoire de façon arbitraire et endommager les données de tous les projets
    Au final, choisir par défaut des assert non sûrs en build de release revient à optimiser prématurément des points arbitraires du code, au prix d’une moindre capacité à contenir les dégâts lorsqu’un problème survient. Je trouve que Rust est bien conçu sur ce point : assert!() panic toujours, debug_assert!() ne panic qu’en mode debug, et assert_unchecked() panic en debug et devient un indice d’optimisation en release

    • Si le rayon d’impact des erreurs vous inquiète, il faut utiliser ReleaseSafe plutôt que ReleaseFast
    • Je ne m’oppose pas à la désactivation de certains assert individuellement, mais à leur désactivation globale comme recommandation générale
      Il est tout à fait raisonnable de juger qu’un assert a un impact trop fort sur les performances pour être conservé en build de release. De plus, comme indiqué plus haut, un assert coûteux en calcul a très peu de chances d’apporter un gain de performance
      Il y a quelques exemples de ce type dans Zine :
      https://github.com/kristoff-it/zine/…
      https://github.com/kristoff-it/zine/…
      Zig n’a pas de « mode release par défaut ». Il faut toujours choisir explicitement comment traiter les assert, et les options globales sont le crash ou l’optimisation ; aucune des deux ne peut vraiment être considérée comme plus par défaut que l’autre
  • Le fait que les deux CVE relativement graves publiées jusqu’ici dans Ghostty aient toutes deux conduit à une exécution de commandes arbitraires sans corruption mémoire me paraît très étrange. Que cela se produise alors même que le logiciel était distribué en ReleaseFast contredit frontalement ma compréhension du fonctionnement du monde

    • Je ne trouve pas cela si étrange. Même si l’on croit les rapports disant que 70 % des vulnérabilités graves sont liées à la mémoire, cela vaut pour C et C++, et Zig peut être un peu meilleur sur la sûreté mémoire. En plus, avec un échantillon de 2 cas, il ne serait pas surprenant qu’environ un projet sur dix aboutisse à ce genre de résultat
      Pour avoir travaillé sur des émulateurs de terminal, ces vulnérabilités me paraissent être exactement le type de problèmes pénibles mais prévisibles qu’on rencontre. Ce n’est pas pour rabaisser les développeurs ou les chercheurs : ce genre d’injection de commandes dans un endroit inattendu est pratiquement un classique du domaine, un peu comme d’autres familles de vulnérabilités par injection dans d’autres secteurs
  • C’est amusant d’entendre depuis presque 40 ans qu’il faudrait désactiver assert et les contrôles de bornes en production « pour des raisons de performance ». Pendant ce temps, les ordinateurs sont devenus plus rapides de plusieurs ordres de grandeur et les logiciels se sont bien plus profondément intégrés dans la vie de tout le monde ; l’exactitude à l’exécution est donc plus importante que jamais
    Pour rendre la discussion plus productive, chez Microsoft autrefois, en plus des assert, check et autres mécanismes classiques, il existait une forme d’assert de reporting que j’ai rarement vue ailleurs. On l’utilisait lorsqu’une condition n’était pas entièrement sous notre contrôle, qu’on supposait vraie, qu’on gérait quand même de manière défensive le cas où elle serait fausse, mais qu’on voulait savoir via les logs ou la télémétrie si elle se produisait réellement en production. Par exemple, on peut supposer qu’un utilisateur ne mettra jamais plus de 1000 éléments dans une liste et utiliser donc un algorithme quadratique, ou supposer qu’une latence réseau restera sous 200 ms et choisir un protocole avec beaucoup d’allers-retours

    • En quoi est-ce différent d’un check ?
  • En tant que l’une des personnes mentionnées ici, je trouve que cela transforme ma position sur assert en une fausse alternative ridicule et caricaturale. Comme je l’ai aussi écrit dans un autre commentaire, je préfère décider au cas par cas, pour chaque assert, s’il faut ou non le convertir en comportement indéfini. Ma critique de ReleaseFast est qu’il lie ce choix non seulement à tous les assert d’une certaine portée, mais aussi à toutes les vérifications de sûreté
    Je suis d’accord avec kristoff quand il dit qu’il est absurde de désactiver des assert simplement parce qu’ils provoqueraient un crash s’ils ne sont pas corrigés. En revanche, je ne suis pas d’accord avec l’idée que les seules alternatives raisonnables soient « crash » ou « comportement indéfini ». La position de goldstein dans le commentaire voisin est, à mon avis, plus proche de la mienne

  • Il est difficile de défendre l’idée de faire du comportement de assert_unchecked() la valeur par défaut globale, mais cela peut être raisonnable comme technique d’optimisation des performances. Si convertir tous les assert en hypothèses accélère fortement un build de production, il est possible qu’une petite poignée d’hypothèses, voire idéalement une seule, soit responsable de l’essentiel du gain, et qu’on puisse la trouver par une méthode comme la recherche dichotomique

    • Il n’y a pas de valeur par défaut : l’utilisateur doit choisir explicitement entre ReleaseSafe et ReleaseFast/ReleaseSmall
  • Dans la littérature sur l’analyse de programmes, il existe une dualité qui sépare les assertions dans le code, ou assert, en deux formes. L’une concerne le contexte autour du code — pour une fonction, les conditions que l’appelant doit satisfaire — et l’autre concerne le code lui-même — pour une fonction, les conditions que la fonction doit satisfaire.
    Cette distinction devient claire si on l’examine à travers la notion académique standard de « responsabilité » (blame) dans la littérature sur les contrats et les types graduels. Si une assertion sur le contexte échoue, ce n’est pas notre faute : la responsabilité incombe au contexte ou à l’appelant ; mais il est aussi possible que l’appelant ait raison et que l’assertion elle-même soit boguée. Si une assertion sur le code lui-même échoue, c’est notre responsabilité ; mais là encore, il est possible que le code ait raison et que l’assertion elle-même soit boguée.
    Au niveau des fonctions, une précondition est une assertion sur le contexte, et une postcondition est une assertion sur le code lui-même. Cela dit, on peut placer les deux au milieu du code également. Certains frameworks de vérification utilisent assert pour les assertions sur le code, et assume pour les assertions sur le contexte. Cela rejoint aussi la manière dont certains frameworks de test, en particulier les frameworks de test aléatoire, interprètent ces notions. Si assert échoue, le test est marqué comme un échec ; si assume échoue, le test est ignoré.

    • BIND9 suit un style proche de la conception par contrat, où la macro REQUIRE() vérifie les préconditions que l’appelant doit satisfaire, et ENSURE() les postconditions que la fonction garantit. Il existe aussi INSIST() pour les vérifications intermédiaires, et INVARIANT() pour les boucles ou les structures de données. La documentation des fonctions doit inclure des notes « requires » et « ensures » correspondant aux préconditions et aux postconditions.
  • Cela semble faire allusion à Bun, donc j’aimerais rendre le lien un peu plus explicite. Il existe un ticket Zig de 2024 où Jarred Sumner, le créateur de Bun, a proposé que unreachable déclenche un panic en ReleaseFast. Les commentaires d’Andrew Kelley et de Matthew Lugg dans ce fil sont liés à cette discussion.
    => https://github.com/ziglang/zig/issues/19664
    Bun utilise ses propres fonctions assert, qui paniquent ou sont supprimées en mode release, mais n’introduisent pas de comportement indéfini. Cela dit, il faut aussi garder en tête la note de bas de page de Loris : « en tant que langage, Zig ajoute implicitement au code de nombreuses assertions impossibles à désactiver ».
    Je ne veux pas trop m’étendre sur Bun, car il s’agit d’un projet unique porté par une petite équipe. L’idée essentielle, c’est que s’il y a le moindre doute, il faut utiliser ReleaseSafe. ReleaseSafe a la réputation d’être lent, mais sur mes petits projets Zig, je n’ai pas réussi à mesurer de différence en benchmark entre ReleaseSafe et ReleaseFast. Il y a malgré tout de fortes chances que ce soit encore plus rapide que beaucoup d’autres langages.

    • Dire qu’il faut utiliser ReleaseSafe au moindre doute est juste. On peut aussi imaginer des stratégies plus intéressantes. Pendant qu’on modifie le code — donc tant qu’on risque d’introduire des bugs — on peut rester en ReleaseSafe, puis basculer vers ReleaseFast une fois le code stabilisé, éprouvé en conditions réelles, et si le gain de performance en vaut la peine.
      Ou bien, si cela a du sens dans le contexte, on peut distribuer un exécutable ReleaseFast, puis revenir à ReleaseSafe quand des rapports de bugs non déterministes commencent à arriver à cause d’un comportement indéfini. Cela permet alors de recueillir des rapports de bugs exploitables indiquant quelle assert a échoué, ainsi que des accès hors limites, des débordements, etc., puis de corriger le code. J’irais jusqu’à recommander cette approche même si la décision initiale de livrer en ReleaseFast a été prise dans un contexte où il ne fallait pas le faire :^)
      On peut aussi faire la même chose sur certaines parties du projet seulement, en ajustant les dépendances et en utilisant @setRuntimeSafety. Au fond, tous les outils nécessaires sont là, à condition d’avoir la volonté de s’en servir intelligemment.
  • Il ne faut pas écrire comme si on pouvait mettre des expressions avec effets de bord dans un appel à assert. C’est une mauvaise pratique. Il faut aussi éviter d’utiliser assert pour vérifier des erreurs. Pour être juste, je n’ai pas l’impression que l’auteur défende cela.
    À l’inverse, il est aussi expliqué que si assert dépend d’un calcul complexe, ce calcul n’est pas forcément éliminé en mode unchecked ; il faut donc le protéger avec comptime if.
    J’espère que l’auteur n’a pas raté l’ironie de la formule « une bonne occasion de se débarrasser du traumatisme laissé par les macros et d’embrasser la simplicité ». Cela revient à inviter à adopter « la simplicité » qui consiste à tenir compte du mode de build du programme et à parsemer partout des comptime if défensifs.

    • Pourquoi est-ce une mauvaise pratique ?
  • J’écris un peu de code de calcul numérique en C#, et j’utilise beaucoup d’assert qui sont désactivés en release. C’est trop coûteux pour être exécuté dans chaque boucle serrée, mais dans les tests unitaires, il est utile que l’exécution s’arrête immédiatement dès qu’une routine rencontre pour la première fois une entrée NaN.
    Ces NaN viennent souvent moins d’une entrée utilisateur que de bugs dans le code — par exemple quand l’optimiseur passe là où il ne devrait pas — et montrent qu’il faut de meilleures contraintes de frontière. Bien sûr, l’entrée utilisateur peut aussi nécessiter une validation, mais cela doit se faire à la frontière la plus externe, pas au plus profond de l’algorithme. Ce serait bien d’avoir un système de preuve permettant de démontrer, à partir de la validation des entrées utilisateur, les invariants internes de l’algorithme sans assert, mais c’est un projet annexe et personne ne mourra s’il plante.

  • 90 % des désaccords sur assert viennent du fait que la définition du mot est faible et multiple, ce qui brouille la réflexion et la communication. Il faut donc séparer le concept en trois noms distincts et les employer rigoureusement.
    assert(bool) — ou en Rust, assert_unchecked() — désigne quelque chose que le programmeur croit toujours vrai, et que le compilateur suppose également toujours vrai pour l’utiliser dans ses optimisations. Pour éviter l’association avec les anciens assert de vérification dans les vieux langages, il vaudrait peut-être mieux appeler cela assume().
    check(bool) panique si la condition est fausse et continue si elle est vraie ; c’est toujours son comportement.
    debug_check(bool) se comporte comme check() en mode debug, et continue toujours en mode release. En pratique, ce comportement est contrôlé par un flag --debug_checks, activé par défaut en mode debug.
    Il faudrait aussi un flag compilateur --check_asserts qui transforme assert() en check(). On l’utiliserait lorsqu’on soupçonne ses propres assert et qu’on veut les vérifier, et il serait activé par défaut en mode debug. Tant qu’on n’est pas extrêmement clair sur ce qu’on entend par « assert », une discussion mature est impossible et on ne fait que gaspiller des mots.