Rust: Problema de seguimiento de const fn (RFC 911)

Creado en 6 abr. 2015  ·  274Comentarios  ·  Fuente: rust-lang/rust

https://github.com/rust-lang/rust/issues/57563 | nuevo problema de meta seguimiento

Contenido antiguo

Problema de seguimiento para rust-lang/rfcs#911.

Este problema se ha cerrado en favor de problemas más específicos:

Cosas que hacer antes de estabilizar:

CTFE = https://en.wikipedia.org/wiki/Compile_time_function_execution

A-const-eval A-const-fn B-RFC-approved B-RFC-implemented B-unstable C-tracking-issue T-lang

Comentario más útil

Por favor, ignore esto si estoy completamente fuera de lugar.

El problema que veo con este RFC es que, como usuario, debe marcar tantas funciones como const fn como sea posible porque probablemente sea la mejor práctica. Lo mismo está sucediendo actualmente en C++ con contexpr. Creo que esto es solo una verbosidad innecesaria.

D no tiene const fn pero permite llamar a cualquier función en tiempo de compilación (con algunas excepciones).

por ejemplo

// Standalone example.
struct Point { x: i32, y: i32 }

impl Point {
    fn new(x: i32, y: i32) -> Point {
        Point { x: x, y: y }
    }

    fn add(self, other: Point) -> Point {
        Point::new(self.x + other.x, self.y + other.y)
    }
}

const ORIGIN: Point = Point::new(0, 0); // works because 0, 0 are both known at compile time 
const ORIGIN2: Point = Point::new(0, 0); // ditto

const ANOTHER: Point = ORIGIN.add(ORIGIN2); // works because ORIGIN and ORIGIN2 are both const.
{
    let x: i32 = 42;
    let y: i32 = 24;
    const SOME_POINT: Point = Point::new(x, y); // Error: x and y are not known at compile time
}
{
    const x: i32 = 42;
    const y: i32 = 24;
    const SOME_POINT: Point = Point::new(x, y); // Works x and y are both known at compile time.
}

Tenga en cuenta que no soy realmente un usuario de Rust y solo he leído el RFC hace unos minutos, por lo que es posible que haya entendido mal algo.

Todos 274 comentarios

¿Está cerrado por #25609?

@Munksgaard Eso solo agrega soporte al compilador AFAIK. Hay muchas funciones en stdlib que deben cambiarse a const fn y probarse en busca de roturas. No sé cuál es el progreso en eso.

Espero que esto se implemente en std::ptr::null() y null_mut() para que podamos usarlos para inicializar static mut *MyTypeWithDrop sin recurrir a 0usize as *mut _

EDITAR: Eliminado porque estaba fuera de tema

Para ser claros, la pregunta aquí no es principalmente sobre la utilidad de la función, sino más bien sobre la mejor manera de formularla (o el mejor marco para formularla). Vea la discusión de RFC.

Este es ahora el problema de seguimiento para una eventual estabilización.

https://github.com/rust-lang/rust/issues/29107 ha sido cerrado.

No estoy de acuerdo con que la "Integración con patrones" o cualquier cambio en la biblioteca estándar bloqueen esto. Esto es muy útil incluso sin esos cambios, y esos cambios se pueden hacer más tarde. En particular, me gustaría comenzar a usar const fn en mi propio código pronto.

En consecuencia, ¿podría reevaluarse el estado de estabilización de este?

No dudo que const fn incluso en su forma limitada actual, sería una funcionalidad útil, pero lo que realmente me gustaría, idealmente antes de continuar por este camino, sería para aquellos a favor de "el const fn approach" para pensar y articular su final preferido. Si continuamos agregando funcionalidades aparentemente útiles de la manera más obvia, me parece muy probable que finalmente terminemos copiando más o menos la totalidad del diseño constexpr de C++. ¿Es algo con lo que nos sentimos cómodos? Incluso si decimos que sí, preferiría que escogiéramos ese camino con lucidez, en lugar de retroceder en él con pequeños pasos a lo largo del tiempo, como el camino de menor resistencia, hasta que se vuelva inevitable.

(Dado que la semántica del código seguro de Rust debería ser completamente definible, parece probable que eventualmente al menos cada función que no dependa (transitivamente) de unsafe debería poder marcarse como const . Y dado que se supone que unsafe es un detalle de implementación, apuesto a que la gente también presionará para aflojar esa restricción de alguna manera. historia bien integrada para puesta en escena y cálculo de nivel de tipo).

@glaebhoerl

No dudo que const fn, incluso en su forma limitada actual, sería una funcionalidad útil, pero lo que realmente me gustaría, idealmente antes de continuar por este camino, sería que aquellos a favor del "enfoque const fn" pensar y articular su juego final preferido... me parece muy probable que finalmente terminemos copiando más o menos la totalidad del diseño constexpr de C++.

Lo que personalmente me gustaría, incluso más que eso, es que tengamos una visión bastante clara de cómo vamos a implementarlo y qué parte del idioma vamos a cubrir. Dicho esto, en mi opinión, esto está muy relacionado con la compatibilidad con constantes asociadas o genéricos sobre números enteros.

@eddyb y yo hicimos algunos bocetos recientemente en un esquema que podría permitir la evaluación constante de una franja muy amplia de código. Básicamente, bajar todas las constantes a MIR e interpretarlo (en algunos casos,
interpretación abstracta, si hay genéricos que aún no puedes evaluar, que es donde las cosas se ponen más interesantes para mí).

Sin embargo, aunque parece que sería bastante fácil admitir una fracción muy grande del "lenguaje integrado", el código real en la práctica choca contra la necesidad de realizar la asignación de memoria muy rápidamente. En otras palabras, quiere usar Vec o algún otro contenedor. Y ahí es donde todo este esquema de interpretación empieza a complicarse para mi mente.

Dicho esto, @glaebhoerl , también me encantaría escucharte articular tu final alternativo preferido. Creo que esbozaste algunos de esos pensamientos en el const fn RFC, pero creo que sería bueno escucharlo nuevamente, y en este contexto. :)

El problema con la asignación es que escape al tiempo de ejecución.
Si de alguna manera podemos impedir cruzar esa barrera de tiempo de compilación/tiempo de ejecución, entonces creo que podríamos tener un liballoc funcional con const fn .
No sería más difícil administrar ese tipo de asignaciones que manejar valores direccionables por bytes en una pila interpretada.

Alternativamente, podríamos generar código de tiempo de ejecución para asignar y completar los valores cada vez que se deba pasar esa barrera, aunque no estoy seguro de qué tipo de casos de uso tiene.

Tenga en cuenta que incluso con una evaluación completa similar a constexpr , const fn _todavía_ sería puro: ejecutarlo dos veces en datos de 'static daría exactamente el mismo resultado y no efectos secundarios.

@nikomatsakis Si tuviera uno, lo habría mencionado. :) Principalmente solo veo incógnitas conocidas. Todo el asunto con const s como parte del sistema genérico fue, por supuesto, parte de lo que entendí como el diseño de C++. En cuanto a tener asociados const s y const parámetros genéricos, teniendo en cuenta que ya tenemos matrices de tamaño fijo con const s como parte de su tipo y nos gustaría abstraernos ellos, me sorprendería si hubiera una manera mucho mejor, en lugar de simplemente más general, de hacerlo. La parte const fn de las cosas se siente más separable y variable. Es fácil imaginar llevar las cosas más lejos y tener cosas como const impl s y const Trait límites en genéricos, pero estoy _seguro_ que existe un estado de la técnica para este tipo de cosas generales que ya ha resuelto las cosas y debemos tratar de encontrarlo.

De los principales casos de uso del lenguaje Rust, los que principalmente necesitan un control de bajo nivel, como los núcleos, ya parecen razonablemente bien atendidos, pero otra área en la que Rust podría tener mucho potencial son las cosas que principalmente necesitan un alto rendimiento, y en ese soporte espacial poderoso (de alguna forma) para el cálculo por etapas (del cual const fn ya es una instancia muy limitada) parece que podría cambiar las reglas del juego. (Solo en las últimas semanas me encontré con dos tweets separados de personas que decidieron cambiar de Rust a un idioma con mejores capacidades de preparación). No estoy seguro de si alguna de las soluciones existentes en idiomas "cerca de nosotros" -- constexpr C++, CTFE ad-hoc de D, nuestras macros de procedimiento: realmente se sienten inspiradores y poderosos/lo suficientemente completos para este tipo de cosas. (Las macros de procedimiento parecen algo bueno, pero más para abstracción y DSL, no tanto para la generación de código orientada al rendimiento).

En cuanto a lo que _sería_ inspirador y lo suficientemente bueno... Todavía no lo he visto, y no estoy lo suficientemente familiarizado con todo el espacio para saber, con precisión, dónde buscar. Por supuesto, según lo anterior, es posible que queramos al menos echar un vistazo a Julia y Terra, incluso si parecen lenguajes bastante diferentes de Rust en muchos aspectos. Sé que Oleg Kiselyov ha realizado un trabajo muy interesante en esta área. El trabajo de Tiark Rompf en Lancet y el montaje modular ligero para Scala definitivamente vale la pena mirarlo. Recuerdo haber visto una presentación de @kmcallister en algún momento sobre cómo se vería un Rust tipado de forma dependiente (que al menos podría ser más general que pegar const todas partes), y también recuerdo haber visto algo de Oleg en el sentido que los tipos en sí mismos son una forma de puesta en escena (lo cual se siente natural considerando que la separación de fase entre compilación y tiempo de ejecución se parece mucho a las etapas)... muchas conexiones potenciales emocionantes en muchas direcciones diferentes, por lo que se sentiría como un error oportunidad si nos comprometiéramos con la primera solución que se nos ocurra. :)

(Esto fue solo una descarga de cerebro y casi seguramente he caracterizado de manera imperfecta muchas cosas).

Sin embargo, aunque parece que sería bastante fácil admitir una fracción muy grande del "lenguaje integrado", el código real en la práctica choca contra la necesidad de realizar la asignación de memoria muy rápidamente. En otras palabras, desea utilizar Vec o algún otro contenedor. Y ahí es donde todo este esquema de interpretación empieza a complicarse para mi mente.

No estoy de acuerdo con esa caracterización de "código real en la práctica". Creo que hay un gran interés en Rust porque ayuda a reducir la necesidad de asignación de memoria en montón. Mi código, en particular, hace un esfuerzo concreto para evitar la asignación de montones siempre que sea posible.

Ser capaz de hacer más que eso sería _bueno_ pero es esencial poder construir instancias estáticas de tipos no triviales con invariantes forzadas por el compilador. El enfoque de C++ constexpr es extremadamente limitante, pero es más de lo que necesito para mis casos de uso: necesito proporcionar una función que pueda construir una instancia de tipo T con parámetros x , y y z manera que la función garantiza que x , y y z son válidos (p. ej., x < y && z > 0 ), de modo que el resultado puede ser una variable static , sin el uso del código de inicialización que se ejecuta al inicio.

@briansmith FWIW, otro enfoque que tiene la posibilidad de resolver los mismos casos de uso sería si las macros tuvieran higiene de privacidad , que creo (espero) que estamos planeando hacer que tengan.

@briansmith FWIW, otro enfoque que tiene la posibilidad de resolver los mismos casos de uso sería si las macros tuvieran higiene de privacidad, que creo (espero) que estamos planeando hacer que tengan.

Supongo que si usa las macros de procedimiento, puede evaluar x < y && z > 0 en tiempo de compilación. Pero, parece que pasarán muchos, muchos meses antes de que las macros de procedimiento puedan usarse en Rust estable, si es que alguna vez lo hacen. const fn es interesante porque se puede habilitar para Rust estable _ahora_, según tengo entendido el estado de las cosas.

@glaebhoerl No aguantaría la respiración por una higiene estricta, es muy posible que tengamos un mecanismo de escape (al igual que los LISP reales), por lo que es posible que no lo desee para ningún tipo de seguridad.

@glaebhoerl también hay https://anydsl.github.io/, que incluso usa
Sintaxis similar a la de Rust;) básicamente están dirigidos a la computación por etapas y en
evaluación parcial particular.

El sábado 16 de enero de 2016 a las 18:29, Eduard-Mihai Burtescu <
[email protected]> escribió:

@glaebhoerl https://github.com/glaebhoerl No aguantaría la respiración por
higiene estricta, es muy posible que tengamos un mecanismo de escape
(al igual que los LISP reales), por lo que es posible que no lo desee por ningún tipo de seguridad
propósitos


Responda a este correo electrónico directamente o véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment -172271960.

29525 debe corregirse antes de la estabilización

Dado que la semántica del código seguro de Rust debería ser completamente definible, parece probable que eventualmente al menos cada función que no dependa (transitivamente) de unsafe debería poder marcarse como const . Y dado que se supone que unsafe es un detalle de implementación, apuesto a que la gente también presionará para aflojar esa restricción de alguna manera.

Solo un pensamiento: si alguna vez definimos formalmente el modelo de memoria de Rust , entonces incluso el código unsafe podría evaluarse de manera segura y sensata en tiempo de compilación al interpretarlo de manera abstracta/simbólica, es decir, el uso de punteros sin formato sería t convertirse en accesos directos a la memoria como en tiempo de ejecución, sino algo (solo como un ejemplo para ilustración) como una búsqueda en un mapa hash de direcciones asignadas, junto con sus tipos y valores, o similar, con cada paso verificado para verificar su validez, por lo que que cualquier ejecución cuyo comportamiento no esté definido sería _estrictamente_ un error informado por el compilador, en lugar de una vulnerabilidad de seguridad en rustc . (Esto también podría estar relacionado con la situación en la que se manejan isize y usize en tiempo de compilación simbólicamente o dependiendo de la plataforma).

No estoy seguro de dónde nos deja eso con respecto a const fn . Por un lado, eso probablemente abriría un código mucho más útil para estar disponible en el momento de la compilación: Box , Vec , Rc , cualquier cosa que use unsafe para la optimización del rendimiento, lo cual es bueno. Una restricción arbitraria menos. Por otro lado, el límite de "lo que posiblemente puede ser un const fn " ahora se movería esencialmente hacia afuera a _cualquier cosa que no involucre a la FFI_. Entonces, lo que _realmente_ estaríamos rastreando en el sistema de tipos, bajo la apariencia de const ness, es qué cosas (transitivamente) dependen del FFI y qué cosas no. Y si algo usa o no el FFI es _todavía_ algo que la gente considera legítimamente como un detalle de implementación interna, y esta restricción (a diferencia unsafe ) _realmente_ no parece factible de eliminar. Y bajo este escenario, probablemente tendría muchos más fn s elegibles para const ness que los que no lo harían.

Por lo tanto, todavía tendría const ness girando en torno a una restricción arbitraria que expone la implementación, y también terminaría escribiendo const casi todas partes. Eso tampoco suena muy atractivo...

es decir, el uso de punteros sin formato no se convertiría en accesos directos a la memoria como en tiempo de ejecución, sino en algo... como una búsqueda en un mapa hash de direcciones asignadas,

@glaebhoerl Bueno, ese es más o menos el modelo que describí y que miri de @tsion está implementando.

Creo que la distinción FFI es muy importante debido a la pureza, que es _requerida_ para la coherencia.
Ni siquiera _podría_ usar GHC para Rust const fn s porque tiene unsafePerformIO .

No me gusta demasiado la palabra clave const por lo que estoy de acuerdo con const fn foo<T: Trait> en lugar de const fn foo<T: const Trait> (para requerir un const impl Trait for T ).

Al igual que Sized , probablemente tengamos los valores predeterminados incorrectos, pero no he visto ninguna otra propuesta que pueda funcionar de manera realista.

@eddyb Creo que quería vincular a https://internals.rust-lang.org/t/mir-constant-e Evaluation/3143/31 (comentario 31, no 11).

@tsion Arreglado, gracias!

Por favor, ignore esto si estoy completamente fuera de lugar.

El problema que veo con este RFC es que, como usuario, debe marcar tantas funciones como const fn como sea posible porque probablemente sea la mejor práctica. Lo mismo está sucediendo actualmente en C++ con contexpr. Creo que esto es solo una verbosidad innecesaria.

D no tiene const fn pero permite llamar a cualquier función en tiempo de compilación (con algunas excepciones).

por ejemplo

// Standalone example.
struct Point { x: i32, y: i32 }

impl Point {
    fn new(x: i32, y: i32) -> Point {
        Point { x: x, y: y }
    }

    fn add(self, other: Point) -> Point {
        Point::new(self.x + other.x, self.y + other.y)
    }
}

const ORIGIN: Point = Point::new(0, 0); // works because 0, 0 are both known at compile time 
const ORIGIN2: Point = Point::new(0, 0); // ditto

const ANOTHER: Point = ORIGIN.add(ORIGIN2); // works because ORIGIN and ORIGIN2 are both const.
{
    let x: i32 = 42;
    let y: i32 = 24;
    const SOME_POINT: Point = Point::new(x, y); // Error: x and y are not known at compile time
}
{
    const x: i32 = 42;
    const y: i32 = 24;
    const SOME_POINT: Point = Point::new(x, y); // Works x and y are both known at compile time.
}

Tenga en cuenta que no soy realmente un usuario de Rust y solo he leído el RFC hace unos minutos, por lo que es posible que haya entendido mal algo.

@MaikKlein hubo mucha discusión sobre CTFE en la discusión de RFC

No veo ningún comentario reciente que explique los bloqueadores aquí, y la operación no es muy esclarecedora. ¿Cuál es el estado? ¿Cómo podemos mover esto a través de la línea de meta?

Esto es utilizado por Rocket: https://github.com/SergioBenitez/Rocket/issues/19#issuecomment -269052006

Consulte https://github.com/rust-lang/rust/issues/29646#issuecomment -271759986. También debemos reconsiderar nuestra posición sobre lo explícito, ya que miri empuja el límite de los "efectos secundarios globales" ( @solson y @nikomatsakis estaban hablando de esto en IRC).

