Rust: les portées d'emprunt ne doivent pas toujours être lexicales

Créé le 10 mai 2013  ·  44Commentaires  ·  Source: rust-lang/rust

Si vous empruntez de manière immuable dans un test if , l'emprunt dure toute l'expression if . Cela signifie que les emprunts modifiables dans les clauses entraîneront l'échec du vérificateur d'emprunt.

Cela peut également se produire lors d'un emprunt dans l'expression de correspondance et lorsque vous avez besoin d'un emprunt mutable dans l'un des bras.

Voir ici pour un exemple où le if emprunte des boîtes, ce qui fait geler le @mut vers le haut le plus proche. Puis remove_child() qui a besoin d'emprunter des conflits mutables.

https://github.com/mozilla/servo/blob/master/src/servo/layout/box_builder.rs#L387 -L411

Exemple mis à jour de @Wyverald

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
A-borrow-checker NLL-fixed-by-NLL

Commentaire le plus utile

Ce n'est pas encore sorti tous les soirs, mais je veux juste dire que ceci compile maintenant :

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

Tous les 44 commentaires

nomination pour la production prête

J'appellerais cela soit bien défini, soit rétrocompatible.

Le code s'est réorganisé, mais voici l'apaisement spécifique du vérificateur d'emprunt que j'ai dû faire :

https://github.com/metajack/servo/commit/5324cabbf8757fa68b1aa36548b992041be94ef9

https://github.com/metajack/servo/commit/7234635aa580c8a821003882e77d8e043d247687

Après quelques discussions, il est clair que le vrai problème ici est que le vérificateur d'emprunt ne fait aucun effort pour suivre les alias, mais s'appuie plutôt toujours sur le système de région pour déterminer quand un emprunt sort de la portée. Je suis réticent à changer cela, du moins pas à court terme, car il y a un certain nombre d'autres problèmes en suspens que j'aimerais aborder en premier et tout changement serait une modification importante du vérificateur d'emprunt. Voir le numéro 6613 pour un autre exemple connexe et une explication quelque peu détaillée.

Je me demande si nous pourrions améliorer les messages d'erreur pour rendre plus clair ce qui se passe ? Les portées lexicales sont relativement faciles à comprendre, mais dans les exemples de ce problème que j'ai rencontrés, ce qui se passait n'était en aucun cas évident.

juste un bug, suppression d'un jalon/nomination.

accepté pour un jalon dans un avenir lointain

bosse de triage

J'ai eu quelques réflexions sur la meilleure façon de résoudre ce problème. Mon plan de base est que nous aurions une idée du moment où une valeur "s'échappe". Il faudra du travail pour formaliser cette notion. Fondamentalement, lorsqu'un pointeur emprunté est créé, nous suivrons ensuite s'il s'est échappé. Lorsque le pointeur est mort , s'il ne s'est pas échappé, cela peut être considéré comme un kill de l'emprunt. Cette idée de base couvre des cas tels que "let p = &...; use-pa-bit-but-ever-again; expect-loan-to-expired-here;" Une partie de l'analyse sera une règle qui indique quand une valeur de retour qui contient un pointeur emprunté peut être considérée comme ne s'étant pas encore échappée. Cela couvrirait des cas comme "match table.find(...) { ... None => { expect-table-not-to-be-loaned-here; } }"

La partie la plus intéressante de tout cela est bien sûr les règles d'évasion. Je pense que les règles devraient prendre en compte la définition formelle de la fonction, et en particulier tirer parti de la connaissance que nous donnent les durées de vie liées. Par exemple, la plupart des analyses d'échappement considéreraient un pointeur p pour s'échapper s'ils voient un appel comme foo(p) . Mais nous n'aurions pas nécessairement à le faire. Si la fonction était déclarée comme :

fn foo<'a>(x: &'a T) { ... }

alors en fait, nous savons que foo ne conserve pas p plus longtemps que la durée a vie bar devrait être considérée comme un échappement :

fn bar<'a>(x: &'a T, y: &mut &'a T)

On peut donc supposer que les règles d'échappement devraient considérer si la durée de vie liée est apparue dans un emplacement modifiable ou non. Il s'agit en fait d'une forme d'analyse d'alias basée sur le type. Un raisonnement similaire, je pense, s'applique aux valeurs de retour de fonction. Par conséquent, find doit être considéré comme renvoyant un résultat sans échappement :

fn find<'a>(&'a self, k: &K) -> Option<&'a V>

