Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

make @boolToInt always return a u1 even when the bool is comptime known #7950

Closed
SpexGuy opened this issue Feb 5, 2021 · 19 comments · Fixed by #15701
Closed

make @boolToInt always return a u1 even when the bool is comptime known #7950

SpexGuy opened this issue Feb 5, 2021 · 19 comments · Fixed by #15701
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Milestone

Comments

@SpexGuy
Copy link
Contributor

SpexGuy commented Feb 5, 2021

A core design pillar of the language is that code that works at runtime should be able to work at compile time (barring significant metaprogramming or weird pointer shenanigans). This rule is broken here:

const signed_bool = @bitCast(i1, @boolToInt(value));

This code will compile with a runtime known value but not with a comptime known value. The workaround is to use @bitCast(i1, @as(u1, @boolToInt(value))), but this feels like a "trick" to make the code safe for use at comptime. The fewer of those we have, the better.

Instead, @boolToInt should return a comptime known u1 when the parameter is comptime known, to be consistent.

@ifreund
Copy link
Member

ifreund commented Feb 5, 2021

Reading this made me wonder why @boolToInt() exists in the first place while if (value) 1 else 0 would work just as well. It could also be implemented in the std as a function and return an u1 instead of a comptime_int if we wanted to match the current builtin's API (with this proposed amendment):

pub fn boolToInt(value: bool) i1 {
    return if (value) @as(u1, 1) else @as(u1, 0);
}

@SpexGuy
Copy link
Contributor Author

SpexGuy commented Feb 5, 2021

The nice thing about @boolToInt is that it won't generate a branch. If you use the if version, you are creating a branch and then trusting the optimizer to delete it. Which will sometimes work, but it's not the nicest way of doing things. The equivalent C code to what I'm writing looks like this: -(a < b). In Zig though it needs a whole bunch of casts. Which isn't necessarily a problem, but needing a whole bunch of casts and branching control flow feels like a bridge too far.

@LemonBoy
Copy link
Contributor

LemonBoy commented Feb 5, 2021

