Search code examples
crystal-lang

Crystal-Lang - Cross-Macro Macro-variables


So I'm building a data type where, I would like, optional auto-casting. The last question I asked is related to this also.

The code I currently have can be found below:

class Test(T)
  @@auto_cast = false

  def initialize(var : T)
    @var = var
  end
  def self.auto_cast
    @@auto_cast
  end
  def self.auto_cast=(val)
    @@auto_cast = val
  end

  def self.auto_cast(forced_value=true,&block)
    #Force value, but store initial value:
    ac = @@auto_cast
    @@auto_cast = forced_value
      block.call
    @@auto_cast = ac
  end

  def +(val)
    var = @var
    if @@auto_cast
      if var.is_a? String
        casted_arg = val.to_s
        return var + casted_arg
      else
        casted_arg = typeof(var).new(val)
        return var + casted_arg
      end
    else
      if typeof(var) != typeof(val)
        {{raise "Error: Type of <<var>> is not equal to type of <<val>> while auto_cast is false."}}
      else
        return var + val
      end
    end
  end
end

When I try to test the data type however:

Test.auto_cast do
  puts Test.auto_cast
  puts Test.new(1) + "1"
  puts Test.new("1") + 1
end

It throws an error at return var + val:

  if typeof(var) != typeof(val)
    {{raise "Error: Type of <<var>> is not equal to type of <<val>> while auto_cast is false."}}
  else
    ERROR! --> return var + val
  end

At first I was confused why, but now it makes sense.

  1. I believe the Crystal compiler cannot be certain that @@auto_cast will be true whenever I intend to auto_cast (and to be fair, when auto-casting is disabled, I want the syntax error).
  2. A compile error occurs because the value of @@auto_cast is unknown at compile time.
  3. Due to the contradictory nature of the bodies:

.

if var.is_a? String
  casted_arg = val.to_s
  return var + casted_arg
else
  casted_arg = typeof(var).new(val)
  return var + casted_arg
end

and

  if typeof(var) != typeof(val)
    {{raise "Error: Type of <<var>> is not equal to type of <<val>> while auto_cast is false."}}
  else
    return var + val
  end

Each definition should only be used when the user explicitly declares it. Thus this is more suited to a macro.

Given these reasons I started trying to build the functionality into a macro instead:

  def +(val)
    var = @var
    {%if @@auto_cast%}
      if var.is_a? String
        casted_arg = val.to_s
        return var + casted_arg
      else
        casted_arg = typeof(var).new(val)
        return var + casted_arg
      end
    {%else%}
      if typeof(var) != typeof(val)
        {{raise "Error: Type of <<var>> is not equal to type of <<val>> while auto_cast is false."}}
      else
        return var + val
      end
    {%end%}
  end

I thought this would work because, this way code is only ever generated if @@auto_cast is set. However what I forgot was premise #2. I.E. the value of @@auto_cast is unknown at compile time. Ultimately, in order to make this work I would need a variable which can be:

  1. Set at compile time.
  2. Used globally within macros at compile time.

Ultimately I figured I could do something along the lines of this:

SET_AUTOCAST_VARIABLE true
  puts Test.auto_cast
  puts Test.new(1) + "1"
  puts Test.new("1") + 1
SET_AUTOCAST_VARIABLE false

and then in the +() definition:

{%if autocast_variable %}
   ...
{%else%}
   ...
{%end%}

The problem is, I do not think such a macro global variable exists... I was thinking about ways to get around this issue and so far the only solution I can come up with is using some external file at compile time:

{{File.write("/tmp/cct","1")}}
  puts Test.auto_cast
  puts Test.new(1) + "1"
  puts Test.new("1") + 1
{{File.write("/tmp/cct","")}}

and in the method definition:

{%if File.read("/tmp/cct")=="1" %}
   ...
{%else%}
   ...
{%end%}

This feels really hacky though... I was wondering whether there were any other alternatives, (or, even, if this just won't work at all)?


Solution

  • This can't work. Methods are only instantiated once, it is not possible to have two implementations of the same method with the same argument types. In the following example both + methods will inevitably have the same implementation.

    Test.auto_cast do
      Test.new(1) + "1"
    end
    Test.new(1) + "1"
    

    You can't have different implementations of the same method depending on lexical scope. The method is exactly instantiated once and so is the macro inside it.

    I don't understand your overall use case, but maybe there are other ways to achieve what you need.


    For completeness: You can utilize a constant as a macro global variable. Constants can't be redefined, but altered through macro expressions. That can be used to store state between macros. For example:

    FOO = [true]
    {{ FOO[0] }} # => true
    {% FOO.clear; FOO << false %}
    {{ FOO[0] }} # => false
    

    That's pretty hacky, though ;)