Rust: Problème de suivi pour la RFC 2342, "Autoriser 'if' et 'match' dans les constantes"

Créé le 18 mars 2018  ·  83Commentaires  ·  Source: rust-lang/rust

Il s'agit d'un problème de suivi pour la RFC "Autoriser if et match dans les constantes" (rust-lang/rfcs#2342).

Veuillez rediriger la constitution de fonctions ou de problèmes spécifiques que vous souhaitez signaler vers de nouveaux problèmes et les étiqueter de manière appropriée avec F-const_if_match afin que ces problèmes ne soient pas inondés de commentaires éphémères obscurcissant des développements importants.

Pas:

  • [x] Mettre en œuvre le RFC
  • [ ] Ajuster la documentation ( voir les instructions sur la forge )
  • [x] Stabilisation PR ( voir instructions sur forge )
  • [x] liaisons let dans les constantes qui utilisent les opérations || court-circuit && et || . Ceux-ci sont traités comme & et | intérieur des éléments const et static ce moment.

Questions non résolues :

Rien

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

Commentaire le plus utile

Maintenant que #64470 et #63812 ont été fusionnés, tous les outils nécessaires pour cela existent dans le compilateur. Je dois encore apporter quelques modifications au système de requête autour de la qualification const pour m'assurer qu'il n'est pas inutilement inefficace avec cette fonctionnalité activée. Nous faisons des progrès ici, et je pense qu'une implémentation expérimentale de ceci sera disponible tous les soirs dans des semaines, pas des mois (derniers mots célèbres :sourire:).

Tous les 83 commentaires

  1. ajoutez une porte de fonctionnalité pour cela
  2. switch terminaisons switchInt dans https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 doivent avoir un code personnalisé dans cas où la fonctionnalité de porte est active
  3. au lieu d'avoir un seul bloc de base actuel (https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328), il doit s'agir d'un conteneur contenant une liste de blocs de base qu'il doit encore traiter.

@oli-obk C'est un peu plus délicat car le flux de contrôle complexe signifie que l'analyse du flux de données doit être utilisée. Je dois revenir à @alexreg et trouver comment intégrer leurs modifications.

@eddyb Un bon point de départ serait probablement de prendre ma branche const-qualif (moins le premier commit), de la rebaser sur master (cela ne sera pas amusant), puis d'ajouter des éléments d'annotation de données, non ?

des nouvelles à ce sujet?

@mark-im Hélas non. Je pense que @eddyb a été très occupé en effet, car je n'ai même pas pu le cingler sur IRC ces dernières semaines hah. Malheureusement, ma branche const-qualif ne compile même pas depuis la dernière fois que je l'ai rebasée sur master. (Je ne crois pas avoir encore poussé.)

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

D'accord, assez curieusement, je me suis rebasé à nouveau juste aujourd'hui et tout semble bien se construire maintenant ! On dirait qu'il y a eu une régression, et ça vient d'être corrigé. C'est à @eddyb maintenant.

@alexreg Désolé, je suis passé à un horaire de sommeil local et je vois que vous m'avez contacté lorsque je me réveille, mais vous êtes alors hors ligne toute la journée lorsque je suis réveillé (fuseaux horaires).
Dois-je simplement faire un PR de votre succursale ? J'ai oublié ce qu'on était censé en faire ?

@eddyb C'est bon heh. Vous devez vous coucher tôt, car je suis généralement à partir de 20h00 GMT, mais tout va bien ! :-)

Je suis vraiment désolé, il m'a fallu un certain temps pour réaliser que la série de correctifs en question nécessite de supprimer Qualif::STATIC{,_REF} , c'est-à-dire les erreurs d'accès à la statique au moment de la compilation. OTOH, c'est déjà cassé en termes de const fn s et d'accès à static s :

#![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);
}

Ceci n'est pas détecté statiquement, au lieu miri cela, static s au lieu de "pointeur suspendu").

