The Crystal Programming Language Forum

Macro expansion in the type argument of generics

Following code which includes a macro expansion on generic argument wrapped with {% begin %} .. {% end %} works fine.

class A; end

class G(T)
  def a
    puts T
  end
end

{% begin %}
  {% t = A %}
  v = G({{t}}).new
  v.a
{% end %}

Following code does not work but can be acceptable if this is considered as the macro expansion does not expand to complete code (I don’t think so, though).

class A; end

class G(T)
  def a
    puts T
  end
end

v = G({{A}}).new
v.a
syntax error in eval:9
Error: unexpected token: {{

Using G({{x}}).new in macro methods is fine:

class A; end
 
class G(T)
  def x
    p T
  end
end

macro a(x)
  G({{x}}).new
end

f = a(A)
f.x

I have put this into a macro method with block passed to another macro method, and this also works fine:

class A; end
 
class G(T)
  def x
    p T
  end
end
 
macro b(&)
  {{yield "A".id}}
end
 
macro a(x)
  G({{x}}).new
end
 
macro c
  b do |t|
    a(\{{t}})
  end
end
 
f = c
f.x

However, the compiler rejects when we bypass the use of macro a:

class A; end
 
class G(T)
  def x
    p T
  end
end
 
macro b(&)
  {{yield "A".id}}
end
 
macro c
  b do |t|
    G(\{{t}}).new
  end
end
 
f = c
f.x
There was a problem expanding macro 'c'


Called macro defined in eval:13:1

Which expanded to:

 > 1 | b do |t|
 > 2 |   G({{t}}).new
           ^
Error: unexpected token: {{

The question is:

  • Is the error in the last example the same reason as the second example?
  • Why macro expansion on the argument of generics is not possible without {%begin%}?

yield inside macros is something that I can’t remember how it works. It’s not documented and it’s not clear how it works. So I wouldn’t use it.

Because nobody needed it. And you can always do it by surrounding things with {%being%}.

Macro yield should just inline the AST of the block body. AFAIK there’s no difference between yield and block.body.

And the observed behaviour is identical without b and its macro block.

macro c
  G(\{{A}}).new # Error: unexpected token: {{
end

The result of macro c is equivalent to the code in the second examples. So yes it’s the same reason why this is invalid.

I don’t think there’s a technical reason to disallow macro expressions in top-level scope. But as @asterite said, there has never been a strong use case calling for that.
And I honestly like that macro contexts are restricted to macro defs or {% begin %} blocks. That ensure encapsulation. Related macro expressions all over the code base doesn’t strike as a good idea.

3 Likes