crystal
crystal copied to clipboard
Inline constants in the generated code
Discussion
What aspect of the language would you like to see improved?
I'm curious whether we can propagate known constants in Crystal to the emitted LLVM so they can be stored as data.
What are the reasons?
It should ideally result in more efficient code and less strain on the garbage collector.
Include practical examples to illustrate your points.
The specific example that made me think of this is that I want the UI of my app to provide hints for keyboard shortcuts. For example:
MODIFIER_KEY_STR = {% if flag?(:darwin) %} "Cmd" {% else %} "Ctrl" {% end %}
{"Fullscreen", "{MODIFIER_KEY_STR}+F"}
Today, this generates a call to String.interpolation, which allocates a new string on every call. This also happens when concatenating the strings with +. Since MODIFIER_KEY_STR is a constant, I'd hope that we could concatenate the strings at compile-time and store the resulting constant in the binary.
Optionally add one (or more) proposals to improve the current situation.
I have no idea what this would look like in the compiler.
I had a branch for that but I didn't think it was that important. I lost it but I can redo it.
To be fair, I haven't compared measured the impact of performance. I don't actually know whether it's significant. My app barely uses the GC today though, so this topic came to mind when writing the code. It's a few hundred allocations per second that probably don't need to exist
String.interpolation is not guaranteed to be a constant expression, since it is only a regular method. It does not even exist in an empty prelude. Instead you could do the following to force constant evaluation:
{% begin %}
MODIFIER_KEY_STR = ...
{% end %}
{{ "#{MODIFIER_KEY_STR.id}+F" }}
I think the proposal here is for the compiler to notice that the interpolations interpolates a constant, so it could very well interpolate things at compile time.
I actually had a branch with this working, I just didn't sent it to avoid overwhelming the team with such PRs at that time.
Doing so involves running an arbitrary amount of non-macro code during compilation, because there is no bound on the #to_s method's complexity. At this time I wouldn't worry about such an optimization or whitelist certain interpolations when the {{ "#{}" }} workaround suffices.
@HertzDevil This should be limited to constants of type String (or perhaps some primitive types as well?).
Basically, the literal expander would just resolve constant values and if it's of type String, combine it with neighbouring string literals.
But since constants are not immutable, this wouldn't work for arbitrary types anyway, because the constant could have a different value each time it is used in string expansion (simple example: it's an array and elements get added and removed at runtime).
I think there's a misunderstanding here. This is about constants whose value is a string literal. Just that.
MODIFIER_KEY_STR's initializer is a MacroIf that only gets expanded in the main phase, rather than a StringLiteral. The two are not quite the same.
The idea would be to try to resolve that before expanding the interpolation. I'll send a PR later during this week.
Here's the PR: https://github.com/crystal-lang/crystal/pull/12524/files
#12671 was eventually reverted in https://github.com/crystal-lang/crystal/pull/14155 due to https://github.com/crystal-lang/crystal/issues/14150.