From 55cac65f957fc374e4e369e26bd338f11b8b37ee Mon Sep 17 00:00:00 2001 From: Evan Haas Date: Mon, 7 Dec 2020 14:20:45 -0800 Subject: [PATCH] Support casting enums to all int types. In C, enums are represented as signed integers, so casting from an enum to an integer should use the "cast integer to integer" translation code path. Previously it used the "cast enum to generic non-enum" code path, because enums were not being treated as integers. Ultimately this can produce zig code that fails to compile if the destination type does not support the full range of enum values (e.g. translated C code that casts an enum value to an unsigned integer would fail to compile since enums are signed integers, and unsigned integers cannot represent the full range of values that signed ones can). One interesting thing that came up during testing is that the implicit enum-to-int cast that occurs when an enum is used in a boolean expression was parsed as an (int) by some versions of the zig compiler, and an (unsigned int) cast by others. Specifically, the following code: ```c enum Foo {Bar, Baz}; // ... enum Foo foo = Bar; if (0 || foo) { // do something } ``` When tested on MacOS, Linux, and Windows using a compiler built from the Windows Zig Compiler Dev Kit, the above code would emit a cast to c_uint: `if (false or (@bitCast(c_uint, @enumToInt(foo)) != 0)) {}` However when tested on Windows with a Zig compiler built using MSVC, it produces: `if (false or (@bitCast(c_int, @enumToInt(foo)) != 0)) {}` In this particular case I don't think it matters, since a c_int and c_uint will have the same representation for zero, but I'm not sure if this is ultimately the result of implementation-defined behavior or something else. Because of this, I added explicit casts in the `translate_c.zig` tests, to ensure that the emitted zig source exactly matches across platforms. I also added a behavior test in `run_translated_c.zig` that uses the old implicit casts from `translate_c.zig` to ensure that the emitted Zig code behaves the same as the C code regardless of what cast is used. --- src/translate_c.zig | 60 +++++++++++++------- test/run_translated_c.zig | 113 ++++++++++++++++++++++++++++++++++++++ test/translate_c.zig | 12 ++-- 3 files changed, 160 insertions(+), 25 deletions(-) diff --git a/src/translate_c.zig b/src/translate_c.zig index 424691513a..7469c6e996 100644 --- a/src/translate_c.zig +++ b/src/translate_c.zig @@ -1993,6 +1993,21 @@ fn transStringLiteral( } } +fn cIsEnum(qt: clang.QualType) bool { + return qt.getCanonicalType().getTypeClass() == .Enum; +} + +/// Get the underlying int type of an enum. The C compiler chooses a signed int +/// type that is large enough to hold all of the enum's values. It is not required +/// to be the smallest possible type that can hold all the values. +fn cIntTypeForEnum(enum_qt: clang.QualType) clang.QualType { + assert(cIsEnum(enum_qt)); + const ty = enum_qt.getCanonicalType().getTypePtr(); + const enum_ty = @ptrCast(*const clang.EnumType, ty); + const enum_decl = enum_ty.getDecl(); + return enum_decl.getIntegerType(); +} + fn transCCast( rp: RestorePoint, scope: *Scope, @@ -2005,40 +2020,45 @@ fn transCCast( if (dst_type.eq(src_type)) return expr; if (qualTypeIsPtr(dst_type) and qualTypeIsPtr(src_type)) return transCPtrCast(rp, loc, dst_type, src_type, expr); - if (cIsInteger(dst_type) and cIsInteger(src_type)) { - // 1. Extend or truncate without changing signed-ness. - // 2. Bit-cast to correct signed-ness + if (cIsInteger(dst_type) and (cIsInteger(src_type) or cIsEnum(src_type))) { + // 1. If src_type is an enum, determine the underlying signed int type + // 2. Extend or truncate without changing signed-ness. + // 3. Bit-cast to correct signed-ness + + const src_type_is_signed = cIsSignedInteger(src_type) or cIsEnum(src_type); + const src_int_type = if (cIsInteger(src_type)) src_type else cIntTypeForEnum(src_type); + const src_int_expr = if (cIsInteger(src_type)) expr else try transEnumToInt(rp.c, expr); // @bitCast(dest_type, intermediate_value) const cast_node = try rp.c.createBuiltinCall("@bitCast", 2); cast_node.params()[0] = try transQualType(rp, dst_type, loc); _ = try appendToken(rp.c, .Comma, ","); - switch (cIntTypeCmp(dst_type, src_type)) { + switch (cIntTypeCmp(dst_type, src_int_type)) { .lt => { - // @truncate(SameSignSmallerInt, src_type) + // @truncate(SameSignSmallerInt, src_int_expr) const trunc_node = try rp.c.createBuiltinCall("@truncate", 2); - const ty_node = try transQualTypeIntWidthOf(rp.c, dst_type, cIsSignedInteger(src_type)); + const ty_node = try transQualTypeIntWidthOf(rp.c, dst_type, src_type_is_signed); trunc_node.params()[0] = ty_node; _ = try appendToken(rp.c, .Comma, ","); - trunc_node.params()[1] = expr; + trunc_node.params()[1] = src_int_expr; trunc_node.rparen_token = try appendToken(rp.c, .RParen, ")"); cast_node.params()[1] = &trunc_node.base; }, .gt => { - // @as(SameSignBiggerInt, src_type) + // @as(SameSignBiggerInt, src_int_expr) const as_node = try rp.c.createBuiltinCall("@as", 2); - const ty_node = try transQualTypeIntWidthOf(rp.c, dst_type, cIsSignedInteger(src_type)); + const ty_node = try transQualTypeIntWidthOf(rp.c, dst_type, src_type_is_signed); as_node.params()[0] = ty_node; _ = try appendToken(rp.c, .Comma, ","); - as_node.params()[1] = expr; + as_node.params()[1] = src_int_expr; as_node.rparen_token = try appendToken(rp.c, .RParen, ")"); cast_node.params()[1] = &as_node.base; }, .eq => { - cast_node.params()[1] = expr; + cast_node.params()[1] = src_int_expr; }, } cast_node.rparen_token = try appendToken(rp.c, .RParen, ")"); @@ -2119,7 +2139,7 @@ fn transCCast( return &cast_node.base; } - if (dst_type.getCanonicalType().getTypeClass() == .Enum) { + if (cIsEnum(dst_type)) { const builtin_node = try rp.c.createBuiltinCall("@intToEnum", 2); builtin_node.params()[0] = try transQualType(rp, dst_type, loc); _ = try appendToken(rp.c, .Comma, ","); @@ -2127,13 +2147,8 @@ fn transCCast( builtin_node.rparen_token = try appendToken(rp.c, .RParen, ")"); return &builtin_node.base; } - if (src_type.getCanonicalType().getTypeClass() == .Enum and - dst_type.getCanonicalType().getTypeClass() != .Enum) - { - const builtin_node = try rp.c.createBuiltinCall("@enumToInt", 1); - builtin_node.params()[0] = expr; - builtin_node.rparen_token = try appendToken(rp.c, .RParen, ")"); - return &builtin_node.base; + if (cIsEnum(src_type) and !cIsEnum(dst_type)) { + return transEnumToInt(rp.c, expr); } const cast_node = try rp.c.createBuiltinCall("@as", 2); cast_node.params()[0] = try transQualType(rp, dst_type, loc); @@ -2143,6 +2158,13 @@ fn transCCast( return &cast_node.base; } +fn transEnumToInt(c: *Context, enum_expr: *ast.Node) TypeError!*ast.Node { + const builtin_node = try c.createBuiltinCall("@enumToInt", 1); + builtin_node.params()[0] = enum_expr; + builtin_node.rparen_token = try appendToken(c, .RParen, ")"); + return &builtin_node.base; +} + fn transExpr( rp: RestorePoint, scope: *Scope, diff --git a/test/run_translated_c.zig b/test/run_translated_c.zig index 35f15eed4c..eb0b7db50a 100644 --- a/test/run_translated_c.zig +++ b/test/run_translated_c.zig @@ -371,4 +371,117 @@ pub fn addCases(cases: *tests.RunTranslatedCContext) void { \\ return 0; \\} , ""); + + cases.add("assign enum to uint, no explicit cast", + \\#include + \\typedef enum { + \\ ENUM_0 = 0, + \\ ENUM_1 = 1, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_1; + \\ unsigned int x = val; + \\ if (x != 1) abort(); + \\ return 0; + \\} + , ""); + + cases.add("assign enum to int", + \\#include + \\typedef enum { + \\ ENUM_0 = 0, + \\ ENUM_1 = 1, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_1; + \\ int x = val; + \\ if (x != 1) abort(); + \\ return 0; + \\} + , ""); + + cases.add("cast enum to smaller uint", + \\#include + \\#include + \\typedef enum { + \\ ENUM_0 = 0, + \\ ENUM_257 = 257, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_257; + \\ uint8_t x = (uint8_t)val; + \\ if (x != (uint8_t)257) abort(); + \\ return 0; + \\} + , ""); + + cases.add("cast enum to smaller signed int", + \\#include + \\#include + \\typedef enum { + \\ ENUM_0 = 0, + \\ ENUM_384 = 384, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_384; + \\ int8_t x = (int8_t)val; + \\ if (x != (int8_t)384) abort(); + \\ return 0; + \\} + , ""); + + cases.add("cast negative enum to smaller signed int", + \\#include + \\#include + \\typedef enum { + \\ ENUM_MINUS_1 = -1, + \\ ENUM_384 = 384, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_MINUS_1; + \\ int8_t x = (int8_t)val; + \\ if (x != -1) abort(); + \\ return 0; + \\} + , ""); + + cases.add("cast negative enum to smaller unsigned int", + \\#include + \\#include + \\typedef enum { + \\ ENUM_MINUS_1 = -1, + \\ ENUM_384 = 384, + \\} my_enum_t; + \\ + \\int main() { + \\ my_enum_t val = ENUM_MINUS_1; + \\ uint8_t x = (uint8_t)val; + \\ if (x != (uint8_t)-1) abort(); + \\ return 0; + \\} + , ""); + + cases.add("implicit enum cast in boolean expression", + \\#include + \\enum Foo { + \\ FooA, + \\ FooB, + \\ FooC, + \\}; + \\int main() { + \\ int a = 0; + \\ float b = 0; + \\ void *c = 0; + \\ enum Foo d = FooA; + \\ if (a || d) abort(); + \\ if (d && b) abort(); + \\ if (c || d) abort(); + \\ return 0; + \\} + , ""); } diff --git a/test/translate_c.zig b/test/translate_c.zig index afe7d1d9f5..676ea89c6e 100644 --- a/test/translate_c.zig +++ b/test/translate_c.zig @@ -2000,9 +2000,9 @@ pub fn addCases(cases: *tests.TranslateCContext) void { \\ int h = (a || b); \\ int i = (b || c); \\ int j = (a || c); - \\ int k = (a || d); - \\ int l = (d && b); - \\ int m = (c || d); + \\ int k = (a || (int)d); + \\ int l = ((int)d && b); + \\ int m = (c || (unsigned int)d); \\ SomeTypedef td = 44; \\ int o = (td || b); \\ int p = (c && td); @@ -2027,9 +2027,9 @@ pub fn addCases(cases: *tests.TranslateCContext) void { \\ var h: c_int = @boolToInt(((a != 0) or (b != 0))); \\ var i: c_int = @boolToInt(((b != 0) or (c != null))); \\ var j: c_int = @boolToInt(((a != 0) or (c != null))); - \\ var k: c_int = @boolToInt(((a != 0) or (@enumToInt(d) != 0))); - \\ var l: c_int = @boolToInt(((@enumToInt(d) != 0) and (b != 0))); - \\ var m: c_int = @boolToInt(((c != null) or (@enumToInt(d) != 0))); + \\ var k: c_int = @boolToInt(((a != 0) or (@bitCast(c_int, @enumToInt(d)) != 0))); + \\ var l: c_int = @boolToInt(((@bitCast(c_int, @enumToInt(d)) != 0) and (b != 0))); + \\ var m: c_int = @boolToInt(((c != null) or (@bitCast(c_uint, @enumToInt(d)) != 0))); \\ var td: SomeTypedef = 44; \\ var o: c_int = @boolToInt(((td != 0) or (b != 0))); \\ var p: c_int = @boolToInt(((c != null) and (td != 0)));