From 0d7c4d105c7310b09201799bdb0141bc148f3c03 Mon Sep 17 00:00:00 2001 From: Richard Wheeler Date: Tue, 3 Mar 2026 11:20:45 -0500 Subject: [PATCH] cgen: use unsigned int for enum bit fields or unspecified size in @[minify] structs (#26678) * cgen: use unsigned int for enum bit fields of unspecified size in @[minify] structs In C, the signedness of a bit field declared with an enum type is implementation-defined. Clang on Windows x64 treats it as signed, so enum values >= 64 stored in a 7-bit field become negative, corrupting token kinds, type kinds, operators, etc. TCC and GCC default to unsigned, masking the bug. Fix: when emitting a bit-field for an enum field in a @[minify] struct, use `unsigned int` as the declared type unless the enum has an explicit unsigned base type (e.g. `as u8`), in which case the typedef is already unsigned and we keep it to preserve compact layout. Also add a regression test with a 65-value enum (forces a 7-bit field) to confirm values >= 64 are not corrupted, and update the existing minify_option_field must_have file to match the new output. Fixes #26658 Co-Authored-By: Claude Sonnet 4.6 * ast, token: restore @[minify] on structs removed as workaround for #26658 PR #26661 removed @[minify] from ~50 structs across ast.v, attr.v, types.v, and token.v as a workaround for enum bit-field signedness corruption with Clang. PR #26678 fixes the root cause by emitting `unsigned int` for default-typed enum bit fields, making the workaround unnecessary. Restore all removed annotations. Co-Authored-By: Claude Sonnet 4.6 --------- Co-authored-by: Richard Wheeler <18647491+PythonWillRule@users.noreply.github.com> Co-authored-by: Claude Sonnet 4.6 --- vlib/v/ast/ast.v | 45 ++++++++++ vlib/v/ast/attr.v | 1 + vlib/v/ast/types.v | 7 ++ vlib/v/gen/c/struct.v | 19 +++- ...enum_unsigned_not_msvc_windows.c.must_have | 1 + .../minify_enum_unsigned_not_msvc_windows.out | 2 + .../minify_enum_unsigned_not_msvc_windows.vv | 86 +++++++++++++++++++ vlib/v/token/token.v | 1 + 8 files changed, 161 insertions(+), 1 deletion(-) create mode 100644 vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.c.must_have create mode 100644 vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.out create mode 100644 vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.vv diff --git a/vlib/v/ast/ast.v b/vlib/v/ast/ast.v index 8f5956fb3..a7f1217ab 100644 --- a/vlib/v/ast/ast.v +++ b/vlib/v/ast/ast.v @@ -250,6 +250,7 @@ pub: pos token.Pos } +@[minify] pub struct StringLiteral { pub: val string @@ -301,6 +302,7 @@ pub enum GenericKindField { } // `foo.bar` +@[minify] pub struct SelectorExpr { pub: pos token.Pos @@ -351,6 +353,7 @@ pub: pos token.Pos } +@[minify] pub struct StructField { pub: pos token.Pos @@ -413,6 +416,7 @@ pub mut: } // const declaration +@[minify] pub struct ConstDecl { pub: is_pub bool @@ -424,6 +428,7 @@ pub mut: is_block bool // const() block } +@[minify] pub struct StructDecl { pub: pos token.Pos @@ -467,6 +472,7 @@ pub: comments []Comment } +@[minify] pub struct InterfaceDecl { pub: name string @@ -510,6 +516,7 @@ pub mut: // ...a // field1: 'hello' // }` +@[minify] pub struct StructInit { pub: pos token.Pos @@ -574,6 +581,7 @@ pub mut: } // function or method declaration +@[minify] pub struct FnDecl { pub: name string // 'math.bits.normalize' @@ -667,6 +675,7 @@ pub fn (f &FnDecl) new_method_with_receiver_type(new_type_ Type) FnDecl { } } +@[minify] pub struct FnTrace { pub mut: name string @@ -678,6 +687,7 @@ pub: is_fn_var bool } +@[minify] pub struct Fn { pub: is_variadic bool @@ -729,6 +739,7 @@ fn (f &Fn) method_equals(o &Fn) bool { && f.name == o.name } +@[minify] pub struct Param { pub: pos token.Pos @@ -794,6 +805,7 @@ fn (p []Param) equals(o []Param) bool { } // break, continue +@[minify] pub struct BranchStmt { pub: kind token.Kind @@ -869,6 +881,7 @@ pub enum CallKind { } // function or method call expr +@[minify] pub struct CallExpr { pub: pos token.Pos @@ -926,6 +939,7 @@ pub struct AutofreeArgVar { */ // function call argument: `f(callarg)` +@[minify] pub struct CallArg { pub: is_mut bool @@ -963,6 +977,7 @@ pub enum ComptimeVarKind { aggregate // aggregate var } +@[minify] pub struct Var { pub: name string @@ -1001,6 +1016,7 @@ pub mut: // used for smartcasting only // struct fields change type in scopes +@[minify] pub struct ScopeStructField { pub: struct_type Type // type of struct @@ -1015,6 +1031,7 @@ pub: // 12 <- the current casted type (typ) } +@[minify] pub struct GlobalField { pub: name string @@ -1049,6 +1066,7 @@ pub mut: end_comments []Comment } +@[minify] pub struct EmbeddedFile { pub: compression_type string @@ -1136,6 +1154,7 @@ pub mut: // TODO: (joe) remove completely, use ident.obj // instead which points to the scope object +@[minify] pub struct IdentVar { pub mut: typ Type @@ -1158,6 +1177,7 @@ pub enum IdentKind { } // A single identifier +@[minify] pub struct Ident { pub: language Language @@ -1219,6 +1239,7 @@ pub fn (i &Ident) var_info() IdentVar { // left op right // See: token.Kind.is_infix +@[minify] pub struct InfixExpr { pub: op token.Kind @@ -1257,6 +1278,7 @@ pub mut: } // See: token.Kind.is_prefix +@[minify] pub struct PrefixExpr { pub: op token.Kind @@ -1268,6 +1290,7 @@ pub mut: is_option bool // IfGuard } +@[minify] pub struct IndexExpr { pub: pos token.Pos @@ -1286,6 +1309,7 @@ pub mut: typ Type } +@[minify] pub struct IfExpr { pub: is_comptime bool @@ -1333,6 +1357,7 @@ pub mut: scope &Scope = unsafe { nil } } +@[minify] pub struct MatchExpr { pub: is_comptime bool @@ -1374,6 +1399,7 @@ pub mut: expected_type Type // for debugging only } +@[minify] pub struct SelectBranch { pub: pos token.Pos @@ -1421,6 +1447,7 @@ pub mut: scope &Scope = unsafe { nil } } +@[minify] pub struct ForInStmt { pub: key_var string @@ -1482,6 +1509,7 @@ pub mut: } // variable assign statement +@[minify] pub struct AssignStmt { pub: op token.Kind // include: =,:=,+=,-=,*=,/= and so on; for a list of all the assign operators, see vlib/token/token.v @@ -1538,6 +1566,7 @@ pub mut: } // enum declaration +@[minify] pub struct EnumDecl { pub: name string @@ -1607,6 +1636,7 @@ pub enum DeferMode { // TODO: handle this differently // v1 excludes non current os ifdefs so // the defer's never get added in the first place +@[minify] pub struct DeferStmt { pub: pos token.Pos @@ -1628,6 +1658,7 @@ pub mut: comments []Comment } +@[minify] pub struct GoExpr { pub: pos token.Pos @@ -1636,6 +1667,7 @@ pub mut: is_expr bool } +@[minify] pub struct SpawnExpr { pub: pos token.Pos @@ -1658,6 +1690,7 @@ pub: pos token.Pos } +@[minify] pub struct ArrayInit { pub: pos token.Pos // `[]` in []Type{} position @@ -1705,6 +1738,7 @@ pub mut: elem_type Type } +@[minify] pub struct MapInit { pub: pos token.Pos @@ -1724,6 +1758,7 @@ pub mut: } // s[10..20] +@[minify] pub struct RangeExpr { pub: has_high bool @@ -1736,6 +1771,7 @@ pub mut: typ Type // filled in by checker; the type of `0...1` is `int` for example, while `a`...`z` is `rune` etc } +@[minify] pub struct CastExpr { pub mut: arg Expr // `n` in `string(buf, n)` @@ -1747,6 +1783,7 @@ pub mut: pos token.Pos } +@[minify] pub struct AsmStmt { pub: arch pref.Arch @@ -1764,6 +1801,7 @@ pub mut: local_labels []string // local to the assembly block } +@[minify] pub struct AsmTemplate { pub mut: name string @@ -1951,6 +1989,7 @@ pub: } // `assert a == 0, 'a is zero'` +@[minify] pub struct AssertStmt { pub: pos token.Pos @@ -2007,6 +2046,7 @@ pub: */ // deprecated +@[minify] pub struct Assoc { pub: var_name string @@ -2038,6 +2078,7 @@ pub mut: typ Type } +@[minify] pub struct OffsetOf { pub: struct_type Type @@ -2068,6 +2109,7 @@ pub mut: expr Expr } +@[minify] pub struct TypeOf { pub: is_type bool @@ -2077,6 +2119,7 @@ pub mut: typ Type } +@[minify] pub struct DumpExpr { pub: pos token.Pos @@ -2111,6 +2154,7 @@ pub mut: val string } +@[minify] pub struct ComptimeSelector { pub: has_parens bool // if $() is used, for vfmt @@ -2139,6 +2183,7 @@ pub enum ComptimeCallKind { compile_error } +@[minify] pub struct ComptimeCall { pub: pos token.Pos diff --git a/vlib/v/ast/attr.v b/vlib/v/ast/attr.v index ca8075aa3..445651994 100644 --- a/vlib/v/ast/attr.v +++ b/vlib/v/ast/attr.v @@ -14,6 +14,7 @@ pub enum AttrKind { } // e.g. `@[unsafe]` +@[minify] pub struct Attr { pub: name string // [name] diff --git a/vlib/v/ast/types.v b/vlib/v/ast/types.v index baf9099b9..f356f9378 100644 --- a/vlib/v/ast/types.v +++ b/vlib/v/ast/types.v @@ -106,6 +106,7 @@ pub fn pref_arch_to_table_language(pref_arch pref.Arch) Language { // * Table.type_kind(typ) not TypeSymbol.kind. // Each TypeSymbol is entered into `Table.type_symbols`. // See also: Table.sym. +@[minify] pub struct TypeSymbol { pub mut: parent_idx int @@ -168,6 +169,7 @@ pub mut: func Fn } +@[minify] pub struct Struct { pub: attrs []Attr @@ -197,6 +199,7 @@ pub mut: concrete_types []Type // concrete types, e.g. [int, string] } +@[minify] pub struct Interface { pub mut: types []Type // all types that implement this interface @@ -225,6 +228,7 @@ pub: name_pos token.Pos } +@[minify] pub struct Alias { pub mut: parent_type Type @@ -249,6 +253,7 @@ pub mut: elem_type Type } +@[minify] pub struct ArrayFixed { pub: size int @@ -276,6 +281,7 @@ pub mut: name_pos token.Pos } +@[minify] pub struct SumType { pub mut: fields []StructField @@ -1730,6 +1736,7 @@ fn (t &Table) shorten_user_defined_typenames(original_name string, import_aliase return '${mod}.${typ}' } +@[minify] pub struct FnSignatureOpts { pub: skip_receiver bool diff --git a/vlib/v/gen/c/struct.v b/vlib/v/gen/c/struct.v index aa809f496..68a367fa1 100644 --- a/vlib/v/gen/c/struct.v +++ b/vlib/v/gen/c/struct.v @@ -703,6 +703,7 @@ fn (mut g Gen) struct_decl(s ast.Struct, name string, is_anon bool, is_option bo field_name := c_name(field.name) volatile_prefix := if field.is_volatile { 'volatile ' } else { '' } mut size_suffix := '' + mut is_enum_bitfield := false if is_minify && !g.is_cc_msvc && !g.pref.output_cross_c && !field.typ.has_flag(.option) && !field.typ.has_flag(.result) { if field.typ == ast.bool_type_idx { @@ -718,6 +719,7 @@ fn (mut g Gen) struct_decl(s ast.Struct, name string, is_anon bool, is_option bo l >>= 1 } size_suffix = ' : ${bits_needed}' + is_enum_bitfield = true } } } @@ -734,7 +736,22 @@ fn (mut g Gen) struct_decl(s ast.Struct, name string, is_anon bool, is_option bo } } if !field_is_anon { - g.type_definitions.writeln('\t${volatile_prefix}${type_name} ${field_name}${size_suffix};') + actual_type := if is_enum_bitfield { + enum_info := field_sym.info as ast.Enum + if enum_info.typ == ast.int_type { + // default enum base type (int): C bit-field signedness is + // implementation-defined; force unsigned to avoid Clang treating + // it as signed (which corrupts values >= 64 in a 7-bit field, etc.) + 'unsigned int' + } else { + // explicit base type (as u8, as i8, as i32, …): the typedef + // already carries the correct signedness, so respect the user's choice + type_name + } + } else { + type_name + } + g.type_definitions.writeln('\t${volatile_prefix}${actual_type} ${field_name}${size_suffix};') } } } else { diff --git a/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.c.must_have b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.c.must_have new file mode 100644 index 000000000..0b308aef7 --- /dev/null +++ b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.c.must_have @@ -0,0 +1 @@ +unsigned int kind : 7; diff --git a/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.out b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.out new file mode 100644 index 000000000..c6ce294ea --- /dev/null +++ b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.out @@ -0,0 +1,2 @@ +true +64 diff --git a/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.vv b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.vv new file mode 100644 index 000000000..814beaed6 --- /dev/null +++ b/vlib/v/gen/c/testdata/minify_enum_unsigned_not_msvc_windows.vv @@ -0,0 +1,86 @@ +// Test that enum bit fields in @[minify] structs use unsigned int, +// so large enum values (>= 64) are not corrupted. +module main + +// 65 values forces bits_needed = 7; value .a64 = 64 would overflow signed 7-bit +enum LargeEnum { + a0 + a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10 + a11 + a12 + a13 + a14 + a15 + a16 + a17 + a18 + a19 + a20 + a21 + a22 + a23 + a24 + a25 + a26 + a27 + a28 + a29 + a30 + a31 + a32 + a33 + a34 + a35 + a36 + a37 + a38 + a39 + a40 + a41 + a42 + a43 + a44 + a45 + a46 + a47 + a48 + a49 + a50 + a51 + a52 + a53 + a54 + a55 + a56 + a57 + a58 + a59 + a60 + a61 + a62 + a63 + a64 +} + +@[minify] +struct Status { + kind LargeEnum + done bool +} + +fn main() { + s := Status{ + kind: .a64 + } + println(s.kind == .a64) + println(int(s.kind)) +} diff --git a/vlib/v/token/token.v b/vlib/v/token/token.v index 1020f2d94..a7ae53c8b 100644 --- a/vlib/v/token/token.v +++ b/vlib/v/token/token.v @@ -5,6 +5,7 @@ module token const orm_custom_operators = ['like', 'ilike'] +@[minify] pub struct Token { pub: kind Kind // the token number/enum; for quick comparisons -- 2.39.5