> makes fexpr a kind of all powerful form that can do both what macros and functions can do.

A fexpr cannot do everything a macro can do.

A macro can be expanded upfront, so that even code that is not reached during execution is expanded.

The separate macro expansion pass can take place in a different host environment, such as a developer's build machine. The expanded code then executes elsewhere: a target machine different from the developer's machine.

A macro can, for instance, grab a text file on the build machine, massage it and turn it into a literal (or even code, obviously).

Macro expansion can perform basic checks on code. For instance, in TXR Lisp, unbound variables are reported by the macro expander, before the code is executed, and even if it isn't.

This error is caught during expansion. The function is still interpreted:

1> (defun foo () (list a (cons)))
** expr-1:1: warning: unbound variable a
foo
2> (fun foo)
#<interpreted fun: foo nil>

Cons not being given enough arguments is caught in the compiler:

3> (compile 'foo)
** expr-1:1: warning: cons: too few arguments: needs 2, given 0
#<vm fun: 0 param>
4> (fun foo)
#<vm fun: 0 param>

Application-defined macros can implement their own static checks. Commonly, macros check arguments for validity and such, but more advanced checks are possible.

For instance, in the awk macro provided the same Lisp dialect, it's possible to perpetrate a situation in which code that looks like it is scoped a certain way isn't. The macro diagnoses it:

5> (awk ((let ((x 1) (y 2)) (rng x y)) (prn)))
** expr-5:1: warning: rng: form x
is moved out of the apparent scope
and thus cannot refer to variables (x)
** expr-5:1: warning: rng: form y
is moved out of the apparent scope
and thus cannot refer to variables (y)
** expr-5:1: warning: unbound variable x
** expr-5:1: warning: unbound variable y

The issue is that (rng ...) expressions, which test for a record being in a range indicated by two conditions, are not evaluated in the apparent scope where they are physically located. They are evaluated when a new record is read and delimited into fields, before the ladder of condition/action pairs is evaluated. The information is recorded in a hidden vector of Boolean values, and when the (rng ...) expression is encountered, it simply retrieves its corresponding pre-computed Boolean.

A fexpr could implement this check but (1) it would be expensive at run-time to be digging into scopes, and (2) it would have to be executed and (3) it would need some stateful hack to avoid diagnosing repeatedly.

Another thing fexprs cannot do is to be debuggable via inspecting an expansion. To debug the form which uses the fexpr we have to debug into the fexpr. To debug a form using a macro, we can inspect the expansion. We can understand what is wrong in terms of the behavior of the expansion, which can be very simple compared to the macro. Then work backwards to understand why the bad expansion was produced. At that point we can dig into the macro, and that is a different problem: a pure code generation problem we can likely debug in isolation in a development system.

Yet another thing fexprs cannot do is simply go away entirely in a software build. We can stage macros such that their definitions are available at compile time, but are not propagated into the compiled program.