Rust: Problema de seguimiento para RFC 2342, "Permitir` if` y `match` en constantes"

Creado en 18 mar. 2018  ·  83Comentarios  ·  Fuente: rust-lang/rust

Este es un problema de seguimiento para el RFC "Permitir if y match en constantes" (rust-lang / rfcs # 2342).

Redirija la constificación de funciones o problemas específicos que desea informar a problemas nuevos y etiquételos adecuadamente con F-const_if_match para que estos problemas no se inunden con comentarios efímeros que oculten desarrollos importantes.

Pasos:

Preguntas sin resolver:

Ninguna

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

Comentario más útil

Ahora que se han fusionado # 64470 y # 63812, todas las herramientas necesarias para esto existen en el compilador. Todavía necesito hacer algunos cambios en el sistema de consulta sobre la calificación const para asegurarme de que no sea innecesariamente ineficiente con esta función habilitada. Estamos progresando aquí, y creo que una implementación experimental de esto estará disponible todas las noches en semanas, no en meses (las famosas últimas palabras: sonrisa :).

Todos 83 comentarios

  1. agregue una puerta de función para ello
  2. switch terminadores switchInt en https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 deben tener un código personalizado en caso de que la puerta de función esté activa
  3. en lugar de tener un solo bloque básico actual (https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328), debe ser un contenedor que tenga una lista de bloques básicos que todavía tiene que procesar.

@ oli-obk Es un poco más complicado porque el flujo de control complejo significa que se debe emplear el análisis del flujo de datos. Necesito volver a @alexreg y averiguar cómo integrar sus cambios.

@eddyb Un buen punto de partida probablemente sería tomar mi rama const-qualif (menos la confirmación superior), rebasarla sobre la maestra (no va a ser divertido) y luego agregar cosas de anotación de datos, ¿verdad?

¿Alguna noticia sobre esto?

@ mark-im Alas no. Creo que @eddyb ha estado muy ocupado, porque ni siquiera he podido hacer ping en el IRC durante las últimas semanas, ja. Lamentablemente, mi rama const-qualif ni siquiera se compila desde la última vez que la modifiqué sobre master. (Aunque no creo que haya empujado todavía).

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

De acuerdo, curiosamente, ¡volví a basarme hoy mismo y parece que todo va bien ahora! Parece que hubo una regresión y se solucionó. Todo a @eddyb ahora.

@alexreg Lo siento,
¿Debería hacer un PR de tu sucursal? Olvidé lo que se suponía que teníamos que hacer con él.

@eddyb Eso está bien, je. Debes irte a dormir temprano, ya que normalmente estoy a partir de las 8:00 p.m. GMT, ¡pero todo está bien! :-)

Lo siento mucho, me tomó un tiempo darme cuenta de que la serie de parches en cuestión requiere eliminar Qualif::STATIC{,_REF} , es decir, los errores sobre el acceso a las estáticas en tiempo de compilación. OTOH, esto ya está roto en términos de const fn sy acceso a 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);
}

Esto no se detecta estáticamente, en su lugar miri queja de que "el puntero colgante fue desreferenciado" (lo que realmente debería decir algo sobre static s en lugar de "puntero colgante").

Así que creo que leer static s en tiempo de compilación debería estar bien, pero algunas personas quieren que const fn sea ​​"puro" (es decir, "referencialmente transparente" o algo parecido) en tiempo de ejecución, lo que significaría que una lectura de const fn detrás de una referencia que obtuvo como argumento está bien, pero un const fn nunca debería poder obtener una referencia a un static de la nada (incluyendo desde const s).

Creo que entonces podemos seguir negando estáticamente la mención de static s (aunque solo sea para tomar su referencia) en const s, const fn y otros contextos constantes (incluidas las promociones).
Pero aún tenemos que eliminar el truco STATIC_REF que permite a static s tomar la referencia de otros static s pero (lo intenta mal y no logra) negar la lectura detrás de esas referencias .

¿Necesitamos un RFC para esto?

Suena justo con una lectura de estática. Dudo que necesite un RFC, tal vez solo un cráter, pero probablemente no soy el mejor para decirlo.

Tenga en cuenta que no restringiríamos nada, relajaríamos una restricción que ya se ha roto.

Oh, leí mal. Entonces, ¿la evaluación constante seguiría siendo sólida, pero no referencialmente transparente?

