cmake_language(DEFER) not evaluating variable references is inconsistent

The docs for the new cmake_language(DEFER) subcommand added for CMake 3.19 say the following:

Variable references in arguments are evaluated at the time the deferred call is executed.

This makes this command behave differently to other CMake commands (and even other subcommands of cmake_language()) and is rather unintuitive. If the user wants to, it is easy to avoid variable dereferences by using bracket syntax, so I’m wondering why the command was given this inconsistent behavior. I’d recommend we avoid this inconsistency and evaluate variables just like other commands do. We should make this change now since the 3.19 release is the first version in which it will appear.

@kyle.edwards @alcroito Sorry, I meant to CC you both in the original message.

Check Brad’s reasoning at https://gitlab.kitware.com/cmake/cmake/-/merge_requests/5262#note_835244
and my comments before that.

Thanks, yes now I recall seeing that back then but didn’t get to follow up at the time. I’m not sure I necessarily agree with the reasoning stated there. @brad.king’s comment:

The existing cmake_language(CALL <command> [<args>...]) evaluates <args> after the command name at the point at which the call is made. No additional layer of evaluation is added. It just happens that the call is made immediately in that case.

I recall having previous discussions about that, see issue 20707 for the details. To the user, this appears as an internal implementation detail, since it still looks to them as though variables are evaluated as normal. Maybe there’s a corner case where it makes a difference, but I note that there is no mention in the docs about this delayed evaluation.

Continuing with Brad’s reasoning:

In the DEFER case, the CALL has the same argument evaluation semantics, but the time the call is made happens to be later. Delaying argument evaluation:

  • is consistent with the existing CALL signature,
  • is consistent with the documentation stating that the call is made as if written at the end of the CMakeLists.txt file,
  • provides a lot of power from which one can opt-out with EVAL anyway, and
  • happens to be easier possible to implement.

I’m suggesting that while the DEFER case might be consistent with the implementation of the CALL signature, it doesn’t seem consistent with the perceived behavior. Also, rather than having to opt-out using EVAL, one could effectively opt-in by using bracket syntax to avoid evaluation at the call site.

Reading back over the discussions again in issue 20707, I can’t say how feasible it is to evaluate variables immediately at the call to cmake_command(). I’ve raised my concerns about consistency and intuitive behavior, but I’ll have to defer to @brad.king on this.

I thought extensively about this when implementing the feature and stand by my justifications for delayed evaluation. As just one example, without it the following would not work:

cmake_language(DEFER CALL set var "value")
cmake_language(DEFER CALL message "${var}") # prints "value"

On the implementation side, it is not really possible to implement anything else anyway. Every command does its own argument evaluation after cmMakefile calls it, and there is no way to tell commands that their arguments have already been evaluated. Adding one would require updating every single command’s implementation to have such special cases.

I consider this decision closed.