I don't like math syntax

@divTrunc(d, 4); // d is a i32

instead of

d / 4;

Any chance this gets better?

1 Like

There’s this post that goes into depth on this.

EDIT:

unless i’m misunderstanding you, and you are saying you prefer the former and not the latter.

The closest thing to a “Zig” way I can think of to change this would be something like a @setDivisionBehaviour() builtin that sets the behaviour of the division operator (based on a pre-specified enum) until the builtin is called again or control flow leaves the current block.

That way, instead of using @divTrunc() many times, you might instead do @setDivisionBehaviour(.truncate) and then use the division operator.

1 Like

To me a required builtin is always a point to stop and think for a second what I’m doing here.
And I think division is one such case where you should do this.
The truncation behavior of signed division in other languages has caused many long debugging session for me in the past, and it’s not the only problem that integer division can have. There are also performance problems to integer division, especially for runtime-known numbers, and even in your variant a @divTrunc is slower than a @divFloor aka >> 2.

Also in my experience that number of times that you need to divide signed integers is quite small, so in my opinion the advantages outweigh the syntactic noise that come from it. I would be interested to hear what you are doing that makes you need to use signed integer division so much that it bothers you. I’m sure there is probably a better way to solve your problems without it.

4 Likes

I also prefer unsigned because that’s what Zig encourages, due to the use of usize for indexing and lengths. @ericlang do you prefer signed ints?

It might help to understand why @as(i32, x) / @as(i32, y) is a compile error in the first place.

When dividing a positive integer by another positive integer, the result should be rounded down. 23 / 5 == 4. Programmers universally agree on and understand this definition of integer division.

But what if the integers are signed and the dividend (the number on the left) is negative? What is -23 / 5? If we choose to round toward 0, which is called truncated division, it is -4. If we choose to round toward negative infinity, which is called floored division, it is -5.

In hardware, signed integer division is most often and most efficiently implemented as truncated division, meaning that -23 / 5 == -4. But on some hardware, floored division is more efficient.

Truncated division is also the definition that C uses since C99 (before that it was implementation-defined!) and most other C-inspired languages have followed suit.

But truncated division is often not the most convenient definition for users, and many people have a mental model that aligns more closely with floored division. This can often result in developers writing buggy code involving / division or % remainder that produce completely incorrect results when passed negative values.

One example: You are developing a 2D game where the world is made out of “chunks” 100x100 units in size. The player is located at coordinates x = 74, y = 283 and an enemy is stationed at x = -83, y = 210. Do they both occupy the same chunk? It’s pretty clear that they do not. A naive algorithm for testing this would be to check if player.x / 100 == enemy.x / 100 and player.y / 100 == enemy.y / 100. But this algorithm is wrong and yields true under truncated division, and only works under floored division.

Another (almost canonical) example: You want to check if a number is odd. Easy, just check if x % 2 == 1. But this is once again incorrect because -3 % 2 == -1 under truncated division.

Mistakes like these is why Zig forces you to explicitly state in your code whether you are assuming truncated or floored division when dividing numbers that could be negative, by using @divTrunc() or @divFloor() (or @divExact()). And as another bonus, by having the common / division operator not assume any specific definition, the compiler can choose whichever definition is the most efficient for any given target.

I think my main gripe with Zig’s integer division is that the corresponding builtins for remainder are called @rem and @mod. I believe this is a mistake and that they should be called @remTrunc() and @remFloor(), because the term “modulo” is just as ambiguous about rounding direction as “remainder” and it’s better to have the name of the builtin be explicit.


Note also that I only mentioned negative dividends (left-hand side). It gets even more confusing and unintuitive for humans when negative divisors (right-hand side) are involved. Some programming languages opt for an integer division definition called euclidean division, where the remainder is always >= 0. This is the same as floored division for positive divisors but produces different and sometimes more useful results for negative divisors. There’s also ceiling division which is less useful for the common cases but still has its place for certain tasks.

Recommended reading:

27 Likes

I have written the type of code you’re talking about and I can confirm that it’s very easy to get confused when doing integer division with negative numbers.

