It works as it does because when you write return func(arg);, the func(arg) bit executes the function. What is returned is then the result of executing func(arg).
However, you're right that say() doesn't actually return a value, it just logs to the console within that function. The output you're seeing is the result of that log command. If you logged the returned value instead you'd see undefined as the result.
But if you'd passed a different function to exec which did return a value, then you'd need the return in exec() for it to work properly.
P.S. I'm not sure if this is part of what you're asking, but the difference between that and when you wrote exec(say, "hi there"); is that in that code, say is a reference to the "say" function. At that moment it's treated like any other variable, just the same as if you passed in a number or a string, for example.
The difference is the (), which causes the function to be executed, rather than passed as a reference.
In the question you seem to imply that you'd expect the source code of the say() function to be displayed, but this is not what it does, and also is not possible anyway.
exec(say, "Hi, there.");it would be undefined.