Rust: escopos emprestados nem sempre devem ser lexicais

Criado em 10 mai. 2013  ·  44Comentários  ·  Fonte: rust-lang/rust

Se você tomar emprestado imutavelmente em um teste if , o empréstimo dura por toda a expressão if . Isso significa que empréstimos mutáveis ​​nas cláusulas farão com que o verificador de empréstimo falhe.

Isso também pode acontecer ao pedir emprestado na expressão de correspondência e precisar de um empréstimo mutável em um dos braços.

Veja aqui um exemplo em que if empresta caixas, o que faz com que o @mut mais próximo para cima congele. Então remove_child() que precisa tomar emprestado conflitos mutáveis.

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

Exemplo atualizado 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

Comentários muito úteis

Ainda não foi atingido todas as noites, mas só quero dizer que agora compila:

#![feature(nll)]

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

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

Todos 44 comentários

nomeação para produção pronta

Eu chamaria isso de bem definido ou compatível com versões anteriores.

O código foi reorganizado, mas aqui está o apaziguamento específico do verificador de empréstimo que tive que fazer:

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

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

Depois de alguma discussão, fica claro que o problema real aqui é que o verificador de empréstimo não faz nenhum esforço para rastrear aliases, mas sempre confia no sistema de região para determinar quando um empréstimo sai do escopo. Estou relutante em mudar isso, pelo menos não no curto prazo, porque há uma série de outras questões pendentes que eu gostaria de abordar primeiro e qualquer mudança seria uma modificação significativa para o verificador de empréstimo. Consulte a edição nº 6613 para obter outro exemplo relacionado e uma explicação um tanto detalhada.

Será que poderíamos melhorar as mensagens de erro para deixar mais claro o que está acontecendo? Os osciloscópios lexicais são relativamente fáceis de entender, mas nos exemplos desse problema que encontrei não era de forma alguma óbvio o que estava acontecendo.

apenas um bug, removendo marco / nomeação.

aceito para um marco futuro distante

colisão de triagem

Eu pensei sobre a melhor maneira de consertar isso. Meu plano básico é que tenhamos uma noção de quando um valor "escapa". Vai demorar algum trabalho para formalizar essa noção. Basicamente, quando um ponteiro emprestado é criado, rastrearemos se ele escapou. Quando o ponteiro está morto , se não escapou, isso pode ser considerado como uma anulação do empréstimo. Essa ideia básica cobre casos como "deixe p = & ...; use-pa-bit-but-never-again; espere-o-empréstimo-para-expirar-aqui;" Parte da análise será uma regra que indica quando um valor de retorno que contém um ponteiro emprestado pode ser considerado como ainda não escapado. Isso cobriria casos como "match table.find (...) {... None => {expect-table-not-to-be-emprested-here;}}"

A parte mais interessante de tudo isso são as regras de escape, é claro. Eu acho que as regras teriam que levar em conta a definição formal da função e, em particular, tirar vantagem do conhecimento limitado que as vidas nos dão. Por exemplo, a maioria das análises de escape consideraria um ponteiro p para escapar se virem uma chamada como foo(p) . Mas não teríamos necessariamente que fazer isso. Se a função foi declarada como:

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

então, de fato, sabemos que foo não guarda p por mais tempo do que a vitalício. No entanto, uma função como bar teria que ser considerada como escape:

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

Portanto, presumivelmente, as regras de escape teriam que considerar se o tempo de vida vinculado apareceu em um local mutável ou não. Esta é efetivamente uma forma de análise de alias baseada em tipo. Penso que raciocínio semelhante se aplica aos valores de retorno da função. Portanto, find deve ser considerado para retornar um resultado sem escape:

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