Je pense donc que la lecture de static au moment de la compilation devrait convenir, mais certaines personnes veulent que const fn soit "pur" (c'est-à-dire "référentiellement transparent" ou à peu près) au moment de l'exécution, ce qui signifierait que une lecture de const fn derrière une référence qu'elle a obtenue comme argument est bien, mais un const fn ne devrait obtenir une référence à un static partir de rien (y compris à partir de const s).

Je pense alors que nous pouvons continuer à nier statiquement la mention de static s (même si ce n'est que pour prendre leur référence) dans const s, const fn , et d'autres contextes constants (y compris les promus).
Mais nous devons toujours supprimer le hack STATIC_REF qui permet à static s de prendre la référence d'autres static s mais (mal essaye et échoue) nier la lecture derrière ces références .

Avons-nous besoin d'un RFC pour cela?

Cela semble juste par rapport à la lecture de la statique. Je doute qu'il ait besoin d'un RFC, peut-être juste d'une course de cratère, mais je ne suis probablement pas le meilleur pour le dire.

Notez que nous ne restreindrions rien, nous relâcherions une restriction qui est déjà brisée.

Oh, j'ai mal lu. Donc, l'évaluation const serait toujours solide, mais pas référentiellement transparente ?

Le dernier paragraphe décrit une approche référentiellement transparente (mais nous perdons cette propriété si nous commençons à autoriser la mention de static s dans const s et const fn s). Je ne pense pas que la solidité était vraiment en discussion.

Eh bien, "pointeur suspendu" ressemble bien à un problème de solidité, mais je vais vous faire confiance à ce sujet !

"dangling pointer" est un mauvais message d'erreur, c'est juste miri interdisant la lecture de static s. Les seuls contextes constants qui peuvent même faire référence à static s sont d'autres static s, donc nous pourrions "juste" autoriser ces lectures, puisque tout ce code s'exécute toujours une fois, au moment de la compilation.

(de IRC) Pour résumer, const fn référentiellement transparent ne peut atteindre que des allocations gelées, sans passer par des arguments, ce qui signifie que const besoin de la même restriction, et les allocations non gelées ne peuvent provenir que de static s.

J'aime préserver la transparence référentielle donc l'idée de @eddyb semble fantastique !

Ouais, je suis aussi un pro qui fait de la const fns pure.

Attention, certains plans en apparence anodins peuvent ruiner la transparence référentielle, par exemple :

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
}

Cela échouerait avec une erreur miri au moment de la compilation, mais serait non déterministe au moment de l'exécution (car nous ne pouvons pas marquer cette adresse mémoire comme "abstraite" comme miri le peut).

EDIT : @Centril a eu l'idée de faire certaines opérations de pointeur brutes (telles que des comparaisons et des conversions en entiers) unsafe dans const fn (ce que nous pouvons faire jusqu'à ce que nous stabilisions const fn ) et déclarent qu'ils ne peuvent être utilisés que d'une manière que miri autoriserait au moment de la compilation.
Par exemple, soustraire deux pointeurs dans le même local devrait être correct (vous obtenez une distance relative qui ne dépend que de la disposition du type, des indices de tableau, etc.), mais formater l'adresse d'une référence (via {:p} ) est une utilisation incorrecte et donc fmt::Pointer::fmt ne peut pas être marqué const fn .
De plus, aucun des traits impls Ord / Eq pour les pointeurs bruts ne peut être marqué comme const (chaque fois que nous avons la possibilité de les annoter comme tels), car ils sont sûrs mais l'opération est unsafe dans const fn .

Cela dépend de ce que vous entendez par "inoffensif"... Je vois certainement pourquoi nous voudrions interdire un tel comportement non déterministe.

Ce serait fantastique si le travail se poursuivait là-dessus.

@lachlansneff Ça avance... pas aussi vite qu'on le souhaiterait, mais le travail est en cours. Pour le moment, nous attendons https://github.com/rust-lang/rust/pull/51110 en tant que bloqueur.

@alexreg Ah, merci. Il serait très utile de pouvoir marquer une correspondance ou si comme const même lorsqu'il n'est pas dans une fn const.

des mises à jour de statut maintenant que #51110 est fusionné ?

@programmerjake J'attends des commentaires de @eddyb sur https://github.com/rust-lang/rust/pull/52518 avant de pouvoir fusionner (espérons-le très bientôt). Il a été très occupé ces derniers temps (toujours très demandé), mais il est revenu aux critiques et ainsi de suite ces derniers jours, alors j'ai bon espoir. Après cela, il aura besoin d'un peu de travail de sa part, je suppose, car l'ajout d'une analyse de flux de données appropriée est une affaire compliquée. On verra quand même.

Quelque part dans les listes TODO dans le(s) premier(s) message(s), il devrait être ajouté pour supprimer l'horrible hack actuel qui traduit && et || en & et | intérieur des constantes.

@RalfJung N'était-ce pas une partie de l'ancien const eval, qui a complètement disparu maintenant que MIRI CTFE est en place?

AFAIK, nous faisons cette traduction quelque part dans l'abaissement HIR, car nous avons du code dans const_qualify qui rejette les terminaisons SwitchInt qui seraient autrement générées par || / && .

Aussi, un autre point: @oli-obk a dit quelque part (mais je ne trouve pas où) que les conditionnels sont en quelque sorte plus compliqués qu'on ne le penserait naïvement... était-ce "juste" à propos de l'analyse de la chute/mutabilité intérieure?

était-ce « juste » à propos de l'analyse de la mutabilité goutte/intérieur ?