La raison ici est que parce que 'a est lié à find , il ne peut pas apparaître dans les paramètres de type Self ou K , et donc nous savons qu'il le peut' t être stocké dans ceux-ci, et il n'apparaît dans aucun emplacement modifiable. (Notez que nous pouvons appliquer le même algorithme d'inférence que celui utilisé aujourd'hui et qui sera utilisé dans le cadre du correctif pour #3598 pour nous dire si les durées de vie apparaissent dans un emplacement modifiable)

Une autre façon de penser à cela n'est pas que le prêt a expiré _tôt_, mais plutôt que la portée d'un prêt commence (généralement) comme étant liée à la _variable_ empruntée et non à la durée de vie complète, et n'est promue à la durée de vie complète que lorsque la variable _évasions_.

Les réemprunts sont une légère complication, mais ils peuvent être gérés de différentes manières. Un réemprunt est lorsque vous empruntez le contenu d'un pointeur emprunté -- ils se produisent _tout le temps_ parce que le compilateur les insère automatiquement dans presque chaque appel de méthode. Considérons un pointeur emprunté let p = &v et un réemprunt comme let q = &*p . Ce serait bien si quand q était mort, vous pouviez réutiliser p -- et si les deux p et q étaient morts, vous pourriez utiliser v nouveau (en supposant que ni p ni q s'échappent). La complication ici est que si q s'échappe, p doit être considéré comme échappé jusqu'à ce que la durée de vie de q expire. Mais je pense que cela tombe _un peu_ naturellement de la façon dont nous le gérons aujourd'hui : c'est-à-dire que le compilateur note que q a emprunté p pour (initialement) la durée de vie "q" (c'est-à-dire, de la variable elle-même) et si q devait s'échapper, cela serait promu à la durée de vie lexicale complète. Je suppose que la partie délicate est dans le flux de données, savoir où insérer les kills -- nous ne pouvons pas insérer le kill pour p tout de suite quand p meurt s'il est réemprunté. Eh bien, je ne vais pas perdre plus de temps là-dessus, cela semble faisable, et au pire, il existe des solutions plus simples qui seraient adéquates pour des situations courantes (par exemple, considérez que p s'est échappé pendant toute la durée de vie de q , que le prêt de q échappe ou non).

Quoi qu'il en soit, plus de réflexion est justifiée, mais je commence à voir comment cela pourrait fonctionner. Je suis toujours réticent à me lancer dans des extensions comme celle-ci jusqu'à ce que #2202 et #8624 soient corrigés, ceux-ci étant les deux problèmes connus avec l'emprunt. J'aimerais aussi avoir plus de progrès sur une preuve de solidité avant d'étendre le système. L'autre extension qui est sur la timeline est #6268.

Je crois que j'ai rencontré ce bug. Mon cas d'utilisation et mes tentatives de contournement :

https://gist.github.com/toffaletti/6770126

Voici un autre exemple de ce bug (je pense):

use std::util;

enum List<T> {
    Cons(T, ~List<T>),
    Nil
}

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => {}, // NB: can't return Some(prev) here
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    };
    return Some(prev)
}

j'aimerais écrire :

fn find_mut<'a,T>(prev: &'a mut ~List<T>, pred: |&T| -> bool) -> Option<&'a mut ~List<T>> {
    match prev {
        &~Cons(ref x, _) if pred(x) => return Some(prev),
        &~Cons(_, ref mut rs) => return find_mut(rs, pred),
        &~Nil => return None
    }
}

le raisonnement selon lequel l'emprunt x disparaît dès que nous avons fini d'évaluer le prédicat, mais bien sûr, l'emprunt s'étend sur l'ensemble du match en ce moment.

J'ai eu plus de réflexions sur la façon de coder cela. Mon plan de base est que pour chaque prêt, il y aurait deux bits : une version échappée et une version non échappée. Initialement, nous ajoutons la version sans échappement. Lorsqu'une référence s'échappe, nous ajoutons les bits échappés. Lorsqu'une variable (ou temporaire, etc.) meurt, nous tuons les bits non échappés -- mais laissons les bits échappés (s'ils sont définis) intacts. Je pense que cela couvre tous les exemples majeurs.

cc @flaper87

Ce problème couvre-t-il cela ?

use std::io::{MemReader, EndOfFile, IoResult};

fn read_block<'a>(r: &mut Reader, buf: &'a mut [u8]) -> IoResult<&'a [u8]> {
    match r.read(buf) {
        Ok(len) => Ok(buf.slice_to(len)),
        Err(err) => {
            if err.kind == EndOfFile {
                Ok(buf.slice_to(0))
            } else {
                Err(err)
            }
        }
    }
}

