Rust: los alcances prestados no siempre deben ser léxicos

Creado en 10 may. 2013  ·  44Comentarios  ·  Fuente: rust-lang/rust

Si toma prestado de manera inmutable en una prueba if , el préstamo dura toda la expresión if . Esto significa que los préstamos mutables en las cláusulas harán que el verificador de préstamos falle.

Esto también puede suceder cuando se pide prestado en la expresión de coincidencia y se necesita un préstamo mutable en uno de los brazos.

Vea aquí un ejemplo donde el if toma prestados cuadros, lo que hace que el @mut más cercano se congele. Luego remove_child() que necesita pedir prestado conflictos mutantes.

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

Ejemplo actualizado 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

Comentario más útil

Todavía no ha llegado todas las noches, pero solo quiero decir que esto ahora compila:

#![feature(nll)]

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

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

Todos 44 comentarios

nominando para producción listo

Yo llamaría a esto bien definido o compatible con versiones anteriores.

El código se reorganizó, pero aquí está el apaciguamiento específico del verificador de préstamos que tuve que hacer:

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

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

Después de un poco de discusión, está claro que el verdadero problema aquí es que el verificador de préstamos no hace ningún esfuerzo por rastrear los alias, sino que siempre confía en el sistema de la región para determinar cuándo un préstamo sale del alcance. Soy reacio a cambiar esto, al menos no a corto plazo, porque hay una serie de otras cuestiones pendientes que me gustaría abordar primero y cualquier cambio sería una modificación significativa para el verificador de préstamos. Consulte el número 6613 para ver otro ejemplo relacionado y una explicación algo detallada.

Me pregunto si podríamos mejorar los mensajes de error para dejar más claro lo que está pasando. Los ámbitos léxicos son relativamente fáciles de entender, pero en los ejemplos de este problema con los que me he encontrado, no era de ninguna manera obvio lo que estaba sucediendo.

solo un error, eliminando hito / nominación.

aceptado para un hito en el futuro lejano

golpe de triaje

He tenido algunas ideas sobre la mejor manera de solucionar este problema. Mi plan básico es que tengamos una noción de cuándo un valor "escapa". Se necesitará algo de trabajo para formalizar esa noción. Básicamente, cuando se crea un puntero prestado, seguiremos si se ha escapado. Cuando el puntero está muerto , si no se ha escapado, se puede considerar que esto mata el préstamo. Esta idea básica cubre casos como "deje p = & ...; use-pa-bit-pero-nunca-más; espere-que-el-préstamo-caduque-aquí;" Parte del análisis será una regla que indique cuándo se puede considerar que un valor de retorno que contiene un puntero prestado aún no se ha escapado. Esto cubriría casos como "match table.find (...) {... None => {tabla-de-espera-que-no-se-prestará-aquí;}}"

La parte más interesante de todo esto son las reglas de escape, por supuesto. Creo que las reglas tendrían que tener en cuenta la definición formal de la función y, en particular, aprovechar el conocimiento que nos brindan las vidas ligadas. Por ejemplo, la mayoría de los análisis de escape considerarían un puntero p para escapar si ven una llamada como foo(p) . Pero no necesariamente tendríamos que hacerlo. Si la función se declaró como:

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

entonces, de hecho, sabemos que foo no se aferra a p durante más tiempo que la vida útil de a . Sin embargo, una función como bar debería considerarse como un escape:

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

Entonces, presumiblemente, las reglas de escape tendrían que considerar si la vida útil vinculada apareció en una ubicación mutable o no. Ésta es efectivamente una forma de análisis de alias basado en tipos. Creo que un razonamiento similar se aplica a los valores de retorno de la función. Por lo tanto, se debe considerar que find devuelve un resultado sin escape:

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