J'essaye actuellement d'éclaircir ça. Je reviendrai vers vous quand j'aurai toutes les informations

Quel est le statut de ceci? Cela a-t-il besoin de main-d'œuvre ou est-il bloqué pour résoudre un problème ?

@mark-im Il est bloqué sur la mise en œuvre d'une analyse de flux de données appropriée pour la qualification const. @eddyb est le plus compétent dans ce domaine, et il avait déjà travaillé sur ce sujet. (Moi aussi, mais ce genre de stagnation ...) Si @eddyb n'a toujours pas le temps, peut-être que @oli-obk ou @RalfJung pourraient s'attaquer à cela bientôt. :-)

58403 est un petit pas vers la qualification basée sur les flux de données.

@eddyb, vous avez mentionné la préservation de la transparence référentielle dans const fn , ce qui, à mon avis, est une bonne idée. Et si vous empêchiez d'utiliser des pointeurs dans const fn ? Ainsi, votre exemple de code précédent ne compilerait plus :

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
}

Les références seraient toujours autorisées mais vous ne seriez pas autorisé à les introspecter :

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

Faites-moi savoir si je suis complètement hors de propos, j'ai juste pensé que ce serait un bon moyen de rendre ce déterministe.

@jyn514 qui est déjà couvert en rendant instables les casts d' usize (https://github.com/rust-lang/rust/issues/51910), mais les utilisateurs peuvent également comparer des pointeurs bruts (https://github.com/rust- lang/rust/issues/53020) qui est tout aussi mauvais et donc également instable. Nous pouvons les gérer indépendamment du flux de contrôle.

Du nouveau à ce sujet ?

Il y a une discussion sur https://rust-lang.zulipchat.com/#narrow/stream/146212 -t-compiler.2Fconst-eval/topic/dataflow-based.20const.20qualification.20MVP

@oli-obk votre lien ne fonctionne pas. Ça dit quoi?

Cela fonctionne pour moi... vous devez cependant vous connecter à Zulip.

@alexreg hmm ouais, je suppose qu'il s'agissait du travail de qualification const basé sur le flux de données. @alexreg savez-vous pourquoi c'est nécessaire pour if et match dans les constantes ?

si nous n'avons pas de version basée sur le flux de données, nous autorisons accidentellement &Cell<T> intérieur des constantes ou interdisons accidentellement None::<&Cell<T>> (qui fonctionne sur stable. Il est essentiellement impossible d'implémenter correctement sans flux de données (ou toute implémentation sera être une mauvaise version ad-hoc cassée du flux de données)

@ est31 Eh bien, @oli-obk comprend cela beaucoup mieux que moi, mais à partir d'un niveau élevé, tout ce qui implique la création de branches va prédire l'analyse du flux de données, à moins que vous ne vouliez un tas de cas extrêmes. Quoi qu'il en soit, il semble que cette personne sur Zulip essaie de travailler dessus, et sinon, je sais qu'oli-obk et eddyb ont l'intention de le faire, peut-être ce mois-ci ou le prochain (à partir de la dernière fois où je leur en ai parlé), bien que je puisse 't/ne fera pas de promesses en leur nom.

@alexreg @mark-im @est31 @oli-obk Je devrais pouvoir publier mon implémentation WIP de la qualification const basée sur les flux de données cette semaine. Il y a beaucoup de risques de compatibilité ici, donc la fusion peut prendre un certain temps.

Super; hâte d'y.

