Rust: Tracking-Problem für RFC 2342, "Zulassen von `if` und `match` in Konstanten"

Erstellt am 18. März 2018  ·  83Kommentare  ·  Quelle: rust-lang/rust

Dies ist ein Tracking-Problem für den RFC "Allow if and match in Constants" (rust-lang/rfcs#2342).

Bitte leiten Sie die Zusammenfassung bestimmter Funktionen oder Probleme, die Sie melden möchten, auf neue Probleme um und kennzeichnen Sie sie entsprechend mit F-const_if_match damit diese Probleme nicht mit kurzlebigen Kommentaren überflutet werden, die wichtige Entwicklungen verdecken.

Schritte:

  • [x] Implementieren Sie den RFC
  • [ ] Dokumentation anpassen ( siehe Anleitung zum Schmieden )
  • [x] Stabilisierung PR ( siehe Anleitung zur Schmiede )
  • [x] let Bindungen in Konstanten, die && und || Kurzschlussoperationen verwenden. Diese werden derzeit als & und | innerhalb von const und static .

Ungelöste Fragen:

Keiner

A-const-eval A-const-fn B-RFC-approved C-tracking-issue F-const_if_match T-lang disposition-merge finished-final-comment-period

Hilfreichster Kommentar

Nachdem #64470 und #63812 zusammengeführt wurden, sind alle dafür notwendigen Werkzeuge im Compiler vorhanden. Ich muss noch einige Änderungen am Abfragesystem um die const-Qualifizierung vornehmen, um sicherzustellen, dass es nicht unnötig ineffizient ist, wenn diese Funktion aktiviert ist. Wir machen hier Fortschritte, und ich glaube, eine experimentelle Implementierung davon wird in Wochen, nicht in Monaten, jede Nacht verfügbar sein (berühmte letzte Worte :smile:).

Alle 83 Kommentare

  1. füge ein Feature-Gate dafür hinzu
  2. switch und switchInt Terminatoren in https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 müssen benutzerdefinierten Code enthalten falls das Feature Gate aktiv ist
  3. Anstatt einen einzigen aktuellen Basisblock (https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328) zu haben, muss dies ein Container sein, der eine Liste von enthält Grundblöcke, die es noch verarbeiten muss.

@oli-obk Es ist etwas schwieriger, weil der komplexe Kontrollfluss bedeutet, dass eine Datenflussanalyse eingesetzt werden muss. Ich muss zu @alexreg zurückkehren und herausfinden, wie ich ihre Änderungen integrieren kann.

@eddyb Ein guter Ausgangspunkt wäre wahrscheinlich, meinen const-qualif Zweig (abzüglich des obersten Commits) zu nehmen, ihn über den Master zu rebasieren (wird nicht lustig) und dann Datenanmerkungen hinzuzufügen, oder?

Gibt es Neuigkeiten zu diesem Thema?

@mark-im Leider nein. Ich denke, @eddyb war in der Tat sehr beschäftigt, weil ich ihn in den letzten Wochen nicht einmal im IRC anpingen konnte, hah. Leider wird mein const-qualif-Zweig nicht einmal kompiliert, seit ich ihn das letzte Mal über Master umbasiert habe. (Ich glaube jedoch nicht, dass ich noch gedrängt habe.)

thread 'main' panicked at 'assertion failed: position <= slice.len()', libserialize/leb128.rs:97:1
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Could not compile `rustc_llvm`.

Caused by:
  process didn't exit successfully: `/Users/alex/Software/rust/build/bootstrap/debug/rustc --crate-name build_script_build librustc_llvm/build.rs --error-format json --crate-type bin --emit=dep-info,link -C opt-level=2 -C metadata=74f2a810ad96be1d -C extra-filename=-74f2a810ad96be1d --out-dir /Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/build/rustc_llvm-74f2a810ad96be1d -L dependency=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps --extern build_helper=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps/libbuild_helper-89aaac40d3077cd7.rlib --extern cc=/Users/alex/Software/rust/build/x86_64-apple-darwin/stage1-rustc/release/deps/libcc-ead7d4af4a69e776.rlib` (exit code: 101)
warning: build failed, waiting for other jobs to finish...
error: build failed
command did not execute successfully: "/Users/alex/Software/rust/build/x86_64-apple-darwin/stage0/bin/cargo" "build" "--target" "x86_64-apple-darwin" "-j" "8" "--release" "--manifest-path" "/Users/alex/Software/rust/src/librustc_trans/Cargo.toml" "--features" " jemalloc" "--message-format" "json"
expected success, got: exit code: 101
thread 'main' panicked at 'cargo must succeed', bootstrap/compile.rs:1085:9
note: Run with `RUST_BACKTRACE=1` for a backtrace.
failed to run: /Users/alex/Software/rust/build/bootstrap/debug/bootstrap -i build

Okay, witzigerweise habe ich gerade heute wieder ein Rebase gemacht und es scheint jetzt alles gut zu bauen! Sieht so aus, als hätte es eine Regression gegeben, die gerade behoben wurde. Alles zu @eddyb jetzt.

@alexreg Entschuldigung, ich habe auf einen lokalen Schlafplan umgestellt und sehe, dass du mich beim Aufwachen angepingt hast, aber dann bist du den ganzen Tag offline, wenn ich wach bin (ugh Zeitzonen).
Soll ich einfach eine PR aus Ihrer Branche machen? Ich habe vergessen, was wir damit machen sollten?

@eddyb Das ist in Ordnung, heh. Sie müssen früh ins Bett gehen, da ich normalerweise ab 20:00 Uhr GMT da bin, aber alles ist gut! :-)

Es tut mir wirklich leid, es dauerte eine Weile, bis ich erkannte, dass die fragliche Reihe von Patches das Entfernen von Qualif::STATIC{,_REF} erfordert, dh die Fehler beim Zugriff auf die Statik zur Kompilierzeit. OTOH, dies ist bereits in Bezug auf const fn s und Zugriff auf static s gebrochen:

