kit

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

commit 07dcaf160e8114e93708a51433fca6e5157ee891
parent 034c337916199487593e4e99dfddfed77e66cc6d
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 17:41:19 -0700

arm32 Phase 2: -O1 known-frame prologue/epilogue + large-frame IP staging

Diffstat:
Msrc/arch/arm32/isa.h | 2++
Msrc/arch/arm32/native.c | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
2 files changed, 277 insertions(+), 37 deletions(-)

diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -244,6 +244,8 @@ static inline u16 arm_bx(u32 rm) { return (u16)(0x4700u | (rm << 3)); } static inline u16 arm_blx_reg(u32 rm) { return (u16)(0x4780u | (rm << 3)); } static inline u16 arm_bkpt(u32 imm8) { return (u16)(0xbe00u | (imm8 & 0xffu)); } static inline u16 arm_nop16(void) { return (u16)0xbf00u; } +/* NOP.W (32-bit) — pads a multi-slot prologue placeholder region. */ +static inline u32 arm_nop32(void) { return arm_t32(0xf3afu, 0x8000u); } /* --------- push / pop (32-bit, STMDB sp! / LDMIA sp!) --------- */ static inline u32 arm_push_w(u32 reglist) { return arm_t32(0xe92du, reglist & 0xdfffu); } diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -49,6 +49,10 @@ extern void debug_func_pc_range(Debug*, ObjSecId text_section, u32 begin_ofs, * register between calls is free and cannot collide with an NDT operand in IP. */ #define ARM_TMP 14u #define ARM_MAX_REG_ARG_MOVES 16u +/* Worst-case T32 instruction slots the single-pass deferred-`SUB sp` region must + * hold: an IP-staged large frame is MOVW + MOVT + SUB sp,sp,ip = 3 words. A + * small frame patches the first slot with one SUBW and NOP-fills the rest. */ +#define ARM_NDT_SUB_WORDS 3u /* ============================ state ============================ */ @@ -66,6 +70,15 @@ typedef struct Arm32NativeTarget { u32 func_start; u32 prologue_sub_pos; /* file offset of the deferred SUB sp (patched at end) */ MCLabel epilogue_label; + /* Known-frame (-O1) state. On the known-frame path the optimizer hands us the + * exact frame up front, so the prologue is emitted final (no deferred SUB to + * patch in arm_func_end). saved_reglist is the PUSH/POP register set (callee- + * saves + r7 + lr); saved_block_bytes is its size in bytes, which incoming + * stack args sit above (so bind_param adds it instead of the bare 8). */ + u8 known_frame; + u8 slim_prologue; /* leaf, no frame: emit BX lr, no PUSH/anchor */ + u16 saved_reglist; /* registers PUSHed by the prologue (POP restores via pc) */ + u32 saved_block_bytes; } Arm32NativeTarget; static Arm32NativeTarget* arm_of(NativeTarget* t) { @@ -327,6 +340,24 @@ static u32 arm_addr_base(Arm32NativeTarget* a, const NativeAddr* addr, } } +/* Materialize the unsigned 32-bit constant `v` into `rd` (MOVW + optional MOVT). + * Used to stage large frame/memory offsets and SUB-sp amounts that exceed the + * inline immediate forms. */ +static void arm_emit_load_u32(Arm32NativeTarget* a, u32 rd, u32 v) { + MCEmitter* mc = a->base.mc; + u32 enc; + if (thumb_expand_imm_encode(v, &enc)) { + arm_emit_t32(mc, arm_mov_imm(rd, enc)); + return; + } + if (thumb_expand_imm_encode(~v, &enc)) { + arm_emit_t32(mc, arm_mvn_imm(rd, enc)); + return; + } + arm_emit_t32(mc, arm_movw(rd, v & 0xffffu)); + if ((v >> 16) != 0u) arm_emit_t32(mc, arm_movt(rd, (v >> 16) & 0xffffu)); +} + /* Emit a load (is_load=1) or store of `reg` to [base, #off], dispatching the * width from mem.size. Loads zero-extend (LDR/LDRB/LDRH); signed narrowing is a * separate convert. */ @@ -357,7 +388,18 @@ static void arm_emit_mem(Arm32NativeTarget* a, int is_load, NativeLoc reg, } else if (off >= 0 && off <= 255) { arm_emit_t32(mc, arm_ldst_t4(t4hw1, rt, base, (u32)off, 1u)); } else { - arm_panic(a, "frame/memory offset out of range (needs IP fallback)"); + /* Large frame/memory offset (-O1 big frames): stage base+off into ARM_TMP + * (lr) and access [lr]. lr is reserved (never an NDT operand) and dead in + * the body, so it can never alias the transfer reg rt — including the + * rt==IP stack-arg path. base is a frame/general register here, never + * ARM_TMP: a GLOBAL base (the only ARM_TMP producer) folds its full byte + * offset into the MOVW/MOVT reloc addend, so it always reaches arm_emit_mem + * with off in range and never takes this branch. */ + if (base == ARM_TMP) arm_panic(a, "large offset on lr-staged base"); + arm_emit_load_u32(a, ARM_TMP, (u32)(off < 0 ? -off : off)); + arm_emit_t32(mc, off < 0 ? arm_sub_reg(ARM_TMP, base, ARM_TMP) + : arm_add_reg(ARM_TMP, base, ARM_TMP)); + arm_emit_t32(mc, arm_t32(t3hw1 | ARM_TMP, (rt << 12) | 0u)); } } @@ -424,7 +466,12 @@ static void arm_emit_base_off(Arm32NativeTarget* a, u32 rd, u32 base, i32 off) { } else if (off < 0 && thumb_expand_imm_encode((u32)(-off), &enc)) { arm_emit_t32(mc, arm_dp_imm(13u, 0u, rd, base, enc)); /* SUB rd, base, #-off */ } else { - arm_panic(a, "load_addr offset out of range"); + /* Large frame/array offset (-O1 big frames): stage |off| in ARM_TMP (lr) so + * the base register survives even when rd aliases it, then add/sub into rd. + * lr is reserved and dead in the body. */ + arm_emit_load_u32(a, ARM_TMP, (u32)(off < 0 ? -off : off)); + arm_emit_t32(mc, off < 0 ? arm_sub_reg(rd, base, ARM_TMP) + : arm_add_reg(rd, base, ARM_TMP)); } } @@ -679,6 +726,9 @@ static void arm_cmp_branch(NativeTarget* t, CmpOp op, NativeLoc a_loc, /* ============================ frame lifecycle ============================ */ +static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, + int* variadic, u32* nparams); + static NativeFrameSlot arm_frame_slot(NativeTarget* t, const NativeFrameSlotDesc* d) { return native_frame_slot_alloc(&arm_of(t)->frame, d); @@ -709,6 +759,89 @@ static void arm_reserve_callee_saves(NativeTarget* t, const u32* used_by_class, NULL, 0, 0); } +/* AAPCS callee-saved integer registers this backend lets the allocator use: + * r4..r11 except r7 (the frame pointer, preserved by the saved-pair head, not as + * an ordinary callee-save). Matches arm_int_phys's ARM_PHYS_CALLEE set. */ +static int arm_reg_is_callee_int(Reg r) { + return r >= 4u && r <= 11u && r != ARM_FP; +} + +/* Build the callee-saved set the prologue must preserve, from the allocator's + * per-class masks. Only the integer class exists in this soft-float backend, so + * the FP/vec classes are ignored; inline asm is unsupported (asm_block is not + * installed), so there are no opaque asm clobbers to fold in. Restricts to the + * register file's actual callee-saved set as a guard. */ +static u32 arm_known_callee_saves(const NativeKnownFrameDesc* frame) { + u32 mask = 0; + if (frame && frame->callee_saved_used && frame->ncallee_classes > NATIVE_REG_INT) + mask = frame->callee_saved_used[NATIVE_REG_INT]; + for (Reg r = 0; r < 16u; ++r) + if ((mask & (1u << r)) && !arm_reg_is_callee_int(r)) mask &= ~(1u << r); + return mask; +} + +/* Build the `SUB sp, sp, #n` instruction sequence (n a multiple of 8) into + * words[], returning the count (<= ARM_NDT_SUB_WORDS). Small amounts use one + * inline SUBW; large amounts (n > 4095, big frames) stage n into IP via + * MOVW(+MOVT) and `SUB sp, sp, ip`. IP is the emit scratch, dead at the + * prologue. Shared by the inline known-frame emit and the single-pass patch. */ +static u32 arm_build_sub_sp(u32 n, u32* words) { + u32 enc, wi = 0; + if (n == 0) return 0; + if (n <= 0xfffu) { + words[wi++] = arm_sub_imm12(13u, 13u, n); + return wi; + } + if (thumb_expand_imm_encode(n, &enc)) { + words[wi++] = arm_dp_imm(13u, 0u, 13u, 13u, enc); /* SUB.W sp,sp,#modimm */ + return wi; + } + words[wi++] = arm_movw(ARM_SCRATCH, n & 0xffffu); + if ((n >> 16) != 0u) words[wi++] = arm_movt(ARM_SCRATCH, (n >> 16) & 0xffffu); + words[wi++] = arm_sub_reg(13u, 13u, ARM_SCRATCH); + return wi; +} + +/* Emit `SUB sp, sp, #n` inline (known-frame prologue, where the size is final). */ +static void arm_emit_sub_sp(Arm32NativeTarget* a, u32 n) { + u32 words[ARM_NDT_SUB_WORDS]; + u32 nwords = arm_build_sub_sp(n, words), i; + for (i = 0; i < nwords; ++i) arm_emit_t32(a->base.mc, words[i]); +} + +/* sret: reserve a hidden frame slot for the incoming destination pointer (r0) + * and advance the param cursor past it. Shared by the single-pass and + * known-frame paths (slot creation must happen at the same point in both). */ +static void arm_reserve_sret_slot(Arm32NativeTarget* a) { + NativeTarget* t = &a->base; + if (a->has_sret) { + NativeFrameSlotDesc sd; + memset(&sd, 0, sizeof sd); + sd.type = builtin_id(KIT_CG_BUILTIN_I32); + sd.size = 4; + sd.align = 4; + sd.kind = NATIVE_FRAME_SLOT_SAVE; + a->sret_ptr_slot = arm_frame_slot(t, &sd); + a->next_param_int = 1; /* r0 consumed by the sret pointer */ + } +} + +/* Spill the incoming sret pointer (r0) to its hidden home. The home is + * r7-anchored, so this runs correctly after the frame is set up. */ +static void arm_emit_sret_store(Arm32NativeTarget* a) { + NativeTarget* t = &a->base; + if (a->has_sret && a->sret_ptr_slot != NATIVE_FRAME_SLOT_NONE) { + KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); + NativeAddr addr; + memset(&addr, 0, sizeof addr); + addr.base_kind = NATIVE_ADDR_BASE_FRAME; + addr.base.frame = a->sret_ptr_slot; + addr.base_type = i32t; + arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0), addr, + native_mem_for_type(t, i32t, 4)); + } +} + static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) { Arm32NativeTarget* a = arm_of(t); MCEmitter* mc = t->mc; @@ -721,6 +854,10 @@ static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) { a->has_sret = (abi && abi->has_sret) ? 1u : 0u; a->is_variadic = (abi && abi->variadic) ? 1u : 0u; a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE; + a->known_frame = 0; + a->slim_prologue = 0; + a->saved_reglist = (u16)((1u << ARM_FP) | (1u << 14u)); /* {r7, lr} */ + a->saved_block_bytes = 8u; /* two words */ mc_set_section(mc, fd->text_section_id); mc_emit_align(mc, 4, 0); @@ -729,40 +866,118 @@ static void arm_func_begin(NativeTarget* t, const CGFuncDesc* fd) { mc_cfi_startproc(mc); a->epilogue_label = mc_label_new(mc); - /* Prologue: save fp+lr, set the frame anchor, reserve the (deferred) frame. */ + /* Prologue: save fp+lr, set the frame anchor, reserve the (deferred) frame. + * The deferred `SUB sp` is patched in arm_func_end once cum_off/max_outgoing + * are final. Reserve ARM_NDT_SUB_WORDS T32 slots so a large frame (> 4095) can + * be patched as an IP-staged MOVW/MOVT/SUB sequence; a small frame patches the + * first slot with SUBW and fills the rest with NOPs. */ arm_emit_t32(mc, arm_push_w((1u << ARM_FP) | (1u << 14u))); /* PUSH {r7, lr} */ arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ a->prologue_sub_pos = mc_pos(mc); - arm_emit_t32(mc, arm_sub_imm12(13u, 13u, 0u)); /* SUB sp, sp, #0 (patched) */ + for (u32 i = 0; i < ARM_NDT_SUB_WORDS; ++i) + arm_emit_t32(mc, arm_nop32()); /* placeholder, patched in arm_func_end */ /* sret: spill the incoming destination pointer (r0) to a hidden home. */ - if (a->has_sret) { - NativeFrameSlotDesc sd; - NativeAddr addr; - KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); - memset(&sd, 0, sizeof sd); - sd.type = i32t; - sd.size = 4; - sd.align = 4; - sd.kind = NATIVE_FRAME_SLOT_SAVE; - a->sret_ptr_slot = arm_frame_slot(t, &sd); - a->next_param_int = 1; - memset(&addr, 0, sizeof addr); - addr.base_kind = NATIVE_ADDR_BASE_FRAME; - addr.base.frame = a->sret_ptr_slot; - addr.base_type = i32t; - arm_emit_mem(a, 0, native_loc_reg(i32t, NATIVE_REG_INT, ARM_R0), addr, - native_mem_for_type(t, i32t, 4)); - } -} - + arm_reserve_sret_slot(a); + arm_emit_sret_store(a); +} + +/* Optimizer (-O1) entry point: the optimizer has run register allocation and + * supplies the exact frame up front, so the prologue is emitted final the moment + * it is built — no deferred `SUB sp, #0` placeholder, no arm_func_end patch + * (arm_func_end skips patching when known_frame). The callee-saved registers the + * allocator used are folded directly into the PUSH/POP register list rather than + * given their own frame slots: they ride above the frame anchor r7 in the + * STMDB-saved block, which keeps slot offsets ([r7, #-off]) byte-identical to + * the single-pass layout and lets one LDMIA-via-pc restore-and-return. + * + * Slot creation order matches the single-pass path: static slots first, then the + * sret entry-save slot. (Callee-saves take no slots here, so order vs. them is + * moot.) */ static void arm_func_begin_known_frame(NativeTarget* t, const CGFuncDesc* fd, const NativeKnownFrameDesc* kf, NativeFrameSlot* out_slots) { - (void)fd; - (void)kf; - (void)out_slots; - arm_panic(arm_of(t), "known-frame (-O1) path not implemented in Phase 1"); + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fd->fn_type); + u32 cs_mask = arm_known_callee_saves(kf); + u32 reglist, nsaved, frame, i; + + a->func = fd; + a->loc = fd->loc; + native_frame_reset(&a->frame); + a->next_param_int = 0; + a->next_param_stack = 0; + a->has_sret = (abi && abi->has_sret) ? 1u : 0u; + a->is_variadic = (abi && abi->variadic) ? 1u : 0u; + a->sret_ptr_slot = NATIVE_FRAME_SLOT_NONE; + a->known_frame = 1; + a->slim_prologue = 0; + a->frame.known_frame = 1; + a->frame.has_alloca = kf ? kf->has_alloca : 0u; + + mc_set_section(mc, fd->text_section_id); + mc_emit_align(mc, 4, 0); + a->func_start = mc_pos(mc); + mc_begin_function(mc, fd->sym, fd->text_section_id, a->func_start); + mc_cfi_startproc(mc); + a->epilogue_label = mc_label_new(mc); + + /* Record the callee-save set (for parity / debug) and build the body's slots + * and outgoing-area reservation, then settle the exact frame. */ + if (cs_mask) { + u32 used[NATIVE_CALL_PLAN_CLASSES] = {0, 0, 0}; + used[NATIVE_REG_INT] = cs_mask; + arm_reserve_callee_saves(t, used, NATIVE_CALL_PLAN_CLASSES); + } + if (kf) { + for (i = 0; i < kf->nslots; ++i) { + NativeFrameSlot slot = arm_frame_slot(t, &kf->slots[i]); + if (out_slots) out_slots[i] = slot; + } + arm_reserve_sret_slot(a); + native_frame_note_outgoing(&a->frame, kf->max_outgoing); + } else { + arm_reserve_sret_slot(a); + } + + /* The PUSH/POP register list: callee-saves (low to high) + r7 + lr. STMDB + * stores in ascending register order regardless of the bit order, so r7 lands + * in the block correctly and `MOV r7, sp` anchors just below the whole block. + * The popped lr is restored into pc, returning. */ + reglist = cs_mask | (1u << ARM_FP) | (1u << 14u); + nsaved = 0; + for (i = 0; i < 16u; ++i) + if (reglist & (1u << i)) nsaved++; + a->saved_reglist = (u16)reglist; + a->saved_block_bytes = nsaved * 4u; + + /* Leaf no-frame tier (aa64 slim_prologue / rv frameless equivalent): a leaf + * with no callee-saves, no body slots, no outgoing args, no sret/variadic and + * only register params never touches the stack or clobbers lr, so emit no + * prologue at all and let arm_func_end emit a bare `BX lr`. Inline asm cannot + * occur (asm_block is not installed), so no opaque-lr-clobber guard is needed. + */ + a->slim_prologue = kf && kf->is_leaf && cs_mask == 0 && + !a->frame.has_alloca && a->frame.cum_off == 0 && + a->frame.max_outgoing == 0 && !a->has_sret && + !a->is_variadic && + arm_signature_stack_bytes(t, fd->fn_type, NULL, NULL) == 0; + if (a->slim_prologue) { + a->saved_reglist = 0; + a->saved_block_bytes = 0; + native_frame_set_final(&a->frame); + return; + } + + /* Emit the final prologue: PUSH the save set, anchor r7, drop the frame. */ + arm_emit_t32(mc, arm_push_w(reglist)); + arm_emit_t16(mc, arm_mov_hi(ARM_FP, 13u)); /* MOV r7, sp */ + frame = arm_frame_size(a); + arm_emit_sub_sp(a, frame); + + arm_emit_sret_store(a); + native_frame_set_final(&a->frame); } static void arm_func_end(NativeTarget* t) { @@ -771,14 +986,35 @@ static void arm_func_end(NativeTarget* t) { u32 frame; /* Place the epilogue and emit the teardown. */ mc_label_place(mc, a->epilogue_label); - arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 */ - arm_emit_t32(mc, arm_pop_w((1u << ARM_FP) | (1u << 15u))); /* POP {r7, pc} */ + if (a->slim_prologue) { + /* Frameless leaf: nothing saved, sp untouched — return through lr directly. */ + arm_emit_t16(mc, arm_bx(14u)); /* BX lr */ + } else { + /* MOV sp, r7 drops the locals frame back to the saved-register block; POP + * the whole save set, restoring lr into pc to return. On the known-frame + * path the save set is {callee-saves, r7, lr}; on the single-pass path it is + * the bare {r7, lr}. Both pop lr → pc. */ + u32 poplist = (a->saved_reglist & ~(1u << 14u)) | (1u << 15u); + arm_emit_t16(mc, arm_mov_hi(13u, ARM_FP)); /* MOV sp, r7 */ + arm_emit_t32(mc, arm_pop_w(poplist)); /* POP {.., r7, pc} */ + } - native_frame_set_final(&a->frame); - frame = arm_frame_size(a); - if (frame > 0xfffu) - arm_panic(a, "frame too large for Phase 1 (needs IP-staged SUB)"); - arm_patch_t32(a, a->prologue_sub_pos, arm_sub_imm12(13u, 13u, frame)); + /* Single-pass path: settle the frame and patch the deferred `SUB sp` region. + * The known-frame path emitted its prologue final in + * arm_func_begin_known_frame (frame already settled there), so it never + * patches. The reserved region is ARM_NDT_SUB_WORDS NOP.W slots; patch the + * leading slots with the built SUB sequence (one SUBW for a small frame, an + * IP-staged MOVW/MOVT/SUB for a large one) and leave the trailing NOP.W slots, + * which execute harmlessly. */ + if (!a->known_frame) { + u32 words[ARM_NDT_SUB_WORDS]; + u32 nwords, i; + native_frame_set_final(&a->frame); + frame = arm_frame_size(a); + nwords = arm_build_sub_sp(frame, words); + for (i = 0; i < nwords; ++i) + arm_patch_t32(a, a->prologue_sub_pos + i * 4u, words[i]); + } /* Publish the function symbol. ARM marks Thumb STT_FUNC symbols with the * low bit set (the Thumb bit), so &fn and indirect BLX reach Thumb state; @@ -817,14 +1053,16 @@ static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p, if (a->next_param_int < 4u) { src = native_loc_reg(p->type, NATIVE_REG_INT, (Reg)(a->next_param_int++)); } else { - /* Incoming stack arg: above the saved {r7,lr} pair = [r7 + 8 + k]. */ + /* Incoming stack arg: above the saved register block = [r7 + N + k], + * where N = saved_block_bytes (8 for the bare {r7,lr} -O0 prologue; larger + * when the -O1 known-frame prologue also PUSHes callee-saves). */ NativeAddr sa; src = native_loc_reg(p->type, NATIVE_REG_INT, ARM_SCRATCH); memset(&sa, 0, sizeof sa); sa.base_kind = NATIVE_ADDR_BASE_REG; sa.base.reg = ARM_FP; sa.base_type = p->type; - sa.offset = (i32)(8u + a->next_param_stack); + sa.offset = (i32)(a->saved_block_bytes + a->next_param_stack); arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, p->type, part->size)); a->next_param_stack += 4u; }