(copie de #57563 par demande)

Serait-il possible de casser bool && bool , bool || bool , etc. ? Ils peuvent actuellement être exécutés dans un const fn , mais cela nécessite des opérateurs au niveau du bit, ce qui est parfois indésirable.

Ils sont déjà spéciaux dans les éléments const et static -- en les traduisant en opérations au niveau du bit. Mais ce boîtier spécial est un énorme hack et il est très difficile de s'assurer qu'il est réellement correct. Comme vous l'avez dit, c'est aussi parfois indésirable. Nous préférons donc ne pas le faire plus souvent.

Bien faire les choses prendra un peu de temps, mais cela arrivera. Si nous empilons trop de hacks entre-temps, nous pourrions nous mettre dans un coin dont nous ne pouvons pas sortir (si certains de ces hacks finissent par interagir de manière incorrecte, stabilisant ainsi accidentellement un comportement que nous ne voulons pas).

Maintenant que #64470 et #63812 ont été fusionnés, tous les outils nécessaires pour cela existent dans le compilateur. Je dois encore apporter quelques modifications au système de requête autour de la qualification const pour m'assurer qu'il n'est pas inutilement inefficace avec cette fonctionnalité activée. Nous faisons des progrès ici, et je pense qu'une implémentation expérimentale de ceci sera disponible tous les soirs dans des semaines, pas des mois (derniers mots célèbres :sourire:).

@ecstatic-morse Super à entendre ! Merci pour vos efforts concertés pour y parvenir ; Personnellement, je suis passionné par cette fonctionnalité depuis un certain temps maintenant.

J'adorerais voir la prise en charge de l'allocation de tas pour CTFE une fois cela fait. Je ne sais pas si vous ou quelqu'un d'autre êtes intéressé à travailler là-dessus, mais si ce n'est pas le cas, je pourrais peut-être vous aider.

@alexreg Merci !

La discussion sur l'allocation de tas au moment de la compilation est terminée sur rust-rfcs/const-eval#20. AFAIK, les développements les plus récents étaient autour d'un paradigme ConstSafe / ConstRefSafe pour déterminer ce qui peut être observé directement/derrière une référence dans la valeur finale d'un const . Je pense qu'il y a plus de travail de conception nécessaire cependant.

Pour ceux qui suivent, #65949 (qui dépend lui-même de quelques PR plus petits) est le prochain bloqueur pour cela. Bien que cela puisse sembler n'être lié que de manière tangentielle, le fait que la vérification/qualification de la const était si étroitement associée à la promotion était en partie la raison pour laquelle cette fonctionnalité a été bloquée pendant si longtemps. Je prévois d'ouvrir un PR ultérieur qui supprimera complètement l'ancien vérificateur const (actuellement, nous exécutons les deux vérificateurs en parallèle). Cela évitera les inefficacités que j'ai mentionnées plus tôt.

Une fois les deux PR susmentionnés fusionnés, if et match dans les constantes bénéficieront de quelques améliorations de diagnostic et d'un indicateur de fonctionnalité ! Oh, et aussi des tests, tellement de tests...

Si vous avez besoin de tests, je ne sais pas par où commencer mais je suis plus que disposé à contribuer ! Dites-moi simplement où les tests devraient aller/à quoi ils devraient ressembler/sur quelle branche je devrais baser le code :)

Le prochain PR à surveiller est le #66385. Cela supprime complètement l'ancienne logique de qualification const (qui ne pouvait pas gérer le branchement) au profit de la nouvelle version basée sur le flux de données.

@jyn514 Ce serait génial ! Je vous enverrai un ping lorsque je commencerai à rédiger la mise en œuvre. Il serait également très utile pour les gens d'essayer de violer la sécurité const (en particulier la partie HasMutInterior ) une fois que if et match sont disponibles tous les soirs.

66507 contient une implémentation initiale de la RFC 2342.

Je pense qu'il faudra un certain temps pour supprimer les aspérités, en particulier en ce qui concerne les diagnostics, et la couverture des tests est assez clairsemée ( @jyn514 nous devrions nous coordonner sur ce problème). Néanmoins, j'espère que nous pourrons publier cela derrière un indicateur de fonctionnalité dans les prochaines semaines.

Cela a été implémenté dans #66507 et peut maintenant être utilisé dans le dernier nightly . Il existe également un article de blog Inside Rust qui détaille les opérations nouvellement disponibles ainsi que certains problèmes que vous pourriez rencontrer avec l'implémentation existante autour des types avec mutabilité intérieure ou un Drop impl personnalisé.

Allez-y et constifiez !

Il semble que l'égalité ne soit pas const ? Ou est-ce que je me trompe :

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 Cela devrait en effet fonctionner . Peut-être un problème d'amorçage ? Parlons de Zulip.

Je ne sais pas si c'est intentionnel, mais essayer de faire correspondre une énumération donne l'erreur

const fn avec un code inaccessible n'est pas stable

malgré le fait que l'énumération soit exhaustive et définie dans la même caisse.

@jhpratt peux-tu poster le code ? Je peux faire correspondre des énumérations simples sans problème : https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=585e9c2823afcb49c6682f69569c97ea

@jhpratt peux-tu poster le code ? Je peux faire correspondre sur des énumérations simples sans problème :

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

Battez-moi de quelques secondes. C'est un exemple minimal démontrant mon cas exact.

@jhpratt Certainement pas intentionnel. Pourriez-vous ouvrir un problème ?

Veuillez rediriger la constitution de fonctions ou de problèmes spécifiques que vous souhaitez signaler vers de nouveaux problèmes et les étiqueter de manière appropriée avec F-const_if_match afin que ces problèmes ne soient pas inondés de commentaires éphémères obscurcissant des développements importants.

@Centril Ce

Mise à jour du statut:

Ceci est prêt pour la stabilisation du point de vue de la mise en œuvre, mais la question se pose de savoir si nous voulons conserver le flux de données basé sur la valeur que nous avons actuellement au lieu d'un flux basé sur le type (mais moins puissant). Le flux de données basé sur la valeur est un peu plus cher (plus de détails plus bas), et nous en avons besoin pour des fonctions telles que

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

