Pertinence du développement système avec Hare

Prérequis

  • Langage C
  • Syntaxe Hare non nécessaire

Introduction

Si vous avez déjà utilisé un langage système comme le C, le C++ ou le Rust, vous savez qu’un grand pouvoir implique de grandes responsabilités. Les langages systèmes offrent une capacité d’intéraction direct avec le matériel et des gains de performances. Cependant, le développement bas-niveau manque encore de facilité et de sureté. C’est en partant de ces constats que Drew Devault s’est lancé dans le développement du Hare. Comment l’Hare permet d’allier bas-niveau et aisance de développement ? Dans cet article, je détaille uniquement le positionnement du Hare par rapport au C en exposant ses mécanismes de sureté et de facilité d’utilisation.

Fiabilité

Le langage C offre énormément de liberté mais il est facile de faire des erreurs. Hare apporte des garantis sur certains undefined behaviours observables en C.

L’assignation des variables

Comme en C, Hare ne permet pas d’utiliser une variable tant qu’elle n’est pas assignée. L’Hare oblige l’assignation de l’ensemble des variables déclarées. Cependant, ici, le code échoue à la compilation. En C, il faudra attendre non seulement l’exécution, mais aussi l’utilisation de cette variable pendant l’exécution et la lecture.

En Hare :

export fn main() void = {
    let x: int;  //expected '='
};

Sortie : unexpected ';' at /home/user/main.ha:2:15, expected '='

Équivalent en C :

#include <stdio.h>

int main()
{
    int *x;
    *x = *x;  //Segfault
}

Néanmoins si vous avez l’habitude d’utiliser des arbres, des listes chaînées … il peut être nécessaire pour d’utiliser les NULL. C’est pourquoi, pour éviter les segfaults, les valeurs ne peuvent être NULL par défaut.

export fn main() void = {
	let x = "toto";
	let y: *str = &x;            //Garantie non-null
	let z: nullable *str = y;    //Possiblement null

	*y;
	*z;

	match (z) {
	case null =>
		abort();
	case ...
	};
};

Le code ci-dessus échoue a la ligne 7. Il est nécessaire, avant d’utiliser z, de s’assurer qu’il peut l’être. C’est ce que je fais, ligne 9.

Moins d'undefined behaviour

Au-delà d’obligations, pour éviter les undefined behaviour, l’Hare défini lui-même certains comportements que le C ne définit pas.

Sous couvert d’optimisation les compilateurs C ont une fâcheuse tendance à faire ce qu’ils veulent. Cela se place en opposition totale avec la philosophie du Hare. Le Hare se veut constant et choisit donc toujours les mêmes instructions assembleur, pour les mêmes comportements.

Un cas d’exemple de ces comportements non prédictibles est celui des octets en C. Effectivement, en C, un octet ne vaut pas toujours 8 bits. Il est nécessaire d’utiliser un uint8_t pour garantir sa taille. En Hare, un octet vaudra toujours 8 bits.

En C, un shift supérieur à la largeur de votre valeur entraine une perte de bits, en Hare, ce problème n’existe pas.

Également, l’overflow et l’underflow sont définis. Dans le cadre de l’overflow d’un nombre :

  • Entier, celui-ci est tronqué vers les bits les moins significatifs (LSB)
  • Flottant, celui-ci est tronqué vers zéro.
  • Signé, la troncature entraîne le changement du bit de signe.

Aisance de développement

Contrairement au C, l’Hare apporte un grand nombre d’abstractions pour faciliter le développement. Celles-ci permettent majoritairement de travailler des groupes d’éléments afin de repeter l’écriture de boucle.

L’opérateur ...

L’opérateur ... permet d’initialiser un ensemble de valeurs d’une variable.

Dans le cas ci-dessous, y est défini à la valeur par défaut d’un int, soit 0. Nul besoin prendre le temps de définir y tant que l’on ne s’en sert pas.

use fmt;

type coords = struct { x: int, y: int };

export fn main() void = {
    let A = coords { x = 1337, ... };
	fmt::printfln("({}, {})", A.x, A.y)!;
};

Sortie : (1337, 0)

