commit 796293a771df0878c28f90f91e19fca054054552
parent 5098244ac67063d5fd9879937551d40974fa6137
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Fri, 12 Jun 2026 10:23:01 -0700
fix(c): aggregate-typed conditional with mismatched qualifiers
A struct-typed ?: whose arms differ only in qualification (e.g. a const
struct lvalue '*top' from a const T* vs a plain struct subobject
's->stop.regs') reconciled the arms with pcg_convert, which had no
aggregate case and fell into the scalar bitcast path — pushing the
aggregate as a value. The CG rejects that ('aggregate must be a place');
on aarch64 it surfaced as 'scalar too large'. The trigger is exactly
src/dbg/symbolic.c's 'start = top ? *top : s->stop.regs'.
C has no struct/union conversion, so guard pcg_convert: an aggregate on
the stack only reconciles a qualifier difference — retag the place and
emit nothing.
Diffstat:
3 files changed, 56 insertions(+), 0 deletions(-)
diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c
@@ -846,6 +846,18 @@ void pcg_convert(Parser* p, const Type* dst) {
KitCgTypeId id = pcg_tid(p, dst);
int emit = pcg_emit_enabled(p);
if (src == dst) return;
+ /* C has no struct/union conversion: a pcg_convert reaching here with an
+ * aggregate on the stack is only reconciling a qualifier difference between
+ * two compatible types — e.g. the arms of a `?:` where one is a `const`
+ * struct lvalue (`*top`, top a `const T*`) and the other is not. The
+ * representation is identical, so retag the place to dst and emit nothing.
+ * Falling through would hand the aggregate to the scalar bitcast path, which
+ * pushes it as a value — illegal for an aggregate (api_push rejects it). The
+ * flag-preserving retag keeps it a place so the caller can copy it. */
+ if (src->kind == TY_STRUCT || src->kind == TY_UNION) {
+ pcg_retag_keep_flags(p, 0, dst);
+ return;
+ }
/* Conversion to _Bool is "value != 0", not a truncation: a value whose set
* bits all lie above the bool storage width (e.g. 256, or the sign bit of a
* 128-bit operand) must still become 1. Emit an explicit compare-against-zero
diff --git a/test/parse/cases/6_5_15_06_cond_struct_qualifier_mismatch.c b/test/parse/cases/6_5_15_06_cond_struct_qualifier_mismatch.c
@@ -0,0 +1,43 @@
+/* §6.5.15: when both operands of a conditional operator are structures, the
+ * result has their common, unqualified structure type. The arms may differ in
+ * qualification — e.g. a `const` struct lvalue reached through a `const T*`
+ * (`*cp`) versus a plain struct subobject (`s->n.f`).
+ *
+ * Regression: the parser reconciled the two arms with pcg_convert, which —
+ * having no aggregate case — fell into the scalar bitcast path and pushed the
+ * struct as a value. The CG rejects that ("aggregate must be a place, not a
+ * value"); on aarch64 it instead surfaced as "scalar too large". The trigger
+ * is exactly src/dbg/symbolic.c's `start = top ? *top : s->stop.regs`. */
+typedef struct {
+ int a, b;
+} P;
+struct N {
+ P f;
+};
+struct S {
+ struct N n;
+};
+
+/* const-deref arm vs nested-member arm (the symbolic.c shape). */
+static int pick(const P* cp, struct S* s, int useptr) {
+ P r = useptr ? *cp : s->n.f;
+ return r.a * 10 + r.b;
+}
+
+/* const-deref arm vs non-const-deref arm: same qualifier mismatch, both arms
+ * materialized. */
+static int pick2(const P* cp, P* np, int useconst) {
+ P r = useconst ? *cp : *np;
+ return r.a * 100 + r.b;
+}
+
+int test_main(void) {
+ P p = {3, 4};
+ P q = {7, 8};
+ struct S s = {{{5, 6}}};
+ int viaptr = pick(&p, &s, 1); /* *cp -> 34 */
+ int viamem = pick(&p, &s, 0); /* s->n.f -> 56 */
+ int c = pick2(&p, &q, 1); /* *cp -> 304 */
+ int n = pick2(&p, &q, 0); /* *np -> 708 */
+ return (viaptr == 34 && viamem == 56 && c == 304 && n == 708) ? 0 : 1;
+}
diff --git a/test/parse/cases/6_5_15_06_cond_struct_qualifier_mismatch.expected b/test/parse/cases/6_5_15_06_cond_struct_qualifier_mismatch.expected
@@ -0,0 +1 @@
+0