La razón aquí es que debido a que 'a está vinculado a find , no puede aparecer en los parámetros de tipo Self o K y, por lo tanto, sabemos que puede ' t almacenarse en esos, y no aparece en ninguna ubicación mutable. (Tenga en cuenta que podemos aplicar el mismo algoritmo de inferencia que se usa hoy y que se usará como parte de la corrección para # 3598 para decirnos si la vida útil aparece en una ubicación mutable)

Otra forma de pensar en esto no es que el préstamo esté vencido _ anticipadamente_, sino que el alcance de un préstamo comienza (típicamente) vinculado a la _variable_ prestada y no a la vida útil completa, y solo se promueve a la vida útil completa cuando la variable _escapes_.

Los renacimientos son una ligera complicación, pero pueden manejarse de varias formas. Un cambio de mañana es cuando toma prestado el contenido de un puntero prestado; suceden _ todo el tiempo_ porque el compilador los inserta automáticamente en casi todas las llamadas a métodos. Considere un puntero borowed let p = &v y un reborrow como let q = &*p . Sería bueno si cuando q estuviera muerto, pudiera usar p nuevamente, y si tanto p como q estuvieran muertos, podría usar v nuevo (suponiendo que ni p ni q escapen). La complicación aquí es que si q escapa, p debe considerarse como un escape hasta que expire la vida útil de q . Pero creo que esto se cae _algo_ naturalmente de cómo lo manejamos hoy: es decir, el compilador observa que q ha pedido prestado p para (inicialmente) la vida útil "q" (es decir, de la variable en sí) y si q debe escapar, se promoverá a la vida léxica completa. Supongo que la parte complicada está en el flujo de datos, saber dónde insertar las muertes; no podemos insertar las muertes por p inmediato cuando p muere si se vuelve a prestar. Bueno, no perderé más tiempo en esto, parece factible y, en el peor de los casos, hay soluciones más simples que serían adecuadas para situaciones comunes (por ejemplo, considere que p han escapado durante toda la vida de q , independientemente de si el q escapa o no).

De todos modos, se justifica pensar más, pero estoy empezando a ver cómo podría funcionar esto. Sigo siendo reacio a embarcarme en extensiones como esta hasta que se solucionen los números 2202 y 8624, ya que son los dos problemas conocidos con el préstamo. También me gustaría tener más avances en una prueba de solidez antes de continuar con la ampliación del sistema. La otra extensión que está en la línea de tiempo es # 6268.

Creo que me encontré con este error. Mi caso de uso e intentos alternativos:

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

Aquí hay otro ejemplo de este error (creo):

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)
}

Me gustaría escribir:

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
    }
}

razonando que el x desaparece tan pronto como terminamos de evaluar el predicado, pero, por supuesto, el préstamo se extiende a toda la coincidencia en este momento.

He tenido más pensamientos sobre cómo codificar esto. Mi plan básico es que por cada préstamo habría dos partes: una versión con escape y una versión sin escape. Inicialmente agregamos la versión sin escape. Cuando una referencia escapa, agregamos los bits escapados. Cuando una variable (o temporal, etc.) se apaga, matamos los bits que no han escapado, pero dejamos intactos los bits que han escapado (si están configurados). Creo que esto cubre todos los ejemplos principales.

cc @ flaper87

¿Este problema cubre esto?

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
}

ccme

Buenos ejemplos en # 9113

ccme

Podría estar equivocado, pero el siguiente código parece estar afectando a este error también:

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);
}

Idealmente, el préstamo mutable causado por llamar a set_val terminaría tan pronto como regrese la función. Tenga en cuenta que eliminar el campo 'int_ref' de la estructura (y el código asociado) hace que el problema desaparezca. El comportamiento es inconsistente.

@SergioBenitez No creo que sea el mismo problema. Está solicitando explícitamente que la duración de la referencia &mut self sea ​​la misma que la duración de la estructura.

Pero no es necesario que haga esto. No necesitas toda una vida en set_val() .

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

Encontré otro caso que es bastante complicado de solucionar:

/// 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()
        }
    }

…lo que da:

/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     }

Esto es básicamente equivalente a # 12147. La variable read está enterrada en un ámbito interno, pero return vincula la vida útil de read a la de toda la función. La mayoría de las soluciones obvias fallan:

  1. No puedo llamar a input.fill_buf dos veces, porque la interfaz Buffer no garantiza que devuelva los datos que acabo de validar la segunda vez. Si intento esto, el código es técnicamente incorrecto, pero el verificador de tipo lo pasa felizmente.
  2. No puedo hacer mucho con top_up , porque es una pieza de código malvada que necesita mutar todo de manera complicada.
  3. No puedo mover el bind + test + return ofensivo a otra función, porque la nueva API seguirá teniendo los mismos problemas (a menos que if let me permita probar _then_ bind?).

Casi se siente como si la restricción 'a idealmente no debería propagarse de regreso a read . pero estoy sobre mi cabeza aquí. Voy a intentar if let continuación.

Bueno, if let no se incluyó en la compilación anoche, pero dado que supuestamente es solo una reescritura de AST, supongo que probablemente falla de la misma manera que match (lo que he también probé aquí).

No estoy seguro de cómo proceder, salvo en el uso de unsafe .

Mi truco actual aquí se ve así:

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 teoría aquí es que estoy eliminando la vida útil de read (que estaba vinculada a self.input ) e inmediatamente estoy aplicando una nueva vida útil basada en self , que posee self.input . Idealmente, quiero que read tenga una vida léxica igual al bloque A, y no quiero que se eleve al nivel del bloque _lexical_ solo porque lo pasé a return . Obviamente, el verificador de por vida todavía necesita demostrar que el resultado es compatible de por vida con 'a , pero no entiendo por qué eso significa que LIFETIME ( read ) debe unificarse con LIFETIME ( 'a ).

