Let's Play With Procedural Macro

Powered by

> whoami

michele.damico@gmail.com

https://github.com/la10736

https://www.linkedin.com/in/damico/

@PhenoCoder

Perché?

  1. La Meta Programmazione è Strafiga?
  2. E' Molto Difficile?
  3. Possiamo Raccontarlo In Giro?
  1. La Meta Programmazione è Strafiga?
  2. E' Molto Difficile?
  3. Possiamo Raccontarlo In Giro?
  • Vogliamo scrivere codice in Rust
  • Togliamo la magia
  • Rust Difficile ma ha degli ottimi strumenti di Meta Programmazione

Cosa ?

Un semplice clone di              .....

pytest

..... O meglio implementiamo il concetto di fixture

Fixture

def test_dump_empty_repository(out, repository):
    dump_repository(repository, out)

    content = open(out).read()
    assert content.strip() == "No Entries"


def test_dump_no_empty_repository(out, two_entries_repository):
    dump_repository(two_entries_repository, out)

    content = open(out).read()
    assert "Michele, d'Amico : 44" in content
    assert "John, Doe : 37" in content
    assert 2 == len(content.splitlines())
@pytest.fixture
def repository():
    return Repository()


@pytest.fixture
def out(tmpdir):
    return os.path.join(tmpdir, "dump")


@pytest.fixture
def two_entries_repository(repository):
    repository.insert(Person("Michele", "d'Amico", 44))
    repository.insert(Person("John", "Doe", 37))
    return repository

Desiderata

#[rstest]
fn dump_empty_repository<R: Repository>(out: &Path, repository: R) {
    dump_repository(&repository, out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

#[rstest]
fn dump_no_empty_repository<R: Repository>(out: &Path, two_entries_repository: R) {
    dump_repository(&two_entries_repository, out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert!(content.contains("Michele, d'Amico : 44"));
    assert!(content.contains("John, Doe : 37"));
    assert_eq!(2, content.lines().count());
}

Magari...

  • Generics
  • &Path
  • Run parallelo

Fattibile

#[rstest]
fn dump_empty_repository(out: TempPath, repository: Repository) {
    dump_repository(&repository, &out);

    let mut content= String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

#[rstest]
fn dump_no_empty_repository(out: TempPath, two_entries_repository: Repository) {
    dump_repository(&two_entries_repository, &out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert!(content.contains("Michele, d'Amico : 44"));
    assert!(content.contains("John, Doe : 37"));
    assert_eq!(2, content.lines().count());
}

L'idea...

Sostituire ogni argomento con una variabile locale inizializzata chiamando la funzione della fixture

#[test]
fn dump_empty_repository() {
    let out = out();
    let repository = repository();

    dump_repository(&repository, &out);

    let mut content = String::new();
    File::open(&out).unwrap().read_to_string(&mut content).unwrap();

    assert_eq!(content.trim(), "No Entries");
}

Come

Usando le macro procedurali (Attribute-like): proc_macro 

  • Fn (tokens_attr, tokens_body) -> tokens_result
  • La funzione puo' essere usata come attributo e viene chiamata dal parser
  • Ogni panic lanciato dalla funzione viene intercettato e mostrato come errore all'utente
  • unstable (anche chi usa deve usare nightly e abilitarle)
  • Scarsa documentazione
  • Supporto Stackoveflow quasi nullo
  • Tokens -> Ast -> Modifica Ast -> Tokens

Problemini:

Hello proc_macro

Struttura

#![feature(proc_macro)]
extern crate hello_proc_macro;

use hello_proc_macro::hello;

#[hello]
fn other(_s: &str, _v: i32) {}

fn main() { other("pippo", 42); }
main.rs
[workspace]

[dependencies]
hello_proc_macro = { path = "hello_proc_macro" }
Cargo.toml
[lib]
proc-macro = true

[dependencies]
syn = {version="*", features=["full"]}
quote = "*"
hello_proc_macro/Cargo.toml

Hello proc_macro (cont.)

#![feature(proc_macro)]
// Omiss ....

#[proc_macro_attribute]
pub fn hello(_attrs: TokenStream, body: TokenStream) -> TokenStream{
    // Parse the string representation
    let ast = syn::parse(body).unwrap();

    // Build the impl
    let gen = impl_hello_world(ast);

    // Return the generated impl
    println!("RESULT CODE -> {}", gen);
    gen.into()
}

fn impl_hello_world(mut ast: syn::Item) -> quote::Tokens {
    if let syn::Item::Fn(ref mut item_fn) = ast {
        let args = &item_fn.decl.inputs;
        let stmt = format!(
            r#"println!("Hello Word!!! :  args tokens = {:?}");"#,
            args.clone().into_tokens()
        );

        item_fn.block.stmts.insert(0, syn::parse_str(&stmt).unwrap());
    }
    ast.into_tokens()
}

Core code:

Obbiettivo

#![feature(proc_macro)]
extern crate rstest;

use rstest::rstest;

fn fixture() -> &'static str {
    "42"
}

fn fix_string() -> String {
    "String".to_string()
}

#[rstest]
fn some_test(fixture: &str, fix_string: String) {
    assert_eq!(fixture, "42");
    assert_eq!(fix_string, "String".to_string());
}

Aggiungere un crate rstest al workspace con una macro procedurale rstest che

trasformi le fixture in variabili locali

Tips:

  • la macro quote!{} puo' essere utile per scrivere il risultato finale
  • potrebbe essere piu' semplice lasciare il codice originale in un blocco scritto dopo le variabili

Un po di link

Ora iniziamo!

> git clone https://github.com/la10736/rstest_playground

Let's Play with Procedural Macro

By Michele D'Amico

Let's Play with Procedural Macro

Giochiamo con le macro procedurali per scrivere in clone di pytest

  • 210