Crystal: Inconsistant macro behavior

Created on 25 May 2016  路  6Comments  路  Source: crystal-lang/crystal

The example below is not properly making use of macro defs, yet the inherited method seems to work like a macro method on line 20. It does not work the same way when called on line 26

https://play.crystal-lang.org/#/r/zks

bug compiler

Most helpful comment

We'd have to first ask the question: when would you want to use {{@type}} but not have the method be a macro def. My thought is: if I use {{@type}} is because I want it to change according to the class having it. Otherwise I would use Filter.

Stepping back a bit, imagine there's no concept of macro defs in the language, and {{@type}} always resolves to the most specific type in a method. That the compiler internally marks the method as "this needs to be instantiated for each specific type" is an implementation detail. That's the behavior I want to define, just that @type always resolve to the most specific type.

This way the language becomes more intuitive too. You just define methods, and the compiler takes care of solving @type.

All 6 comments

Slightly simplified:

class Filter
  def whoami
    {% begin %}
      {{ @type }}
    {% end %}
  end
end

class Thing1 < Filter
  def whoami
    "I'm overridden"
  end
end

class Thing2 < Filter
end

thing1 = Thing1.new
thing2 = Thing2.new
puts thing1.whoami
puts thing2.whoami
instances = [thing1, thing2]

instances.each do |instance|
  puts "#{instance} (#{typeof(instance)}) :: #{instance.whoami}"  
end

https://play.crystal-lang.org/#/r/zls

The issue is that the outcome nor behavior of a method should change depending on what the compile time type of a variable its called on is (and ideally only the runtime type should matter). The easy fix is to make @type always refer to the type the current method is defined in, however having it refer to the runtime type the method is called upon seems to be a bit more intuitive.

Perhaps macro methods should move from an explicit language construct to an implicit one, a method that calls into the macro language becomes a macro method.

This is not a bug, it's how the compiler and language work. In the case of thing2.whoami the compiler knows thing2 is a Thing2, and when you invoke a method on it the compiler instantiates a method for that specific type. Yes, this might result in some code duplication at the end (in the generated code), but after optimizations and inlining this duplication is usually gone.

I don't understand what's the problem though. Did you forget to say its a macro def and you expected to notice the mistake sooner, or do you need this behaviour?

My idea was to automatically make all methods that have {{@type}} inside them become macro defs automatically. I think this is what you'd always want. See #2565 . Because if you don't want the {{@type}} to change, you can just put Foo instead.

@asterite I totally forgot to say its a macro def and did expect to notice the mistake sooner.

From my view whoami is being called in both cases on an instance of Thing2 but yielding different results, which seems like a bug to me. If I understand what you've written it's because in one case the compiler knows the variable is a instance of Thing2 and in the other it only knows that the the variable is an instance of a Filter, so the result makes perfect sense.

You idea to automatically make all methods that have {{@type}} inside them become macro defs automatically seems to make sense on some level, though personally I'm a little concerned about implicitly changing behaviour based on the presence of a keyword.

How do you feel about having the compiler print a warning instead?

We'd have to first ask the question: when would you want to use {{@type}} but not have the method be a macro def. My thought is: if I use {{@type}} is because I want it to change according to the class having it. Otherwise I would use Filter.

Stepping back a bit, imagine there's no concept of macro defs in the language, and {{@type}} always resolves to the most specific type in a method. That the compiler internally marks the method as "this needs to be instantiated for each specific type" is an implementation detail. That's the behavior I want to define, just that @type always resolve to the most specific type.

This way the language becomes more intuitive too. You just define methods, and the compiler takes care of solving @type.

@asterite Ah, okay that totally makes sense. Thanks for the explaination

I just pushed a fix for this. With this change your code prints:

I'm overridden
Thing2
#<Thing1:0x10b3f3f90> :: I'm overridden
#<Thing2:0x10b3f3f80> :: Thing2

which I guess is more expected.

As a side node, in this case just {{@type}} inside the method is good, there's no need for {% begin %} ... {% end %} here.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

pbrusco picture pbrusco  路  3Comments

jhass picture jhass  路  3Comments

oprypin picture oprypin  路  3Comments

asterite picture asterite  路  3Comments

costajob picture costajob  路  3Comments