Why does `@as` expect to be passed a value of the same type that it's type-coercing to?

const std = @import("std");

fn subtract_two_numbers(a: u32, b: u32) i32 {
    return @as(i32, a) - @as(i32, b);
}

pub fn main() !void {
    const a_number: u32 = 32;
    const a_bigger_number: u32 = 50;
    const subtracted: i32 = @as(i32, a_number) - @as(i32, a_bigger_number);
    std.debug.print("{}\n", .{subtracted});

    const subtracted_but_via_a_function = subtract_two_numbers(a_number, a_bigger_number);
    std.debug.print("{}\n", .{subtracted_but_via_a_function});
}

To the best of my understanding, both subtracted and subtracted_but_via_a_function are calculated the same way: Two u32 values are coerced to i32 (which, yes, I know that isn’t safe…but bear with me…), then are subtracted. Yet, these behave in different ways:

  • If I comment out the bottom two lines, subtracted is calculated just fine, and printed.
  • If the bottom two lines are kept in, I get the following error message
scratch.zig:4:21: error: expected type 'i32', found 'u32'
    return @as(i32, a) - @as(i32, b);
                    ^

which seems to imply that Zig expected an i32 value to be passed to @as(i32, _). Isn’t that the one type you wouldn’t expect to be passed to a coercion-to-i32? And why is casting-a-u32-to-an-i32 permitted when not part of a separate function?

That error message is a little misleading. The @as builtin performs type coercion, it can only convert between types when the conversion is unambiguous and safe. So

var a: u32 = 50;
_ = &a;
var b: i32 = a;
_ = &b;

is equivalent to

var a: u32 = 50;
_ = &a;
var b = @as(i32, a);
_ = &b;

In both cases there is an error since not all u32 values fit in i32.

If the value is evaluated at comptime, such as when assigning a literal to a constant, then the compiler can perform the coercion if the value can safely coerce. In your case 50 does fit in an i32.

const a: u32 = 50; 
var b i32 = a; // no error, 50 fits in i32
_ = &b;
const a: u32 = 0xffffffff; 
var b i32 = a; // error, 0xffffffff doesn't fit in i32
_ = &b;

Function calls are only evaluated at comptime if they are specifically marked with comptime (or if the function has a comptime return type). So when you moved the type coercion into a function call it resulted in errors.

If you want to cast between runtime integer values you should use the @intCast builtin.

var a: u32 = 50;
_ = &a;
var b: i32 = @intCast(a);
_ = &b;

Note that such a cast assumes the value will fit and is safety checked in safe builds.

var a: u32 = 0xffffffff;
_ = &a;
var b: i32 = @intCast(a); // runtime error in safe build
_ = &b;

There is also @truncate and @bitCast.

6 Likes

Ah, I see - that’s helpful, thanks! I’ve seen the concept of comptime thrown around in docs, but I haven’t really grappled with it - this description makes sense, though. Thank you!

to add on to what @permutationlock explained

@as(T,v) exists to provide a type where zig cannot infer a type, or it defaults to a type that you dont want.

for example, fn foo(anytype)... does stuff
but foo(5) passes a comptime_int but thats not the behaviour you want you can do foo(@as(i32, 5)) to force zig to use an i32 instead of comptime_int

a more likely example, your doing some math that involves multiple casts

    const f: f32 = 15.24;
    const a: f32 = @floatFromInt(@intFromFloat(f) / 3) * 3.5;

doesnt compile because the compiler doesnt know what int type to cast f to
@as(i32, @intFromFloat(f))
compiles

3 Likes