Rust: Rastreamento de problema para RFC 2342, "Permitir` if` e `match` em constantes"

Criado em 18 mar. 2018  ·  83Comentários  ·  Fonte: rust-lang/rust

Este é um problema de rastreamento para o RFC "Permitir if e match em constantes" (rust-lang / rfcs # 2342).

Redirecione a constificação de funções ou problemas específicos que deseja relatar para novos problemas e rotule-os apropriadamente com F-const_if_match para que esses problemas não sejam inundados com comentários efêmeros que obscurecem desenvolvimentos importantes.

Degraus:

  • [x] Implementar o RFC
  • [] Ajustar a documentação ( ver instruções sobre forja )
  • [x] Estabilização PR ( ver instruções sobre forja )
  • [x] let ligações em constantes que usam && e || operações de curto-circuito. Estes são tratados como & e | dentro de const e static itens agora.

Perguntas não resolvidas:

Nenhum

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

Comentários muito úteis

Agora que # 64470 e # 63812 foram mesclados, todas as ferramentas necessárias para isso existem no compilador. Ainda preciso fazer algumas alterações no sistema de consulta em torno da qualificação const para me certificar de que não seja desnecessariamente ineficiente com esse recurso habilitado. Estamos progredindo aqui e acredito que uma implementação experimental disso estará disponível todas as noites em semanas, não meses (últimas palavras famosas: sorria :).

Todos 83 comentários

  1. adicione um portão de recursos para isso
  2. switch e switchInt terminadores em https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L347 precisam ter um código personalizado em caso o portão de recursos esteja ativo
  3. em vez de ter um único bloco básico atual (https://github.com/rust-lang/rust/blob/master/src/librustc_mir/transform/qualify_consts.rs#L328), precisa ser algum contêiner que tenha uma lista de blocos básicos que ainda precisam ser processados.

@oli-obk É um pouco mais complicado porque o fluxo de controle complexo significa que a análise de fluxo de dados precisa ser empregada. Preciso voltar ao @alexreg e descobrir como integrar suas alterações.

@eddyb Um bom ponto de partida provavelmente seria pegar meu branch const-qualif (sem o commit principal), rebase-o sobre o master (não vai ser divertido) e, em seguida, adicionar coisas de anotação de dados, certo?

Alguma novidade sobre isso?

@ mark-im Alas, não. Acho que @eddyb tem estado muito ocupado, porque nem mesmo consegui pingá-lo no IRC nas últimas semanas hah. Infelizmente, meu branch const-qualif nem mesmo compila desde a última vez que fiz um rebase sobre o master. (Eu não acredito que tenha pressionado ainda).

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

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

Ok, curiosamente, eu rebasei novamente apenas hoje e parece estar construindo tudo bem agora! Parece que houve uma regressão e ela acabou de ser corrigida. Tudo para @eddyb agora.

@alexreg Desculpe, mudei para um horário de sono local e vejo que você me deu um ping quando acordo, mas fica offline o dia todo quando estou acordado (ugh fusos horários).
Devo apenas fazer um PR do seu ramo? Esqueci o que deveríamos fazer com isso?

@eddyb Tudo bem heh. Você deve ir para a cama cedo, já que geralmente fico acordado a partir das 20h GMT, mas está tudo bem! :-)

Sinto muito, demorei um pouco para perceber que a série de patches em questão requer a remoção de Qualif::STATIC{,_REF} , ou seja, os erros de acesso à estática em tempo de compilação. OTOH, isso já está quebrado em termos de const fn s e acesso a static s:

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

Isso não é detectado estaticamente, em vez disso miri reclama que "ponteiro pendente foi desreferenciado" (o que deveria realmente dizer algo sobre static s em vez de "ponteiro pendente").

Então eu acho que ler static s em tempo de compilação deve ser bom, mas algumas pessoas querem que const fn seja "puro" (ou seja, "referencialmente transparente" ou próximo) em tempo de execução, o que significaria que uma leitura de const fn por trás de uma referência que obteve como argumento está bem, mas um const fn nunca deve ser capaz de obter uma referência a static do nada (incluindo de const s).

Acho que podemos continuar negando estaticamente a menção de static s (mesmo que apenas para tomar sua referência) em const s, const fn e outros contextos constantes (incluindo promovidos).
Mas ainda temos que remover o hack STATIC_REF que permite a static s tomar a referência de outros static s, mas (mal tenta e não consegue) negar a leitura por trás dessas referências .

Precisamos de um RFC para isso?

Parece razoável ler a estática. Duvido que seja necessário um RFC, talvez apenas uma corrida na cratera, mas provavelmente não sou o melhor para dizer.

Observe que não estaríamos restringindo nada, estaríamos relaxando uma restrição que já foi quebrada.

Oh, eu entendi mal. Portanto, a avaliação const ainda seria válida, apenas não referencialmente transparente?

O último parágrafo descreve uma abordagem referencialmente transparente (mas perderemos essa propriedade se começarmos a permitir a menção de static s em const s e const fn s). Não acho que a solidez esteja realmente em discussão.

Bem, "ponteiro pendente" com certeza soa como um problema de solidez, mas vou confiar em você nisso!

"dangling pointer" é uma mensagem de erro ruim, que é apenas miri proibindo a leitura de static s. Os únicos contextos constantes que podem até mesmo se referir a static s são outros static s, então poderíamos "apenas" permitir essas leituras, já que todo aquele código sempre roda uma vez, em tempo de compilação.

(do IRC) Para resumir, const fn referencialmente transparente só poderia alcançar alocações congeladas, sem passar por argumentos, o que significa que const precisa da mesma restrição, e alocações não congeladas só podem vir de static s.

Eu gosto de preservar a transparência referencial, então a ideia de @eddyb parece fantástica!

Sim, eu também sou profissional em fazer const fns puro.

Observe que certos planos aparentemente inofensivos podem arruinar a transparência referencial, por exemplo:

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

Isso iria falhar com um erro miri em tempo de compilação, mas seria não determinístico em tempo de execução (porque não podemos marcar esse endereço de memória como "abstrato" como o miri pode).

EDITAR : @Centril teve a ideia de fazer certas operações de ponteiro bruto (como comparações e conversões para inteiros) unsafe dentro de const fn (o que podemos fazer até estabilizar const fn ), e afirmam que eles podem ser usados ​​de maneiras que miri permitiria em tempo de compilação.
Por exemplo, subtrair dois ponteiros no mesmo local deve ser adequado (você obtém uma distância relativa que depende apenas do layout do tipo, índices de array, etc.), mas formatar o endereço de uma referência (via {:p} ) é um uso incorreto e, portanto, fmt::Pointer::fmt não pode ser marcado const fn .
Além disso, nenhum dos impls de traço Ord / Eq para ponteiros brutos pode ser marcado como const (sempre que conseguirmos anotá-los como tal), porque eles são seguros mas a operação é unsafe em const fn .

Depende do que você entende por "inofensivo" ... Eu certamente posso ver a razão de querermos banir tal comportamento não determinístico.

Seria fantástico se o trabalho fosse continuado.

@lachlansneff Está se movendo ... não tão rápido quanto gostaríamos, mas o trabalho está sendo feito. No momento, estamos esperando em https://github.com/rust-lang/rust/pull/51110 como um bloqueador.

@alexreg Ah, obrigado. Seria muito útil ser capaz de marcar uma correspondência ou se for constante mesmo quando não estiver em uma const fn.

alguma atualização de status agora que o # 51110 foi mesclado?

@programmerjake Estou esperando algum feedback de @eddyb em https://github.com/rust-lang/rust/pull/52518 antes que possa ser mesclado (espero que muito em breve). Ele tem estado muito ocupado ultimamente (sempre em alta demanda), mas ele voltou a fazer avaliações e outros enfeites nos últimos dias, então estou esperançoso. Depois disso, ele precisará de algum trabalho pessoal dele, eu suspeito, já que adicionar uma análise de fluxo de dados adequada é uma tarefa complicada. Veremos embora.

Em algum lugar nas listas TODO na (s) primeira (s) postagem (ões), deve ser adicionado para remover o hack horrível atual que traduz && e || para & e | dentro das constantes.

@RalfJung Isso não era parte da velha const avaliação, que acabou completamente agora que o MIRI CTFE está em vigor?

AFAIK fazemos essa tradução em algum lugar na redução HIR, porque temos o código em const_qualify que rejeita SwitchInt terminadores que, de outra forma, seriam gerados por || / && .

Além disso, outro ponto: @oli-obk disse em algum lugar (mas não consigo encontrar onde) que as condicionais são de alguma forma mais complicadas do que se poderia pensar ingenuamente ... isso era "apenas" sobre a análise de queda / mutabilidade interior?

isso foi "apenas" sobre a análise de queda / mutabilidade interior?

No momento, estou tentando esclarecer isso. Entrarei em contato com você quando tiver todas as informações

Qual é a situação disso? Precisa de mão de obra ou está bloqueado para resolver algum problema?

@ mark-im Está bloqueado na implementação de análise de fluxo de dados adequada para qualificação const. @eddyb é o que tem mais conhecimento nessa área e já havia trabalhado nisso. (Eu também, mas meio que estagnou ...) Se @eddyb ainda não tiver tempo, talvez @oli-obk ou @RalfJung possam resolver isso em algum momento em breve. :-)