1 Like

Unfortunately this would change the meaning of division symbols depending on context, which breaks the radical symplicity that Zig spends so much effort striving for.

To expand on this idea, perhaps you could have @div(a, b, .mode), but is it that any better than just having separate functions? :man_shrugging:

1 Like

I prefer signed ints in some cases because of the nasty builtins, which give me unreadable code.
Sometimes I only need u8 but need to do math on it. And that is “impossible”.

Also very confusing for me:

// does not compile
fn b() void {
    var i: i32 = -127;
    i += 2;
    const d: u8 = 2;
    const r = i / d;
    std.debug.print("{}\n", .{ r });
}

// does compile
fn c() void {
    const i: i32 = -129;
    const r = i / 2;
    std.debug.print("{}\n", .{ r });
}

That is very confusing. I’m not sure if that is intended behaviour or not.

It may be confusing, but it has a simple explanation.

In b, you are saying that you are dividing an negative number by a strictly positive number. This requires the semantics as discussed further up in this thread.

In c, you are dividing by a comptime_int which gets promoted to an i32 when you do the division. so in this case, the 2 is used as an i32 which is very different from the d: u8 = 2 case in b.
Because you don’t explicitly set it to an unsigned int and it is a comptime known value, the compiler is free to use the typing that it needs to make it work.

1 Like

Yep. Strange math, strange computers :slight_smile:
I did never realise how a int division by 2 could make such waves.

I’m with @floooh on this one.

It’s so rare I actually use the last bit that I’ll rather waste it than loose my sanity over something which is only significant when the full moon is out.

Honestly I would prefer we had u32, i32, and (s32 or n32) with the latter being signed and don’t worry about it.

2 Likes

or separate operators, similar to the separate + and +% operators for panic vs overflow addition.

maybe we could have / for truncating division (as is done in most programming languages) and /_ for floor division. then we can also have % for remainder, and %_ for floor-remainder (aka modulus).

3 Likes

Btw somewhat related, this proposal out of the C++ world (from 2018) which argues that ‘Subscripts and sizes should be signed’:

I wasn’t really aware of this before, but it’s interesting that those things being unsigned quantities in the C++ stdlib was more or less a historical accident.

4 Likes

Wow, what a wonderful piece of advice passed on from the pains of others. While we cannot ensure that the extra bit won’t be useful on tiny devices I think that author is right in identifying that on majority of systems the cons to signed is are insignificant, not even a performance hit. But the cons of unsigned are abundant.

2 Likes

While I really would like to have signed for everything, are the cons true for Zig?

The main problem that C/C++ has is that “unsigned” is implemented in a way that sucks because of both implicit casting and unchecked overflow.

Zig doesn’t suffer from the same problems.

However, I do very much hate usize and all the casting it causes.

Tbf, this problem is arguably worse in day-to-day coding than the implicit signed/unsigned conversion in other languages :wink:

E.g. the implicit conversion in C/C++ rarely causes actual issues (but if it does you can blow your foot off).

OTH the excessive casting in Zig is a constant source of friction (and also reduces readability by drowning the actual expression in ‘casting noise’). Yes, one can argue that it’s better to avoid this casting by carefully picking the correct integer types, but some language features are hardwired to unsigned integers, so the programmer doesn’t have much choice in the matter.

1 Like

FWIW, in TigerBeetle, where we generally avoid usize, casting hasn’t been a problem, outside o the specific issue that for (0...) loop gives you usize indexes (those we often have to cut to u8).

Otherwise, the places where you go from usize to “what makes sense for my domain” are usually a good spot for deploying defense-in-depth assertions. Eg, from yesterday’s code:

const write_size = bus.io.send_now(
    connection.fd.?,
    message.buffer[connection.send_progress..message.header.size],
) orelse return;

assert(write_size <= constants.message_size_max);

connection.send_progress += @intCast(write_size);

send syscalls returns me usize, I do a sanity assertion that it can’t be larger than the maximal size of the message, and that justifies the cast to u32 on the next line.

2 Likes