El último párrafo describe un enfoque referencialmente transparente (pero perdemos esa propiedad si comenzamos a permitir mencionar static s en const sy const fn s). No creo que la solidez estuviera realmente en discusión.

Bueno, "puntero colgante" seguro que suena como un problema de solidez, ¡pero confiaré en ti!

"puntero colgante" es un mensaje de error incorrecto, eso es solo miri prohibiendo la lectura de static s. Los únicos contextos constantes que incluso pueden referirse a static s son otros static s, por lo que podríamos "solo" permitir esas lecturas, ya que todo ese código siempre se ejecuta una vez, en tiempo de compilación.

(de IRC) Para resumir, const fn referencialmente transparente solo podría alcanzar asignaciones congeladas, sin pasar por argumentos, lo que significa que const necesita la misma restricción, y las asignaciones no congeladas solo pueden provenir de static s.

Me gusta preservar la transparencia referencial, ¡así que la idea de @eddyb suena fantástica!

Sí, también soy un profesional haciendo que las constantes sean puras.

Tenga en cuenta que ciertos planes aparentemente inofensivos podrían arruinar la transparencia referencial, por ejemplo:

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
}

Esto fallaría con un error miri en tiempo de compilación, pero no sería determinista en tiempo de ejecución (porque no podemos marcar esa dirección de memoria como "abstracta" como miri puede).

EDITAR : @Centril tuvo la idea de hacer ciertas operaciones de puntero sin procesar (como comparaciones y conversiones a enteros) unsafe dentro de const fn (lo que podemos hacer hasta estabilizar const fn ), y afirman que solo se pueden usar de la manera que miri permitiría en tiempo de compilación.
Por ejemplo, restar dos punteros en el mismo local debería estar bien (obtienes una distancia relativa que solo depende del diseño de tipo, índices de matriz, etc.), pero formateando la dirección de una referencia (a través de {:p} ) es un uso incorrecto y, por lo tanto, fmt::Pointer::fmt no se puede marcar const fn .
Además, ninguna de las implicaciones de rasgo Ord / Eq para punteros sin procesar se puede marcar como const (siempre que tengamos la capacidad de anotarlos como tales), porque son seguros pero la operación es unsafe en const fn .

Depende de lo que quieras decir con "inofensivo" ... Ciertamente puedo ver la razón por la que querríamos prohibir ese comportamiento no determinista.

Sería fantástico que se siguiera trabajando en esto.

@lachlansneff Se está moviendo ... no tan rápido como nos gustaría, pero se está trabajando. Por el momento estamos esperando https://github.com/rust-lang/rust/pull/51110 como bloqueador.

@alexreg Ah, gracias. Sería muy útil poder marcar una coincidencia o si como constante incluso cuando no esté en una constante fn.

¿Alguna actualización de estado ahora que se fusionó # 51110?

@programmerjake Estoy esperando comentarios de @eddyb en https://github.com/rust-lang/rust/pull/52518 antes de que pueda fusionarse (con suerte, muy pronto). Ha estado muy ocupado últimamente (siempre en alta demanda), pero ha vuelto a las críticas y todo eso en los últimos días, así que tengo esperanzas. Después de eso, será necesario que él lo trabaje personalmente, sospecho, ya que agregar un análisis de flujo de datos adecuado es un asunto complicado. Aunque ya veremos.

En algún lugar de las listas TODO en las primeras publicaciones, se debe agregar para eliminar el truco horrible actual que se traduce && y || en & y | constantes internas.

@RalfJung ¿No era eso parte de la vieja evaluación constante, que se ha ido por completo ahora que MIRI CTFE está en su lugar?

AFAIK, hacemos esa traducción en algún lugar de la reducción de HIR, porque tenemos código en const_qualify que rechaza los terminadores SwitchInt que de otro modo serían generados por || / && .

Además, otro punto: @ oli-obk dijo en alguna parte (pero no puedo encontrar dónde) que los condicionales son de alguna manera más complicados de lo que uno pensaría ingenuamente ... ¿fue eso "solo" sobre el análisis de la mutabilidad de caída / interior?

¿Fue eso "solo" sobre el análisis de mutabilidad de gota / interior?

Actualmente estoy tratando de aclarar eso. Te responderé cuando tenga toda la información.

¿Cuál es el estado de esto? ¿Necesita mano de obra o está bloqueado para resolver algún problema?