58403 é um pequeno passo em direção à qualificação baseada em fluxo de dados.

@eddyb, você mencionou a preservação da transparência referencial em const fn , o que acho uma boa ideia. E se você evitasse o uso de ponteiros em const fn ? Portanto, seu exemplo de código anterior não compilaria mais:

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

Referências ainda seriam permitidas, mas você não teria permissão para introspectá-las:

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

Avise-me se eu estiver completamente errado. Achei que essa seria uma boa maneira de tornar isso determinístico.

@ jyn514 que já está coberta pela tomada como usize lança instável (https://github.com/rust-lang/rust/issues/51910), mas os usuários também podem comparar ponteiros crus (https://github.com/rust- lang / rust / issues / 53020) que é igualmente ruim e, portanto, também instável. Podemos lidar com isso independentemente do fluxo de controle.

Alguma novidade nisso?

Há alguma discussão em https://rust-lang.zulipchat.com/#narrow/stream/146212 -t-compiler.2Fconst-eval / topic / dataflow-based.20const.20qualification.20MVP

@oli-obk seu link não funciona. O que isso quer dizer?

Funciona para mim ... você tem que se registrar em Zulip, entretanto.

@alexreg hmm sim, presumo que tenha sido sobre o trabalho de qualificação const baseado em fluxo de dados. @alexreg você sabe por que é necessário para if e match em constantes?

se não tivermos uma versão baseada no fluxo de dados, permitimos acidentalmente &Cell<T> dentro das constantes ou proibimos acidentalmente None::<&Cell<T>> (que funciona no estável. É essencialmente impossível implementar corretamente sem fluxo de dados (ou qualquer implementação irá ser uma versão ad-hoc corrompida de fluxo de dados)

@ est31 Bem, @oli-obk entende isso muito melhor do que eu, mas de um alto nível, basicamente, qualquer coisa envolvendo ramificação irá predicar a análise de fluxo de dados, a menos que você queira um monte de casos extremos. De qualquer forma, parece que essa pessoa em Zulip está tentando trabalhar nisso, e se não eu sei que oli-obk e eddyb têm intenções de, talvez neste mês ou no próximo (desde a última vez que falei com eles sobre isso), embora eu possa Não farei promessas em nome deles.

@alexreg @ mark-im @ est31 @ oli-obk Devo ser capaz de publicar minha implementação WIP de qualificação const baseada em fluxo de dados ainda esta semana. Há muitos riscos de compatibilidade aqui, então pode demorar um pouco para realmente fazer a fusão.

Super; espero por isso.

(copiando de # 57563 por solicitação)

Seria possível criar um caso especial de bool && bool , bool || bool , etc.? Eles podem atualmente ser executados em const fn , mas fazer isso requer operadores bit a bit, o que às vezes é indesejado.

Eles já são casados ​​em const e static itens - traduzindo-os para operações bit a bit. Mas essa caixa especial é um grande hack e é muito difícil ter certeza de que isso está realmente correto. Como você disse, às vezes também é indesejado. Portanto, preferimos não fazer isso com mais frequência.

Fazer as coisas certas vai demorar um pouco, mas vai acontecer. Se acumularmos muitos hacks nesse meio tempo, podemos nos colocar em um canto do qual não podemos escapar (se alguns desses hacks acabarem interagindo de maneiras erradas, estabilizando acidentalmente um comportamento que não queremos).

Agora que # 64470 e # 63812 foram mesclados, todas as ferramentas necessárias para isso existem no compilador. Ainda preciso fazer algumas alterações no sistema de consulta em torno da qualificação const para me certificar de que não seja desnecessariamente ineficiente com esse recurso habilitado. Estamos progredindo aqui e acredito que uma implementação experimental disso estará disponível todas as noites em semanas, não meses (últimas palavras famosas: sorria :).

@ ecstatic-morse Bom saber! Obrigado por seus esforços concentrados para fazer isso; Eu, pessoalmente, estou interessado nesse recurso há algum tempo.

Adoraria ver o suporte de alocação de heap para CTFE depois que isso for feito. Não sei se você ou qualquer outra pessoa está interessada em trabalhar nisso, mas se não, talvez eu possa ajudar.

@alexreg Obrigado!

A discussão sobre a alocação de heap em tempo de compilação terminou em rust-rfcs / const-eval # 20. AFAIK, os desenvolvimentos mais recentes giraram em torno de um paradigma ConstSafe / ConstRefSafe para determinar o que pode ser observado diretamente / por trás de uma referência no valor final de const . Eu acho que há mais trabalho de design necessário.

Para aqueles que estão acompanhando, # 65949 (que depende de alguns PRs menores) é o próximo bloqueador para isso. Embora possa parecer apenas tangencialmente relacionado, o fato de que a verificação / qualificação const estava tão intimamente associada à promoção foi parte do motivo pelo qual esse recurso foi bloqueado por tanto tempo. Estou pensando em abrir um PR subsequente que removerá o verificador de const antigo inteiramente (atualmente executamos os dois verificadores em paralelo). Isso evitará as ineficiências que mencionei anteriormente.

Depois que ambos os PRs mencionados acima forem mesclados, if e match em constantes serão algumas melhorias de diagnóstico e um sinalizador de recurso de distância! Ah, e também testes, tantos testes ...

Se você precisar de testes, não sei como começar, mas estou mais do que disposto a contribuir! Apenas me diga para onde os testes devem ir / como eles devem ser / em que branch devo basear o código :)

O próximo PR para assistir é # 66385. Isso remove a velha lógica de qualificação const (que não conseguia lidar com ramificações) completamente em favor da nova versão baseada em fluxo de dados.

@ jyn514 Isso seria ótimo! Vou enviar um ping para você quando começar a esboçar a implementação. Também seria muito útil para as pessoas tentarem violar a segurança const (especialmente a parte HasMutInterior ), uma vez que if e match estão disponíveis todas as noites.

66507 contém uma implementação inicial do RFC 2342.

Espero que demore um pouco para remover as arestas, especialmente no que diz respeito aos diagnósticos, e a cobertura do teste é bem esparsa ( @ jyn514 , devemos nos coordenar nesse assunto). No entanto, estou esperançoso de que possamos lançar isso atrás de um sinalizador de recurso nas próximas semanas.

Isso foi implementado no # 66507 e agora pode ser usado nas últimas noites . Há também uma postagem no blog Inside Rust que detalha as operações recentemente disponíveis, bem como alguns problemas que você pode encontrar com a implementação existente em torno de tipos com mutabilidade interior ou um Drop impl personalizado.

Vá em frente e constifique!

Parece que a igualdade não é const ? Ou estou enganado:

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

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

error: aborting due to 2 previous errors

@ mark-im Isso realmente deve

Não tenho certeza se isso é intencional, mas tentar fazer a correspondência em um enum dá o erro

const fn com código inacessível não é estável

apesar do fato de que o enum é exaustivo e definido na mesma caixa.

@jhpratt você pode postar o código? Posso corresponder enums simples sem problemas: https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=585e9c2823afcb49c6682f69569c97ea

@jhpratt você pode postar o código? Posso combinar enums simples sem problemas:

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

Me bata por alguns segundos. Esse é um exemplo mínimo que demonstra meu caso exato.

@jhpratt Definitivamente não foi intencional. Você poderia abrir um problema?

Redirecione a constificação de funções ou problemas específicos que deseja relatar para novos problemas e rotule-os apropriadamente com F-const_if_match para que esses problemas não sejam inundados com comentários efêmeros obscurecendo desenvolvimentos importantes.

@Centril Não é uma coisa ruim colocar no comentário principal para que o seu não seja enterrado.

Atualização de status:

Isso está pronto para a estabilização de uma perspectiva de implementação, mas há a questão de saber se queremos manter o fluxo de dados baseado em valor que temos agora, em vez de um tipo (mas menos poderoso). O fluxo de dados baseado em valor é um pouco mais caro (mais sobre isso mais abaixo), e precisamos dele para funções como

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

que uma análise baseada em tipo rejeitaria, porque um Option<T> pode ter destruidores que agora tentariam ser executados e, portanto, poderiam executar código não constante.

Podemos recorrer a uma análise baseada em tipo no momento em que houver ramos, mas isso significaria que rejeitaríamos

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

o que provavelmente seria muito surpreendente para os usuários.

@ ecstatic-morse executou a análise em todas as funções, não apenas em const fn e viu lentidão em até 5% (https://perf.rust-lang.org/compare.html?start=93dc97a85381cc52eb872d27e50e4d518926a27c&end=51cf313c7946365d5becf37037037033946365d5becf21133950d511cf33946365d511cf511339a370339439ac). Observe que esta é uma versão pessimista, uma vez que significa que também é executada em funções que não se tornarão e nem sempre poderão se tornar const fn .

Isso significa que, se fizermos muitas funções const fn, podemos ver algumas lentidões na compilação devido a essa análise baseada em valor.

Um meio-termo poderia ser apenas executar a análise baseada em valor se a análise baseada em tipo falhar. Isso significa que, se não houver destruidores, não precisamos executar a análise baseada em valor para descobrir se os destruidores inexistentes não serão executados (sim, eu sei, muitas negações aqui). Em outras palavras: só executamos a análise baseada em valor se houver destruidores presentes.

Estou nomeando isso para discussão @ rust-lang / lang para que possamos descobrir se queremos ir com

  • a opção baseada em tipo na presença de loops ou ramos (dando comportamento estranho aos usuários)
  • análise baseada no valor total (mais cara, mas expressividade total para os usuários)
  • esquema misto, ainda com expressividade total para os usuários, alguma complexidade implícita extra, mas deve reduzir os problemas de tempo de compilação para os casos que precisam.

@ oli-obk

a opção baseada em tipo na presença de loops ou ramos (dando comportamento estranho aos usuários)

Só para verificar: não é uma opção ter uma análise baseada em tipo, mesmo em código linear? Imagino que seja incompatível com as versões anteriores, visto que já aceitamos o seguinte ( playground ):

struct Foo { }

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

const T: Option<Foo> = None;

fn main() { }

Pessoalmente, tendo a achar que devemos buscar uma experiência melhor e mais consistente para os usuários. Parece que podemos otimizar conforme necessário e, em qualquer caso, o custo não é tão ruim. Mas eu gostaria de entender um pouco melhor exatamente o que está acontecendo nesta análise mais cara: é a ideia de que estamos basicamente fazendo "propagação constante", de modo que sempre que algo é descartado, analisamos o valor exato que está sendo descartado para determinar se pode conter um valor que precisaria para executar um destruidor? (ou seja, se for None , para usar o exemplo comum de Option<T> )

Só para verificar: não é uma opção ter uma análise baseada em tipo, mesmo em código linear? Imagino que seja incompatível com as versões anteriores, visto que já aceitamos o seguinte (playground):

Sim, esse é o motivo pelo qual não podemos simplesmente mudar totalmente para a análise baseada em tipo.

é a ideia de que estamos basicamente fazendo "propagação constante", de modo que sempre que algo é descartado, analisamos o valor exato que está sendo descartado para determinar se ele pode conter um valor que precisaria para executar um destruidor? (ou seja, se for Nenhum, para usar o exemplo comum de Opção)

Estamos apenas propagando uma lista de sinalizadores ( Drop e Freeze , acabei de mostrar Drop aqui porque é mais fácil de explicar). Quando alcançamos um terminador Drop sem definir o sinalizador Drop , ignoramos o terminador Drop . Isso permite códigos como o seguinte:

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

Isso não acontece no momento da avaliação, então o seguinte não está certo:

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

Verificamos se todos os caminhos de execução possíveis não causam Drop .

A propagação constante é uma boa analogia. É outro problema de fluxo de dados cuja função de transferência não pode ser expressa com conjuntos gen / kill, que não lidam com a cópia de estado entre variáveis. No entanto, a propagação constante precisa armazenar o valor real de cada variável, mas a verificação de const precisa apenas armazenar um único bit indicando se essa variável tem um Drop impl personalizado ou não é Freeze fazendo isso um pouco menos caro do que a propagação constante seria.

Para ficar claro, o primeiro exemplo de @oli-obk compila no stable hoje, e desde 1.38.0 , que não incluía # 64470.

Além disso, const X: Option<Foo> = None; compila desde 1.0, todo o resto é apenas uma extensão natural com os novos recursos que const eval ganhou.

OK, eu acredito que faz sentido adotar a opção puramente baseada em valor então.

Acho que podemos cobrir isso na reunião e relatar de volta =)

Resumo

Proponho que estabilizemos #![feature(const_if_match)] com a semântica atual.

Especificamente, as expressões if e match , bem como os operadores lógicos de curto-circuito && e || se tornarão legais em todos os contextos const . Um contexto const é qualquer um dos seguintes:

  • O inicializador de um const , static , static mut ou discriminante enum.
  • O corpo de um const fn .
  • O valor de um gen const (apenas noturno).
  • O comprimento de um tipo de array ( [u8; 3] ) ou uma expressão de repetição de array ( [0u8; 3] ).

Além disso, os operadores lógicos de curto-circuito não serão mais reduzidos a seus equivalentes bit a bit ( & e | respectivamente) nos inicializadores const e static (ver # 57175). Como resultado, as ligações let podem ser usadas junto com a lógica de curto-circuito nesses inicializadores.

Problema de rastreamento: # 49146
Meta de versão: 1.45 (2020-06-16)

Histórico de Implementação

64470 implementou uma análise estática baseada em valor que suportava fluxo de controle condicional e era baseada em fluxo de dados. Isso, junto com o # 63812, nos permitiu substituir o antigo código de verificação de const por um que funcionava em gráficos de fluxo de controle complexos. O antigo verificador de const foi executado em paralelo com o baseado em fluxo de dados por um tempo para garantir que eles concordassem em programas com fluxo de controle simples. # 66385 removeu o verificador de const antigo em favor do baseado em fluxo de dados.

66507 implementou a porta de recursos #![feature(const_if_match)] com a semântica que agora está sendo proposta para estabilização.

Qualificação Const

Fundo

[Miri] desenvolveu avaliação de função em tempo de compilação (CTFE) em rustc por vários anos e foi capaz de avaliar instruções condicionais por pelo menos esse tempo. Durante o CTFE, devemos evitar certas operações, como chamar Drop impls personalizados ou fazer uma referência a um valor com mutabilidade interior . Coletivamente, essas propriedades desqualificadoras são conhecidas como "qualificações", e o processo de determinar se um valor possui uma qualificação em um ponto específico do programa é conhecido como "qualificação const".

Miri é perfeitamente capaz de emitir um erro ao encontrar uma operação ilegal em um valor qualificado e pode fazer isso sem falsos positivos. No entanto, o CTFE ocorre pós-monomorfização, o que significa que não pode saber se as constantes definidas em um contexto genérico são válidas até que sejam instanciadas, o que poderia acontecer em outra caixa. Para obter erros de pré-monomorfização, devemos implementar uma análise estática que faça a qualificação const. No caso geral, a qualificação const é indecidível (consulte o Teorema de Rice ), portanto, qualquer análise estática pode apenas aproximar as verificações que Miri realiza durante o CTFE.

Nossa análise estática deve proibir uma referência a um tipo com mutabilidade interior (por exemplo, &Cell<i32> ) de aparecer no valor final de const . Se isso fosse permitido, um const poderia ser modificado em tempo de execução.

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

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

No entanto, permitimos que o usuário defina um const cujo tipo tem mutabilidade interior ( !Freeze ), desde que possamos provar que o valor final desse const não tem. Por exemplo, o seguinte foi compilado desde a primeira edição da ferrugem estável :

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

Essa abordagem de análise estática, que chamarei de baseada em valor em oposição a baseada em tipo, também é usada para verificar se há código que pode resultar na chamada de Drop impl. Chamar Drop impls é problemático porque eles não são verificados por const e, portanto, podem conter código que não seria permitido em um contexto const. O raciocínio baseado em valor foi estendido para oferecer suporte a declarações let , o que significa que as seguintes compilações estão em ferrugem 1.42.0 estável .

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

Semântica noturna atual

O comportamento atual de #![feature(const_if_match)] estende a semântica baseada em valor para trabalhar em gráficos de fluxo de controle complexos usando fluxo de dados. Ou seja, tentamos comprovar que uma variável não possui a qualificação em questão ao longo de todos os caminhos possíveis no programa.

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

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

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

Todos os caminhos possíveis através do programa incluem aqueles que podem nunca ser alcançados na prática. Um exemplo, usando o mesmo Int enum acima:

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

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

Esta análise trata as chamadas de função como opacas, assumindo que seu valor de retorno pode conter qualquer valor de seu tipo. Também recorremos a uma análise baseada em tipo para uma variável assim que uma referência mutável a ela é criada. Observe que a criação de uma referência mutável em um contexto const é atualmente proibida em ferrugem estável.

#![feature(const_mut_refs)]

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

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

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

    let _ = &mut x;

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

Você pode ver mais exemplos de como uma análise baseada em valor é conservadora em relação à mutabilidade interna e impls de queda personalizados , bem como alguns casos em que uma análise conservadora é capaz de provar que nada de ilegal pode acontecer no conjunto de testes.

Alternativas

Achei difícil encontrar alternativas práticas e compatíveis com as versões anteriores para a abordagem existente. Poderíamos recorrer à análise baseada em tipo para todas as variáveis ​​assim que as condicionais fossem usadas em um contexto const. No entanto, isso também seria difícil de explicar aos usuários, uma vez que adições aparentemente não relacionadas fariam com que o código não compilasse mais, como assert no exemplo a seguir de @ oli-obk.

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

A expressividade aumentada da análise baseada em valores não é gratuita. Uma execução de desempenho que fez a qualificação const em todos os corpos de itens, não apenas const uns, mostrou uma regressão de 5% nas compilações de cheques . Este é o pior cenário, pois pressupõe que todos os itens serão feitos em const em algum ponto no futuro. Otimizações possíveis, como a de # 71330, foram discutidas anteriormente neste tópico.

Trabalho futuro

No momento, a verificação const é executada antes da elaboração do drop, o que significa que alguns terminadores de drop permanecem no MIR que são inacessíveis na prática. Isso está evitando que Option::unwrap se torne const fn (consulte # 66753). Isso não é muito difícil de resolver, mas exigirá a divisão da passagem de verificação de constância em duas fases (elaboração pré e pós-descarte).

Uma vez que #![feature(const_if_match)] esteja estabilizado, uma grande quantidade de funções de biblioteca podem ser feitas em const fn . Isso inclui muitos métodos em tipos de inteiros primitivos, que foram enumerados em # 53718.

Os loops em um contexto const são bloqueados na mesma questão de qualificação const que os condicionais. A abordagem atual baseada em fluxo de dados também funciona para CFGs cíclicos sem modificações, portanto, se #![feature(const_if_match)] estiver estabilizado, o bloqueador principal para # 52000 terá desaparecido.

Reconhecimentos

Agradecimentos especiais a @ oli-obk e @eddyb , que foram os revisores principais para a maior parte do trabalho de implementação, bem como para o restante de @ rust-lang / wg-const-eval por me ajudarem a entender as questões relevantes em torno de const qualificação. Nada disso seria possível sem Miri, que foi criada por @solson e agora mantida por @RalfJung e @ oli-obk.

Este é o relatório de estabilização anterior ao FCP. Não consigo abrir o FCP, no entanto.

@ ecstatic-morse Muito obrigado por todo o seu trabalho árduo nesta questão!

Excelente relatório!

Uma coisa que acho que gostaria de ver, @ ecstatic-morse, é

  • links para alguns testes representativos no repo, para que possamos observar o comportamento
  • se há implicações em torno do semver ou qualquer outra coisa - acho que a resposta é basicamente não , certo? Em outras palavras, estamos decidindo sobre a análise usada para determinar se o corpo de um const fn é legal, mas dado um const fn, nossas escolhas aqui não determinam coisas como "o que o chamador de const fn pode fazer com o resultado ", certo? Estou tentando descobrir que exemplo poderia ser do que estou falando - suponho que o autor da chamada não saiba exatamente quais variantes de um enum foram usadas, apenas isso - qualquer valor foi retornado - ele não tinha mutabilidade interior (na qual eles presumivelmente não podem confiar ao corresponder, desde então).

Em outras palavras, estamos decidindo sobre a análise usada para determinar se o corpo de um const fn é legal, mas dado um const fn, nossas escolhas aqui não determinam coisas como "o que o chamador de const fn pode fazer com o resultado ", certo? Estou tentando descobrir que exemplo poderia ser do que estou falando - suponho que o autor da chamada não saiba exatamente quais variantes de um enum foram usadas, apenas isso - qualquer valor foi retornado - ele não tinha mutabilidade interior (na qual eles presumivelmente não podem confiar ao corresponder, desde então).

Sim, o corpo de um const fn é opaco. Isso está em contraste com a expressão inicializador de const item. Você pode observar isso pelo fato de

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

pode ser usado para criar um &'static Option<Cell<i32>>

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

enquanto um const fn com o mesmo corpo não pode:

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

demonstração de parque infantil

Quando introduzimos o fluxo de controle nas constantes, isso significa que

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

também funcionará, irrelevante do valor de MEH e

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

não funcionará, novamente, irrelevante do valor de MEH .

O fluxo de controle não muda nada sobre os sites de chamada de const fn , apenas sobre o código permitido dentro desse fn const.

links para alguns testes representativos no repo, para que possamos observar o comportamento.

Eu adicionei um parágrafo no final da seção "Current Nightly Semantics" com links para alguns casos de teste interessantes. Sinto que precisamos de mais testes (uma afirmação que seja verdadeira independentemente das circunstâncias) antes que isso seja estabilizado, mas isso pode ser resolvido assim que decidirmos se a semântica atual é desejável.

se há implicações em torno de semver ou qualquer outra coisa.

Além do que @ oli-obk disse acima, quero salientar que alterar o valor final de const é tecnicamente uma mudança ininterrupta:

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

// Downstream crate

extern crate upstream;

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

No entanto, como não podemos fazer a qualificação const com precisão perfeita, alterar uma constante para usar if ou match pode quebrar o código downstream, mesmo se o valor final não mudar. Por exemplo:

// Changing from `cfg` attributes...

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

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

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

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

// Downstream

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

Isso não se aplica a alterações dentro do corpo de const fn , porque sempre usamos a qualificação baseada em tipo para o valor de retorno, mesmo dentro da mesma caixa.

Na minha opinião, o "pecado original" aqui foi não cair na qualificação baseada em tipo para const e static s definidos em caixas externas. No entanto, acredito que tenha sido assim desde a 1.0, e suspeito que uma grande parte do código depende disso. Assim que você permitir inicializadores const para os quais a análise estática não pode ser perfeitamente precisa, torna-se possível modificar esses inicializadores de forma que eles produzam o mesmo valor sem que a análise estática seja capaz de prová-lo.

editar:

Não há nada de exclusivo em if e match esse respeito. Por exemplo, atualmente é uma alteração significativa refatorar um inicializador const em um const fn se a caixa downstream estava contando com a qualificação baseada em valor.

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

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

// Downstream

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

@ ecstatic-morse Obrigado por escrever o relatório de estabilização! Vamos avaliar o consenso de forma assíncrona:

@rfcbot merge

Se alguém quiser discutir isso de forma síncrona em uma reunião, renomeie.

O membro da equipe @joshtriplett propôs fundir isso. A próxima etapa é a revisão pelo restante dos membros da equipe marcados:

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

Nenhuma preocupação listada atualmente.

Assim que a maioria dos revisores aprovar (e no máximo 2 aprovações pendentes), isso entrará em seu período final para comentários. Se você identificar uma questão importante que não foi levantada em qualquer ponto deste processo, fale!

Consulte este documento para obter informações sobre quais comandos os membros da equipe marcados podem me dar.

: bell: Agora está entrando em seu período de comentários final , conforme a revisão acima . :Sino:

Isso também permite o uso de ? em const fn ?

Usar ? significa usar o traço Try . Usar traços em const fn é instável, consulte https://github.com/rust-lang/rust/issues/67794.

@TimDiekmann por enquanto, você terá que escrever macros proc que diminuam o? manualmente. O mesmo vale para loop e for , pelo menos até um certo limite (estilo de recursão primitiva), mas const eval tem tais limites de qualquer maneira. Este recurso é tão incrível que permite MUITAS coisas que não eram possíveis anteriormente. Você pode até construir um minúsculo wasm vm em const fn, se desejar.

O período de comentários final, com uma disposição para mesclar , conforme a revisão acima , agora está completo .

Como representante automatizado do processo de governança, gostaria de agradecer ao autor por seu trabalho e a todos que contribuíram.

O RFC será mesclado em breve.

Esta página foi útil?
0 / 5 - 0 avaliações