commit 82ec9ebef078a59b33a21a88ab8ddbd476f7e038
parent 38dcb984f7b8380b2dfda9bc137e8c06f1a138ac
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Sun, 10 May 2026 18:56:04 -0700
parse: route &&/|| result through a temp slot so if-cond sees merged value
The short-circuit arms each did `cg_push_int` then met at a label, but
cg's value stack is linear-flow only — the two pushes ended up at
different abstract slots, and consumers saw only the second. `cg_branch_false`
constant-folds an IMM-1 cond to a no-op, so `if (a || b)` always entered
the body. Mirror the ternary lowering: allocate a frame slot, store from
each arm, load at the merge.
Surfaced by test-libc on macOS (`if (!msg || !*msg)`); add a test-parse
case covering the if-consumed shape across D/R/E/J.
Diffstat:
2 files changed, 62 insertions(+), 13 deletions(-)
diff --git a/src/parse/parse.c b/src/parse/parse.c
@@ -3044,26 +3044,50 @@ static void parse_bor(Parser* p) {
* a && b lowers to: a || b lowers to:
* <a>; jz Lfalse <a>; jnz Ltrue
* <b>; jz Lfalse <b>; jnz Ltrue
- * push 1; jmp Lend push 0; jmp Lend
- * Lfalse: push 0 Ltrue: push 1
- * Lend: Lend:
- */
+ * store 1 → tmp; jmp Lend store 0 → tmp; jmp Lend
+ * Lfalse: store 0 → tmp Ltrue: store 1 → tmp
+ * Lend: load tmp Lend: load tmp
+ *
+ * The result is routed through a frame slot for the same reason ternary
+ * is: cg's abstract value stack is linear-flow only, so a naive push
+ * from each arm leaves two operands at the merge instead of one. */
+static FrameSlot ll_tmp_slot(Parser* p, const Type* ty) {
+ FrameSlotDesc fsd;
+ memset(&fsd, 0, sizeof fsd);
+ fsd.type = ty;
+ fsd.size = abi_sizeof(p->abi, ty);
+ fsd.align = abi_alignof(p->abi, ty);
+ fsd.kind = FS_LOCAL;
+ fsd.flags = FSF_NONE;
+ return cg_local(p->cg, &fsd);
+}
+
+static void ll_store_const(Parser* p, FrameSlot tmp, const Type* ty, i64 v) {
+ cg_push_local_typed(p->cg, tmp, ty);
+ cg_push_int(p->cg, v, ty);
+ cg_store(p->cg);
+ cg_drop(p->cg);
+}
+
static void parse_land(Parser* p) {
parse_bor(p);
while (is_punct(&p->cur, P_AND)) {
CGLabel L_false = cg_label_new(p->cg);
CGLabel L_end = cg_label_new(p->cg);
+ const Type* result_ty = ty_int(p);
+ FrameSlot tmp = ll_tmp_slot(p, result_ty);
advance(p);
to_rvalue(p);
cg_branch_false(p->cg, L_false);
parse_bor(p);
to_rvalue(p);
cg_branch_false(p->cg, L_false);
- cg_push_int(p->cg, 1, ty_int(p));
+ ll_store_const(p, tmp, result_ty, 1);
cg_jump(p->cg, L_end);
cg_label_place(p->cg, L_false);
- cg_push_int(p->cg, 0, ty_int(p));
+ ll_store_const(p, tmp, result_ty, 0);
cg_label_place(p->cg, L_end);
+ cg_push_local_typed(p->cg, tmp, result_ty);
}
}
@@ -3072,17 +3096,20 @@ static void parse_lor(Parser* p) {
while (is_punct(&p->cur, P_OR)) {
CGLabel L_true = cg_label_new(p->cg);
CGLabel L_end = cg_label_new(p->cg);
+ const Type* result_ty = ty_int(p);
+ FrameSlot tmp = ll_tmp_slot(p, result_ty);
advance(p);
to_rvalue(p);
cg_branch_true(p->cg, L_true);
parse_land(p);
to_rvalue(p);
cg_branch_true(p->cg, L_true);
- cg_push_int(p->cg, 0, ty_int(p));
+ ll_store_const(p, tmp, result_ty, 0);
cg_jump(p->cg, L_end);
cg_label_place(p->cg, L_true);
- cg_push_int(p->cg, 1, ty_int(p));
+ ll_store_const(p, tmp, result_ty, 1);
cg_label_place(p->cg, L_end);
+ cg_push_local_typed(p->cg, tmp, result_ty);
}
}
@@ -3094,11 +3121,9 @@ static void parse_lor(Parser* p) {
* (matches the §6.5.15 corpus rows; full usual-conversions rules slot in
* with Phase 7).
*
- * Likewise `&&` / `||` produce a 0/1 int and we lower them with explicit
- * push/jump per branch, but since the result is a fresh constant in each
- * arm, no temp slot is needed. The ternary case is special because the
- * two arms can be arbitrary expressions whose computed values must
- * appear on the same physical register/slot at the merge. */
+ * `&&` / `||` use the same temp-slot merge pattern (see parse_land /
+ * parse_lor above); the ternary differs only in that its two arms are
+ * arbitrary expressions rather than the constant 0/1. */
static void parse_ternary(Parser* p) {
parse_lor(p);
if (!is_punct(&p->cur, '?')) return;
diff --git a/test/parse/cases/6_5_13b_logical_or_in_if.c b/test/parse/cases/6_5_13b_logical_or_in_if.c
@@ -0,0 +1,24 @@
+/* `||` and `&&` results must remain coherent when consumed by control
+ * flow. Earlier the merge of the short-circuit arms left two values on
+ * the cg abstract stack instead of one, so `if (a || b)` always entered
+ * the body and `if (a && b)` always skipped it, regardless of operands. */
+int test_main(void) {
+ int a = 0, b = 0;
+ if (a || b) return 1; /* 0 || 0 → false; must skip */
+ if (!(a || (b = 1))) return 2;
+ if (b != 1) return 3; /* short-circuit right side did run */
+
+ int c = 1, d = 0;
+ if (c && d) return 4; /* 1 && 0 → false; must skip */
+ if (!(c && (d = 7))) return 5;
+ if (d != 7) return 6;
+
+ /* Nested with `!` — the failing test/libc shape. */
+ const char *msg = "hi";
+ if (!msg || !*msg) return 7;
+
+ /* Constant-folded paths still work. */
+ if (!(1 || 0)) return 8;
+ if (0 && 1) return 9;
+ return 0;
+}