`runtimeValue`?

Sometimes, tests using compile-time literals can mislead API designers, making a function that actually fails to compile at runtime appear to work correctly under test.

This is a buggy inline function that expects an integer and a floating-point number to be passed in, multiplies them, and outputs an integer. This function fails to compile correctly for runtime values. However, when tested with literal values, this bug is not exposed, and the test runs fine.

fn FloatIntToIntType(Ta: type, Tb: type) type {
    const a_is_int = switch (@typeInfo(Ta)) {
        .int, .comptime_int => true,
        .float, .comptime_float => false,
        else => unreachable,
    };
    const b_is_int = switch (@typeInfo(Tb)) {
        .int, .comptime_int => true,
        .float, .comptime_float => false,
        else => unreachable,
    };
    if (a_is_int and b_is_int) return @TypeOf(Ta, Tb) else if (a_is_int) return Ta else return Tb;
}
inline fn floatMulIntToInt(a: anytype, b: anytype) !FloatIntToIntType(@TypeOf(a), @TypeOf(b)) {
    const result = a * b;
    const ResultType = FloatIntToIntType(@TypeOf(a), @TypeOf(b));
    if (!std.math.isFinite(result) or result > std.math.maxInt(ResultType) or result < std.math.minInt(ResultType)) return error.Overflow;
    return @as(ResultType, @intFromFloat(result));
}

test floatMulIntToInt {
    const a: usize = 1437;
    const b: f32 = 1.2;
    try std.testing.expectEqual(1724, floatMulIntToInt(a, b));
}

My personal solution is to introduce a runtimeValue. I am not sure if my implementation is the best one, maybe there is a better one?

fn runtimeValue(T: type, v: anytype) T {
    var ret: T = undefined;
    @as(*volatile T, &ret).* = v;
    return ret;
}

The test based on runtimeValue() can correctly check the compilation errors of this inline function for runtime values.

test floatMulIntToInt {
    const a = runtimeValue(usize, 1437);
    const b = runtimeValue(f32, 1.2);
    try std.testing.expectEqual(1724, floatMulIntToInt(a, b));
}
test.zig:266:22: error: incompatible types: 'usize' and 'f32'
    const result = a * b;
                   ~~^~~
test.zig:266:20: note: type 'usize' here
    const result = a * b;
                   ^
test.zig:266:24: note: type 'f32' here
    const result = a * b;
                       ^
test.zig:275:55: note: called inline here
    try std.testing.expectEqual(1724, floatMulIntToInt(a, b));
                                      ~~~~~~~~~~~~~~~~^~~~~~

I’m not sure if there’s a function in the current standard library that does something similar to runtimeValue. If not, I think it’s definitely worth adding to std.testing

you can just use var instead of const

2 Likes

The easier way to force a variable to be runtime-known is to use

var a: i32 = undefined;
a = 123;

or

var b: i32 = 123;
_ = &b;

Also note that in your example, simply removing inline from your function makes the test fail even when passed comptime-known values. Why is it inline in the first place? inline fn should be used with caution and is mainly meant to be used when you need to propagate the comptime-knownness of arguments, which doesn’t seem relevant in this case.

5 Likes

Oh, that’s correct. It seems I was wrong to assume that var assigned to a literal value would also be prioritized for compile-time value optimization.

Yes, but I think in this case it does lend itself to being an inline function, and the comptime preference for passing by value does have value here.

that’s const

trust the compiler more, LLVM will most likely inline, and evaluate the comptime args itself, and if it doesn’t it means it probably wasn’t worth it.