qu'une analyse basée sur les types rejetterait, car un Option<T> peut avoir des destructeurs qui essaieraient maintenant de s'exécuter et pourraient donc exécuter du code non const.

On peut se rabattre sur une analyse de type dès qu'il y a des branches, mais cela voudrait dire qu'on rejetterait

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

ce qui serait probablement très surprenant pour les utilisateurs.

@ecstatic-morse a exécuté l'analyse sur toutes les fonctions, pas seulement sur const fn et a enregistré des ralentissements allant jusqu'à 5% (https://perf.rust-lang.org/compare.html?start=93dc97a85381cc52eb872d27e50e4d518926a27c&end=51cf313c7946365d5bea381139509703c2) Notez qu'il s'agit d'une version pessimiste, car cela signifie qu'elle est également exécutée sur des fonctions qui ne deviendront pas et ne pourront souvent jamais devenir const fn .

Cela signifie que si nous créons de nombreuses fonctions const fn, nous pouvons constater des ralentissements de la compilation en raison de cette analyse basée sur les valeurs.

Un juste milieu pourrait consister à n'exécuter l'analyse basée sur la valeur que si l'analyse basée sur le type échoue. Cela signifie que s'il n'y a pas de destructeurs, nous n'avons pas besoin d'exécuter l'analyse basée sur les valeurs pour déterminer si les destructeurs inexistants ne seront pas exécutés (oui, je sais, beaucoup de négations ici). Pour le formuler différemment : nous n'exécutons l'analyse basée sur les valeurs que s'il y a des destructeurs présents.

Je propose ceci pour la discussion @rust-lang/lang afin que nous puissions déterminer si nous voulons aller avec

  • l'option basée sur le type en présence de boucles ou de branches (donnant un comportement étrange aux utilisateurs)
  • analyse basée sur la valeur complète (plus chère, mais pleine expressivité pour les utilisateurs)
  • schéma mixte, toujours pleine expressivité pour les utilisateurs, une complexité d'implémentation supplémentaire, mais devrait réduire les problèmes de compilation aux cas qui en ont besoin.

@oli-obk

l'option basée sur le type en présence de boucles ou de branches (donnant un comportement étrange aux utilisateurs)

Juste pour vérifier ceci : n'est-il pas possible d'avoir une analyse basée sur les types, même dans le code en ligne droite ? J'imagine que c'est un peu rétrocompatible, étant donné que nous acceptons déjà ce qui suit ( aire de jeux ):

struct Foo { }

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

const T: Option<Foo> = None;

fn main() { }

Personnellement, j'ai tendance à penser que nous devrions faire pression pour une expérience plus cohérente et meilleure pour les utilisateurs. Il semble que nous puissions optimiser au besoin, et en tout cas le coût n'est pas trop mal. Mais j'aimerais comprendre un peu mieux ce qui se passe exactement dans cette analyse plus coûteuse : est-ce que l'idée que nous faisons fondamentalement une "propagation constante", de sorte que chaque fois que quelque chose est abandonné, nous analysons la valeur exacte supprimée pour déterminer si il peut contenir une valeur qui aurait besoin d'exécuter un destructeur ? (c'est-à-dire, si c'est None , pour utiliser l'exemple courant de Option<T> )

Juste pour vérifier ceci : n'est-il pas possible d'avoir une analyse basée sur les types, même dans le code en ligne droite ? J'imagine que c'est un peu rétrocompatible, étant donné que nous acceptons déjà ce qui suit (terrain de jeu) :

Oui, c'est la raison pour laquelle nous ne pouvons pas simplement passer entièrement à l'analyse basée sur les types.

est-ce que l'idée que nous faisons essentiellement une "propagation constante", de telle sorte que chaque fois que quelque chose est supprimé, nous analysons la valeur exacte supprimée pour déterminer si elle peut contenir une valeur qui aurait besoin d'exécuter un destructeur ? (c'est-à-dire, si c'est Aucun, pour utiliser l'exemple courant d'Option)

Nous ne faisons que propager une liste de drapeaux ( Drop et Freeze , j'ai juste montré Drop ici parce que c'est plus facile à expliquer). Lorsque nous atteignons un terminateur Drop sans avoir défini le drapeau Drop , nous ignorons le terminateur Drop . Cela autorise un code comme celui-ci :

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

Cela ne se produit pas au moment de l'évaluation, donc ce qui suit n'est pas correct :

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

Nous vérifions que tous les chemins d'exécution possibles ne provoquent pas un Drop .

