Many practical applications of eval() boil down to accessing (and perhaps manipulating) scopes in a manner which the host language does not normally allow. Likewise, many potential dangers or pitfalls of eval() relate to how it allows (or does not allow) dynamically-assembled code to inherit access to some spatial or temporal scope of variables, definitions, "unsafe" libraries, and so forth. The interaction of eval() with scope is much a much more interesting design question than whether it accepts an AST, a string, or something in-between, except to the extent that non-stringy "programs" represent some form of reified closure.
I mean, you aren't wrong. But, little bobby tables is essentially what happens when you build up eval statements using a string, no?
if and only if a piece of eval'd code has the ability to produce harmful (let alone observable) side effects, which in a functional language will in turn largely be a consequence of what is imported into the code's scope.
Strictly, this isn't true, either? If you can influence what is returned from the evaluation, that may be enough to cause bugs, no? Without needing any access to scope or side effects. Consider a case where you use sql injection style modification to change an authz query so that it returns indicating the current user has admin rights. (Notably without making any external change in doing this.)
Granted, I fully cede that your point is largely right. Just feels like you are a bit too strong with the "if and only if" on it.