Proposal: Better lossy casting for int-to-float

I’m creating a window/graphics library in Zig, and during this process I’ve hit some more headaches with the casting syntax.

This stems from the changes with implicit int-to-float conversions (small enough integer can implicitly convert to a float) as well as using @trunc, @floor, @ceil, @round in place of @intFromFloat.

These changes are great, as they drastically improve ergonomics and more importantly, code readability. However, a common pattern I’m running into is around float arithmetic involving either array length or for loop indexes.

The implicit casting makes sense for Zig’s goals, as it only occurs when no information is lost. If an integer width cannot fit into a float, an explicit cast is needed.

After doing some experiments with sine waves, I was needing to use a sine buffer’s length or the index of a for loop, which are usize, during arithmetic. This resulted in some ugly lines such as

for (sin_d, 0..) |s, i| {
  const ratio: f32 = @as(f32, @floatFromInt(i)) / @as(f32, @floatFromInt(sin_d.len - scale));
  ...
}

For lossy float-to-int conversions, we have the previously mentioned @trunc, @floor, @ceil, and @round. These are a big improvement over the previous syntax because they’re short, they explain what they do immediately, and they don’t hurt code readability.

The above example, in a perfect world, would instantly convey to the reader that ratio, a f32, is simply the for loop index divided by (array length - some factor). Instead, my eyes have to search through all the “@” builtins to see what’s actually going on.

Proposal

All I’m proposing is a simpler, shorter lossy int-to-float conversion syntax, similar to @trunc and related functions. My immediate thought was along the lines of @float(value) (where “float” could be any name, preferably just as short but perhaps more descriptive), which performs a (potentially) lossy cast of an int to a float. It would work on integer widths that could implicitly convert to a float, but ommitting it would cause a compile error on widths that can’t, just like current behavior.

An issue that I see with this is that @as(T, value) is used in the example because Zig’s PTR can’t discern what type of float these values should be. In that case, a function such as @float(T, value) may be better. It might be important to mention that value is required to be an int, as this is supposed to replace @as(T, @floatFromInt(value)).

My argument for this is that if value is required to be an int, it is just as explicit as @as(T, @floatFromInt(value)), while making the code much more readable. The programmer is still bound by the current casting requirements, and the reader is getting the same information, just in a more clear and readable format.

This proposed change doesn’t affect current language syntax (in the way that value as f32 would), just a different builtin casting function (or family of).

There’s been a lot of discussion and debate around the casting in Zig over the years, I’m curious to know thoughts on this.

1 Like

I kind of like this, using the usize of for loops as a number (float or int) is really cumbersome as right now. This addresses the float issue. Is there an easy way to use it as an int? (Other than the @as( T, …) )

If you’re asking if there’s an easy way to use loop indexes as an int, I guess it depends on what you’re doing. I run into issues a lot with unsigned ints in general, where a snippet like

const bar: i8 = -3;
var foo: u32 = 10;
foo += bar;

is a compile error, due to RLS, and must be written as

foo = @intCast(@as(i8, @intCast(foo)) + bar);