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.
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.
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
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: Carcin.
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).
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"