Rust: Ausleihbereiche sollten nicht immer lexikalisch sein

Erstellt am 10. Mai 2013  ·  44Kommentare  ·  Quelle: rust-lang/rust

Wenn Sie in einem if Test unveränderlich ausleihen, dauert die Ausleihe für den gesamten if Ausdruck. Dies bedeutet, dass veränderliche Kredite in den Klauseln dazu führen, dass die Kreditprüfung fehlschlägt.

Dies kann auch passieren, wenn im Match-Ausdruck geborgt wird und in einem der Arme ein veränderliches Borgen benötigt wird.

Sehen Sie hier für ein Beispiel, in dem if Boxen ausleiht, was dazu führt, dass die nächsthöheren @mut einfrieren. Dann remove_child() die wechselweise Konflikte ausleihen müssen.

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

Aktualisiertes Beispiel von @Wyverald

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

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

Hilfreichster Kommentar

Es ist noch nicht allabendlich, aber ich möchte nur sagen, dass dies jetzt kompiliert:

#![feature(nll)]

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

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

Alle 44 Kommentare

Nominierung für die Produktion bereit

Ich würde dies entweder wohldefiniert oder rückwärtskompat nennen.

Der Code wurde neu organisiert, aber hier ist die spezifische Beschwichtigung des Kreditprüfers, den ich tun musste:

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

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

Nach einigen Diskussionen ist klar, dass das eigentliche Problem hier darin besteht, dass der Ausleihprüfer keine Anstrengungen unternimmt, Aliase zu verfolgen, sondern sich immer auf das Regionssystem verlässt, um festzustellen, wann ein Ausleihen außerhalb des Gültigkeitsbereichs liegt. Ich möchte dies nur ungern ändern, zumindest nicht kurzfristig, da es noch eine Reihe anderer offener Fragen gibt, die ich zuerst ansprechen möchte und jede Änderung eine wesentliche Änderung des Kreditprüfers bedeuten würde. Siehe Ausgabe Nr. 6613 für ein weiteres ähnliches Beispiel und eine etwas detaillierte Erklärung.

Ich frage mich, ob wir die Fehlermeldungen verbessern könnten, um klarer zu machen, was los ist. Lexikalische Bereiche sind relativ leicht zu verstehen, aber in den Beispielen dieses Problems, über das ich gestolpert bin, war es keineswegs offensichtlich, was vor sich ging.

nur ein Fehler, der Meilenstein / Nominierung entfernt.

für einen fernen zukünftigen Meilenstein akzeptiert

Triage-Beule

Ich habe mir einige Gedanken gemacht, wie ich das am besten beheben kann. Mein grundlegender Plan ist, dass wir eine Vorstellung davon haben, wann ein Wert "entkommt". Es wird einige Arbeit erfordern, diesen Begriff zu formalisieren. Grundsätzlich wird beim Erstellen eines geliehenen Zeigers nachverfolgt, ob er entkommen ist. Wenn der Zeiger tot ist , wenn er nicht entkommen ist, kann dies als das Abbrechen des Darlehens angesehen werden. Diese Grundidee umfasst Fälle wie "lass p = &...; use-pa-bit-but-never- again; Expect-Darlehen-to-be-ab-repired-hier;" Ein Teil der Analyse wird eine Regel sein, die angibt, wann ein Rückgabewert, der einen geliehenen Zeiger enthält, als noch nicht entkommen betrachtet werden kann. Dies würde Fälle abdecken wie "match table.find(...) { ... None => { Expect-Table-not-to-be-be-loaned-here; } }"

Das Interessanteste an all dem sind natürlich die Fluchtregeln. Ich denke, dass die Regeln die formale Definition der Funktion berücksichtigen und insbesondere die wissensgebundenen Lebensdauern nutzen müssen. Zum Beispiel würden die meisten Escape-Analysen einen Zeiger p als Escape betrachten, wenn sie einen Aufruf wie foo(p) . Aber das müssten wir nicht unbedingt tun. Wenn die Funktion wie folgt deklariert wurde:

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

dann wissen wir tatsächlich, dass foo nicht länger als a an p festhält. Eine Funktion wie bar müsste jedoch als Escape-Funktion betrachtet werden:

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

Vermutlich müssten die Fluchtregeln also berücksichtigen, ob die gebundene Lebensdauer an einem veränderlichen Ort auftauchte oder nicht. Dies ist effektiv eine Form der typbasierten Aliasanalyse. Ähnliche Argumente gelten meiner Meinung nach für Funktionsrückgabewerte. Daher sollte find betrachtet werden, um ein Ergebnis ohne Escape-Zeichen zurückzugeben:

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

