commit 9f6c53254dc71957f285663f22f230c6e50ff64a
parent c69c97e24cedb77bac58398c95fec87a0c06328b
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 12 Jun 2026 09:43:01 -0700
feat(c): support C23 enum with a fixed underlying type
Parse `enum [tag] : T` (C23 §6.7.2.2) in both the defining and forward-declaring
forms, adopting T as the enum's representation type (so sizeof matches). A bodyless
`enum E : T;` is recorded as an incomplete forward declaration that a later
`enum E : T { ... }` completes. Needed to compile modern SDK headers (e.g.
`typedef enum __enum_options : uint64_t { ... }`).
Diffstat:
3 files changed, 46 insertions(+), 2 deletions(-)
diff --git a/lang/c/parse/parse_type.c b/lang/c/parse/parse_type.c
@@ -1143,6 +1143,16 @@ const Type* parse_enum(Parser* p, Attr** anon_attrs_out) {
tag_name = p->cur.v.ident;
advance(p);
}
+ /* C23 §6.7.2.2: an optional fixed underlying type — `enum [tag] : T` —
+ * may follow the tag (or `enum`), in both the defining and the
+ * forward-declaring forms. T is an integer type-specifier list; we reuse
+ * the type-name parser and adopt T as the enum's representation type. */
+ const Type* underlying = ty_int(p);
+ int has_fixed_type = 0;
+ if (accept_punct(p, ':')) {
+ underlying = parse_type_name(p);
+ has_fixed_type = 1;
+ }
int has_body = is_punct(&p->cur, '{');
if (!has_body && tag_name == 0) {
perr(p, "expected tag name or '{' after enum");
@@ -1156,10 +1166,24 @@ const Type* parse_enum(Parser* p, Attr** anon_attrs_out) {
if (e) {
perr(p, "tag redeclared with wrong kind");
}
- perr(p, "enum tag declared without definition");
+ if (!has_fixed_type) {
+ perr(p, "enum tag declared without definition");
+ }
+ /* C23 `enum E : T;` with no enumerators is a forward declaration carrying a
+ * fixed underlying type. Record an *incomplete* tag (so the type knows T)
+ * that a later `enum E : T { ... }` can complete; a redefinition is only
+ * diagnosed once an enumerator body actually arrives. */
+ {
+ TagId ftid = type_tag_new(p->pool, TAG_ENUM, tag_name, tag_loc);
+ const Type* fet = type_enum(p->pool, ftid, tag_name, underlying);
+ TagEntry* te =
+ tag_define(p, tag_name, TAG_ENUM, (Type*)fet, /*complete=*/0);
+ attr_list_append(&te->attrs, rec_attrs);
+ return fet;
+ }
}
TagId tid = type_tag_new(p->pool, TAG_ENUM, tag_name, tag_loc);
- const Type* et = type_enum(p->pool, tid, tag_name, ty_int(p));
+ const Type* et = type_enum(p->pool, tid, tag_name, underlying);
expect_punct(p, '{', "'{'");
i64 next_val = 0;
for (;;) {
diff --git a/test/parse/cases/6_7_2_2_01_enum_fixed_underlying_type.c b/test/parse/cases/6_7_2_2_01_enum_fixed_underlying_type.c
@@ -0,0 +1,19 @@
+/* C23 §6.7.2.2: enum with a fixed underlying type — `enum E : T { ... }`.
+ * The underlying type sets the enum's representation (and thus sizeof); the
+ * enumerator values and a char-backed sizeof are checked. No sysroot needed
+ * (builtin integer types only), so this runs on every lane. */
+enum Color : unsigned char { RED = 1, GREEN = 2, BLUE = 39 };
+enum Wide : long long { LO = 40 };
+
+/* Forward `enum E : T;` is a complete type in C23 even with no body. */
+enum Fwd : int;
+enum Fwd : int { F0 = 7 };
+
+int test_main(void) {
+ enum Color c = BLUE; /* 39 */
+ enum Wide w = LO; /* 40 */
+ return (int)c /* 39 */
+ + (w == 40) /* + 1 */
+ + (sizeof(enum Color) == 1) /* + 1 (char-backed) */
+ + (F0 == 7) - (sizeof(enum Wide) != 8); /* + 1 - 0 */
+}
diff --git a/test/parse/cases/6_7_2_2_01_enum_fixed_underlying_type.expected b/test/parse/cases/6_7_2_2_01_enum_fixed_underlying_type.expected
@@ -0,0 +1 @@
+42