El problema que veo con este RFC es que, como usuario, debe marcar tantas funciones const fn como sea posible porque esa será probablemente la mejor práctica.

Si bien podemos hacer que las funciones arbitrarias sean invocables, si esas funciones acceden al código C o estático, no podremos calcularlas. Como solución sugiero un lint que advertirá sobre funciones públicas que podrían ser const fn.

Estoy de acuerdo con la pelusa. Es similar a las pelusas integradas existentes missing_docs , missing_debug_implementations y missing_copy_implementations .

Sin embargo, hay una especie de problema con tener la pelusa activada de forma predeterminada ... advertiría sobre las funciones que explícitamente no desea que sean const , por ejemplo, porque planea cambiar más tarde la función de modo que no puede ser const y no desea comprometer su interfaz a const (eliminar const es un cambio importante).

Supongo que #[allow(missing_const)] fn foo() {} podría funcionar en esos casos.

@eddyb @nikomatsakis Mi punto "eliminar const es un cambio radical" sugiere que, después de todo, querremos tener la palabra clave, ya que es una promesa para los usuarios posteriores de que el fn _permanecerá_ const hasta la próxima versión principal.

Va a ser una pena la cantidad const que será necesario repartir entre std y otras bibliotecas, pero no veo cómo se puede evitar, a menos que solo se requiera en el público. elementos enfrentados, y eso parece una regla confusa.

a menos que solo se requiera en artículos de cara al público, y eso parece una regla confusa.

Me gusta este... No creo que sea confuso. Su interfaz pública está protegida ya que no puede hacer una función no constante que sea llamada por un const fn

Técnicamente, sería mejor anotar funciones como notconst , porque espero que haya mucho más const fn que al revés.

notconst también sería más consistente con la filosofía de diseño de Rust. (es decir, " mut , no const ")

a menos que solo se requiera en artículos de cara al público, y eso parece una regla confusa.

Me gusta este... No creo que sea confuso.

Estoy flip-flopping en esta idea. Tiene sus beneficios ( solo piense en const fn cuando tome decisiones de interfaz pública) pero pensé en otra forma en que podría ser confuso:

Su interfaz pública está protegida ya que no puede hacer que una función no constante sea llamada por un const fn

Esto es cierto, y desafortunadamente implicaría que cuando un autor de la biblioteca marca una función pública const , también está marcando implícitamente todas las funciones llamadas transitivamente por esa función const , y existe la posibilidad están marcando involuntariamente funciones que no quieren, lo que les impide volver a escribir esas funciones internas utilizando funciones no constantes en el futuro.


Espero que haya mucho más const fn que al revés.

Pensé de esta manera por un tiempo, pero solo será cierto para las cajas de biblioteca de Rust puro. No será posible hacer constantes fns basadas en FFI (incluso si solo están basadas en FFI transitivamente, lo cual es un montón de cosas), por lo que la gran cantidad de const fn puede no ser tan mala como tú y yo pensamos.


Mi conclusión actual: cualquier const fn no explícito parece problemático. Puede que simplemente no haya una buena manera de evitar escribir mucho la palabra clave.

Además, para que conste, notconst sería un cambio importante.

@solson Un muy buen punto.

Tenga en cuenta que la palabra clave se vuelve aún más peluda si intenta usarla con métodos de rasgos. Restringirlo a la definición del rasgo no es lo suficientemente útil y anotar impls da como resultado reglas imperfectas de "const fn parametrim".

Siento que esta compensación se discutió bastante a fondo cuando adoptamos const fn en primer lugar. Creo que el análisis de @solson también es correcto. Supongo que lo único que ha cambiado es que tal vez el porcentaje de policías fns ha crecido, pero no creo que lo suficiente como para cambiar la compensación fundamental aquí. Va a ser molesto tener que agregar gradualmente const fn a sus interfaces públicas, etc., pero así es la vida.

@nikomatsakis Lo que me preocupa es la combinación de estos dos hechos:

  • no podemos verificar todo con anticipación, el código unsafe puede ser "dinámicamente no constante"
  • hagamos lo que hagamos para genéricos e impls de rasgos, será un compromiso entre "correcto" y flexible

Dado que los "efectos secundarios globales" son lo principal que evita que el código sea const fn , ¿no es este el "sistema de efectos" que Rust solía tener y se eliminó?
¿No deberíamos hablar de "estabilidad del efecto"? Parece similar al código, suponiendo que alguna biblioteca nunca entre en pánico, en mi opinión.

@eddyb absolutamente const es un sistema de efectos y sí, viene con todas las desventajas que nos hicieron querer evitarlos tanto como sea posible... Es plausible que si vamos a soportar el dolor de agregar en un sistema de efectos, es posible que deseemos considerar alguna sintaxis que podamos escalar a otros tipos de efectos. Como ejemplo, estamos pagando un precio similar con unsafe (también un efecto), aunque no estoy seguro de que tenga sentido pensar en unificarlos.

Sin embargo, el hecho de que las infracciones pueden ocurrir dinámicamente parece ser una razón más para hacer esta opción, ¿no?

Qué tal esto:

En general, creo que const fn s solo deben usarse para constructores ( new ) o donde sea absolutamente necesario.

Sin embargo, a veces puede querer usar otros métodos para crear convenientemente una constante. Creo que podríamos resolver este problema en muchos casos haciendo que la constancia sea la predeterminada , pero solo para el módulo de definición . De esta manera, los dependientes no pueden asumir la constancia a menos que se garantice explícitamente con const , y al mismo tiempo tener la conveniencia de crear constantes con funciones sin hacer que todo sea constante .

@torkleyy Ya puede hacerlo teniendo ayudantes que no se exportan.

No veo un argumento sólido de que las funciones auxiliares privadas no deberían ser implícitamente const , cuando sea posible. Creo que @solson estaba diciendo que hacer que const sea explícito, incluso para las funciones auxiliares, obliga al programador a hacer una pausa y considerar si quiere comprometerse con que esa función sea const . Pero si los programadores ya están obligados a pensar en eso para las funciones públicas, ¿no es suficiente? ¿No valdría la pena no tener que escribir const todas partes?

En IRC , @eddyb propuso dividir esta puerta de características para que pudiéramos estabilizar las llamadas a const fns antes de descubrir los detalles de su declaración y cuerpos. ¿Suena como una buena idea?

@durka Eso me suena genial, como usuario de Rust que no sabe mucho sobre los componentes internos del compilador.

Disculpe mi falta de comprensión aquí, pero ¿qué significa estabilizar la llamada a una función const sin estabilizar la declaración?

¿Estamos diciendo que el compilador de alguna manera sabrá qué es y qué no es constante a través de algún medio, pero deja esa parte abierta para discusión/implementación por el momento?

Entonces, ¿cómo se pueden estabilizar las llamadas si el compilador puede cambiar de opinión más tarde sobre lo que es constante?

@nixpulvis Algunos const fn ya existen en la biblioteca estándar, por ejemplo, UnsafeCell::new . Esta propuesta permitiría llamar a tales funciones en contextos constantes, por ejemplo, el inicializador de un elemento static .

@nixpulvis Lo que quise decir fueron llamadas a funciones const fn definidas por código de uso inestable (como la biblioteca estándar), desde contextos constantes, no funciones regulares definidas en código Rust estable.

Si bien estoy totalmente a favor de estabilizar las llamadas a const fn primero si eso puede suceder más rápido, no tengo claro qué bloquea la estabilización de toda la función const fn . ¿Cuáles son las preocupaciones restantes hoy? ¿Cuál sería un camino para abordarlos?

@SimonSapin Es más que no tenemos claro el diseño para declarar const fn s hoy escala bien ni estamos seguros de las interacciones entre ellos y los rasgos y cuánta flexibilidad debería haber.

Creo que me inclino a estabilizar los usos de const fn. Esto parece una victoria ergonómica y expresiva y todavía no puedo imaginar una mejor manera de manejar la evaluación constante en tiempo de compilación que simplemente poder "escribir código normal".

estabilizar usos de const fn.

Esto también estabiliza algunas funciones en la biblioteca estándar como const , el equipo de la biblioteca debería hacer al menos una auditoría.

Envié un PR https://github.com/rust-lang/rust/issues/43017 para estabilizar las invocaciones, junto con una lista de funciones para ser auditadas por @petrochenkov.

Tengo una pregunta/comentario sobre cómo se podría usar esto en ciertas situaciones de rasgos/implicaciones. Hipotéticamente, digamos que tenemos una biblioteca matemática con un rasgo Zero :

pub trait Zero {
    fn zero () -> Self;
}

Este rasgo no requiere que el método zero sea const , ya que esto evitaría que sea implementado por algún tipo BigInt respaldado por un Vec . Pero para escalares de máquina y otros tipos simples, sería mucho más práctico si el método fuera const .

impl Zero for i32 {
    const fn zero () -> i32 { 0 } // const
}

impl Zero for BigInt {
    fn zero () -> BigInt { ... } // not const
}

El rasgo no requiere que el método sea const , pero aun así debe permitirse, ya que const agrega una restricción a la implementación y no la ignora. Esto evita tener una versión normal y una versión const de la misma función para algunos tipos. Lo que me pregunto es si esto ya se ha abordado.

¿Por qué querrías que diferentes implementaciones del rasgo se comportaran de manera diferente? No puedes usar eso en un contexto genérico. Simplemente puede hacer un impl local en el escalar con un const fn.

@Daggerbot Esa es la única forma que veo para const fn en rasgos: tener el rasgo que requiere que todos los impls sean const fn es mucho menos común que tener efectivamente " const impl s" .

@jethrogb Podría, aunque requiere que la constancia sea una propiedad del impl.
Lo que tengo en mente es que un const fn genérico con un límite, por ejemplo, T: Zero , requerirá el impl de Zero para el T s se llama para contener solo const fn métodos, cuando la llamada proviene de un contexto constante en sí mismo (por ejemplo, otro const fn ).

No es perfecto, pero no se ha presentado ninguna alternativa superior: en mi opinión, lo más cercano a eso sería "permitir cualquier llamada y error profundo de la pila de llamadas si se intenta algo que no es posible en tiempo de compilación", que no es tan malo como puede parecer a primera vista: la mayor parte de la preocupación tiene que ver con la compatibilidad con versiones anteriores, es decir, marcar una función const fn garantiza que el hecho se registre y realizar operaciones no válidas en tiempo de compilación requeriría que no const fn .

¿Esto no resolvería el problema?

pub trait Zero {
    fn zero() -> Self;
}

pub trait ConstZero: Zero {
    const fn zero() -> Self;
}

impl<T: ConstZero> Zero for T {
    fn zero() -> Self {
        <Self as ConstZero>::zero()
    }
}

El repetitivo podría reducirse con macros.

Aparte del inconveniente menor de tener dos rasgos separados ( Zero y ConstZero ) que hacen casi exactamente lo mismo, veo un problema potencial cuando se usa una implementación general:

// Blanket impl
impl<T: ConstZero> Zero for T {
    fn zero () -> Self { T::const_zero() }
}

pub struct Vector2<T> {
    pub x: T,
    pub y: T,
}

impl<T: ConstZero> ConstZero for Vector2<T> {
    const fn const_zero () -> Vector2<T> {
        Vector2 { x: T::const_zero(), y: T::const_zero() }
    }
}

// Error: This now conflicts with the blanket impl above because Vector2<T> implements ConstZero and therefore Zero.
impl<T: Zero> Zero for Vector2<T> {
    fn zero () -> Vector2<T> {
        Vector2 { x: T::zero(), y: T::zero() }
    }
}

El error desaparecería si quitáramos el impl de manta. Con todo, este es probablemente el más fácil de implementar en un compilador, ya que agrega la menor complejidad al lenguaje.

Pero si pudiéramos agregar const a un método implementado donde no se requiere, podemos evitar esta duplicación, aunque todavía no perfectamente:

impl<T: Zero> Zero for Vector2<T> {
    const fn zero () -> Vector2<T> {
        Vector2 { x: T::zero(), y: T::zero() }
    }
}

IIRC, C++ permite algo como esto cuando se trabaja con constexpr . La desventaja aquí es que este const solo sería aplicable si <T as Zero>::zero también es const . ¿Debería ser un error o el compilador debería ignorar este const cuando no es aplicable (como C++)?

Ninguno de estos ejemplos aborda el problema a la perfección, pero realmente no puedo pensar en una mejor manera.

Editar: la sugerencia de @andersk haría posible el primer ejemplo sin errores. Esta sería probablemente la solución mejor/más simple en lo que respecta a la implementación del compilador.

@Daggerbot Esto suena como un caso de uso para la regla de "celosía" propuesta cerca del final de RFC 1210 (especialización) . Si tú escribes

impl<T: ConstZero> Zero for T {…}  // 1
impl<T: ConstZero> ConstZero for Vector2<T> {…}  // 2
impl<T: Zero> Zero for Vector2<T> {…}  // 3
impl<T: ConstZero> Zero for Vector2<T> {…}  // 4

entonces, aunque 1 se superpone con 3, su intersección está cubierta precisamente por 4, por lo que estaría permitido bajo la regla de la red.

Consulte también http://smallcultfollowing.com/babysteps/blog/2016/09/24/intersection-impls/.

Ese es un sistema increíblemente complejo, que queremos evitar.

Sí, se necesitaría la regla de celosía.

@eddyb , ¿qué consideras complejo?

@Kixunil Duplicar casi todos los rasgos de la biblioteca estándar, en lugar de "simplemente" marcar algunos impl como const fn .

Aquí nos estamos desviando. Actualmente, el problema se trata de estabilizar los usos de const fn . Permitir que los métodos de rasgo const fn o const impl Trait for Foo sean ortogonales entre sí y con los RFC aceptados.

@oli-obk Este no es el nuevo RFC, sino el problema de seguimiento de const fn .

Acabo de darme cuenta y edité mi comentario.

@eddyb sí, pero es más simple para el compilador (menos la especialización, pero probablemente querremos la especialización de todos modos) y permite que las personas se vinculen por ConstTrait también.

De todos modos, no me opongo a marcar impls como const. También me imagino que el compilador genera automáticamente ConstTrait: Trait .

@Kixunil No es mucho más simple, especialmente si puedes hacerlo con especialización.
El compilador no tendría que generar automáticamente nada como ConstTrait: Trait , ni el sistema de rasgos necesita saber nada de esto, solo se necesita recurrir a las implementaciones (ya sea un impl concreto o un límite where ) que proporciona el sistema de rasgos y verifíquelos.

Me pregunto si const fns debería prohibir el acceso a UnsafeCell . Probablemente sea necesario permitir un comportamiento verdaderamente constante:

const fn dont_change_anything(&self) -> bool {
    let old = self.cell.get();
    self.cell.set(!old);
    old
}

Hasta ahora he visto que set no es const . La pregunta es si esto se quedará para siempre. En otras palabras: ¿Puede el código unsafe confiar en que cuando se ejecuta la misma función const en datos inmutables siempre devolverá el mismo resultado hoy y en cada versión futura del lenguaje/biblioteca?

const fn no significa inmutable, significa que se puede llamar en tiempo de compilación.

Veo. Apreciaría mucho si pudiera garantizar de alguna manera que una función siempre devuelve lo mismo cuando se la llama varias veces sin usar los rasgos unsafe , si es posible de alguna manera.

@jethrogb ¡ Gracias por el enlace!

Noté que mem::size_of se implementa como const fn todas las noches. ¿Sería esto posible para mem::transmute y otros? Los intrínsecos de Rust funcionan internamente en el compilador, y no estoy lo suficientemente familiarizado como para realizar los cambios adecuados para permitir esto. De lo contrario, estaría feliz de implementarlo.

Desafortunadamente, operar con valores es un poco más difícil que simplemente crearlos mágicamente. transmute requiere miri. Ya está en marcha un primer paso para incluir a miri en el compilador: #43628

¡Entonces! ¿Algún interés en la estabilización constante de *Cell::new , mem::{size,align}_of , ptr::null{,_mut} , Atomic*::new , Once::new y {integer}::{min,max}_value ? ¿Tendremos FCP aquí o crearemos problemas de seguimiento individuales?

Si.

No soy parte de ningún equipo que tenga poder de decisión sobre esto, pero mi opinión personal es que todos estos, excepto mem::{size,align}_of son lo suficientemente triviales como para que puedan estabilizarse ahora sin pasar por los movimientos de un sello de goma. FCP.

Como usuario, me gustaría usar mem::{size,align}_of en expresiones constantes lo antes posible, pero he leído que @nikomatsakis expresa su preocupación acerca de que sean insta-const-estables cuando se crearon const fn s . No sé si hay preocupaciones específicas o solo una precaución general, pero IIRC es por eso que se agregaron puertas de características por función. Me imagino que las preocupaciones de estos dos serían lo suficientemente similares como para compartir un FCP. No sé si @rustbot puede manejar FCP separados en el mismo subproceso de GitHub, por lo que probablemente sea mejor abrir problemas separados.

@durka , ¿puede abrir un solo problema de seguimiento para estabilizar la constancia de todas esas funciones? Propondré FCP una vez que esté activo.

Para seguir una pista en una discusión sobre const fns en alloc::Layout :
¿Se puede permitir pánico en un const fn y tratarlo como un error de compilación? Esto es similar a lo que se hace ahora con expresiones aritméticas constantes, ¿no?

Sí, esa es una característica súper trivial una vez que miri se fusiona

¿Es este el lugar adecuado para solicitar que las funciones std adicionales se conviertan en const ? Si es así, Duration:: { new , from_secs , from_millis } debería ser seguro generar const .

@remexre La forma más fácil de lograrlo es probablemente hacer una RP y solicitar una revisión del equipo de libs allí.

Publicada como https://github.com/rust-lang/rust/pull/47300. También agregué const a los constructores inestables mientras estaba en eso.

