Repository navigation
unable to infer expression type when adding u32 to if-else number literals #137
Description
Activity
- addedenhancementSolving this issue will likely involve adding new logic or components to the codebase.Solving this issue will likely involve adding new logic or components to the codebase.
on Apr 12, 2016 Also this should work:
const x = u32(switch (foo) { 3 => 1, 4 => 2, else => 5, });
i had some problems when defining consts and then using them to make values for variables :
something like this would fail:const TINY_QUANTUM_SHIFT = 4; const TINY_QUANTUM_SIZE = 1 << TINY_QUANTUM_SHIFT; var block_aligned_stuff: usize = (@sizeOf(variable) + TINY_QUANTUM_SIZE) & ~(TINY_QUANTUM_SIZE -1 );i resolved this casting the const to the appropriate type every time i need to use them.
in the code example here, i think the compiler does not know what type to use for TINY_QUANTUM_SIZE, so it cant do the negation and the and operation on 32 nor 64 bits, same with any other bitwise operator, I would be a good idea to define literals as java/c++ does :
const sizevalue = 3123UL; // to be used as a unsigned long const floatvalue = 1.0f; // to be used as a float const doublevalue = 1.0; // to be used as a double // etc..@thejoshwolfe I don't currently have an elegant way to solve this problem. Do you know how it should work? Currently there is no concept of "expected type". Instead there is implicit casting (passing null to ?i32 parameter) and peer type resolution (
a + bwhere a and b are i16 and i32).fn foo(b: bool) { const a = if (b) 1 else 2; bar(a) }
What should the above code do?
My guess would be that the type of
ashould be the narrowest signed type able to satisfy storage needs of both clauses of the if/else. Ifbar(a)then accepts a wider type,acan be extended. So literals can be parsed under the widest type but then narrowed in terms of the type they are then given.My guess would be that the type of a should be the narrowest signed type able to satisfy storage needs of both clauses of the if/else.
Consider this code:
fn foo(condition: bool) { const a = if(condition) 50 else 100; // narrowest signed type is i8 const b = a + 100; // crashes the program due to integer overflow }
This seems like it would be really easy to accidentally cause a crash (or undefined behavior in release mode). Also, I'm considering adding integers with arbitrary bit width, so then it would make the choice kind of weird. If the numbers are 0 and 1, do we choose only 1 bit?
thejoshwolfe commented
on Jan 20, 2017 SponsorContributorAuthorMore actionsDespite the
constin @andrewrk's example,ais actually a runtime variable; it's just restricted to single assignment. I think an explicit type should be required somewhere inconst a = if (runtimeThing) 1 else 0;just like it is forvar a = 0; if (runtimeThing) { a = 1; }. This would be consistent with zig as it is today.Unfortunately, the proposal in the above paragraph would mean that the expression
if (runtimeThing) 1 else 0needs an explicit type too, because that expression has a runtime-known value. I don't like this idea, because it seems so silly to explicitly put a type on 0 and 1.Java has a solution to this dilemma, which is assume number literals are s32. this works great for casual math, but breaks down when you deal with arithmetic that would overflow s32 in subtle ways, and it's also pretty silly when you try to pass a 0 as a byte argument: you get a "int not implicitly castable to byte" compile error unless you explicitly cast
(byte)0. (maybe java 7 or 8 fixes this; I haven't been keeping up with the latest java.)So here's a different proposal. Similar to the integer literal type that zig currently uses at compile time for constants like
const a = 300;, we also have a "integer that isn't known at compile time, but its bounds are known at compile time".- The type of
if (runtimeThing) 1 else 0would be "integer between 0 and 1". - The type of
if (runtimeThing) -1 else 0would be "integer between -1 and 0". - The type of
(if (a) 50 else 100) + (if (b) -25 else 100)would be "integer between 25 and 200". - The type of
(if (a) -1000 else -1001) + 1001would be "integer between 0 and 1".
Note that despite the presence of negative numbers in an expression, the concept of signed or unsigned in the inputs does not infect the output. When it comes time to implicitly cast one of these bounded integers to a runtime type, like u8, then the rule is simply that both the bounds have to fit into the destination type. For peer type resolution with a runtime type (like u32), the bounded integer must implicitly cast to the peer type.
A suggestion for implementing this is that the current concept of number literals can be implemented as "integer between 15 and 15". This would fit nicely into a case where you multiply a bounded integer by 0.
I tried to write down the algorithms for how the (min,max) would be calculated for various operators, but the multiplicative ones are pretty tricky. Worst case for any operator is simply calculate all 4 possible values and pick the min and max of the set. This entire proposal would only work on the assumption that more extreme inputs result in more extreme outputs for all operators, which I believe is true as long as overflow is not allowed. We may want to carefully study every single operator individually to make sure this is true.
The above describes how the compiler would track the bounds of the values, but what about the generated runtime code? All operators need types at runtime. We can prevent overflow by enforcing that the inputs and output of every intermediate operation fit into some runtime type, and then using appropriate operators and type casts to make it all work. I'm not super confident about this, but I think it will work. One implication of this is that bounded integers are not allowed to be arbitrary precision; they all need to work at runtime. This might be an argument for not implementing number literals as bounded integers with equal upper and lower bounds.
This proposal cannot accurately predict when the runtime will do integer division by 0. But as long as that's undefined behavior, we shouldn't have any problem with an inability to detect that. (Of course, we could still detect if you would definitely divide by 0, just like we can now.)
- The type of
Asked to put this here by Andrew... Examples of possibly troublesome type unifications.
function f(runtime_x: int8_t) -> ??? { var a = if (runtime_x > 5) then 0 else 255; //typeof(a) can unify to a uint8_t var b = if (runtime_x == 9) then -2 else 8; //typeof(b) can unify to an int8_t var c = if (runtime_x < 5) then 0 else 10; //would typeof(c) unify to a uint8_t or an int8_t? var d = if (runtime_x < 5) then 0 else 250 + runtime_x; //would typeof(d) unify to a uint8_t or an int16_t/uint16_t? (i.e. is it smart enough to recognize the constraint met by the if clause?) var e = if (runtime_x < 30) then runtime_x else 250 + (runtime_x % 5); //would typeof(e) unify to a uint8_t or an int16_t/uint16_t? (i.e. is it smart enough to recognize the constraint met by the modulo expression?) return a; //or b, or c, or d, or e }- added a commit that references this issue
on May 25, 2017 I want to note that rust has not solved this problem, because their number literals are of type
i32:fn thing(x: i32) { let y = 2147483646 + match x { 3 => 1, 4 => 2, _ => 3 }; println!("{}", y); } fn main() { thing(1); }
andy@xps ~/tmp> rustc test.rs andy@xps ~/tmp> ./test thread 'main' panicked at 'attempt to add with overflow', test.rs:2:13 note: Run with `RUST_BACKTRACE=1` for a backtrace.Zig at least catches this with a compile error.
14 remaining items
- addedfrontendTokenization, parsing, AstGen, Sema, and Liveness.Tokenization, parsing, AstGen, Sema, and Liveness.
on Oct 9, 2020 Yikes, a 5+ year old issue! Doesn't every newcomer run into this on the first day?
My recommendations for addressing this issue in priority order:
- Please issue a better error message.
- Please add a syntax for type annotation.
- Please fix the type checker.
The following code "day one" code breaks in Zig:
const oflags : u32 = std.os.O_CLOEXEC | std.os.O_NOATIME | (if (flags.read_only) std.os.O_RDONLY else (std.os.O_RDWR | std.os.O_CREAT));The current error, cannot store runtime value in type 'comptime__int', makes no sense: the error occurs inside a pure expression, there is no storage. Unfortunately, the most likely naive interpretation is that this error message refers to the initialization of the constant (conflating 'comptime' with 'const', and 'storage' with 'initilization'). This creates a garden path which misinforms beginners that, "constants can't be initialized with runtime expressions" (untrue). In fact Zig does support such expressions, the real issue is that the compiler (currently) requires a type annotation on some literals with unresolved types:
const oflags : u32 = std.os.O_CLOEXEC | std.os.O_NOATIME | (if (flags.read_only) @intCast(u32, std.os.O_RDONLY) else (std.os.O_RDWR | std.os.O_CREAT));This error is surprising, since the programmer understands that every leaf in the expression tree is a
comptime__int, and every node in the expression legally combinescomptime_ints.Even a slight variations such as annotating the
ifexpression will currently fail. Apparently twocomptime_ints just can't pass through a conditional. This just seems like a bug in the type checker (ifshould remain polymorphic oncomptime_int). While this issue remains, the error message should be improved:-
Don't mention storage in a pure computation--this is very confusing.
-
Do indicate that a type annotation is sometimes necessary on literals--the current compiler even has a little green arrow pointing to exactly where the annotation is needed.
Finally, it should be possible work around this issue without a cast, but I could not find a syntax for type annotation in the Zig docs. Something like this:
const oflags : u32 = std.os.O_CLOEXEC | std.os.O_NOATIME | (if (flags.read_only) (std.os.O_RDONLY : u32) else (std.os.O_RDWR | std.os.O_CREAT));Type annotations can be a very useful sanity checks for a programmer, even when they are not explicitly required by the compiler.
Sorry for the long message. Thanks for all the hard work! I'm enjoying exploring Zig.
Regarding "Finally, it should be possible work around this issue without a cast, but I could not find a syntax for type annotation in the Zig docs.".
@asallows this:const oflags = std.os.O_CLOEXEC | std.os.O_NOATIME | if (flags.read_only) @as(u32, std.os.O_RDONLY) else (std.os.O_RDWR | std.os.O_CREAT);
Reacted by Anthony Carrico and Matheus C. FrançaGood. In the semantics of Zig, I was wrong to say literals are unresolved types (although maybe they should be), so a cast does makes sense, and
@asis certainly better than@intCast. Thank you for the helpful comment.Type annotation would be a useful tool as an assertion to make sure the programmer and the type checker agree on what is going on without casting, but that is a separate issue.
@asis basically a type annotation. It is not a cast, it will only coerce things that can coerce normally.
unresolved literal ints are of type comptime_int