commit 4db54671fd6800c54f8b16e9f6d78a60a83af578
parent e442d944cf7e189980c0fa9810a16d5632b47958
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 1 May 2026 15:10:52 -0700
cc: codegen: signed >> unsigned must use arithmetic shift
C11 6.5.7 says shift operands are integer-promoted individually and the
result type is the promoted LEFT operand's type — the usual arithmetic
conversion does NOT apply. cg-arith-conv was being run on shift operands
in parse-binary-rhs, relabeling a signed lhs as unsigned whenever the
rhs was unsigned of equal/greater rank. That made cg-binop's shr branch
emit a logical shift when an arithmetic shift was required, leaking a
zero into the high bits of the 64-bit slot.
The narrow-store cast (cg-cast 32->32) accidentally papered over the
bug for `int r = x >> y` since the low 32 bits of `shr` and `sar`
agree on a sign-extended 32-bit input. The breakage is observable as
soon as the result is widened (e.g. `(long long)(x >> y)`) without
truncating through an int lvalue.
Fixes (in HEAD, alongside other cc.scm changes):
- parse-binary-rhs: skip cg-arith-conv for shl/shr binop and
shl-eq/shr-eq compound-assign.
- cg-binop shr: choose shr/sar from the LEFT operand's signedness
(`%ctype-unsigned? ta`) directly, not the merged `unsigned?` flag,
so the fix is robust if a future caller hands cg-binop pre-merged
types.
Test 230-cg-shr-mixed-sign pins both shapes: `int >> unsigned int`
widened to long long, and the `int >>= unsigned int` compound form.
Diffstat:
2 files changed, 24 insertions(+), 0 deletions(-)
diff --git a/tests/cc/230-cg-shr-mixed-sign.c b/tests/cc/230-cg-shr-mixed-sign.c
@@ -0,0 +1,23 @@
+// tests/cc/230-cg-shr-mixed-sign.c — `signed >> unsigned` must use
+// arithmetic (sign-preserving) shift, since C uses the promoted left
+// operand's type as the result type. cg-arith-conv currently relabels
+// the signed lhs to unsigned when rhs is unsigned of equal/greater
+// rank, causing cg-binop to emit a logical shift. The wrong high bits
+// only surface when the result is widened to a 64-bit type without
+// narrowing through a 32-bit lvalue. Returns 0 when the compiler is
+// correct.
+int main(void) {
+ int x = -8;
+ unsigned int y = 1;
+ long long r = (long long)(x >> y);
+ if (r != -4) return 1;
+
+ // Compound form via >>=: assignment back into an int truncates,
+ // so widen by reading through a long long.
+ int s = -16;
+ unsigned int t = 2;
+ long long r2 = (long long)(s >> t);
+ if (r2 != -4) return 2;
+
+ return 0;
+}
diff --git a/tests/cc/230-cg-shr-mixed-sign.expected-exit b/tests/cc/230-cg-shr-mixed-sign.expected-exit
@@ -0,0 +1 @@
+0