Constraints for `@generated` function

It’s really not clear to me what is and isn’t allowed in a generated function. I get this error:

ERROR: generated function body is not pure. this likely means it contains a closure or comprehension.

Ok, but where’s the problem? Is there any way of narrowing it down? Especially confusing is trying to pin down exactly what closures are unallowable. For example, this code works with no problems:

@generated function foo(x)
    g(y) = :($(one(x)) + $(one(y)))
    g(x)
end

So maybe the limitation is not really with closures, but state that’s stored outside the generated function. Is that right?

My generated code had called functions that call gensym or rand, and I’ve been moving things around with no luck yet. It feels like I’m stumbling around in the dark here, are there any guidelines for debugging generated functions I should be following?

I’ve made progress on this thanks to helpful guidance from @thautwarm. Here’s my current understanding of the constraints, in case it might be helpful to others:

Like macros, a generated function requires building an AST. Most discussions of “code in generated functions” aren’t very clear about this, but there are really two kinds of code involved, with different constraints:

  • There’s the code you use the build the AST. The constraints on this are relatively loose. The big thing is, there are no guarantees regarding when it will execute, so you should avoid referencing global state. Also as I understand it, eval is disallowed here.
    Also, any reference in this code to the value passed to the function will be interpreted not as the value, but as its type (good details on this part in the docs).

  • Then there’s the code that actually ends up in the AST. Here the value passed in refers to the actual value, but you need to interpolate with $ to express it. Also, this portion is very constrained in that it does not allow closures (it’s fine to call a closure from a generated function, but not within the AST. A closure is a function that refers to values outside its scope.

2 Likes

You’ve got the gist of it. To expand a little:

  • The generator code (used to build the AST from the argument types) cannot call functions or methods which are defined after the generated function is defined. So you can’t generally call things like trait functions within your generator if they may be overloaded for user defined types. This may eventually be fixed in a future julia version with some sufficiently clever tracking of function dependencies (“backedges”), we will see.
  • The generated code (which is invoked on the argument values) can call any function, but it may not define new types. This means it can’t use closures because closures are lowered to new types internally. I believe this may also eventually be fixed, but for now we need to put up with it.
7 Likes