#![feature(const_fn)]
const fn read<T: Copy>(x: &T) -> T { *x }
static FOO: u32 = read(&BAR);
static BAR: u32 = 5;
fn main() {
    println!("{}", FOO);
}

Dies wird nicht statisch erkannt, stattdessen beschwert sich miri , dass "dangling pointer wurde dereferenziert" (was eigentlich etwas über static s anstelle von "dangling pointer" sagen sollte).

Ich denke also, dass das Lesen von static s zur Kompilierzeit in Ordnung sein sollte, aber einige Leute wollen, dass const fn zur Laufzeit "rein" (dh "referentiell transparent" oder so ungefähr) ist, was das bedeuten würde ein const fn Lesen hinter einer Referenz, die es als Argument erhalten hat, ist in Ordnung, aber ein const fn sollte niemals in der Lage sein , eine Referenz auf ein static aus dem Nichts zu erhalten (einschließlich ab const s).

Ich denke, dann können wir die Erwähnung von static s (selbst wenn es nur ihre Referenz ist) in const s, const fn und anderen konstanten Kontexten (einschließlich Beförderungen) statisch leugnen.
Aber wir müssen noch den STATIC_REF Hack entfernen, der es static s ermöglicht, die Referenz anderer static s zu übernehmen, aber (versucht und scheitert) das Lesen hinter diesen Referenzen zu verweigern .

Brauchen wir dafür einen RFC?

Klingt fair in Bezug auf die Statik. Ich bezweifle, dass es einen RFC braucht, vielleicht nur einen Kraterlauf, aber dann bin ich wahrscheinlich nicht der Beste, der das sagt.

Beachten Sie, dass wir nichts einschränken würden, wir würden eine Einschränkung lockern, die bereits gebrochen ist.

Oh, ich habe falsch gelesen. Eine const-Auswertung wäre also immer noch solide, nur nicht referentiell transparent?

Der letzte Absatz beschreibt einen referenziell transparenten Ansatz (aber wir verlieren diese Eigenschaft, wenn wir anfangen, static s in const s und const fn s zu erwähnen). Ich glaube nicht, dass Solidität wirklich diskutiert wurde.

Nun, "baumelnder Zeiger" klingt sicher nach einem Problem mit der Solidität, aber ich vertraue Ihnen diesbezüglich!

"dangling pointer" ist eine schlechte Fehlermeldung, das ist nur miri, das das Lesen von static s verbietet. Die einzigen konstanten Kontexte, die sogar auf static s verweisen können , sind andere static s, also könnten wir diese Lesevorgänge "nur" erlauben, da der gesamte Code immer einmal zur Kompilierzeit ausgeführt wird.

(vom IRC) Zusammenfassend lässt sich sagen, dass referenziell transparente const fn immer nur eingefrorene Zuweisungen erreichen könnten, ohne Argumente zu durchlaufen, was bedeutet, dass const dieselbe Einschränkung benötigt und nicht eingefrorene Zuweisungen nur von static s.

Ich mag es, referenzielle Transparenz zu bewahren, also klingt die Idee von

Ja, ich bin auch ein Profi, der Const Fns rein macht.

Bitte beachten Sie, dass bestimmte scheinbar harmlose Pläne die referentielle Transparenz ruinieren können, z. B.:

let x = 0;
let non_deterministic = &x as *const _ as usize;
if non_deterministic.count_ones() % 2 == 0 {
    // do one thing
} else {
    // do a completely different thing
}

Dies würde zur Kompilierzeit mit einem miri-Fehler fehlschlagen, wäre aber zur Laufzeit nicht deterministisch (da wir diese Speicheradresse nicht wie bei Miri als "abstrakt" markieren können).

EDIT: @Centril hatte die Idee, bestimmte Rohzeiger (wie Vergleiche und Würfe auf ganze Zahlen) unsafe innerhalb von const fn ) und geben an, dass sie nur auf eine Weise verwendet werden können, die miri zur Kompilierzeit erlauben würde.
Zum Beispiel sollte das Subtrahieren von zwei Zeigern in dasselbe Local in Ordnung sein (Sie erhalten einen relativen Abstand, der nur vom Typlayout, den Array-Indizes usw. abhängt), aber das Formatieren der Adresse einer Referenz (über {:p} ) ist eine falsche Verwendung und daher kann fmt::Pointer::fmt nicht als const fn markiert werden.
Außerdem kann keines der Ord / Eq Trait Impls für Rohzeiger als const markiert werden (wenn wir die Möglichkeit haben, sie als solche zu kommentieren), weil sie sicher sind aber die Operation ist unsafe in const fn .

Hängt davon ab, was Sie mit "harmlos" meinen ... Ich kann sicherlich sehen, warum wir ein solches nicht deterministisches Verhalten verbieten möchten.

Es wäre fantastisch, wenn daran weitergearbeitet würde.

@lachlansneff Es geht um... nicht so schnell wie wir es gerne hätten, aber es wird gearbeitet. Momentan warten wir auf https://github.com/rust-lang/rust/pull/51110 als Blocker.

@alexreg Ah, danke. Es wäre sehr nützlich, eine Übereinstimmung oder wenn als Konstante markieren zu können, auch wenn nicht in einer Konstanten fn.

Gibt es Statusaktualisierungen, nachdem #51110 zusammengeführt wurde?

@programmerjake Ich warte auf ein Feedback von @eddyb auf https://github.com/rust-lang/rust/pull/52518, bevor es zusammengeführt werden kann (hoffentlich sehr bald). Er war in letzter Zeit sehr beschäftigt (immer sehr gefragt), aber er hat sich in den letzten Tagen wieder auf Kritiken und so weiter konzentriert, also bin ich hoffnungsvoll. Danach wird es, vermute ich, einige Arbeit von ihm persönlich erfordern, da das Hinzufügen einer richtigen Datenflussanalyse eine komplizierte Angelegenheit ist. Wir werden sehen.

Irgendwo zu den TODO-Listen in den ersten Posts sollte es hinzugefügt werden, um den aktuellen schrecklichen Hack zu entfernen, der && und || in & und | innerhalb von Konstanten.

@RalfJung War das nicht Teil des alten

