Wie kann ich hygienische Kennungen in Code erstellen, der durch prozedurale Makros generiert wird?

8

Beim Schreiben eines deklarativen ( macro_rules!) Makros erhalten wir automatisch eine Makrohygiene . In diesem Beispiel deklariere ich eine fim Makro benannte Variable und übergebe einen Bezeichner, fder zu einer lokalen Variablen wird:

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

Dieser Code wird kompiliert, aber wenn Sie sich den teilweise erweiterten Code ansehen, können Sie feststellen, dass ein offensichtlicher Konflikt vorliegt:

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

Ich schreibe das Äquivalent dieses deklarativen Makros als prozedurales Makro, weiß jedoch nicht, wie ich potenzielle Namenskonflikte zwischen den vom Benutzer angegebenen Bezeichnern und den von meinem Makro erstellten Bezeichnern vermeiden kann. Soweit ich sehen kann, hat der generierte Code keine Vorstellung von Hygiene und ist nur eine Zeichenfolge:

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"

Mit Rust 1.40 erzeugt dies den Compilerfehler:

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

Welche Techniken gibt es, um meine Bezeichner von Bezeichnern außerhalb meiner Kontrolle zu benennen?

Shepmaster
quelle
1
Offensichtliche Idee (weiß nicht, ob es funktioniert): Schreiben Sie ein Proc-Makro, das ein deklaratives generiert, und rufen Sie es dann auf?
6.
Der Lisp-Begriff für dieses Dienstprogramm lautet gensym , und anscheinend gibt es dafür mindestens eine Kiste . Die Implementierung ist jedoch genau die gleiche wie in der französischen Antwort.
user4815162342

Antworten:

6

Zusammenfassung : Sie können noch keine hygienischen Kennungen mit Proc-Makros auf stabilem Rost verwenden. Am besten verwenden Sie einen besonders hässlichen Namen wie __your_crate_your_name.


Sie erstellen Bezeichner (insbesondere f) mithilfe von quote!. Dies ist sicherlich praktisch, aber es ist nur ein Hilfsmittel für die eigentliche Proc-Makro-API, die der Compiler anbietet . Schauen wir uns also diese API an, um zu sehen, wie wir Bezeichner erstellen können! Am Ende brauchen wir einTokenStream , denn das ist es, was unser Proc-Makro zurückgibt. Wie können wir einen solchen Token-Stream erstellen?

Wir können es aus einer Zeichenfolge analysieren, z "let f = 3;".parse::<TokenStream>(). Dies war jedoch im Grunde eine frühe Lösung und wird jetzt nicht empfohlen. In jedem Fall verhalten sich alle auf diese Weise erstellten Kennungen nicht hygienisch, sodass Ihr Problem dadurch nicht gelöst wird.

Die zweite Möglichkeit (die quote!unter der Haube verwendet wird) besteht darin, eine TokenStreammanuell zu erstellen, indem eine Reihe von TokenTrees erstellt wird . Eine Art von TokenTreeist ein Ident(Bezeichner). Wir können ein IdentVia erstellen new:

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

Der stringParameter ist selbsterklärend, aber der spanParameter ist der interessante Teil! A Spanspeichert die Position von etwas im Quellcode und wird normalerweise für die Fehlerberichterstattung verwendet (um beispielsweise rustcauf den falsch geschriebenen Variablennamen zu verweisen). Im Rust-Compiler enthalten die Bereiche jedoch mehr als nur Standortinformationen: die Art der Hygiene! Wir können zwei Konstruktorfunktionen sehen für Span:

  • fn call_site() -> Span: Erstellt eine Spanne mit Anrufstellenhygiene . Dies ist das, was Sie als "unhygienisch" bezeichnen und entspricht "Kopieren und Einfügen". Wenn zwei Bezeichner dieselbe Zeichenfolge haben, kollidieren sie oder beschatten sich gegenseitig.

  • fn def_site() -> Span: das ist was du suchst. Technisch als Definition Site Hygiene bezeichnet , ist dies das, was Sie als "hygienisch" bezeichnen. Die von Ihnen definierten Bezeichner und die Ihres Benutzers leben in verschiedenen Universen und werden niemals kollidieren. Wie Sie in den Dokumenten sehen können, ist diese Methode immer noch instabil und daher nur auf einem nächtlichen Compiler verwendbar. Schade!

Es gibt keine wirklich guten Problemumgehungen. Das Offensichtliche ist, einen wirklich hässlichen Namen wie zu verwenden __your_crate_some_variable. Um es Ihnen ein bisschen einfacher zu machen, können Sie diesen Bezeichner einmal erstellen und darin verwenden quote!( etwas bessere Lösung hier ):

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

Manchmal können Sie sogar alle Bezeichner des Benutzers durchsuchen, die mit Ihren kollidieren könnten, und dann einfach algorithmisch einen Bezeichner auswählen, der nicht kollidiert. Dies ist eigentlich das, wofür wir es getan habenauto_impl , mit einem super hässlichen Fallback-Namen. Dies diente hauptsächlich dazu, die generierte Dokumentation zu verbessern, indem sie super hässliche Namen enthält.

Abgesehen davon fürchte ich, dass Sie nichts wirklich tun können.

Lukas Kalbertodt
quelle
5

Sie können dank einer 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
quelle
Verhindert irgendetwas, dass der Benutzer eine Kennung übergibt, die (unglücklicherweise) mit der von mir generierten Kennung übereinstimmt?
Shepmaster
1
@ Shepmaster Die Gesetze der Wahrscheinlichkeit, denke ich
Boiethios
2
@ Shepmaster Es ist ein astronomisch unwahrscheinliches Ereignis, da eine v4-UUID aus 128 zufälligen Bits besteht. Mit einem korrekt gesetzten PRNG sollte es vergleichbar sein mit der Frage, ob Ihr Git-Repo durch zwei Commits unterbrochen werden könnte, die unglücklicherweise denselben SHA1 haben.
user4815162342