Limitations of generated functions versus macros or functions

Tentatively writing a generated function and relearning things, and ran into a couple questions:

  1. The most warned limitation of generated functions is most side effects in the method body (throwing errors is a notable exception). The docs currently words the rationale:

The number of times a generated function is generated might be only once, but it might also be more often, or appear to not happen at all. As a consequence, you should never write a generated function with side effects - when, and how often, the side effects occur is undefined. (This is true for macros too - and just like for macros, the use of eval in a generated function is a sign that you’re doing something the wrong way.)

I never noticed the mention of macros there. Macro calls are documented to execute when called at parse-time, with no mention of caching like generated functions, so are side effects in macro bodies really undefined behavior? I wouldn’t want macro calls to depend on global state or anything, but the documented parse-time println examples with no warnings seem to contradict that.

  1. I never fully understood why a generated function’s method is stuck in the world age during definition, e.g. callees only use prior defined methods. Issue #23223 suggests to me that the core reason is the method’s execution being part of compile-time, but normal functions can opt to execute callees at compile-time without being stuck in a world age. In fact, my primary consideration now is a semantic guarantee of compile-time computation, no side-effects of course. Is the generated expression stuck per MethodInstance somehow? Hypothetically, if there were a @compiletime hint in normal functions e.g. foo(x::T, ::Val{N}) where {T, N} = x + @compiletime(bar(T, N)) to do what I intend, would that somehow avoid the limitations of generated functions except for side effects?
1 Like

maybe JuliaC is a better fit? in any case I don’t think @generated really provides this guarantee in the way that you want it because it’s allowed to run the @generated function whenever and as often as it wants (potentially many more than 1 times).

1 Like

Just to clarify, I mean computing something from argument types and parameters in the generated function’s body, before simply interpolating it into an otherwise unchanging generated expression. Ideally occurs once and gets cached, so good catch on that not being guaranteed. How could JuliaC help? Haven’t really looked much into it, haven’t heard of any compile-time guarantees or hints.

There’s just so many ways a Julia program might get run. It might be interpreted, it might be JIT’ed, it might be precompiled into a package cache, it might be in a sysimage, it might be compiled dll/so/dylib or executable. When’s parse time? Heck, I don’t even know when compile time is (or if it exists at all).

So instead, better to use normal functions. If they need help coaxing some compile-time optimization in some situation, I’d look to @assume_effects.

1 Like

in particular, @assume_effects :foldable and ensuring the arguments are known at compile time (e.g. const or are types) I would expect goes a long way to making things happen at compile time.

1 Like