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.

Hi, I am having trouble using cmake_language(DEFER) for the aforementioned reasons.

I have a function like function my_finalizer(some_param) which should be automatically scheduled to be called from within another function function(do_stuff some_param)

Initially in my do_stuff() function I tried:

cmake_language(DEFER CALL my_finalizer "${some_param}")

but that obviously failed, for the reasons outlined above.

I even tried the following:

cmake_language(DEFER CALL set "some_param" "${some_param}")
cmake_language(DEFER CALL my_finalizer "${some_param}")

which in hindsight, is obviously no better than the single line I tried before, but I am a bit desperate here, ngl :expressionless:

Sorry if I overlooked anything obvious, but how would I achieve what I want here with the way how cmake_language(DEFER) works currently ?
PS: I would really like to not rely on any global variables / global state … I know that this would work, but IMO is really bad practice.

Thanks for any help

If I understand your difficulty correctly, you need to wrap the cmake_language(DEFER) in a cmake_language(EVAL) in order to force the immediate evaluation of the variable. Something like the following is what I think you’re looking for:

cmake_language(EVAL CODE
    "cmake_language(DEFER CALL my_finalizer [[${some_param}]])"
)

Thanks for the quick reply @craig.scott

The code you gave indeed does work for me :raised_hands:

And now I had a second look at the cmake_language(DEFER) manual page, and indeed this is mentioned, but I had to scroll way down to the ‘examples’ section of the page.

I think it would be worthwhile to note this behavior also at the very top of the documentation.

There is already a sentence hinting at this behavior at the top of function documentation, but the workaround for it is not mentioned:

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

I would suggest to enhance that a bit like so:

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

If you need variable references to be evaluated at the call-site of cmake_language(DEFER), you need to wrap it in a call of cmake_language(EVAL CODE) and surround each of the variable references with square brackets [[${variable}]]

(see the Deferred Call Examples for details)

Thanks