commit e55ca1f0335e253b0d5b7c61cf5a5e34c1cc6970
parent e51e9c845ab7a87b875bd188fe1b90bff39705cc
Author: Ryan Sepassi <rsepassi@gmail.com>
Date: Mon, 15 Jun 2026 09:06:33 -0700
Fold CG memory op constant offsets
Diffstat:
| M | src/cg/control.c | | | 108 | ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------- |
| M | test/api/cg_type_test.c | | | 143 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 226 insertions(+), 25 deletions(-)
diff --git a/src/cg/control.c b/src/cg/control.c
@@ -510,8 +510,7 @@ static void api_scope_reload_one(KitCg* g, CGLocal local, KitCgTypeId type) {
* bottom->top). The top of stack — the last result — is popped first. */
static void api_scope_pop_results(KitCg* g, ApiCgScope* s, ApiSValue* out) {
u32 k;
- for (k = 0; k < s->nresults; ++k)
- out[s->nresults - 1u - k] = api_pop(g);
+ for (k = 0; k < s->nresults; ++k) out[s->nresults - 1u - k] = api_pop(g);
}
/* Pop N results off the value stack and store each into its carry local. */
@@ -549,8 +548,9 @@ static void api_scope_reload_params(KitCg* g, ApiCgScope* s) {
}
/* Allocate a memory-resident carry local of the given type. `resolved` is the
- * canonicalized type id (for the local's type); `raw` is the as-supplied id used
- * for the ABI size/align query, matching the historical single-result path. */
+ * canonicalized type id (for the local's type); `raw` is the as-supplied id
+ * used for the ABI size/align query, matching the historical single-result
+ * path. */
static CGLocal api_scope_alloc_carry_local(KitCg* g, KitCgTypeId resolved,
KitCgTypeId raw) {
CGLocalDesc ld;
@@ -634,7 +634,8 @@ static KitCgScope api_scope_begin_sig_kind(KitCg* g, u8 kind,
if (s->generation == 0) s->generation = ++g->scope_generation;
s->active = 1;
if (!api_scope_setup_sig(g, s, sig)) {
- compiler_panic(g->c, g->cur_loc, "KitCg: out of memory for scope signature");
+ compiler_panic(g->c, g->cur_loc,
+ "KitCg: out of memory for scope signature");
return 0;
}
for (k = 0; k < s->nresults; ++k)
@@ -645,7 +646,8 @@ static KitCgScope api_scope_begin_sig_kind(KitCg* g, u8 kind,
/* Loop preheader: snapshot the params off the value stack into their carry
* locals BEFORE the loop header, so the pop runs once in the preheader and
* every back edge (store_params + jump cont) re-stores into the same locals.
- * Allocating a local emits nothing; the param stores belong before cont_lbl. */
+ * Allocating a local emits nothing; the param stores belong before cont_lbl.
+ */
if (kind == SCOPE_LOOP && s->nparams) {
for (k = 0; k < s->nparams; ++k)
s->param_locals[k] =
@@ -757,8 +759,8 @@ void kit_cg_scope_end_unreachable(KitCg* g, KitCgScope scope) {
/* Close a scope whose fall-through is unreachable (its body terminated via a
* branch/return). There are no fall-through results to pop; the carry locals
* already hold whatever the branch that exited stored. Mirrors scope_end but
- * skips the leading store_results. The caller must have the value stack at the
- * scope's base depth. */
+ * skips the leading store_results. The caller must have the value stack at
+ * the scope's base depth. */
ApiCgScope* s =
api_scope_from_handle(g, scope, 1, "KitCg: scope_end_unreachable");
if (!s) return;
@@ -799,9 +801,8 @@ static void api_break_cond(KitCg* g, KitCgScope scope, int break_when,
}
{
Heap* h = g->c->ctx->heap;
- ApiSValue* rs =
- (ApiSValue*)h->alloc(h, sizeof(ApiSValue) * s->nresults,
- _Alignof(ApiSValue));
+ ApiSValue* rs = (ApiSValue*)h->alloc(h, sizeof(ApiSValue) * s->nresults,
+ _Alignof(ApiSValue));
if (!rs) compiler_panic(g->c, g->cur_loc, "KitCg: out of memory");
api_scope_pop_results(g, s, rs);
if (cond.kind == SV_OPERAND && cond.op.kind == OPK_IMM) {
@@ -1059,6 +1060,14 @@ static int cg_scale_to_log2(u32 scale) {
}
}
+static int cg_checked_scaled_offset(i64 index, u32 scale, i64 offset,
+ i64* out) {
+ i64 product;
+ if (__builtin_mul_overflow(index, (i64)scale, &product)) return 0;
+ if (__builtin_add_overflow(product, offset, out)) return 0;
+ return 1;
+}
+
/* Shared core for kit_cg_elem / kit_cg_elem_scaled. `elem_size` is the index
* stride in bytes; 0 means "derive from the base pointee size" (the kit_cg_elem
* behavior). The place's access type is always the base pointee. */
@@ -1088,23 +1097,59 @@ static void api_cg_elem(KitCg* g, u32 elem_size, int64_t offset) {
elemsz = elem_size ? elem_size : (u32)abi_cg_sizeof(g->c->abi, elem_ty);
idx_ty = idx.type ? idx.type : idx.op.type;
if (!idx_ty) idx_ty = builtin_id(KIT_CG_BUILTIN_I64);
- base_op = api_force_local(g, &base, base_ptr_ty);
- base_local = base_op.v.local;
idx_op = api_force_local_unless_imm(g, &idx, idx_ty);
/* Constant index folds entirely into the displacement — no instructions, just
- * a larger offset on the OPK_INDIRECT. */
+ * a larger offset on the place. A symbol-address base can remain a GLOBAL
+ * place, folding the symbol addend with index*scale+offset instead of first
+ * materializing &sym into a temporary. */
if (idx_op.kind == OPK_IMM) {
- i64 ofs = idx_op.v.imm * (i64)elemsz + offset;
+ i64 ofs;
Operand place;
- if (ofs >= INT32_MIN && ofs <= INT32_MAX) {
- place = api_op_indirect(base_local, (i32)ofs, elem_ty);
+ if (cg_checked_scaled_offset(idx_op.v.imm, elemsz, offset, &ofs) &&
+ base.kind == SV_OPERAND && base.op.kind == OPK_GLOBAL &&
+ !__builtin_add_overflow(base.op.v.global.addend, ofs, &ofs)) {
+ place = api_op_global(base.op.v.global.sym, ofs, elem_ty);
} else {
- CGLocal r = api_alloc_temp_local(g, base_ptr_ty);
- Operand ro = api_op_local(r, base_ptr_ty);
- T->binop(T, BO_IADD, ro, api_op_local(base_local, base_ptr_ty),
- api_op_imm(ofs, base_ptr_ty));
- place = api_op_indirect(r, 0, elem_ty);
+ base_op = api_force_local(g, &base, base_ptr_ty);
+ base_local = base_op.v.local;
+ if (cg_checked_scaled_offset(idx_op.v.imm, elemsz, offset, &ofs) &&
+ ofs >= INT32_MIN && ofs <= INT32_MAX) {
+ place = api_op_indirect(base_local, (i32)ofs, elem_ty);
+ } else if (cg_checked_scaled_offset(idx_op.v.imm, elemsz, offset, &ofs)) {
+ CGLocal r = api_alloc_temp_local(g, base_ptr_ty);
+ Operand ro = api_op_local(r, base_ptr_ty);
+ T->binop(T, BO_IADD, ro, api_op_local(base_local, base_ptr_ty),
+ api_op_imm(ofs, base_ptr_ty));
+ place = api_op_indirect(r, 0, elem_ty);
+ } else {
+ CGLocal ir = api_alloc_temp_local(g, idx_ty);
+ Operand idx_local = api_op_local(ir, idx_ty);
+ int lg2 = cg_scale_to_log2(elemsz);
+ u8 log2_scale = 0;
+ i32 place_offset;
+ T->load_imm(T, idx_local, idx_op.v.imm);
+ if (lg2 >= 0) {
+ log2_scale = (u8)lg2;
+ } else {
+ CGLocal sr = api_alloc_temp_local(g, idx_ty);
+ T->binop(T, BO_IMUL, api_op_local(sr, idx_ty), idx_local,
+ api_op_imm((i64)elemsz, idx_ty));
+ ir = sr;
+ }
+ if (offset >= INT32_MIN && offset <= INT32_MAX) {
+ place_offset = (i32)offset;
+ } else {
+ CGLocal r = api_alloc_temp_local(g, base_ptr_ty);
+ Operand ro = api_op_local(r, base_ptr_ty);
+ T->binop(T, BO_IADD, ro, api_op_local(base_local, base_ptr_ty),
+ api_op_imm(offset, base_ptr_ty));
+ base_local = r;
+ place_offset = 0;
+ }
+ place = api_op_indirect_indexed(base_local, ir, log2_scale,
+ place_offset, elem_ty);
+ }
}
api_release(g, &base);
api_release(g, &idx);
@@ -1120,9 +1165,22 @@ static void api_cg_elem(KitCg* g, u32 elem_size, int64_t offset) {
int lg2 = cg_scale_to_log2(elemsz);
CGLocal ir;
u8 log2_scale;
+ i32 place_offset;
+ base_op = api_force_local(g, &base, base_ptr_ty);
+ base_local = base_op.v.local;
+ if (offset >= INT32_MIN && offset <= INT32_MAX) {
+ place_offset = (i32)offset;
+ } else {
+ CGLocal r = api_alloc_temp_local(g, base_ptr_ty);
+ Operand ro = api_op_local(r, base_ptr_ty);
+ T->binop(T, BO_IADD, ro, api_op_local(base_local, base_ptr_ty),
+ api_op_imm(offset, base_ptr_ty));
+ base_local = r;
+ place_offset = 0;
+ }
/* Force the index to a local first so we can tell whether it is a dead
- * transient (provably not referenced elsewhere) that we can take over as the
- * index slot directly, skipping the ownership copy. */
+ * transient (provably not referenced elsewhere) that we can take over as
+ * the index slot directly, skipping the ownership copy. */
idx_op = api_force_local_unless_imm(g, &idx, idx_ty);
if (idx.op.kind == OPK_LOCAL) idx_op = idx.op;
if (lg2 >= 0 && idx_op.kind == OPK_LOCAL && idx_op.v.local != base_local &&
@@ -1143,7 +1201,7 @@ static void api_cg_elem(KitCg* g, u32 elem_size, int64_t offset) {
api_release(g, &base);
api_release(g, &idx);
api_push(g, api_make_lv(api_op_indirect_indexed(base_local, ir, log2_scale,
- (i32)offset, elem_ty),
+ place_offset, elem_ty),
elem_ty));
}
}
diff --git a/test/api/cg_type_test.c b/test/api/cg_type_test.c
@@ -1200,6 +1200,148 @@ static void exercise_cg_constfold_phases(KitCompiler* c, KitCgTypeId i32_ty,
partial_size, local_size);
}
+static void exercise_cg_memop_constfold_shape(KitCompiler* c,
+ KitCgTypeId i32_ty,
+ KitCgTypeId i64_ty,
+ KitCgTypeId ptr_i32) {
+ KitCodeOptions opts;
+ KitObjBuilder* ob;
+ KitWriter* dump = NULL;
+ KitCg* cg;
+ KitCgFuncParam params[2];
+ KitCgFuncSig sig;
+ KitCgDecl decl;
+ KitCgSym fn_sym;
+ KitCgSym data_sym;
+ KitCgLocalAttrs attrs;
+ KitCgLocal base_param;
+ KitCgLocal idx_param;
+ KitCgMemAccess mem_i32;
+ KitCgMemAccess mem_i64;
+ KitCgMemAccess mem_ptr;
+ const uint8_t data[64] = {0};
+ const uint8_t* bytes;
+ size_t len = 0;
+ char text[8192];
+ size_t copy_len;
+
+ memset(&opts, 0, sizeof opts);
+ opts.opt_level = 1;
+ opts.emit_ir = true;
+ EXPECT(kit_writer_mem(&g_u.heap, &dump) == KIT_OK && dump != NULL,
+ "memop fold dump writer allocation failed");
+ if (!dump) return;
+ opts.ir_dump_writer = dump;
+
+ ob = new_obj(c);
+ EXPECT(ob != NULL, "memop fold obj builder allocation failed");
+ if (!ob) {
+ kit_writer_close(dump);
+ return;
+ }
+ cg = NULL;
+ (void)kit_cg_new(c, &cg);
+ if (cg) (void)kit_cg_begin(cg, ob, &opts);
+ EXPECT(cg != NULL, "memop fold cg allocation failed");
+ if (!cg) {
+ kit_obj_builder_free(ob);
+ kit_writer_close(dump);
+ return;
+ }
+
+ data_sym = kit_cg_const_data(cg, data, sizeof data, 4, i32_ty);
+ EXPECT(data_sym != KIT_CG_SYM_NONE, "memop fold const data failed");
+
+ memset(params, 0, sizeof params);
+ params[0].type = ptr_i32;
+ params[1].type = i64_ty;
+ memset(&sig, 0, sizeof sig);
+ KitCgFuncResult sig_result;
+ memset(&sig_result, 0, sizeof sig_result);
+ sig_result.type = i32_ty;
+ sig.result = sig_result;
+ sig.params = params;
+ sig.nparams = 2;
+ sig.call_conv = KIT_CG_CC_TARGET_C;
+
+ memset(&decl, 0, sizeof decl);
+ decl.kind = KIT_CG_DECL_FUNC;
+ decl.linkage_name =
+ kit_sym_intern(c, KIT_SLICE_LIT("cg_memop_constfold_shape"));
+ decl.display_name = decl.linkage_name;
+ decl.type = kit_cg_type_func(c, sig);
+ decl.sym.bind = KIT_SB_GLOBAL;
+ decl.sym.visibility = KIT_CG_VIS_DEFAULT;
+ fn_sym = kit_cg_decl(cg, decl);
+ EXPECT(fn_sym != KIT_CG_SYM_NONE, "memop fold function decl failed");
+
+ memset(&mem_i32, 0, sizeof mem_i32);
+ mem_i32.type = i32_ty;
+ mem_i32.align = kit_cg_type_align(c, i32_ty);
+ memset(&mem_i64, 0, sizeof mem_i64);
+ mem_i64.type = i64_ty;
+ mem_i64.align = kit_cg_type_align(c, i64_ty);
+ memset(&mem_ptr, 0, sizeof mem_ptr);
+ mem_ptr.type = ptr_i32;
+ mem_ptr.align = kit_cg_type_align(c, ptr_i32);
+
+ kit_cg_func_begin(cg, fn_sym);
+ memset(&attrs, 0, sizeof attrs);
+ attrs.name = kit_sym_intern(c, KIT_SLICE_LIT("base"));
+ base_param = kit_cg_param(cg, 0, ptr_i32, attrs);
+ attrs.name = kit_sym_intern(c, KIT_SLICE_LIT("idx"));
+ idx_param = kit_cg_param(cg, 1, i64_ty, attrs);
+ EXPECT(base_param != KIT_CG_LOCAL_NONE && idx_param != KIT_CG_LOCAL_NONE,
+ "memop fold params failed");
+
+ /* Constant base addend + constant index*scale + offset: 11 + 3*8 + 5. */
+ kit_cg_push_symbol_addr(cg, data_sym, 11);
+ kit_cg_push_int(cg, 3, i64_ty);
+ kit_cg_elem_scaled(cg, 8, 5);
+ kit_cg_load(cg, mem_i32);
+ kit_cg_drop(cg);
+
+ /* Non-global offsets wider than the memop displacement are materialized into
+ * the base instead of truncated to i32. */
+ kit_cg_push_local(cg, base_param);
+ kit_cg_load(cg, mem_ptr);
+ kit_cg_push_local(cg, idx_param);
+ kit_cg_load(cg, mem_i64);
+ kit_cg_elem_scaled(cg, 8, 2147483656LL);
+ kit_cg_load(cg, mem_i32);
+ kit_cg_drop(cg);
+
+ /* Dynamic base and index keep the scale and offset on the memory operand. */
+ kit_cg_push_local(cg, base_param);
+ kit_cg_load(cg, mem_ptr);
+ kit_cg_push_local(cg, idx_param);
+ kit_cg_load(cg, mem_i64);
+ kit_cg_elem_scaled(cg, 8, 5);
+ kit_cg_load(cg, mem_i32);
+ kit_cg_ret(cg);
+ kit_cg_func_end(cg);
+
+ EXPECT(kit_cg_finish(cg, NULL) == KIT_OK, "memop fold cg finish failed");
+ bytes = kit_writer_mem_bytes(dump, &len);
+ copy_len = len < sizeof text - 1u ? len : sizeof text - 1u;
+ if (bytes && copy_len) memcpy(text, bytes, copy_len);
+ text[copy_len] = '\0';
+ EXPECT(bytes != NULL && len > 0 && len < sizeof text,
+ "memop fold IR dump missing or too large");
+ EXPECT(strstr(text, "addr_of") == NULL,
+ "memop fold should not materialize folded addresses:\n%s", text);
+ EXPECT(strstr(text, "sym#") != NULL && strstr(text, "+40") != NULL,
+ "memop fold should fold symbol base/addend/index/offset:\n%s", text);
+ EXPECT(strstr(text, "*8+5") != NULL,
+ "memop fold should preserve dynamic index scale/offset:\n%s", text);
+ EXPECT(strstr(text, "imm:2147483656") != NULL,
+ "memop fold should materialize offsets wider than i32:\n%s", text);
+
+ kit_cg_free(cg);
+ kit_obj_builder_free(ob);
+ kit_writer_close(dump);
+}
+
typedef struct BadStoreCtx {
KitCompiler* c;
KitCgTypeId i32_ty;
@@ -1591,6 +1733,7 @@ int main(void) {
exercise_cg_data_entsize(c, i8_ty);
exercise_cg_literal_folds(c, i32_ty);
exercise_cg_constfold_phases(c, i32_ty, i8_ty);
+ exercise_cg_memop_constfold_shape(c, i32_ty, i64_ty, ptr_i32);
exercise_cg_memory_mismatch_diags(c, i32_ty, i64_ty, rec);
exercise_compile_session_two_deltas(c);
exercise_cg_begin_end_two_objects(c);