fn main() {
    let mut buf = [0u8, ..2];
    let mut reader = MemReader::new(~[67u8, ..10]);
    let mut block = read_block(&mut reader, buf);
    loop {
        //process block
        block = read_block(&mut reader, buf); //error here
}

cc moi

De bons exemples en #9113

cc moi

Je peux me tromper, mais le code suivant semble également rencontrer ce bogue :

struct MyThing<'r> {
  int_ref: &'r int,
  val: int
}

impl<'r> MyThing<'r> {
  fn new(int_ref: &'r int, val: int) -> MyThing<'r> {
    MyThing {
      int_ref: int_ref,
      val: val
    }
  }

  fn set_val(&'r mut self, val: int) {
    self.val = val;
  }
}


fn main() {
  let to_ref = 10;
  let mut thing = MyThing::new(&to_ref, 30);
  thing.set_val(50);

  println!("{}", thing.val);
}

Idéalement, l'emprunt mutable causé par l'appel de set_val se terminerait dès le retour de la fonction. Notez que la suppression du champ 'int_ref' de la structure (et du code associé) fait disparaître le problème. Le comportement est incohérent.

@SergioBenitez Je ne pense pas que ce soit le même problème. Vous demandez explicitement que la durée de vie de la référence &mut self soit la même que la durée de vie de la structure.

Mais vous n'avez pas besoin de faire cela. Vous n'avez pas du tout besoin d'une vie en set_val() .

fn set_val(&mut self, val: int) {
    self.val = val;
}

J'ai trouvé un autre cas assez difficile à résoudre :

/// A buffer which breaks chunks only after the specified boundary
/// sequence, or at the end of a file, but nowhere else.
pub struct ChunkBuffer<'a, T: Buffer+'a> {
    input:  &'a mut T,
    boundary: Vec<u8>,
    buffer: Vec<u8>
}

impl<'a, T: Buffer+'a> ChunkBuffer<'a,T> {
    // Called internally to make `buffer` valid.  This is where all our
    // evil magic lives.
    fn top_up<'b>(&'b mut self) -> IoResult<&'b [u8]> {
        // ...
    }
}

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        if self.buffer.as_slice().contains_slice(self.boundary.as_slice()) {
            // Exit 1: Valid data in our local buffer.
            Ok(self.buffer.as_slice())
        } else if self.buffer.len() > 0 {
            // Exit 2: Add some more data to our local buffer so that it's
            // valid (see invariants for top_up).
            self.top_up()
        } else {
            {
                // Exit 3: Exit on error.
                let read = try!(self.input.fill_buf());
                if read.contains_slice(self.boundary.as_slice()) {
                    // Exit 4: Valid input from self.input. Yay!
                    return Ok(read)
                }
            }
            // Exit 5: Accumulate sufficient data in our local buffer (see
            // invariants for top_up).
            self.top_up()
        }
    }

…qui donne:

/path/to/mylib/src/buffer.rs:168:13: 168:17 error: cannot borrow `*self` as mutable more than once at a time
/path/to/mylib/src/buffer.rs:168             self.top_up()
                                                        ^~~~
/path/to/mylib/src/buffer.rs:160:33: 160:43 note: previous borrow of `*self.input` occurs here; the mutable borrow prevents subsequent moves, borrows, or modification of `*self.input` until the borrow ends
/path/to/mylib/src/buffer.rs:160                 let read = try!(self.input.fill_buf());
                                                                            ^~~~~~~~~~
<std macros>:1:1: 3:2 note: in expansion of try!
/path/to/mylib/src/buffer.rs:160:28: 160:56 note: expansion site
/path/to/mylib/src/buffer.rs:170:6: 170:6 note: previous borrow ends here
/path/to/mylib/src/buffer.rs:149     fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
...
/path/to/mylib/src/buffer.rs:170     }

Ceci est fondamentalement équivalent à #12147. La variable read est enterrée dans une portée interne, mais le return lie la durée de vie de read à celle de la fonction entière. La plupart des solutions de contournement évidentes échouent :

  1. Je ne peux pas appeler input.fill_buf deux fois, car l'interface Buffer ne garantit pas qu'elle renvoie les données que je viens de valider la deuxième fois. Si j'essaye ceci, le code est techniquement incorrect mais le vérificateur de type le passe avec plaisir.
  2. Je ne peux pas faire grand-chose à propos de top_up , car c'est un morceau de code maléfique qui doit tout muter de manière compliquée.
  3. Je ne peux pas déplacer le bind+test+return incriminé dans une autre fonction, car la nouvelle API aura toujours les mêmes problèmes (à moins que if let me permette de tester _then_ bind ?).

