kit

kit
git clone https://git.ryansepassi.com/git/kit.git
Log | Files | Refs | README

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:
Msrc/parse/parse.c | 51++++++++++++++++++++++++++++++++++++++-------------
Atest/parse/cases/6_5_13b_logical_or_in_if.c | 24++++++++++++++++++++++++
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; +}