commit 9b07f8b452c18d654cb26c509ba4fc465504949a
parent 07644c78ebe68b366f0516109ee887de254b9c6e
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Wed, 20 May 2026 19:58:03 -0700
c-frontend: integer-promote switch controlling expression
C99 6.8.4.2 requires the integer promotions on the switch controlling
expression. Without that, a signed char selector landed in a byte-sized
slot and was reloaded via unsigned byte load — losing the sign bit and
mismatching against negative case constants. Manifested only at runtime
(D/E paths); R is a structural roundtrip and hid it.
6_8_31_switch_char_extremes exercises -128..127 and pins the fix.
Diffstat:
3 files changed, 37 insertions(+), 0 deletions(-)
diff --git a/lang/c/parse/parse_stmt.c b/lang/c/parse/parse_stmt.c
@@ -335,6 +335,19 @@ static void parse_switch_stmt(Parser* p) {
vty = cg_top_type(p->cg);
if (!vty) vty = type_prim(p->pool, TY_INT);
if (!type_is_int(vty)) perr(p, "switch expression requires integer type");
+ /* C99 6.8.4.2: the integer promotions are performed on the controlling
+ * expression. Without this, a `signed char` selector is stored to a
+ * byte-sized slot and reloaded with an unsigned-byte load, dropping the
+ * sign bit and miscomparing against negative case constants. */
+ {
+ const Type* prom = type_unqual(p->pool, vty);
+ if (prom && prom->kind == TY_ENUM) prom = type_prim(p->pool, TY_INT);
+ else prom = type_promoted(p->pool, prom);
+ if (prom && prom != vty) {
+ cg_convert(p->cg, prom);
+ vty = prom;
+ }
+ }
expect_punct(p, ')', "')' after switch expression");
memset(&ctx, 0, sizeof ctx);
diff --git a/test/parse/cases/6_8_31_switch_char_extremes.c b/test/parse/cases/6_8_31_switch_char_extremes.c
@@ -0,0 +1,23 @@
+/* signed char selector at the extremes of i8 range. A jump-table
+ * lowering that pre-promotes the selector to a wider type without
+ * preserving signedness would silently miscompile case INT8_MIN. */
+static int pick(signed char c) {
+ switch (c) {
+ case -128: return 1;
+ case -127: return 2;
+ case 0: return 3;
+ case 126: return 4;
+ case 127: return 5;
+ default: return 9;
+ }
+}
+
+int test_main(void) {
+ int s = 0;
+ s += pick((signed char)-128);
+ s += pick((signed char)-127);
+ s += pick((signed char)0);
+ s += pick((signed char)127);
+ s += pick((signed char)42); /* miss -> 9 */
+ return s - 15; /* 1+2+3+5+9 - 15 = 5 */
+}
diff --git a/test/parse/cases/6_8_31_switch_char_extremes.expected b/test/parse/cases/6_8_31_switch_char_extremes.expected
@@ -0,0 +1 @@
+5