La propagation constante est une bonne analogie cependant. C'est un autre problème de flux de données dont la fonction de transfert ne peut pas être exprimée avec des ensembles gen/kill, qui ne gèrent pas la copie d'état entre les variables. Cependant, la propagation constante doit stocker la valeur réelle de chaque variable, mais la vérification constante n'a besoin de stocker qu'un seul bit indiquant si cette variable a un Drop impl personnalisé ou n'est pas Freeze rendant un peu moins cher que la propagation constante serait.

Pour être clair, le premier exemple de @oli-obk compile sur stable aujourd'hui, et depuis 1.38.0 , qui n'incluait pas #64470.

De plus, const X: Option<Foo> = None; compile depuis la 1.0, tout le reste n'est qu'une extension naturelle de cela avec les nouvelles fonctionnalités que const eval a acquises.

OK, je pense qu'il est alors logique d'adopter l'option purement basée sur la valeur.

Je suppose que nous pouvons le couvrir lors de la réunion et faire rapport =)

Résumé

Je propose que l'on stabilise #![feature(const_if_match)] avec la sémantique actuelle.

Plus précisément, les expressions if et match ainsi que les opérateurs logiques de court-circuit && et || deviendront légaux dans tous les contextes const . Un contexte const est l'un des suivants :

  • L'initialiseur d'un const , static , static mut ou discriminant enum.
  • Le corps d'un const fn .
  • La valeur d'un générique const (tous les soirs uniquement).
  • La longueur d'un type de tableau ( [u8; 3] ) ou d'une expression de répétition de tableau ( [0u8; 3] ).

De plus, les opérateurs logiques de court-circuit ne seront plus abaissés à leurs équivalents au niveau du bit ( respectivement & et | ) dans les initialiseurs const et static (voir #57175). En conséquence, les liaisons let peuvent être utilisées avec la logique de court-circuit dans ces initialiseurs.

Problème de suivi : #49146
Version cible : 1.45 (2020-06-16)

Historique de mise en œuvre

64470 a mis en œuvre une analyse statique basée sur la valeur qui prenait en charge le flux de contrôle conditionnel et était basée sur le flux de données. Ceci, ainsi que #63812, nous a permis de remplacer l'ancien code de vérification de const par un autre qui fonctionnait sur des graphiques de flux de contrôle complexes. L'ancien contrôleur const a été exécuté en parallèle avec celui basé sur le flux de données pendant un certain temps pour s'assurer qu'ils étaient d'accord sur les programmes avec un flux de contrôle simple. #66385 a supprimé l'ancien vérificateur const en faveur de celui basé sur le flux de données.

66507 a implémenté la porte de fonctionnalité #![feature(const_if_match)] avec la sémantique qui est maintenant proposée pour la stabilisation.

Qualification de la constance

Fond

[Miri] alimente l'évaluation des fonctions au moment de la compilation (CTFE) dans rustc depuis plusieurs années maintenant, et est capable d'évaluer les instructions conditionnelles depuis au moins aussi longtemps. Pendant CTFE, nous devons éviter certaines opérations, comme appeler des Drop impls personnalisés ou prendre une référence à une valeur avec mutabilité intérieure . Collectivement, ces propriétés disqualifiantes sont connues sous le nom de « qualifications », et le processus consistant à déterminer si une valeur a une qualification à un moment spécifique du programme est connu sous le nom de « qualification const ».

Miri est parfaitement capable d'émettre une erreur lorsqu'il rencontre une opération illégale sur une valeur qualifiée, et il peut le faire sans faux positifs. Cependant, CTFE se produit après la monomorphisation, ce qui signifie qu'il ne peut pas savoir si les constantes définies dans un contexte générique sont valides jusqu'à ce qu'elles soient instanciées, ce qui pourrait arriver dans une autre caisse. Pour obtenir des erreurs de pré-monomorphisation, nous devons implémenter une analyse statique qui fait const qualification. Dans le cas général, la qualification const est indécidable (voir le théorème de Rice ), donc toute analyse statique ne peut qu'approximer les vérifications que Miri effectue pendant CTFE.

Notre analyse statique doit interdire à une référence à un type avec mutabilité intérieure (par exemple &Cell<i32> ) d'apparaître dans la valeur finale d'un const . Si cela était autorisé, un const pourrait être modifié au moment de l'exécution.

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

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

Cependant, nous permettons à l'utilisateur de définir un const dont le type a une mutabilité intérieure ( !Freeze ) tant que nous pouvons prouver que la valeur finale de ce const n'en a pas. Par exemple, ce qui suit a été compilé depuis la première édition de la rouille stable :

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

Cette approche de l'analyse statique, que j'appellerai basée sur la valeur par opposition à basée sur le type, est également utilisée pour vérifier le code qui peut entraîner l'appel d'une impl Drop . L'appel de Drop impls est problématique car ils ne sont pas contrôlés par const et peuvent donc contenir du code qui ne serait pas autorisé dans un contexte const. Le raisonnement basé sur la valeur a été étendu pour prendre en charge les instructions let , ce qui signifie que ce qui suit se compile sur 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
};

