6

I have an procedural attribute macro which given a function operates on each binary expression e.g. let a = b + c; and returns another expression depending on it. With the + operation depending on the types, it needs to know the types of a, b and c.

Is there a way to get the inferred types of variables at compile time?

(like rust-analyser might display inferred types, can I get these inside a macro?)

Quick example - Playground

To illustrate my approach a bit more succinctly in a Rust playground we can use a declarative macro which calls a function on a given variable, the specifics of this function being based on the type of the given variable.

The closest I can get to my desired functionality in a Rust playground:

macro_rules! SomeMacro {
    ($x:expr) => {{
        $x.some_function(3.)
    }};
}
trait SomeTrait {
    fn some_function(&self,x:f32) -> f32;
}
impl SomeTrait for u32 {
    fn some_function(&self,x:f32) -> f32 {
        x * 3.
    }
}

fn main() {
    let a = 3u32;
    let b = SomeMacro!(a);
    assert_eq!(b,9.);
}

How would I make something work like:

fn some_function<u32>(x:f32) -> f32 {
    3. * x
}
fn some_function<u32,i8,f32>(x:f32) -> f32 {
    3. * x
}
fn main() {
    let a = 3u32;
    let b = <type_of<a>()>::some_function(3.);
    assert_eq!(b,9.);
    let c = 5i8;
    let d = <type_of<a>(),type_of<b>(),type_of<c>()>::some_function(2.);
    assert_eq!(c,6.);
}

Comprehensive example - .zip

lib.rs
extern crate proc_macro;
use proc_macro::TokenStream;

