Thank you for your help! Yes, wrapping everything in an escape call works, although it is of course a bit ugly (and throws off Emacs’ indentation).
A colleague pointed out that with Julia 1.11, an issue with @kwdef has been fixed so that it is now easier to use in macros: Support escape expressions in @kwdef by ettersi · Pull Request #53230 · JuliaLang/julia · GitHub
This allows the following code:
macro species(name, body)
quote
@kwdef mutable struct $(esc(name))
const id::Int
$(body.args...)
end
$(esc(name))(id) = $(esc(name))(id=id)
end
end
The change to the first version is simply that the struct name is also escaped, which I personally find cleaner.