kit

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

commit de680188bbd640e6176fdbc739a3a4a363611dd1
parent 07dcaf160e8114e93708a51433fca6e5157ee891
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 17:47:11 -0700

arm32 Phase 2: switch jump tables + bitfields + byte-reverse/CLZ/CTZ + barrier intrinsics

Diffstat:
Msrc/arch/arm32/arch.c | 27+++++++++++++++------------
Msrc/arch/arm32/isa.h | 48++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/arch/arm32/native.c | 131++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
3 files changed, 176 insertions(+), 30 deletions(-)

diff --git a/src/arch/arm32/arch.c b/src/arch/arm32/arch.c @@ -190,18 +190,29 @@ static int arm32_supports_call_conv(const Compiler* c, KitCgCallConv cc) { return cc == KIT_CG_CC_TARGET_C; } -/* Kept in lockstep with arm32_intrinsic in native.c: returning 1 for an +/* Kept in lockstep with arm_intrinsic in native.c: returning 1 for an * intrinsic this backend does not lower panics at emit. No default: case so - * -Wswitch enforces the twin-sync invariant. Phase 1 lowers only TRAP. */ + * -Wswitch enforces the twin-sync invariant. Lowered: TRAP, CLZ, CTZ (RBIT+CLZ), + * BSWAP (REV/REV16; the 64-bit form is a __bswapdi2 libcall in cg), the no-op / + * event hints (NOP/YIELD/WFI/WFE/SEV), and the barriers (DMB/DSB/ISB). POPCOUNT, + * the overflow forms, FMA, syscall/setjmp, IRQ/cache ops are other tracks. */ static int arm32_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { (void)c; switch (intrin) { case KIT_CG_INTRIN_TRAP: - return 1; case KIT_CG_INTRIN_CLZ: case KIT_CG_INTRIN_CTZ: - case KIT_CG_INTRIN_POPCOUNT: case KIT_CG_INTRIN_BSWAP: + case KIT_CG_INTRIN_CPU_NOP: + case KIT_CG_INTRIN_CPU_YIELD: + case KIT_CG_INTRIN_ISB: + case KIT_CG_INTRIN_DMB: + case KIT_CG_INTRIN_DSB: + case KIT_CG_INTRIN_WFI: + case KIT_CG_INTRIN_WFE: + case KIT_CG_INTRIN_SEV: + return 1; + case KIT_CG_INTRIN_POPCOUNT: case KIT_CG_INTRIN_SADD_OVERFLOW: case KIT_CG_INTRIN_UADD_OVERFLOW: case KIT_CG_INTRIN_SSUB_OVERFLOW: @@ -211,14 +222,6 @@ static int arm32_supports_intrinsic(const Compiler* c, KitCgIntrinsic intrin) { case KIT_CG_INTRIN_PREFETCH: case KIT_CG_INTRIN_EXPECT: case KIT_CG_INTRIN_ASSUME_ALIGNED: - case KIT_CG_INTRIN_CPU_NOP: - case KIT_CG_INTRIN_CPU_YIELD: - case KIT_CG_INTRIN_ISB: - case KIT_CG_INTRIN_DMB: - case KIT_CG_INTRIN_DSB: - case KIT_CG_INTRIN_WFI: - case KIT_CG_INTRIN_WFE: - case KIT_CG_INTRIN_SEV: case KIT_CG_INTRIN_FRAME_ADDRESS: case KIT_CG_INTRIN_RETURN_ADDRESS: case KIT_CG_INTRIN_READCYCLECOUNTER: diff --git a/src/arch/arm32/isa.h b/src/arch/arm32/isa.h @@ -251,4 +251,52 @@ static inline u32 arm_nop32(void) { return arm_t32(0xf3afu, 0x8000u); } static inline u32 arm_push_w(u32 reglist) { return arm_t32(0xe92du, reglist & 0xdfffu); } static inline u32 arm_pop_w(u32 reglist) { return arm_t32(0xe8bdu, reglist & 0xffffu); } +/* --------- bit-field extract / insert (Thumb-2 T1) --------- */ +/* lsb is split into imm3:imm2 = (lsb>>2):(lsb&3). UBFX/SBFX encode width-1; + * BFI/BFC encode msb = lsb + width - 1. */ +static inline u32 arm_ubfx(u32 rd, u32 rn, u32 lsb, u32 width) { + u32 imm3 = (lsb >> 2) & 7u, imm2 = lsb & 3u, widthm1 = (width - 1u) & 0x1fu; + return arm_t32(0xf3c0u | (rn & 0xfu), + (imm3 << 12) | (rd << 8) | (imm2 << 6) | widthm1); +} +static inline u32 arm_sbfx(u32 rd, u32 rn, u32 lsb, u32 width) { + u32 imm3 = (lsb >> 2) & 7u, imm2 = lsb & 3u, widthm1 = (width - 1u) & 0x1fu; + return arm_t32(0xf340u | (rn & 0xfu), + (imm3 << 12) | (rd << 8) | (imm2 << 6) | widthm1); +} +/* BFI rd, rn, #lsb, #width : insert width bits of rn into rd starting at lsb. */ +static inline u32 arm_bfi(u32 rd, u32 rn, u32 lsb, u32 width) { + u32 imm3 = (lsb >> 2) & 7u, imm2 = lsb & 3u, + msb = (lsb + width - 1u) & 0x1fu; + return arm_t32(0xf360u | (rn & 0xfu), (imm3 << 12) | (rd << 8) | (imm2 << 6) | msb); +} +/* BFC rd, #lsb, #width : clear width bits of rd (BFI with Rn=1111). */ +static inline u32 arm_bfc(u32 rd, u32 lsb, u32 width) { + return arm_bfi(rd, 0xfu, lsb, width); +} + +/* --------- count / reverse --------- */ +static inline u32 arm_clz(u32 rd, u32 rm) { + return arm_t32(0xfab0u | (rm & 0xfu), 0xf080u | (rd << 8) | (rm & 0xfu)); +} +static inline u32 arm_rbit(u32 rd, u32 rm) { + return arm_t32(0xfa90u | (rm & 0xfu), 0xf0a0u | (rd << 8) | (rm & 0xfu)); +} +/* 16-bit byte-reverse (T1): REV / REV16 / REVSH. */ +static inline u16 arm_rev(u32 rd, u32 rm) { return (u16)(0xba00u | ((rm & 7u) << 3) | (rd & 7u)); } +static inline u16 arm_rev16(u32 rd, u32 rm) { return (u16)(0xba40u | ((rm & 7u) << 3) | (rd & 7u)); } +static inline u16 arm_revsh(u32 rd, u32 rm) { return (u16)(0xbac0u | ((rm & 7u) << 3) | (rd & 7u)); } + +/* --------- barriers (32-bit T1, hw2 = 0x8f00 | op<<4 | option) --------- */ +/* option = 0xf for the "sy" full-system barrier (the only form kit emits). */ +static inline u32 arm_dmb(u32 option) { return arm_t32(0xf3bfu, 0x8f50u | (option & 0xfu)); } +static inline u32 arm_dsb(u32 option) { return arm_t32(0xf3bfu, 0x8f40u | (option & 0xfu)); } +static inline u32 arm_isb(u32 option) { return arm_t32(0xf3bfu, 0x8f60u | (option & 0xfu)); } + +/* --------- 16-bit hint instructions (0xbf__) --------- */ +static inline u16 arm_yield16(void) { return (u16)0xbf10u; } +static inline u16 arm_wfe16(void) { return (u16)0xbf20u; } +static inline u16 arm_wfi16(void) { return (u16)0xbf30u; } +static inline u16 arm_sev16(void) { return (u16)0xbf40u; } + #endif diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -1381,10 +1381,22 @@ static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) { } static void arm_indirect_branch(NativeTarget* t, NativeLoc addr, const MCLabel* targets, u32 n) { - (void)addr; + /* The cg layer materialized the dense switch's target code address into + * `addr` (a load of table[idx] from the .Lkit_jt rodata table). The table + * entries are R_ARM_ABS32 relocations against per-block local label symbols, + * which carry NO Thumb bit (only STT_FUNC symbols get it in arm_func_end), so + * the loaded address is even. BX to an even address would switch to ARM + * state (UNPREDICTABLE on M-profile); force the Thumb bit with ORR #1 first. + * `addr` is released by the caller right after this, so clobbering it is free. + */ + MCEmitter* mc = t->mc; + u32 ra = loc_reg(addr); + u32 enc; (void)targets; (void)n; - ARM_UNIMPL("indirect_branch (switch table)"); + thumb_expand_imm_encode(1u, &enc); + arm_emit_t32(mc, arm_dp_imm(2u, 0u, ra, ra, enc)); /* ORR ra, ra, #1 */ + arm_emit_t16(mc, arm_bx(ra)); /* BX ra */ } static void arm_tls_addr_of(NativeTarget* t, NativeLoc dst, ObjSymId sym, i64 addend) { @@ -1452,19 +1464,41 @@ static void arm_set_bytes(NativeTarget* t, NativeAddr dst, NativeLoc v, arm_emit_mem(a, 0, v, d, mem); /* STRB v, [dst+off] */ } } +/* C bit-fields. The storage unit {bf.storage.type, bf.storage.size} lives at + * bf.storage_offset from `addr`; the field is `bf.bit_width` bits at bit + * bf.bit_offset within it. Load = load the unit (zero-extended) then UBFX/SBFX; + * store = load the unit, BFI the value in, store the unit back. Sub-word storage + * units (1/2 bytes) are loaded/stored at their natural width, so the BFI/UBFX + * geometry (offset+width <= storage_bits) stays in range. */ static void arm_bitfield_load(NativeTarget* t, NativeLoc dst, NativeAddr addr, BitFieldAccess bf) { - (void)dst; - (void)addr; - (void)bf; - ARM_UNIMPL("bitfield_load"); + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rd = loc_reg(dst); + u32 width = bf.bit_width ? bf.bit_width : 1u; + NativeAddr saddr = addr; + NativeLoc unit = dst; + saddr.offset += (i32)bf.storage_offset; + unit.type = bf.storage.type ? bf.storage.type : dst.type; + arm_emit_mem(a, 1, unit, saddr, bf.storage); /* LDR(B/H) rd, [addr+soff] */ + if (bf.signed_) + arm_emit_t32(mc, arm_sbfx(rd, rd, bf.bit_offset, width)); + else + arm_emit_t32(mc, arm_ubfx(rd, rd, bf.bit_offset, width)); } static void arm_bitfield_store(NativeTarget* t, NativeAddr addr, NativeLoc v, BitFieldAccess bf) { - (void)addr; - (void)v; - (void)bf; - ARM_UNIMPL("bitfield_store"); + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + u32 rv = loc_reg(v); + u32 width = bf.bit_width ? bf.bit_width : 1u; + KitCgTypeId unit_ty = bf.storage.type ? bf.storage.type : v.type; + NativeLoc word = native_loc_reg(unit_ty, NATIVE_REG_INT, ARM_TMP); + NativeAddr saddr = addr; + saddr.offset += (i32)bf.storage_offset; + arm_emit_mem(a, 1, word, saddr, bf.storage); /* LDR tmp, [addr] */ + arm_emit_t32(mc, arm_bfi(ARM_TMP, rv, bf.bit_offset, width)); /* insert v */ + arm_emit_mem(a, 0, word, saddr, bf.storage); /* STR tmp, [addr] */ } static void arm_alloca(NativeTarget* t, NativeLoc dst, NativeLoc size, u32 align) { @@ -1489,18 +1523,79 @@ static void arm_atomic_store(NativeTarget* t, NativeAddr addr, NativeLoc v, (void)order; ARM_UNIMPL("atomic_store"); } +/* The ARMv7-M barrier option for a full-system barrier ("sy"). 64-bit clz/ctz/ + * bswap on a 32-bit target are already routed to __*di2 libcalls by cg, so the + * backend only ever sees the 16/32-bit forms here. Each lowered intrinsic must + * stay in lockstep with arm32_supports_intrinsic (arch.c). */ +#define ARM_BARRIER_SY 0xfu + static void arm_intrinsic(NativeTarget* t, IntrinKind kind, const NativeLoc* dsts, u32 ndst, const NativeLoc* args, u32 narg) { - (void)dsts; - (void)ndst; - (void)args; - (void)narg; - if (kind == INTRIN_TRAP) { - arm_trap(t); - return; + Arm32NativeTarget* a = arm_of(t); + MCEmitter* mc = t->mc; + switch (kind) { + case INTRIN_TRAP: + arm_trap(t); + return; + case INTRIN_CLZ: + if (ndst == 1u && narg == 1u) { + arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(args[0]))); + return; + } + break; + case INTRIN_CTZ: + /* RBIT reverses the bit order, so the trailing-zero count becomes a + * leading-zero count of the reversed word. */ + if (ndst == 1u && narg == 1u) { + arm_emit_t32(mc, arm_rbit(loc_reg(dsts[0]), loc_reg(args[0]))); + arm_emit_t32(mc, arm_clz(loc_reg(dsts[0]), loc_reg(dsts[0]))); + return; + } + break; + case INTRIN_BSWAP: + if (ndst == 1u && narg == 1u) { + u32 width = native_type_size(t, dsts[0].type); + if (width == 4u) { + arm_emit_t16(mc, arm_rev(loc_reg(dsts[0]), loc_reg(args[0]))); + return; + } + if (width == 2u) { + /* REV16 byte-swaps within each halfword: the low halfword of the + * input becomes the swapped 16-bit result in the low halfword. */ + arm_emit_t16(mc, arm_rev16(loc_reg(dsts[0]), loc_reg(args[0]))); + return; + } + } + break; + case INTRIN_CPU_NOP: + arm_emit_t16(mc, arm_nop16()); + return; + case INTRIN_CPU_YIELD: + arm_emit_t16(mc, arm_yield16()); + return; + case INTRIN_WFI: + arm_emit_t16(mc, arm_wfi16()); + return; + case INTRIN_WFE: + arm_emit_t16(mc, arm_wfe16()); + return; + case INTRIN_SEV: + arm_emit_t16(mc, arm_sev16()); + return; + case INTRIN_DMB: + arm_emit_t32(mc, arm_dmb(ARM_BARRIER_SY)); + return; + case INTRIN_DSB: + arm_emit_t32(mc, arm_dsb(ARM_BARRIER_SY)); + return; + case INTRIN_ISB: + arm_emit_t32(mc, arm_isb(ARM_BARRIER_SY)); + return; + default: + break; } - ARM_UNIMPL("intrinsic"); + arm_panic(a, "intrinsic not lowered (overflow/FMA/syscall/etc. are other tracks)"); } /* ============================ construction ============================ */