Rust: lingkup pinjaman tidak harus selalu leksikal

Dibuat pada 10 Mei 2013  ·  44Komentar  ·  Sumber: rust-lang/rust

Jika Anda meminjam secara permanen dalam pengujian if , pinjaman berlangsung untuk seluruh ekspresi if . Ini berarti bahwa pinjaman yang dapat diubah dalam klausa akan menyebabkan pemeriksa pinjaman gagal.

Ini juga bisa terjadi ketika meminjam dalam ekspresi kecocokan, dan membutuhkan pinjaman yang bisa berubah di salah satu lengan.

Lihat di sini untuk contoh di mana kotak if meminjam, yang menyebabkan @mut terdekat ke atas membeku. Kemudian remove_child() yang perlu meminjam konflik yang bisa berubah.

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

Contoh yang diperbarui dari @Wyverald

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

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

Komentar yang paling membantu

Ini belum hit setiap malam, tetapi saya hanya ingin mengatakan bahwa ini sekarang dikompilasi:

#![feature(nll)]

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

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

Semua 44 komentar

nominasi untuk produksi siap

Saya akan menyebut ini terdefinisi dengan baik atau mundur-compat.

Setelah beberapa diskusi, jelas bahwa masalah sebenarnya di sini adalah bahwa pemeriksa pinjaman tidak berusaha untuk melacak alias, melainkan selalu bergantung pada sistem wilayah untuk menentukan kapan pinjaman keluar dari ruang lingkup. Saya enggan untuk mengubah ini, setidaknya tidak dalam jangka pendek, karena ada sejumlah masalah lain yang belum terselesaikan yang ingin saya tangani terlebih dahulu dan setiap perubahan akan menjadi modifikasi signifikan pada pemeriksa pinjaman. Lihat edisi #6613 untuk contoh terkait lainnya dan penjelasan yang agak mendetail.

Saya ingin tahu apakah kami dapat memperbaiki pesan kesalahan untuk memperjelas apa yang terjadi? Lingkup leksikal relatif mudah dipahami, tetapi dalam contoh masalah ini yang saya temukan, sama sekali tidak jelas apa yang sedang terjadi.

hanya bug, menghapus pencapaian/nominasi.

diterima untuk tonggak masa depan yang jauh

benjolan triase

Saya memiliki beberapa pemikiran tentang cara terbaik untuk memperbaikinya. Rencana dasar saya adalah bahwa kita akan memiliki gagasan tentang kapan suatu nilai "lolos". Ini akan mengambil beberapa pekerjaan untuk meresmikan gagasan itu. Pada dasarnya, ketika pointer yang dipinjam dibuat, kami kemudian akan melacak apakah pointer tersebut telah lolos. Ketika pointer mati , jika tidak lolos, ini dapat dianggap membunuh pinjaman. Ide dasar ini mencakup kasus-kasus seperti "let p = &...; use-pa-bit-but-never-again; expect-loan-to-be-expired-here;" Bagian dari analisis akan menjadi aturan yang menunjukkan ketika nilai pengembalian yang berisi pointer yang dipinjam dapat dianggap belum lolos. Ini akan mencakup kasus-kasus seperti "match table.find(...) { ... None => { expect-table-not-to-be-loaned-here; } }"

Bagian yang paling menarik dari semua ini adalah aturan melarikan diri, tentu saja. Saya pikir aturan harus mempertimbangkan definisi formal fungsi, dan khususnya untuk mengambil keuntungan dari pengetahuan yang diberikan oleh masa hidup kepada kita. Misalnya, sebagian besar analisis escape akan mempertimbangkan pointer p untuk escape jika mereka melihat panggilan seperti foo(p) . Tapi kita tidak harus melakukannya. Jika fungsi dideklarasikan sebagai:

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

maka sebenarnya kita tahu bahwa foo tidak mempertahankan p lebih lama dari seumur hidup a . Namun, fungsi seperti bar harus dianggap melarikan diri:

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

Jadi mungkin aturan melarikan diri harus mempertimbangkan apakah masa hidup terikat muncul di lokasi yang bisa berubah atau tidak. Ini secara efektif merupakan bentuk analisis alias berbasis tipe. Alasan serupa menurut saya berlaku untuk nilai pengembalian fungsi. Karenanya find harus dipertimbangkan untuk mengembalikan hasil yang tidak lolos:

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