Der Grund dafür ist, dass 'a an find gebunden ist und daher nicht in den Parametern des Typs Self oder K kann. Es kann nicht in diesen gespeichert werden, und es erscheint nicht an veränderlichen Orten. (Beachten Sie, dass wir denselben Inferenzalgorithmus anwenden können, der heute verwendet wird und der als Teil des Fixes für #3598 verwendet wird, um uns mitzuteilen, ob Lebenszeiten an einem veränderlichen Ort erscheinen)

Eine andere Möglichkeit, dies zu bedenken, ist nicht, dass das Darlehen _früh_ ausläuft, sondern dass der Umfang eines Darlehens (normalerweise) an die geliehene _Variable_ und nicht an die volle Laufzeit gebunden beginnt und erst dann auf die volle Laufzeit hochgestuft wird, wenn die Variable _entkommt_.

Wiederausleihen sind eine leichte Komplikation, können aber auf verschiedene Weise gehandhabt werden. Ein Reborrow ist, wenn Sie den Inhalt eines geliehenen Zeigers ausleihen -- sie passieren _die ganze Zeit_ , weil der Compiler sie automatisch in fast jeden Methodenaufruf einfügt. Betrachten Sie einen geliehenen Zeiger let p = &v und einen Reborrow wie let q = &*p . Es wäre schön, wenn, wenn q tot wäre, Sie p wieder verwenden könnten – und wenn sowohl p als auch q tot wären, könnten Sie verwenden v erneut (vorausgesetzt, weder p noch q entkommen). Die Komplikation hierbei ist, dass, wenn q entweicht, p als entkommen betrachtet werden muss, bis die Lebensdauer von q abgelaufen ist. Aber ich denke, dass dies _etwas_ natürlich aus unserer heutigen Handhabung herausfällt: Das heißt, der Compiler stellt fest, dass q p für (zunächst) die Lebensdauer "q" geliehen hat (das heißt, der Variablen selbst) und wenn q entkommen sollte, würde dies auf die volle lexikalische Lebensdauer hochgestuft. Ich denke, der knifflige Teil liegt im Datenfluss, zu wissen, wo die Kills eingefügt werden müssen – wir können die Kills nicht sofort für p einfügen, wenn p tot ist, wenn sie erneut ausgeliehen werden. Na ja, ich werde nicht mehr Zeit auf diese verschwenden, scheint es tun können, und im schlimmsten Fall gibt es einfachere Lösungen , die für allgemeine Situationen angemessen wäre (zB betrachten p haben , für die volle Lebensdauer entkommen von q , unabhängig davon, ob das Darlehen von q entgeht oder nicht).

Wie auch immer, mehr Nachdenken ist angebracht, aber ich fange an zu sehen, wie das funktionieren könnte. Ich zögere immer noch, solche Erweiterungen in Angriff zu nehmen, bis #2202 und #8624 behoben sind, dies sind die beiden bekannten Probleme mit Borrowck. Ich hätte auch gerne mehr Fortschritte bei einem Soliditätsnachweis, bevor wir das System erweitern. Die andere Erweiterung auf der Timeline ist #6268.

Ich glaube, ich bin auf diesen Fehler gestoßen. Mein Anwendungsfall und Umgehungsversuche:

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

Hier ist ein weiteres Beispiel für diesen Fehler (glaube ich):

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

Ich möchte schreiben:

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

mit der Begründung, dass die x Ausleihe tot ist, sobald wir die Auswertung des Prädikats abgeschlossen haben, aber natürlich erstreckt sich die Ausleihe jetzt für das gesamte Match.

Ich habe mir mehr Gedanken gemacht, wie ich das codieren kann. Mein grundlegender Plan ist, dass es für jedes Darlehen zwei Bits gibt: eine Escape-Version und eine Nicht-Escape-Version. Zunächst fügen wir die nicht-escaped-Version hinzu. Wenn eine Referenz mit Escapezeichen versehen ist, fügen wir die Escape-Bits hinzu. Wenn eine Variable (oder temporär usw.) tot ist, töten wir die Bits ohne Escape-Zeichen – lassen die Bits mit Escape jedoch (sofern gesetzt) ​​unberührt. Ich glaube, das deckt alle wichtigen Beispiele ab.

cc @flaper87

Deckt dieses Problem dies ab?

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 ich

Gute Beispiele in #9113

cc ich

Ich könnte mich irren, aber der folgende Code scheint auch diesen Fehler zu treffen:

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