AFAIK machen wir diese Übersetzung irgendwo in der HIR-Absenkung, weil wir Code in const_qualify , der SwitchInt Terminatoren ablehnt, die sonst von || / && generiert würden.

Außerdem noch ein Punkt: @oli-obk hat irgendwo (aber ich kann nicht finden, wo) gesagt, dass Bedingungen irgendwie komplizierter sind, als man naiv denken würde ...

ging es da "nur" um die analyse der abfall-/innenveränderlichkeit?

Das versuche ich gerade aufzuklären. Melde mich wieder wenn ich alle Infos habe

Wie ist der Stand davon? Benötigt dies Personal oder ist es bei der Lösung eines Problems blockiert?

@mark-im Es ist blockiert bei der Implementierung der richtigen Datenflussanalyse für die const-Qualifizierung. @eddyb ist der @eddyb immer noch keine Zeit hat, könnten @oli-obk oder @RalfJung das vielleicht bald in Angriff nehmen. :-)

58403 ist ein kleiner Schritt zur datenflussbasierten Qualifizierung.

@eddyb Sie haben erwähnt, dass die referenzielle Transparenz in const fn bleibt, was meiner Meinung nach eine gute Idee ist. Was wäre, wenn Sie die Verwendung von Zeigern in const fn ? Ihr vorheriges Codebeispiel würde also nicht mehr kompiliert:

let x = 0;
// compile time error: cannot cast reference to pointer in `const fun`
let non_deterministic = &x as *const _ as usize;
if non_deterministic.count_ones() % 2 == 0 {
    // do one thing
} else {
    // do a completely different thing
}

Referenzen wären weiterhin erlaubt, aber Sie könnten sie nicht selbst einsehen:

let x = 0;
let p = &x;
if *p != 0 {  // this is fine
    // do one thing
} else {
    // do a completely different thing
}

Lassen Sie es mich wissen, wenn ich völlig daneben liege, ich dachte nur, dies wäre eine gute Möglichkeit, dies deterministisch zu machen.

