その手の平は尻もつかめるさ

ギジュツ的な事をメーンで書く予定です

Zigのビットシフト演算がちょっと面白い

なんとなくZigを触っているのですが、ビットシフト演算が独特の挙動で面白かったです。

const n: u8 = 0b00000001;
const shifted: u8 = n << 1;
std.debug.print("{b}\n", .{shifted}); // => 0b00000010

これは直感的なコードでしょう。n が1バイト左にシフトしています。

さて以下のコードはどうでしょうか。

const n: u8 = 0b00000001;
const shifted: u8 = n << 8;
std.debug.print("{b}\n", .{shifted}); // expects 0, but...

このコードは

./main.zig:5:30: error: integer value 8 cannot be coerced to type 'u3'
    const shifted: u8 = n << 8;

というコンパイルエラーが返却されます。u8型の値に対しては u3 型すなわち高々 7 までの整数値でのみシフトできるということです。たしかに8 bitの値なんだから最大7 bitシフトできれば良いでしょう、ということのようです。なるほど。
これはシフト対象の型 T に対し、シフトbit数の型が Log2T と定義されているからです。Log2T 型って面白いですね。u3 みたいな型を今まであまり見かけたことが無かったので斬新でした。まあこれが便利かどうかでいうと賛否ありそうな感じはしますね。

// generates the u3 random number
var rnd = std.rand.DefaultPrng.init(@intCast(u64, std.time.milliTimestamp()));
const random_num: u3 = rnd.random().int(u3);
std.debug.print("[debug] rand: {}\n", .{random_num});

const n: u8 = 0b00000001;
const shift_amout: u3 = 7;
const shifted: u8 = n << (shift_amout + random_num);
std.debug.print("{b}\n", .{shifted});

ではこのような、実行するまでシフトするbit数が不定の場合はどうなるでしょうか。
生成された乱数が 0 だった場合は単に7 bitシフトして 0b10000000 が無事得られますが、1以上だった場合は……

[debug] rand: 2
thread 431510 panic: integer overflow
/private/tmp/main.zig:10:39: 0x1005e6727 in main (main)
const shifted: u8 = n << (shift_amout + random_num);
                                      ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:551:22: 0x1005e9b7c in std.start.callMain (main)
            root.main();
                     ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:495:12: 0x1005e6de7 in std.start.callMainWithArgs (main)
    return @call(.{ .modifier = .always_inline }, callMain, .{});
           ^
/usr/local/Cellar/zig/0.9.1_1/lib/zig/std/start.zig:460:12: 0x1005e6d25 in std.start.main (main)
    return @call(.{ .modifier = .always_inline }, callMainWithArgs, .{ @intCast(usize, c_argc), c_argv, envp });
           ^
???:?:?: 0x10302351d in ??? (???)
???:?:?: 0x0 in ??? (???)
Abort trap: 6

このようなruntime panicが発生します。キャー

とはいえ、そうは言っても様々な理由で Log2T 型 以上のbit数でshiftしたい時もあるじゃないですか、どうすれば……

const n: u8 = 0b00000001;
const shift_amout: u3 = 7;
var shifted: u8 = n << (shift_amout);

var additional_shift_amount: u3 = 1;

var i: u3 = 0;
while (i < additional_shift_amount) {
    shifted = shifted << 1;
    i += 1;
}

std.debug.print("{b}\n", .{shifted}); // => 0

というわけでこうです。追加でシフトしたいbit数 (上記の場合 additional_shift_amount) ぶんだけループで1 bitずつシフトしてあげることでコンパイルエラーとruntime panicを回避することができます。良かった良かった。

ちなみに左シフトをしつつその操作によるオーバーフロー・アンダーフローの発生場合を検出したい時には @shlWithOverflow() という組み込み関数が利用できます。

[追記]

一旦整数のbit数を拡張して、十分な空間でシフトしてから元の型にtruncateするという方法。これも (むしろこれで) 良さそうですね。ありがとうございます。

[追記ここまで]




なお、なんとなくZigを触った結果としてはこのようなライブラリができつつあります。