¿Alguna idea sobre permitir que se declaren más funciones std const ? Específicamente, mem::uninitialized y mem::zeroed ? Creo que ambos son candidatos adecuados para funciones adicionales de const . El único inconveniente que se me ocurre es el mismo inconveniente de mem::uninitialized , donde la creación de estructuras que implementan Drop se crean y se escriben sin ptr::write .

También puedo adjuntar un PR si esto suena adecuado.

¿Cuál es la motivación para eso? Parece una pistola inútil para permitir crear patrones de bits no válidos que luego no se pueden sobrescribir (porque están en constante), pero tal vez estoy pasando por alto lo obvio.

mem::uninitialized es absolutamente una pistola, una que también te dispara a través de las manos si se apunta incorrectamente. En serio, no puedo exagerar lo increíblemente peligroso que puede ser el uso de esta función, a pesar de que está marcada como unsafe .

La motivación detrás de declarar estas funciones adicionales const se deriva de la naturaleza de estas funciones, ya que llamar a mem::uninitialized<Vec<u32>> devolverá el mismo resultado cada vez, sin efectos secundarios. Obviamente, si se deja sin inicializar, esto es algo terrible. Por lo tanto, el unsafe todavía está presente.

Pero para un caso de uso, considere un temporizador global, uno que rastrea el inicio de alguna función. Su estado interno se determinará más adelante, pero necesitamos una forma de presentarlo como una estructura global estática creada en la ejecución.

use std::time::Instant;

pub struct GlobalTimer {
    time: UnsafeCell<Instant>
}

impl TimeManager {
    pub const fn init() -> TimeManager {
        TimeManager {
            time: UnsafeCell::new(Instant::now())
        }
    }
}

Este código no compila, debido a que Instant::now() no es una función const . Reemplazar Instant::now() con mem::uninitialized::<Instant>()) solucionaría este problema si mem::uninitialized fuera un const fn . Idealmente, el desarrollador inicializará esta estructura una vez que el programa comience a ejecutarse. Y aunque este código se considera oxidado no idiomático (el estado global es generalmente muy malo), este es solo uno de los muchos casos en los que las estructuras estáticas globales son útiles.

Creo que esta publicación brinda una buena base para el futuro del código Rust que se ejecuta en tiempo de compilación. Las estructuras estáticas globales en tiempo de compilación son una característica con algunos casos de uso importantes (los sistemas operativos también vienen a la mente) que actualmente faltan en el óxido. Se pueden dar pequeños pasos hacia este objetivo al agregar lentamente const a las funciones de biblioteca que se consideren adecuadas, como mem::uninitialized y mem::zeroed , a pesar de sus marcas unsafe .

Editar: olvidé el const en la firma de función de TimeManager::init()

Hmm, ese código se compila, así que todavía me falta la motivación exacta aquí... si pudieras escribir código como

const fn foo() -> Whatever {
    unsafe { 
        let mut it = mem::uninitialized();
        init_whatever(&mut it);
        it
    }
}

Pero las const fns actualmente están tan restringidas que ni siquiera puedes escribir eso...

Agradezco la justificación teórica, pero const no es lo mismo que pure y no creo que debamos hacer nada para fomentar el uso de estas funciones si no es necesario para algún caso de uso convincente. .

Creo que hay frutos mucho más bajos que podrían estabilizarse primero. Sin miri, los intrínsecos no inicializados y puestos a cero tienen poco sentido de todos modos. Aunque me gustaría verlos algún día. Incluso podríamos estabilizarlos inicialmente y exigir que todas las constantes produzcan un resultado inicializado, incluso si los cálculos intermedios pueden no inicializarse.

Dicho esto, con uniones y código no seguro puede emular de todos modos sin inicializar o poner a cero, por lo que no tiene mucho sentido mantenerlos no constantes.

Con la ayuda de union s, el código anterior ahora compila . Es absolutamente aterrador 😅.

Todos los puntos buenos también. Estas funciones intrínsecas ocupan un lugar bastante bajo en la lista de casos de uso, pero siguen siendo candidatos adecuados para el eventual const -ness.

Eso es terriblemente asombroso.

Entonces... ¿por qué exactamente estás abogando por constituir mem::uninitialized, como
opuesto a, digamos, Instant::now? :)

La necesidad de tener inicializadores constantes para estructuras con no constante
interiores es real (ver: Mutex). Pero no creo que hacer esta tontería
más fácil es la forma correcta de conseguir eso!

El jueves 25 de enero de 2018 a las 2:21 a. m., Stephen Fleischman <
[email protected]> escribió:

Con la ayuda de uniones, el código anterior ahora compila
https://play.rust-lang.org/?gist=be075cf12f63dee3b2e2b65a12a3c854&version=nightly .
Es absolutamente aterrador 😅.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment-360382201 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAC3n-HyWD6MUEbfHkUUXonh9ORGPSRoks5tOCtegaJpZM4D66IA
.

Instant::now no se puede const. ¿Qué devolvería esa función? ¿El momento de la compilación?

¿Alguien puede resumir lo que hay que hacer para estabilizar esto? ¿A qué decisión hay que llegar? Ya sea para estabilizar esto en absoluto?