@jyn514 , das bereits abgedeckt ist, indem as usize casts instabil gemacht wird (https://github.com/rust-lang/rust/issues/51910), aber Benutzer können auch

Gibt es dazu Neues?

Es gibt einige Diskussionen auf https://rust-lang.zulipchat.com/#narrow/stream/146212 -t-compiler.2Fconst-eval/topic/dataflow-based.20const.20qualification.20MVP

@oli-obk dein Link funktioniert nicht. Was sagt es?

Bei mir funktioniert es... Sie müssen sich jedoch bei Zulip anmelden.

@alexreg hmm ja, ich @alexreg wissen Sie, warum es für if und match in Konstanten benötigt wird?

Wenn wir keine datenflussbasierte Version haben, lassen wir entweder versehentlich &Cell<T> in Konstanten zu oder verbieten versehentlich None::<&Cell<T>> (was auf Stable funktioniert. Es ist im Wesentlichen unmöglich, ohne Datenfluss richtig zu implementieren (oder jede Implementierung wird es) eine schlechte defekte Ad-hoc-Version des Datenflusses sein)

@est31 Nun, @oli-obk versteht das viel besser als ich, aber von einer hohen Ebene aus wird im Grunde alles, was mit Verzweigungen zu tun hat, eine Datenflussanalyse prädizieren, es sei denn, Sie möchten eine Reihe von Grenzfällen. Wie auch immer, es scheint, als würde diese Person auf Zulip versuchen, daran zu arbeiten, und wenn nicht, weiß ich, dass oli-obk und eddyb Absichten haben, vielleicht diesen oder nächsten Monat (seitdem ich das letzte Mal mit ihnen darüber gesprochen habe), obwohl ich es kann 't/will keine Versprechungen in ihrem Namen machen.

@alexreg @mark-im @est31 @oli-obk Ich sollte irgendwann in dieser Woche in der Lage sein, meine WIP-Implementierung der datenflussbasierten const-Qualifizierung zu veröffentlichen. Hier gibt es viele Kompatibilitätsrisiken, daher kann es eine Weile dauern, bis es tatsächlich zusammengeführt wird.

Super; Ich freue mich auf.

(Kopieren von #57563 pro Anfrage)

Wäre es möglich, bool && bool , bool || bool usw. in Sonderfälle zu schreiben? Sie können derzeit in einem const fn , dies erfordert jedoch bitweise Operatoren, was manchmal unerwünscht ist.

Sie sind bereits in const und static -Elementen in Sonderzeichen geschrieben – indem sie in bitweise Operationen übersetzt werden. Aber dieses Spezialgehäuse ist ein riesiger Hack und es ist sehr schwer sicherzustellen, dass dies tatsächlich richtig ist. Wie Sie sagten, ist es manchmal auch unerwünscht. Deshalb machen wir das lieber nicht öfter.

Es wird ein bisschen dauern, die Dinge richtig zu machen, aber es wird passieren. Wenn wir in der Zwischenzeit zu viele Hacks anhäufen, könnten wir uns in eine Ecke quälen, aus der wir nicht herauskommen (wenn einige dieser Hacks falsch interagieren und so versehentlich ein Verhalten stabilisieren, das wir nicht wollen).

Nachdem #64470 und #63812 zusammengeführt wurden, sind alle dafür notwendigen Werkzeuge im Compiler vorhanden. Ich muss noch einige Änderungen am Abfragesystem um die const-Qualifizierung vornehmen, um sicherzustellen, dass es nicht unnötig ineffizient ist, wenn diese Funktion aktiviert ist. Wir machen hier Fortschritte, und ich glaube, eine experimentelle Implementierung davon wird in Wochen, nicht in Monaten, jede Nacht verfügbar sein (berühmte letzte Worte :smile:).

@ecstatic-morse Schön zu hören! Vielen Dank für Ihre konzertierten Bemühungen, dies zu erreichen; Ich persönlich bin schon seit einiger Zeit an dieser Funktion interessiert.

Würde gerne eine Unterstützung für die Heap-Zuweisung für CTFE sehen, nachdem dies erledigt ist. Ich weiß nicht, ob Sie oder jemand anderes daran interessiert ist, daran zu arbeiten, aber wenn nicht, kann ich vielleicht helfen.

@alexreg Danke!

Die Diskussion über die Heap-Zuweisung zur Kompilierzeit ist unter rust-rfcs/const-eval#20 beendet. AFAIK, die jüngsten Entwicklungen waren ungefähr ein ConstSafe / ConstRefSafe Paradigma, um zu bestimmen, was direkt/hinter einer Referenz im Endwert eines const . Ich denke, es ist jedoch noch mehr Designarbeit erforderlich.

Für diejenigen, die mitmachen, ist #65949 (die selbst von ein paar kleineren PRs abhängt) der nächste Blocker dafür. Auch wenn es nur tangential zusammenhängt, war die Tatsache, dass die ständige Überprüfung/Qualifizierung so eng mit der Beförderung verbunden war, einer der Gründe, warum diese Funktion so lange blockiert war. Ich plane, eine nachfolgende PR zu eröffnen, die den alten const-checker vollständig entfernt (derzeit führen wir beide Checker parallel aus). Dadurch werden die oben erwähnten Ineffizienzen vermieden.

Nachdem die beiden oben genannten PRs zusammengeführt wurden, werden if und match in Konstanten ein paar Diagnoseverbesserungen und ein Feature-Flag entfernt sein! Oh, und auch Tests, so viele Tests...

Wenn Sie Tests benötigen, bin ich mir nicht sicher, wie ich anfangen soll, aber ich bin mehr als bereit, dazu beizutragen! Lassen Sie mich einfach wissen, wohin die Tests gehen sollen / wie sie aussehen sollten / auf welchem ​​​​Zweig ich den Code aufbauen soll :)

Der nächste PR, den Sie sich ansehen sollten, ist #66385. Dadurch wird die alte const-Qualifizierungslogik (die keine Verzweigungen verarbeiten konnte) vollständig zugunsten der neuen datenflussbasierten Version entfernt.

@jyn514 Das wäre toll! Ich werde Sie anpingen, wenn ich mit dem Entwurf der Implementierung beginne. Es wäre auch sehr hilfreich für Leute zu versuchen, die const-Sicherheit (insbesondere den HasMutInterior Teil) zu verletzen, sobald if und match am Abend verfügbar sind.

66507 enthält eine erste Implementierung von RFC 2342.

Ich gehe davon aus, dass es eine Weile dauern wird, die Ecken und Kanten zu entfernen, insbesondere in Bezug auf die Diagnose, und die Testabdeckung ist ziemlich spärlich ( @jyn514 wir sollten uns bei diesem Problem

Dies wurde in #66507 implementiert und kann nun in der neuesten Nightly verwendet werden . Es gibt auch einen Inside Rust-Blog-Post , der die neu verfügbaren Operationen sowie einige Probleme beschreibt, die bei der vorhandenen Implementierung von Typen mit innerer Veränderlichkeit oder einer benutzerdefinierten Drop Impl.

Geh hin und konstituiere!

Es scheint, dass Gleichheit nicht const ? Oder irre ich mich:

error[E0019]: constant function contains unimplemented expression type
  --> src/liballoc/raw_vec.rs:55:22
   |
55 |         let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
   |                      ^^^^^^^^^^^^^^^^^^^^^^^^

error[E0019]: constant function contains unimplemented expression type
  --> src/liballoc/raw_vec.rs:55:19
   |
55 |         let cap = if mem::size_of::<T>() == 0 { !0 } else { 0 };
   |                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error: aborting due to 2 previous errors

@mark-im Das sollte in der Tat funktionieren . Vielleicht ein Bootstrapping-Problem? Lassen Sie uns über Zulip diskutieren.

Ich bin mir nicht sicher, ob dies beabsichtigt ist, aber der Versuch, eine Aufzählung zu finden, führt zu einem Fehler

const fn mit nicht erreichbarem Code ist nicht stabil

trotz der Tatsache, dass die Aufzählung erschöpfend ist und in derselben Kiste definiert ist.

@jhpratt kannst du den Code posten? Ich kann problemlos auf einfache Enumerationen abgleichen: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=585e9c2823afcb49c6682f69569c97ea

@jhpratt kannst du den Code posten? Ich kann problemlos auf einfache Enumerationen abgleichen:

Hier:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=13a9fbc4251d7db80f5d63b1dc35a98b

Schlag mich um ein paar Sekunden. Das ist ein minimales Beispiel, das meinen genauen Fall demonstriert.

@jhpratt Definitiv nicht beabsichtigt. Könnten Sie ein Problem eröffnen?

Bitte leiten Sie die Zusammenfassung bestimmter Funktionen oder Probleme, die Sie melden möchten, auf neue Probleme um und kennzeichnen Sie sie entsprechend mit F-const_if_match damit diese Probleme nicht mit kurzlebigen Kommentaren überflutet werden, die wichtige Entwicklungen verdecken.

@Centril Keine schlechte Sache in den Top-Kommentar, damit Ihrer nicht begraben wird.

Status-Update:

Dies ist aus Implementierungssicht zur Stabilisierung bereit, aber es stellt sich die Frage, ob wir den wertbasierten Datenfluss, den wir derzeit haben, beibehalten möchten, anstatt einen typbasierten (aber weniger leistungsstarken) zu verwenden. Der wertbasierte Datenfluss ist etwas teurer (dazu weiter unten mehr), und wir brauchen ihn für Funktionen wie

const fn foo<T>() {
    let x = Option::<T>::None;
    {x};
}

was eine typbasierte Analyse ablehnen würde, weil ein Option<T> Destruktoren haben könnte, die nun versuchen würden, zu laufen, und daher möglicherweise nicht konstanten Code ausführen.

Wir können auf eine typbasierte Analyse zurückgreifen, sobald es Verzweigungen gibt, aber das würde bedeuten, dass wir ablehnen

const fn foo<T>(b: bool) {
    let x = Option::<T>::None;
    assert!(b);
    {x};
}

was die Benutzer wahrscheinlich sehr überraschen würde.

@ecstatic-morse hat die Analyse für alle Funktionen ausgeführt, nicht nur für const fn und sah Verlangsamungen von bis zu 5 % (https://perf.rust-lang.org/compare.html?start=93dc97a85381cc52eb872d27e50e4d518926a27c&end=51cf313c7946365d5be38113950f2f3) Beachten Sie, dass dies eine pessimistische Version ist, da dies bedeutet, dass diese auch auf Funktionen ausgeführt wird, die nie const fn werden und oft nicht werden können.

Das bedeutet, dass, wenn wir viele Funktionen const fn erstellen, wir aufgrund dieser wertbasierten Analyse einige Kompilierungsverlangsamungen feststellen können.

Ein Mittelweg könnte darin bestehen, die wertbasierte Analyse nur auszuführen, wenn die typbasierte Analyse fehlschlägt. Dies bedeutet, dass wir, wenn keine Destruktoren vorhanden sind, die wertbasierte Analyse nicht ausführen müssen, um herauszufinden, ob die nicht vorhandenen Destruktoren nicht ausgeführt werden (ja, ich weiß, viele Negationen hier). Anders ausgedrückt: Wir führen die wertbasierte Analyse nur dann durch, wenn Destruktoren vorhanden sind.

Ich nominiere das für die @rust-lang/lang-Diskussion, damit wir herausfinden können, ob wir mitmachen wollen

  • die typbasierte Option bei Vorhandensein von Schleifen oder Verzweigungen (was den Benutzern ein seltsames Verhalten gibt)
  • vollständige wertbasierte Analyse (teurer, aber volle Aussagekraft für Benutzer)
  • gemischtes Schema, immer noch volle Ausdruckskraft für Benutzer, etwas zusätzliche implizite Komplexität, sollte aber die Kompilierzeitprobleme auf die Fälle reduzieren, die es erfordern.

@oli-obk

die typbasierte Option bei Vorhandensein von Schleifen oder Verzweigungen (was den Benutzern ein seltsames Verhalten gibt)

Zur Überprüfung: Ist eine typbasierte Analyse nicht auch im linearen Code eine Option? Ich stelle mir das irgendwie abwärtskompatibel vor, da wir bereits Folgendes akzeptieren ( Spielplatz ):

struct Foo { }

impl Drop for Foo {
    fn drop(&mut self) { }
}

const T: Option<Foo> = None;

fn main() { }

Persönlich denke ich, dass wir auf eine konsistentere, bessere Erfahrung für die Benutzer drängen sollten. Es scheint, als könnten wir nach Bedarf optimieren, und die Kosten sind auf jeden Fall nicht zu hoch. Aber ich würde gerne etwas besser verstehen, was genau bei dieser teureren Analyse passiert: Ist die Idee, dass wir im Grunde eine "konstante Ausbreitung" machen, so dass wir jedes Mal, wenn etwas fallengelassen wird, den genauen Wert analysieren, der fallengelassen wird, um festzustellen, ob es kann einen Wert enthalten, der einen Destruktor ausführen müsste? (dh wenn es None ist, um das übliche Beispiel von Option<T> )

Zur Überprüfung: Ist eine typbasierte Analyse nicht auch im linearen Code eine Option? Ich stelle mir das irgendwie abwärtskompatibel vor, da wir bereits Folgendes akzeptieren (Spielplatz):

Ja, das ist der Grund, warum wir nicht einfach auf die typbasierte Analyse übergehen können.

ist die Idee, dass wir im Grunde eine "konstante Ausbreitung" durchführen, so dass wir jedes Mal, wenn etwas gelöscht wird, den genauen Wert analysieren, der gelöscht wird, um festzustellen, ob er einen Wert enthält, der einen Destruktor ausführen muss? (dh wenn es None ist, um das allgemeine Beispiel von Option . zu verwenden)

Wir propagieren nur eine Liste von Flags ( Drop und Freeze , ich habe hier nur Drop , weil es einfacher zu erklären ist). Wenn wir einen Drop Terminator erreichen, ohne das Drop Flag gesetzt zu haben, ignorieren wir den Drop Terminator. Dies ermöglicht Code wie den folgenden:

{
    let mut x = None;
    // Drop flag for x: false
    let y = Some(Foo);
    // Drop flag for y: true
    x = y; // Dropping x is fine, because Drop flag for x is false
    // Drop flag for y: false, Drop flag for x: true
    x
    // Dropping y is fine, because Drop flag for y is false
}

Dies geschieht zum Zeitpunkt der Bewertung nicht, daher ist Folgendes nicht in Ordnung:

{
    let mut x = Some(Foo);
    if false {
        x = None;
    }
    x
}

Wir prüfen, ob alle möglichen Ausführungspfade Drop .

Konstante Ausbreitung ist jedoch eine gute Analogie. Es ist ein weiteres Datenflussproblem, dessen Übertragungsfunktion nicht mit Gen/Kill-Sets ausgedrückt werden kann, die das Kopieren von Zuständen zwischen Variablen nicht handhaben. Die konstante Weitergabe muss jedoch den tatsächlichen Wert jeder Variablen speichern, aber die konstante Überprüfung muss nur ein einzelnes Bit speichern, das angibt, ob diese Variable eine benutzerdefinierte Drop Impl hat oder nicht Freeze sie erzeugt etwas billiger als die ständige Vermehrung wäre.

Um es klar zu sagen, das erste Beispiel von @oli-obk wird heute auf Stable kompiliert und hat seit 1.38.0 , das #64470 nicht enthielt.

Außerdem wird const X: Option<Foo> = None; seit 1.0 kompiliert, alles andere ist nur eine natürliche Erweiterung davon mit den neuen Funktionen, die const eval gewonnen hat.

Okay, ich glaube, es macht dann Sinn, die rein wertbasierte Option zu wählen.

Ich denke, wir können es im Meeting besprechen und zurückmelden =)

Zusammenfassung

Ich schlage vor, dass wir #![feature(const_if_match)] mit der aktuellen Semantik stabilisieren.

Insbesondere werden die Ausdrücke if und match sowie die Verknüpfungsoperatoren && und || in allen const-Kontexten zulässig. Ein const-Kontext ist einer der folgenden:

  • Der Initialisierer einer const , static , static mut oder Aufzählungsdiskriminante.
  • Der Körper eines const fn .
  • Der Wert eines const generischen (nur nächtlich).
  • Die Länge eines Array-Typs ( [u8; 3] ) oder eines Array-Wiederholungsausdrucks ( [0u8; 3] ).

Darüber hinaus werden die Kurzschluss-Logikoperatoren in den Initialisierern const und static nicht mehr auf ihre bitweisen Äquivalente ( & bzw. | ) reduziert (siehe #57175). Als Ergebnis können let Bindungen neben der Kurzschlusslogik in diesen Initialisierern verwendet werden.

Tracking-Problem: #49146
Versionsziel: 1.45 (16.06.2020)

Implementierungshistorie

64470 implementierte eine wertbasierte statische Analyse, die bedingten Kontrollfluss unterstützte und auf Datenfluss basierte. Dies ermöglichte uns zusammen mit #63812, den alten const-checking-Code durch einen Code zu ersetzen, der bei komplexen Kontrollflussdiagrammen funktionierte. Der alte const-checker wurde zeitweise parallel zum datenflussbasierten ausgeführt, um sicherzustellen, dass man sich auf Programme mit einfacher Kontrollfluss einigen konnte. #66385 entfernte den alten const-checker zugunsten des datenflussbasierten.

66507 hat das Feature-Gate #![feature(const_if_match)] mit der Semantik implementiert, die jetzt zur Stabilisierung vorgeschlagen wird.

Const-Qualifikation

Hintergrund

[Miri] unterstützt seit mehreren Jahren die Compile-Time Function Evaluation (CTFE) in rustc und ist seit mindestens so lange in der Lage, bedingte Anweisungen auszuwerten. Während CTFE müssen wir bestimmte Operationen vermeiden, wie das Aufrufen von benutzerdefinierten Drop Impls oder das innerer Veränderlichkeit . Zusammen werden diese disqualifizierenden Eigenschaften als "Qualifikationen" bezeichnet, und der Prozess der Bestimmung, ob ein Wert an einem bestimmten Punkt im Programm eine Qualifikation hat, wird als "const-Qualifikation" bezeichnet.

Miri ist durchaus in der Lage, einen Fehler auszugeben, wenn eine unzulässige Operation für einen qualifizierten Wert festgestellt wird, und dies ohne Fehlalarme. CTFE tritt jedoch nach der Monomorphisierung auf, was bedeutet, dass es nicht wissen kann, ob in einem generischen Kontext definierte Konstanten gültig sind, bis sie instanziiert werden, was in einer anderen Kiste passieren könnte. Um Fehler vor der Monomorphisierung zu erhalten, müssen wir eine statische Analyse implementieren, die eine konstante Qualifizierung durchführt. Im allgemeinen Fall ist die const-Qualifikation unentscheidbar (siehe Rice's Theorem ), daher kann jede statische Analyse die Prüfungen, die Miri während des CTFE durchführt, nur annähern.

Unsere statische Analyse muss verhindern, dass ein Verweis auf einen Typ mit innerer Veränderlichkeit (zB &Cell<i32> ) im Endwert von const auftaucht. Wenn dies zulässig wäre, könnte ein const zur Laufzeit geändert werden.

const X: &std::cell::Cell<i32> = std::cell::Cell::new(0);

fn main() {
  X.get(); // 0
  X.set(42);
  X.get(); // 42
}

Wir erlauben dem Benutzer jedoch, ein const zu definieren, dessen Typ eine innere Veränderlichkeit ( !Freeze ) hat, solange wir beweisen können, dass der Endwert dieses const dies nicht tut. Zum Beispiel hat sich seit der ersten Ausgabe von stable rost folgendes zusammengestellt:

const _X: Option<&'static std::cell::Cell<i32>> = None;

Dieser Ansatz der statischen Analyse, den ich als wertbasiert und nicht als typbasiert bezeichnen werde, wird auch verwendet, um nach Code zu suchen, der dazu führen kann, dass ein benutzerdefiniertes Drop Impl aufgerufen wird. Der Aufruf von Drop Impls ist problematisch, da sie nicht const-geprüft werden und daher Code enthalten können, der in einem const-Kontext nicht erlaubt wäre. Die wertbasierte Argumentation wurde erweitert, um let Anweisungen zu unterstützen, was bedeutet, dass die folgenden Kompilierungen auf rust 1.42.0 stable .

const _: Option<Vec<i32>> = {
  let x = None;
  let mut y = x;
  y = Some(Vec::new()); // Causes the old value in `y` to be dropped.
  y
};

Aktuelle nächtliche Semantik

Das aktuelle Verhalten von #![feature(const_if_match)] erweitert die wertbasierte Semantik auf die Arbeit mit komplexen Kontrollflussdiagrammen unter Verwendung von Datenfluss. Mit anderen Worten, wir versuchen zu beweisen, dass eine Variable nicht auf allen möglichen Pfaden durch das Programm die fragliche Qualifikation besitzt.

enum Int {
    Zero,
    One,
    Many(String), // Dropping this variant is not allowed in a `const fn`...
}

// ...but the following code is legal under this proposal...
const fn good(x: i32) {
    let i = match x {
        0 => Int::Zero,
        1 => Int::One,
        _ => return,
    };

    // ...because `i` is never `Int::Many` on any possible path through the program.
    std::mem::drop(i);
}

Alle möglichen Wege durch das Programm beinhalten solche, die in der Praxis vielleicht nie erreicht werden. Ein Beispiel mit derselben Int Enumeration wie oben:

const fn bad(b: bool) {
    let i = if b == true {
        Int::One
    } else if b == false {
        Int::Zero
    } else {
        // This branch is dead code. It can never be reached in practice.
        // However, const qualification treats it as a possible path because it
        // exists in the source.
        Int::Many(String::new())
    };

    // ILLEGAL: `i` was assigned the `Int::Many` variant on at least one code path.
    std::mem::drop(i);
}

Diese Analyse behandelt Funktionsaufrufe als undurchsichtig, vorausgesetzt, ihr Rückgabewert kann einen beliebigen Wert seines Typs enthalten. Wir greifen auch für eine Variable auf eine typbasierte Analyse zurück, sobald eine veränderliche Referenz darauf erstellt wird. Beachten Sie, dass das Erstellen einer veränderlichen Referenz in einem const-Kontext derzeit auf stabilem Rost verboten ist.

#![feature(const_mut_refs)]

const fn none() -> Option<Cell<i32>> {
    None
}

// ILLEGAL: We must assume that `none` may return any value of type `Option<Cell<i32>>`.
const BAD: &Option<Cell<i32>> = none();

const fn also_bad() {
    let x = Option::<Box<i32>>::None;

    let _ = &mut x;

    // ILLEGAL: because a mutable reference to `x` was created, we can no
    // longer assume anything about its value.
    std::mem::drop(x)
}

Sie können weitere Beispiele dafür sehen, wie eine wertbasierte Analyse in innere Veränderlichkeit und benutzerdefinierte Drop-Impls konservativ ist, sowie einige Fälle, in denen eine konservative Analyse beweisen kann, dass in der Testsuite

Alternativen

Ich fand es schwierig, praktische, abwärtskompatible Alternativen zum bestehenden Ansatz zu finden. Wir könnten für alle Variablen auf die typbasierte Analyse zurückgreifen, sobald Bedingungen in einem const-Kontext verwendet werden. Dies wäre den Benutzern jedoch auch schwer zu erklären, da scheinbar unzusammenhängende Zusätze dazu führen würden, dass Code nicht mehr kompiliert wird, wie etwa assert im folgenden Beispiel von @oli-obk.

const fn foo<T>(b: bool) {
    let x = Option::<T>::None;
    assert!(b);
    {x};
}

Die gesteigerte Aussagekraft der wertorientierten Analyse ist nicht kostenlos. Ein Perf-Lauf, der eine konstante Qualifizierung für alle Item-Bodys durchführte, nicht nur const , zeigte eine Regression von bis zu const werden. Mögliche Optimierungen, wie die in #71330, wurden weiter oben im Thread besprochen.

Zukünftige Arbeit

Im Moment wird const-checking vor der Drop-Elaboration ausgeführt, was bedeutet, dass einige Drop-Terminatoren in der MIR verbleiben, die in der Praxis nicht erreichbar sind. Dadurch wird verhindert, dass aus Option::unwrap const fn (siehe #66753). Dies ist nicht allzu schwer zu lösen, erfordert jedoch eine Aufteilung des Const-Checking-Passes in zwei Phasen (Ausarbeitung vor und nach dem Drop).

Sobald #![feature(const_if_match)] stabilisiert ist, können viele Bibliotheksfunktionen zu const fn . Dazu gehören viele Methoden für primitive Integer-Typen, die in #53718 aufgezählt wurden.

Schleifen in einem const-Kontext werden bei derselben const-Qualifikationsfrage wie Bedingungen blockiert. Der aktuelle datenflussbasierte Ansatz funktioniert auch für zyklische CFGs ohne Modifikationen. Wenn also #![feature(const_if_match)] stabilisiert ist, wird der Hauptblocker für #52000 weg sein.

Danksagung

Besonderer Dank gilt @oli-obk und @eddyb , die die All dies wäre ohne Miri nicht möglich, die von @RalfJung und @oli-obk gepflegt wird.

Dies soll der Stabilisierungsbericht vor dem FCP sein. Ich kann FCP jedoch nicht öffnen.

@ecstatic-morse Vielen Dank für Ihre harte Arbeit an diesem Thema!

Toller Bericht!

Eine Sache, von der ich denke, dass ich sie gerne sehen würde, @ecstatic-morse, ist

  • Links zu einigen repräsentativen Tests im Repo, damit wir das Verhalten beobachten können
  • ob es Auswirkungen auf den Semver oder etwas anderes gibt - ich denke, die Antwort ist weitgehend nein , oder? Mit anderen Worten, wir entscheiden über die Analyse, die verwendet wird, um zu bestimmen, ob der Körper eines const fn zulässig ist, aber bei einem const fn bestimmen unsere Entscheidungen hier nicht Dinge wie "was der Aufrufer von const fn tun kann". das Ergebnis", oder? Ich versuche herauszufinden, was ein Beispiel für das sein könnte, wovon ich spreche – ich nehme an, es wäre, dass der Aufrufer nicht genau erfährt, welche Varianten einer Aufzählung verwendet wurden, nur dass – welcher Wert auch immer zurückgegeben wurde -- es hatte keine innere Veränderlichkeit (auf die sie sich vermutlich auch beim Abgleichen nicht verlassen können, da).

Mit anderen Worten, wir entscheiden über die Analyse, die verwendet wird, um zu bestimmen, ob der Körper eines const fn zulässig ist, aber bei einem const fn bestimmen unsere Entscheidungen hier nicht Dinge wie "was der Aufrufer von const fn tun kann". das Ergebnis", oder? Ich versuche herauszufinden, was ein Beispiel für das sein könnte, wovon ich spreche – ich nehme an, es wäre, dass der Aufrufer nicht genau erfährt, welche Varianten einer Aufzählung verwendet wurden, nur dass – welcher Wert auch immer zurückgegeben wurde -- es hatte keine innere Veränderlichkeit (auf die sie sich vermutlich auch beim Abgleichen nicht verlassen können, da).

Ja, der Körper einer const fn ist undurchsichtig. Dies steht im Gegensatz zum Initialisierungsausdruck eines const -Elements. Sie können dies daran erkennen, dass

const FOO: Option<Cell<i32>> = None;

kann verwendet werden, um ein &'static Option<Cell<i32>> zu erstellen

const BAR: &'static Option<Cell<i32>> = &FOO;

während ein const fn mit demselben Körper nicht:

const fn foo() -> Option<Cell<i32>> { None }
const BAR: &'static Option<Cell<i32>> = &foo();

Spielplatz-Demo

Wenn wir den Kontrollfluss in Konstanten einführen, bedeutet dies, dass

const FOO: Option<Cell<i32>> = if MEH { None } else { None };

funktioniert auch, unabhängig vom Wert von MEH und

const FOO: Option<Cell<i32>> = if MEH { Some(Cell::new(42)) } else { None };

wird nicht funktionieren, unabhängig vom Wert von MEH .

Die Ablaufsteuerung ändert nichts an den Aufrufstellen von const fn , sondern nur daran, welcher Code in dieser const fn zulässig ist.

Links zu einigen repräsentativen Tests im Repo, damit wir das Verhalten beobachten können.

Ich habe am Ende des Abschnitts "Current Nightly Semantics" einen Absatz hinzugefügt, der auf einige interessante Testfälle verweist. Ich habe das Gefühl, dass wir mehr Tests brauchen (eine Aussage, die unabhängig von den Umständen zutrifft), bevor sich dies stabilisiert, aber das kann angegangen werden, sobald wir entschieden haben, ob die aktuelle Semantik wünschenswert ist.

ob es Auswirkungen auf den Semver oder etwas anderes gibt.

Zusätzlich zu dem, was @oli-obk oben gesagt hat, möchte ich darauf hinweisen, dass das Ändern des Endwerts von const technisch bereits eine schwerwiegende Änderung ist:

// Upstream crate
const IDX: usize = 1; // Changing this to `3` will break downstream code!

// Downstream crate

extern crate upstream;

const X: i32 = [0, 1, 2][upstream::IDX]; // Only compiles if `upstream::IDX <= 2`

Da wir jedoch konst Qualifikation mit perfekter Präzision nicht tun können, um eine konstante Veränderung verwenden if oder match Downstream - Code brechen könnte, auch wenn der endgültige Wert ändert sich nicht. Beispielsweise:

// Changing from `cfg` attributes...

#[cfg(not(FALSE))]
const X: Option<Vec<i32>> = None;
#[cfg(FALSE)]
const X: Option<Vec<i32>> = Some(Vec::new());

// ...to the `cfg` macro...

const X: Option<Vec<i32>> = if !cfg!(FALSE) { None } else { Some(Vec::new() };

// ...could break downstream crates, even though `X` is still `None`!

// Downstream

 // Only legal if static analysis can prove the qualifications in `X`
const _: () =  std::mem::drop(upstream::X); 

Dies gilt nicht für Änderungen im Rumpf von const fn , da wir immer eine typbasierte Qualifizierung für den Rückgabewert verwenden, selbst innerhalb derselben Crate.

Meiner Ansicht nach bestand die "Erbsünde" hier nicht darin, auf eine typbasierte Qualifizierung für const und static die in externen Kisten definiert sind. Ich glaube jedoch, dass dies seit 1.0 der Fall ist, und ich vermute, dass ziemlich viel Code davon abhängt. Sobald Sie const-Initialisatoren zulassen, für die die statische Analyse nicht ganz genau sein kann, ist es möglich, diese Initialisierer so zu ändern, dass sie den gleichen Wert liefern, ohne dass die statische Analyse dies beweisen kann.

bearbeiten:

In dieser Hinsicht gibt es nichts Einzigartiges an if und match . Beispielsweise ist es derzeit eine bahnbrechende Änderung, einen const Initialisierer in einen const fn umzugestalten, wenn die nachgelagerte Crate auf einer wertbasierten Qualifizierung beruhte.

// Upstream
const fn none<T>() -> Option<T> { None }

const VALUE_BASED: Option<Vec<i32>> = None;
const TYPE_BASED: Option<Vec<i32>> = none();

// Downstream

const OK: () = { std::mem::drop(upstream::VALUE_BASED); };
const ERROR: () = { std::mem::drop(upstream::TYPE_BASED); };

@ecstatic-morse Danke für das Verfassen des Stabilisierungsberichts! Lassen Sie uns den Konsens asynchron messen:

@rfcbot zusammenführen

Wenn jemand dies synchron in einem Meeting besprechen möchte, bitte erneut nominieren.

Teammitglied @joshtriplett hat vorgeschlagen, dies zusammenzuführen. Der nächste Schritt ist die Überprüfung durch den Rest der markierten Teammitglieder:

  • [x] @cramertj
  • [x] @joshtriplett
  • [x] @nikomatsakis
  • [x] @pnkfelix
  • [ ] @scottmcm
  • [ ] @ohneboote

Derzeit keine Bedenken aufgeführt.

Sobald die Mehrheit der Gutachter zugestimmt hat (und höchstens 2 Genehmigungen ausstehen), tritt die letzte Kommentierungsphase ein. Wenn Sie ein wichtiges Problem entdecken, das zu keinem Zeitpunkt in diesem Prozess angesprochen wurde, melden Sie sich bitte!

In diesem Dokument finden Sie Informationen darüber, welche Befehle mir markierte Teammitglieder geben können.

:bell: Dies geht jetzt in seine letzte Kommentarperiode , wie in der obigen Überprüfung beschrieben . :Glocke:

Erlaubt dies auch die Verwendung von ? in const fn ?

Die Verwendung von ? bedeutet die Verwendung der Eigenschaft Try . Die Verwendung von Traits in const fn ist instabil, siehe https://github.com/rust-lang/rust/issues/67794.

@TimDiekmann vorerst müssen Sie loop und for , zumindest bis zu einem gewissen Limit (primitiver Rekursionsstil), aber const eval hat solche Limits trotzdem. Diese Funktion ist so großartig, dass sie viele Dinge ermöglicht, die vorher nicht möglich waren. Sie können sogar einen winzigen wasm vm in const fn erstellen, wenn Sie möchten.

Die letzte Kommentierungsfrist mit der Möglichkeit zur Fusion gemäß der obigen Überprüfung ist nun abgeschlossen .

Als automatisierter Vertreter des Governance-Prozesses möchte ich dem Autor für seine Arbeit und allen anderen, die dazu beigetragen haben, danken.

Der RFC wird in Kürze zusammengeführt.

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen