Open justin330 opened 6 months ago
note that @clampTo
can already be estimated with @max
. for example @max(unsigned_int, 255)
will always yield a u8
.
Thanks @nektro, while not immediately obvious, I did eventually learn that @max/@min
actually stops the compiler from failing due to signed to unsigned conversions, though it does seem to be a hit-or-miss at times.
Its not about Zig lacking the ability to do these things modularly, its about improving the ergonomics of how it does it to:
Excuse me if this comes across as a rant, it just feels like it brings out the worst aspect of Zig (verbosity).
note that
@clampTo
can already be estimated with@max
. for example@max(unsigned_int, 255)
will always yield au8
.
This is simply not true. @max
will yield the larger type in the coerce.
EDIT: you're probably thinking of @min
.
I think it's better to not add new builtins for the sake of simplification.
Buitins should be keep for things we can't do "by hand" or things that can be highly optimized when done as compiler intrinsics.
They could be implemented in user land, and added to std if useful enough.
@Lking03x While I understand your point and strongly agree in principal, I don't think that's the case here.
What I'm requesting isn't simplification for the sake of simplification, its a refinement which improves Zig in many aspects, through addressing a type of boilerplate that is forced, frequent, and trivial to do incorrectly (runtime UB).
As for why it should be a built-in that is readily-available and easy to access, because:
It is frequent, and involves the most primitive aspects of the language.
It is necessary, as Zig requires us to explicitly handle range conversions ourselves anyway.
The compiler is already capable of presuming most of what we need to and are going to write anyway and will guide us towards it, but it cannot do it for us because: 1) In most cases there are two equally applicable options to pick from for handling range-fitting (at least AFAIK). 2) Automatically choosing either would be a form of hidden control flow.
Writing out specifically-tailored versions of 'wrapping' and 'clamping' inline seems like something the compiler should be doing instead, with less room for mistakes.
Use throughout Zig will likely solve those OOB errors caused from small mistakes writing that the compiler can't catch. eg:
// type difference has been resolved and so will compile, but the 'why' has not, and so may eventually panic
.y = @as(i16, @intFromFloat(@round(cursor_position[1]))),
// this handles the *why*, and will not panic
.y = core.clampTo(i16, @round(cursor_position[1])), // @clamp(@round(cursor_position[1])) as a built in
Only built-ins can infer return types, which simplifies the syntax and reduces verbosity. For something as frequent as a simple range conversion it's significant.
Hope you can see what I mean by this.
As a bit of an update; I've implemented both in user space and made them accessible through a module "core", it is extremely useful when working with fields of packed structs, though I realised that "wrapTo" can only work on integers without error reporting, for when a float is +-inf.
It really feels like exactly what has been missing; sat and wrap ops for "type" conversions. Would greatly appreciate these as built-ins someday. (with better names of course)
Also, to the team congratulations and great work on the 0.12 release!
I thought I had done this already but seems I forgotten; here's what I have so far:
//! fit.zig, fit.cap, fit.wrap
const std = @import("std");
/// fit by limiting to bounds
pub inline fn cap(comptime ToType: type, value: anytype) ToType {
const math = std.math;
const FromType = @TypeOf(value);
const from_info: std.builtin.Type = @typeInfo(FromType);
const error_message = "unsupported cast to: '" ++ @typeName(ToType) ++ "', from: '" ++ @typeName(FromType) ++ "'.";
return switch(@typeInfo(ToType)) {
.ComptimeFloat,.Float => @as(ToType, switch(from_info) {
.ComptimeInt,.Int => @floatFromInt(value), // safe
.ComptimeFloat,.Float => @floatCast(value), // safe
.Bool => @floatFromInt(@intFromBool(value)), // safe
else => @compileError(error_message),
}),
.ComptimeInt,.Int => @as(ToType, switch(from_info) {
.ComptimeFloat,.Float =>
if (@trunc(value) <= @as(FromType, math.minInt(ToType))) @truncate(math.minInt(ToType)) // cap to lower, handles -inf
else if (@trunc(value) >= @as(FromType, math.maxInt(ToType))) @truncate(math.maxInt(ToType)) // cap to upper, handles +inf
else if (std.math.isNan(value)) @panic("nan prevention must be handled at callsite") else @intFromFloat(@trunc(value)),
.Int,.ComptimeInt =>
if (value <= @as(ToType, math.minInt(ToType))) @truncate(math.minInt(ToType)) // safe cast
else if (value >= @as(ToType, math.maxInt(ToType))) @truncate(math.maxInt(ToType)) // safe
else @intCast(value), // safe
.Bool => @intFromBool(value),
else => @compileError(error_message),
}),
else => @compileError(error_message),
};
}
/// returns error on NaN so may provide a fallback using 'catch'
pub inline fn capOrNaN(comptime ToType: type, value: anytype) error{NaN}!ToType {
const math = std.math;
const FromType = @TypeOf(value);
const from_info: std.builtin.Type = @typeInfo(FromType);
const compile_error_message = "unsupported fit to: '" ++ @typeName(ToType) ++ "', from: '" ++ @typeName(FromType) ++ "'.";
return switch(@typeInfo(ToType)) {
.ComptimeFloat,.Float => @as(ToType, switch(from_info) {
.ComptimeInt,.Int => @floatFromInt(value), // safe
.ComptimeFloat,.Float => @floatCast(value), // safe
.Bool => @floatFromInt(@intFromBool(value)), // safe
else => @compileError(compile_error_message),
}),
.ComptimeInt,.Int => @as(ToType, switch(from_info) {
.ComptimeFloat,.Float =>
if (@trunc(value) <= @as(FromType, math.minInt(ToType))) @truncate(math.minInt(ToType)) // cap to lower, handles -inf
else if (@trunc(value) >= @as(FromType, math.maxInt(ToType))) @truncate(math.maxInt(ToType)) // cap to upper, handles +inf
else if (std.math.isNan(value)) return error.NaN else @intFromFloat(@trunc(value)), // nan or truncated float
.Int,.ComptimeInt =>
if (value <= @as(ToType, math.minInt(ToType))) @truncate(math.minInt(ToType)) // safe
else if (value >= @as(ToType, math.maxInt(ToType))) @truncate(math.maxInt(ToType)) // safe
else @intCast(value), // safe
.Bool => @intFromBool(value),
else => @compileError(compile_error_message),
}),
else => @compileError(compile_error_message),
};
}
/// fit by wrapping, ints only. as how else should I wrap +-inf floats, also floats lack upper and lower bounds to wrap around
pub inline fn wrap(comptime ToType: type, value: anytype) ToType {
const math = std.math;
const FromType = @TypeOf(value);
const error_message = "unsupported fit to: '" ++ @typeName(ToType) ++ "', from: '" ++ @typeName(FromType) ++ "'.";
return @as(ToType, switch(@typeInfo(ToType)) {
// extended std.zig.c_translation.castInt, with support for comptime int
.Int => |dest| switch(@typeInfo(FromType)) {
.Int => |source| @bitCast(@as(std.meta.Int(source.signedness, dest.bits), @truncate(value))),
.ComptimeInt => @truncate(value),
else => @compileError(error_message),
},
else => @compileError(error_message),
});
}
Feel free to try this out and use it however you'd like.
Notes:
Like a saturating operator for assignment/conversions. Safe for all values with the exception of nan -> int.
@clampTo(i2, @as(f32, @trunc(-1023.33))) == -1
Like int -> int conversions in C, truncating bits. but can now be applied to float -> int conversions as well.
@wrapTo(u4, @trunc(-1023.33)) == 0
And so we go from the current state of rewriting subsets of this logic at every nearly every assignment, making it very easy to screw up, harder to read, and still ending up with runtime panics if not done correctly... To these simpler, well-defined conversions for passing values through different numerical ranges. Leaving the only concern being a nan -> int, which could resolved in a similar way to @divExact().
e.g:
const x: u8 = @bitCast(@as(i8, @truncate(y -% @as(i32, @bitCast(@as(u32, @truncate(i)))))));
becomes:
const x = @wrapTo(u8, y +% @wrapTo(i32, i));
or with more developed inference, simply:
const x: u8 = @wrap(y +% @wrap(i));
(note: still using version 0.11, so just close if this has already been addressed in 0.12.0-dev)