#[proc_macro_attribute]
pub fn my_attribute_macro(_attr: TokenStream, item: TokenStream) -> TokenStream {
    let ast = syn::parse_macro_input!(item as syn::Item);
    // eprintln!("{:#?}",ast);

    // Checks item is function.
    let mut function = match ast {
        syn::Item::Fn(func) => func,
        _ => panic!("Only `fn` items are supported."),
    };
    let block = &mut function.block;

    // Updates statements
    let statements = block.stmts
        .iter()
        .filter_map(|statement| update_statements(statement))
        .collect::<Vec<_>>();
    block.stmts = statements;

    let new = quote::quote! { #function };
    TokenStream::from(new)
}
fn update_statements(stmt: &syn::Stmt) -> Option<syn::Stmt> {
    let local = match stmt {
        syn::Stmt::Local(local) => local,
        _ => return Some(stmt.clone())
    };
    let init = &local.init;
    let bin_expr = match *init.as_ref().unwrap().1 {
        syn::Expr::Binary(ref bin) => bin,
        _ => return Some(stmt.clone())
    };

    eprintln!("looking at: {:#?}",stmt);
    // 
    None
}
main.rs
use macro_test::*;
// Goals: 
// - Map from `x` being equal to `a+b` to `x` being equal to `a*b` based off `x` being `f32`.
// - Map from `y` being equal to `c+d` to `y` being equal to `c/d` based off `y` being `u32`.
#[my_attribute_macro]
fn my_function(a: f32, b: f32, c: u32, d: u32) {
    let x = a + b;
    let y = c + d;
}

fn main() {
}

With one of the prints looking like (from cargo expand --bin macro-test):

looking at: Local(
    Local {
        attrs: [],
        let_token: Let,
        pat: Ident(
            PatIdent {
                attrs: [],
                by_ref: None,
                mutability: None,
                ident: Ident {
                    ident: "y",
                    span: #0 bytes(316..317),
                },
                subpat: None,
            },
        ),
        init: Some(
            (
                Eq,
                Binary(
                    ExprBinary {
                        attrs: [],
                        left: Path(
                            ExprPath {
                                attrs: [],
                                qself: None,
                                path: Path {
                                    leading_colon: None,
                                    segments: [
                                        PathSegment {
                                            ident: Ident {
                                                ident: "c",
                                                span: #0 bytes(320..321),
                                            },
                                            arguments: None,
                                        },
                                    ],
                                },
                            },
                        ),
                        op: Add(
                            Add,
                        ),
                        right: Path(
                            ExprPath {
                                attrs: [],
                                qself: None,
                                path: Path {
                                    leading_colon: None,
                                    segments: [
                                        PathSegment {
                                            ident: Ident {
                                                ident: "d",
                                                span: #0 bytes(324..325),
                                            },
                                            arguments: None,
                                        },
                                    ],
                                },
                            },
                        ),
                    },
                ),
            ),
        ),
        semi_token: Semi,
    },
)
kmdreko
  • 42,554
  • 6
  • 57
  • 106
Jonathan Woollett-light
  • 2,813
  • 5
  • 30
  • 58
  • I don't want to play the stereotype, but you want to...have addition implicitly turn into division because the type is `u32`? This seems like a recipe for confusion. – GManNickG Dec 18 '21 at 22:13
  • @GManNickG Just for an example, in practicality it turns statements into their derivatives. But that adds unnecessary code to this example. – Jonathan Woollett-light Dec 18 '21 at 22:27
  • 1
    Does this answer your question? [How do I match the type of an expression in a Rust macro?](https://stackoverflow.com/questions/34214136/how-do-i-match-the-type-of-an-expression-in-a-rust-macro) – Chayim Friedman Dec 19 '21 at 00:48

2 Answers2

5

The compiler doesn't decide the types until the macros have been processed. In many cases, a macro can change the inferred types. This is why it's possible to write things like:

let mut xs = vec![];
xs.push(1);

Type inference can go back and assign the right type to xs. This wouldn't be possible if vec! had to know the type prior to expansion. You will need to approach this problem another way. See @PossiblyAShrub's answer for one approach.

As The Rust Programming Language:Macros notes (emphasis added):

Also, macros are expanded before the compiler interprets the meaning of the code, so a macro can, for example, implement a trait on a given type.

You may want to explore the Native Differential Programming Support for Rust discussion from the forums, and particularly the follow-up threads linked.

Rob Napier
  • 286,113
  • 34
  • 456
  • 610
2

Addition

OP has updated their post clarifying their issue, this addition is in response to that.

It appears that you may want to implement the new type pattern. In your comment you mention that you want to change the behavior of standard operators (+, -, *, /, ...) on functions decorated with a specific procedural macro.

The newtype pattern looks like this:

struct Differentiable(f32);

Note: we could even use generics here such that our new type can be over f32, u32, u8 ...

We can now implement our desired operators on top of our new type.

impl Add for Differentiable {
    type Output = Self;

    fn add(self, other: Self) -> Self {
        Self(self.0 * other.0)
    }
}

This gives us the behavior:

fn main() {
  let x = Differentiable(5.0);
  let y = Differentiable(2.0);
  assert_eq!(x + y, 10);
}

Furthermore we could implement Into and From on the type to make it's use more ergonomic. We can also implement the other operators via implementing the traits in std::ops.

Now, you mentioned wanting to implement this as a procedural macro on a function. I would strongly suggest otherwise. I would argue that such a feature would:

  • Increase likelihood for confusion
  • Increase the complexity of your library
  • Directly violate standard rust idoms
  • Impact IDE usage
  • Increase compile times

See:

Personally I do not see the value in providing such a macro, nor how it could even be implemented in a way which "feels" right. Nevertheless it is possible:

Simply convert from this ast:

fn my_function(a: f32, b: f32, c: u32, d: u32) {
  let x = a + b;
  let y = c + d;
}

to:

fn my_function(a: f32, b: f32, c: u32, d: u32) {
  fn inner(
    a: Differentiable,
    b: Differentiable,
    c: Differentiable,
    d: Differentiable,
  ) {
    let x = a + b;
    let y = c + d;
  }

  inner(
    Differentiable(a),
    Differentiable(b),
    Differentiable(c),
    Differentiable(d),
  )
}

Further reading:


Previous response

I am not sure what use knowing the type of an expression inside of a macro would be for you. Rust's type inference system should be powerful enough to handle most cases of type resolution.

For example, I would implement the above snippet using generics and trait bounds:

use std::ops::Add;

fn add<A: Into<B>, B: Add>(a: A, b: B) -> <B as Add>::Output {
    a.into() + b
}

fn main() {
    let a: u8 = 4;
    let b: f32 = 3.5;
    assert_eq!(add(a, b), 7.5);
}

Rust will automatically find the right values for A and B. In the case that it cannot, due to our trait restrictions, rustc will output helpful error message:

fn main() {
    let a = "Hello, world!";
    let b = 56;
    add(a, b);
}

Results in:

error[E0277]: the trait bound `{integer}: From<&str>` is not satisfied
  --> src/main.rs:10:12
   |
10 |     add(a, b);
   |     ---    ^ the trait `From<&str>` is not implemented for `{integer}`
   |     |
   |     required by a bound introduced by this call
   |
   = help: the following implementations were found:
             <Arc<B> as From<Cow<'a, B>>>
             <Arc<CStr> as From<&CStr>>
             <Arc<CStr> as From<CString>>
             <Arc<OsStr> as From<&OsStr>>
           and 329 others
   = note: required because of the requirements on the impl of `Into<{integer}>` for `&str`
note: required by a bound in `add`
  --> src/main.rs:3:11
   |
3  | fn add<A: Into<B>, B: Add>(a: A, b: B) -> <B as Add>::Output {
   |           ^^^^^^^ required by this bound in `add`

For more information about this error, try `rustc --explain E0277`.

Here rust could not resolve the type of a to fit our restrictions in the add function.

Now this works through macros, with no extra work necessary:

macro_rules! some_macro {
  ($x:expr) => (add($x, 3.0))
}

fn main() {
    let x = 42u32;
    let y = some_macro!(x);
    assert_eq!(y, 45.0);
}

Further reading:

PossiblyAShrub
  • 447
  • 5
  • 6
  • Added a more explicit longer example that should highlight why this doesn't work and I require what I asked about. – Jonathan Woollett-light Dec 18 '21 at 20:18
  • I have updated my response accordingly. – PossiblyAShrub Dec 18 '21 at 23:08
  • I asked for something, I want this in the context my examples. Neither of your responses are what I asked for. I want to know a values type at compile time. – Jonathan Woollett-light Dec 19 '21 at 00:11
  • 3
    No this is not possible with macros. At the time that macros are run, types have not yet been fully resolved. Nor can your macro derive that information on it's own. This is why I am providing an answer which attempts to demonstrate that macros not necessary. The rust type system is perfectly capable of providing the behavior in question without macros. My apologies, I should've placed this reasoning in my answer. – PossiblyAShrub Dec 19 '21 at 00:35