On a presque l'impression que la contrainte 'a ne devrait idéalement pas se propager jusqu'à read . mais je suis au-dessus de ma tête ici. Je vais essayer if let ensuite.

Eh bien, if let n'a pas été intégré à la compilation hier soir, mais comme il ne s'agit que d'une réécriture AST, je suppose que cela échoue probablement de la même manière que match (ce que j'ai aussi essayé ici).

Je ne sais pas comment procéder, à moins d'utiliser unsafe .

Mon hack actuel ressemble à ceci:

impl<'a,T: Buffer+'a> Buffer for ChunkBuffer<'a,T> {
    fn fill_buf<'a>(&'a mut self) -> IoResult<&'a [u8]> {
        // ...

            { // Block A.
                let read_or_err = self.input.fill_buf();
                match read_or_err {
                    Err(err) => { return Err(err); }
                    Ok(read) => {
                        if read.contains_slice(self.boundary.as_slice()) {
                               return Ok(unsafe { transmute(read) });
                        }
                    }
                }
            }
            self.top_up()

La théorie ici est que je supprime la durée de vie de read (qui était liée à self.input ), et applique immédiatement une nouvelle durée de vie basée sur self , qui possède self.input . Idéalement, je veux que read ait une durée de vie lexicale égale au bloc A, et je ne veux pas qu'il soit hissé au niveau du bloc _lexical_ simplement parce que je l'ai passé à return . Évidemment, le vérificateur de durée de vie doit encore prouver que le résultat a une durée de vie compatible avec 'a , mais je ne comprends pas pourquoi cela signifie que LIFETIME( read ) doit être unifié avec LIFETIME( 'a ).

Il est tout à fait possible que je sois extrêmement confus ou que mon code soit horriblement dangereux. :-) Mais j'ai l'impression que cela devrait fonctionner, ne serait-ce que parce que je peux appeler return self.input.fill_buf() sans aucun problème. Existe-t-il un moyen de formaliser cette intuition ?

@emk c'est donc le "code dur" que les régions SEME (c'est-à-dire les régions non lexicales) ne corrigent pas, du moins pas par elles-mêmes. J'ai quelques idées sur la façon de le corriger correctement dans le compilateur, mais c'est une extension non triviale aux régions SEME. Il existe généralement un moyen de contourner ce problème en restructurant le code. Laissez-moi voir si je peux jouer avec et produire un bel exemple.

Je voudrais savoir si cela est reconsidéré pour 1.0. Cela a fait un _lot_ récemment, et je crains que cela passe d'une coupure de papier à une blessure de chair une fois que la 1.0 attirera l'attention. En tant que fonctionnalité la plus visible et dont on parle le plus de Rust, il est très important que l'emprunt soit poli et utilisable.

Y a-t-il un délai sur un RFC pour cela?

@nikomatsakis J'ai un exemple simple du monde réel qui est tombé du travail sur l'API Entry, si cela peut aider:

use std::collections::SmallIntMap;