Integración con patrones (por ejemplo, https://gist.github.com/d0ff1de8b6fc15ef1bb6)

Ya comenté sobre la esencia, pero dado que const fn actualmente no se pueden comparar con un patrón, esto no debería bloquear la estabilización, ¿verdad? Siempre podríamos permitirlo después si tiene sentido.

Instant::now no puede ser constante. ¿Qué devolvería esa función? ¿El momento de la compilación?

Pero podría haber Instant::zero() o Instant::min_value() que es const.

¿Alguien puede resumir lo que hay que hacer para estabilizar esto? ¿A qué decisión hay que llegar? Ya sea para estabilizar esto en absoluto?

Creo que la única pregunta abierta es si nuestras comprobaciones de const fn son lo suficientemente estrictas como para no permitir/estabilizar accidentalmente algo que no queremos dentro de const fn.

¿Podemos hacer la integración con patrones a través de rust-lang/rfcs#2272? Los patrones ya son dolorosos como lo son actualmente, no los hagamos más dolorosos.

Creo que la única pregunta abierta es si nuestras comprobaciones de const fn son lo suficientemente estrictas como para no permitir/estabilizar accidentalmente algo que no queremos dentro de const fn.

Corrígeme si me equivoco, pero ¿no son esos controles idénticos a los controles que se permiten actualmente en el cuerpo de un valor de const ? Tenía la impresión de que const FOO: Type = { body }; y const FOO: Type = foo(); const fn foo() -> Type { body } son idénticos en lo que permiten para cualquier cuerpo arbitrario

@sgrif Creo que la preocupación está relacionada con los argumentos, que const fn tienen pero const no.
Además, no está claro que a largo plazo queramos mantener el sistema de const fn de hoy.

¿Está sugiriendo genéricos const (en ambos sentidos) en su lugar? (por ejemplo <const T> + const C<T> también conocido como const C<const T> ?)

Realmente me gustaría tener una macro try_const! que intente evaluar cualquier expresión en tiempo de compilación y entrar en pánico si no es posible. Esta macro podrá llamar a fns no constantes (¿usando miri?), por lo que no tenemos que esperar hasta que todas las funciones en std hayan sido marcadas const fn. Sin embargo, como su nombre lo indica, puede fallar en cualquier momento, por lo que si una función se actualiza y ahora no puede ser constante, dejará de compilarse.

@Badel2 Entiendo por qué querrías una función de este tipo, pero sospecho que su uso generalizado sería realmente malo para el ecosistema de cajas. Porque de esta manera, su caja podría terminar dependiendo de que una función en otra caja sea evaluable en tiempo de compilación, y luego el autor de la caja cambia algo que no afecta la firma de la función pero evita que la función sea evaluable en tiempo de compilación.

Si la función se marcó const fn en primer lugar, entonces el autor de la caja habría detectado el problema directamente al intentar compilar la caja y puede confiar en la anotación.

Si tan solo esto funcionara en el patio de recreo... https://play.rust-lang.org/?gist=6c0a46ee8299e36202f959908e8189e6&version=stable

Esta es una forma no portátil (de hecho, tan no portátil que funciona en mi sistema pero no en el patio de recreo, pero ambos son Linux) de incluir el tiempo de compilación en el programa construido.

La forma portátil sería permitir SystemTime::now() en la evaluación constante.

(Este es un argumento para const/compile-time-eval de CUALQUIER función/expresión, sin importar si es const fn o no).

Eso me suena como un argumento para prohibir rutas absolutas en include_bytes 😃

Si permite SystemTime::now en const fn, const FOO: [u8; SystemTime::now()] = [42; SystemTime::now()]; generará un error aleatorio según el rendimiento de su sistema, el programador y la posición de Júpiter.

Peor aún:

const TIME: SystemTime = SystemTime::now();

No significa que el valor de TIME sea el mismo en todos los sitios de uso, especialmente en compilaciones con cajas incrementales y entre cajas.

Y aún más loco es que puedes arruinar foo.clone() de maneras muy poco sólidas, porque podrías terminar seleccionando el impl clonado de una matriz con longitud 3 pero el tipo de retorno podría ser una matriz de longitud 4.

Entonces, incluso si permitiéramos que se llamaran funciones arbitrarias, nunca permitiríamos que SystemTime::new() regresara con éxito, al igual que nunca permitiríamos verdaderos generadores de números aleatorios

@ SoniEx2 Supongo que esto está un poco fuera de tema aquí, pero ya puede implementar algo así hoy usando un archivo cargo build.rs . Ver Build Scripts en el Cargo Book, específicamente la sección sobre el estudio de caso de generación de código.

@oli-obk Creo que no es completamente el mismo problema porque uno se trata de la seguridad de la API de versiones mientras que el otro se trata del entorno de construcción, sin embargo, estoy de acuerdo en que ambos pueden provocar la ruptura del ecosistema si no se aplican con cuidado.

No permita obtener la hora actual en un const fn ; no necesitamos agregar más/formas más fáciles de hacer que las compilaciones no sean reproducibles.

No podemos permitir ningún tipo de no determinismo (como números aleatorios, la hora actual, etc.) en const fn , lo que conduce a la falta de solidez del sistema de tipos, ya que rustc supone que las expresiones constantes siempre se evalúan con el mismo resultado dado. la misma entrada. Ver aquí para un poco más de explicación.

Un método futuro para manejar casos como en https://github.com/rust-lang/rust/issues/24111#issuecomment -376352844 sería usar una macro de procedimiento simple que obtenga la hora actual y la emita como un número simple o token de cadena. Las macros de procedimiento son más o menos un código completamente sin restricciones que puede obtener el tiempo mediante cualquiera de las formas portátiles habituales que usaría el código Rust no constante.

fusión @rfcbot fcp

Propongo que combinemos esto, porque es una opción algo sensata, no es un cambio de ruptura, evita cambios de ruptura accidentales (cambiar una función de una manera que la hace no evaluable de manera constante mientras que otras cajas usan la función en contextos constantes) y el único Lo realmente malo es que tenemos que lanzar const antes de un montón de declaraciones de funciones.

@rfcbot fcp merge en nombre de @oli-obk: parece que vale la pena pensar en la estabilización y discutir los problemas

El miembro del equipo @nrc ha propuesto fusionar esto. El siguiente paso es la revisión por parte del resto de equipos etiquetados:

  • [x] @aturon
  • [x] @cramertj
  • [ ] @eddyb
  • [x] @joshtriplett
  • [ ] @nikomatsakis
  • [x] @nrc
  • [ ] @pnkfelix
  • [x] @scottmcm
  • [x] @sinbarcos

Preocupaciones:

  • diseño (https://github.com/rust-lang/rust/issues/24111#issuecomment-376829588)
  • Parallel -const-traits resueltos por https://github.com/rust-lang/rust/issues/24111#issuecomment -377133537
  • prioridad (https://github.com/rust-lang/rust/issues/24111#issuecomment-376652507)
  • direcciones de puntero de tiempo de ejecución (https://github.com/rust-lang/rust/issues/24111#issuecomment-386745312)

Una vez que la mayoría de los revisores lo aprueben (y ninguno se oponga), entrará en su período final de comentarios. Si detecta un problema importante que no se ha planteado en ningún momento de este proceso, ¡dígalo!

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

Prioridad de preocupación de @rfcbot

Es posible que queramos dejar esto hasta después de la edición, ya que no creo que tengamos el ancho de banda para lidiar con las consecuencias.

@rfcbot se preocupa por todo-const

Terminamos en un mundo un poco C++ donde hay un incentivo para hacer cada función que puedas const .

Un resumen de una breve discusión con @oli-obk:

  1. En el futuro, casi todas las funciones podrían marcarse como const . Por ejemplo, todo en Vec podría ser const . En ese mundo, podría tener sentido deshacerse de la palabra clave const por completo: casi todo puede ser const , y uno tendrá que esforzarse para cambiar una función de const a non -const, por lo que los riesgos de compatibilidad con versiones anteriores sobre la constness inferida probablemente no serían terriblemente altos.

  2. Sin embargo, deshacerse de const hoy no es factible. El miri de hoy no puede interpretarlo todo , y en realidad no se ha probado a fondo en producción.

  3. En realidad, es compatible con versiones anteriores para requerir const hoy, y luego desaprobar esta palabra clave y cambiar a la constancia inferida en el futuro.

Poniendo 1, 2 y 3 juntos, parece una buena opción para estabilizar la palabra clave const hoy, que expandir el conjunto de funciones evaluables constantemente en versiones futuras. Después de un tiempo, tendremos un evaluador constante de tejón de miel completamente probado en batalla, que puede evaluar todo. En ese momento, podemos cambiar a constante inferida.

Indique los peligros de las consecuencias: const fn se ha usado mucho en las noches, especialmente en incrustados. Además, el verificador const fn es el mismo verificador que se usa para inicializadores y constantes estáticas (excepto para algunas verificaciones específicas estáticas y argumentos de función).

La principal desventaja que veo es que esencialmente abogamos por rociar const generosamente en las cajas (por ahora, vea la publicación de @matklad para futuras ideas

@rfcbot preocupación paralelo-const-traits

Parece que estabilizar esto dará como resultado inmediatamente un montón de cajas haciendo una jerarquía de características paralela con Const al frente: ConstDefault , ConstFrom , ConstInto , ConstClone , ConstTryFrom , ConstTryInto , etc y pidiendo ConstIndex y tal. Eso no es terrible, ciertamente lo tenemos un poco con Try hoy, aunque estabilizar TryFrom ayudará, pero creo que sería bueno tener al menos un esbozo del plan para resolverlo mejor. (¿Eso es https://github.com/rust-lang/rfcs/pull/2237? No lo sé).

( @nrc : Parece que el bot solo registró una de sus inquietudes)

Parallel-const-traits tiene la solución trivial en la hipotética futura versión const-all-the-things. Simplemente trabajarían.

En el mundo de const fn, no terminaría con la duplicación de rasgos, siempre que no permitamos métodos de rasgos const fn (que no atm), solo porque no puede. Por supuesto, podría crear constantes asociadas (por la noche), que es una especie de situación en la que se encontraba libstd hace un año, donde teníamos un montón de constantes para inicializar varios tipos dentro de estáticas/constantes, sin exponer sus campos privados. Pero eso es algo que ya podría haber estado sucediendo durante un tiempo y no sucedió.

Para ser claros, ConstDefault ya es posible hoy sin const fn , y el resto de esos ejemplos ( ConstFrom , ConstInto , ConstClone , ConstTryFrom , ConstTryInto ) no será posible incluso con esta función estabilizada, ya que no agrega métodos de rasgos constantes como mencionó @oli-obk.

( ConstDefault es posible usando una const asociada en lugar de una const fn asociada, pero es equivalente en poder hasta donde yo sé).

@scottmcm const fn en definiciones de rasgos no es posible hoy (oh , @solson ya lo mencionó).

Idea aleatoria de @eddyb : ¿qué pasaría si hiciéramos posible const impl un rasgo en lugar de agregar const fn en las definiciones de rasgo? (Estos dos tampoco son mutuamente excluyentes).

@whitequark https://github.com/rust-lang/rfcs/pull/2237 cubre esa idea, a través de una combinación de const impl expandiéndose a const fn en cada fn en el impl , y permitir un impl con todos los métodos const para satisfacer un límite T: const Trait , sin marcar ninguno de los métodos const en la propia definición del rasgo.

@rfcbot diseño de preocupación

Históricamente, hemos apostado por estabilizar cualquier sistema const fn específico por varias razones:

  • el actual no es compatible trait s que requieren métodos const fn , ni con los rasgos impl s que proporcionan métodos const fn (consulte https://github. com/rust-lang/rfcs/pull/2237 para conocer algunas formas de hacerlo)
  • relacionado, existe el problema de solicitar un método usado a través de un T: Trait destinado a ser const fn sin tener características separadas, y preferiblemente solo cuando se usa en tiempo de compilación (por ejemplo Option::map funcionaría igual en tiempo de ejecución pero requeriría un cierre invocable constante en CTFE)
  • con una gran cantidad de algoritmos, colecciones y abstracciones potencialmente evaluables de manera constante, podría haber cajas enteras que usarían const fn todas partes (me viene a la mente libcore )

Hay diferentes opciones de diseño que aliviarían la mayoría o todos estos problemas (a costa de introducir otros), por ejemplo, estos son algunos de los que han surgido:

  • no requiere ninguna anotación y solo emite errores de compilación y miri no puede evaluar

    • pros: bases de código más limpias, la evaluación constante ya puede fallar dependiendo de los valores involucrados

    • contras: no hay documentación de comportamiento a nivel de idioma y límite de semver, puede arrojar el código de cualquier otra persona a miri y observar cualquier cambio menor que hayan realizado, por ejemplo, el registro de depuración en tiempo de ejecución se agrega en una versión de parche de una de sus dependencias; además, la promoción de rvalue es más difícil de hacer

  • una forma de optar por el comportamiento anterior por función/impl/módulo/etc.

    • los cuerpos de estas funciones (al menos en términos de const fn ) se comportarían como macros

    • pros: no tener las implicaciones de semver, limitando el alcance de algunos análisis

    • contras: todavía no hay una gran documentación, no está claro qué comportamiento debería verse afectado (¿ solo la capacidad de evaluación constante?), cualquier cambio en el cuerpo podría contar como una interrupción del servicio

Porque de esta manera, su caja podría terminar dependiendo de que una función en otra caja sea evaluable en tiempo de compilación, y luego el autor de la caja cambia algo que no afecta la firma de la función pero evita que la función sea evaluable en tiempo de compilación.

@leoschwarz ¿no es esto ya un problema con los rasgos automáticos? Tal vez la solución a esto sea integrar rust-semverver con la carga para detectar este tipo de rotura no intencionada.

Dicho esto, no me queda claro qué sucede si miri tiene un límite de tiempo de evaluación que usted (como autor de la biblioteca) excede accidentalmente, lo que provoca una falla de compilación posterior.

@nrc Creo que "everything-const" es cierto, pero no es un problema. Sí, terminaremos marcando una gran cantidad de cosas const .

Solo quiero señalar que no estoy seguro de querer que todo lo que se infiera sea
constante Es una decisión sobre si el tiempo de ejecución o el tiempo de compilación es más
importante. A veces pienso que el compilador hace suficientes cálculos en
¡tiempo de compilación!

El miércoles 28 de marzo de 2018 a las 14:49, Josh Triplett [email protected]
escribió:

@nrc https://github.com/nrc Creo que "everything-const" es cierto, pero no
un problema. Sí, terminaremos marcando una gran franja de cosas constantes.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment-376914220 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAC3ny9Wm9JK6p-fXf6gbaEgjFtBpMctks5ti6LigaJpZM4D66IA
.

límite de tiempo de evaluación

ese límite se ha ido pronto.

Solo quiero señalar que no estoy seguro de querer que todo lo inferido sea constante.

Oh no, no vamos a calcular cosas aleatoriamente en tiempo de compilación. Simplemente permita que se calculen cosas aleatorias en el cuerpo de estáticas, constantes, discriminantes de variantes de enumeración y longitudes de matriz

@rfcbot resolvió rasgos de const paralelos

¡Gracias por las correcciones, amigos!

ese límite se ha ido pronto.

Increíble. En ese caso, auto-const-fn (en combinación con alguna integración de rust-semverver o similar para brindar información sobre roturas) suena increíble, aunque "agregar registro y causar roturas" podría ser problemático. Aunque puede aumentar el número de versión, supongo, no es que sean finitos.

El registro y la impresión son efectos secundarios "finos" en mi modelo de constantes. Podríamos encontrar una solución para eso si todos están de acuerdo. Incluso podríamos escribir en archivos (no realmente, pero actuar como si lo hiciéramos y tirarlo todo).

Estoy realmente preocupado por desechar silenciosamente los efectos secundarios.

Podemos discutir eso una vez que creamos un RFC alrededor de ellos. Por ahora, simplemente no puede tener "efectos secundarios" en las constantes. El tema es ortogonal a estabilizar const fn

Estoy un poco preocupado por el enfoque de "solo haz una advertencia semver" para inferir
constancia Si un autor de cajas que nunca pensó en la constancia ve
"advertencia: el cambio que acaba de hacer hace que sea imposible llamar a foo() en
contexto const, que antes era posible", ¿simplemente lo verán como un
non-sequitur y silenciarlo? Claramente, las personas en este tema con frecuencia piensan
sobre qué funciones pueden ser const. Y sería bueno si más personas lo hicieran.
eso (una vez que const_fn es estable). Pero son las advertencias inesperadas lo correcto
manera de alentar eso?

El jueves 29 de marzo de 2018 a las 4:36 a. m., Oliver Schneider [email protected]
escribió:

Podemos discutir eso una vez que creamos un RFC alrededor de ellos. Por ahora solo
no puede tener "efectos secundarios" en las constantes. El tema es ortogonal a
constante estabilizadora fn


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment-377164275 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAC3n3MtmvrDF42Iy0nhZ2q8xC-QGcvXks5tjJ0ggaJpZM4D66IA
.

Creo que const fn explícito puede ser molesto y desordenar muchas API; sin embargo, creo que la alternativa de asumir implícitamente tiene demasiados problemas para ser practicable:

  • Descartar los efectos secundarios puede hacer que el código se comporte de manera diferente (por ejemplo, escribir y luego leer un archivo) si se llama const o non const.
  • Llamar a funciones externas significa que siempre puede haber efectos secundarios, por lo que el código inseguro probablemente nunca podría inferirse const fn.
  • ¿Cuándo se puede inferir const fn para el código genérico? ¿Se haría esto en el momento de la monomorfización?

Sin embargo, realmente veo que el mayor problema de no hacerlo explícito es que alguien puede romper accidentalmente muchos códigos con un solo cambio sin siquiera darse cuenta. Esto es especialmente preocupante con los gráficos de dependencia largos comunes en el ecosistema de Rust. Si requiere un cambio explícito en la firma de la función, uno se dará cuenta más fácilmente de que se trata de un cambio importante.

Tal vez tal característica podría implementarse como un indicador de configuración de nivel de caja que se puede agregar en la raíz de la caja, #![infer_const_fn] o algo así, y permanecer habilitado para siempre. Si la bandera se agrega const fn se inferiría donde sea posible en el cajón y también se reflejaría en los documentos (y requeriría que las funciones llamadas también sean const fn), si un autor de cajón agrega esta bandera, se compromete a ser cauteloso sobre versiones y tal vez rust-semverver podría incluso ser forzado.

¿Qué hay de hacerlo al revés?

En lugar de tener fn constante, tenga fn lateral.

Todavía es explícito (debe poner side fn para llamar a side fn, rompiendo explícitamente la compatibilidad) y elimina el desorden. (Algunos) intrínsecos y cualquier cosa con asm sería un lado fn.

Eso no es compatible con versiones anteriores, aunque supongo que se puede agregar en una edición.

El 30 de marzo de 2018 2:43:06 a. m. GMT+08:00, "Soni L." [email protected] escribió:

¿Qué hay de hacerlo al revés?

En lugar de tener fn constante, tenga fn lateral.

Todavía es explícito (debe poner side fn para llamar side fn,
rompiendo explícitamente la compatibilidad) y elimina el desorden. (Algunos)
intrínsecos y cualquier cosa con asm sería un lado fn.

--
Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente o véalo en GitHub:
https://github.com/rust-lang/rust/issues/24111#issuecomment-377333542

--
Enviado desde mi dispositivo Android con K-9 Mail. Por favor, disculpe mi brevedad.

Creo que el mayor problema es que sería un verdadero shock para los principiantes, ya que eso no es lo que hacen la mayoría de los lenguajes de programación.

@whitequark No estoy de acuerdo con nada que simplemente haga eso ("desechar los efectos secundarios"), creo que @oli-obk estaba hablando de una futura extensión, pero por las discusiones en las que participé, sé lo siguiente

  • podemos distinguir "efectos secundarios deterministas" (que no le devuelven datos impuros)
  • podríamos tener API especiales si quisiéramos, por ejemplo, "registrar datos en tiempo de compilación"

    • esto es bastante difícil según el nivel de determinismo externo que desee, porque la compilación incremental a pedido no necesariamente dará como resultado un resultado consistente

  • específicamente, no tocaríamos nada que exista actualmente y use globals / C FFI

EDITAR : solo para que la discusión no se descarrile, por ejemplo:

Descartar los efectos secundarios puede hacer que el código se comporte de manera diferente (por ejemplo, escribir y luego leer un archivo) si se llama const o non const.

Todos podemos (¿probablemente?) asumir que @oli-obk se equivocó al descartar efectos secundarios como ese.

Tal vez tal característica podría implementarse como un indicador de configuración de nivel de caja que se puede agregar en la raíz de la caja.

Ese es un subconjunto del segundo ejemplo de sugerencias anteriores, de https://github.com/rust-lang/rust/issues/24111#issuecomment -376829588.
Si tenemos un "indicador de configuración" con alcance, el usuario debería poder elegir alcances más detallados en mi opinión.

¿Qué hay de hacerlo al revés?
En lugar de tener fn constante, tenga fn lateral.
Todavía es explícito (debe poner side fn para llamar a side fn, rompiendo explícitamente la compatibilidad) y elimina el desorden. (Algunos) intrínsecos y cualquier cosa con asm sería un lado fn.

En https://github.com/rust-lang/rust/issues/24111#issuecomment -376829588 Traté de señalar que las bibliotecas enteras podrían ser "todas const fn " o "todas side fn ".
Si no estuviera en las declaraciones de funciones, sino en el alcance, tal vez podría funcionar en una edición futura.
Sin embargo, sin la semántica de "inferir del cuerpo", tiene que diseñar las interacciones de características incluso para la opción side fn , por lo que no está ganando nada y está introduciendo una fricción potencialmente masiva.

La sección 3.3 del artículo de Kenton Varda "Los solteros considerados dañinos" parece relevante aquí (honestamente, vale la pena leer todo el asunto).

¿Qué pasa con el registro de depuración?

En la práctica, todos reconocen que el registro de depuración debería estar disponible para cada pieza de código. Hacemos una excepción por ello. La base teórica exacta para esta excepción, para aquellos que se preocupan, se puede proporcionar de varias maneras.

Desde el punto de vista de la seguridad, el registro de depuración es un singleton benigno. No se puede utilizar como canal de comunicación porque es de solo escritura. Y es claramente imposible causar algún tipo de daño escribiendo en un registro de depuración, ya que el registro de depuración no es un factor en la corrección del programa. Incluso si un módulo malicioso "envía spam" al registro, los mensajes de ese módulo se pueden filtrar fácilmente, ya que los registros de depuración normalmente identifican exactamente qué módulo produjo cada mensaje (a veces incluso proporcionan un seguimiento de la pila). Por lo tanto, no hay problema con proporcionarlo.

Se pueden hacer argumentos análogos para mostrar que el registro de depuración no daña la legibilidad, la capacidad de prueba o la capacidad de mantenimiento.

Otra justificación teórica para el registro de depuración dice que la función de registro de depuración es en realidad una no operación que el depurador observa. Cuando no se está ejecutando ningún depurador, la función no hace nada. La depuración en general obviamente rompe todo el modelo de capacidad de objetos, pero también es obviamente una operación privilegiada.

Mi declaración sobre "podemos encontrar una solución [para la depuración]" se refería a una posible API futura, que se puede llamar desde consts, pero tiene alguna forma de impresión. La implementación aleatoria de operaciones de impresión específicas de la plataforma (solo para que podamos hacer que el código existente con declaraciones de impresión/depuración sea constante) no es algo que deba hacer un evaluador constante. Esto sería simplemente participar, explícitamente sin tener un comportamiento observable diferente (por ejemplo, advertencias en const eval y línea de comando/salida de archivo en tiempo de ejecución). La semántica exacta se deja para futuros RFC y debe considerarse completamente ortogonal a const fn en general

¿Existen desventajas significativas para const impl Trait y T: const Trait ?

Además de rociar más const , solo algunos métodos de rasgos pueden ser constantes, lo que requeriría un control más detallado. Sin embargo, no conozco una sintaxis ordenada para especificar eso. Quizás where <T as Trait>::some_method is const ( is podría ser una palabra clave contextual).

Nuevamente, eso es ortogonal a const fn.

[u8; SizeOf<T>::Output]

Si const y side fns están separados, debemos tener en cuenta las consideraciones de diseño reales. La forma más fácil de separarlos es hacer que const fns sea una extensión de algo que tenemos hoy: el sistema de tipo turing-completo.

Alternativamente, haga que const fns sea realmente const: cualquier cosa const fn debe evaluarse como si cada parámetro fuera un const genérico.

Esto hace mucho más fácil razonar sobre ellos, porque personalmente no puedo razonar sobre las const fns tal como están actualmente. Puedo razonar sobre tipos completos de turing, macros, fns normales, etc., pero me resulta imposible razonar sobre fn const, ya que incluso los detalles menores cambian su significado por completo.

ya que incluso los detalles menores cambian su significado por completo.

¿Podría elaborar? ¿Te refieres a extensiones como const fn punteros, const límites de rasgos, ...? Porque no veo ningún detalle menor en la propuesta const fn .

Alternativamente, haga que const fns sea realmente const: cualquier cosa const fn debe evaluarse como si cada parámetro fuera un const genérico.

Eso es lo que estamos haciendo en tiempo de compilación. Solo que en tiempo de ejecución la función se usa como cualquier otra función.

El problema es que cualquier detalle menor puede convertir una evaluación constante en una evaluación de tiempo de ejecución. Esto puede no parecer un gran problema, al principio, pero puede serlo .

Digamos que la llamada a la función es muy larga porque todo es const fns. Y quieres dividirlo en varias líneas.

Así que agrega algunos let s.

Ahora su programa tarda 20 veces más en ejecutarse.

@SoniEx2 El tamaño de las matrices ( $N en [u8; $N] ) siempre se evalúa en tiempo de compilación. Si esa expresión no es const , la compilación fallará. Por el contrario, let x = foo() llamará a foo en tiempo de ejecución, sea o no const fn (módulo de la propagación constante y en línea del optimizador, pero eso es completamente independiente de const fn ). Si desea nombrar el resultado de evaluar alguna expresión en tiempo de compilación, necesita un elemento const .

Ahora su programa tarda 20 veces más en ejecutarse.

¡Así no es como funciona const fn!

Si declara una función const fn y agrega un enlace let dentro de ella, su código deja de compilarse.

Si elimina const de un const fn , es un cambio importante y romperá todos los usos de esa función dentro, por ejemplo const , static o longitudes de matriz. Su código que era código de tiempo de ejecución y ejecutaba const fn , nunca se ejecutaría en tiempo de compilación. Es solo una llamada de función de tiempo de ejecución normal, por lo que no se vuelve más lenta.

Editar: @SimonSapin se me adelantó :D

Const fn se evalúa en tiempo de compilación si es posible.

Es decir,

const fn random() -> i32 {
    4
}

fn thing() -> i32 {
    let i = random(); // the RHS of this binding is evaluated at compile-time, there is no call to random at runtime.
}

Ahora digamos que tienes una const fn que acepta argumentos. Esto se evaluaría en tiempo de compilación:

fn thing() {
    let x = const_fn_with_1_arg(const_fn_returns_value());
}

Esto haría que se evaluara const_fn_with_1_arg en tiempo de ejecución:

fn thing() {
    let x = const_fn_returns_value();
    let y = const_fn_with_1_arg(x); // suddenly your program takes 20x longer to run, and compiles 20x faster.
}

@eddyb Me pregunto si la preocupación por el diseño se puede resolver mediante la observación de que la "const fn mínima" es compatible con todas las posibles extensiones futuras. Es decir, tengo entendido que queremos estabilizar

marcar funciones libres y métodos inherentes como const, lo que les permite ser llamados en contextos constantes, con argumentos constantes.

Esto parece ser trivialmente totalmente compatible con cualquier diseño de "efectos constantes para rasgos". También es compatible con el diseño de "const. inferidas", porque podemos hacer que const sea opcional más adelante.

¿Existen diseños futuros alternativos que sean incompatibles con la propuesta actual de "mínima const fn"?

@nrc

Tenga en cuenta que rfcbot no registró su inquietud everything-const (¡una inquietud por comentario!) Sin embargo, parece ser un subconjunto de la inquietud de diseño, que se aborda en mi comentario anterior (TL; DR: propuesta mínima actual es totalmente compatible con todo, podríamos hacer que la palabra clave const sea opcional en el futuro).

En cuanto a la preocupación de prioridad/resultados, me gustaría documentar lo que hemos discutido en todas las manos y lo que aún no hemos documentado:

  • const fn foo(x: i32) -> i32 { body } es una adición relativamente menor sobre const FOO: i32 = body; , por lo que el riesgo de consecuencias es pequeño. Es decir, la mayor parte del código que realmente implementa const fn ya está trabajando duro en el compilador estable (descargo de responsabilidad: esto es algo que escuché de @oli-obk, es posible que haya escuchado mal).

  • el grupo de trabajo incrustado quiere mucho const fn :)

Además, tenga en cuenta que no estabilizar const fn conduce a la proliferación de API subóptimas en las bibliotecas, porque tienen que usar trucos como ATOMIC_USIZE_INIT para solucionar la falta de const fns.

sea ​​i = aleatorio(); // el RHS de este enlace se evalúa en tiempo de compilación, no hay llamada aleatoria en tiempo de ejecución.

No, eso no está sucediendo en absoluto. Podría estar sucediendo (y llvm probablemente esté haciendo esto), pero no puede esperar que ocurra ninguna optimización del compilador que dependa de la heurística. Si desea que algo se calcule en tiempo de compilación, introdúzcalo en un const y obtendrá esa garantía.

Entonces, const fn solo se evalúa en un const, ¿y esto es básicamente inútil de lo contrario?

¿Por qué no tener const y non-const fn estrictamente separados entonces?

vea que la semántica es un desastre porque intencionalmente mezclan cosas de tiempo de compilación y tiempo de ejecución.

Entonces, const fn solo se evalúa en un const, ¿y esto es básicamente inútil de lo contrario?

No son inútiles, se ejecutan en tiempo de ejecución como cualquier otra función. Esto significa que no tiene que usar diferentes "sublenguajes" de Rust dependiendo de si está en una evaluación constante o no.

¿Por qué no tener const y non-const fn estrictamente separados entonces?

Toda la motivación de const fn es no tener esta separación. De lo contrario, tendríamos que duplicar todo tipo de funciones: AtomicUsize::new() + AtomicUsize::const_new() , aunque ambos cuerpos son idénticos.

¿Realmente quiere tener que escribir el 90% de libcore dos veces, una para const eval y otra para runtime? Lo mismo probablemente se aplica a muchas otras cajas.

Estaba pensando AtomicUsize::Of<value> . Y sí, prefiero tener que escribir todo dos veces que tener garantías desconocidas. (Además, esto no se comportaría de manera diferente en función de si algo se está evaluando o no).

¿Puede declarar consts en const fn para garantizar la evaluación de const (para const fn recursivo)? ¿O necesita pasar por genéricos const? Etc

@SoniEx2 como ejemplo de cómo debe escribirse su ejemplo para aprovechar const fn y convertirse en un error de tiempo de compilación si cualquiera de las funciones deja de ser const :

fn thing() {
    const x: u32 = const_fn_returns_value();
    const y: u32 = const_fn_with_1_arg(x);
}

(ejemplo completo de ejecución en el patio de recreo)

Ligeramente menos ergonómico porque no hay tipo de inferencia, pero quién sabe, eso puede cambiar en el futuro.

que tener garantías desconocidas.

¿Serías tan amable de dar algunos ejemplos de dónde crees que algo no está claro?

¿Puede declarar consts en const fn para garantizar la evaluación de const (para const fn recursivo)? ¿O necesita pasar por genéricos const? Etc

El objetivo de const fn no es evaluar mágicamente las cosas en tiempo de compilación. Es poder evaluar cosas en tiempo de compilación.

La evaluación mágica de las cosas en tiempo de compilación ya está sucediendo desde que rustc se basó en llvm. Entonces... exactamente cuando dejó de implementarse en ocaml. No creo que nadie quiera eliminar la propagación constante de rustc.

const fn no influye en la propagación constante de ninguna manera. Si tuviera una función que accidentalmente pudiera propagarse de manera constante, y llvm lo hiciera, y usted cambiara esa función de manera que ya no sea propagable de manera constante, llvm dejaría de hacerlo. Esto es completamente independiente de adjuntar const a una función. Para llvm no hay diferencia entre un const fn y un fn .

Al mismo tiempo, rustc no cambia su comportamiento en absoluto cuando adjunta const a fn (asumiendo que la función es una const fn válida y, por lo tanto, aún se compila después de hacerlo). Solo le permite llamar a esta función en constantes a partir de ahora .

No estoy pensando en LLVM, estoy pensando en óxido. LLVM no me importa aquí.

@SoniEx2

Const fn se evalúa en tiempo de compilación si es posible.

Esto no es correcto. const fn , cuando se llama en un contexto particular, se evaluará en tiempo de compilación. Si eso no es posible, hay un error grave. En todos los demás contextos, la parte const no importa en absoluto.

Ejemplos de tales contextos que requieren const fn son las longitudes de matriz. Puedes escribir [i32; 15] . También puede escribir [i32; 3+4] porque el compilador puede calcular 7 . No puede escribir [i32; read_something_from_network()] , porque ¿cómo tendría sentido eso? Con esta propuesta, PUEDE escribir [i32; foo(15)] si foo es const fn , lo que asegura que se parece más a una suma y menos a un acceso a la red.

No se trata en absoluto de código que puede ejecutarse o se ejecutará cuando se ejecute el programa. No hay "tal vez evaluar en tiempo de compilación". Simplemente "tiene que evaluar en tiempo de compilación o abortar la compilación".

Lea también el RFC: https://github.com/rust-lang/rfcs/blob/master/text/0911-const-fn.md

En lugar de tener una anotación const fn , ¿qué pasaría si fuera una propiedad inferida? No sería explícito en el código fuente, pero podría etiquetarse automáticamente como tal en la documentación generada automáticamente. Esto permitiría una eventual ampliación de lo que se considera const , sin que los autores de la biblioteca necesiten cambiar su código. Al principio, la inferencia podría limitarse a lo que const fn s admita actualmente (¿funciones deterministas puras sin enlaces let?).

Según este enfoque, la evaluación se produciría en tiempo de compilación si el resultado está vinculado a una variable const y, de lo contrario, en tiempo de ejecución. Esto parece más deseable, ya que le da a la persona que llama (en lugar de a la persona que llama) el control sobre cuándo se evalúa la función.

Esto ya se ha discutido bastante a fondo. La desventaja de ese enfoque es que hace que sea más fácil ir accidentalmente en la otra dirección: alguien podría estar usando una función de biblioteca en un contexto const , pero luego el autor de la biblioteca podría dejar de ser const sin siquiera darse cuenta.

Hmmm, sí que es un problema...

Editar: la única solución que se me ocurre, sin llegar hasta const fn , sería tener una anotación de exclusión, de modo que el autor de la biblioteca pueda reservarse el derecho de romper const ness. Sin embargo, no estoy seguro de que sea mejor que esparcir const fn por todas partes. El único beneficio real sería una adopción más rápida de una definición más amplia de const .

Edición 2: supongo que eso rompería la compatibilidad con versiones anteriores, por lo que no es un comienzo. Perdón por el desvío.

Así que... la discusión se ha calmado. Resumamos:

el comentario de rfcbot es https://github.com/rust-lang/rust/issues/24111#issuecomment -376649804

Preocupaciones actuales

  • Esto no es una prioridad, y el lanzamiento de 2018 ya ha puesto suficiente en nuestros platos.
  • Empezamos a marcar todo const , lo cual es molesto
  • ¿Es este el diseño con el que queremos comprometernos?

Realmente no puedo hablar sobre la prioridad + la preocupación por las consecuencias, excepto que const fn se ha horneado todas las noches durante mucho, mucho tiempo.

Los otros dos puntos están íntimamente relacionados. El diseño de @eddyb (https://github.com/rust-lang/rust/issues/24111#issuecomment-376829588 como lo entendí) es no tener const fn , sino tener #[const] atributo que puedes abofetear en cosas:

#[const]
mod foo {
    pub fn square(i: i32) -> i32 { i * i }
}
#[const]
fn bar(s: &str) -> &str i{ s }
#[const]
fn boo() -> fn(u32) -> u32 { meh }
fn meh(u: u32) -> u32 { u + 1 }

y eso recursivamente entra en lo que esté marcado con él, por lo que cualquier función dentro de un módulo #[const] es un #[const] fn . Una función declarada dentro de un #[const] fn también es un #[const] fn .

Esto reduce la cantidad de anotaciones necesarias, ya que algunas cajas simplemente colocarán #![const] en los lib.rs y terminarán con eso.

Problemas que veo con ese diseño (pero esos problemas también existen a menudo en const fn ):

  • podría requerir soporte de exclusión voluntaria, porque es posible que desee poder declarar algunas funciones no constantes en lo profundo de un árbol de módulos #[const] .
  • ¿Se requiere que los punteros de función a #[const] fn en la posición de tipo de argumento/retorno sean #[const] fn ?

    • de nuevo, sería necesario darse de baja

Necesitamos pensar en estas cosas, por lo que no diseñamos un sistema que sea incompatible con una versión futura en la que queremos poder llamar funciones a través de punteros de función durante una const eval.

Tenga en cuenta que no propuse un diseño determinado, sino que simplemente enumeré algunas direcciones plausibles conocidas.
La idea original era un atributo generalizado de "exponer el cuerpo de la función", no limitado a const , pero hay muchas variaciones posibles, y algunas de ellas podrían incluso ser buenas .

EDITAR : (no quiero olvidarme de esto) @solson me estaba mostrando cómo Lean tiene atributos como @pattern que derivan automáticamente varias cosas del cuerpo de una función.

@oli-obk Creo que no deberíamos ir con atributos, porque unsafe no usa un atributo.
Además, async actualmente tampoco lo hace. Y si introducimos try fn dados try { .. } bloques, entonces tenemos otra cosa que no está basada en atributos. Creo que deberíamos tratar de mantenernos lo más consistentes posible con las cosas que son similares a los efectos. usar atributos o no. #[target_feature(..)] pone una arruga en la consistencia general aunque.

PD: Podrías usar const mod { .. } para obtener el mismo efecto que #![const] más o menos. Esto también podría aplicarse a try mod , async mod , unsafe mod .

Siempre me inclinaré por hacer cosas con tipos especiales.

struct SizeOf<T>;

impl<T> SizeOf<T> {
    const intrisic Result: usize;
}

es más fácil aprender nuevos tipos utilizando la sintaxis existente que aprender nuevos conceptos con una nueva sintaxis.

y luego podemos admitir el sistema de tipo en tiempo de ejecución.

fn sq(v: i32) -> i32 {
    Square<v>::Result
}

tipos en tiempo de compilación, const genéricos en tiempo de compilación o en tiempo de ejecución.

Entonces... Sugiero ignorar el hecho de que podría haber un diseño posiblemente mejor, porque tenemos un diseño que es

  1. Fácil de razonar sobre
  2. Compatible hacia adelante con cualquier diseño más permisivo
  3. Históricamente se ha utilizado en código inestable con gran éxito, y la queja principal es que no tiene suficientes características.
  4. Se puede usar linted para sugerir agregar la anotación a funciones aún no anotadas que tienen un cuerpo que permite agregarla.
  5. Personalmente, como se publicó en la solicitud de fusión, veo que es el único diseño que podemos estabilizar sin otro período de prueba de varios años.
  6. Permite que el código se comparta entre el tiempo de ejecución y el código de evaluación constante, en lugar de requerir un código personalizado para cada uno.

y luego podemos admitir el sistema de tipo en tiempo de ejecución.

Eso es tipeo dependiente, que está lejos , mientras que llamar a const fn en tiempo de ejecución funciona bien hoy.

@oli-obk ¿Qué pasa con los rasgos? No quiero estabilizar const fn sin tener una idea de lo que vamos a hacer con los métodos de rasgos que son const fn solo en algunos de los impl de los rasgos.

@eddyb parece que debería acelerar la escritura de los nuevos límites y métodos constantes en ese momento. :)

@Centril Mi punto es que la propuesta de atributos (ya sea que use una palabra clave o no) daría como resultado un enfoque mucho más diferente para tratar los métodos de rasgos, y tenemos que comparar eso .
El enfoque actual const fn puede parecer simple y extensible, pero no cuando está realmente extendido.

const genéricos + const genéricos:

intrinsic const SizeOf<T>: usize;

const Pow<const V: usize>: usize = V*V;

@eddyb Tengo varias soluciones en mente que son totalmente compatibles con el diseño const fn. Lo escribiré.

Woah, acabo de ver que han pasado dos años desde que comenzó. ¿Hay alguna fecha prevista de estabilización? Tengo una caja que está casi disponible en estable porque espera a que se estabilice esta extensión. :)

@rfcbot preocupación runtime-pointer-addresses

En un tema diferente, surgió la pregunta de si queremos transparencia referencial de const fn , y apareció el problema de las direcciones de punteros sin procesar que se usan como un oráculo de no determinismo: https://github.com/rust- lang/rust/issues/49146#issuecomentario -386727325. Allí se describe una solución, pero implica realizar algunas operaciones de puntero en bruto unsafe (no estoy seguro de cuántas de ellas están permitidas hoy en día), antes de la estabilización.

@eddyb ¿ E0018 no se aplicaría también a const fn s?

El C Way es que se permite que todos los punteros de objetos sean 0 a menos que sean relativos (es decir, dentro de un objeto) y rastreados en tiempo de ejecución de alguna manera.

No estoy seguro de si el óxido es compatible con las reglas de alias de C.

@sgrif Muchos de los errores emitidos sobre las constantes desaparecerán tarde o temprano: a miri no le importa el tipo de valor que se ve, una ubicación abstracta que está en un usize sigue siendo una ubicación abstracta (y lanzarlo a un puntero le devuelve el puntero original).

Acabo de comprobar y, por ahora , tanto la conversión de punteros a enteros como los operadores de comparación entre punteros están prohibidos en contextos constantes. Sin embargo, esto es justo lo que pensamos , todavía tengo miedo.

@eddyb Bastante justo. Sin embargo, espero que cualquier inquietud que tenga que llegue a const fn ya llegue a cualquier bloque const hoy.

@sgrif La diferencia es que const (incluso los const asociados que dependen de parámetros de tipo genérico) se evalúan completamente en tiempo de compilación, bajo miri, mientras que const fn no es un const fn para llamadas en tiempo de ejecución.
Entonces, si realmente queremos transparencia referencial, debemos asegurarnos de que no permitimos (al menos en código seguro) cosas que pueden causar no determinismo en el tiempo de ejecución, incluso si están bien bajo miri .
Lo que probablemente significa que obtener los bits de flotación también es un problema, porque, por ejemplo, las cargas útiles de NaN.

Cosas que deberías considerar hacer en miri:

Todos los punteros son 0 a menos que sean relativos. P.ej:

#[repr(C)]
struct X {
    a: usize,
    b: u8,
}
let x = X { a: 1, b: 2 };
let y: usize = 3;
assert_eq!(&x as *const _ as usize, 0);
assert_eq!(&x.a as *const _ as usize, 0);
assert_eq!(&x.b as *const _ as usize, 8);
assert_eq!(&y as *const _ as usize, 0);

Luego los rastreas en la evaluación de miri. Algunas cosas serían UB, como pasar de puntero a uso de nuevo a puntero, pero son fáciles de rechazar (prohibir las conversiones de tamaño a puntero, ya que ya estaría rastreando punteros en tiempo de ejecución/evaluación).

En cuanto a los bits flotantes, ¿normalización de NaN?

Creo que ambos harían que todo fuera determinista.

Lo que probablemente significa que obtener los bits de flotación también es un problema, porque, por ejemplo, las cargas útiles de NaN.

Creo que para una transparencia referencial total, tendríamos que hacer que todas las operaciones flotantes no sean seguras.

el determinismo de punto flotante es difícil

El punto más importante aquí es que el optimizador de LLVM puede y cambia el orden de las operaciones de flotación, así como ~realiza~ operaciones de fusibles para las que tiene un código de operación combinado. Estos cambios afectan el resultado de la operación, incluso si la diferencia real es mínima. Esto afecta la transparencia referencial porque miri ejecuta el mir no optimizado para llvm mientras que el objetivo ejecuta el código llvm optimizado y posiblemente reordenado y, por lo tanto, con una semántica diferente a la nativa.

Estaría de acuerdo con una primera característica estable de const fn sin flotadores por ahora hasta que haya una decisión sobre la importancia de la transparencia referencial, pero no me gustaría que const fn se ralentizara o bloqueara por esa discusión.

El punto más importante aquí es que el optimizador de LLVM puede y cambia el orden de las operaciones flotantes, así como también realiza operaciones de fusibles para las que tiene un código de operación combinado. Estos cambios afectan el resultado de la operación, incluso si la diferencia real es mínima.

Las operaciones de fusión (supongo que se refiere a mul-add) no están permitidas/no se realizan sin banderas de matemáticas rápidas precisamente porque cambia el redondeo de los resultados. LLVM tiene mucho cuidado de preservar la semántica exacta de las operaciones de punto flotante cuando se dirige a hardware compatible con IEEE, dentro de los límites establecidos por el "entorno de punto flotante predeterminado".

Dos cosas que LLVM no intenta preservar son las cargas útiles de NaN y la señalización de NaN, porque ambas no se pueden observar en el entorno de fp predeterminado, solo mediante la inspección de los bits de los flotadores, que por lo tanto tendríamos que prohibir. (E incluso si LLVM fuera más cuidadoso con eso, el hardware también varía en su tratamiento de las cargas útiles de NaN, por lo que no puede fijar esto en LLVM).

El otro caso importante que conozco en el que la decisión del compilador puede marcar la diferencia en los resultados de punto flotante es la ubicación de derrames y recargas en el código x87 (anterior a SSE). Y esto es principalmente un problema porque x87 por defecto redondea a 80 bits para resultados intermedios y redondea a 32 o 64 bits en las tiendas. Es posible configurar correctamente el modo de redondeo antes de cada instrucción FPU para lograr resultados redondeados correctamente, pero no es realmente práctico y, por lo tanto, (creo) LLVM no lo admite.

Sobre la Transparencia Referencial Plena

Mi punto de vista es que deberíamos optar por una total transparencia referencial porque concuerda con el mensaje general de Rust de escoger seguridad/corrección sobre conveniencia/completitud .

La transparencia referencial agrega muchos beneficios de razonamiento, como permitir el razonamiento ecuacional (hasta el fondo, pero el razonamiento rápido y suelto es moralmente correcto ).

Sin embargo, hay, por supuesto, inconvenientes wrt. perder la integridad wrt. CTFE. Con esto quiero decir que un mecanismo const fn referencialmente transparente no sería capaz de evaluar tanto en tiempo de compilación como podría hacerlo un esquema const fn sin transparencia de referencia. A aquellos que proponen no ceñirse a la transparencia referencial, les pediría que proporcionen tantos casos de uso concretos como puedan contra esta propuesta para que podamos evaluar las ventajas y desventajas.

De acuerdo, parece que tiene razón sobre el punto LLVM, de hecho, parece evitar operaciones computacionalmente incorrectas a menos que habilite el modo matemático rápido.

Sin embargo, todavía hay un montón de estados de los que dependen las operaciones de punto flotante como la precisión interna. ¿El evaluador flotante de CTFE conoce el valor de precisión interno en tiempo de ejecución?

Además, durante el vertido de valores a la memoria, convertimos el valor interno al formato ieee754 y, por lo tanto, cambiamos la precisión. Esto también puede afectar el resultado, y el algoritmo mediante el cual los compiladores realizan el derrame no está especificado, ¿verdad?

@ est31 Tenga en cuenta que asumí que no nos importa si el comportamiento del tiempo de compilación y el tiempo de ejecución difieren, solo que las llamadas repetidas (con gráficos de objetos congelados) son consistentes y no tienen efectos secundarios globales.

Entonces, si realmente queremos transparencia referencial, debemos asegurarnos de no permitir (al menos en código seguro) cosas que puedan causar no determinismo en el tiempo de ejecución, incluso si están bien bajo miri.

Entonces, el objetivo aquí es garantizar que un const fn sea determinista y no tenga efectos secundarios en tiempo de ejecución, incluso si miri comete un error al respecto durante la ejecución , al menos si el const fn es totalmente seguro. ¿código? Nunca consideré que eso fuera importante, TBH. Es un requisito bastante estricto y, de hecho, al menos tendríamos que hacer que ptr-to-int y float-to-bits no sean seguros, lo cual será difícil de explicar. OTOH, entonces también deberíamos hacer que la comparación de punteros sin procesar no sea segura y me alegraría por eso: D

¿Cuál es la motivación para esto? Parece que estamos tratando de volver a introducir la pureza y un sistema de efectos, que son cosas que Rust alguna vez tuvo y perdió, presumiblemente porque no tenían su peso (EDIT: o porque simplemente no era lo suficientemente flexible para hacer todas las diferentes cosas para las que la gente quería usarlo, consulte https://mail.mozilla.org/pipermail/rust-dev/2013-April/003926.html).

ptr-to-int es seguro si se convierte desde el inicio de un objeto y usted permite que int-to-ptr falle. sería 100% determinista. y cada llamada produciría los mismos resultados.

ptr-to-int es seguro, int-to-ptr no es seguro. ptr-to-int debe permitirse resultados "inesperados" en la evaluación const .

y realmente debería usar la canonicalización de NaN en un intérprete/VM. (un ejemplo de esto es LuaJIT, que hace que todos los resultados de NaN sean el NaN canónico, y cualquier otro NaN es un NaN empaquetado)

ptr-to-int es seguro si se convierte desde el inicio de un objeto y usted permite que int-to-ptr falle. sería 100% determinista. y cada llamada produciría los mismos resultados.

¿Cómo sugiere que implementemos esto en tiempo de ejecución? (&mut *Box::new(...)) as usize es una función no determinista en este momento y debería ser todo const fn . Entonces, ¿cómo sugiere que nos aseguremos de que sea "referencialmente transparente en tiempo de ejecución" en el sentido de devolver siempre el mismo valor?

Box::new debe devolver un puntero rastreado en la máquina virtual.

La conversión daría como resultado 0, en tiempo de compilación (es decir, en una evaluación const). En la evaluación no constante, devolvería lo que sea.

Un puntero rastreado funciona así:

Asignas un puntero y lo asignas. Cuando lo asigna, la máquina virtual establece el valor del puntero en 0, pero toma la dirección de memoria del puntero y lo adjunta a una tabla de búsqueda. Cuando usa el puntero, recorre la tabla de búsqueda. Cuando obtiene el valor del puntero (es decir, los bytes que lo componen), obtiene 0. Suponiendo que es el puntero base de un objeto.

Box::new debe devolver un puntero rastreado en la máquina virtual.

Estamos hablando del comportamiento en tiempo de ejecución aquí. Como en, lo que sucede cuando esto se ejecuta en el binario. No hay máquina virtual.

miri ya puede manejar todo esto muy bien y completamente determinista.


Además, volviendo a la preocupación sobre const fn y la determinación del tiempo de ejecución: si queremos eso (que no estoy seguro de que lo hagamos, todavía estoy esperando que alguien me explique por qué nos importa: D), podríamos simplemente declarar ptr-to-int y float-to-bits para que sean operaciones no constantes. ¿Hay algún problema con eso?

@RalfJung Enlacé https://github.com/rust-lang/rust/issues/49146#issuecomment -386727325 ya en el comentario de preocupación, tal vez se perdió en otros mensajes, incluye una descripción de una solución propuesta por @Centril ( realice operaciones unsafe y agregue a la lista blanca algunos "usos correctos", por ejemplo, offset_of para ptr-to-int y float-to-bits normalizado NaN).

@eddyb seguro, hay varias soluciones; lo que estoy pidiendo es la motivación. Yo tampoco lo he encontrado por ahí.

Además, citándome a mí mismo del IRC: creo que incluso podemos argumentar con razón que CTFE es determinista, incluso cuando se permite la conversión de ptr a int. Todavía se necesita un código que no sea CTFE (por ejemplo, extraer bits de un ptr convertido a un int) para observar realmente cualquier no determinismo.

const fn thing() -> usize {
    (*Box::new(0)) as *const _ as usize
}

const X: usize = thing();
const Y: usize = thing();

es X == Y?

con mi sugerencia, X == Y.

@SoniEx2 que rompe foo as *const _ as usize as *const _ que es totalmente un noop en este momento

Motivación escrita:
Personalmente, no veo un problema con el comportamiento del tiempo de ejecución que difiera del comportamiento del tiempo de compilación o incluso que const fn no sea determinista en el tiempo de ejecución. La seguridad no es el problema aquí, por lo que inseguro es una palabra clave totalmente incorrecta. Este es un comportamiento sorprendente que detectamos totalmente en la evaluación en tiempo de compilación. Ya puede hacer esto en las funciones normales, así que no hay sorpresas. Const fn se trata de marcar funciones existentes como evaluables en tiempo de compilación sin afectar el tiempo de ejecución. No se trata de pureza, al menos esa es la vibra que tengo cuando la gente habla sobre el "puro infierno" (yo no estaba presente, así que podría estar malinterpretándolo)

@SoniEx2 Sí, X == Y siempre.

Sin embargo, si tienes:

const fn oracle() -> bool { let x = 0; let y = &x as *const _ as usize; even(y) }

fn main() {
    assert_eq!(oracle(), oracle());
}

Entonces main puede entrar en pánico en tiempo de ejecución.

@RalfJung

o porque simplemente no era lo suficientemente flexible para hacer todas las cosas diferentes que la gente quería usar para [..]

¿Qué eran esas cosas? Es difícil evaluar esto sin concreción.

Creo que incluso podemos argumentar con razón que CTFE es determinista, incluso cuando se permite la conversión de ptr a int.
[...]
Todavía se necesita un código que no sea CTFE (por ejemplo, extraer bits de un ptr convertido a un int) para observar realmente cualquier no determinismo.

Sí, const fn sigue siendo determinista cuando se ejecuta en tiempo de compilación, pero no creo que sea determinista en tiempo de ejecución porque invariablemente habrá algo que no sea const fn (solo fn ) código si el resultado de const fn va a ser útil para cualquier cosa en tiempo de ejecución, y luego ese código fn observará los efectos secundarios del const fn ejecutado.

El punto central de la separación entre el código puro y no puro en Haskell es que puede tomar la decisión de "habrá efectos secundarios" localmente donde no tiene que pensar en los posibles efectos secundarios a nivel mundial. Es decir, dado:

reverse :: [a] -> [a]
reverse []       = []
reverse (x : xs) = reverse xs ++ [x]

putStrLn :: String -> IO ()
getLine :: IO String

main :: IO ()
main = do
    line <- getLine
    let revLine = reverse line
    putStrLn revLine

Sabe que el resultado de revLine en let revLine = reverse line solo puede depender del estado pasado a reverse que es line . Esto proporciona beneficios de razonamiento y una separación limpia.

Me pregunto cómo se veía el sistema de efectos en ese entonces... Creo que necesitamos uno con const fn , async fn , etc. de todos modos para hacerlo limpiamente y reutilizar el código (algo https:// github.com/rust-lang/rfcs/pull/2237 pero con algunos cambios...) y la parametricidad parece una buena idea para eso.

En las palabras inmortales de Phil Wadler y Conor McBride:

¿Seré puro o impuro?
—Felipe Wadler [60]

Decimos 'Sí': la pureza es una elección a realizar localmente

https://arxiv.org/pdf/1611.09259.pdf

@oli-obk

No envidio a la persona que tiene que documentar este comportamiento sorprendente de que el resultado de la ejecución puede diferir en const fn si se ejecuta en tiempo de ejecución o en tiempo de compilación con los mismos argumentos.

Como opción conservadora, propongo que retrasemos la decisión sobre la transparencia/pureza referencial al estabilizar const fn como referencialmente transparente, haciéndolo unsafe (o no const ) para violar pero en realidad no garantizamos la transparencia referencial, por lo que la gente tampoco puede asumirlo.

En un momento posterior, cuando hayamos adquirido experiencia, podremos tomar la decisión.
Ganar experiencia incluye personas que intentan hacer uso del no determinismo pero fallan, y luego lo informan y nos cuentan sus casos de uso.

Sí, pero eso no es una evaluación constante.

Convertir int a ptr en const no parece una gran pérdida. Obtener compensaciones de campo parece más importante, y eso es lo que estoy tratando de preservar.

No me gustaría cambiar silenciosamente el comportamiento al agregar const a una función @SoniEx2 y eso es esencialmente lo que haría su sugerencia.

@centril Estoy de acuerdo, hagamos lo conservador ahora. así que no los hacemos inseguros, sino inestables. de esta manera, libstd puede usarlo, pero luego podemos decidir los detalles.

Un problema que tenemos es que los usuarios siempre pueden bloquear cualquier análisis que hagamos introduciendo uniones y haciendo algunas conversiones sofisticadas, por lo que tendríamos que hacer un UB de conversiones tan elusivo para que podamos cambiarlo más tarde.

seguiría siendo el mismo en tiempo de ejecución, la const solo cambiaría las cosas en el momento de la const, lo cual... ni siquiera sería posible prescindir de la const.

está bien que const fns sea un sublenguaje con diferentes reglas de alias de puntero.

@ Centril @ est31 No estoy seguro de qué tiene que ver el clorotrifluoroetileno con nada (¿podemos evitar usar un montón de acrónimos sin definirlos, por favor?)

@sgrif CTFE (evaluación de la función en tiempo de compilación) se ha utilizado durante mucho tiempo para Rust (incluso en las partes antiguas de este hilo). Es uno de esos términos que ya deberían estar definidos en algún lugar central.

@sgrif lol, lo siento, la mayoría de las veces soy la persona que se pregunta "¿qué palabras extrañas están usando ahora?". Creo que estaba hablando con @eddyb en IRC cuando mencionó "CTFE" y tuve que preguntarle qué significaba: p.

@eddyb ctrl+f + CTFE en esta página no conduce a nada que la defina. De cualquier manera, en su mayoría estoy suponiendo que no queremos obligar a las personas a investigar mucho para participar en las discusiones aquí. "Ya deberías saber lo que esto significa" es bastante excluyente en mi opinión.

@Centril @est31 gracias :heart:

Así que... alguna idea sobre

Un problema que tenemos es que los usuarios siempre pueden bloquear cualquier análisis que hagamos introduciendo uniones y haciendo algunas conversiones sofisticadas, por lo que tendríamos que hacer un UB de conversiones tan elusivo para que podamos cambiarlo más tarde.

¿Deberían ser también inestables los accesos al campo de unión dentro de const fn?

ctrl+f + CTFE en esta página no conduce a nada que la defina.

Lo siento, lo que quise decir con eso es que su uso en el diseño y desarrollo de Rust es anterior a este problema anterior a 1.0.
Debería haber un documento central para tales términos, pero lamentablemente, el glosario de referencia realmente falta.
HRTB y NLL son dos ejemplos de otros acrónimos que son más nuevos pero que tampoco se explican en línea.

No quiero que ninguna discusión sea excluyente, pero no creo que sea justo pedirle a @Centril o @est31 que definan CTFE, ya que ninguno de ellos introdujo el acrónimo en primer lugar.
Hay una solución que, lamentablemente, no necesariamente funcionaría en GitHub, que he visto en cierto subreddit, donde un bot creará un comentario de nivel superior y lo mantendrá actualizado , con una lista de expansiones para todos los acrónimos de uso común. de ese subreddit que aparece en la discusión.

Además, tengo curiosidad si estoy en una burbuja de Google, ya que los dos artículos de wikipedia para CTFE ("Clorotrifluoroetileno" y "Ejecución de la función de tiempo de compilación") son los dos primeros resultados para mí. Incluso entonces, el primero comienza con "Para la característica del compilador, consulte la ejecución de la función de tiempo de compilación".

( Puse el enlace wiki de CTFE en la parte superior; ahora, ¿podemos volver a const fn s? :P)

¿Alguien podría agregar CTFE al glosario de la guía rustc (estoy en un dispositivo móvil)?

Además, creo que deberíamos estabilizar lo mínimo y ver con qué se encuentra la gente en la práctica, lo que envía al enfoque actual con expresiones const if y match.

@centril

No envidio a la persona que tiene que documentar este comportamiento sorprendente de que el resultado de la ejecución puede diferir para const fn si se ejecuta en tiempo de ejecución o en tiempo de compilación con los mismos argumentos.

El even que escribió anteriormente fallaría si se ejecutara en el momento de CTFE porque inspecciona los bits de un puntero. (Aunque en realidad podemos decir de manera determinista que esto se debe incluso a la alineación, y si la prueba de uniformidad se realiza mediante operaciones de bits, "full miri" lo haría correctamente. Pero supongamos que está probando todo el byte menos significativo, no solo el último bit.)
El caso del que estamos hablando aquí es una función que falla con un error de intérprete en tiempo de compilación, pero tiene éxito en tiempo de ejecución. La diferencia está en si la función incluso completa la ejecución. No creo que sea difícil de explicar.

Acepto que si CTFE tiene éxito con un resultado , entonces también se debe garantizar que la versión en tiempo de ejecución de la función tenga éxito con el mismo resultado. Pero esa es una garantía mucho más débil de lo que estamos hablando aquí, ¿no es así?

@oli-obk

Estoy de acuerdo, hagamos lo conservador ahora. así que no los hacemos inseguros, sino inestables. de esta manera, libstd puede usarlo, pero luego podemos decidir los detalles.

Perdí el contexto, ¿qué es "estos" aquí?

En este momento, afortunadamente, CTFE miri simplemente se niega rotundamente a hacer cualquier cosa con los valores de los punteros: aritmética, comparación, todos los errores. Esta es una verificación realizada en el momento de CTFE en función de los valores realmente utilizados en el cálculo, las uniones no pueden eludirla y, de todos modos, el código que se necesitaría para realizar la aritmética/comparación simplemente no existe . Por lo tanto, estoy bastante seguro de que cumplimos con la garantía que mencioné anteriormente.

Podría imaginar problemas si tuviéramos que CTFE devolviera un valor de puntero, pero ¿cómo tendría sentido un valor de puntero calculado en tiempo de compilación en cualquier lugar? Supongo que ya verificamos lo que Miri calcula para que no contenga valores de puntero porque tenemos que convertirlo en bits.

Cuidadosamente podríamos agregar operaciones a CTFE miri y, de hecho, todo lo que necesitamos para el offset_of [1] de @eddyb es la resta de punteros. Ese código existe en "full miri", y solo tiene éxito si ambos punteros están dentro de la misma asignación, lo cual es suficiente para mantener la garantía anterior. Lo que no funcionaría es el assert que @eddyb agregó como protección.
También podríamos permitir operaciones de bits en valores de puntero si las operaciones solo afectan la parte alineada del puntero, eso sigue siendo determinista y el código ya existe en "full miri".

EDITAR: [1] Como referencia, me refiero a su macro en este hilo al que no podemos vincular porque estaba marcado como fuera de tema, así que aquí hay una copia:

macro_rules! offset_of {
    ($Struct:path, $field:ident) => ({
        // Using a separate function to minimize unhygienic hazards
        // (e.g. unsafety of #[repr(packed)] field borrows).
        // Uncomment `const` when `const fn`s can juggle pointers.
        /*const*/ fn offset() -> usize {
            let u = $crate::mem::MaybeUninit::<$Struct>::uninit();
            // Use pattern-matching to avoid accidentally going through Deref.
            let &$Struct { $field: ref f, .. } = unsafe { &*u.as_ptr() };
            let o = (f as *const _ as usize).wrapping_sub(&u as *const _ as usize);
            // Triple check that we are within `u` still.
            assert!((0..=$crate::mem::size_of_val(&u)).contains(&o));
            o
        }
        offset()
    })
}

EDIT2: En realidad, también lo publicó aquí .

"estos" son flotantes -> conversión de bits y puntero -> usar conversiones

Acepto que si CTFE tiene éxito con un resultado, entonces también se debe garantizar que la versión en tiempo de ejecución de la función tenga éxito con el mismo resultado. Pero esa es una garantía mucho más débil de lo que estamos hablando aquí, ¿no es así?

Entonces, ¿una función llamada con argumentos en tiempo de ejecución solo tiene pureza garantizada si realmente termina si se evalúa en tiempo de compilación con los mismos argumentos?

Eso haría nuestras vidas un millón de veces más fáciles, especialmente porque no veo una manera de prevenir el no determinismo sin dejar lagunas o paralizar las constantes en las formas de romper el cambio.

Entonces, ¿una función llamada con argumentos en tiempo de ejecución solo tiene pureza garantizada si realmente termina si se evalúa en tiempo de compilación con los mismos argumentos?

Sí, eso es lo que estoy proponiendo.


Pensándolo un poco más (y leyendo la respuesta de @oli-obk que apareció mientras escribía esto), tengo la sensación de que lo que quieres es una garantía adicional del tipo "un const fn seguro no generará un error en tiempo CTFE (aparte de pánicos) cuando se llama con argumentos válidos". Algún tipo de garantía de "seguridad constante". Junto con la garantía que mencioné anteriormente sobre la coincidencia exitosa de CTFE con el comportamiento en tiempo de ejecución, eso proporcionaría una garantía de que un const fn seguro será determinista en tiempo de ejecución, porque coincide con la ejecución exitosa de CTFE.

Estoy de acuerdo en que es una garantía más difícil de obtener. Para bien o para mal, Rust tiene varias operaciones seguras que CTFE miri no puede garantizar que se ejecuten siempre con éxito mientras se mantiene el determinismo, como la variante de oracle de @Centril que prueba el byte menos significativo para que sea 0. Desde la perspectiva de CTFE en esta configuración, los "valores válidos de tipo usize " constituyen solo valores que son PrimVal::Bytes , mientras que un PrimVal::Ptr no debe permitirse [1]. Es como si tuviéramos un sistema de tipos ligeramente diferente en el contexto constante: no estoy proponiendo que cambiemos lo que hace miri, estoy proponiendo que cambiemos lo que "significan" los distintos tipos de Rust cuando se adjuntan a const fn . Tal sistema de tipos garantizaría que todas las operaciones aritméticas y de bits seguras no puedan fallar en CTFE: para entradas válidas de CTFE, la resta de enteros nunca puede fallar en CTFE porque ambos lados son PrimVal::Bytes . Por supuesto, ptr-to-int tiene que ser una operación insegura en esta configuración porque su valor de retorno tiene el tipo usize pero no es un usize válido para CTFE.

Si esta es una garantía que nos importa, no me parece descabellado hacer que el sistema de tipos sea más estricto al verificar las funciones de CTFE; después de todo, queremos usarlo para hacer más cosas (comprobando "seguridad constante"). Creo que tendría mucho sentido, entonces, declarar conversiones de ptr a int unsafe en el contexto const , argumentando que esto es necesario porque el contexto const hace garantías adicionales.

Al igual que nuestra "seguridad en tiempo de ejecución" normal puede ser subvertida por un código inseguro, también lo puede ser "seguridad constante", y eso está bien. No veo ningún problema con el código inseguro que todavía puede hacer conversiones de ptr a int a través de uniones; después de todo, eso es un código inseguro . En este mundo, las obligaciones de prueba para código inseguro en un contexto constante son más fuertes que las de un contexto no constante; si su función devuelve un número entero, debe probar que siempre será un PrimVal::Bytes y nunca un PrimVal::Ptr .

La macro de @eddyb necesitaría un bloque inseguro, pero aún sería seguro de usar porque solo hace uso de estas "características inseguras constantes" (ptr-to-usize y luego restando el resultado, o simplemente usando la resta intrínseca del puntero directamente) de manera que se garantice que no generará un error CTFE.

El costo de tal sistema sería que un const fn de orden superior seguro debe tomar un cierre de const fn para poder garantizar que ejecutar el cierre no violará la "seguridad constante". Ese es el precio de obtener las garantías adecuadas sobre const fn seguros.

[1] Estoy ignorando completamente los flotadores aquí ya que no sé mucho acerca de dónde surgiría el no determinismo. ¿Alguien puede proporcionar un ejemplo de código de punto flotante que se comportaría de manera diferente en tiempo de CTFE que en tiempo de ejecución? ¿Sería suficiente, por ejemplo, hacer miri error si, al hacer una operación de coma flotante, uno de los operandos es un NaN de señalización (para obtener la primera garantía, la de mi publicación anterior), y decir que el sistema de tipo CTFE no permite la señalización de NaN en el tipo f32 / f64 (para obtener "seguridad constante")?

Lo que no funcionaría es la afirmación que @eddyb agregó como salvaguarda.

Claro, pero puedes reescribir assert!(condition); a [()][!condition as usize]; por ahora.

Claro, pero puedes reescribir aseverar!(condición); a [()][!condicionar como tamaño de uso]; por ahora.

No es la afirmación en la que estaba pensando, es la prueba de igualdad de punteros en su condición. La igualdad de punteros es mala y preferiría que no pudiéramos permitirla en CTFE.

EDITAR: No importa, me acabo de dar cuenta de que assert prueba el desplazamiento. Entonces, de hecho, nunca puede fallar en el tiempo CTFE porque si los punteros no están en el mismo bloque cuando se hace wrapping_sub , miri generará un error.

// guess we can't have this as const fn
fn is_eq<T>(a: &T, b: &T) -> bool {
    a as *const _ == b as *const _
}

como dije antes, use punteros virtuales en miri, en lugar de punteros reales. puede proporcionar determinismo constante en tiempo constante. si la función está escrita correctamente, el tiempo de ejecución y el comportamiento en tiempo de compilación deberían producir los mismos resultados independientemente de que el tiempo de ejecución no sea determinista mientras que el tiempo de compilación sí lo es. puede tener un comportamiento determinista en entornos no deterministas si lo codifica.

obtener un desplazamiento de campo es determinista. con punteros virtuales, permanece determinista. con punteros reales, sigue siendo determinista.

obtener la uniformidad del bit 14 de un puntero no es determinista. con punteros virtuales, se vuelve determinista. con punteros reales, no es determinista. esto está bien, porque uno ocurre en tiempo de compilación (un entorno determinista), mientras que el otro ocurre en tiempo de ejecución (un entorno no determinista).

const fn debe ser tan determinista como el entorno en el que se utilizan.

@SoniEx2

// guess we can't have this as const fn

De hecho no podemos. Creo que podría vivir con una versión de comparación de puntero sin procesar que arroja errores si alguno de los punteros no es actualmente desreferenciable en el sentido de estar dentro de un objeto asignado (pero eso no es lo que implementa actualmente "full miri"). Sin embargo, eso todavía haría que is_eq no sea "const safe" porque si T tiene un tamaño cero, podría apuntar uno más allá del final de un objeto, incluso si solo consideramos el código seguro.

C ++ permite comparar punteros uno más allá del final para producir un resultado indeterminado (piense: no determinista). Tanto C como C++ permiten comparar un puntero colgante para obtener un resultado indeterminado. No está claro qué garantiza LLVM, pero prefiero no apostar por garantías que excedan lo que tienen que garantizar para C/C++ (el más débil de los dos, si difieren). Este es un problema si queremos garantizar el determinismo en tiempo de ejecución para todo lo que se ejecuta con éxito en CTFE, lo cual creo que hacemos.

@RalfJung

La diferencia está en si la función incluso completa la ejecución.

Abogado del diablo: "devolver ⊥" en un caso es lo mismo que tener resultados diferentes.

No creo que sea difícil de explicar.

Personalmente lo veo como un comportamiento sorprendente; Puede explicarlo, y puede que lo entienda (pero no soy representativo...), pero no se ajusta a mi intuición.

@oli-obk

Entonces, ¿una función llamada con argumentos en tiempo de ejecución solo tiene pureza garantizada si realmente termina si se evalúa en tiempo de compilación con los mismos argumentos?

Personalmente, no encuentro esta garantía suficiente. Creo que primero deberíamos ver hasta dónde podemos llegar con la pureza y solo cuando sepamos que es paralizante en la práctica deberíamos avanzar hacia garantías más débiles.

@RalfJung

eso proporcionaría una garantía de que una const fn segura será determinista en tiempo de ejecución, porque coincide con la ejecución exitosa de CTFE.

OK; Me perdiste; No veo cómo llegaste a esta garantía de "safe const fn is deterministic" dadas las dos premisas; ¿podrías profundizar en el razonamiento?

Si esta es una garantía que nos importa, no me parece descabellado hacer que el sistema de tipos sea más estricto al verificar las funciones de CTFE; después de todo, queremos usarlo para hacer más cosas (comprobando "seguridad constante"). Creo que tendría mucho sentido, entonces, declarar que las conversiones de ptr a int no son seguras en el contexto const, argumentando que esto es necesario porque el contexto const ofrece garantías adicionales.

Al igual que nuestra "seguridad en tiempo de ejecución" normal puede ser subvertida por un código inseguro, también lo puede ser "seguridad constante", y eso está bien. No veo ningún problema con el código inseguro que aún puede hacer conversiones de ptr a int a través de uniones; después de todo, ese es un código inseguro. En este mundo, las obligaciones de prueba para código inseguro en un contexto constante son más fuertes que las de un contexto no constante; si su función devuelve un número entero, debe probar que siempre será un PrimVal::Bytes y nunca un PrimVal::Ptr .

¡Estos párrafos son música para mis oídos! ❤️ ¿Esto parece garantizar el determinismo ("pureza")? y es precisamente lo que tenía en mente antes. ¡Creo que la seguridad constante también es un término excelente!

Para futuras referencias, permítanme llamar a esta garantía "solidez de CTFE": si CTFE no falla, entonces su comportamiento coincide con el tiempo de ejecución: ambos divergen o ambos terminan con el mismo valor. (Estoy ignorando por completo los valores de retorno de orden superior aquí).

@centril

Abogado del diablo: "devolver ⊥" en un caso es lo mismo que tener resultados diferentes.

Bueno, eso es claramente una cuestión de definición. Creo que entendiste la garantía de solidez de CTFE que estaba describiendo y estás de acuerdo en que es una garantía que queremos; si es todo lo que queremos está en discusión :)

OK; Me perdiste; No veo cómo llegaste a esta garantía de "safe const fn is deterministic" dadas las dos premisas; ¿podrías profundizar en el razonamiento?

Digamos que tenemos una llamada a foo(x) donde foo es una función constante segura y x es un valor válido constante (es decir, no es &y as *const _ as usize ). Entonces sabemos que foo(x) se ejecutará en CTFE sin generar un error, por seguridad constante. Como consecuencia, por la solidez de CTFE, en tiempo de ejecución foo(x) se comportará de la misma manera que lo hizo en CTFE.

Esencialmente, creo que descompuse su garantía en dos partes: una que garantiza que una const fn segura nunca intentará hacer algo que CTFE no admita (como leer desde stdin o determinar si el byte menos significativo de un puntero es 0), y otro que garantiza que cualquier cosa que CTFE admita coincida con el tiempo de ejecución.

¡Estos párrafos son música para mis oídos! corazón ¿Esto parece garantizar el determinismo ("pureza")? y es precisamente lo que tenía en mente antes. ¡Creo que la seguridad constante también es un término excelente!

Me alegro de que te guste. :) Esto significa que finalmente entiendo de lo que estamos hablando aquí. "pureza" puede significar tantas cosas diferentes que a menudo me siento un poco incómodo cuando se usa el término. Y el determinismo no es una condición suficiente para la seguridad de const, el criterio relevante es si la ejecución en CTFE genera un error. (Un ejemplo de una función determinista no segura de constantes es mi variante de su orcale multiplicado por 0. Esto no está bien incluso usando un código inseguro ya que miri generará un error al inspeccionar los bytes de un puntero, incluso si los bytes finalmente no importan. Es como si la operación que extrae el byte menos significativo de un puntero es "const-UB" y, por lo tanto, no está permitida incluso en código const inseguro).

sí, un puntero uno más allá del final de un elemento de matriz apuntaría al siguiente elemento de matriz, probablemente. ¿así que lo que? no es realmente no determinista? determinista en tiempo de compilación es todo lo que importa de todos modos. en lo que a mí respecta, la evaluación del tiempo de ejecución podría fallar por agotamiento de la pila.

@RalfJung

El costo de tal sistema sería que un const fn de orden superior seguro debe tomar un cierre const fn para poder garantizar que ejecutar el cierre no violará, en sí mismo, la "seguridad const". Ese es el precio de obtener las garantías adecuadas sobre const fn seguros.

Creo que esto se puede mitigar severamente para admitir la transformación de la mayoría del código existente mediante la introducción de ?const donde puede escribir funciones de orden superior cuyo resultado se puede vincular a un const si y solo si la función proporcionada es ?const fn(T) -> U y donde is_const(x : T) ; Así que tienes:

?const fn twice(fun: ?const fn(u8) -> u8) { fun(fun(42)) }

fn id_impure(x: u8) -> u8 { x }
const fn id_const(x: u8) -> u8 { x }
?const fn id_maybe_const(x: u8) -> u8 { x }

fn main() {
    let a = twice(id_impure); // OK!
    const b = twice(id_impure); // ERR!
    let c = twice(id_const); // OK!
    const d = twice(id_const); // OK!
    let e = twice(id_maybe_const); // OK!
    const f = twice(id_maybe_const); // OK!
}

Escribiré un RFC proponiendo algo en este sentido (y más) en una semana más o menos.

@Centril en este punto está desarrollando un sistema de efectos con polimorfismo de efectos. Sé que esa siempre fue tu agenda secreta (?), Solo dejarte saber que se está volviendo descaradamente obvio.^^

@RalfJung Ya revelé el secreto en https://github.com/rust-lang/rfcs/pull/2237 el año pasado pero tendré que reescribirlo ;)
Prácticamente de dominio público ahora ^,-

@SoniEx2

sí, un puntero uno más allá del final de un elemento de matriz apuntaría al siguiente elemento de matriz, probablemente. ¿así que lo que? no es realmente no determinista? determinista en tiempo de compilación es todo lo que importa de todos modos. en lo que a mí respecta, la evaluación del tiempo de ejecución podría fallar por agotamiento de la pila.

El problema está en situaciones como la siguiente (en C++):

int x[2];
int y; // let's assume y is put right after the end of x in the stack frame
if (&x[0] + 2 == &y) {
  // ...
}

Los compiladores de C quieren (¡y lo hacen!) optimizar esa comparación a false . Después de todo, un puntero apunta a x y otro a y , por lo que es imposible que sean iguales.
Excepto, por supuesto, que las direcciones son iguales en la máquina porque un puntero apunta justo al final de x , que es la misma dirección que (el comienzo de) y . Por lo tanto, si oscurece el código lo suficiente como para que el compilador ya no vea de dónde provienen las direcciones, puede decir que la comparación se evalúa como true . Por lo tanto, el estándar C++ permite que ambos resultados ocurran de forma no determinista, lo que justifica tanto la optimización (que dice false ) como la compilación para ensamblar (que dice true ). El estándar C no permite esto, lo que hace que los compiladores no cumplan con LLVM (y GCC), ya que ambos realizarán este tipo de optimizaciones.

Escribí un resumen de mis ideas de seguridad constante, solidez constante, etc. que han surgido aquí en este hilo y/o en una discusión relacionada en IRC: https://www.ralfj.de/blog/2018/07/19/ const.html

Este tema aquí se ha vuelto algo difícil de desentrañar porque se han discutido muchas cosas. @oli-obk ayudó a crear un repositorio para inquietudes de const-eval, por lo que un buen lugar para discutir subtemas específicos es probablemente el rastreador de problemas de https://github.com/rust-rfcs/const-eval.

@Centril sugirió estabilizar una versión mínima que sea compatible con futuras extensiones:

  • sin argumentos genéricos con límites de rasgos
  • sin argumentos ni tipos de retorno de puntero fn o tipo dyn Trait

    • verificado recursivamente en el tipo de argumento, por lo que los campos de argumentos tampoco pueden ser de estos tipos

  • sin código inseguro (porque no sabemos si hay cosas allí que sean problemáticas)

    • Personalmente, creo que está bien, excepto por union s que ya están detrás de una puerta de función adicional y desrefs de puntero sin procesar (generalmente prohibido en cualquier constante en este momento). Cualquier otro código inseguro debe pasar por otras constantes inseguras o constantes intrínsecas, que requieren su propia discusión y estabilización.

(nit: mi sugerencia también incluía una verificación recursiva para punteros fn o dyn Trait en el tipo de retorno de const fn s)

sin argumentos genéricos con límites de rasgos

Para aclarar, ¿se aceptaría o no algo así?

struct Mutex<T> where T: Send { /* .. */ }

impl<T> Mutex<T> where T: Send {
    pub const fn new(val: T) -> Self { /* .. */ }
}

El límite no es parte del const fn en sí mismo.

También para aclarar: ¿la propuesta es estabilizar esas cosas y dejar el resto detrás de la puerta de características O estabilizarlas y hacer que el resto sea un error por completo?

@mark-im, el resto se quedaría detrás de una puerta de características.

@oli-obk ¿cuál es el problema con el código inseguro? Permitimos que no sea seguro en const X : Ty = ... , que tiene los mismos problemas. Creo que los cuerpos const fn deben verificarse exactamente como los cuerpos const .

Creo que queremos seguir siendo conservadores en torno a las operaciones "no constantes", operaciones que son seguras pero no constantes (básicamente cualquier cosa en punteros sin procesar), pero esas ya están completamente prohibidas en contexto constante, ¿verdad?

El límite no es parte de la const fn en sí.

No, los límites en el bloque impl tampoco estarían permitidos bajo esa propuesta

¿Cuál es el problema con el código no seguro?

No veo ningún problema como se indica en mi comentario. Cada función/característica insegura constante necesita pasar por su propia estabilización de todos modos.

@RalfJung Creo que el problema es " @Centril está nervioso porque nos perdimos algo malo. Esos ya están completamente prohibidos en contexto constante". ;) Pero tenemos que estabilizar unsafe { .. } en const fn en algún momento, así que si está seguro de que no hay problemas y de que detectamos todas las operaciones no constantes, ¿vamos a hacerlo?

Además, si nos perdimos algo, ya estamos un poco jodidos porque la gente puede usarlo en const .

Todavía planeo escribir un PR completando las partes de seguridad/promoción const del repositorio const fn RFC ; ese sería el momento de verificar cuidadosamente si cubrimos todo (y tenemos casos de prueba).

Otra cosa que surgió en Discord son las operaciones de FP: actualmente no podemos garantizar que coincidan con el hardware real. CTFE seguirá exactamente IEEE, pero es posible que LLVM/hardware no.

Esto también se aplica a los elementos const , pero nunca se ejecutarán en tiempo de ejecución, mientras que const fn podría serlo. Por lo tanto, parece prudente no estabilizar las operaciones de FP en const fn .

OTOH, ¿ya promovemos resultados de operaciones de FP? Así que ahí ya tenemos ese desajuste entre el tiempo de ejecución y el tiempo de compilación observable en la versión estable. ¿Valdría la pena recorrer el cráter para ver si podemos deshacer eso?

Para referencia futura, el siguiente artículo es relevante con respecto a los puntos flotantes y el determinismo:

@RalfJung

¿Valdría la pena recorrer el cráter para ver si podemos deshacer eso?

Me sorprendería si pudiéramos hacer esto, pero al menos vale la pena intentarlo. :)

@RalfJung puede haber una entrada interesante más adelante en este hilo https://github.com/rust-lang/rust/issues/24111#issuecomment -386764565

Suponiendo que queremos mantener la forma de palabra clave const fn , creo que estabilizar algo ahora , que es lo suficientemente limitado, es una solución bastante decente (¡¿cómo no lo vi antes?!)

Cuando hacemos estas estabilizaciones por partes, solo quiero registrar un
solicitud de una lista clara de las restricciones y sus justificaciones. Es
va a ser frustrante cuando los usuarios hagan un cambio aparentemente inocuo y
nos encontramos con un error de compilación, por lo que al menos podemos corregir los errores. I
reconocer que el razonamiento se extiende a través de muchas discusiones, no todas las cuales
Lo he seguido, pero creo que debería haber una tabla en los documentos (o en el
referencia, o el nomicon, al menos) enumerando cada operación no permitida, el
problema que podría causar, y las perspectivas de estabilización (por ejemplo, "nunca",
"si se implementa RFC XYZ", "después de concretar esta parte de la especificación").

El lunes 20 de agosto de 2018 a las 13:44 Eduard-Mihai Burtescu <
[email protected]> escribió:

Suponiendo que queremos mantener la forma de palabra clave const fn, creo que estabilizar
algo ahora , que es lo suficientemente limitado, es una solución bastante decente (cómo
¡¿No lo vi antes?!)


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment-414403036 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAC3n80yVxIa3agsJP4wXZFkgyHVmAsjks5uSvWXgaJpZM4D66IA
.

@est31 Como @rkruppe ya escribió, esos fusibles serían ilegales sin -ffast-math , y creo que LLVM lo maneja correctamente.

Por lo que recuerdo, la aritmética básica ni siquiera es tan problemática (excepto en x86 de 32 bits porque x87...), pero las funciones trascendentales sí lo son. Y esos no son const fn , ¿verdad? Así que espero que al final podamos tener paridad entre const artículos, promoción y const fn en este sentido también.

@RalfJung Todavía no estoy convencido de que, por ejemplo, derramar y luego volver a cargarlo en los registros de FPU entre algunas operaciones dé los mismos resultados que el mismo cálculo sin ese derrame.

Por lo que recuerdo, la aritmética básica ni siquiera es tan problemática (excepto en x86 de 32 bits porque x87...), pero las funciones trascendentales sí lo son.

¿Cómo significarían un problema las funciones trascendentales?

IIRC, si bien algunas funciones trascendentales son compatibles con los procesadores x86 comunes, esas funciones son lentas y evitadas, y solo se incluyen para completar y compatibilidad con las implementaciones existentes. Así, casi en todas partes, las funciones trascendentales se expresan en términos de combinaciones de funciones aritméticas básicas. Esto significa que no hay diferencia en su transparencia referencial a la de las funciones aritméticas. Si las funciones básicas son "seguras", entonces cualquier cosa construida sobre ellas lo es, incluidas las funciones trascendentales. La única fuente de "falta de transparencia referencial" aquí podría ser diferentes aproximaciones (implementaciones) de esas funciones trascendentales en términos de esas funciones aritméticas básicas. ¿Es esa la fuente del problema?

@ est31 Si bien la mayoría de las funciones trascendentales son, en última instancia, solo código de biblioteca compuesto por operaciones primitivas de números enteros y flotantes, estas implementaciones no están estandarizadas y, en la práctica, un programa Rust puede interactuar con quizás tres implementaciones diferentes a lo largo de su vida útil, algunas de las cuales también varían según el host o el objetivo plataforma:

  • Existe la implementación de tiempo de ejecución que el programa usa en el destino (por ejemplo, la plataforma de destino libm o las instrucciones de hardware en algunas circunstancias)
  • Está la carpeta constante que usa LLVM (aparentemente, esta es solo la plataforma host C libm)
  • Hay cualquier cosa que MIRI haría con estas operaciones (por ejemplo, algo en rustc_apfloat , o interpretar una implementación de Rust como https://github.com/japaric/libm/)

Si alguno de estos no está de acuerdo entre sí, puede obtener diferentes resultados dependiendo de cuándo se evalúe una expresión.

@rfcbot cancelar

Es poco probable que estabilicemos el monto completo en un futuro cercano.
En su lugar, me gustaría desarrollar un consenso para un subconjunto más mínimo (como se describe aproximadamente en https://github.com/rust-lang/rust/issues/24111#issuecomment-414310119) que, con suerte, podemos estabilizar a corto plazo. .
Este subconjunto se rastrea en #53555. Más descripción está disponible allí.

Propuesta de @Centril cancelada.

@rkruppe , ¿hay alguna razón para reducir las funciones trascendentales a los intrínsecos de llvm? ¿No podemos simplemente evitar todo el problema reduciéndolos a implementaciones bien conocidas, solo oxidadas, que controlamos y que son las mismas en todas las plataformas?

¿hay alguna razón para bajar las funciones trascendentales a llvm intrínsecas?

Además de la simplicidad de no implementar una libm multiplataforma completa, los intrínsecos tienen una ventaja en el optimizador y el generador de código de LLVM que una función de biblioteca común no obtendrá. Obviamente, el plegado constante (y cosas relacionadas como el análisis del rango de valores) es un problema en este contexto, pero es bastante útil de lo contrario. También hay identidades algebraicas (aplicadas por el pase SimplifyLibCalls). Finalmente, algunas funciones (principalmente sqrt y su recíproco, que no son trascendentales pero lo que sea) tienen soporte de generación de código especial para, por ejemplo, generar sqrtss en x86 con SSE.

¿No podemos simplemente evitar todo el problema reduciéndolos a implementaciones bien conocidas, solo oxidadas, que controlamos y que son las mismas en todas las plataformas?

Incluso ignorando todo lo anterior, esta no es una gran opción en mi opinión. Si la plataforma de destino tiene una biblioteca C, debería ser posible usarla, ya sea porque está más optimizada o para evitar la sobrecarga.

los intrínsecos tienen una ventaja en el optimizador y la generación de código de LLVM que una función de biblioteca ordinaria no obtendrá.

¿Pensé que tales optimizaciones solo se habilitan si se activan las matemáticas rápidas?

Si la plataforma de destino tiene una biblioteca C, debería ser posible usarla, ya sea porque está más optimizada o para evitar la sobrecarga.

Por supuesto. Siempre debería haber una opción para intercambiar esta propiedad bastante académica, la transparencia referencial, a favor de cosas que importan más, como una velocidad mejorada del binario compilado o binarios más pequeños. Por ejemplo, usando la plataforma libm o activando el modo matemático rápido. ¿O sugiere que deberíamos prohibir el modo matemático rápido por toda la eternidad?

En mi opinión, no deberíamos prohibir f32::sin() en contextos constantes, al menos no si permitimos + , - etc. Tal prohibición obligará a las personas a crear y usar cajas. que proporcionan implementaciones compatibles con const.

¿Pensé que tales optimizaciones solo se habilitan si se activan las matemáticas rápidas?

La evaluación constante de estas funciones y la emisión de sqrtss es fácil de justificar sin -ffast-math, ya que se puede redondear correctamente.

¿O sugiere que deberíamos prohibir el modo matemático rápido por toda la eternidad?

No estoy sugiriendo nada, ni tengo una opinión (atm) sobre si tal propiedad debe garantizarse. Simplemente estoy informando limitaciones.

No se pudo encontrar un problema abierto para ICE causado por Vec en el contexto de const fn.

https://play.rust-lang.org/?version=nightly&mode=debug&edition=2015&gist=508238a9f06fd85720307bf6cc227586

¿Debería abrir un nuevo número para esto?

@Voultapher, sí, parece un nuevo ICE.

Bien abierto #55063.

Si el compilador puede comprobar si se puede llamar a una función en una constexpr en tiempo de compilación cuando un usuario la anota con const fn , ¿por qué no realizar automáticamente la comprobación de todas las funciones? (similar a los rasgos automáticos)? No puedo pensar en ningún efecto adverso real, y el claro beneficio es que no tenemos que depender del juicio humano propenso a errores.

El principal inconveniente es que se convierte en un detalle público, por lo que una implementación
el cambio en una función que no está destinada a ser constante ahora se está rompiendo.

Además, no necesitamos confiar en el juicio humano. Podemos tener una pelusa recortada que nos diga cuándo una función sin anotaciones podría ser const fn : https://github.com/rust-lang/rust-clippy/issues/2440

Esto es similar a cómo no inferimos la mutabilidad de las variables locales, sino que el compilador nos dice dónde agregar o eliminar mut .

@remexre const fn actúa como una especificación de interfaz. No estoy muy familiarizado con los pequeños detalles de esta característica (y tal vez lo que sigue aquí ya esté pensado), pero dos casos en los que puedo pensar en el compilador que dice cuándo una función está anotada incorrectamente como const es para fallar la compilación si dicha función toma un &mut como parámetro o si llama a otras funciones que no sean const . Entonces, si cambia la implementación de un const fn y rompe esas restricciones, el compilador lo detendrá. Luego, puede optar por implementar los bits que no son const en (una) función(es) separada(s) o interrumpir la API si se trata de un cambio previsto.

Hay otro punto medio que no he visto discutido y es la posibilidad de introducir un opuesto de este marcador y algún tipo de "inferencia de pureza de función" cuando no está establecido explícitamente. Luego, los documentos mostrarían el marcador real pero con algún tipo de advertencia sobre no garantizar la estabilidad de ese marcador si es un const . El problema es que esto podría fomentar la pereza y hacer esto casi siempre, lo cual no es su propósito.

¿debería un const fn ser capaz de producir una salida? ¿Por qué se debe rechazar &mut ?

@aledomu Mi comentario fue dirigido a @AGaussman; Estoy hablando del caso en el que el autor de una biblioteca expone una función que no está "destinada a ser" constante (en el sentido de que la constante no está destinada a ser parte de la API); si se infiriera const, sería un cambio radical hacer que dicha función no sea const.

@SoniEx2 const fn es una función que se puede evaluar en tiempo de compilación, lo que sucede solo en el caso de cualquier función pura.

@remexre Si no está destinado a ser una parte estable de la API, simplemente no lo marque.

Para el bit de inferencia que comenté, es por eso que mencioné la necesidad de alguna advertencia en los documentos del cajón.

¿cual es la diferencia? ¡absolutamente ninguno!

const fn add_1(x: &mut i32) { x += 1; }
let mut x = 0;
add_1(&mut x);
assert_eq!(x, 1);
x = 0;
add_1(&mut x);
assert_eq!(x, 1);

const fn added_1(x: i32) -> i32 { x + 1 }
let mut x = 0;
x = added_1(x);
assert_eq!(x, 1);
x = 0;
x = added_1(x);
assert_eq!(x, 1);

He presentado problemas específicos para:

Ya existen los siguientes problemas específicos:

Si hay otras áreas, que aún no han sido rastreadas por otros problemas, deben discutirse por escrito. const eval y const fn , sugiero que la gente haga nuevos problemas (y cc me + @oli-obk en ellos).

Esto concluye la utilidad de este problema, que por la presente se cierra.

No tengo todos los detalles en mente, pero ¿no hay mucho más compatible con miri pero que aún no está habilitado en min_const_fn ? Por ejemplo, punteros en bruto.

@SimonSapin Sí, buena captura. Hay algunos problemas más existentes para eso. He actualizado el comentario + la descripción del problema. Si hay algo que no está cubierto con lo que te encuentras, haz nuevos problemas, por favor.

Creo que no es apropiado cerrar un problema de seguimiento de meta cuando no está del todo claro que lo que cubre está cubierto exhaustivamente por problemas más específicos.

Cuando elimino #![feature(const_fn)] en Servo, los mensajes de error son:

  • trait bounds other than `Sized` on const fn parameters are unstable
  • function pointers in const fn are unstable

(Estos const fn son todos constructores triviales para tipos con campos privados. El mensaje anterior está en el constructor de struct Guard<T: Clone + Copy> , aunque Clone no se usa en el constructor. El el último es para inicializar Option<fn()> (simplificado) a None o Some(name_of_a_function_item) .)

Sin embargo, ni los rasgos ni los tipos de punteros de función se mencionan en la descripción de este problema.

No quiero decir que debamos tener solo dos problemas más específicos para lo anterior. Quiero decir que deberíamos reabrir este hasta que de alguna manera nos aseguremos de que todo lo que está detrás de la puerta de características const_fn (que todavía apunta aquí en los mensajes de error) tenga un problema de seguimiento. O hasta que const_fn se estabilice por completo.

@SimonSapin

Creo que no es apropiado cerrar un problema de seguimiento de meta cuando no está del todo claro que lo que cubre está cubierto exhaustivamente por problemas más específicos.

Este problema tiene el sabor de https://github.com/rust-lang/rust/issues/34511 , que es uno de los mayores líos en lo que respecta a los problemas de seguimiento. Este problema también ha sido un problema general durante algún tiempo, por lo que no actúa como un meta-problema en este momento. Para tales juegos gratuitos, utilice http://internals.rust-lang.org/ en su lugar.

Sin embargo, ni los rasgos ni los tipos de punteros de función se mencionan en la descripción de este problema.

No quiero decir que debamos tener solo dos problemas más específicos para lo anterior.

Eso es exactamente lo que creo que se debe hacer. Desde la perspectiva del triaje de T-Lang, es favorable tener problemas específicos y procesables .

Quiero decir que deberíamos reabrir este hasta que de alguna manera nos aseguremos de que todo lo que está detrás de la puerta de características const_fn (que todavía apunta aquí en los mensajes de error) tenga un problema de seguimiento. O hasta que const_fn se estabilice por completo.

Ni siquiera me queda claro qué const_fn constituye la puerta de características o si todo se estabilizará en algún momento. Todo, además de los límites y los punteros de función del RFC original, tiene problemas abiertos y algo más.

Ni siquiera me queda claro qué constituye const_fn la puerta de características

Es exactamente por eso que no deberíamos cerrarlo hasta que lo averigüemos, en mi opinión.

Todo

¿Es realmente todo, sin embargo?

¿Alguien sabe qué pasó con la función const_string_new ? ¿Hay un problema de seguimiento para ello? El libro inestable solo enlaza aquí.

@phansch Eso es porque todos los rustc_const_unstable apuntan aquí. (cc @oli-obk ¿podemos arreglar eso?)

El tema debería estar abierto entonces. Es insultante como usuario ser señalado.
a un asunto cerrado.

El miércoles 9 de enero de 2019 a las 04:05 Mazdak Farrokhzad < [email protected]
escribió:

@phansch https://github.com/phansch Eso es porque todos
rustc_const_unstable punto aquí.


Estás recibiendo esto porque te mencionaron.
Responda a este correo electrónico directamente, véalo en GitHub
https://github.com/rust-lang/rust/issues/24111#issuecomment-452622097 ,
o silenciar el hilo
https://github.com/notifications/unsubscribe-auth/AAC3n7JhzsZZpizmWlp0Nww5bcfIqH2Vks5vBbC8gaJpZM4D66IA
.

@durka : Siempre habrá una ventana posible en la que algo se cierra todas las noches y la resolución aún no ha llegado a ser estable. ¿Cómo es eso insultante?

Me he estado resistiendo a comentar aquí, y tal vez deberíamos mover esta conversación a un hilo sobre aspectos internos (¿ya hay uno?), pero...

La decisión de cerrar esto no tiene sentido para mí. Es un problema de seguimiento, porque aparece en los mensajes de error del compilador, y no es el único; consulte esta publicación para ver más ejemplos: https://internals.rust-lang.org/t/psa-tracking-for-gated -idioma-características/2887. Cerrar este tema para mí implica estabilidad, lo que obviamente aún no es el caso.

Francamente, no puedo ver un argumento para cerrar esto... Me alegra que ahora exista un problema más específico, por lo que la implementación puede avanzar, con suerte con una nueva discusión y enfoque, pero no veo una forma clara de asociar el compilador. mensajes con esos.

Nuevamente, si esto necesita (o ya tiene) un hilo sobre aspectos internos, ¿quizás pasemos esta conversación allí?

EDITAR: ¿O el problema es solo que el libro está desactualizado? Probar el ejemplo del RFC (faltan un par #[derive(...)] s) parece funcionar sin errores en Rust rustc 1.31.1. ¿Todavía hay un mensaje de error del compilador que apunta aquí? Sería bueno tener un lugar para vincular errores como:

error: only int, `bool` and `char` operations are stable in const fn

Si queremos tenerlos vinculados a los problemas específicos, posiblemente sería una mejora.

Ok, entonces aquí debería haber una fuerte evidencia de que este problema permanece abierto. Por lo que puedo decir esto:

https://github.com/rust-lang/rust/blob/6ecad338381cc3b8d56e2df22e5971a598eddd6c/src/libsyntax/feature_gate.rs#L194

es la única función active que apunta a un problema cerrado.

En un mundo ideal, creo que este tipo de discusiones realmente deberían automatizarse, ya que, como hemos descubierto, las personas tienen diferentes opiniones e ideas sobre cómo deberían funcionar las cosas. Pero esa no es realmente una conversación para este hilo...

Si queremos tenerlos vinculados a los problemas específicos, posiblemente sería una mejora.

Sí, esta es la solución correcta y lo que @Centril ya sugirió .

El comentario inicial también se ha editado para redirigir a las personas a los problemas específicos que llegan aquí en la "ventana" que menciona @ErichDonGubler .

https://github.com/rust-lang/rust/issues/57563 ahora se ha abierto para rastrear las características constantes inestables restantes.

Entonces, ¿alguien podría editar el cuerpo del problema aquí para vincularlo de manera destacada a # 57563?

@glaebhoerl hecho :)

Hola, llegué aquí porque obtuve error[E0658]: const fn is unstable (see issue #24111) al compilar ncurses-rs. ¿Qué tengo que hacer? ¿Actualizar el óxido? Tengo

$ cargo version
cargo 1.27.0
$ rustc --version
rustc 1.27.2

EDITAR: hizo brew uninstall rust y siguió las instrucciones de instalación de rustup , ahora rustc --version es rustc 1.33.0 (2aa4c46cf 2019-02-28) y ese error desapareció.

Sí, para poder usar const fn en la versión estable, deberá actualizar su compilador.

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