commit f725b1162f0038013abb884ca73dd3b10a7ddb89
parent bab1f12ae5c8898d5b816a20dae3f067479e884c
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sun, 26 Apr 2026 21:50:39 -0700
cc/cg: signed narrowing sign-extends on re-widen (§A.4)
cg-cast's narrowing branch used to mask only, so (int)(char)-3 came
back as 253 instead of -3. Now it shli/sari's for signed narrow
targets (i8/i16/i32) — truncate-and-sign-extend in one step — so the
slot holds the canonical 64-bit form and the subsequent widening
relabel preserves the value. Unsigned targets still mask.
cc-cg/18-sext-narrow and cc-parse/18-sext-narrow lock the behavior in.
Diffstat:
6 files changed, 43 insertions(+), 5 deletions(-)
diff --git a/cc/cg.scm b/cc/cg.scm
@@ -462,8 +462,16 @@
((>= to-sz from-sz)
(cg-push cg (%opnd (opnd-kind p) to-type (opnd-ext p) (opnd-lval? p))))
(else
+ ;; Narrowing cast. Signed targets (i8/i16/i32) shli/sari to
+ ;; truncate-and-sign-extend in one step, so the slot holds the
+ ;; canonical 64-bit form and a subsequent widening cast (which
+ ;; is relabel-only) restores the value. Unsigned targets mask
+ ;; off high bits to zero-extend.
(%cg-load-opnd-into cg p 't0)
(cond
+ ((eq? to-kind 'i8) (%cg-emit-sext cg 't0 56))
+ ((eq? to-kind 'i16) (%cg-emit-sext cg 't0 48))
+ ((eq? to-kind 'i32) (%cg-emit-sext cg 't0 32))
((= to-sz 1) (%cg-emit-many cg (list "%andi(t0, t0, 255)\n")))
((= to-sz 2)
(%cg-emit-many cg (list "%li(t1, 65535)\n%and(t0, t0, t1)\n")))
diff --git a/docs/CC-PUNCHLIST.md b/docs/CC-PUNCHLIST.md
@@ -66,11 +66,13 @@ upstream of nearly everything else. Land this first.
indirect path now uses `t2` so multi-byte gathers don't alias
dest with base.
-- [ ] **Signed narrowing keeps sign on re-widen**
- - cg: `cc-cg/NN-sext-narrow.scm` — `(unsigned)(int)(char)-3` → exit 253.
- - parse: `cc-parse/NN-sext-narrow.c`
- - Needs: `cg-cast` emits sign-extend on the narrow path (or signed
- `%lds*` loads); `cg-promote` emits sext when source rank < int.
+- [x] **Signed narrowing keeps sign on re-widen**
+ - cg: `cc-cg/18-sext-narrow.scm` — `(int)(char)-3 == -3` → exit 1.
+ - parse: `cc-parse/18-sext-narrow.c`
+ - Done: `cg-cast`'s narrowing branch now `shli`/`sari`'s for signed
+ narrow targets (i8/i16/i32) instead of masking, so the slot holds
+ the canonical sign-extended 64-bit form. The widening cast back
+ (relabel-only) preserves it.
- [ ] **Unsigned narrowing zero-extends**
- cg: `cc-cg/NN-zext-narrow.scm` — `(unsigned)(unsigned char)-3` → 253.
diff --git a/tests/cc-cg/18-sext-narrow.expected-exit b/tests/cc-cg/18-sext-narrow.expected-exit
@@ -0,0 +1 @@
+1
diff --git a/tests/cc-cg/18-sext-narrow.scm b/tests/cc-cg/18-sext-narrow.scm
@@ -0,0 +1,20 @@
+;; tests/cc-cg/18-sext-narrow.scm — signed narrowing keeps sign on
+;; re-widen (§A.4 of docs/CC-PUNCHLIST.md).
+;;
+;; Models: ((int)(char)-3) == -3.
+;; Forces the cg-cast narrowing path to sign-encode the result so
+;; the subsequent widening cast restores -3, not 0xFD (253). The
+;; comparison is against -3 as i32, so a buggy cg that masks-only
+;; yields a0=253 vs a1=-3 → not equal → exit 0. Correct cg sign-
+;; extends → equal → exit 1.
+
+(let ((cg (cg-init)))
+ (cg-fn-begin cg "main" '() %t-i32)
+ (cg-push-imm cg %t-i32 -3)
+ (cg-cast cg %t-i8)
+ (cg-cast cg %t-i32)
+ (cg-push-imm cg %t-i32 -3)
+ (cg-binop cg 'eq)
+ (cg-return cg)
+ (cg-fn-end cg)
+ (write-bv-fd 1 (cg-finish cg)))
diff --git a/tests/cc-parse/18-sext-narrow.c b/tests/cc-parse/18-sext-narrow.c
@@ -0,0 +1,6 @@
+// tests/cc-parse/18-sext-narrow.c — signed narrowing keeps sign on
+// re-widen. §A.4 of docs/CC-PUNCHLIST.md.
+
+int main() {
+ return ((int)(char)-3) == -3;
+}
diff --git a/tests/cc-parse/18-sext-narrow.expected-exit b/tests/cc-parse/18-sext-narrow.expected-exit
@@ -0,0 +1 @@
+1