Summary: Explicit 'async' functions avoid implicit messiness and allow control over which sections of code are running at one time.

> From what I understand, in JavaScript at least, putting `await foo()` inside an async function, splits the calling function in two, with the 2nd half being converted to a callback

I wouldn't say that's wrong, but it may be a slight over-simplification if your mental-model isn't complete.
For example you can await from within a loop which would make function-splitting kind of a strange thing to think about.
But yes, every `await` is a suspension point that will be resumed and the function carries on, so effectively
the rest of the function becomes a callback, but let's call it a "continuation", which is the callback that
continues from where we left off.

Of course it's implementation specific, but at a high level these continuations are basically normal functions which
may return either the "incomplete" pair (new_awaitable, next_continuation) for `await new_awaitable` statements, or the
"complete" return-value for `return` statements. (Maybe other things like exceptions, but let's not go there)

It's the event-loop/task-scheduler which keeps track of these callback/continuations: when one returns with a complete-value,
the scheduler will go find the continuation that was awaiting it, and call that with the new value. If it returns a new
(awaitable, continuation) object, the loop will start the awaitable and repeat until it returns a value, then resume the continuation with the value.

So you can think of await as "return to the event loop with awaitable and keep calling the callbacks until a final value is returned".

This is where the function coloring comes from: async functions need access to that event loop to keep returning
to and continuing from, they know they're being called in a special way and return these special values for await/return that
the event loop knows how to deal with; normal functions don't have access and just return values in the "normal" way.

Of course, to reap any benefit from this you have to have multiple tasks running "at the same time". To just await one thing which awaits one
thing is not much different than a normal function call/stack. But when you're listening on sockets and fetching HTTP resources
and making database queries and waiting for user responses, each executing simultaneously but on one thread (so no thread contention),
that's when async shines. But note: they all must be talking to the same event loop which manages all these coroutines, running
one at a time.

But what happens if "normal" functions could await?

Put another way: effectively de-color functions by making them all "async" and thus able to await. (Unless I'm misinterpreting the initial question)

From what I said above, all we have to do is change the implementation such that when a function returns, internally it actually this
special "complete" variable type (with the real returned value inside). Then, with an event loop you get normal behavior and forward
to whatever function had called it. Then you just add the awaits which return "incomplete" variables and you get waiting behavior.

This would work, BUT it introduces an overhead of going to the event loop checking if the return is "complete", finding the caller,
and forwarding it for EVERY function call. Rather than the usual stack popping/register storing of standard languages.
Optimizable? yeah probably, but it's not zero-cost, especially true in languages like Python which doesn't have an implicit event loop like JavaScript,
so you're making huge changes to the implementation to begin with.

We could approach from the other side: returns stay the same, but the act of calling await creates an event loop or some object
which does the "callback-the-callbacks" routine.
I guess this would be the stop-the-world approach? This one function would keep looping through the callbacks until a final value reached.
On second thought, this isn't different than a standard function call, because any internal `awaits` would setup their own loops and you always
end up with a single returned value (or infinite loop). There's no way for multiple loops to running the continuations.

If you really don't like the keyword "async" before your function definition you could implement
implicit async functions: just make any function in your language which includes an `await` implicitly become "async".
But this only kicks the can down the road, as if you use await, that means you're returning continuations, and any
callers need to await your value, and therefore your function is now implicitly colored.

And how do you implement the equivalent of

async function() { return 42; }

maybe?

function() { return 42; await; }

Implicit event loop.

Maybe this is the core of the question: for a language like JavaScript which already has an event loop, why can't we do

function foo() { let x = await whatever(); return x; }
console.log(foo());

The await in a non-async function could be implemented such that it goes out to the global event loop, "registers" itself as dependent on
the Promise returned from "whatever()" then sit and wait for the event loop to return the value. This should sound familiar to you as
the behavior of the standard async function / continuation business, but now we're trying to say that foo doesn't return a stream of continuations,
(it just takes a long time to run).

While this may be doable, a problem arises if foo was called from within an async function. Part of "contract" of async programming is your function is
run as usual between calls to await; at those points you return to the event loop and let other continuations run, but until the next await,
shared/global variables will remain untouched because nobody else is running. If you called foo() which then starts running `whatever` on the event loop
again any guarantees about ordering of events or state of variables may change unexpectedly.