Why void is a value in Vx
TL;DR — In Vx,voidis an ordinary type with exactly one value. A function that "returns nothing" returns that value. Rust does the same with(); C does not, and C++ has paid for it ever since. Until this week the Vx checker treated avoidvalue as something that could be moved, so using it twice was an error. That is fixed. Writing this post turned up four more gaps.
"Returns nothing" can be a hole in the type system, or it can be a type like any other. Vx picks the second, because generic code then works without special cases.
Where a void value pays off
Anywhere code is generic over a type T, someone will eventually pick
T = void. If void is a real type with one value, everything written for
T still works. If it is not, each of those places needs its own exception.
spawn on.spawn on(τ) { ... }runs a block on a device and gives back its result asPinned<T, τ>: a value of typeTthat lives onτ. A block that only writes to a tensor has no result, soTisvoidand the result isPinned<void, τ>. The placement rules apply to it unchanged. This program runs today:fn main() -> i32 { let mut c = Tensor<f32, [4], Memory::NPU_HBM>::uninit(); let p = spawn on(Topology::NPU[0]) { c[0] = 1.0; }; let q = p; let r = p; print("done"); return 0; }- Generic types and functions: a
Result<void, E>for an operation that can fail but has nothing to return, or a genericfn apply<T>(f : fn() -> T) -> Tcalled with a function that returns nothing. - Expressions: a block or an
ifwith no value at the end still has a type,void, so the checker never needs a "this expression has no type" case.
C and Rust
In C, void is not a value type: you cannot declare a void variable.
C++ inherited that, and had to add special versions of its generic library types for it, such as
std::future<void> and std::promise<void>. The "regular void"
proposal (P0146) tries to undo this. Rust went the other way: () is a real type of
size zero that implements Copy, and generic code never notices it. Vx is on the Rust
side.
void is not the same as the never type !, which Vx added this month
(#1273). void has exactly
one value: the function returns, and the value tells you nothing. ! has no values
at all: the function never returns.
The bug
A value of a type with only one possible value carries no information, so "moving" it means nothing. Reading it twice cannot be a use after a move. Vx got this wrong (#402):
fn nothing() -> void { }
fn main() -> i32 {
let z = nothing();
let a = z;
let b = z;
return 0;
}
Error[E4001] at 5:11: Use of moved or consumed linear variable: z
Inside the compiler, void is written as a struct named void, and
by default a struct is linear: it moves when you use it, so the old variable cannot be used
again. void picked that rule up by accident. The fix
(#1381) makes the check skip
void. The test tests/backend/pass/void_value_used_twice.vx reads a
void value twice and runs on both code generators.
What is still wrong
The fix is an exception added to a general rule, and it finds void by its name.
So a struct you declare yourself and name void gets the exception too:
struct void {
x : i32,
}
fn main() -> i32 {
let a = void { x: 5 };
let b = a;
let c = a;
print(c.x);
return 0;
}
This compiles and prints 5. Rename the struct to Plain and the
compiler reports E4001, as it should
(#1386). The better fix is to make void a built-in
type that no program can declare, and have the check ask whether a type is plain data that can
be copied freely, rather than compare names. Then void and the next built-in type
of that kind are copyable for the same reason, with no list of exceptions.
Trying the examples above for this post found three more problems:
Result<void, i32>is rejected with a message that contradicts itself:Type mismatch on return. Expected Result<void, i32>, got Result<void, i32>. The standard library already works around this:core::fmtreturnsResult<i32, Error>withOk(0)where it meansResult<void, Error>(#1387).apply<T>above, called with a function returningvoid, passes the checker but fails when the compiler checks the MLIR it generated (#1388).let u = if x > 2 { print("big"); };passes the checker and then crashes the code generator (#1389).
Each of these is a place where generic code meets T = void and needs a special
case it does not have, which is the problem this post started with.
Vx is Apache 2.0 with the LLVM exception — install it and try the examples.