How to resolve a Path to TypeNode in a macro inside a module

Code setup

macro emphasize_type(type)
  {% if type.resolve == Int32 %}
    "***Int32***"
  {% else %}
  "***No type***"
  {% end %}
end

class Foo
  macro emphasize_type(type)
    {% if type.resolve == Int32 %}
    	"***Int32***"
    {% else %}
    	"***No type***"
    {% end %}
  end
end

class Bar(T)
  def func1
	  puts emphasize_type(T)
  end

  def func2
	  puts Foo.emphasize_type(T)
  end
end

This works

Bar(Int32).new.func1

[gshah@workstation Desktop]$ crystal delete.cr --error-trace
***Int32***

This does not

Bar(Int32).new.func2

[gshah@workstation Desktop]$ crystal delete.cr --error-trace
Showing last frame. Use --error-trace for full trace.

In delete.cr:25:26

25 | puts Foo.emphasize_type(T)
^
Error: undefined constant T

Why should defining a macro inside a class throw an error?

I think this is a catch of macros in that T is being passed as a Path literal and not the TypeNode that path points to. When you then call .resolve it’s trying to resolve it in the context of Foo so it loses the reference to the actual T. This can be seen more clearly if you run with --error-trace that its failing on the .resolve method:

Error: instantiating 'Bar(Int32)#func2()'


In test.cr:25:14

 25 | puts Foo.emphasize_type(T)
               ^-------------
Error: expanding macro


In test.cr:11:16

 11 | {% if type.resolve == Int32 %}
                 ^------
Error: undefined constant T


In test.cr:25:29

 25 | puts Foo.emphasize_type(T)
                              ^
Error: undefined constant T

In order to pass the type it represents try Foo.emphasize_type({{T}}) which then works.

1 Like

Thanks @Blacksmoke16.
That solves it for simple types like Int32, but not for instances of generic classes like Array(String).

For example, with this code set up

class Foo
  macro emphasize_type(type)
    {% if type.resolve == Int32 %}
    	"***Int32***"
    {% else %}
    	"***No type***"
    {% end %}
  end
end

class Bar(T)
  def func2
	  puts Foo.emphasize_type({{T}})
  end
end

as @Blacksmoke16 stated, this works now.

Bar(Int32).new.func2

[gshah@workstation ~]$ crystal Desktop/delete.cr --error-trace
***Int32***

But this still does not work

Bar(Array(String)).new.func2

[gshah@workstation ~]$ crystal Desktop/delete.cr --error-trace
Showing last frame. Use --error-trace for full trace.

There was a problem expanding macro ‘macro_140386515094528’

Code in Desktop/delete.cr:25:28

25 | puts Foo.emphasize_type({{T}})
^
Called macro defined in Desktop/delete.cr:25:28

25 | puts Foo.emphasize_type({{T}})

Which expanded to:

1 | Array(String)
^
Error: Array(T).class is not a generic type, it’s a metaclass

Can it be made to work with Array(String) ?

I think this is essentially the same problem as before, but one that isn’t solved by {{T}}. Basically Array(Int32) is being passed where Int32 actually is just the name of the generic var, essentially the same as Array(T). Somewhat of a hack but you could use parse_type to parse it into a TypeNode which you could then use. Then check to see if its an Array type, and if so check its type vars to see if any of them are Int32. Something like:

macro emphasize_type(type)
  {% type = parse_type(type.stringify) %}
  {% if type.resolve <= Array && type.type_vars.any? &.resolve.==(Int32) %}
    "***Int32***"
  {% else %}
    "***No type***"
  {% end %}
end

But what exactly are you wanting to do that you need this for? There might be a better less brittle way to go about it that doesn’t involve you needing to handle all these diff cases.

2 Likes

Is that code a simplified version of what you want/need, or exactly what you need? Because if so, you don’t need macros at all:

def emphasize_type(type : Int32.class)
  "***Int32***"
end

def emphasize_type(type : Class)
  "***No type***"
end

class Foo
  def self.emphasize_type(type : Int32.class)
    "***Int32***"
  end

  def self.emphasize_type(type : Class)
    "***No type***"
  end
end

class Bar(T)
  def func1
    puts emphasize_type(T)
  end

  def func2
    puts Foo.emphasize_type(T)
  end
end

puts Bar(Int32).new.func1
puts Bar(Int32).new.func2
1 Like

The code I shared is highly simplified version of what I am trying to do but Blacksmoke’s solution using parse_type works for me.

Thanks everyone for quick response and help with this.