Overlapping I/O usually costs you the colour of every function that
touches it. One call goes async, so its caller must await, so that
caller goes async too, and the keyword climbs your call stack until half
the codebase is a different kind of function from the other half. SCUA
overlaps I/O without any of it. There is no async keyword
and nothing to await.
#What colouring actually costs
The tax is not the two keywords. It is that your functions divide into two incompatible sets, and a synchronous one cannot call an asynchronous one without becoming asynchronous itself. Utility code gets duplicated. A library that picked the wrong colour is unusable. Refactoring a leaf function to do I/O rewrites everything above it.
Languages that have it mostly cannot remove it, because the colour is how the compiler knows where to split the function into a state machine.
#How SCUA avoids it
By default SCUA I/O blocks, which is the right default for a script
or a tool: simple, predictable, finished. A host that wants overlap
installs an I/O platform, or you pass --io=async.
Then a capability call inside a message handler parks the handler rather than blocking the thread. The host performs the operation however it likes, completes it by token, and the handler resumes with an ordinary value. The function that made the call is an ordinary function, its caller is an ordinary function, and nothing above it changed.
The embedding example in the repository runs the whole loop:
$ zig build embed-async
SCUA async-I/O embedding demo (scua 0.27.0-dev.b13ad50)
[host] script parked: http GET https://api.example/data (token 1)
[host] completed token 1 with HTTP 200 "pong"; polling...
handler resumed: http.get -> status=200, body="pong"
PASS: script parked on http.get, host completed it by token, poll resumed the handler
The script side of that is http.get(url) returning a
value. There is no callback, no promise and no await, because the
parking happens underneath the call rather than in the type of the
function that made it.
#Fan-out, which is the gap uncoloured I/O usually leaves
Removing await tends to cost you the thing
Promise.all is for: many independent operations that should
run together. SCUA has wait_all and map_all
built in.
let results = wait_all([
fn() return 1 + 1 end,
fn() return "two" end,
fn() return [3, 3, 3] end,
])
$ scua --fast examples/wait_all.scua
first arm: 2
second arm: two
arm 1 failed: fault=true (boom)
doubled: 20, 40, 60, 80
Results come back in order, and it is all-settled: one arm failing
never cancels the others, so its error sits in its slot and the rest
still run. Under --io=async, arms that do real I/O park
independently and the waits overlap. map_all does the same
across a list under a concurrency cap.
The third line of that output is an arm that faulted rather than
failed, and it comes back carrying fault = true so you can
tell a bug apart from an ordinary failure before deciding whether to
retry.
#Background work
spawn do ... end starts a task alongside a partition's
message handling. The handler that spawned it returns immediately.
partition Greeter
on Greet(name)
print(`hi {name}, checking back soon`)
spawn do
wait(1s)
print(`still here, {name}?`)
end
end
end
While that task is parked the partition keeps handling other messages. None of this uses threads or locks; it rides the same one-handler-at-a-time model partitions already give you, so a handler never has to reason about another one running underneath it.
#The catch
Only handler turns park. A top-level scua_eval blocks
regardless of whether an I/O platform is installed.
That is a real constraint rather than a detail. If you want overlap, the work has to be inside a handler, which in practice means structuring the program around partitions and messages rather than as a top-level script that calls out. For a server or an embedding host that is the shape you wanted anyway. For a one-shot tool it is not, and blocking is the better fit there. Hence the default.
Uninstalling the platform with
scua_set_io_platform(p, NULL, NULL) puts everything back to
blocking.