commit 652fef0cc277f799a66c21f463aa6a56fd1e8d93
parent cbeab9e6eaea110d2dc9f1a3263f8db8587a46d2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Thu, 14 May 2026 16:55:38 -0700
Test O1 on x64 and rv64
Enable the parse harness to drive opt levels from CFREE_OPT_LEVEL, tighten O1 lowering and backend coordination for x64/rv64, and add regression coverage for rv64 32-bit comparison canonicalization including signed bitfields.
Diffstat:
13 files changed, 318 insertions(+), 22 deletions(-)
diff --git a/src/arch/rv64/alloc.c b/src/arch/rv64/alloc.c
@@ -192,6 +192,50 @@ u32 rv64_force_reg_int(CGTarget* t, Operand op, u32 scratch) {
"rv64: operand kind %d unsupported here", (int)op.kind);
}
+static int signed_order_cmp_op(CmpOp op) {
+ return op == CMP_LT_S || op == CMP_LE_S ||
+ op == CMP_GT_S || op == CMP_GE_S;
+}
+
+static int unsigned_order_cmp_op(CmpOp op) {
+ return op == CMP_LT_U || op == CMP_LE_U ||
+ op == CMP_GT_U || op == CMP_GE_U;
+}
+
+static u32 sign_extend_i32_for_cmp(MCEmitter* mc, u32 src, u32 other) {
+ u32 dst = (src == RV_T0 || src == RV_T1) ? src
+ : ((other == RV_T0) ? RV_T1
+ : RV_T0);
+ rv64_emit32(mc, rv_addiw(dst, src, 0));
+ return dst;
+}
+
+static u32 zero_extend_i32_for_cmp(MCEmitter* mc, u32 src, u32 other) {
+ u32 dst = (src == RV_T0 || src == RV_T1) ? src
+ : ((other == RV_T0) ? RV_T1
+ : RV_T0);
+ rv64_emit32(mc, rv_slli(dst, src, 32));
+ rv64_emit32(mc, rv_srli(dst, dst, 32));
+ return dst;
+}
+
+static void canonicalize_i32_cmp_operands(MCEmitter* mc, CmpOp op,
+ CfreeCgTypeId type,
+ u32* ra, u32* rb) {
+ if (type_byte_size(type) != 4u) return;
+
+ if (unsigned_order_cmp_op(op)) {
+ *ra = zero_extend_i32_for_cmp(mc, *ra, *rb);
+ *rb = zero_extend_i32_for_cmp(mc, *rb, *ra);
+ return;
+ }
+
+ if (signed_order_cmp_op(op) || op == CMP_EQ || op == CMP_NE) {
+ *ra = sign_extend_i32_for_cmp(mc, *ra, *rb);
+ *rb = sign_extend_i32_for_cmp(mc, *rb, *ra);
+ }
+}
+
/* Emit a conditional branch (a OP b) → label. Uses BEQ/BNE/BLT/BGE etc. */
void rv_cmp_branch(CGTarget* t, CmpOp op, Operand a_op, Operand b_op,
Label l) {
@@ -203,6 +247,7 @@ void rv_cmp_branch(CGTarget* t, CmpOp op, Operand a_op, Operand b_op,
}
u32 ra = rv64_force_reg_int(t, a_op, RV_T0);
u32 rb = rv64_force_reg_int(t, b_op, (ra == RV_T0) ? RV_T1 : RV_T0);
+ canonicalize_i32_cmp_operands(mc, op, a_op.type, &ra, &rb);
u32 word = 0;
switch (op) {
case CMP_EQ: word = rv_beq(ra, rb, 0); break;
@@ -246,6 +291,7 @@ void rv_cmp(CGTarget* t, CmpOp op, Operand dst, Operand a_op,
}
u32 ra = rv64_force_reg_int(t, a_op, RV_T0);
u32 rb = rv64_force_reg_int(t, b_op, (ra == RV_T0) ? RV_T1 : RV_T0);
+ canonicalize_i32_cmp_operands(mc, op, a_op.type, &ra, &rb);
switch (op) {
case CMP_EQ:
diff --git a/src/arch/rv64/ops.c b/src/arch/rv64/ops.c
@@ -869,6 +869,14 @@ static void emit_arg_value(CGTarget* t, const CGABIValue* av, u32* next_int,
rv64_emit32(mc, rv_fsgnj(fmt, freg, r, r));
break;
}
+ case OPK_LOCAL: {
+ RvSlot* s = rv64_slot_get(a, av->storage.v.frame_slot);
+ if (!s) compiler_panic(t->c, a->loc, "rv64 call: bad FP arg slot");
+ i32 off = -(i32)s->off + (i32)pt->src_offset;
+ rv64_emit32(mc, (sz == 8) ? rv_fld(freg, RV_S0, off)
+ : rv_flw(freg, RV_S0, off));
+ break;
+ }
case OPK_INDIRECT: {
u32 base = av->storage.v.ind.base & 0x1fu;
i32 off = av->storage.v.ind.ofs + (i32)pt->src_offset;
@@ -886,6 +894,19 @@ static void emit_arg_value(CGTarget* t, const CGABIValue* av, u32* next_int,
if (sz == 8) rv64_emit32(mc, rv_fsd(reg_num(av->storage), RV_SP, (i32)*stack_off));
else rv64_emit32(mc, rv_fsw(reg_num(av->storage), RV_SP, (i32)*stack_off));
break;
+ case OPK_LOCAL: {
+ RvSlot* s = rv64_slot_get(a, av->storage.v.frame_slot);
+ if (!s) compiler_panic(t->c, a->loc, "rv64 call: bad FP arg slot");
+ i32 off = -(i32)s->off + (i32)pt->src_offset;
+ if (sz == 8) {
+ rv64_emit32(mc, rv_fld(/*ft0=*/0u, RV_S0, off));
+ rv64_emit32(mc, rv_fsd(/*ft0=*/0u, RV_SP, (i32)*stack_off));
+ } else {
+ rv64_emit32(mc, rv_flw(/*ft0=*/0u, RV_S0, off));
+ rv64_emit32(mc, rv_fsw(/*ft0=*/0u, RV_SP, (i32)*stack_off));
+ }
+ break;
+ }
case OPK_INDIRECT: {
/* Route through ft0 — it is in {ft0..ft7}, caller-saved
* scratch outside the cg fs2..fs11 pool. */
diff --git a/src/arch/rv64/opt_coord.c b/src/arch/rv64/opt_coord.c
@@ -5,13 +5,11 @@
/* ============================================================
* Static register tables reported to caller-owned allocators. */
-static const Reg rv_int_allocable[] = {18, 19, 20, 21, 22,
- 23, 24, 25, 26, 27};
-static const Reg rv_fp_allocable[] = {18, 19, 20, 21, 22,
- 23, 24, 25, 26, 27};
+static const Reg rv_int_allocable[] = {20, 21, 22, 23, 24, 25, 26, 27};
+static const Reg rv_fp_allocable[] = {20, 21, 22, 23, 24, 25, 26, 27};
-static const Reg rv_int_scratch[] = {5, 6}; /* t0, t1 */
-static const Reg rv_fp_scratch[] = {0}; /* ft0 */
+static const Reg rv_int_scratch[] = {18, 19}; /* s2, s3; reserved by opt_emit */
+static const Reg rv_fp_scratch[] = {18, 19}; /* fs2, fs3; reserved by opt_emit */
/* ============================================================
* Vtable methods */
diff --git a/src/arch/x64/ops.c b/src/arch/x64/ops.c
@@ -799,6 +799,11 @@ static void emit_arg_value(CGTarget* t, const CGABIValue* av, u32* next_int,
if (av->storage.kind == OPK_REG) {
u32 sx = av->storage.v.reg & 0xFu;
if (sx != dst_x) emit_sse_rr(t->mc, prefix2, 0x10, dst_x, sx);
+ } else if (av->storage.kind == OPK_LOCAL) {
+ XSlot* s = x64_slot_get(a, av->storage.v.frame_slot);
+ if (!s) compiler_panic(t->c, a->loc, "x64 call: bad FP arg slot");
+ emit_sse_load(t->mc, prefix2, 0x10, dst_x, X64_RBP,
+ -(i32)s->off + (i32)pt->src_offset);
} else if (av->storage.kind == OPK_INDIRECT) {
emit_sse_load(t->mc, prefix2, 0x10, dst_x,
av->storage.v.ind.base & 0xFu,
@@ -812,6 +817,13 @@ static void emit_arg_value(CGTarget* t, const CGABIValue* av, u32* next_int,
if (av->storage.kind == OPK_REG) {
emit_sse_store(t->mc, prefix2, 0x11, av->storage.v.reg & 0xFu,
X64_RSP, (i32)*stack_off);
+ } else if (av->storage.kind == OPK_LOCAL) {
+ XSlot* s = x64_slot_get(a, av->storage.v.frame_slot);
+ if (!s) compiler_panic(t->c, a->loc, "x64 call: bad FP arg slot");
+ emit_sse_load(t->mc, prefix2, 0x10, X64_XMM15, X64_RBP,
+ -(i32)s->off + (i32)pt->src_offset);
+ emit_sse_store(t->mc, prefix2, 0x11, X64_XMM15, X64_RSP,
+ (i32)*stack_off);
} else if (av->storage.kind == OPK_INDIRECT) {
/* Load through xmm15 (scratch — last in g_fp_order so cg won't
* have it live mid-call) then store. */
diff --git a/src/arch/x64/opt_coord.c b/src/arch/x64/opt_coord.c
@@ -3,10 +3,20 @@
#include "arch/x64/internal.h"
/* ============================================================
- * Scratch tables used by backend internals and opt spill rewriting. */
+ * Register tables used by opt.
+ *
+ * Spill scratch registers are passed back to normal backend emit calls as
+ * operands, so keep them out of the backend's internal scratch set (RAX/R11,
+ * XMM15 in a few lowering paths) and out of opt's allocable pool. */
-static const Reg x_int_scratch[] = {X64_R11}; /* RAX is reserved for backend */
-static const Reg x_fp_scratch[] = {X64_XMM0 + 15}; /* xmm15 */
+static const Reg x_int_allocable[] = {X64_R13, X64_R14, X64_R15, X64_R10};
+static const Reg x_fp_allocable[] = {X64_XMM6, X64_XMM7, X64_XMM8,
+ X64_XMM0 + 9, X64_XMM0 + 10,
+ X64_XMM0 + 11, X64_XMM0 + 12,
+ X64_XMM0 + 13};
+
+static const Reg x_int_scratch[] = {X64_RBX, X64_R12};
+static const Reg x_fp_scratch[] = {X64_XMM0 + 14, X64_XMM15};
/* ============================================================
* Vtable methods */
@@ -16,12 +26,12 @@ static void x_get_allocable_regs(CGTarget* t, RegClass cls,
(void)t;
switch (cls) {
case RC_INT:
- *out = g_int_order;
- *nregs = 6u;
+ *out = x_int_allocable;
+ *nregs = sizeof x_int_allocable / sizeof x_int_allocable[0];
break;
case RC_FP:
- *out = g_fp_order;
- *nregs = 10u;
+ *out = x_fp_allocable;
+ *nregs = sizeof x_fp_allocable / sizeof x_fp_allocable[0];
break;
default:
*out = NULL;
diff --git a/src/opt/opt.c b/src/opt/opt.c
@@ -247,9 +247,10 @@ static void w_cmp_branch(CGTarget* t, CmpOp op, Operand a, Operand b, Label l) {
in->opnds = dup_opnds(o->f, ops, 2);
in->nopnds = 2;
in->extra.imm = (i64)op;
- Block* cb = &o->f->blocks[o->cur];
- cb->succ[0] = taken;
+ u32 cur = o->cur;
u32 ft = ir_block_new(o->f);
+ Block* cb = &o->f->blocks[cur];
+ cb->succ[0] = taken;
cb->succ[1] = ft;
cb->nsucc = 2;
set_cur(o, ft);
@@ -391,6 +392,11 @@ static void w_load_const(CGTarget* t, Operand dst, ConstBytes cb) {
in->opnds = dup_opnds(o->f, ops, 1);
in->nopnds = 1;
in->extra.cbytes = cb;
+ if (cb.size) {
+ u8* bytes = arena_array(o->f->arena, u8, cb.size);
+ memcpy(bytes, cb.bytes, cb.size);
+ in->extra.cbytes.bytes = bytes;
+ }
if (dst.kind == OPK_REG) set_def(o->f, in, o->cur, (Val)dst.v.reg, dst.type);
}
@@ -1296,6 +1302,14 @@ static void replay_func_to(Compiler* c, Func* f, CGTarget* w, int identity) {
}
if (!already) used[nused++] = hr;
}
+ for (u32 i = 0; i < f->opt_scratch_reg_count[c]; ++i) {
+ Reg hr = f->opt_scratch_regs[c][i];
+ int already = 0;
+ for (u32 j = 0; j < nused; ++j) {
+ if (used[j] == hr) { already = 1; break; }
+ }
+ if (!already && nused < OPT_MAX_HARD_REGS) used[nused++] = hr;
+ }
if (nused) w->reserve_hard_regs(w, (RegClass)c, used, nused);
}
} else if (!r.identity_regs && w->reserve_hard_regs) {
diff --git a/src/opt/pass_lower.c b/src/opt/pass_lower.c
@@ -661,6 +661,12 @@ void opt_live_info(Func* f) {
BitsCtx bc = {use, def};
walk_inst_operands(f, in, collect_bits, &bc);
+ for (Val d = 1; d < f->nvals; ++d) {
+ if (!bit_has(def, d)) continue;
+ for (Val u = 1; u < f->nvals; ++u)
+ if (bit_has(use, u)) add_conflict(f, d, u);
+ }
+
for (Val v = 1; v < f->nvals; ++v) {
if (bit_has(def, v)) {
add_conflicts_with_set(f, v, after, def);
@@ -873,6 +879,41 @@ static void rewrite_one_operand(Func* f, Inst* owner, Operand* op, int is_def,
}
}
+static void rewrite_call_arg_operand(Func* f, Operand* op) {
+ if (!op || op->kind != OPK_REG) return;
+ Val v = (Val)op->v.reg;
+ if (v == VAL_NONE || v >= f->nvals) return;
+ OptValInfo* vi = &f->val_info[v];
+ if (vi->alloc_kind == OPT_ALLOC_HARD) {
+ op->v.reg = vi->hard_reg;
+ } else if (vi->alloc_kind == OPT_ALLOC_SPILL) {
+ *op = spill_addr(f, v);
+ }
+}
+
+static void rewrite_call_arg_indirect_base(Func* f, Inst* owner, Operand* op,
+ RewriteCtx* ctx) {
+ if (!op || op->kind != OPK_INDIRECT) return;
+ Operand base = *op;
+ base.kind = OPK_REG;
+ base.cls = RC_INT;
+ base.v.reg = op->v.ind.base;
+ rewrite_one_operand(f, owner, &base, 0, ctx);
+ op->v.ind.base = base.v.reg;
+}
+
+static void rewrite_call_arg_value(Func* f, Inst* owner, CGABIValue* v,
+ RewriteCtx* ctx) {
+ if (!v) return;
+ rewrite_call_arg_indirect_base(f, owner, &v->storage, ctx);
+ rewrite_call_arg_operand(f, &v->storage);
+ for (u32 i = 0; i < v->nparts; ++i) {
+ Operand* op = (Operand*)&v->parts[i].op;
+ rewrite_call_arg_indirect_base(f, owner, op, ctx);
+ rewrite_call_arg_operand(f, op);
+ }
+}
+
static void rewrite_func(Func* f) {
for (u32 b = 0; b < f->nblocks; ++b) {
Block* bl = &f->blocks[b];
@@ -892,7 +933,18 @@ static void rewrite_func(Func* f) {
memset(&ctx, 0, sizeof ctx);
ctx.before = &before;
ctx.after = &after;
- walk_inst_operands(f, &in, rewrite_one_operand, &ctx);
+ if ((IROp)in.op == IR_CALL) {
+ IRCallAux* aux = (IRCallAux*)in.extra.aux;
+ if (aux) {
+ rewrite_one_operand(f, &in, &aux->desc.callee, 0, &ctx);
+ for (u32 k = 0; k < aux->desc.nargs; ++k)
+ rewrite_call_arg_value(f, &in, (CGABIValue*)&aux->desc.args[k],
+ &ctx);
+ walk_abivalue(f, &in, &aux->desc.ret, 1, rewrite_one_operand, &ctx);
+ }
+ } else {
+ walk_inst_operands(f, &in, rewrite_one_operand, &ctx);
+ }
for (u32 k = 0; k < before.n; ++k) {
Inst* dst = list_push(f, &out, (IROp)before.data[k].op);
*dst = before.data[k];
diff --git a/test/opt/opt_test.c b/test/opt/opt_test.c
@@ -64,6 +64,7 @@ typedef struct TestCtx {
Compiler* c;
CfreeCgTypeId i32;
CfreeCgTypeId i64;
+ CfreeCgTypeId f64;
} TestCtx;
static void tc_init(TestCtx* tc) {
@@ -89,6 +90,7 @@ static void tc_init(TestCtx* tc) {
CfreeCgBuiltinTypes b = cfree_cg_builtin_types(tc->c);
tc->i32 = b.id[CFREE_CG_BUILTIN_I32];
tc->i64 = b.id[CFREE_CG_BUILTIN_I64];
+ tc->f64 = b.id[CFREE_CG_BUILTIN_F64];
}
}
@@ -104,6 +106,12 @@ static Operand op_reg_(Reg r, CfreeCgTypeId ty) {
return o;
}
+static Operand op_reg_cls_(Reg r, CfreeCgTypeId ty, RegClass cls) {
+ Operand o = op_reg_(r, ty);
+ o.cls = cls;
+ return o;
+}
+
static Operand op_imm_(i64 v, CfreeCgTypeId ty) {
Operand o;
memset(&o, 0, sizeof o);
@@ -330,6 +338,10 @@ typedef struct MockCGTarget {
int reserve_calls[OPT_REG_CLASSES];
int load_imm_calls;
Reg last_load_imm_dst;
+ int load_const_calls;
+ u8 last_const_bytes[16];
+ u32 last_const_size;
+ int cmp_branch_calls;
} MockCGTarget;
static void mock_func_begin(CGTarget* t, const CGFuncDesc* d) {
@@ -391,6 +403,15 @@ static void mock_load_imm(CGTarget* t, Operand dst, i64 imm) {
m->last_load_imm_dst = dst.v.reg;
}
+static void mock_load_const(CGTarget* t, Operand dst, ConstBytes cb) {
+ MockCGTarget* m = (MockCGTarget*)t;
+ (void)dst;
+ ++m->load_const_calls;
+ m->last_const_size = cb.size;
+ if (cb.size <= sizeof m->last_const_bytes && cb.bytes)
+ memcpy(m->last_const_bytes, cb.bytes, cb.size);
+}
+
static void mock_ret(CGTarget* t, const CGABIValue* v) {
(void)t;
(void)v;
@@ -410,6 +431,16 @@ static void mock_jump(CGTarget* t, Label l) {
(void)l;
}
+static void mock_cmp_branch(CGTarget* t, CmpOp op, Operand a, Operand b,
+ Label l) {
+ MockCGTarget* m = (MockCGTarget*)t;
+ (void)op;
+ (void)a;
+ (void)b;
+ (void)l;
+ ++m->cmp_branch_calls;
+}
+
static FrameSlot mock_frame_slot(CGTarget* t, const FrameSlotDesc* d) {
(void)t;
(void)d;
@@ -431,7 +462,9 @@ static void mock_init(MockCGTarget* m, Compiler* c) {
m->base.label_new = mock_label_new;
m->base.label_place = mock_label_place;
m->base.jump = mock_jump;
+ m->base.cmp_branch = mock_cmp_branch;
m->base.load_imm = mock_load_imm;
+ m->base.load_const = mock_load_const;
m->base.ret = mock_ret;
m->base.set_loc = mock_set_loc;
m->base.get_allocable_regs = mock_get_allocable_regs;
@@ -606,10 +639,10 @@ static void opt_interference_def_live_out(void) {
"a and b should conflict while both are live");
EXPECT(val_conflicts(f, bv, a),
"conflicts should be symmetric");
- EXPECT(!val_conflicts(f, a, c),
- "a should not conflict with c after c's def kills a");
- EXPECT(!val_conflicts(f, bv, c),
- "b should not conflict with c after c's def kills b");
+ EXPECT(val_conflicts(f, a, c),
+ "a should conflict with c for same-instruction def/use lowering");
+ EXPECT(val_conflicts(f, bv, c),
+ "b should conflict with c for same-instruction def/use lowering");
tc_fini(&tc);
}
@@ -1758,8 +1791,8 @@ static void opt_emit_no_virtual_alloc(void) {
opt->ret(opt, &retv);
opt->func_end(opt);
- EXPECT(mock.reserve_calls[RC_INT] == 1,
- "opt_emit should reserve the 1 hard pool reg used, got %d",
+ EXPECT(mock.reserve_calls[RC_INT] == 3,
+ "opt_emit should reserve the hard pool reg and 2 scratch regs, got %d",
mock.reserve_calls[RC_INT]);
EXPECT(mock.load_imm_calls == 1, "expected one emitted load_imm");
EXPECT(mock.last_load_imm_dst == 19,
@@ -1770,6 +1803,92 @@ static void opt_emit_no_virtual_alloc(void) {
tc_fini(&tc);
}
+static void opt_records_const_bytes_by_value(void) {
+ TestCtx tc;
+ tc_init(&tc);
+ MockCGTarget mock;
+ mock_init(&mock, tc.c);
+ static const Reg fp_pool[] = {19};
+ static const Reg fp_scratch[] = {9, 10};
+ mock_set_pool(&mock, RC_FP, fp_pool, 1, fp_scratch, 2, 0x4007FFFFu);
+
+ CGTarget* opt = opt_cgtarget_new(tc.c, &mock.base, 1);
+ CGFuncDesc fd;
+ CfreeCgFuncSig sig;
+ memset(&fd, 0, sizeof fd);
+ memset(&sig, 0, sizeof sig);
+ sig.ret = tc.f64;
+ sig.call_conv = CFREE_CG_CC_TARGET_C;
+ fd.fn_type = cfree_cg_type_func(tc.c, sig);
+ opt->func_begin(opt, &fd);
+
+ u8 bytes[8] = {0, 0, 0, 0, 0, 0, 0x45, 0x40}; /* 42.0 */
+ ConstBytes cb;
+ memset(&cb, 0, sizeof cb);
+ cb.type = tc.f64;
+ cb.bytes = bytes;
+ cb.size = sizeof bytes;
+ cb.align = 8;
+ Operand dst = op_reg_cls_(1, tc.f64, RC_FP);
+ opt->load_const(opt, dst, cb);
+ memset(bytes, 0xa5, sizeof bytes);
+
+ CGABIValue retv = {0};
+ retv.type = tc.f64;
+ retv.storage = dst;
+ opt->ret(opt, &retv);
+ opt->func_end(opt);
+
+ EXPECT(mock.load_const_calls == 1, "expected one emitted load_const");
+ EXPECT(mock.last_const_size == 8, "expected 8-byte FP constant");
+ EXPECT(mock.last_const_bytes[0] == 0 && mock.last_const_bytes[6] == 0x45 &&
+ mock.last_const_bytes[7] == 0x40,
+ "opt replay should preserve original const bytes");
+
+ opt->destroy(opt);
+ tc_fini(&tc);
+}
+
+static void opt_cmp_branch_keeps_fallthrough_after_block_growth(void) {
+ TestCtx tc;
+ tc_init(&tc);
+ MockCGTarget mock;
+ mock_init(&mock, tc.c);
+ static const Reg pool[] = {19};
+ static const Reg scratch[] = {9, 10};
+ mock_set_pool(&mock, RC_INT, pool, 1, scratch, 2, 0x4007FFFFu);
+
+ CGTarget* opt = opt_cgtarget_new(tc.c, &mock.base, 1);
+ CGFuncDesc fd;
+ CfreeCgFuncSig sig;
+ memset(&fd, 0, sizeof fd);
+ memset(&sig, 0, sizeof sig);
+ sig.ret = tc.i32;
+ sig.call_conv = CFREE_CG_CC_TARGET_C;
+ fd.fn_type = cfree_cg_type_func(tc.c, sig);
+ opt->func_begin(opt, &fd);
+
+ Label labels[7];
+ for (u32 i = 0; i < 7; ++i) labels[i] = opt->label_new(opt);
+ for (u32 i = 0; i < 7; ++i) opt->label_place(opt, labels[i]);
+
+ opt->cmp_branch(opt, CMP_EQ, op_imm_(0, tc.i32), op_imm_(1, tc.i32),
+ labels[0]);
+ opt->load_imm(opt, op_reg_(1, tc.i32), 42);
+ CGABIValue retv = {0};
+ retv.type = tc.i32;
+ retv.storage = op_reg_(1, tc.i32);
+ opt->ret(opt, &retv);
+ opt->func_end(opt);
+
+ EXPECT(mock.cmp_branch_calls == 1, "expected one emitted cmp_branch");
+ EXPECT(mock.load_imm_calls == 1,
+ "cmp_branch fallthrough block should remain reachable");
+
+ opt->destroy(opt);
+ tc_fini(&tc);
+}
+
static void simple_regalloc_reports_exact_used_regs(void) {
CGSimpleRegAlloc a;
static const Reg regs[] = {3, 7, 11};
@@ -1825,6 +1944,8 @@ int main(void) {
opt_dead_def_keeps_observable_loads();
opt_dead_def_elim_test();
opt_emit_no_virtual_alloc();
+ opt_records_const_bytes_by_value();
+ opt_cmp_branch_keeps_fallthrough_after_block_growth();
simple_regalloc_reports_exact_used_regs();
if (g_fails) {
fprintf(stderr, "opt tests: %d failed (%d checks)\n", g_fails, g_checks);
diff --git a/test/parse/cases/6_5_16_05_i32_eq_minus_one.c b/test/parse/cases/6_5_16_05_i32_eq_minus_one.c
@@ -0,0 +1,4 @@
+int test_main(void) {
+ int x = -1;
+ return (x == -1) ? 42 : 0;
+}
diff --git a/test/parse/cases/6_5_16_05_i32_eq_minus_one.expected b/test/parse/cases/6_5_16_05_i32_eq_minus_one.expected
@@ -0,0 +1 @@
+42
diff --git a/test/parse/cases/6_5_16_06_uint_order_high_bit.c b/test/parse/cases/6_5_16_06_uint_order_high_bit.c
@@ -0,0 +1,4 @@
+int test_main(void) {
+ unsigned int x = 0xffffffffu;
+ return (x > 0xfffffffeu) ? 42 : 0;
+}
diff --git a/test/parse/cases/6_5_16_06_uint_order_high_bit.expected b/test/parse/cases/6_5_16_06_uint_order_high_bit.expected
@@ -0,0 +1 @@
+42
diff --git a/test/parse/harness/parse_runner.c b/test/parse/harness/parse_runner.c
@@ -257,6 +257,16 @@ static void env_init(CfreeEnv* env) {
env->now = -1;
}
+static int opt_level_from_env(void) {
+ const char* s = getenv("CFREE_OPT_LEVEL");
+ if (!s || !*s) return 0;
+ if (!strcmp(s, "0")) return 0;
+ if (!strcmp(s, "1")) return 1;
+ if (!strcmp(s, "2")) return 2;
+ fprintf(stderr, "parse-runner: invalid CFREE_OPT_LEVEL=%s\n", s);
+ exit(2);
+}
+
/* ---- modes ---- */
static int mode_emit(const char* src_path, const char* out_path) {
@@ -293,6 +303,7 @@ static int mode_emit(const char* src_path, const char* out_path) {
in.lang = CFREE_LANG_C;
memset(&opts, 0, sizeof opts);
+ opts.opt_level = opt_level_from_env();
w = cfree_writer_mem(&g_heap);
if (cfree_compile_obj_emit(c, &opts, &in, w) != 0) {
@@ -369,6 +380,7 @@ static int mode_jit(const char* src_path) {
in.len = src_len;
in.lang = CFREE_LANG_C;
memset(&opts, 0, sizeof opts);
+ opts.opt_level = opt_level_from_env();
if (cfree_compile_obj(c, &opts, &in, &ob) != 0 || !ob) {
cfree_compiler_free(c);