Lazy switch case analysis, should switches allow cases that are impossible?

While writing a generic serializer for message pack, I came across a use case for a switch that has more cases than values in the switch expression:

const std = @import("std");

fn encodeInteger(int_value: anytype) void {
    switch (int_value) {
        0...std.math.maxInt(u8) => std.debug.print("value: {}", .{int_value}),
        std.math.maxInt(u8) + 1...std.math.maxInt(u16) => @compileError("branch analyzed"),
        std.math.maxInt(u16) + 1...std.math.maxInt(u32) => @compileError("branch analyzed"),
        std.math.maxInt(u32) + 1...std.math.maxInt(u64) => @compileError("branch analyzed"),
        else => unreachable,
    }
}

test "comptime unreachable ranged switch" {
    _ = encodeInteger(@as(u3, 4));
}

Which currently provides the following compile error:

$ zig test test.zig 
test.zig:5:28: error: type 'u3' cannot represent integer value '255'
        0...std.math.maxInt(u8) => std.debug.print("value: {}", .{int_value}),

Do you think the test should compile?

The real code I have right now looks like this, and is based on the bits of the integer, but it might be easier to switch on the value of the integer instead. This is also sub-optimal, because small values should use the smallest possible encoding, regardless of how big the type used to store them is.

const format: Spec.Format = switch (@typeInfo(T).int.signedness) {
      .unsigned => switch (@typeInfo(T).int.bits) {
          0...7 => .{ .positive_fixint = .{ .value = value } },
          8 => .{ .uint_8 = {} },
          9...16 => .{ .uint_16 = {} },
          17...32 => .{ .uint_32 = {} },
          33...64 => .{ .uint_64 = {} },
          else => unreachable,
      },
      .signed => switch (@typeInfo(T).int.bits) {
          0...6 => blk: {
              if (value >= 0) {
                  break :blk .{ .positive_fixint = .{ .value = @intCast(value) } };
              } else if (value >= -32) {
                  break :blk .{ .negative_fixint = .{ .value = value } };
              } else {
                  break :blk .{ .int_8 = {} };
              }
          },
          7...8 => .{ .int_8 = {} },
          9...16 => .{ .int_16 = {} },
          17...32 => .{ .int_32 = {} },
          33...64 => .{ .int_64 = {} },
          else => unreachable,
      },
  };

I’m leaning towards no, because in the nominal case of exhaustive switching, it would damage readability if you could have over-exhaustive switches.

Since your largest integer type is known, rather than making encodeInteger take a anytype, take a u64 instead by upcasting smaller ints. This way your switch is always the same, and you produce a smaller binary as well as the function doesn’t have to specialize.

4 Likes