@ mark-im Está bloqueado para implementar un análisis de flujo de datos adecuado para la calificación const. @eddyb es el que tiene más conocimientos en esta área, y anteriormente había trabajado un poco en esto. (Yo también, pero eso se estancó ...) Si @eddyb todavía no tiene tiempo, tal vez @ oli-obk o @RalfJung podrían abordar esto en algún momento pronto. :-)

58403 es un pequeño paso hacia la calificación basada en el flujo de datos.

@eddyb mencionaste preservar la transparencia referencial en const fn , lo cual creo que es una buena idea. ¿Qué pasa si evitó el uso de punteros en const fn ? Entonces su muestra de código anterior ya no se compilaría:

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
}

Las referencias seguirían estando permitidas, pero no se le permitiría realizar una introspección:

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

Avísame si estoy completamente equivocado, solo pensé que esta sería una buena manera de hacer esto determinista.

@ jyn514 que ya está cubierto al hacer que los moldes de uso sean inestables (https://github.com/rust-lang/rust/issues/51910), pero los usuarios también pueden comparar punteros sin procesar (https://github.com/rust- lang / rust / issues / 53020) que es igual de malo y, por lo tanto, también inestable. Podemos manejarlos independientemente del flujo de control.

¿Alguna novedad en esto?

Existe cierta discusión sobre https://rust-lang.zulipchat.com/#narrow/stream/146212 -t-compiler.2Fconst-eval / topic / dataflow-based.20const.20qualification.20MVP

@ oli-obk tu enlace no funciona. ¿Qué dice?

A mí me funciona ... aunque tienes que iniciar sesión en Zulip.

@alexreg hmm sí, supongo que se trataba del trabajo de calificación const basado en el flujo de datos. @alexreg , ¿sabes por qué es necesario para if y match en constantes?

si no tenemos una versión basada en flujo de datos, permitimos accidentalmente &Cell<T> dentro de las constantes o prohibimos accidentalmente None::<&Cell<T>> (que funciona en estable. Es esencialmente imposible de implementar correctamente sin flujo de datos (o cualquier implementación lo hará) ser una versión ad-hoc defectuosa del flujo de datos)

@ est31 Bueno, @ oli-obk entiende esto mucho mejor que yo, pero desde un nivel alto, básicamente, cualquier cosa que involucre bifurcaciones predicará el análisis de flujo de datos a menos que desee un montón de casos extremos. De todos modos, parece que esta persona en Zulip está tratando de trabajar en eso, y si no sé que oli-obk y eddyb tienen intenciones de hacerlo, tal vez este mes o el próximo (desde la última vez que hablé con ellos), aunque puedo No haré promesas en su nombre.

@alexreg @ mark-im @ est31 @ oli-obk Debería poder publicar mi implementación WIP de la calificación const basada en el flujo de datos en algún momento de esta semana. Aquí hay muchos peligros de compatibilidad, por lo que puede llevar un tiempo fusionarlos.

Súper; lo espero con ansias.

(copia de # 57563 por solicitud)

¿Sería posible en casos especiales bool && bool , bool || bool , etc.? Actualmente se pueden realizar en un const fn , pero hacerlo requiere operadores bit a bit, lo que a veces no es deseado.

Ya están en mayúsculas y minúsculas especiales en los elementos const y static , traduciéndolos a operaciones bit a bit. Pero esa carcasa especial es un gran truco y es muy difícil asegurarse de que esto sea realmente correcto. Como dijiste, a veces también es indeseado. Así que preferimos no hacer esto más a menudo.

Hacer las cosas bien tomará un poco, pero sucederá. Si acumulamos demasiados trucos mientras tanto, podríamos arrinconarnos en una esquina de la que no podamos salir (si algunos de esos trucos terminan interactuando de manera incorrecta, estabilizando accidentalmente el comportamiento que no queremos).

Ahora que se han fusionado # 64470 y # 63812, todas las herramientas necesarias para esto existen en el compilador. Todavía necesito hacer algunos cambios en el sistema de consulta sobre la calificación const para asegurarme de que no sea innecesariamente ineficiente con esta función habilitada. Estamos progresando aquí, y creo que una implementación experimental de esto estará disponible todas las noches en semanas, no en meses (las famosas últimas palabras: sonrisa :).

@ extático-morse ¡Qué bueno escuchar! Gracias por sus esfuerzos concertados para lograrlo; Personalmente, he estado interesado en esta función desde hace un tiempo.

Me encantaría ver el soporte de asignación de montón para CTFE una vez hecho esto. No sé si usted o alguien más está interesado en trabajar en esto, pero si no, quizás pueda ayudar.

@alexreg ¡Gracias!

La discusión sobre la asignación del montón en tiempo de compilación finaliza en rust-rfcs / const-eval # 20. AFAIK, los desarrollos más recientes fueron alrededor de un paradigma ConstSafe / ConstRefSafe para determinar qué se puede observar directamente / detrás de una referencia en el valor final de un const . Sin embargo, creo que se necesita más trabajo de diseño.

Para aquellos que lo siguen, # 65949 (que a su vez depende de algunos PR más pequeños) es el siguiente bloqueador para esto. Si bien puede parecer solo tangencialmente relacionado, el hecho de que la verificación de constantes / calificación estuviera tan estrechamente unida a la promoción fue parte de la razón por la que esta función estuvo bloqueada durante tanto tiempo. Planeo abrir un PR posterior que eliminará por completo el antiguo comprobador de constantes (actualmente ejecutamos ambos comprobadores en paralelo). Esto evitará las ineficiencias que mencioné anteriormente.

Después de que los dos PR mencionados anteriormente se fusionen, if y match en constantes serán algunas mejoras de diagnóstico y un indicador de función. Ah, y también pruebas, tantas pruebas ...

Si necesita pruebas, no estoy seguro de cómo empezar, ¡pero estoy más que dispuesto a contribuir! Solo avíseme dónde deben ir las pruebas / cómo deben verse / en qué rama debo basar el código :)

El siguiente PR a seguir es el # 66385. Esto elimina la antigua lógica de calificación const (que no podía manejar la ramificación) completamente a favor de la nueva versión basada en flujo de datos.

@ jyn514 ¡ Eso sería genial! Te enviaré un ping cuando empiece a redactar el borrador de la implementación. También sería muy útil para las personas intentar violar la seguridad constante (especialmente la parte HasMutInterior ) una vez que if y match estén disponibles cada noche.

66507 contiene una implementación inicial de RFC 2342.

Supongo que llevará un tiempo eliminar las asperezas, especialmente con respecto a los diagnósticos, y la cobertura de la prueba es bastante escasa ( @ jyn514 deberíamos coordinarnos sobre ese tema). Sin embargo, espero que podamos publicar esto detrás de una bandera de funciones en las próximas semanas.

Esto se implementó en # 66507 y ahora se puede usar en la última versión publicación de blog Inside Rust que detalla las operaciones recientemente disponibles, así como algunos problemas que puede encontrar con la implementación existente en torno a tipos con mutabilidad interior o un Drop impl personalizado.

¡Ve y constifica!

Parece que la igualdad no es const ? O me equivoco:

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 Eso debería funcionar . ¿Quizás un problema de arranque? Discutamos sobre Zulip.

No estoy seguro de si esto es intencional, pero intentar hacer coincidir una enumeración da el error

const fn con código inalcanzable no es estable

a pesar de que la enumeración es exhaustiva y está definida en la misma caja.

@jhpratt ¿puedes publicar el código? Puedo hacer coincidir enumeraciones simples sin ningún problema: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=585e9c2823afcb49c6682f69569c97ea

@jhpratt ¿puedes publicar el código? Puedo coincidir en enumeraciones simples sin ningún problema:

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

Golpéame por un par de segundos. Ese es un ejemplo mínimo que demuestra mi caso exacto.

@jhpratt Definitivamente no es intencional. ¿Podrías abrir un problema?

Redirija la constificación de funciones específicas o problemas que desea informar a problemas nuevos y etiquételos adecuadamente con F-const_if_match para que estos problemas no se inunden con comentarios efímeros que oculten desarrollos importantes.

@Centril No es malo ponerlo en el comentario principal para que el tuyo no

Actualización de estado:

Esto está listo para la estabilización desde una perspectiva de implementación, pero existe la cuestión de si queremos mantener el flujo de datos basado en valores que tenemos ahora en lugar de uno basado en tipos (pero menos poderoso). El flujo de datos basado en valor es un poco más caro (más sobre eso más abajo), y lo necesitamos para funciones como

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

que un análisis basado en tipos rechazaría, porque un Option<T> puede tener destructores que ahora intentarían ejecutarse y, por lo tanto, podrían ejecutar código no constante.

Podemos recurrir a un análisis basado en tipos en el momento en que hay ramas, pero eso significaría que rechazaríamos

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

lo que probablemente sorprendería mucho a los usuarios.

@ ecstatic-morse ejecutó el análisis en todas las funciones, no solo en const fn y vio ralentizaciones de hasta un 5% (https://perf.rust-lang.org/compare.html?start=93dc97a85381cc52eb872d27e50e4d518926a27c&end=51cf313c794650d5bec38639ea). Tenga en cuenta que esta es una versión pesimista, ya que significa que también se ejecuta en funciones que no se convertirán en const fn .

Esto significa que si hacemos muchas funciones const fn, es posible que veamos algunas ralentizaciones de compilación debido a este análisis basado en valores.

Un término medio podría ser ejecutar el análisis basado en el valor solo si falla el análisis basado en el tipo. Esto significa que si no hay destructores, no necesitamos ejecutar el análisis basado en valores para averiguar si los destructores no existentes no se ejecutarán (sí, lo sé, hay muchas negaciones aquí). Para decirlo de otra manera: solo ejecutamos el análisis basado en valores si hay destructores presentes.

Estoy nominando esto para la discusión de @ rust-lang / lang para que podamos averiguar si queremos ir con

  • la opción basada en tipo en presencia de bucles o ramas (dando un comportamiento extraño a los usuarios)
  • análisis basado en el valor completo (más caro, pero con total expresividad para los usuarios)
  • esquema mixto, todavía expresividad total para los usuarios, cierta complejidad implícita adicional, pero debería reducir los problemas de tiempo de compilación a los casos que lo necesitan.

@ oli-obk

la opción basada en tipo en presencia de bucles o ramas (dando un comportamiento extraño a los usuarios)

Solo para verificar esto: ¿no es una opción tener un análisis basado en tipos incluso en código de línea recta? Me imagino que eso es algo incompatible al revés, dado que ya aceptamos lo siguiente ( patio de recreo ):

struct Foo { }

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

const T: Option<Foo> = None;

fn main() { }

Personalmente, tiendo a pensar que deberíamos impulsar una experiencia mejor y más consistente para los usuarios. Parece que podemos optimizar según sea necesario y, en cualquier caso, el costo no es tan malo. Pero me gustaría entender un poco mejor exactamente lo que está sucediendo en este análisis más costoso: es la idea de que básicamente estamos haciendo "propagación constante", de modo que cada vez que se cae algo, analizamos el valor exacto que se elimina para determinar si ¿Puede contener un valor que necesitaría ejecutar un destructor? (es decir, si es None , para usar el ejemplo común de Option<T> )

Solo para verificar esto: ¿no es una opción tener un análisis basado en tipos incluso en código de línea recta? Me imagino que eso es algo incompatible al revés, dado que ya aceptamos lo siguiente (patio de recreo):

Sí, esa es la razón por la que no podemos pasar por completo al análisis basado en tipos.

¿Es la idea de que básicamente estamos haciendo "propagación constante", de modo que cada vez que se cae algo, analizamos el valor exacto que se elimina para determinar si puede contener un valor que necesitaría ejecutar un destructor? (es decir, si es Ninguno, para usar el ejemplo común de Option)

Solo estamos propagando una lista de banderas ( Drop y Freeze , acabo de mostrar Drop aquí porque es más fácil de explicar). Cuando llegamos a un terminador Drop sin haber establecido el indicador Drop , ignoramos el terminador Drop . Esto permite código como el siguiente:

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

Esto no sucede en el momento de la evaluación, por lo que lo siguiente no está bien:

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

Verificamos que todas las rutas de ejecución posibles no causen un Drop .

Sin embargo, la propagación constante es una buena analogía. Es otro problema de flujo de datos cuya función de transferencia no se puede expresar con conjuntos gen / kill, que no manejan el estado de copia entre variables. Sin embargo, la propagación constante necesita almacenar el valor real de cada variable, pero la verificación constante solo necesita almacenar un solo bit que indique si esa variable tiene un Drop implícito personalizado o no Freeze lo que lo hace un poco menos costoso que la propagación constante.

Para ser claros, el primer ejemplo de @ oli-obk se compila en estable hoy, y lo ha hecho desde 1.38.0 , que no incluía # 64470.

Además, const X: Option<Foo> = None; compila desde 1.0, todo lo demás es solo una extensión natural de eso con las nuevas características que ha ganado const eval.

De acuerdo, creo que tiene sentido adoptar la opción puramente basada en valores.

Supongo que podemos cubrirlo en la reunión e informar =)

Resumen

Propongo que estabilicemos #![feature(const_if_match)] con la semántica actual.

Específicamente, las expresiones if y match así como los operadores lógicos de cortocircuito && y || serán legales en todos los contextos const . Un contexto constante es cualquiera de los siguientes:

  • El inicializador de un discriminante const , static , static mut o enumeración.
  • El cuerpo de un const fn .
  • El valor de un genérico const (solo por la noche).
  • La longitud de un tipo de matriz ( [u8; 3] ) o una expresión de repetición de matriz ( [0u8; 3] ).

Además, los operadores lógicos de cortocircuito ya no se reducirán a sus equivalentes bit a bit ( & y | respectivamente) en los inicializadores const y static (ver # 57175). Como resultado, los enlaces let se pueden usar junto con la lógica de cortocircuito en esos inicializadores.

Problema de seguimiento: # 49146
Objetivo de la versión: 1.45 (16-06-2020)

Historial de implementación

64470 implementó un análisis estático basado en valores que admitió el flujo de control condicional y se basó en el flujo de datos. Esto, junto con el # 63812, nos permitió reemplazar el antiguo código de verificación constante por uno que funcionaba en gráficos de flujo de control complejos. El antiguo const-checker se ejecutó en paralelo con el basado en el flujo de datos durante un tiempo para asegurarse de que estaban de acuerdo con los programas con un flujo de control simple. # 66385 eliminó el antiguo const-checker a favor del basado en flujo de datos.

66507 implementó la puerta de función #![feature(const_if_match)] con la semántica que ahora se propone para la estabilización.

Calificación constante

Fondo

[Miri] ha impulsado la evaluación de funciones en tiempo de compilación (CTFE) en rustc desde hace varios años, y ha podido evaluar declaraciones condicionales durante al menos ese tiempo. Durante CTFE, debemos evitar ciertas operaciones, como llamar a Drop impls personalizados o tomar una referencia a un valor con mutabilidad interior . En conjunto, estas propiedades descalificantes se conocen como "calificaciones", y el proceso de determinar si un valor tiene una calificación en un punto específico del programa se conoce como "calificación constante".

Miri es perfectamente capaz de emitir un error cuando encuentra una operación ilegal sobre un valor calificado, y puede hacerlo sin falsos positivos. Sin embargo, CTFE ocurre después de la monomorfización, lo que significa que no puede saber si las constantes definidas en un contexto genérico son válidas hasta que se instancian, lo que podría suceder en otra caja. Para obtener errores de premonomorfización, debemos implementar un análisis estático que no califique const. En el caso general, la calificación const es indecidible (ver Teorema de Rice ), por lo que cualquier análisis estático solo puede aproximarse a las verificaciones que realiza Miri durante CTFE.

Nuestro análisis estático debe prohibir que una referencia a un tipo con mutabilidad interior (por ejemplo, &Cell<i32> ) aparezca en el valor final de un const . Si esto estuviera permitido, un const podría modificarse en tiempo de ejecución.

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

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

Sin embargo, permitimos que el usuario defina un const cuyo tipo tenga mutabilidad interior ( !Freeze ) siempre que podamos demostrar que el valor final de ese const no lo tiene. Por ejemplo, se ha compilado lo siguiente desde la primera edición de stable rust :

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

Este enfoque del análisis estático, que llamaré basado en valores en lugar de basado en tipos, también se usa para verificar el código que puede resultar en una llamada Drop impl personalizada. Llamar a Drop impls es problemático porque no se comprueban const y, por lo tanto, pueden contener código que no estaría permitido en un contexto const. El razonamiento basado en valores se extendió para admitir declaraciones let , lo que significa que las siguientes compilaciones en rust 1.42.0 estable .

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

Semántica nocturna actual

El comportamiento actual de #![feature(const_if_match)] extiende la semántica basada en valores para trabajar en gráficos de flujo de control complejos mediante el uso de flujo de datos. En otras palabras, intentamos demostrar que una variable no tiene la calificación en cuestión a lo largo de todos los caminos posibles a través del programa.

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

Todos los caminos posibles a través del programa incluyen aquellos a los que puede que nunca se llegue en la práctica. Un ejemplo, usando la misma enumeración Int que la anterior:

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

Este análisis trata las llamadas a funciones como opacas, asumiendo que su valor de retorno puede contener cualquier valor de su tipo. También recurrimos a un análisis basado en tipos para una variable tan pronto como se crea una referencia mutable a ella. Tenga en cuenta que la creación de una referencia mutable en un contexto constante está prohibida actualmente en el óxido estable.

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

Puede ver más ejemplos de cómo un análisis basado en valores es conservador en torno a la mutabilidad interior y las implicaciones de caída personalizadas , así como algunos casos en los que un análisis conservador puede demostrar que no puede suceder nada ilegal en el conjunto de pruebas.

Alternativas

Me ha resultado difícil encontrar alternativas prácticas y compatibles con versiones anteriores al enfoque existente. Podríamos recurrir al análisis basado en tipos para todas las variables tan pronto como se utilicen condicionales en un contexto constante. Sin embargo, eso también sería difícil de explicar a los usuarios, ya que las adiciones aparentemente no relacionadas causarían que el código ya no se compilara, como assert en el siguiente ejemplo de @ oli-obk.

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

La mayor expresividad del análisis basado en valores no es gratuita. Una ejecución de rendimiento que hizo calificación constante en todos los cuerpos de elementos, no solo const , mostró una regresión del 5% en las compilaciones de verificación . Este es el peor de los casos, ya que se supone que todos los elementos se fabricarán const en algún momento en el futuro. Las posibles optimizaciones, como la de # 71330, se han discutido anteriormente en el hilo.

Trabajo futuro

Por el momento, la verificación constante se ejecuta antes de la elaboración de la gota, lo que significa que algunos terminadores de la gota permanecen en el MIR que son inalcanzables en la práctica. Esto evita que Option::unwrap convierta en const fn (ver # 66753). Esto no es demasiado difícil de resolver, pero requerirá dividir el pase de verificación constante en dos fases (elaboración previa y posterior a la caída).

Una vez que #![feature(const_if_match)] se estabiliza, una gran cantidad de funciones de biblioteca se pueden convertir en const fn . Esto incluye muchos métodos sobre tipos de enteros primitivos, que se han enumerado en # 53718.

Los bucles en un contexto const se bloquean en la misma pregunta de calificación const que los condicionales. El enfoque actual basado en el flujo de datos también funciona para CFG cíclicos sin modificaciones, por lo que si #![feature(const_if_match)] se estabiliza, el bloqueador principal para # 52000 desaparecerá.

Agradecimientos

Un agradecimiento especial a @ oli-obk y @eddyb , quienes fueron los revisores principales de la mayor parte del trabajo de implementación, así como al resto de @ rust-lang / wg-const-eval por ayudarme a comprender los problemas relevantes en torno a const. calificación. Nada de esto sería posible sin Miri, que fue creada por @solson y ahora mantenida por @RalfJung y @ oli-obk.

Este está destinado a ser el informe de estabilización anterior al FCP. Sin embargo, no puedo abrir FCP.

@ ecstatic-morse ¡Muchas gracias por todo su arduo trabajo en este tema!

¡Gran informe!

Una cosa que creo que me gustaría ver, @ extático-morse, es

  • enlaces a algunas pruebas representativas en el repositorio, para que podamos observar el comportamiento
  • si hay implicaciones en torno a semver o cualquier otra cosa, creo que la respuesta es en gran medida no , ¿verdad? En otras palabras, estamos decidiendo sobre el análisis usado para determinar si el cuerpo de una constante fn es legal, pero dada una constante fn, nuestras opciones aquí no determinan cosas como "lo que el llamador de la constante fn puede hacer con el resultado ", ¿verdad? Estoy tratando de averiguar qué ejemplo podría ser de lo que estoy hablando; supongo que la persona que llama no llega a saber con precisión qué variantes de una enumeración se usaron, solo eso, cualquier valor fue devuelto: no tenía mutabilidad interior (en la que presumiblemente tampoco pueden confiar cuando se combinan).

En otras palabras, estamos decidiendo sobre el análisis usado para determinar si el cuerpo de una constante fn es legal, pero dada una constante fn, nuestras opciones aquí no determinan cosas como "lo que el llamador de la constante fn puede hacer con el resultado ", ¿verdad? Estoy tratando de averiguar qué ejemplo podría ser de lo que estoy hablando; supongo que la persona que llama no llega a saber con precisión qué variantes de una enumeración se usaron, solo eso, cualquier valor fue devuelto: no tenía mutabilidad interior (en la que presumiblemente tampoco pueden confiar cuando se combinan).

Sí, el cuerpo de una constante fn es opaco. Esto contrasta con la expresión inicializadora de un elemento const . Puede observar esto por el hecho de que

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

se puede utilizar para crear un &'static Option<Cell<i32>>

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

mientras que una constante fn con el mismo cuerpo no puede:

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

demostración del patio de recreo

Cuando introducimos el flujo de control a las constantes, esto significa que

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

también funcionará, independientemente del valor de MEH y

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

no funcionará, nuevamente, sin importar el valor de MEH .

El flujo de control no cambia nada acerca de los sitios de llamadas de const fn , solo qué código está permitido dentro de esa constante fn.

enlaces a algunas pruebas representativas en el repositorio, para que podamos observar el comportamiento.

Agregué un párrafo al final de la sección "Semántica nocturna actual" que enlaza con algunos casos de prueba interesantes. Siento que necesitamos más pruebas (una afirmación que es cierta independientemente de las circunstancias) antes de que esto se estabilice, pero eso se puede abordar una vez que decidamos si la semántica actual es deseable.

si hay implicaciones en torno a semver o cualquier otra cosa.

Además de lo que @ oli-obk dijo anteriormente, quiero señalar que cambiar el valor final de un const es técnicamente un cambio semver-rompiente ya:

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

Sin embargo, debido a que no podemos hacer una calificación constante con precisión perfecta, cambiar una constante para usar if o match podría romper el código descendente, incluso si el valor final no cambia. Por ejemplo:

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

Esto no se aplica a los cambios dentro del cuerpo de un const fn , porque siempre usamos una calificación basada en el tipo para el valor de retorno, incluso dentro de la misma caja.

En mi opinión, el "pecado original" aquí no fue recurrir a la calificación basada en tipos para const y static s definidos en cajas externas. Sin embargo, creo que este ha sido el caso desde la versión 1.0, y sospecho que una gran cantidad de código depende de ello. Tan pronto como permita inicializadores constantes para los cuales el análisis estático no puede ser perfectamente preciso, será posible modificar esos inicializadores de tal manera que producirán el mismo valor sin que el análisis estático pueda probarlo.

editar:

No hay nada único en if y match a este respecto. Por ejemplo, actualmente es un cambio rotundo refactorizar un inicializador const en un const fn si la caja posterior dependía de la calificación basada en el valor.

// 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 ¡Gracias por redactar el informe de estabilización! Evaluemos el consenso de forma asincrónica:

@rfcbot fusionar

Si alguien quiere discutir esto sincrónicamente en una reunión, vuelva a nominar.

El miembro del equipo @joshtriplett ha propuesto fusionar esto. El siguiente paso lo revisan el resto de los miembros del equipo etiquetados:

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

No hay preocupaciones actualmente enumeradas.

Una vez que la mayoría de los revisores aprueben (y como máximo 2 aprobaciones estén pendientes), entrará en su período de comentarios final. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡hable!

Consulte este documento para obtener información sobre los comandos que pueden darme los miembros del equipo etiquetados.

: bell: Esto ahora está entrando en su período de comentarios final , según la revisión anterior . :campana:

¿Esto también permite usar ? en const fn ?

Usar ? significa usar el rasgo Try . El uso de rasgos en const fn es inestable, consulte https://github.com/rust-lang/rust/issues/67794.

@TimDiekmann por el momento, tendrá que escribir macros proc que loop y for , al menos hasta un cierto límite (estilo de recursividad primitiva) pero const eval tiene esos límites de todos modos. Esta característica es tan asombrosa que permite MUCHAS cosas que antes no eran posibles. Incluso puede construir un diminuto wasm vm en const fn si lo desea.

El período de comentarios final, con una disposición para fusionarse , según la revisión anterior , ahora está completo .

Como representante automatizado del proceso de gobernanza, me gustaría agradecer al autor por su trabajo y a todos los que contribuyeron.

El RFC se fusionará pronto.

¿Fue útil esta página
0 / 5 - 0 calificaciones