I couldn't get a bitwise operator to work with non-comptime shift

Hi, there
I was solving the pangram issue from Exercism and my initial thought was of a solution like below:

const std = @import("std");

pub fn isPangram(str: []const u8) bool {
    var pangram_map: u32 = 0;
    for (str) |el| {
        const index: u8 = switch (el) {
            65...90 => @as(u8, el - 65),
            97...122 => @as(u8, el - 97),
            else => 0xFF,
        };
        switch (index) {
            0...26 => {
                const n = @as(u32, 1) <<| index;
                pangram_map = pangram_map | n;
            },
            else => {
                // Other characters do not count for being a pangram or not
            },
        }
    }
    return pangram_map == 0x03FFFFFF;
}

when I run it against some test like the following:

const std = @import("std");
const testing = std.testing;

const pangram = @import("pangram.zig");

test "perfect lower case" {
    try testing.expect(pangram.isPangram("abcdefghijklmnopqrstuvwxyz"));
}

then I get:

❯ zig test test_pangram.zig
LLVM Emit Object... fish: Job 1, 'zig test test_pangram.zig' terminated by signal SIGSEGV (Address boundary error)

I don’t really understand why this would happen. I just tried to do a shift left with a number that is known in runtime to be between 0 and 26, so I’d hope it worked.

Also, I tried the << operator, but then it requires the shift number to be known in compile time. I couldn’t find a particular way to do an “unsafe” cast to u5, so that failed at compile time:

❯ zig test test_pangram.zig
pangram.zig:13:51: error: expected type 'u5', found 'u8'
                const n = @as(u32, 1) <<| @as(u5, index);
                                                  ^~~~~
pangram.zig:13:51: note: unsigned 5-bit int cannot represent all possible unsigned 8-bit values
referenced by:
    test.non-alphanumeric printable ASCII: test_pangram.zig:47:32
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

Finally, I settled on a solution where I could get things on compile time, but I am still curious on how I could get it done with some minimal change to the above code.

For reference, I ended up with an inline for (if anyone wants to comments, I am all ears). Solution below:

const std = @import("std");

pub fn isPangram(str: []const u8) bool {
    var pangram_map: u32 = 0;
    for (str) |el| {
        const index: u8 = switch (el) {
            65...90 => @as(u8, el - 65),
            97...122 => @as(u8, el - 97),
            else => 0xFF,
        };
        inline for (0..26) |i| {
            if (i == index) {
                pangram_map |= @as(u32, 1) << i;
            }
        }
    }
    return pangram_map == 0x03FFFFFF;
}

Also, I am using zig 0.11.

Hi, welcome to the forum!
So, I’m on Zig master right now and this passed for me:

const std = @import("std");

pub fn isPangram(str: []const u8) bool {
    var pangram_map: u32 = 0;
    for (str) |elem| {
        const idx: u8 = switch (elem) {
            65...90 => @intCast(elem - 65),
            97...122 => @intCast(elem - 97),
            else => 0xFF,
        };
        switch (idx) {
            0...26 => pangram_map |= @as(u32, 1) <<| idx,
            else => {},
        }
    }
    return pangram_map == 0x03FFFFFF;
}

test isPangram {
    try std.testing.expect(isPangram("abcdefghijklmnopqrstuvwxyz"));
    try std.testing.expect(isPangram("The quick brown fox jumps over the lazy dog"));
}
2 Likes

Thanks @tensorush ! I installed the latest master as well and indeed it works, so that likely means it was just a bug in 0.11.

1 Like