Im Idealfall würde der durch den Aufruf von set_val verursachte veränderliche Borrow enden, sobald die Funktion zurückkehrt. Beachten Sie, dass das Entfernen des 'int_ref'-Felds aus der Struktur (und dem zugehörigen Code) dazu führt, dass das Problem behoben ist. Das Verhalten ist inkonsistent.

@SergioBenitez Ich glaube nicht, dass das das gleiche Problem ist. Sie fordern explizit an, dass die Lebensdauer der Referenz &mut self mit der Lebensdauer der Struktur übereinstimmt.

Aber Sie müssen dies nicht tun. Sie brauchen kein Leben in set_val() .

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

Ich habe einen anderen Fall gefunden, der ziemlich schwierig zu beheben ist:

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

…was gibt:

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

Dies entspricht im Wesentlichen #12147. Die Variable read ist in einem inneren Gültigkeitsbereich versteckt, aber return bindet die Lebensdauer von read an die der gesamten Funktion. Die meisten der offensichtlichen Problemumgehungen schlagen fehl:

  1. Ich kann input.fill_buf zweimal aufrufen, weil die Buffer Schnittstelle nicht garantiert, dass sie die Daten zurückgibt, die ich gerade das zweite Mal validiert habe. Wenn ich dies _mach_, ist der Code technisch falsch, aber die Typprüfung besteht ihn glücklich.
  2. Ich kann nicht viel mit top_up , weil es ein böser Code ist, der alles auf komplizierte Weise mutieren muss.
  3. Ich kann das störende bind+test+return nicht in eine andere Funktion verschieben, da die neue API immer noch dieselben Probleme haben wird (es sei denn, if let erlaubt mir, _then_ bind zu testen?).

Es fühlt sich fast so an, als ob die 'a Einschränkung idealerweise nicht den ganzen Weg zurück zu read propagiert werden sollte. aber ich bin hier überfordert. Ich werde es als nächstes mit if let versuchen.

Nun, if let hat es letzte Nacht nicht in den Build geschafft, aber da es angeblich nur eine AST-Neufassung ist, schlägt es wahrscheinlich genauso fehl wie match (was ich getan habe) auch hier probiert).

Ich bin mir nicht sicher, wie ich vorgehen soll, außer unsafe .

Mein aktueller Hack hier sieht so aus:

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

Die Theorie hier ist, dass ich die Lebensdauer von read (die an self.input gebunden war) absetze und sofort eine neue Lebensdauer basierend auf self , die self.input besitzt read eine lexikalische Lebensdauer hat, die Block A entspricht, und ich möchte nicht, dass es auf die _lexikalische_ Blockebene angehoben wird, nur weil ich es an return . Natürlich muss der Lifetime-Checker noch beweisen, dass das Ergebnis eine Lebensdauer hat, die mit 'a kompatibel ist, aber ich verstehe nicht, warum das bedeutet, dass LIFETIME( read ) mit LIFETIME( 'a ).

Es ist durchaus möglich, dass ich massiv verwirrt bin oder dass mein Code schrecklich unsicher ist. :-) Aber es fühlt sich so an, als ob das funktionieren sollte, schon allein, weil ich ohne Probleme return self.input.fill_buf() anrufen kann. Gibt es eine Möglichkeit, diese Intuition zu formalisieren?

@emk Dies ist also der "harte Code", den SEME-Regionen (dh nicht-lexikalische Regionen) nicht beheben, zumindest nicht selbst. Ich habe einige Ideen, wie man es im Compiler gut reparieren kann, aber es ist eine nicht triviale Erweiterung für SEME-Regionen. Normalerweise gibt es eine Möglichkeit, dies zu umgehen, indem Sie den Code umstrukturieren. Mal sehen, ob ich damit herumspielen und ein schönes Beispiel produzieren kann.

Ich würde gerne wissen, ob dies für 1.0 überdacht wird. Dies ist in letzter Zeit viel aufgetaucht, und ich befürchte, dass dies von einem Scherenschnitt zu einer Fleischwunde übergehen wird, sobald 1.0 einen Ansturm an Aufmerksamkeit erregt. Als sichtbarstes und am meisten diskutiertes Feature von Rust ist es sehr wichtig, dass die Ausleihe aufpoliert und verwendbar ist.

Gibt es dafür einen RFC-Zeitrahmen?

@nikomatsakis Ich habe ein einfaches Beispiel aus der

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

Laufstall