A razão aqui é que, como 'a está vinculado a find , ele não pode aparecer nos parâmetros de tipo Self ou K e, portanto, sabemos que pode ' t ser armazenado neles e não aparecer em nenhum local mutável. (Observe que podemos aplicar o mesmo algoritmo de inferência que é usado hoje e que será usado como parte da correção para # 3598 para nos dizer se os tempos de vida aparecem em um local mutável)

Outra maneira de pensar sobre isso não é que o empréstimo tenha expirado _ cedo_, mas sim que o escopo de um empréstimo começa (normalmente) como vinculado à _variável_ emprestada e não à vida inteira, e só é promovido para a vida inteira quando a variável _escapes_.

Reborrows são uma pequena complicação, mas podem ser tratados de várias maneiras. Um novo amanhã é quando você toma emprestado o conteúdo de um ponteiro emprestado - eles acontecem _ todo o tempo_ porque o compilador os insere automaticamente em quase todas as chamadas de método. Considere um ponteiro emprestado let p = &v e um novo amanhã como let q = &*p . Seria bom se quando q estivesse morto, você pudesse usar p novamente - e se p e q estivessem mortos, você pudesse usar v novamente (não presumindo que p nem q escapem). A complicação aqui é que se q escapar, p deve ser considerado como escapado até que q expire. Mas eu acho que isso se desprende _um pouco_ naturalmente de como lidamos com isso hoje: ou seja, o compilador observa que q emprestou p para (inicialmente) o tempo de vida "q" (ou seja, da própria variável) e se q escapasse, isso seria promovido para a vida lexical completa. Acho que a parte complicada está no fluxo de dados, saber onde inserir as mortes - não podemos inserir a morte de p imediatamente quando p morrer se for recuperada. Bem, não vou perder mais tempo com isso, parece que é possível e, na pior das hipóteses, existem soluções mais simples que seriam adequadas para situações comuns (por exemplo, considere p como tendo escapado por toda a vida de q , independentemente de o empréstimo de q escapar ou não).

De qualquer forma, é preciso pensar mais, mas estou começando a ver como isso pode funcionar. Ainda estou relutante em embarcar em extensões como essa até que # 2202 e # 8624 sejam corrigidos, sendo esses os dois problemas conhecidos com empréstimos. Eu também gostaria de ter mais progresso em uma prova de solidez antes de estendermos o sistema. A outra extensão que está na linha do tempo é # 6268.

Eu acredito que encontrei esse bug. Meu caso de uso e tentativas de contornar o problema:

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

Aqui está outro exemplo desse bug (eu acho):

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

Eu gostaria de escrever:

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

raciocinando que o empréstimo de x vai morto assim que terminamos de avaliar o predicado, mas é claro, o empréstimo se estende por toda a correspondência agora.

Eu tive mais ideias sobre como codificar isso. Meu plano básico é que para cada empréstimo haja dois bits: uma versão com escape e uma versão sem escape. Inicialmente, adicionamos a versão sem escape. Quando uma referência escapa, adicionamos os bits de escape. Quando uma variável (ou temporária, etc.) fica inativa, eliminamos os bits não escapados - mas deixamos os bits escapados (se configurados) intactos. Eu acredito que isso cobre todos os exemplos principais.

cc @ flaper87

Este problema cobre isso?

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
}

me cc

Bons exemplos em # 9113

me cc

Posso estar enganado, mas o código a seguir parece estar encontrando esse bug também:

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, o empréstimo mutável causado pela chamada de set_val terminaria assim que a função retornasse. Observe que remover o campo 'int_ref' da estrutura (e do código associado) faz com que o problema desapareça. O comportamento é inconsistente.

@SergioBenitez Não acho que seja a mesma coisa. Você está solicitando explicitamente que o tempo de vida da referência &mut self seja igual ao tempo de vida da estrutura.

Mas você não precisa fazer isso. Você não precisa de uma vida inteira em set_val() .

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

Encontrei outro caso que é muito complicado de resolver:

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

…que dá:

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

Isso é basicamente equivalente a # 12147. A variável read está enterrada em um escopo interno, mas o return vincula read ao tempo de vida de toda a função. A maioria das soluções alternativas óbvias falham:

  1. Não posso chamar input.fill_buf duas vezes, porque a interface Buffer não garante que retornará os dados que acabei de validar na segunda vez. Se eu _fazer_ tentar isso, o código está tecnicamente incorreto, mas o verificador de tipo passa com satisfação.
  2. Não posso fazer muito sobre top_up , porque é um código maligno que precisa transformar tudo de maneiras complicadas.
  3. Não consigo mover o bind + test + return ofensivo para outra função, porque a nova API ainda terá os mesmos problemas (a menos que if let me permita testar _then_ bind?).

É quase como se a restrição 'a idealmente não devesse ser propagada de volta para read . mas estou sobrecarregado aqui. Vou tentar if let seguir.

Bem, if let não entrou na compilação ontem à noite, mas como é supostamente apenas uma reescrita de AST, acho que provavelmente falha da mesma forma que match (o que eu fiz também tentei aqui).

Não tenho certeza de como proceder, exceto unsafe .

Meu hack atual é assim:

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

A teoria aqui é que estou retirando o tempo de vida de read (que estava vinculado a self.input ) e aplicando imediatamente um novo tempo de vida baseado em self , que possui self.input . Idealmente, eu quero que read tenha um tempo de vida léxico igual ao Bloco A, e não quero que ele seja içado até o nível do bloco _lexical_ apenas porque o passei para return . Obviamente, o verificador vitalício ainda precisa provar que o resultado tem uma vida útil compatível com 'a , mas não entendo por que isso significa que LIFETIME ( read ) precisa ser unificado com LIFETIME ( 'a ).

