Should Zig acquire a UFO operator?

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", .{}),
}

Multiple-column comparison:

const result = switch (a.x <=> b.x) {
    .eql => switch (a.y <=> b.y) {
        .eql => a.z <=> b.z,
        else => |cy| cy,
    },
    else => |cx| cx,
}

Or possibly:

const result = (a.z <=> b.z).unless(a.y <=> b.y).unless(a.x <=> a.x);

const ComparisonResult = enum(i2) {
    lt = -1,
    eql = 0,
    gt = 1,

    inline fn unless(self: @This(), other: @This()) @This() {
        return if (other == .eql) self else other;
    }
};
1 Like

in the past (before spaceships) it was called Arithmetic IF Statement

IF (THETA-CHI) 50,50,100

4 Likes

Nice, but switching on std.math.order(a, b) seems fine? And it’s purely in userland. Not sure it’s common enough to warrant a new operator.

25 Likes

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.

2 Likes

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!

7 Likes

Seems to me that you provided evidence in favor of status quo

3 Likes

I want to believe.

2 Likes

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.

8 Likes

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.

1 Like

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?

1 Like

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

4 Likes

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:


fn compare(a: *Point, b: *Point) i32 {
    const cv: @Vector(3, i32) = .{ a.x <=> b.x, a.y <=> b.y, a.z <=> b.z };
    const sv: @Vector(3, i32) = .{ 4, 2, 1 };
    return @reduce(.Add, cv * sv);
}

Where a <=> b yields an i2 that’s -1, 0, or 1.

1 Like

Probably should be:

const av: @Vector(3, i32) = .{ a.x, a.y, a.z };
const bv: @Vector(3, i32) = .{ b.x, b.y, b.z };
const cv = av <=> bv;
const sv: @Vector(3, i32) = .{ 4, 2, 1 };
return @reduce(.Add, cv * sv);

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.

2 Likes

that’s neat, so we can just replace cv with

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.

1 Like