Sémantique nocturne actuelle

Le comportement actuel de #![feature(const_if_match)] étend la sémantique basée sur les valeurs pour travailler sur des graphiques de flux de contrôle complexes en utilisant le flux de données. En d'autres termes, nous essayons de prouver qu'une variable n'a pas la qualification en question le long de tous les chemins possibles à travers le programme.

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

Tous les chemins possibles à travers le programme incluent ceux qui peuvent ne jamais être atteints dans la pratique. Un exemple, en utilisant la même énumération Int que ci-dessus :

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

Cette analyse traite les appels de fonction comme opaques, en supposant que leur valeur de retour peut contenir n'importe quelle valeur de son type. On retombe également sur une analyse de type pour une variable dès qu'une référence mutable à celle-ci est créée. Notez que la création d'une référence mutable dans un contexte const est actuellement interdite sur la rouille stable.

#![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)
}

Vous pouvez voir plus d'exemples de la façon dont une analyse basée sur la valeur est conservatrice autour de la mutabilité intérieure et des impls de suppression personnalisés , ainsi que certains cas où une analyse conservatrice est capable de prouver que rien d'illégal ne peut se produire dans la suite de tests.

Alternatives

J'ai eu du mal à trouver des alternatives pratiques et rétrocompatibles à l'approche existante. Nous pourrions revenir à l'analyse basée sur les types pour toutes les variables dès que les conditions sont utilisées dans un contexte const. Cependant, cela serait également difficile à expliquer aux utilisateurs, car des ajouts apparemment sans rapport empêcheraient la compilation du code, comme le assert dans l'exemple suivant de @oli-obk.

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

L'expressivité accrue de l'analyse basée sur la valeur n'est pas gratuite. Une exécution de perf qui a fait une qualification constante sur tous les corps d'élément, pas seulement ceux de const , a montré jusqu'à une régression de 5% sur les builds de contrôle . Il s'agit du pire des cas, car il suppose que tous les objets seront fabriqués en const à un moment donné dans le futur. Les optimisations possibles, telles que celle de #71330, ont été discutées plus tôt dans le fil.

Travail futur

Pour le moment, le contrôle const est exécuté avant l'élaboration de la goutte, ce qui signifie que certains terminateurs de goutte restent dans le MIR et sont inaccessibles dans la pratique. Cela empêche Option::unwrap de devenir const fn (voir #66753). Ce n'est pas trop difficile à résoudre, mais cela nécessitera de diviser la passe de contrôle de const en deux phases (élaboration avant et après la chute).

Une fois que #![feature(const_if_match)] est stabilisé, de nombreuses fonctions de la bibliothèque peuvent être créées const fn . Cela inclut de nombreuses méthodes sur les types entiers primitifs, qui ont été énumérées dans #53718.

Les boucles dans un contexte const sont bloquées sur la même question de qualification const que les conditions. L'approche actuelle basée sur le flux de données fonctionne également pour les CFG cycliques sans aucune modification, donc si #![feature(const_if_match)] est stabilisé, le bloqueur principal pour #52000 aura disparu.

Remerciements

Des remerciements particuliers sont dus à @oli-obk et @eddyb , qui ont été les principaux réviseurs pour la plupart des travaux de mise en œuvre, ainsi qu'au reste de @rust-lang/wg-const-eval pour m'avoir aidé à comprendre les problèmes pertinents autour de const qualification. Rien de tout cela ne serait possible sans Miri, qui a été créé par @solson et maintenant maintenu par @RalfJung et @oli-obk.

Il s'agit du rapport de stabilisation précédant le FCP. Je ne peux pas ouvrir FCP, cependant.

@ecstatic-morse Merci beaucoup pour tout votre travail acharné sur ce problème !

Super reportage !

Une chose que je pense que j'aimerais voir, @ecstatic-morse, c'est

  • des liens vers des tests représentatifs dans le référentiel, afin que nous puissions observer le comportement
  • s'il y a des implications autour de semver ou de quoi que ce soit d'autre - je pense que la réponse est en grande partie non ,

En d'autres termes, nous décidons de l'analyse utilisée pour déterminer si le corps d'un const fn est légal, mais étant donné un const fn, nos choix ici ne déterminent pas des choses comme "ce que l'appelant du const fn peut faire avec le résultat", n'est-ce pas ? J'essaie de comprendre quel pourrait être un exemple de ce dont je parle - je suppose que ce serait que l'appelant ne sache pas précisément quelles variantes d'un enum ont été utilisées, seulement cela - quelle que soit la valeur a été renvoyé - il n'avait pas de mutabilité intérieure (sur laquelle ils ne peuvent probablement pas non plus compter lors de la correspondance, depuis).

