The Crystal Programming Language Forum

How to collect all calls of a macro over the entire program?

I want to implement a macro which wraps a value anywhere in the program so as to make all such values editable through some centralized “console” at run time. This means explicitly declaring some global storage for it and substituting the expression with an accessor to that storage.

The obvious attempt at it doesn’t work:
https://carc.in/#/r/9hzd

broken code
Vars = [] of {String, Int32}

macro v(expr)
  {% Vars << expr %}
  Values.var{{Vars.size}}
end

module Values
  {% for var, i in Vars %}
    class_property var{{i + 1}} = {{var}}
  {% end %}
end

def foo
  v(6.0)
end

puts v(":o")
puts foo

Values.var1 = 9.0

puts foo

because the iteration over all Vars happens before all of them could be populated.

I could not find any way to ensure that the iteration runs “at the very end of everything” so that all those macro invocations managed to finish.


I could switch to doing this at run time, but this ends up really weird, especially because I want to add more features, such as a callback to run whenever that variable is changed.

https://carc.in/#/r/9hzf

weird working code
Values = {} of Int32 => (Float64 | String)

macro v(expr, line=__LINE__)
  (Values[{{line}}] ||= {{expr}}).as(typeof({{expr}}))
end

def foo
  v(6.0)
end

puts v(":o")
puts foo

Values[10] = 9.0

puts foo

Maybe it looks OK in this shape, but it doesn’t work reliably. And funnily enough, a variable isn’t possible to add to that Hash until its first access.

The problem is that macros inside methods are only expanded when those methods are analyzed, and that happens after macro finished is executed. macro finished is only executed once all the top-level code is traversed.

I don’t think there’s any way to do what you want.

That said, I didn’t think stuff like Lucky or the things that @Blacksmoke16 does with annotations were possible with the existing language, so I might be wrong.

I did something similar in https://github.com/bcardiff/afix but my approach there was to perform explicit instrumentation step to add a + AfixMonitor.int(key) to integer expressions. They value for each key will be provided by the runtime.

I kind of like the second “weird working code”, (although i need to use Values[8] = 9.0). Why is it not working reliable? Besides the impossibility of setting falsey values?

Using a global counter and monkey-patching do the job:

module Vars
  GLOBAL_COUNTER = [nil]
end

macro add(value)
  module Vars
    class_property var{{Vars::GLOBAL_COUNTER.size}} = {{value}}
  end
  {{Vars::GLOBAL_COUNTER << nil }}
end

add "e"
add 123

p Vars.var1 #=> "e"
p Vars.var2 #=> 123

Edit: nvm, won’t work inside a method.

Gotta push it to the limit! :laughing:

Another take:

module Vars
  module Container
  end

  struct Values(T)
    include Container
    
    protected getter values : Array(T)
    
    def initialize(@values : Array(T) = Array(T).new)
    end
    
    def add(value : V, &) forall V
      if value.is_a? T
        @values << value
      else
        yield Values(T | V).new @values + [value]
      end
    end
  end

  @@values : Container = Values(Nil).new

  def self.add(value) : Nil
    @@values.add value do |values|
      @@values = values
    end
  end
  
  def self.values : Array
    @@values.values
  end
end

Vars.add "abc"
def m
  Vars.add 123
end
m

p Vars.values[0] #=> "abc"
p Vars.values[1] #=> 123

I recently adopted a new pattern regarding annotations: https://play.crystal-lang.org/#/r/9i57.

You can defer the logic to instantiate a type based on annotation data to inside the type itself via a class method and generic. It also works out quite well to map annotations to structs that represent the data that should be read off that annotation. You can then pass these structs to each class method and use the data within those structs to instantiate the type (such as the value property in that example).

That solution seems functionally equivalent to mine (“weird working code”)

What API would you like not to be weird, accessing through class methods generated instead of index for Array or String for Hash?

Sooo anyway, what I’ve really been looking for is a way to mimick C’s static keyword for defining variables. Which, if you’re not familiar, is basically a global variable but one that is scoped locally (which also lets its initialization be located close to the code where it’s being used).

And now I’ve finally done it!

struct StaticValue(T)
  @@values = {} of Symbol => Void*

  def initialize(@key : Symbol)
    @@values[@key] ||= Box.box(yield)
  end

  def val : T
    Box(T).unbox(@@values[@key])
  end
  def val=(val : T)
    @@values[@key] = Box.box(val)
    val
  end
end

macro static(var, file = __FILE__, line = __LINE__)
  {% key = {file, line, var.target}.symbolize %}
  {{var.target}} = StaticValue(typeof({{var.value}})).new({{key}}) { {{var.value}} }
end

def f
  static x = 5
  x.val += 1
end

def g
  static x = "a"
  x.val += "b"
end

p! f() # => "6"
p! f() # => "7"

p! g() # => "ab"
p! g() # => "abb"

https://carc.in/#/r/9ouk

4 Likes