Alasan di sini adalah karena 'a terikat pada find , itu tidak dapat muncul dalam parameter tipe Self atau K , dan karenanya kita tahu itu bisa' t disimpan di dalamnya, dan itu tidak muncul di lokasi yang bisa berubah. (Perhatikan bahwa kami dapat menerapkan algoritme inferensi yang sama seperti yang digunakan saat ini dan yang akan digunakan sebagai bagian dari perbaikan #3598 untuk memberi tahu kami apakah masa hidup muncul di lokasi yang dapat diubah)

Cara lain untuk memikirkan hal ini bukanlah karena pinjaman tersebut kedaluwarsa _awal_, melainkan bahwa ruang lingkup pinjaman dimulai (biasanya) seperti yang terkait dengan _variabel_ yang dipinjam dan bukan seumur hidup penuh, dan hanya dipromosikan menjadi seumur hidup penuh ketika variabel _kabur_.

Peminjaman ulang adalah sedikit komplikasi, tetapi dapat ditangani dengan berbagai cara. Reborrow adalah ketika Anda meminjam konten dari pointer yang dipinjam -- hal itu terjadi _sepanjang waktu_ karena kompiler memasukkannya secara otomatis ke hampir setiap pemanggilan metode. Pertimbangkan pointer yang dipinjam let p = &v dan reborrow seperti let q = &*p . Akan lebih baik jika q mati, Anda dapat menggunakan p lagi -- dan jika p dan q mati, Anda dapat menggunakan v lagi (dengan asumsi p maupun q lolos). Komplikasi di sini adalah jika q lolos, p harus dianggap lolos hingga masa pakai q berakhir. Tapi saya pikir ini _agak_ jatuh secara alami dari cara kita menanganinya hari ini: yaitu, kompilator mencatat bahwa q telah meminjam p untuk (awalnya) "q" seumur hidup (yaitu, dari variabel itu sendiri) dan jika q harus diloloskan, itu akan dipromosikan ke masa leksikal penuh. Saya kira bagian yang sulit adalah dalam aliran data, mengetahui di mana harus memasukkan kill -- kita tidak dapat memasukkan kill untuk p segera ketika p mati jika dipinjam ulang. Oh well, saya tidak akan membuang lebih banyak waktu untuk ini, tampaknya dapat dilakukan, dan paling buruk ada solusi sederhana yang akan memadai untuk situasi umum (misalnya, pertimbangkan p telah lolos untuk seumur hidup penuh dari q , terlepas dari apakah pinjaman q lolos atau tidak).

Bagaimanapun, lebih banyak pemikiran diperlukan, tetapi saya mulai melihat bagaimana ini bisa berhasil. Saya masih enggan untuk memulai ekstensi seperti ini sampai #2202 dan #8624 diperbaiki, itu adalah dua masalah yang diketahui dengan borrowck. Saya juga ingin memiliki lebih banyak kemajuan pada bukti kesehatan sebelum kita memperluas sistem. Ekstensi lain yang ada di timeline adalah #6268.

Saya yakin saya pernah mengalami bug ini. Kasus penggunaan dan upaya penyelesaian saya:

https://Gist.github.com/tofaletti/6770126

Berikut contoh lain dari bug ini (saya pikir):

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

Saya ingin menulis:

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

dengan alasan bahwa x pinjaman mati segera setelah kami selesai mengevaluasi predikat, tetapi tentu saja, pinjaman meluas untuk seluruh pertandingan sekarang.

Saya memiliki lebih banyak pemikiran tentang cara membuat kode ini. Rencana dasar saya adalah bahwa untuk setiap pinjaman akan ada dua bit: versi yang lolos dan versi yang tidak lolos. Awalnya kami menambahkan versi non-escape. Saat referensi lolos, kami menambahkan bit yang lolos. Ketika sebuah variabel (atau sementara, dll) mati, kita membunuh bit yang tidak lolos -- tetapi membiarkan bit yang lolos (jika disetel) tidak tersentuh. Saya percaya ini mencakup semua contoh utama.

cc @flaper87

Apakah masalah ini mencakup ini?

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 saya

Contoh bagus di #9113

cc saya

Saya bisa saja salah, tetapi kode berikut tampaknya juga mengenai bug ini:

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

Idealnya, peminjaman yang bisa berubah yang disebabkan oleh pemanggilan set_val akan berakhir segera setelah fungsi kembali. Perhatikan bahwa menghapus bidang 'int_ref' dari struct (dan kode terkait) menyebabkan masalah hilang. Perilakunya tidak konsisten.

@SergioBenitez Saya tidak berpikir itu masalah yang sama. Anda secara eksplisit meminta agar masa pakai referensi &mut self sama dengan masa pakai struct.

Tapi Anda tidak perlu melakukan ini. Anda tidak perlu seumur hidup di set_val() sama sekali.

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

Saya menemukan kasus lain yang cukup sulit untuk diperbaiki:

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

…yang memberikan:

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

Ini pada dasarnya setara dengan #12147. Variabel read terkubur di dalam lingkup, tetapi return mengikat read seumur hidup dengan seluruh fungsi. Sebagian besar solusi yang jelas gagal:

  1. Saya tidak dapat memanggil input.fill_buf dua kali, karena antarmuka Buffer tidak menjamin bahwa ia mengembalikan data yang baru saja saya validasi untuk kedua kalinya. Jika saya _do_ mencoba ini, kodenya secara teknis salah tetapi pemeriksa tipe meneruskannya dengan senang hati.
  2. Saya tidak bisa berbuat banyak tentang top_up , karena ini adalah kode jahat yang perlu mengubah semuanya dengan cara yang rumit.
  3. Saya tidak dapat memindahkan bind+test+return yang menyinggung ke fungsi lain, karena API baru akan tetap memiliki semua masalah yang sama (kecuali if let mengizinkan saya menguji _then_ bind?).

Hampir terasa seolah-olah batasan 'a idealnya tidak boleh disebarkan sepanjang perjalanan kembali ke read . tapi aku di atas kepalaku di sini. Saya akan mencoba if let selanjutnya.

Yah, if let tidak berhasil masuk ke build tadi malam, tetapi karena itu seharusnya hanya penulisan ulang AST, saya kira itu mungkin gagal dengan cara yang sama seperti match (yang saya lakukan juga mencoba di sini).

Saya tidak yakin bagaimana melanjutkan, karena tidak menggunakan unsafe .

Retasan saya saat ini di sini terlihat seperti ini:

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

Teorinya di sini adalah bahwa saya melepaskan masa pakai read (yang terikat pada self.input ), dan segera menerapkan masa pakai baru berdasarkan self , yang memiliki self.input . Idealnya, saya ingin read memiliki umur leksikal yang sama dengan Blok A, dan saya tidak ingin itu dinaikkan ke level blok _lexical_ hanya karena saya meneruskannya ke return . Jelas pemeriksa seumur hidup masih perlu membuktikan bahwa hasilnya memiliki seumur hidup yang kompatibel dengan 'a , tetapi saya tidak mengerti mengapa itu berarti LIFETIME( read ) perlu disatukan dengan LIFETIME( 'a ).