É perfeitamente possível que eu esteja extremamente confuso ou que meu código seja terrivelmente inseguro. :-) Mas parece que isso deve funcionar, apenas porque posso chamar return self.input.fill_buf() sem nenhum problema. Existe alguma maneira de formalizar essa intuição?

@emk então este é o "código rígido" que as regiões SEME (ou seja, regiões não lexicais) não corrigem, pelo menos não por si mesmas. Eu tenho algumas idéias de como consertá-lo bem no compilador, mas é uma extensão não trivial para regiões SEME. Geralmente, há uma maneira de contornar isso reestruturando o código. Deixe-me ver se consigo brincar com isso e produzir um bom exemplo.

Gostaria de saber se isso está sendo reconsiderado para 1.0. Isso tem acontecido _muito_ recentemente, e temo que salte de um corte de papel para um ferimento superficial, uma vez que 1.0 chamar a atenção. Como o recurso mais visível e falado do Rust, é muito importante que o empréstimo seja polido e utilizável.

Existe um prazo em um RFC para isso?

@nikomatsakis Eu tenho um exemplo simples do mundo real que não funcionou na API Entry, se isso ajudar:

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

cercadinho

@bstrie Tanto @pcwalton quanto @zwarich passaram algum tempo tentando realmente implementar este trabalho (com um possível RFC vindo de mãos dadas). Eles se depararam com alguma complexidade inesperada, o que significa que exigiria muito mais trabalho do que o esperado. Acho que todos concordam com você que essas limitações são importantes e podem afetar a primeira impressão do idioma, mas é difícil equilibrar isso com alterações incompatíveis com versões anteriores que já estão programadas.

Eu sinto que se isso não for resolvido até 1.0, é o tipo de coisa que levará as pessoas a culpar totalmente a abordagem de verificação de empréstimo, quando esse problema não é um problema inerentemente insolúvel com a verificação de empréstimo AFAIK.

@blaenk É difícil não culpar o verificador de empréstimo, já me deparei com isso e coisas semelhantes (como @Gankro) diariamente. É frustrante quando a solução usual é giros (por exemplo, uma solução alternativa) / ou comentários para reestruturar seu código para ser mais "imutável", funcional, etc.

@mtanski Sim, mas a falha _será_ no verificador de empréstimo AFAIK, não é incorreto culpá-lo. O que estou me referindo é que isso pode levar os recém-chegados a acreditar que é um problema inerente, fundamental e insolúvel com a _abordagem_ de verificação de empréstimo, que é uma crença bastante insidiosa, e AFAIK, incorreta.

Para o caso: "deixe p = & ...; use-pa-bit-mas-nunca-novamente; espere-o-empréstimo-para-expirar-aqui;" Eu acharia aceitável por enquanto uma instrução kill (p) para declarar manualmente o fim do escopo para esse empréstimo. Versões posteriores podem simplesmente ignorar esta instrução se não for necessária ou sinalizá-la como um erro se a reutilização de p for detectada depois dela.

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

Há o método drop () no prelúdio que faz isso. Mas não parece
para ajudar com empréstimos mutáveis.

No domingo, 5 de abril de 2015, 13:41 axeoth [email protected] escreveu:

/ * (desejado) _ // _ fn main () {let mut x = 10; deixe y = & mut x; println! ("x = {}, y = {}", x, _y); * y = 11; println! ("x = {}, y = {}", x, * y);} _ /
/ * teve 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 e-mail diretamente ou visualize-o no GitHub
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

@metajack o link para seu código original é 404. Você pode incluí-lo embutido para as pessoas que estão lendo este bug?

Após algumas pesquisas, acredito que isso seja equivalente ao código original:
https://github.com/servo/servo/blob/5e406fab7ee60d9d8077d52d296f52500d72a2f6/src/servo/layout/box_builder.rs#L374

Ou melhor, essa é a solução alternativa que usei quando registrei este bug. O código original antes dessa mudança parece ser este:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

Não tenho certeza de quão relevantes esses exemplos específicos são agora, já que eram anteriores ao Rust 1.0.

@metajack seria ótimo ter um exemplo ultra simples (pós 1.0) no início desta edição. Este problema agora faz 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;
}

Acho que estava reclamando de algo assim:
https://is.gd/yfxUfw

Esse caso particular parece funcionar agora.

@vitiral Um exemplo do Rust de hoje que acredito se aplicar:

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

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

O braço None falha no empréstimo.

Curiosamente, se você não tentar capturar int no braço Some (ou seja, use Some(_) ), ele compila.

@wyverland oh ya, eu acertei isso ontem, muito chato.

@metajack você pode editar a primeira postagem para incluir esse exemplo?

Ainda não foi atingido todas as noites, mas só quero dizer que agora compila:

#![feature(nll)]

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

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
Esta página foi útil?
0 / 5 - 0 avaliações