Как я могу создать гигиенические идентификаторы c в коде, сгенерированном процедурными макросами? - PullRequest
8 голосов
/ 06 января 2020

При написании декларативного (macro_rules!) макроса мы автоматически получаем макро-гигиена . В этом примере я объявляю переменную с именем f в макросе и передаю идентификатор f, который становится локальной переменной:

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

Этот код компилируется, но если вы посмотрите на частично- В расширенном коде вы можете увидеть очевидный конфликт:

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

Я пишу эквивалент этого декларативного макроса как процедурный макрос, но не знаю, как избежать потенциальных конфликтов имен между предоставленными пользователем идентификаторы и идентификаторы, созданные моим макросом. Насколько я вижу, сгенерированный код не имеет понятия гигиены и представляет собой просто строку:

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

Car go .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 / Car go. 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"

С Rust 1.40 это приводит к ошибке компилятора:

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

Какие существуют методы для пространства имен моих идентификаторов из идентификаторов вне моего контроля?

Ответы [ 2 ]

6 голосов
/ 07 января 2020

Сводка : вы еще не можете использовать гигиенические c идентификаторы с макросами pro c в стабильном Rust. Лучше всего использовать особенно уродливое имя, например __your_crate_your_name.


Вы создаете идентификаторы (в частности, f), используя quote!. Это, конечно, удобно, но это всего лишь помощник для фактического pro c макро API, который компилятор предлагает . Итак, давайте посмотрим на этот API, чтобы увидеть, как мы можем создавать идентификаторы! В конце нам нужен a TokenStream, поскольку это то, что возвращает наш макрос pro c. Как мы можем построить такой поток токенов?

Мы можем разобрать его из строки, например "let f = 3;".parse::<TokenStream>(). Но это было в основном раннее решение и сейчас не рекомендуется. В любом случае, все идентификаторы, созданные таким образом, ведут себя негигиенично c, поэтому это не решит вашу проблему.

Второй способ (который quote! использует под капотом) заключается в создайте TokenStream вручную, создав группу TokenTree s . Один вид TokenTree - это Ident (идентификатор). Мы можем создать Ident через new:

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

Параметр string не требует пояснений, но параметр span является интересной частью! A Span хранит местоположение чего-либо в исходном коде и обычно используется для сообщения об ошибках (например, для rustc для указания на имя переменной с ошибкой). Но в компиляторе Rust пролеты несут не только информацию о местоположении: это гигиена! Мы можем видеть две функции конструктора для Span:

  • fn call_site() -> Span: создает диапазон с гигиеническим вызовом сайта . Это то, что вы называете «unhygieni c» и эквивалентно «копированию и вставке». Если два идентификатора имеют одинаковую строку, они будут сталкиваться или затенять друг друга.

  • fn def_site() -> Span: это то, что вам нужно. Технически названный определение сайта гигиены , это то, что вы называете "гигиена c". Идентификаторы, которые вы определяете, и идентификаторы вашего пользователя живут в разных вселенных и никогда не будут конфликтовать. Как вы можете видеть в документации, этот метод все еще нестабилен и поэтому может использоваться только на ночных компиляторах. Облом!

Нет действительно хороших обходных путей. Очевидным является использование действительно ужасного имени, например __your_crate_some_variable. Чтобы вам было немного проще, вы можете создать этот идентификатор один раз и использовать его в quote! ( немного лучшее решение здесь ):

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

Иногда вы можете даже искать через все идентификаторы пользователя, которые могут столкнуться с вашим, а затем просто алгоритмически выбрали идентификатор, который не конфликтует. Это на самом деле то, что мы сделали для auto_impl, с запасным супер уродливым названием. Это было главным образом для того, чтобы улучшить сгенерированную документацию, добавив в нее супергадкие имена.

Кроме того, я боюсь, что вы ничего не сможете сделать.

5 голосов
/ 06 января 2020

Вы можете благодаря 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())
}
...