I agree it can be easy to make and throw away big arrays if you're not aware of what's going on. But it can also save a lot of time. For loops as arrays are super useful, integrating the equivalent of "map" into the language itself. We also recently added generator versions (for*). JSX is a nice example where for loops as expressions and implicit return are powerful; see e.g. https://civet.dev/reference#code-children
What might work well is a lint rule to error if a loop expression ends an actual function declaration (i.e. not an inline callback), and the function doesn’t explicitly define a return type. I think that catches almost every bad case, aside from the odd memory leak in really unusual edge cases.
For example, you might have an “x” in some inner loop within a function. Then later on, you’d add an “x” earlier in the function, not realizing that that name was used down below. Suddenly the inner loop is referencing that new variable, and there was no way to tell other than the surprising ways your code would suddenly malfunction.
Hang on, how can you have “everything is an expression” without implicit return? If I’m correctly understanding what you’re talking about, implicit return isn’t actually a thing—it’s just that the function body is an expression too.
(Never dealt with CoffeeScript, but I’ve been writing Rust for over a decade now, and learned to love expression-orientation there.)
The other big one, which unfortunately Civet seems to be doing too, is implicit return combined with “everything is an expression”. You can have one of those, but not both. With both, it’s far too easy to write loops intended to be statements, and which accidentally turn into gigantic multi-dimensional array returns which can’t easily be optimized out by the compiler. Fortunately, this would be only mildly inconvenient to work around with a lint rule that forces explicit return in all functions.