kit

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

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:
Msrc/cg/control.c | 108++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
Mtest/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);