janvier
2009
Dans mon précédent billet j’exposais quelques motivations et quelques objectifs de conception pour un langage natif moderne.
Dans ce billet je discute certaines conséquences de la notion de types non-annulables.
Des conséquences non évoquées dans le document d’origine.
Une chose après l’autre
Il y a principalement deux aspects pseudo-innovants dans ma conception de base.
Je dis pseudo parce qu’en cherchant bien on trouvera des langages qui implantent l’un ou les deux aspects en question, je dis innovants parce qu’en pratique je ne retrouve ces deux caractéristiques dans aucun langage impératif.
Ces deux aspects ce sont :
- les types non-annulables
- les classes sous forme d’enregistrement dont les champs sont des fonctions
Ces deux aspects sont typiques de langages de très haut niveau et viennent même d’horizons opposés :
- les types non-annulables sont réservés aux langages à très fort typage statique, le plus souvent ce sont des stratégies de typage à la Hindley-Milner qui autorisent ce genre de sécurité
- au contraire les enregistrement de fonctions sont typiques de langages très dynamiques à la Self
Alors pourquoi intégrer ces deux aspects dans un langage de bas niveau ?
Tout simplement parce que :
- les types non-annulables ne sont pas contradictoires avec des manipulations de bas niveau, au contraire ils les permettent tout les sécurisant davantage et ça correspond bien à l’objectif premier du langage Lombric
- les enregistrements de fonctions sont un bon moyen de réconcilier le style procédural et le style POO, c’est un pas important vers le second de mes objectifs affichés
Cependant, ayant renoncé dans l’immédiat à pousser plus loin la conception de l’aspect POO, tout naturellement mon attention se reporte sur la bonne réalisation des types non-annulables.
Qu’est-ce qu’un type non-annulable ?
Un type non-annulable est un type pointeur dont les valeurs ne vaudront jamais null.
Concrètement, si une valeur est non-annulable alors on ne peut pas lui affecter la valeur null.
Bien sûr il est possible de transtyper une valeur annulable en valeur non-annulable, et ceci à l’aide d’un opérateur de conversion on ne peut plus discret, car la sécurité ne doit pas aller à l’encontre de la productivité.
Mais autrement, aucune opération de déréférencement n’est possible sur une valeur annulable.
J’insiste sur cet aspect du langage car il me paraît fondamental. On ne peut pas affirmer qu’un langage est fortement typé et accepter qu’il échoue dynamiquement parce qu’un certain pointeur ne désigne aucune valeur. Un typage statique fort c’est précisément garantir que l’exécution n’échouera pas pour des raisons aussi banales, et bien que Lombric ne soit pas à l’abri de ce genre d’erreurs (après tout ça n’est qu’un langage de bas niveau) il offre néanmoins assez d’outils pour en limiter le nombre. Faire autrement serait irresponsable. Bien sûr comme c’est un langage de bas niveau il ne va pas jusqu’à prohiber les types annulables, il laisse le programmeur libre du choix de ses armes.
L’exemple qui suit vous convaincra sans doute que cette sécurité n’est ni trop zélée ni trop spécifique à la POO.
On a déclaré deux variables locales :
- l’une est une variable entière n
- l’autre est une fonction f des entiers vers les entiers
LOCAL n:LONG LOCAL f:(LONG)(LONG) n := f(n)
Ce code est-il bien typée ?
La réponse est non.
Ce code n’est pas bien typé parce que les types de fonctions sont non-annulables.
Et d’ailleurs si ce code était bien typé quelle pourrait bien être la valeur de f ?
Probablement aucune valeur qui soit valide pour l’appel de f(n).
Par contre, étant donné une fonction neg appropriée, le code suivant serait tout à fait valide :
LOCAL n:LONG LOCAL f := neg n := f(n)
Voilà qui nous amène à une première question sur les valeurs non-annulables: comment bien les déclarer ?
Types non-annulables et déclarations locales
Le bénéfice évident des types non-annulables c’est que (à l’extrême, si on n’utilise aucun type annulable) on a plus aucune null-pointer-exception.
Cependant il ne faut pas se leurrer, la sûreté ne tombera pas sur vous comme un don du ciel, ce n’est pas le langage qui supprime les erreurs, il vous permet seulement de lui intimer l’interdiction pour vous de les insérer.
Il vous appartient donc, si vous avez fait ce choix, de prouver au compilateur que vous n’annulez jamais vos pointeurs.
En terme d’utilisation cela se traduit par les deux interdits suivants :
- vous ne pouvez pas affecter la valeur null à une valeur non-annulable
- vous ne pouvez pas affecter une valeur annulable à une valeur non-annulable
Le premier point n’étant bien sûr qu’un cas particulier du deuxième.
En terme de création cela se traduit par les deux possibilités suivantes :
- l’expression NEW un_type renvoie une nouvelle valeur non-annulable de type un_type*
- si n est une expression de type non-annulable alors LOCAL m := n déclare une nouvelle variable locale m de même type que l’expression n
À ce propos, le document actuellement en ligne donne à penser qu’une déclaration LOCAL ne peut se faire que dans un module ou en-tête du corps d’une fonction.
Ce qui motivait initialement ce choix d’interdire LOCAL au milieu des corps de fonction c’était la possibilité envisagée d’une extension de NEW autorisant l’allocation dynamique dans la pile.
Dans l’idée de cette extension, l’expression suivante :
NEW LONG COUNT size STACK
aurait pour effet d’allouer un tableau d’entiers de taille (dynamique) size dans la fenêtre de pile.
Cette extension est envisagebale, cependant elle n’est pas compatible avec des déclarations LOCAL au milieu des corps de fonction car ces déclarations allouent elles aussi dans la fenêtre de pile.
Or, à l’usage ou en se projetant l’usage, il est facile de se convaincre qu’il n’est pas toujours facile de connaître la valeur d’une donnée non-annulable en entrée de la fonction, parfois la valeur adéquate dépendra d’une condition ou d’un calcul.
De sorte qu’il est souhaitable d’autoriser LOCAL au milieu des corps de fonction.
En conséquence l’implantation de NEW sur la pile exigera des précautions d’usage supplémentaires qui restent encore à définir. En particulier, dans le flot d’exécution d’une même fonction, une déclaration LOCALE ne devra jamais être précédée par une allocation NEW STACK.
Types non-annulables et résultat d’une fonction
Que faire d’une fonction qui doit renvoyer une valeur non-annulable mais qui ne contient pas d’instruction RETURN ?
Ou alors qui contient une instruction RETURN mais pas pour tous les flots d’exécution possibles.
À l’évidence une telle fonction n’est pas valide car il n’est pas question de renvoyer une valeur par défaut comme null par exemple.
Alors comment valider les fonctions qui renvoient une valeur non-annulable ?
Voici un schéma simple, qui répond à cette question sans avoir à faire une analyse complexe du flot d’exécution :
- soit la fonction se termine par une instruction LOOP qui ne contient pas de BREAK, dans ce cas elle remplira très bien son contrat puisqu’elle ne peut pas en sortir autrement que par un RETURN
- sinon la fonction devra se terminer par une instruction RETURN