Is it possible to run a macro at the very end of compilation?

Hey all,

Long time user of crystal, but first time writer to the forums :) A huge thank you for writing such a fantastic language!

I’ve been working on a shard (cr-i18n)[GitHub - Vici37/cr-i18n] that provides localization support for the crystal language. It has a lot of overlap and inspiration from (crystal-i18n)[GitHub - crystal-i18n/i18n: 🌐 An internationalization library for Crystal.], but offers compiler support via macros to verify if labels are valid or not rather than waiting for runtime errors.

Right now I have a macro label that will raise a compiler error if a compiler flag is used and the macro is invoked with a label identifier that doesn’t exist. I also want to add a feature where the compiler will tell you any labels that exist in label files that aren’t used in your program, but I’m struggling with this one.

Through compiler constants, I can record wherever the label macro is used, including the identifiers it sees, but I have no way of knowing when the compiler is done expanding macros (and therefore we’ve seen all instances of the label macro usage, and can now safely throw an error for any labels not used so far).

As an example, say I have the label yaml file:

labels:
    one: one
    two: two

Then the outcomes I would expect:

# Macro to read and load label identifiers into constansts
CrI18n.compiler_load_labels("mylabels.yml") # => knows identifiers ["labels.one", "labels.two"] exists

label(labels.one) # => "one"
label(labels.three) # => compiler error, `labels.three` doesn't exist
# Compiler error, `labels.two` defined but not used

Any suggestions? I suspect this isn’t possible with today’s macro hooks, but am open to being pleasantly surprised :) The finished macro looks to be the closest, but that seems to be run after types have been read in, but before method definitions (and where the majority of the label macro usage) have been figured out.

I’m not sure if there’s a macro that works how you need, but here’s some pretty active I18n shards that maybe you can get some ideas from?

1 Like

Thanks @jwoertink! I’ll peruse those, the rosetta one in particular looks interesting!

1 Like

Hello tsornson

Interesting question!, I’ve tried different things, but without success… until I got the trick!
You could use the hook finished at top level to execute your macro code, but it will executed only at the end of top level phase, meaning before the inner of defs.

macro finished
  {% puts "two" %}
end

def foo
  {% puts "three" %}
end

foo
{% puts "one" %}

The trick to to put your macro code inside a def into the finished hook.

def very_end
  {% puts "four" %}
end

macro finished
  very_end
  {% puts "two" %}
end

def foo
  {% puts "three" %}
end

foo
{% puts "one" %}

Here a full example:
https://play.crystal-lang.org/#/r/d8ma

I’m not 100% sure, but it seem to works also when the code is spit in multiple file.

Nice trick @I3oris! Unfortunately it doesn’t seem to work across multiple files. I’m able to delay it enough to capture more usages than I did originally, but not quite all. Thanks though, you’ve given me another approach to think about at least. Might still be something to take advantage of there…

oh…
I thought it worked on multiple file because it worked between two file. However I cannot found an example that doesn’t work with multiple file. Can you share an example when that doesn’t works?

Ack! Sabotaged by my own experimentation - it looks like this trick does work, but I had a previous call in another macro to the equivalent of the very_end definition you provided in your example, which triggered it earlier than I wanted to with only the partial list of encountered labels.

After I remove that call entirely, I’m now able to reliably have some macro code run at the very end of everything.

Thanks again @I3oris!

1 Like

I have used this trick as well, but using two finished macros inside each other to make sure the macro executes even after other finished blocks.

def after_everything
  {% puts "after_everything" %}
end

macro finished
  macro finished
    after_everything
  end
end
4 Likes