How to work around map-<expr> pre-evaluation in macros?

It seems expression mappings are evaluated in advance of a macro being executed rather than being evaluated at the time that the mapped left hand side is encountered. Consider the following example:

function! X()
  let line = line('.')
  let col = col('.')
  echomsg "Pressed x at (".l:line.", ".l:col.")"
  return "x"
endfunction

nnoremap <expr> x X()

After sourcing this script, place your cursor at the start of the last line and record the macro fpx. If you look in :messages you’ll see “Pressed x at (8, 8).” If you then undo the change, return your cursor to the start of the last line, and play the recorded macro, you’ll see “Pressed x at (8, 1),” which shows the function is evaluated before the macro is executed.

This creates a surprising difference between behavior during recording and behavior during replay. Is it possible to circumvent this while still using an expression mapping?

It seems like a bug to me.

When using your example and recording the macro fpx;x I see echo’d:

Pressed x at (8, 1)                                                                                                             
Pressed x at (8, 12) 

So the first x is wrong but the second one is right.

Also, when I do lfpx, I see echo’d:

Pressed x at (8, 2)  

So the expression was evaluated after the macro had started (l executes before x is evaluated). Maybe file an issue.

Thanks for digging into this; I’ve opened an issue. We can continue the discussion there.