diff --git a/lib/std/math.zig b/lib/std/math.zig
index 6e7c5c091..f584e2357 100644
--- a/lib/std/math.zig
+++ b/lib/std/math.zig
@@ -1127,7 +1127,7 @@ pub fn maxInt(comptime T: type) comptime_int {
     const info = @typeInfo(T);
     const bit_count = info.Int.bits;
     if (bit_count == 0) return 0;
-    return (1 << (bit_count - @boolToInt(info.Int.signedness == .signed))) - 1;
+    return (1 << (bit_count - @as(comptime_int, @boolToInt(info.Int.signedness == .signed)))) - 1;
 }
 
 pub fn minInt(comptime T: type) comptime_int {
diff --git a/src/codegen.zig b/src/codegen.zig
index 904fda0de..d4f9b505e 100644
--- a/src/codegen.zig
+++ b/src/codegen.zig
@@ -3590,7 +3590,7 @@ fn Function(comptime arch: std.Target.Cpu.Arch) type {
             switch (mcv) {
                 .immediate => |imm| {
                     // This immediate is unsigned.
-                    const U = std.meta.Int(.unsigned, ti.bits - @boolToInt(ti.signedness == .signed));
+                    const U = std.meta.Int(.unsigned, ti.bits - @as(comptime_int, @boolToInt(ti.signedness == .signed)));
                     if (imm >= math.maxInt(U)) {
                         return MCValue{ .register = try self.copyToTmpRegister(inst.src, mcv) };
                     }
diff --git a/src/stage1/ir.cpp b/src/stage1/ir.cpp
index e87687302..d8cefcdbf 100644
--- a/src/stage1/ir.cpp
+++ b/src/stage1/ir.cpp
@@ -27845,15 +27845,18 @@ static IrInstGen *ir_analyze_instruction_bool_to_int(IrAnalyze *ira, IrInstSrcBo
         return ira->codegen->invalid_inst_gen;
     }
 
+    ZigType *u1_type = get_int_type(ira->codegen, false, 1);
+
     if (instr_is_comptime(target)) {
         bool is_true;
         if (!ir_resolve_bool(ira, target, &is_true))
             return ira->codegen->invalid_inst_gen;
 
-        return ir_const_unsigned(ira, &instruction->base.base, is_true ? 1 : 0);
+        IrInstGen *result = ir_const(ira, &instruction->target->base, u1_type);
+        bigint_init_unsigned(&result->value->data.x_bigint, is_true ? 1 : 0);
+        return result;
     }
 
-    ZigType *u1_type = get_int_type(ira->codegen, false, 1);
     return ir_resolve_cast(ira, &instruction->base.base, target, u1_type, CastOpBoolToInt);
 }
 

@ifreund
Copy link
Member

ifreund commented Feb 5, 2021

The nice thing about @boolToInt is that it won't generate a branch. If you use the if version, you are creating a branch and then trusting the optimizer to delete it. Which will sometimes work, but it's not the nicest way of doing things. The equivalent C code to what I'm writing looks like this: -(a < b). In Zig though it needs a whole bunch of casts. Which isn't necessarily a problem, but needing a whole bunch of casts and branching control flow feels like a bridge too far.

That's a good point. Check this out though:

const std = @import("std");

pub fn main() void {
    std.debug.print("{}\n", .{@bitCast(u1, true)}); // 1
    std.debug.print("{}\n", .{@bitCast(u1, false)}); // 0
}

No branching or control flow needed. I'm not sure if this is UB or not though as we don't have a spec yet, but I'm assuming zig's bool will continue have the same ABI as C's _Bool to avoid the need for a c_bool type.

@SpexGuy
Copy link
Contributor Author

SpexGuy commented Feb 5, 2021

I would be open to replacing boolToInt with bitCast, but I think I like bitCast a lot more than Andrew does. It would simplify the language a bit though.

@SpexGuy SpexGuy added the proposal This issue suggests modifications. If it also has the "accepted" label then it is planned. label Mar 16, 2021
@andrewrk andrewrk added the accepted This proposal is planned. label May 19, 2021
@andrewrk andrewrk added this to the 0.9.0 milestone May 19, 2021
@andrewrk andrewrk changed the title Design flaw: @boolToInt returns a comptime_int if the bool is comptime known make @boolToInt always return a u1 even when the bool is comptime known May 19, 2021
@andrewrk
Copy link
Member

I think I have not expressed my love of @bitCast enough. It's great

@andrewrk andrewrk modified the milestones: 0.9.0, 0.10.0 Nov 20, 2021
@andrewrk andrewrk modified the milestones: 0.10.0, 0.11.0 Apr 16, 2022
@tisonkun
Copy link
Contributor

tisonkun commented May 6, 2023

It seems with #13560 the instruction analyze code gets significantly refactor.

@andrewrk could you provide some insights here where is the new code? That is, where we analyze the return type of @boolToInt.

I found:

// src/Air.zig
.bool_to_int => return Type.initTag(.u1),

But it seems there is still another path to resolve to comptime_int.

@mlugg
Copy link
Member

mlugg commented May 6, 2023

You want zirBoolToInt in Sema.zig - the reason it's a comptime_int is because it's returning either the ref .zero or .one, both of which refer to comptime_ints. The fix is pretty trivial, but I've actually incidentally fixed it while working on the InternPool branch literally like an hour ago, so this should be fixed once InternPool is merged.

@tisonkun
Copy link
Contributor

tisonkun commented May 6, 2023

@mlugg That sounds good, lol. Could you link the PR or working branch here?

@mlugg
Copy link
Member

mlugg commented May 6, 2023

#15569 - it's an incredibly major refactor, so will be a while before it's merged

@tisonkun
Copy link
Contributor

tisonkun commented May 6, 2023

Thank you!

but I've actually incidentally fixed it

Out of curiosity, it seems a solowork by @andrewrk, how do you collaborate there?

@mlugg
Copy link
Member

mlugg commented May 6, 2023

The branch is broad enough that it's not too difficult to collaborate without stepping on each others' toes too much - I was eager to get this change in, so I'm doing some bits of work for it on my fork and he's cherry-picking my changes in.

@tisonkun
Copy link
Contributor

tisonkun commented May 13, 2023

UPDATE: it seems I reverse the boolean logic (oops). Let me try to correct it.

original comment @mlugg do you have the concrete commit for implementing this feature? I tried it locally to understand how the Zig compiler works and found that it's more complex than `@boolToInt` is always `u1`.

That is, comptime_int can be resolved to it's real bits in type calculate. If we hard code @boolToInt's return value as u1, the following warnings and errors will generate:

[100%] Building stage3
error: sub-compilation of compiler_rt failed
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i32' cannot represent integer value '4294967295'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixhfsi.zig:11:22: note: called from here
    return floatToInt(i32, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i64' cannot represent integer value '18446744073709551615'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixhfdi.zig:11:22: note: called from here
    return floatToInt(i64, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i128' cannot represent integer value '340282366920938463463374607431768211455'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixhfti.zig:16:22: note: called from here
    return floatToInt(i128, a);
           ~~~~~~~~~~^~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i32' cannot represent integer value '4294967295'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixsfsi.zig:15:22: note: called from here
    return floatToInt(i32, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i64' cannot represent integer value '18446744073709551615'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixsfdi.zig:15:22: note: called from here
    return floatToInt(i64, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i128' cannot represent integer value '340282366920938463463374607431768211455'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixsfti.zig:16:22: note: called from here
    return floatToInt(i128, a);
           ~~~~~~~~~~^~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i32' cannot represent integer value '4294967295'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixdfsi.zig:15:22: note: called from here
    return floatToInt(i32, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i64' cannot represent integer value '18446744073709551615'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixdfdi.zig:15:22: note: called from here
    return floatToInt(i64, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i128' cannot represent integer value '340282366920938463463374607431768211455'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixdfti.zig:16:22: note: called from here
    return floatToInt(i128, a);
           ~~~~~~~~~~^~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i32' cannot represent integer value '4294967295'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixtfsi.zig:16:22: note: called from here
    return floatToInt(i32, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i64' cannot represent integer value '18446744073709551615'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixtfdi.zig:16:22: note: called from here
    return floatToInt(i64, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i128' cannot represent integer value '340282366920938463463374607431768211455'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixtfti.zig:16:22: note: called from here
    return floatToInt(i128, a);
           ~~~~~~~~~~^~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i32' cannot represent integer value '4294967295'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixxfsi.zig:11:22: note: called from here
    return floatToInt(i32, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i64' cannot represent integer value '18446744073709551615'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixxfdi.zig:11:22: note: called from here
    return floatToInt(i64, a);
           ~~~~~~~~~~^~~~~~~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/float_to_int.zig:35:65: note: type 'i128' cannot represent integer value '340282366920938463463374607431768211455'
            return if (negative) math.minInt(I) else math.maxInt(I);
                                                     ~~~~~~~~~~~^~~
    /Users/chenzili/Brittani/zig/lib/compiler_rt/fixxfti.zig:16:22: note: called from here
    return floatToInt(i128, a);
           ~~~~~~~~~~^~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '6'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '9'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '12'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '16'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '16'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
/Users/chenzili/Brittani/zig/lib/std/io.zig:345:75: error: type 'i32' cannot represent integer value '4294967295'
            const events_len = try os.poll(&self.poll_fds, std.math.maxInt(i32));
                                                           ~~~~~~~~~~~~~~~^~~~~
referenced by:
    poll: /Users/chenzili/Brittani/zig/lib/std/io.zig:256:24
    evalZigProcess: /Users/chenzili/Brittani/zig/lib/std/Build/Step.zig:339:29
    remaining reference traces hidden; use '-freference-trace' to see all reference traces

/Users/chenzili/Brittani/zig/lib/std/io.zig:345:75: error: type 'i32' cannot represent integer value '4294967295'
            const events_len = try os.poll(&self.poll_fds, std.math.maxInt(i32));
                                                           ~~~~~~~~~~~~~~~^~~~~
/Users/chenzili/Brittani/zig/lib/std/io.zig:345:75: error: type 'i32' cannot represent integer value '4294967295'
            const events_len = try os.poll(&self.poll_fds, std.math.maxInt(i32));
                                                           ~~~~~~~~~~~~~~~^~~~~
/Users/chenzili/Brittani/zig/lib/std/fs/file.zig:929:94: error: type 'isize' cannot represent integer value '18446744073709551615'
                .tv_sec = math.cast(isize, @divFloor(atime, std.time.ns_per_s)) orelse maxInt(isize),
                                                                                       ~~~~~~^~~~~~~
/Users/chenzili/Brittani/zig/lib/std/io.zig:345:75: error: type 'i32' cannot represent integer value '4294967295'
            const events_len = try os.poll(&self.poll_fds, std.math.maxInt(i32));
                                                           ~~~~~~~~~~~~~~~^~~~~
make[2]: *** [stage3/bin/zig] Error 2
make[1]: *** [CMakeFiles/stage3.dir/all] Error 2

... which means we need new extra type cast.

My change is:

diff --git a/src/Sema.zig b/src/Sema.zig
index 1b55f765f..95697b8d8 100644
--- a/src/Sema.zig
+++ b/src/Sema.zig
@@ -18410,8 +18410,8 @@ fn zirBoolToInt(sema: *Sema, block: *Block, inst: Zir.Inst.Index) CompileError!A
     const operand = try sema.resolveInst(inst_data.operand);
     if (try sema.resolveMaybeUndefVal(operand)) |val| {
         if (val.isUndef()) return sema.addConstUndef(Type.initTag(.u1));
-        const bool_ints = [2]Air.Inst.Ref{ .zero, .one };
-        return bool_ints[@boolToInt(val.toBool())];
+        if (val.toBool()) return sema.addConstant(Type.initTag(.u1), Value.initTag(.zero));
+        return sema.addConstant(Type.initTag(.u1), Value.initTag(.one));
     }
     return block.addUnOp(.bool_to_int, operand);
 }

@mlugg
Copy link
Member

mlugg commented May 13, 2023

The relevant commit was dropped due to how Andrew merged my changes - I'll just PR this to master shortly. I hadn't tested the relevant change since the branch I originally did it on isn't yet usable. There will probably be a few changes needed to support the new change in code which unintentionally assumed the result type was comptime_int - and indeed, it seems like you've caught some of them (although yes, some of those are just from your reversed logic).

@tisonkun
Copy link
Contributor

tisonkun commented May 13, 2023

Thanks for sharing your works. Now the remaining error is:

error: sub-compilation of compiler_rt failed
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '6'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
referenced by:
    type 'u1' cannot represent integer value '6': std/math/ilogb.zig:1:2
    12 reference(s) hidden; use '-freference-trace=14' to see all references
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig: ler_rt failed:1531:1533

    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '9'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '12'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '16'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
    /Users/chenzili/Brittani/zig/lib/std/math/ilogb.zig:51:26: note: type 'u1' cannot represent integer value '16'
        const offset = 1 + exponentBits + @boolToInt(T == f80) - exponentBias;
                       ~~^~~~~~~~~~~~~~
make[2]: *** [stage3/bin/zig] Error 2
make[1]: *** [CMakeFiles/stage3.dir/all] Error 2

I may submit a patch for reference if I have time to work it out.

Although, this can indicate that such a change is a breaking change that may prevent it from accepted.

@mlugg
Copy link
Member

mlugg commented May 13, 2023

The proposal is already accepted. Zig is a v0 language, and as such is very open to breaking changes.

@likern
Copy link

likern commented May 15, 2023

The nice thing about @boolToInt is that it won't generate a branch. If you use the if version, you are creating a branch and then trusting the optimizer to delete it. Which will sometimes work, but it's not the nicest way of doing things. The equivalent C code to what I'm writing looks like this: -(a < b). In Zig though it needs a whole bunch of casts. Which isn't necessarily a problem, but needing a whole bunch of casts and branching control flow feels like a bridge too far.

Can we just guarantee that optimization? It seems weird (right now I came across this). Having @boolToInt I expected @intToBool conversion (or at least something in std).

But it was Ok having if else branching in that case, why wouldn't it's Okay in case of @boolToInt?

I think having guarantee to optimize away when we can (like guaranteed return value optimization in C++ and something similar in Zig) is better way to deal with issue.

@Vexu
Copy link
Member

Vexu commented May 15, 2023

@intToBool(int) is int != 0.

@likern
Copy link

likern commented May 15, 2023

@intToBool(int) is int != 0.

If you find that mentioning somewhere in issues (I found that reply from Andrew).

I bet 95% will write (including myself)

const val = 4;
if(val == 0) false else true 

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
accepted This proposal is planned. proposal This issue suggests modifications. If it also has the "accepted" label then it is planned.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

8 participants