enum Foo<'a>{ A(&'a mut SmallIntMap<uint>), B(&'a mut uint) }

fn main() {
    let mut map = SmallIntMap::<uint>::new();
    do_stuff(&mut map);
}

fn do_stuff(map: &mut SmallIntMap<uint>) -> Foo {
    match map.find_mut(&1) {
        None => {},  // Definitely can't return A here because of lexical scopes
        Some(val) => return B(val),
    }
    return A(map); // ERROR: borrowed at find_mut???
}

parc

@bstrie @pcwalton et @zwarich ont passé un certain temps à essayer de mettre en œuvre ce travail (avec une éventuelle RFC venant de pair). Ils se sont heurtés à une complexité inattendue, ce qui signifie qu'il faudrait beaucoup plus de travail que prévu. Je pense que tout le monde est d'accord avec vous sur le fait que ces limitations sont importantes et peuvent avoir un impact sur la première impression de la langue, mais il est difficile d'équilibrer cela avec les changements rétrocompatibles qui sont déjà programmés.

Je pense que si cela n'est pas résolu par 1.0, c'est le genre de chose qui amènera les gens à blâmer complètement l'approche de la vérification des emprunts, alors que ce problème n'est pas un problème intrinsèquement insoluble avec la vérification des emprunts AFAIK.

@blaenk Il est difficile de ne pas blâmer le vérificateur d'emprunt, j'ai rencontré cela et des choses similaires (comme @Gankro) quotidiennement. C'est frustrant quand la solution habituelle est des girations (par exemple un contournement) / ou des commentaires pour restructurer votre code pour qu'il soit plus "immuable", fonctionnel, etc.

@mtanski Oui, mais la faute réside dans le vérificateur d'emprunt AFAIK, il n'est pas incorrect de le blâmer. Ce à quoi je fais référence, c'est que cela peut amener les nouveaux arrivants à croire qu'il s'agit d'un problème inhérent, fondamental et insoluble avec l'_approche_ de la vérification des emprunts, ce qui est une croyance assez insidieuse, et AFAIK, incorrecte.

Pour le cas : "let p = &...; use-pa-bit-but-jamais-again; expect-loan-to-expired-here;" Je trouverais acceptable pour l'instant une instruction kill(p) pour déclarer manuellement la fin de la portée de cet emprunt. Les versions ultérieures pourraient simplement ignorer cette instruction si elle n'est pas nécessaire ou la signaler comme une erreur si une réutilisation de p est détectée après celle-ci.

/* (wanted) */
/*
fn main() {

    let mut x = 10;

    let y = &mut x;

    println!("x={}, y={}", x, *y);

    *y = 11;

    println!("x={}, y={}", x, *y);
}
*/

/* had to */
fn main() {

    let mut x = 10;
    {
        let y = &x;

        println!("x={}, y={}", x, *y);
    }

    {
        let y = &mut x;

        *y = 11;
    }

    let y = &x;

    println!("x={}, y={}", x, *y);
}

Il y a la méthode drop() dans le prélude qui fait cela. Mais ne semble pas
pour aider avec les emprunts mutables.

Le dimanche 5 avril 2015, 13h41, axeoth [email protected] a écrit :

/* (recherché) _//_fn main() { let mut x = 10; soit y = &mut x; println!("x={}, y={}", x, _y); *y = 11 ; println!("x={}, y={}", x, *y);}_/
/* devait */fn main() {

let mut x = 10;
{
    let y = &x;

    println!("x={}, y={}", x, *y);
}

{
    let y = &mut x;

    *y = 11;
}

let y = &x;

println!("x={}, y={}", x, *y);

}

-
Répondez directement à cet e-mail ou consultez-le sur GitHub
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

@metajack le lien de votre code d'origine est un 404. Pouvez-vous l'inclure en ligne pour les personnes qui lisent ce bogue ?

Après quelques recherches, je pense que cela équivaut au code d'origine :
https://github.com/servo/servo/blob/5e406fab7ee60d9d8077d52d296f52500d72a2f6/src/servo/layout/box_builder.rs#L374

Ou plutôt, c'est la solution de contournement que j'ai utilisée lorsque j'ai signalé ce bogue. Le code d'origine avant ce changement semble être le suivant :
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

Je ne suis pas sûr de la pertinence de ces exemples spécifiques maintenant, car ils étaient antérieurs à Rust 1.0.

@metajack, ce serait formidable d'avoir un exemple ultra simple (post 1.0) en haut de ce numéro. Ce problème fait maintenant partie de https://github.com/rust-lang/rfcs/issues/811

fn main() {
    let mut nums=vec![10i,11,12,13];
    *nums.get_mut(nums.len()-2)=2;
}

Je pense que ce dont je me plaignais était quelque chose comme ça:
https://is.gd/yfxUfw

Ce cas particulier semble fonctionner maintenant.

@vitiral Un exemple dans Rust d'aujourd'hui qui, je crois, s'applique :

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}

Le bras None échoue à l'emprunt.

Curieusement, si vous n'essayez pas de capturer int dans le bras Some (c'est-à-dire utilisez Some(_) ), il compile.

@wyverland oh

@metajack pouvez-vous modifier le premier message pour inclure cet exemple ?

Ce n'est pas encore sorti tous les soirs, mais je veux juste dire que ceci compile maintenant :

#![feature(nll)]

fn main() {
    let mut vec = vec!();

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
Cette page vous a été utile?
0 / 5 - 0 notes

Questions connexes

mcarton picture mcarton  ·  3Commentaires

drewcrawford picture drewcrawford  ·  3Commentaires

Robbepop picture Robbepop  ·  3Commentaires

modsec picture modsec  ·  3Commentaires

tikue picture tikue  ·  3Commentaires