Macro finished

I had a discussion with Gemini about how things ought to work.

It revolved around using the finished macro to add code to register classes. But it didn’t work (the exact case can be fixed with included, but that’s another matter).

The docs state:

finished is invoked after parsing finished, so all types and their methods are known.

But a simple test:

module MyMod
  macro finished
    puts "Finished macro for {{@type.name}}"
  end
end

class MyClass
  include MyMod
end

puts "End of main"

Shows it’s in fact being run at the end of program:

~/d/brainstorm ▶ crystal run macro_test3.cr
End of main
Finished macro for MyMod

So, is the docs or the implementation buggy?

The difference becomes significant when using HTTP::Server, if you follow the docs and think finished gets called before the server starts (and exits).

I think maybe it could be phrased differently, but “end of the program” and “after parsing finished” are essentially analogous no? You can’t be done parsing until you reach the end.

This example matches with my mental model of how finished works. Mainly can think of it like a FIFO queue. As parsing happens any finished macro it finds gets pushed into the “queue”. After everything has been parsed the macros are invoked in the order they were pushed into the queue. If a finished includes another finished then those are also pushed into a fresh queue and the process repeats until there are no more finished macros left. E.g.

module MyMod
  macro finished
    puts "Finished macro for {{@type.name}}"
    macro finished
      puts "Finished macro for {{@type.name}}2"
    end
  end
  
  macro finished
    puts "Finished macro for {{@type.name}}3"
  end
end

class MyClass
  include MyMod
end

puts "End of main"

Yields:

End of main
Finished macro for MyMod
Finished macro for MyMod3
Finished macro for MyMod2

EDIT: But yes, if you did have a server.listen then your puts in a macro finished would indeed not run until after the server exits. From what I have seen most people use finished for compile time things and not for a runtime use case. Could you expand on what you’re wanting to do?

I don’t understand what you see as a problem, except maybe the wording details between “after parsing” vs. “end of program”. Perhaps we could clarify that a bit better, but in my understanding it doesn’t make a big difference.
Is there anything else you’re surprised about?

From the original example it might seem like you’re expecting some other value for {{@type.name}}, perhaps?

The mental model I have (and maybe OP as well) is the sequence: parse → compile → run, so “after parsing finished” should happen between 1 and 2.

Yeah, what @Carlos said. “After parsing” gives me the impression that it runs, well, after parsing. I realize that nothing really runs “after parsing”, it needs to be compiled, and the program actually run, but then I would expect it to run before the main program.

Well, it was actually Gemini that was trying. It was using it to collect the implementors of a module. Thing is, it worked fine in specs, but not when the program ran a HTTP::Server. When pointing out the problem, it figured out that included worked better.

Well, it’s the discrepancy, as I see it, between the documentation and the implementation. My personal feeling is that it should behave as I said at the beginning of this comment, but it’s non something I feel that strongly about. If finished means “runs after the main program”, that’s what the docs should say.

I think it also doesn’t help that there may be some confusion around when the finished macro is invoked and when the invoked code runs. E.g. if you switch all your puts calls to macro puts:

module MyMod
  macro finished
    {% puts "Finished macro for #{@type.name}" %}
    macro finished
    \{% puts "Finished macro for #{@type.name}2" %}
    end
  end
  
  macro finished
    {% puts "Finished macro for #{@type.name}3" %}
  end
end
 
class MyClass
  include MyMod
end
 
puts "End of main"

You get something that may be more aligned with what you were thinking:

Finished macro for MyMod
Finished macro for MyMod3
Finished macro for MyMod2
End of main

So maybe the missing piece just needs to be something like:

Runtime code generated within a macro finished is inserted at the end of a program.

Or something along those lines. As macro finished itself IS being invoked after all parsing has finished but before the program runs, its just the runtime code they’re generating is inserted at the end.

Good point.

Oh hey I wrote these docs!

Macros are run after parsing, it’s just that in the example here the macro doesn’t print anything, so you can’t see that it ran.

Take this example:

puts "I run at runtime"
{% puts "I run at compile time" %}

When you compile the program, the macro is run after parsing and the message is printed. When you run the binary, you get the other message:

$ crystal build test.cr
I run at compile time
$ ./test
I run at runtime

Brilliant

Well, yeah, the macro runs, but not its “content”. I find the Crystal docs to be of excellent quality, so I was a bit tripped up by that statement that’s technically 100% correct but might lead people a bit astray. I’m guessing that properly differentiating between macro and program runtime is one of the more challenging things for most people that comes from languages without this kind of thing.