Since UFO is in the news, I figure it’s good time to bring this up. In PHP, the UFO or flying saucer is used to compare two values for sorting purpose. I imagine a Zig implementation would look something like this:
switch (a <=> b) {
.lt => std.debug.print("a is smaller than b", .{}),
.eql => std.debug.print("a is equal to b", .{}),
.gt => std.debug.print("a is larger than b", .{}),
}
Seeing it next to => in the cases does make it look rather confusing in my opinion, at a glance I wouldn’t know if it is a comparison operator or some other niche language feature.
I think this kind of grammar helps the language remove the bool type and optional types. In fact, we don’t really need bool; switch and tagged unions can give us everything we want.
We aren’t talking about a specialized operation here. The UFO represents a basic CPU instruction, namely CMP. If you look at the output of the follow code in Godbolt:
export fn ufo(a: i32, b: i32) i32 {
return if (a == b) 0 else if (a < b) -1 else 1;
}
You can see it in action:
ufo:
push rbp
mov rbp, rsp
cmp edi, esi
setl al
setg cl
sub cl, al
movsx eax, cl
pop rbp
ret
Note how only one comparison is done even though the original code has two comparisons. And there’s no actual branching.
If you change the implementation to this:
export fn ufo(a: i32, b: i32) i32 {
return if (a < b) -1 else if (a > b) 1 else if (a == b) 0 else unreachable;
}
You would get exactly the same output.
So UFO is totally real. What we gain by denying its existence? Why hide it behind a bunch of fake branches within a library function? Stop the cover-up already!
I think that’s an argument for adding an @order or @cmp built-in function that lowers to the CPU instruction, not necessarily one for adding an additional operator to the grammar.
I agree that IF this seems like a good idea, it should go through an @builtin first. That lets everybody use it to establish usefulness.
An actual operator should almost always have a pretty high hurdle since that gets welded into the grammar while an @builtin doesn’t have that kind of effect.
FWIW I’m actually pretty in favour of this being added as a built-in. It’s an instruction on basically every CPU in current use, why not expose it directly?
The only reason it would be added is if you can’t easily get it with existing control flow, which has already been shown to work in this thread!
That’s how labelled switches got added! The code generation with a loop + switch wasn’t reliable, and I assume they had trouble making it better without changing semantics
The status quo is a lie. Under the hood, a comparison is really a subtraction. The CPU subtracts b from a. If the overflow flag is set, then b is bigger than a. If the zero flag is set, then b is equal to a. The operation in question is arithmetic.
Conscious of this fact, programmers are in a better position to optimize their code. We know that modern CPUs are capable of performing multiple arithmetic ops at once, so parallelism is where we’d look if we’re looking for better performance. We might end up with something like this:
otherwise good point, but I don’t know if there’s any simd instruction that actually produces 0, 1, -1 outputs, there’s comparison operations for sure but they only produce binary outputs. You could do this with masks though.
const V = @Vector(3, i32);
...
const cv: V =
@select(i32, av > bv, @as(V, @splat(1)), @as(V, @splat(0))) -
@select(i32, av < bv, @as(V, @splat(1)), @as(V, @splat(0)));
and use > and < which we have today.
If .{ a.x <=> b.x, a.y <=> b.y, a.z <=> b.z }; in the original example was actually meant only as a convenience to initialize the vector, then a makeshift std.math.order variant that returns i2 is fine imo.