Como posso criar identificadores higiênicos no código gerado por macros procedurais?


8

Ao escrever uma macro_rules!macro declarativa ( ), obtemos automaticamente a higiene da macro . Neste exemplo, declaro uma variável nomeada fna macro e passo um identificador fque se torna uma variável local:

macro_rules! decl_example {
    ($tname:ident, $mname:ident, ($($fstr:tt),*)) => {
        impl std::fmt::Display for $tname {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let Self { $mname } = self;
                write!(f, $($fstr),*)
            }
        }
    }
}

struct Foo {
    f: String,
}

decl_example!(Foo, f, ("I am a Foo: {}", f));

fn main() {
    let f = Foo {
        f: "with a member named `f`".into(),
    };
    println!("{}", f);
}

Esse código é compilado, mas se você observar o código parcialmente expandido, poderá ver que há um conflito aparente:

impl std::fmt::Display for Foo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Self { f } = self;
        write!(f, "I am a Foo: {}", f)
    }
}

Estou escrevendo o equivalente a essa macro declarativa como uma macro processual, mas não sei como evitar possíveis conflitos de nome entre os identificadores fornecidos pelo usuário e os identificadores criados pela minha macro. Tanto quanto posso ver, o código gerado não tem noção de higiene e é apenas uma string:

src / main.rs

use my_derive::MyDerive;

#[derive(MyDerive)]
#[my_derive(f)]
struct Foo {
    f: String,
}

fn main() {
    let f = Foo {
        f: "with a member named `f`".into(),
    };
    println!("{}", f);
}

Cargo.toml

[package]
name = "example"
version = "0.1.0"
edition = "2018"

[dependencies]
my_derive = { path = "my_derive" }

my_derive / src / lib.rs

extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Meta, NestedMeta};

#[proc_macro_derive(MyDerive, attributes(my_derive))]
pub fn my_macro(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);

    let name = input.ident;

    let attr = input.attrs.into_iter().filter(|a| a.path.is_ident("my_derive")).next().expect("No name passed");
    let meta = attr.parse_meta().expect("Unknown attribute format");
    let meta = match meta {
        Meta::List(ml) => ml,
        _ => panic!("Invalid attribute format"),
    };
    let meta = meta.nested.first().expect("Must have one path");
    let meta = match meta {
        NestedMeta::Meta(Meta::Path(p)) => p,
        _ => panic!("Invalid nested attribute format"),
    };
    let field_name = meta.get_ident().expect("Not an ident");

    let expanded = quote! {
        impl std::fmt::Display for #name {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                let Self { #field_name } = self;
                write!(f, "I am a Foo: {}", #field_name)
            }
        }
    };

    TokenStream::from(expanded)
}

my_derive / Cargo.toml

[package]
name = "my_derive"
version = "0.1.0"
edition = "2018"

[lib]
proc-macro = true

[dependencies]
syn = "1.0.13"
quote = "1.0.2"
proc-macro2 = "1.0.7"

Com o Rust 1.40, isso produz o erro do compilador:

error[E0599]: no method named `write_fmt` found for type `&std::string::String` in the current scope
 --> src/main.rs:3:10
  |
3 | #[derive(MyDerive)]
  |          ^^^^^^^^ method not found in `&std::string::String`
  |
  = help: items from traits can only be used if the trait is in scope
  = note: this error originates in a macro outside of the current crate (in Nightly builds, run with -Z external-macro-backtrace for more info)
help: the following trait is implemented but not in scope; perhaps add a `use` for it:
  |
1 | use std::fmt::Write;
  |

Quais técnicas existem para nomear meus identificadores com nomes fora do meu controle?


1
Ideia óbvia (não sei se funciona): escreva uma macro proc que gere uma declarativa e depois a chame?
trentcl 6/01

O termo Lisp para esse utilitário é gensym , e aparentemente existe pelo menos uma caixa para isso . No entanto, a implementação é exatamente a mesma da resposta em francês.
user4815162342 6/01

Respostas:


6

Resumo : você ainda não pode usar identificadores higiênicos com macros proc no Rust estável. Sua melhor aposta é usar um nome particularmente feio como __your_crate_your_name.


Você está criando identificadores (em particular f) usando quote!. Isso certamente é conveniente, mas é apenas um auxiliar na API de proc macro real que o compilador oferece . Então, vamos dar uma olhada nessa API para ver como podemos criar identificadores! No final, precisamos de umTokenStream , pois é isso que nossa macro proc retorna. Como podemos construir um fluxo de token?

Podemos analisá-lo a partir de uma string, por exemplo "let f = 3;".parse::<TokenStream>(). Mas essa foi basicamente uma solução inicial e agora é desencorajada. De qualquer forma, todos os identificadores criados dessa maneira se comportam de maneira não higiênica, portanto, isso não resolverá o seu problema.

A segunda maneira (que é quote!usada sob o capô) é criar um TokenStreammanualmente criando vários TokenTrees . Um tipo de TokenTreeé um Ident(identificador). Podemos criar uma Identvia new:

fn new(string: &str, span: Span) -> Ident

O stringparâmetro é auto-explicativo, mas o spanparâmetro é a parte interessante! A Spanarmazena a localização de algo no código-fonte e geralmente é usado para o relatório de erros ( rustcpara apontar para o nome da variável com erro de ortografia, por exemplo). Porém, no compilador Rust, os vãos contêm mais do que informações de localização: o tipo de higiene! Podemos ver duas funções construtoras para Span:

  • fn call_site() -> Span: cria uma extensão com a higiene do site de chamada . Isso é o que você chama de "anti-higiênico" e é equivalente a "copiar e colar". Se dois identificadores tiverem a mesma sequência, eles colidirão ou sombrearão.

  • fn def_site() -> Span: é isso que você procura. Tecnicamente chamado de definição de higiene do site , é o que você chama de "higiene". Os identificadores que você define e os do seu usuário vivem em universos diferentes e nunca colidem. Como você pode ver nos documentos, esse método ainda é instável e, portanto, apenas utilizável em um compilador noturno. Vadio!

Não há realmente ótimas soluções alternativas. O óbvio é usar um nome realmente feio como __your_crate_some_variable. Para facilitar um pouco, você pode criar esse identificador uma vez e usá-lo dentro quote!( solução um pouco melhor aqui ):

let ugly_name = quote! { __your_crate_some_variable };
quote! {
    let #ugly_name = 3;
    println!("{}", #ugly_name);
}

Às vezes, você pode até pesquisar todos os identificadores do usuário que possam colidir com o seu e, em seguida, simplesmente escolher algoritmicamente um identificador que não colide. Na verdade, foi o que fizemosauto_impl , com um nome super feio de fallback. Isso foi principalmente para melhorar a documentação gerada por ter nomes super feios.

Além disso, receio que você não possa realmente fazer nada.


5

Você pode, graças a um UUID:

fn generate_unique_ident(prefix: &str) -> Ident {
    let uuid = uuid::Uuid::new_v4();
    let ident = format!("{}_{}", prefix, uuid).replace('-', "_");

    Ident::new(&ident, Span::call_site())
}

Alguma coisa impede o usuário de passar um identificador que (des) coincide com sorte com o identificador que eu gerei?
Shepmaster 6/01

1
@ Shepmaster As leis da probabilidade, eu acho
Boiethios 06/01

2
@ Shepmaster É um evento astronomicamente improvável, pois um UUID v4 consiste em 128 bits aleatórios. Com um PRNG corretamente propagado, deve ser semelhante a perguntar se o seu repositório Git pode ser quebrado por dois commits, infelizmente, com hash no mesmo SHA1.
user4815162342 6/01
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.