¿Cómo puedo crear identificadores higiénicos en el código generado por macros de procedimiento?

8

Al escribir una macro_rules!macro declarativa ( ), obtenemos automáticamente la higiene macro . En este ejemplo, declaro una variable nombrada fen la macro y paso un identificador fque se convierte en una variable 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);
}

Este código se compila, pero si observa el código parcialmente expandido, puede ver que hay un conflicto 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)
    }
}

Estoy escribiendo el equivalente de esta macro declarativa como una macro de procedimiento, pero no sé cómo evitar posibles conflictos de nombres entre los identificadores proporcionados por el usuario y los identificadores creados por mi macro. Hasta donde puedo ver, el código generado no tiene noción de higiene y es solo una cadena:

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"

Con Rust 1.40, esto produce el error del 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;
  |

¿Qué técnicas existen para el espacio de nombres de mis identificadores de identificadores fuera de mi control?

Shepmaster
fuente
1
Idea obvia (no sé si funciona): ¿escribir una macro de proceso que genere una declarativa y luego la llame?
trentcl
El término Lisp para esta utilidad es gensym , y aparentemente hay al menos una caja para eso . Sin embargo, la implementación es exactamente la misma que en la respuesta de French.
user4815162342

Respuestas:

6

Resumen : aún no puede utilizar identificadores higiénicos con macros de proceso en Rust estable. Su mejor opción es usar un nombre particularmente feo como __your_crate_your_name.


Está creando identificadores (en particular f) mediante quote!. Esto es ciertamente conveniente, pero es solo una ayuda para la macro API de proceso real que ofrece el compilador . ¡Echemos un vistazo a esa API para ver cómo podemos crear identificadores! Al final necesitamos unTokenStream , ya que eso es lo que devuelve nuestra macro de proceso. ¿Cómo podemos construir una secuencia de tokens?

Podemos analizarlo desde una cadena, por ejemplo "let f = 3;".parse::<TokenStream>(). Pero esta fue básicamente una solución temprana y ahora se desaconseja. En cualquier caso, todos los identificadores creados de esta manera se comportan de manera no higiénica, por lo que esto no resolverá su problema.

La segunda forma (que quote!usa debajo del capó) es crear un TokenStreammanual creando un grupo de TokenTrees . Un tipo de TokenTreees un Ident(identificador). Podemos crear una Identvía new:

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

El stringparámetro se explica por sí mismo, ¡pero el spanparámetro es la parte interesante! A Spanalmacena la ubicación de algo en el código fuente y generalmente se usa para informar errores (para rustcseñalar el nombre de la variable mal escrita, por ejemplo). Pero en el compilador de Rust, los tramos llevan más que información de ubicación: ¡el tipo de higiene! Podemos ver dos funciones de constructor para Span:

  • fn call_site() -> Span: crea un espacio con la higiene del sitio de la llamada . Esto es lo que llamas "antihigiénico" y es equivalente a "copiar y pegar". Si dos identificadores tienen la misma cadena, colisionarán o se sombrearán entre sí.

  • fn def_site() -> Span: esto es lo que buscas. Técnicamente llamado higiene del sitio de definición , esto es lo que usted llama "higiénico". Los identificadores que define y los de su usuario viven en universos diferentes y nunca colisionarán. Como puede ver en los documentos, este método todavía es inestable y, por lo tanto, solo se puede usar en un compilador nocturno. ¡Gorrón!

No hay soluciones realmente buenas. La obvia es usar un nombre realmente feo como __your_crate_some_variable. Para hacerlo un poco más fácil, puede crear ese identificador una vez y usarlo dentro quote!( una solución un poco mejor aquí ):

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

A veces, incluso puede buscar a través de todos los identificadores del usuario que podrían colisionar con los suyos y luego simplemente elegir algorítmicamente un identificador que no colisione. Esto es en realidad lo que hicimosauto_impl , con un nombre super feo alternativo. Esto fue principalmente para mejorar la documentación generada por tener nombres súper feos.

Aparte de eso, me temo que realmente no puedes hacer nada.

Lukas Kalbertodt
fuente
5

Puedes gracias a un 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())
}
Boiethios
fuente
¿Algo impide que el usuario pase un identificador que (desafortunadamente) coincide con el identificador que he generado?
Shepmaster
1
@Shepmaster Las leyes de probabilidad, supongo
Boiethios
2
@Shepmaster Es un evento astronómicamente improbable, ya que un v4 UUID consta de 128 bits aleatorios. Con un PRNG correctamente sembrado, debería ser similar a preguntar si su repositorio de git podría romperse por dos commits que desafortunadamente se combinan con el mismo SHA1.
user4815162342