Dans ce nouvel exemple, l’opérateur ... nous permet cette fois-ci d’assigner l’ensemble de cases de notre tableau, à la valeur 3.

use fmt;

export fn main() void = {
    let x: [4096] int = [3...];
	fmt::printfln("{}", x[6])!;
};

Sortie : 3

Les slices

Si vous voulez changer une portion d’un tableau en C, vous êtes contraint d’écrire une boucle. À l’instar de Python ou de Golang, Hare propose des slices pour éviter cette écriture longue et fastidieuse.

Ce code modifie le tableau y initialiser à 0. Entre les indices 6 (compris) et 9 (non compris).

use fmt;

export fn main() void = {
    let x: [4096] int = [3...];
	x[7] = 9;
	let y: [16] int = [0...];
	y[6..9] = x[6..9];
	fmt::printfln("{}, {}, {}, {}", y[6], y[7], y[8], y[9])!;
};

Sortie : 3, 9, 3, 0

Les tableaux sans limites

Le C n’offre pas de mécanisme simple de tableau sans longueur prédéfini. Vous êtes contraint de créer vos propres vecteurs pour reproduire ce comportement.

Hare offre cette possibilité avec l’opérateur [*]. Dans l’exemple, en observe que j’accède à l’indice 43 de mon tableau dont je n’ai pas défini la longueur.

use fmt;

export fn main() void = {
    let x = [1, 2, 3];
    let y = &x: *[*]int;
    y[42] = 1337;
    y[43];
};

Avertissement : cette pratique n’est pas sûre si l’indice est excessivement grand. Cela peut provoqué un segfault. Mais elle reste pratique pour de petits tests non utilisés en production.

Les formats de string

Le formatage de caractère en C est assez rébarbatif. Vous devez à chaque fois définir le format de votre chaine de caractère et surtout vous rappeler des caractères correspondant à la variable que vous voulez afficher (%s, %d, %lu …).

Pour éviter cela, Hare intègre au type passé à la fonction de formatage de str, la manière de traiter la variable a utilisé.

use fmt;

export fn main() void = {
    let x = 12;
    let c = 'Z';
    let txt = "Hello";
    let y = 3.14;

    fmt::printfln("{} | {} | {} | {}", x, c, txt, y)!;
};

Typage multiple de fonction

La gestion d’erreur en C est assez complexe et le langage manque de constance. En effet, des fois il faut lire errno, d’autres fois évaluer le retour de notre fonction.

Pour faciliter la gestion d’erreur Hare permet aux fonctions de retourner plusieurs types. Dans l’exemple, la fonction fun() peut retourner soit un int, soit un str.

use fmt;

fn fun(boo: bool) (int | str) = {
	let ret = if (boo) {
		yield "toto";
	} else {
		yield 42;
	};
	return ret;
};

export fn main() void = {
	let r1 = fun(true);
	fmt::printfln("{}", r1)!;
	let r2 = fun(false);
	fmt::printfln("{}", r2)!;
};

Indication : Le mot clé yield indique le retour du scope

Les apports des string

Hare cherche à optimiser les performances des strings avec 3 points :

  • Pour faciliter l’usage du texte sur différentes machines, Hare a choisi pour ces chaines de caractères le format UTF-8 et pour ces caractères le format UCS-32. A l’inverse du C qui utilise l’ASCII.
  • Dans un souci d’optimisation, chaque str stock sa longueur.
  • Le charactère null est autorisé à l’intérieur d’un str.

Conclusion

Hare est un langage système cherchant à apporter, vis-à-vis du C, de la fiabilité et une facilité d’écriture tout en gardant la même capacité de contrôle. Hare n’as pas pour but d’apporter de nouveaux paradigmes de programmation mais cherche à concilier bas-niveau, sureté et aisance de développement. On pourrait croire qu’avec ces contraintes, Hare, s’oppose au principe de liberté d’un langage système. Cependant si vous allez plus loin, vous vous apercevrez vite que les sécurités du langage peuvent être outrepassées pour garder cette liberté. Néanmoins, et comme souvent évoqué dans la documentation, attention à ne pas vous tirer une balle dans le pied.

Sources :