Parsing types in a proc macro

Hello,

I'm trying to figure out the right way to create a proc_macro that generates some code for each unique type it's called with. First things first, however, I'm just trying to parse the type at all.

Ideally, I would be able to get the type_id (as in the 64 bit value provided by intrinsics::type_id::<T> at runtime) but I have a vague suspicion that those IDs aren't assigned until after the macros have already run. Is this true?

So if getting the type_id is not possible, I'd settle for a string that identifies a valid type. Unfortunately even this is eluding me. :frowning:

Here is what I have now:

use proc_macro::{*};

#[proc_macro]
pub fn my_proc_macro(input: TokenStream) -> TokenStream {

    let type_string = if let Ok(syn::Type::Path(type_path)) = syn::parse(input) {

        let idents : Vec<_> = type_path.path.segments
            .into_pairs()
            .map(|pair| pair.value().ident.to_string())
            .collect();
        let type_string = idents.join("_");

        println!("type_string = {}", type_string);
    } else {
        panic!("type expected"); //TODO: Throw a nicer compile error
    };

    TokenStream::new() //TODO, return something real
}

my_proc_macro!(i32);
// YAY!  Prints "type_string = i32"

my_proc_macro!(Box<dyn Iterator>);
// NO!  Prints "type_string = Box" but the contents of the Box are part of the type

my_proc_macro!(Option::None);
// NO!  Prints "type_string = Option_None" but an enum variant isn't a type

let my_int = 5;
my_proc_macro!(my_int);
// NO!  Prints "type_string = my_int" but a variable name isn't a type

Is what I am trying to do possible?

A derive macro didn't seem appropriate because I want to work with types that the caller didn't define. For example, primitive types. In addition, the other half of the macros I want to write will be a proc macro that turns into a different function call depending on the type of the arguments being passed.

Can proc macros do this?

Thanks in advance.

Yes, true.

Not like this, I don't think. Macros only work syntactically without much/any additional context besides the tokens passed in as argument. You (i.e. your macro) cannot know whether Option::None is a type. (It might be referring to some different type my::own::Option where None is an associated type from some trait.) You cannot know whether Foo in one place is the same as Foo in a different place, since different paths might be imported.

You are trying to solve a similar problem that traits need to solve - defining something at most once for each type. Traits have orphan rules to avoid multiple conflicting impls; multiple calls of my_proc_macro!(i32) happening in different crates really have no way of knowing about each other and avoiding conflict/duplication. A derive macro is a simple (but indeed quite restrictive) way to make sure a macro is only called at most once per type.

I'm not able to give good feedback on what might be a good / the best approach here, since you didn't really explain at all what kind of code your macro is supposed to generate in the first place and why it's important to programmatically ensure that code is generated at most once per type.

5 Likes

Thank you for the reply.

It's not so much that it's important the type identifiers be unique, as much as that it's important the same types produce the same identifiers.

The thing I envision doing has two parts.

Part One would define a function, and then a macro would create a "name mangled" version of that function for every type that the macro was invoked on.

Part Two would be a second macro to allow the function to be called. The second macro would replace the simple invocation with the type-specific version, depending on the type(s) of the argument(s) passed. So this means I'd also need to be able to get the type for a given variable, within a macro. Sounds like that's not going to be possible even if I can build some logic to recognize which spans of identifiers are types.

You are astute to notice the similarities with the trait system - that's essentially what I'm trying to do: make an alternative trait system to get around the lack of impl specialization.

The big feature I need that the trait system doesn't currently offer is the ability to define blanket implementations that can be superseded by type-specific implementations.

Thanks again for the reply.

I think this is borderline impossible, or at least highly impractical, to solve using macros.

Are you by chance looking for a HashMap<TypeId, fn(…) -> …>?

1 Like

The main** issue with using a HashMap is the runtime cost. I was hoping for zero (or close to zero) runtime cost by being able to perform that lookup at compile time.

Am I misunderstanding your suggestion?

Thanks!

**Also I was hoping to implement polymorphic functions with multiple arguments and "specialization levels", for example my_fn(arg_1 : TypeA, arg_2 : Type B) where I could supply an impl for TypeA == i32, TypeB == i32, another impl for TypeA == i32, TypeB : PrimInt and yet another impl for TypeA : _, TypeB : _. Anyway, I could build this behavior in terms of nested HashMaps, or hashing together all of the arg types into a key or a number of other ways. So the runtime cost is really the big factor.

Well, since TypeId is something that the compiler computes at compile time, and they don't change at runtime, you can theoretically build a perfect hash function over your set of types, which means that dispatching them will cost only a single array indexing.

At this point, however, I have to ask: what are you trying to do at the high level? Emulating the complete trait system doesn't sound like something particularly productive. Perhaps what you are trying to achieve can in fact be done using regular traits.

1 Like

I'm essentially building the runtime of another (experimental) programming framework (programming language minus any syntax) on top of Rust. I am hoping to stay as close as possible to using Rust's primitives for performance and compatibility reasons, but the other framework has different typing rules. (along the lines of gradual typing)

There are two things I want that the regular trait system doesn't provide (or that I couldn't figure out how to get, I should say)

1.) impl Specialization. i.e. I want to be able to provide an impl for a specific type (or multiple specific types if the trait has a higher arity), but then also provide a blanket implementation. So if the specific impl doesn't exist it will use one of the fallbacks as opposed to causing a compile error.

2.) Fn arg types affect the return types. So my_fn(5 as i32) might return i64, but my_fn("hello") might return String.

Thanks again for your help / advice.

This seems legitimate, however, be aware, that this is not as simple as that. In particular, determining which of any set of types is most specific is not at all obvious, and you will probably have to think about it and define additional rules (like the infamous "lattice rule" in the case of Rust's specialization).

This is already doable using the trait system of the stable language today. Apart from the fact that Fn* traits are unstable to implement, if you define your own function trait, you can implement it multiple times for a single type; you just have to know that argument types should be generic type parameters, and the return type should be an associated type, like this:

trait MyFn<Arg> {
    type Ret;
    
    fn call(self, arg: Arg) -> Self::Ret;
}

#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
struct NotAFunction;

impl MyFn<i32> for NotAFunction {
    type Ret = i64;
    
    fn call(self, arg: i32) -> Self::Ret {
        arg.into()
    }
}

impl MyFn<&'_ str> for NotAFunction {
    type Ret = String;
    
    fn call(self, arg: &str) -> Self::Ret {
        arg.into()
    }
}
3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.