ziglang / zig

General-purpose programming language and toolchain for maintaining robust, optimal, and reusable software.
https://ziglang.org
MIT License
34.32k stars 2.51k forks source link

Simplify converting between numerical ranges with the inclusion of built-ins such as @clampTo, and @wrapTo #19456

Open justin330 opened 6 months ago

justin330 commented 6 months ago

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)

nektro commented 6 months ago

note that @clampTo can already be estimated with @max. for example @max(unsigned_int, 255) will always yield a u8.

justin330 commented 6 months ago

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).

Rexicon226 commented 6 months ago

note that @clampTo can already be estimated with @max. for example @max(unsigned_int, 255) will always yield a u8.

This is simply not true. @max will yield the larger type in the coerce.

EDIT: you're probably thinking of @min.

Lking03x commented 6 months ago

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.

justin330 commented 5 months ago

@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:

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!

justin330 commented 2 months ago

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: