Understanding u5 and relation to shift right

Good news, I got a serial cable for my Zig PinePhone project, so finally I can debug without relying on the color of an LED :slight_smile:

So I put together this code to debug some 32bit register addresses & values, and a few surprises caught me out along the way:

  • for(0…7) limited to usize, and forwards only ?
  • u5 errors with >>
  • the number of truncate sprinkles I needed

I ambitiously started with this:

for (0..8) |i| {
    const nibble = (v >> ((7 - i) * 4)) & 0xF;
    ...
}

What I ended up with feels very noisy, and not sure if it is correct? since I dont understand the u5 requirement. But further than I got using while(), and at least it seems to work! :slight_smile:

Can anyone explain the u5 bit please?

pub fn logU32ln(v: u32) void {
    logS("0x");
    for (0..8) |i| {
        const iu8: u8 = @truncate(i);
        const bitShiftCount: u5 = @truncate((7 - iu8) * 4);
        const nibble: u8 = @truncate((v >> bitShiftCount) & 0xF);
        if (nibble < 10) {
            logB('0' + nibble);
        } else {
            logB('a' + (nibble - 10));
        }
    }
    logLN();
}

A u5 represents values between 0-31, which is the number different shifts to the right or left you can do before a u32 is shifted out. If you use a u64 the shifter argument (?) must be a u6 or smaller.

1 Like

Thanks, that kinda makes sense, although seems a bit pendantic :slight_smile:

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

Thanks for a detailed and informative first post :slight_smile: It feels like the intCast there should be interchangable with as(u5) - complains type usize. And that the truncate shouldnt be required - complains expected u4.

So we still end up with 3 magic spells compared to the ā€˜C’ code. Something that looks like the compiler should be able to determine is all in range since i is only 0…8. And anyway I dont see the harm in >> with a larger type/value.

I’ll get my head around it eventually, just feels a bit like casting random spells until I find the magic combination. :slight_smile: Thanks again for the help.

1 Like

I think the difference is that @as runs at comptime(*), but @intCast runs at runtime. So, writing @as(u4, my_u8_var) is a compile error, because the type u8 might hold numbers that the type u4 can’t hold. But var x: u4 = @intCast(my_u8_var) compiles fine – this might error at runtime if my_u8_var happens to be e.g. 200, but you’re telling the compiler ā€œI claim my_u8_var will always fit into a u4, but please crash at runtime if I’m wrongā€(**)

truncate shouldnt be required

no, it’s required! (in my version) I didn’t write &0xF anywhere. I’m relying on @truncate to do that truncation for me, along with changing the type. (It would be great if the compiler could figure out that @as(u4, x&0xF) is valid – that’s one of the things that seems like it might change in the future)


(*)ā€œ@as runs at comptimeā€ - I’m unsure if this is technically true, someone who knows more please chime in! but it seems true when casting numbers
(**)but note this will not crash in ReleaseFast – see the manual on Illegal Behavior)


feels a bit like casting random spells until I find the magic combination

yup, I feel that. I got through it by looking at the docs again and again and again and again… and eventually I learned, somehow

I wanted to contribute something useful and then ended up nerd sniping myself.

For your consideration, maximum magic:

pub fn logUintln(v: anytype) void {
    if (@TypeOf(v) == comptime_int) {
        logS(std.fmt.comptimePrint("{x}", .{v}));
    } else {
        var i: @TypeOf(v) = v;
        for (0..@typeInfo(@TypeOf(v)).int.bits / 4) |_| {
            const n: u8 = @bitReverse(@as(u4, @truncate(@bitReverse(i))));
            logB(if (n < 10) '0' + n else 'a' + n - 10);
            i <<= 4;
        }
    }
    logLn();
}

But more seriously:

For consuming some number bit-by-bit, the <<= and >>= operators are probably semantically cleaner. You don’t need to muck about with an index then.

1 Like