Sangat mungkin bahwa saya sangat bingung, atau kode saya sangat tidak aman. :-) Tapi rasanya ini harus berhasil, jika hanya karena saya bisa menelepon return self.input.fill_buf() tanpa masalah sama sekali. Apakah ada cara untuk memformalkan intuisi itu?

@emk jadi ini adalah "kode keras" yang tidak diperbaiki oleh wilayah SEME (yaitu, wilayah non-leksikal), setidaknya tidak sendiri. Saya punya beberapa ide tentang cara memperbaikinya dengan baik di kompiler, tetapi ini adalah ekstensi non-sepele untuk wilayah SEME. Biasanya ada cara untuk mengatasi ini dengan merestrukturisasi kode. Biarkan saya melihat apakah saya bisa bermain-main dengannya dan menghasilkan contoh yang bagus.

Saya ingin tahu apakah ini sedang dipertimbangkan kembali untuk 1.0. Ini telah muncul _lot_ baru-baru ini, dan saya khawatir ini akan berubah dari potongan kertas menjadi luka daging setelah 1.0 membawa perhatian. Sebagai fitur Rust yang paling terlihat dan dibicarakan, sangat penting untuk meminjam agar dipoles dan dapat digunakan.

Apakah ada kerangka waktu pada RFC untuk ini?

@nikomatsakis Saya memiliki contoh sederhana dunia nyata yang gagal bekerja di Entry API, jika itu membantu:

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