@bstrie Sowohl @pcwalton als auch @zwarich haben einige Zeit damit verbracht, diese Arbeit tatsächlich umzusetzen (mit einem möglichen RFC, der Hand in Hand kommt). Sie stießen auf eine unerwartete Komplexität, die bedeutete, dass es viel mehr Arbeit erfordern würde als erhofft. Ich denke, alle stimmen mit Ihnen überein, dass diese Einschränkungen wichtig sind und den ersten Eindruck der Sprache beeinflussen können, aber es ist schwer, dies gegen abwärtsinkompatible Änderungen abzuwägen, die bereits geplant sind.

Ich denke, wenn dies nicht bis 1.0 gelöst ist, wird dies dazu führen, dass die Leute den Ansatz zur Kreditprüfung insgesamt beschuldigen, obwohl dieses Problem kein von Natur aus unlösbares Problem mit der Kreditprüfung von AFAIK ist.

@blaenk Es ist schwer, dem

@mtanski Ja, aber der Fehler liegt im Kreditprüfer AFAIK, es ist nicht falsch, ihm die Schuld zu geben. Worauf ich mich beziehe, ist, dass es Neuankömmlinge zu der Annahme verleiten kann, dass es sich um ein inhärentes, grundlegendes, unlösbares Problem mit dem _Ansatz_ zur Überprüfung von Krediten_ handelt, was eine ziemlich heimtückische und AFAIK-falsche Überzeugung ist.

Für den Fall: "let p = &...; use-pa-bit-aber-nie-wieder; erwarten-dass-darlehen-hier-abgelaufen-werden;" Ich würde vorerst eine kill(p)-Anweisung akzeptabel finden, um das Ende des Gültigkeitsbereichs für diese Ausleihe manuell zu deklarieren. Spätere Versionen könnten diese Anweisung einfach ignorieren, wenn sie nicht benötigt wird, oder sie als Fehler kennzeichnen, wenn danach eine Wiederverwendung von p erkannt wird.

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

Es gibt die drop()-Methode im Präludium, die das tut. Aber scheint nicht
um mit veränderlichen Krediten zu helfen.

Am Sonntag, 5. April 2015, 13:41 Uhr schrieb axeoth [email protected] :

/* (gesucht) _//_fn main () { let mut x = 10; sei y = &mutx; println!("x={}, y={}", x, _y); *y = 11; println!("x={}, y={}", x, *y);}_/
/* musste */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);

}


Antworten Sie direkt auf diese E-Mail oder zeigen Sie sie auf GitHub an
https://github.com/rust-lang/rust/issues/6393#issuecomment -89848449.

@metajack der Link für Ihren ursprünglichen Code ist ein 404. Können Sie ihn für Leute, die diesen Fehler lesen, inline einfügen?

Nach einigem Graben glaube ich, dass dies dem ursprünglichen Code entspricht:
https://github.com/servo/servo/blob/5e406fab7ee60d9d8077d52d296f52500d72a2f6/src/servo/layout/box_builder.rs#L374

Oder besser gesagt, das ist die Problemumgehung, die ich verwendet habe, als ich diesen Fehler eingereicht habe. Der ursprüngliche Code vor dieser Änderung scheint folgender zu sein:
https://github.com/servo/servo/blob/7267f806a7817e48b0ac0c9c4aa23a8a0d288b03/src/servo/layout/box_builder.rs#L387 -L399

Ich bin mir nicht sicher, wie relevant diese spezifischen Beispiele jetzt sind, da sie vor Rust 1.0 waren.

@metajack es wäre großartig, ein ultra einfaches Beispiel (nach 1.0) oben in dieser Ausgabe zu haben. Dieses Problem ist jetzt Teil von 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;
}

Ich glaube, was ich bemängelt habe, war in etwa so:
https://is.gd/yfxUfw

Dieser spezielle Fall scheint jetzt zu funktionieren.

@vitiral Ein Beispiel im heutigen Rust, das meiner Meinung nach zutrifft:

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

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

Der Arm None schlägt fehl.

Seltsamerweise, wenn Sie nicht versuchen, int im Some Arm einzufangen (dh verwenden Sie Some(_) ), wird es kompiliert.

@wyverland oh ya, das habe ich erst gestern getroffen, ziemlich nervig.

@metajack kannst du den ersten Beitrag so bearbeiten, dass er dieses Beispiel enthält?

Es ist noch nicht allabendlich, aber ich möchte nur sagen, dass dies jetzt kompiliert:

#![feature(nll)]

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

    match vec.first() {
        None => vec.push(5),
        Some(v) => unreachable!(),
    }
}
War diese Seite hilfreich?
0 / 5 - 0 Bewertungen