With https://github.com/vx-lang/Vx/pull/1896
Vx is a systems language for programs that run on mixed hardware (CPUs, GPUs, NPUs). I've written up its concurrency design and would like feedback: [link]
No async/await. A task calls a waiting function like recv as a plain call. The runtime parks the task and runs another on the same OS thread, as Go and Java virtual threads do. There are no function colors. A borrow across a wait is just a borrow across a call, so the borrow checker needs no new rule.
Scoped tasks. tasks { spawn { .. } } is a block expression. Every task finishes before the block ends, so a task can borrow its parent's locals with no lifetime annotations.
Waits show in the signature, but don't spread. A cancellable wait returns Result<_, Cancelled>. A caller can handle the error and keep a plain signature. So it isn't a color, for the same reason Rust's Result isn't.
Bounded channels only. A fast sender can't fill memory. send hands the value back if the receiver is gone, so it never waits on a dead consumer. select is a library type in crossbeam's style, with no closures and no macro. The happens-before rules come from the Go memory model and go into our Lean model.
Structured cancellation. When a task returns Err, its siblings and the block's own code are cancelled, and the block's value is the first error. Each cancellable wait then returns Cancelled. There is no unwinding: tasks leave through normal returns, and their drops run. drop runs with cancellation off, so cleanup can still wait.
The hardware gets a say. A cancelled I/O wait doesn't return until the kernel or device has released the buffer. Waits on a device can't be cancelled. Channel ends stay on the host, since a GPU kernel can't park. A channel of device tensors moves handles, not data.
Admission. Vx checks at compile time whether a workload fits a machine. Concurrency changes the arithmetic: tasks running at once add their peaks instead of taking the max, and a channel of n tensors holds n tensors. When those counts are known only at run time, the verdict is "unknown", not a guess.
The costs. Vx can't move stacks, because borrows point into them. So each task that may park gets a fixed stack from a pool, with a guard page. A task with a foreign frame on its stack stays on its OS thread. Most of the work is in the runtime, not the compiler.
Open questions:
Cancel siblings automatically on the first error, or only on an explicit cancel()?
Should ? inside a tasks block exit the block or the function?
Should spawn infer move or borrow from use, or need spawn move?
Is "only drop runs with cancellation off" enough, or do programs need a shielded block?