Why void is a value in Vx

TL;DR — In Vx, void is 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 a void value 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.

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:

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.