Ante : une nouvelle approche combinant vérification des emprunts et comptage de références
(verdagon.dev)- Ante est une conception de langage système qui cherche à combiner la flexibilité du comptage de références et la sûreté de la vérification des emprunts, tout en évitant les paniques à l’exécution à la Rust ou le surcoût des vérifications d’accès exclusif à la Swift
- Le mécanisme central repose sur la shape-stability et la temporary uniq conversion : il permet de créer en sûreté des emprunts mutables sur les champs de valeurs à comptage de références, et de traiter les valeurs internes des unions comme
uniqseulement dans une portée limitée - En Rust,
Rc<RefCell<T>>peut provoquer une panique à l’exécution en cas de mauvaise utilisation, et le système de borrowing de Swift inclut des vérifications d’accès exclusif à l’exécution ; Ante tente de traiter certains de ces cas par des règles à la compilation - Il s’agit encore d’une conception work-in-progress, partiellement implémentée ; comme elle doit analyser récursivement les types pour déterminer si un objet donné est atteignable, l’ajout d’un champ peut devenir un breaking API change
- Cette approche affaiblit l’hypothèse selon laquelle le shared mutable borrowing serait toujours impossible et, avec des techniques comme Vale, group borrowing et Rust GhostCell, élargit le champ des exceptions dans la conception de la sûreté mémoire
La combinaison visée par Ante
- Ante est un langage de programmation système qui vise à être un Rust plus simple, avec sûreté mémoire et sûreté des threads
- Son modèle de base repose sur la propriété unique et la vérification des emprunts ; les valeurs sont placées inline sur la pile ou dans la structure ou le tableau qui les contient
- Quand on veut privilégier la simplicité, on peut choisir le comptage de références en ajoutant le mot-clé
sharedau type - Avec
shared type Coloretshared type RbTree t, la fonctionbalanced’un arbre red-black est aussi courte que l’exemple Python, et plus petite que les exemples C++ et Rust - La question centrale est de savoir comment emprunter de façon mutable des données à comptage de références sans risque de panique de
borrow_mut()comme en Rust, ni vérification d’accès exclusif à l’exécution comme en Swift - Ante est encore dans un état work-in-progress : une partie est implémentée, une autre est théorique, et la conception évolue encore
- L’avancement est consultable sur le site d’Ante et sur Discord
Shape-stability et références mutables multiples
- La shape-stability d’Ante est l’idée selon laquelle « une référence vers une cible ayant une shape stable reste toujours valide, quels que soient les changements effectués ailleurs »
- Grâce à ce concept, il est possible d’avoir simultanément plusieurs références d’emprunt mutable vers la même structure
- Dans l’exemple
heal (healer: mut Entity) (target: mut Entity), il est possible d’appelerself_healen passant la mêmeEntityaux deux arguments pour qu’elle se soigne elle-même- Même si
healerettargetpointent vers la mêmeEntity, ce code ne peut pas détruire l’Entity, donc les deux références restent valides
- Même si
- Les références mutables vers la structure elle-même, ses champs et les champs de ses champs peuvent aussi être autorisées simultanément
- Même en utilisant simultanément
ship: mut Spaceshipetengine_alias: mut Engine = ship.engine, Ante considère queshipet sonenginene seront pas détruits pendant l’exécution de la fonction
- Même en utilisant simultanément
- Rust et Swift n’autorisent pas une forme où plusieurs références
&mutpointent simultanément vers les mêmes données
Emprunt mutable des champs d’une valeur à comptage de références
- Dans Ante, ajouter
shareddevant une définition de type rend automatiquement ce type à comptage de références - Dans l’exemple
shared mut type Spaceship,launchpossède unSpaceshipcorrespondant à unRcet passemut ship.engineàset_fuel - Comme
launchconserve l’objet conteneurSpaceship, on peut considérer que son champenginereste lui aussi vivant - La règle générale est qu’il est toujours possible de créer une référence d’emprunt
mutvers les champs d’un typeshared mut- En revanche, cela ne signifie pas que l’on puisse toujours créer un emprunt mutable vers tous les objets situés à l’intérieur de ce champ ; des règles séparées sont nécessaires
- Les exemples suivants utilisent la notation plus explicite
Rc Spaceshipau lieu du sucre syntaxiqueshared mut type Spaceshipshared mut type Spaceshipdevienttype Spaceship, etvar ship: Spaceshipdevientvar ship: Rc Spaceship
Là où les unions posent un problème de sûreté
- Les unions peuvent être avantageuses pour la vitesse, car elles stockent leur contenu inline et réduisent les suivis de pointeurs ainsi que les défauts de cache
- Si l’
union Enginede C est incluse dans unestruct Spaceship,StringTheoryEngineetImpulseEnginese trouvent dans la mémoire deSpaceship - Cela contraste avec une approche à la Java utilisant interfaces et pointeurs
- Si l’
- Le problème est qu’il est difficile de prendre en charge les unions de façon sûre dans un langage memory-safe
- Dans l’exemple où
Engineest soitStringTheoryEngine(str: String), soitImpulseEngine(fuel: I32), un segfault peut se produire lorsqueshipetother_shippointent vers le mêmeSpaceship- Après avoir capturé une référence interne à la chaîne avec
match uniq ship.engine - Puis remplacé le même moteur par une autre variante via
other_ship.engine := ImpulseEngine 0x42 - Modifier ensuite l’ancien
strrevient à utiliser l’intérieur d’un conteneur après sa destruction
- Après avoir capturé une référence interne à la chaîne avec
- Ante doit donc empêcher la création d’une référence d’emprunt mutable vers l’une des variantes lorsqu’une référence d’emprunt mutable pointe vers une union
- C’est l’inverse de la règle des structures
- Avec une référence
mutvers une structure, on peut créer une référencemutvers un champ - Avec une référence
mutvers une union, on ne peut pas créer de référencemutvers l’intérieur d’une variante
- Avec une référence
uniq et temporary uniq conversion
uniqdésigne une exclusive mutable reference, c’est-à-dire une référence mutable exclusive- Si une variable contient
uniq Spaceship, il s’agit de la seule référence utilisable vers ceSpaceship- C’est un concept similaire à
&mut Spaceshipen Rust
- C’est un concept similaire à
- Pour manipuler en sûreté l’intérieur des unions, Ante utilise la temporary uniq conversion
- La règle centrale est que l’on peut obtenir temporairement une référence
uniqdans une certaine portée si l’on n’utilise pas d’autres références susceptibles d’être des alias- Dans la section
match uniq ship.engine,ship.engineest accessible comme ununiq - Pendant cette section, le compilateur empêche l’utilisation d’autres variables existantes pouvant contenir indirectement un
Spaceship
- Dans la section
- Rust empêche l’existence même d’un
uniqau motif que « d’autres références peuvent exister quelque part », tandis qu’Ante autoriseuniqà condition de ne pas utiliser ces références dans la portée concernée - Ici,
uniq Spaceshipn’est pas réellement une référence globalement unique, mais la seule référence utilisable dans cette portée- La nuance est proche de celle des pointeurs
restricten C
- La nuance est proche de celle des pointeurs
Accès autorisés et accès rejetés
- Accéder à
other_ship: Rc Spaceshipdans la portée dematch uniq ship.enginedoit provoquer une erreur de compilationother_ship.enginepeut être un alias deship.engine- et modifier
other_ship.enginependant l’utilisation deship.enginepeut entraîner un drop
- Une autre structure possédant un champ
Rc Spaceship, commeHasAShip, est rejetée pour la même raisonother.ship.enginepeut lui aussi atteindre indirectement le mêmeSpaceship
- À l’inverse, un entier comme
new_fuel: I32peut être utilisé- car
I32ne peut pas contenir de référence versSpaceship
- car
- Si
Spaceshipcontient lui-même un champ commefollow_ship: Rc Spaceship, c’est rejeté- Dans ce cas,
uniq Spaceshipredevient atteignable via un chemin interne, donc, en général, une conversionmut -> uniqn’est pas possible pour les types récursifs
- Dans ce cas,
Contraintes sur les appels de fonctions et les retours
- La conversion
mut -> uniqpeut aussi se produire lors d’un appel de fonction - Lorsque
foo (var ship: Rc Spaceship) (new_res: Resonator)appellemaybe_use_resonator ship new_res,shipest converti enuniq Spaceshipau point d’appel- Le compilateur doit seulement vérifier si les autres arguments peuvent contenir une référence à
Spaceship - Dans l’exemple,
Resonatorne contient pas une telle référence, donc l’appel est autorisé
- Le compilateur doit seulement vérifier si les autres arguments peuvent contenir une référence à
- Au retour, une référence
uniqconvertie ne peut pas être renvoyée comme ununiqnormal- Après le retour, le compilateur ne peut plus appliquer la vérification « ne pas utiliser de variables pouvant être des alias dans la portée »
- À la place, on peut définir le type de retour comme
local uniq Foo- En interne, lorsqu’une
mut refest convertie enuniq ref, c’est en réalité toujours un local uniq qui est créé - Dans la plupart des cas, il peut être utilisé comme un
uniqnormal, mais au retour, il faut l’indiquer explicitement
- En interne, lorsqu’une
Coûts de conception et alternatives
- Ante peut transformer une référence à comptage de références comme
Rc Spaceshipenuniq Spaceshiptemporaire sans erreur à l’exécution - L’inconvénient est que le compilateur doit examiner récursivement les types pour répondre à des questions comme « peut-on atteindre
SpaceshipdepuisEngine? » - Ce type d’analyse peut être fragile
- Ajouter un champ à une structure peut devenir un breaking API change
- Jake, le créateur d’Ante, cherche une meilleure façon de conserver cette garantie
- Une approche consistant à associer à chaque type mutable partagé un type de marque anonyme unique, comme dans group borrowing et Flix references
- Une approche consistant à ajouter un effet comme
Mutates 'alors de la modification d’un type partagé, afin de supprimer l’analyse des types - Une approche où l’utilisateur vérifie à l’exécution que deux références pointent vers des objets différents, ou où un contrôle unsafe est encapsulé dans une API safe
- Une approche où le compilateur suit les valeurs qui ne sont pas stockées indirectement dans un
Rcet ne peuvent donc pas être aliasées
- Des idées proches de l’iso permission de Pony, ou des permissions temporaires permettant de regarder à l’intérieur d’une structure tout en empêchant l’utilisation de références vers l’extérieur, restent aussi possibles
- La difficulté consiste à conserver cette flexibilité tout en préservant les objectifs d’Ante : utilisabilité, lisibilité et simplicité
Un mouvement plus large dans la sûreté mémoire
- Le shared mutable borrowing était autrefois considéré comme impossible, et le point de vue sous-jacent est que Rust a aussi été conçu sur cette croyance
- Plusieurs exceptions s’accumulent
- Ante peut obtenir des références d’emprunt
uniqà partir de données shared-mutable grâce à des règles de local uniqueness - Vale peut obtenir des références d’emprunt immuables à partir de données shared-mutable via des pure functions
- group borrowing peut créer des références d’emprunt shared-mutable même sans shape-stability
- Le GhostCell de Rust permet à un graphe d’objets de se référencer librement, mais à un instant donné il ne peut y avoir qu’une seule référence mutable vers l’un d’eux
- Ante peut obtenir des références d’emprunt
- Ce mouvement suggère qu’il pourrait exister des principes plus généraux pour traiter le shared mutable borrowing dans la conception de la sûreté mémoire
Comparaison avec Cell en Rust
- Les utilisateurs de Rust peuvent se demander quelle est la différence entre l’approche d’Ante et le fait de placer un
Celldans un champ de structure - Dans l’exemple d’Ante, on peut obtenir une référence
mut Stringversstatus: StringdepuisRc Spaceshipet y ajouter directement" (refueling)" - Avec l’approche
Cell<String>de Rust, on ne peut pas obtenir de&mut StringdepuisRc<Spaceship>- À la place, il faut insérer temporairement une valeur par défaut avec
status_ref.replace(String::new()) - modifier le
Stringextrait - puis le remettre à la fin avec
replace(status)
- À la place, il faut insérer temporairement une valeur par défaut avec
- Cette méthode présente plusieurs inconvénients
- Il faut créer une instance par défaut, comme
"" - Il existe un risque d’oublier l’appel final à
replace - Quelqu’un peut lire
statuspendant que la valeur a été remplacée
- Il faut créer une instance par défaut, comme
- Ante permet d’obtenir temporairement une référence vers la chaîne
status, et le compilateur garantit que les autres codes ne peuvent pas y accéder pendant ce temps
1 commentaires
Avis sur Lobste.rs
Le fait de considérer les « emprunts mutables partagés » comme impossibles n’était pas simplement un sacrifice consenti par Rust pour atteindre son objectif, mais quelque chose de proche de l’objectif central de Rust lui-même.
En effet, l’état mutable partagé rend difficile le raisonnement local sur le code.
« References are like jumps » par withoutboats traite bien ce point. L’idée est qu’empêcher la modification accidentelle d’un état avec alias est essentiel pour faciliter la création de systèmes qui fonctionnent correctement, et que les règles de durée de vie de Rust ne sont pas seulement un mécanisme destiné à éviter le ramasse-miettes, mais une structure plus profonde qui garantit la possibilité de raisonner dans un langage autorisant à la fois état mutable et état avec alias.
Ça a l’air plutôt pas mal.
Si j’ai bien compris, la magie qui permet de passer d’une référence partagée à une référence mutable est possible parce qu’elle se limite aux types qui ne sont pas partagés entre threads, et l’unicité de
Rcsemble garantie en traitant tous les objets du même type comme s’ils étaient empruntés avec la même durée de vie.Que l’on préfère une syntaxe explicite ou une syntaxe plus naturelle est peut-être affaire de goût, mais cela montre que si le compilateur en sait davantage sur
Cell, il peut autoriser plus souplement les références mutables vers celle-ci.Et cela évite aussi la terminologie déroutante de Rust, où
mutest utilisé comme s’il signifiait non pas mutable, mais exclusif/unique.uniqimplique-t-elle l’acquisition d’un verrou ? », mais je comprends que le point de comparaison estRcet nonArc.mutsignifie exclusif/unique ?Je me demande si quelqu’un devine ce que pourrait être le principe unificateur suggéré à la fin.
Les discussions précédentes sur les billets du blog antelang.org méritent aussi d’être consultées.
Je ne comprends pas bien comment cela fonctionne. On dirait que cela signifie : « si l’on a un pointeur mutable vers un objet, on peut obtenir une référence mutable vers une tranche de cet objet ».
Mais dans ce cas, par exemple, il semble possible d’écrire quelque chose comme
mutref someobjext = …,mutref subfield = someobjext.a.b,someobjext.a = somethingelse, ce qui pourrait invalidersubfieldou le casser en modifiant sa valeur.L’article contenait beaucoup d’explications, de comparaisons avec d’autres langages et d’exemples de code, mais j’ai eu du mal à trouver une présentation de base de la sémantique étape par étape de ce comportement.