Understanding u5 and relation to shift right

fwiw, here’s how I would do it:

pub fn logU32ln(v: u32) void {
    std.debug.print("0x", .{});
    for (0..8) |i| {
        const shift: u5 = @intCast((7 - i) * 4);
        const nibble: u8 = @as(u4, @truncate(v >> shift));

        if (nibble < 10) {
            std.debug.print("{c}", .{'0' + nibble});
        } else {
            std.debug.print("{c}", .{'a' + nibble - 10});
        }
    }
    std.debug.print("\n", .{});
}

(I’ve replaced your log functions with std.debug.print, just for my own convenience when I gave this a quick test)

In particular, your @truncates are slightly odd/not-best-practice – they tell the compiler to throw away any data “above” the truncate. Contrast: the @intCast in const shift: u5 = @intCast((7 - i) * 4); instead tells the compiler “this is purely a type change, this operation won’t lose any data” and then the compiler will holler if you were wrong about that, which is nice. (you might consider a while loop instead – you’re correct that for-loops are usize-only, and only increasing)

Your code works as-is, but the nuances of the different conversion builtins lets the compiler help you out more in case you made a bad assumption, or the code changes later, etc. The point of the verbosity is to squash bugs.

It’s a bit of a headache and it’s pedantic for sure, but it fits zig zen:

  • Communicate intent precisely.
  • Edge cases matter.
  • Favor reading code over writing code.

(you’re not alone in thinking it’s a bit awkward, and that may improve in the future. e.g. see Zig and Emulators and Short math notation, casting, clarity of math expressions - #38 by andrewrk)

1 Like