Es muy posible que esté enormemente confundido o que mi código sea terriblemente inseguro. :-) Pero parece que esto debería funcionar, aunque solo sea porque puedo llamar a return self.input.fill_buf() sin ningún problema. ¿Hay alguna forma de formalizar esa intuición?

@emk entonces este es el "código duro" que las regiones SEME (es decir, regiones no léxicas) no corrigen, al menos no por sí mismas. Tengo algunas ideas sobre cómo solucionarlo bien en el compilador, pero es una extensión no trivial para las regiones SEME. Por lo general, hay una forma de solucionar esto reestructurando el código. Déjame ver si puedo jugar con él y dar un buen ejemplo.

Me gustaría saber si esto se está reconsiderando para 1.0. Esto ha estado apareciendo mucho recientemente, y me temo que esto saltará de un corte de papel a una herida superficial una vez que 1.0 atraiga la atención. Como la característica más visible y comentada de Rust, es muy importante que los préstamos sean pulidos y utilizables.

¿Existe un plazo en un RFC para esto?

@nikomatsakis Tengo un ejemplo simple del mundo real que dejó de funcionar en la API de entrada, si ayuda:

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???
}

corralito

@bstrie Tanto @pcwalton como @zwarich pasaron algún tiempo tratando de implementar este trabajo (con un posible RFC de la mano). Se encontraron con una complejidad inesperada que significa que tomaría mucho más trabajo del esperado. Creo que todos están de acuerdo contigo en que estas limitaciones son importantes y pueden afectar la primera impresión del idioma, pero es difícil equilibrar eso con los cambios incompatibles con versiones anteriores que ya están programados.

Siento que si esto no se resuelve en 1.0, es el tipo de cosa que llevará a la gente a culpar por completo al enfoque de verificación de préstamos, cuando este problema no es un problema inherentemente irresoluble con la verificación de préstamos AFAIK.

@blaenk Es difícil no culpar al verificador de préstamos, me he encontrado con esto y similares (como @Gankro) a diario. Es frustrante cuando la solución habitual son giros (por ejemplo, una solución alternativa) / o comentarios para reestructurar su código para que sea más "inmutable", funcional, etc.

@mtanski Sí, pero la culpa _si_ radica en el verificador de préstamos AFAIK, no es incorrecto culparlo. A lo que me refiero es a que puede llevar a los recién llegados a creer que es un problema inherente, fundamental e irresoluble con el enfoque de verificación de préstamos, que es una creencia bastante insidiosa, y AFAIK, incorrecta.

Para el caso: "deje p = & ...; use-pa-bit-pero-nunca-más; espere-que-el-préstamo-caduque-aquí;" Por ahora, me parecería aceptable una instrucción kill (p) para declarar manualmente el fin del alcance de ese préstamo. Las versiones posteriores podrían simplemente ignorar esta instrucción si no es necesaria o marcarla como un error si se detecta la reutilización de p después de ella.

/* (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);
}

Existe el método drop () en el preludio que hace eso. Pero no parece
para ayudar con préstamos cambiantes.

El domingo 5 de abril de 2015 a la 1:41 p. M. Axeoth [email protected] escribió:

/ * (deseado) _ // _ fn main () {let mut x = 10; sea ​​y = & mut x; println! ("x = {}, y = {}", x, _y); * y = 11; println! ("x = {}, y = {}", x, * y);} _ /
/ * tenía que * / 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);

}

-
Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

@metajack, el enlace de su código original es un 404. ¿Puede incluirlo en línea para las personas que lean este error?

Después de investigar un poco, creo que esto es equivalente al código original:
https://github.com/servo/servo/blob/5e406fab7ee60d9d8077d52d296f52500d72a2f6/src/servo/layout/box_builder.rs#L374

O más bien, esa es la solución alternativa que utilicé cuando presenté este error. El código original antes de ese cambio parece ser este:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

No estoy seguro de cuán relevantes son estos ejemplos específicos ahora, ya que eran anteriores a Rust 1.0.

@metajack sería genial tener un ejemplo ultra simple (publicación 1.0) en la parte superior de este número. Este problema ahora es parte 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;
}

Creo que de lo que me quejaba era de algo como esto:
https://is.gd/yfxUfw

Ese caso particular parece funcionar ahora.

@vitiral Un ejemplo en el Rust de hoy que creo que se aplica:

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

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

El brazo None falla al tomar prestado.

Curiosamente, si no intenta capturar int en el brazo Some (es decir, use Some(_) ), se compilará.

@wyverland oh sí, lo golpeé ayer, bastante molesto.

@metajack, ¿puedes editar la primera publicación para incluir ese ejemplo?

Todavía no ha llegado todas las noches, pero solo quiero decir que esto ahora compila:

#![feature(nll)]

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

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
¿Fue útil esta página
0 / 5 - 0 calificaciones