It may not quite match the description of the OP (it only considers indices starting from 0 and does not account for negative indices). In my ideal native indexing, the following behavior is supported:
(Edit: Added wrap-around operation)
const std = @import("std");
pub fn Index(comptime Backing_or_len: anytype) type {
const IndexBacking: type = switch (@typeInfo(@TypeOf(Backing_or_len))) {
.type => @as(?type, switch (@typeInfo(Backing_or_len)) {
.int => |int_info| if (int_info.signedness == .signed) null else Backing_or_len,
else => null,
}) orelse @compileError("Backing must be unsinged int."),
.comptime_int, .int => if (Backing_or_len == 0) u0 else std.math.IntFittingRange(0, Backing_or_len - 1),
else => comptime unreachable,
};
return packed struct(IndexBacking) {
child: Backing,
pub const Backing = IndexBacking;
pub const len: comptime_int = switch (@typeInfo(@TypeOf(Backing_or_len))) {
.type => std.math.maxInt(Backing) + 1,
.comptime_int, .int => Backing_or_len,
else => unreachable,
};
pub const SignedBacking = if (len == 0 or len == 1) i0 else std.math.IntFittingRange(-len + 1, len - 1);
pub fn fromUnverified(x: Backing) @This() {
std.debug.assert(x < len);
return .{ .child = x };
}
pub fn add(self: @This(), offset: anytype) @This() {
const offset_int = switch (@typeInfo(@TypeOf(offset))) {
.int => offset,
.comptime_int => if (offset < 0) @as(SignedBacking, offset) else @as(Backing, offset),
else => @compileError("offset must be int"),
};
const result: Backing = switch (@typeInfo(@TypeOf(offset_int)).int.signedness) {
.unsigned => self.child + @as(Backing, offset),
.signed => @intCast(@as(SignedBacking, self.child) + @as(SignedBacking, offset)),
};
return .fromUnverified(result);
}
pub fn sub(self: @This(), offset: anytype) @This() {
const offset_int = switch (@typeInfo(@TypeOf(offset))) {
.int => offset,
.comptime_int => if (offset < 0) @as(SignedBacking, offset) else @as(Backing, offset),
else => @compileError("offset must be int"),
};
const result: Backing = switch (@typeInfo(@TypeOf(offset_int)).int.signedness) {
.unsigned => self.child - @as(Backing, offset),
.signed => @intCast(@as(SignedBacking, @intCast(self.child)) - @as(SignedBacking, offset)),
};
return .fromUnverified(result);
}
pub fn addWrap(self: @This(), offset: anytype) @This() {
if (len == 0) @compileError("addWrap on zero length index.");
if (len == 1) return self;
if (len == std.math.maxInt(Backing) + 1) {
const gap: Backing = @truncate(offset);
return .{ .child = self.child +% gap };
}
const gap: Backing = switch (@typeInfo(@TypeOf(offset))) {
.comptime_int => @intCast(@mod(offset, len)),
.int => |offset_int_info| blk: {
// Since `len == maxInt(Backing) + 1` is specialized, `len` now
// fits into `Backing` for all subsequent operations.
const Extend = switch (offset_int_info.signedness) {
.unsigned => if (offset_int_info.bits > @bitSizeOf(Backing)) @TypeOf(offset) else Backing,
.signed => if (offset_int_info.bits > @bitSizeOf(SignedBacking)) @TypeOf(offset) else SignedBacking,
};
break :blk @intCast(@mod(@as(Extend, offset), @as(Extend, len)));
},
else => @compileError("offset must be int"),
};
const threshold: Backing = len - gap;
return .{
.child = if (self.child >= threshold) self.child - threshold else self.child + gap,
};
}
pub fn subWrap(self: @This(), offset: anytype) @This() {
if (len == 0) @compileError("subWrap on zero length index.");
if (len == 1) return self;
if (len == std.math.maxInt(Backing) + 1) {
const gap: Backing = @truncate(offset);
return .{ .child = self.child -% gap };
}
const gap: Backing = switch (@typeInfo(@TypeOf(offset))) {
.comptime_int => @intCast(@mod(offset, len)),
.int => |info| blk: {
// Since `len == maxInt(Backing) + 1` is specialized, `len` now
// fits into `Backing` for all subsequent operations.
const Extend = switch (info.signedness) {
.unsigned => if (info.bits > @bitSizeOf(Backing)) @TypeOf(offset) else Backing,
.signed => if (info.bits > @bitSizeOf(SignedBacking)) @TypeOf(offset) else SignedBacking,
};
break :blk @intCast(@mod(@as(Extend, offset), @as(Extend, len)));
},
else => @compileError("offset must be int"),
};
return .{
.child = if (self.child >= gap) self.child - gap else self.child + (len - gap),
};
}
pub fn offsetSigned(a: @This(), b: @This()) SignedBacking {
return @as(SignedBacking, a.child) - @as(SignedBacking, b.child);
}
pub fn offsetUnsigned(higher: @This(), lower: @This()) Backing {
return higher.child - lower.child;
}
};
}
test "Index basic" {
const MyIdx = Index(100);
var idx = MyIdx{ .child = 10 };
idx = idx.add(@as(MyIdx.Backing, 5));
try std.testing.expect(idx.child == 15);
idx = idx.add(@as(MyIdx.SignedBacking, -5));
try std.testing.expect(idx.child == 10);
const a = MyIdx{ .child = 20 };
const b = MyIdx{ .child = 50 };
try std.testing.expect(a.offsetSigned(b) == -30);
try std.testing.expect(b.offsetSigned(a) == 30);
try std.testing.expect(b.offsetUnsigned(a) == 30);
const Idx16 = Index(16);
try std.testing.expect(Idx16.Backing == u4);
try std.testing.expect(Idx16.SignedBacking == i5);
try std.testing.expect(Idx16.len == 16);
const Idx17 = Index(17);
try std.testing.expect(Idx17.Backing == u5);
try std.testing.expect(Idx17.SignedBacking == i6);
const IdxU16 = Index(u16);
try std.testing.expect(IdxU16.Backing == u16);
try std.testing.expect(IdxU16.SignedBacking == i17);
try std.testing.expect(IdxU16.len == 65536);
}
test "Index wrap" {
const MyIdx = Index(10);
const start = MyIdx.fromUnverified(5);
try std.testing.expectEqual(@as(u4, 2), start.addWrap(@as(u8, 7)).child);
try std.testing.expectEqual(@as(u4, 8), start.subWrap(@as(u8, 7)).child);
try std.testing.expectEqual(@as(u4, 0), start.addWrap(@as(u32, 25)).child);
try std.testing.expectEqual(@as(u4, 7), start.subWrap(@as(i32, -2)).child);
const U8Idx = Index(u8);
const max_val = U8Idx.fromUnverified(255);
try std.testing.expectEqual(@as(u8, 0), max_val.addWrap(@as(u32, 1)).child);
const zero_val = U8Idx.fromUnverified(0);
try std.testing.expectEqual(@as(u8, 255), zero_val.subWrap(@as(u32, 1)).child);
const Tiny = Index(1);
const idx = Tiny.fromUnverified(0);
try std.testing.expectEqual(@as(u0, 0), idx.addWrap(99).child);
try std.testing.expectEqual(@as(u0, 0), idx.subWrap(99).child);
}
test "Index panic 1" {
const MyIdx = Index(50);
const idx = MyIdx{ .child = 40 };
_ = idx.add(20);
}
test "Index panic 2" {
const MyIdx = Index(u8);
const idx = MyIdx{ .child = 0 };
_ = idx.sub(@as(u8, 1));
}
test "Index panic 3" {
const MyIdx = Index(u8);
const idx = MyIdx{ .child = 0 };
_ = idx.sub(@as(i8, 1));
}
test "Index panic 4" {
const MyIdx = Index(u8);
const a = MyIdx{ .child = 20 };
const b = MyIdx{ .child = 50 };
_ = a.offsetUnsigned(b);
}