boks

@bstrie Baik @pcwalton dan @zwarich menghabiskan beberapa waktu untuk benar-benar mengimplementasikan pekerjaan ini (dengan kemungkinan RFC datang bersamaan). Mereka mengalami beberapa kerumitan tak terduga yang berarti akan membutuhkan lebih banyak pekerjaan daripada yang diharapkan. Saya pikir semua orang setuju dengan Anda bahwa batasan ini penting dan dapat memengaruhi kesan pertama bahasa tersebut, tetapi sulit untuk menyeimbangkannya dengan perubahan yang tidak kompatibel dengan sebelumnya yang sudah dijadwalkan.

Saya merasa bahwa jika ini tidak diselesaikan dengan 1.0, itu adalah jenis hal yang akan membuat orang menyalahkan pendekatan pengecekan pinjaman sama sekali, ketika masalah ini bukanlah masalah yang tidak dapat diselesaikan secara inheren dengan pengecekan pinjaman AFAIK.

@blaenk Sulit untuk tidak menyalahkan pemeriksa pinjaman, saya mengalami ini dan serupa (seperti @Gankro) setiap hari. Ini membuat frustrasi ketika solusi yang biasa adalah rotasi (mis. Penyelesaian) / atau komentar untuk merestrukturisasi kode Anda menjadi lebih "tidak berubah", fungsional, dll.

@mtanski Ya, tapi kesalahan _does_ terletak pada pemeriksa pinjaman AFAIK, tidak salah untuk menyalahkannya. Apa yang saya maksud adalah bahwa hal itu dapat membuat pendatang baru percaya bahwa itu adalah masalah yang melekat, mendasar, dan tidak dapat diselesaikan dengan _pendekatan_ pemeriksaan pinjaman, yang merupakan kepercayaan yang cukup berbahaya, dan AFAIK, salah.

Untuk kasus: "let p = &...; use-pa-bit-but-never-again; expect-loan-to-be-expired-here;" Saya akan menemukan instruksi kill(p) yang dapat diterima untuk secara manual mendeklarasikan akhir ruang lingkup untuk pinjaman itu. Versi yang lebih baru dapat dengan mudah mengabaikan instruksi ini jika tidak diperlukan atau menandainya sebagai kesalahan jika penggunaan kembali p terdeteksi setelahnya.

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

Ada metode drop() di pendahuluan yang melakukan itu. Tapi sepertinya tidak
untuk membantu pinjaman yang bisa berubah.

Pada Minggu, 5 April 2015, 13:41 axeoth [email protected] menulis:

/* (ingin) _//_fn main() { biarkan mut x = 10; misalkan y = &mut x; println!("x={}, y={}", x, _y); *y = 11; println!("x={}, y={}", x, *y);}_/
/* harus */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);

}


Balas email ini secara langsung atau lihat di GitHub
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

@metajack tautan untuk kode asli Anda adalah 404. Bisakah Anda memasukkannya sebaris untuk orang yang membaca bug ini?

Atau lebih tepatnya, itulah solusi yang saya gunakan ketika saya mengajukan bug ini. Kode asli sebelum perubahan itu tampaknya seperti ini:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

Saya tidak yakin seberapa relevan contoh spesifik ini sekarang karena sebelum Rust 1.0.

@metajack akan sangat bagus untuk memiliki contoh ultra sederhana (posting 1.0) di bagian atas masalah ini. Masalah ini sekarang menjadi bagian dari 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;
}

Saya pikir apa yang saya keluhkan adalah sesuatu seperti ini:
https://is.gd/yfxUfw

Kasus khusus itu tampaknya berhasil sekarang.

@vitiral Contoh di Rust hari ini yang saya yakini berlaku:

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

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

Lengan None gagal meminjam.

Anehnya, jika Anda tidak mencoba untuk menangkap int di lengan Some (mis. gunakan Some(_) ), itu dikompilasi.

@wyverland oh ya, saya baru saja memukulnya kemarin, cukup menjengkelkan.

@metajack dapatkah Anda mengedit posting pertama untuk memasukkan contoh itu?

Ini belum hit setiap malam, tetapi saya hanya ingin mengatakan bahwa ini sekarang dikompilasi:

#![feature(nll)]

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

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
Apakah halaman ini membantu?
0 / 5 - 0 peringkat