Oui, le corps d'un const fn est opaque. Cela contraste avec l'expression d'initialisation d' const élément

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

peut être utilisé pour créer un &'static Option<Cell<i32>>

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

tandis qu'un const fn avec le même corps ne peut pas :

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

démonstration de terrain de jeu

Lorsque nous introduisons le flux de contrôle aux constantes, cela signifie que

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

fonctionnera également, indépendamment de la valeur de MEH et

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

ne fonctionnera pas, encore une fois, indépendamment de la valeur de MEH .

Le flux de contrôle ne change rien aux sites d'appel de const fn , à peu près quel code est autorisé à l'intérieur de ce const fn.

des liens vers des tests représentatifs dans le référentiel, afin que nous puissions observer le comportement.

J'ai ajouté un paragraphe à la fin de la section "Current Nightly Semantics" qui renvoie à des cas de test intéressants. J'ai l'impression que nous avons besoin de plus de tests (une déclaration qui est vraie quelles que soient les circonstances) avant que cela ne soit stabilisé, mais cela peut être résolu une fois que nous décidons si la sémantique actuelle est souhaitable.

s'il y a des implications autour de semver ou quoi que ce soit d'autre.

En plus de ce que @oli-obk a dit ci-dessus, je tiens à souligner que changer la valeur finale d'un const est déjà techniquement un changement révolutionnaire :

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

Cependant, comme nous ne pouvons pas effectuer la qualification const avec une précision parfaite, changer une constante pour utiliser if ou match pourrait casser le code en aval, même si la valeur finale ne change pas. Par example:

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

Cela ne s'applique pas aux modifications à l'intérieur du corps d'un const fn , car nous utilisons toujours une qualification basée sur le type pour la valeur de retour, même au sein de la même caisse.

À mon avis, le "péché originel" ici n'était pas de revenir à la qualification basée sur le type pour les const et les static définis dans des caisses externes. Cependant, je crois que c'est le cas depuis la 1.0, et je soupçonne que beaucoup de code en dépend. Dès que l'on autorise les initialiseurs const pour lesquels l'analyse statique ne peut pas être parfaitement précise, il devient possible de modifier ces initialiseurs de telle sorte qu'ils produiront la même valeur sans que l'analyse statique puisse le prouver.

Éditer:

Il n'y a rien d'unique à propos de if et match à cet égard. Par exemple, c'est actuellement un changement radical de refactoriser un initialiseur const en un const fn si la caisse en aval reposait sur une qualification basée sur la valeur.

// 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 Merci d'avoir rédigé le rapport de stabilisation ! Évaluons le consensus de manière asynchrone :

@rfcbot fusionner

Si quelqu'un souhaite en discuter de manière synchrone lors d'une réunion, veuillez re-nominer.

Le membre de l'équipe @joshtriplett a proposé de fusionner cela. L'étape suivante est l'examen par le reste des membres de l'équipe tagués :

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

Aucune préoccupation actuellement répertoriée.

Une fois qu'une majorité d'examinateurs approuve (et au plus 2 approbations sont en attente), cela entrera dans sa dernière période de commentaires. Si vous repérez un problème majeur qui n'a été soulevé à aucun moment de ce processus, veuillez en parler !

Consultez ce document pour plus d'informations sur les commandes que les membres de l'équipe tagués peuvent me donner.

:bell: Ceci entre maintenant dans sa dernière période de commentaires , selon l' examen ci-dessus . :cloche:

Cela permet-il également d'utiliser ? dans const fn ?

Utiliser ? signifie utiliser le trait Try . L'utilisation de traits dans const fn est instable, voir https://github.com/rust-lang/rust/issues/67794.

@TimDiekmann pour le moment, vous devrez écrire des macros proc qui abaissent le ? manuellement. Il en va de même pour loop et for , au moins jusqu'à une certaine limite (style de récursivité primitif) mais const eval a de telles limites de toute façon. Cette fonctionnalité est tellement géniale qu'elle permet BEAUCOUP de choses qui n'étaient pas possibles auparavant. Vous pouvez même créer un petit wasm vm dans const fn si vous le souhaitez.

La période de commentaires finale, avec une disposition à fusionner , conformément à l' examen ci - terminée .

En tant que représentant automatisé du processus de gouvernance, je tiens à remercier l'auteur pour son travail et tous ceux qui ont contribué.

Le RFC sera bientôt fusionné.

Cette page vous a été utile?
0 / 5 - 0 notes