kit

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

commit 319f6e7440208c4e0f7a79ec13548c5e32d6dce8
parent 0e43fcea5cb98d166ee610ebda16d422d714b718
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Tue, 16 Jun 2026 18:04:17 -0700

arm32 Phase 2: i64 + soft-double end-to-end (AAPCS pair-align, 8-byte load_const, call/return pairs)

Wide8 i64/double: AAPCS even-register pair alignment (arm_arg_advance), per-lane
i32 types for i64/double call args/returns, 8-byte load_const backstop, and an
arm_cmp operand-aliasing fix (emit CMP before MOV dst,#0; dst can alias a compare
operand, e.g. a soft-float compare's r0 result). Integrated keeping the already-
landed REL emission (Track REL) and O1's saved_block_bytes stack-arg offset
(combined with the per-lane type in bind_param).

Diffstat:
Msrc/abi/abi_aapcs32.c | 9+++++++--
Msrc/arch/arm32/native.c | 213++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-------------------
2 files changed, 169 insertions(+), 53 deletions(-)

diff --git a/src/abi/abi_aapcs32.c b/src/abi/abi_aapcs32.c @@ -31,15 +31,20 @@ static void classify_scalar(TargetABI* a, KitCgTypeId t, ABIArgInfo* out) { (ti.scalar_kind == ABI_SC_INT || ti.scalar_kind == ABI_SC_FLOAT)) { ABIArgPart* parts = arena_array(a->c->tu, ABIArgPart, 2); memset(parts, 0, sizeof(ABIArgPart) * 2); + /* Both lanes carry align=8: AAPCS requires an 8-byte scalar to land in an + * even/odd core-register pair (r0:r1 or r2:r3) and to 8-byte-align its stack + * slot. The native backend reads this part alignment (> the 4-byte slot) as + * the round-NCRN-to-even / 8-align-stack signal — distinguishing an i64/ + * double pair from a 4-byte-aligned two-word aggregate (struct{int;int;}). */ parts[0].cls = ABI_CLASS_INT; parts[0].loc = ABI_LOC_REG; parts[0].size = ARM32_GPR_BYTES; - parts[0].align = ARM32_GPR_BYTES; + parts[0].align = 2u * ARM32_GPR_BYTES; parts[0].src_offset = 0; parts[1].cls = ABI_CLASS_INT; parts[1].loc = ABI_LOC_REG; parts[1].size = ARM32_GPR_BYTES; - parts[1].align = ARM32_GPR_BYTES; + parts[1].align = 2u * ARM32_GPR_BYTES; parts[1].src_offset = ARM32_GPR_BYTES; out->kind = ABI_ARG_DIRECT; out->flags = ABI_AF_NONE; diff --git a/src/arch/arm32/native.c b/src/arch/arm32/native.c @@ -638,7 +638,12 @@ static void arm_emit_cmp_operands(Arm32NativeTarget* a, NativeLoc x, arm_emit_t32(mc, arm_cmp_reg(loc_reg(x), loc_reg(y))); } -/* Materialize the boolean (a OP b) into dst: MOV dst,#0; CMP; IT cc; MOV dst,#1. */ +/* Materialize the boolean (a OP b) into dst: CMP; MOV dst,#0; IT cc; MOV dst,#1. + * The CMP must precede the `MOV dst,#0`: dst can ALIAS a compare operand (e.g. a + * soft-float compare leaves its libcall result in r0 and the bool destination is + * also r0), and a leading `MOV dst,#0` would clobber that operand before CMP + * reads it. MOV.W #imm (T2, S=0) does not set flags, so emitting it after CMP + * leaves the compare flags intact for the IT-predicated `MOV dst,#1`. */ static void arm_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc a_loc, NativeLoc b) { Arm32NativeTarget* a = arm_of(t); @@ -648,8 +653,8 @@ static void arm_cmp(NativeTarget* t, CmpOp op, NativeLoc dst, NativeLoc a_loc, u32 enc0, enc1; thumb_expand_imm_encode(0u, &enc0); thumb_expand_imm_encode(1u, &enc1); - arm_emit_t32(mc, arm_mov_imm(rd, enc0)); /* dst = 0 */ - arm_emit_cmp_operands(a, a_loc, b); /* CMP a, b */ + arm_emit_cmp_operands(a, a_loc, b); /* CMP a, b (sets flags) */ + arm_emit_t32(mc, arm_mov_imm(rd, enc0)); /* dst = 0 (MOV.W, no flag set) */ arm_emit_t16(mc, (u16)(0xbf08u | (cc << 4))); /* IT cc (1 insn, mask 0b1000) */ arm_emit_t32(mc, arm_mov_imm(rd, enc1)); /* MOV<cc> dst, #1 */ } @@ -1037,33 +1042,75 @@ static void arm_func_end(NativeTarget* t) { /* ============================ param binding ============================ */ +#define ARM32_GPR_BYTES 4u + +/* Lane scalar type for an ABI part: an 8-byte i64/double DIRECT argument is two + * 4-byte INT lanes, an aggregate up to four word/sub-word lanes. The NDT sizes + * every load/store/move it emits for a plan-return / plan-call / param-bind move + * from the lane location's TYPE (nd_type_mem -> cg_type_size), so each lane must + * carry a type whose natural size equals the lane's byte width — NOT the full + * 8-byte scalar type (which would drive an 8-byte access the 32-bit backend has + * no single-instruction form for). Mirrors riscv's rv_part_scalar_type. */ +static KitCgTypeId arm_part_scalar_type(const ABIArgPart* part) { + switch (part->size) { + case 1u: return builtin_id(KIT_CG_BUILTIN_I8); + case 2u: return builtin_id(KIT_CG_BUILTIN_I16); + case 8u: return builtin_id(KIT_CG_BUILTIN_I64); + default: return builtin_id(KIT_CG_BUILTIN_I32); + } +} + +/* AAPCS pair rule: an 8-byte scalar argument (i64/double — surfaced by the ABI + * as a 2-word DIRECT arg whose parts carry align=8, the slot-exceeding marker) + * must occupy an even/odd core-register pair (r0:r1 or r2:r3); the cursor rounds + * up to an even index first. Returns 1 if `ai`'s first part demands that. A + * 4-byte-aligned two-word aggregate (struct{int;int;}) has align=4 and is NOT + * pair-aligned, so the shape (nparts==2) alone is insufficient — the alignment + * is the discriminator. */ +static int arm_arg_is_aligned_pair(const ABIArgInfo* ai) { + return ai->kind == ABI_ARG_DIRECT && ai->nparts == 2u && ai->parts && + ai->parts[0].align >= 2u * ARM32_GPR_BYTES; +} + static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p, NativeLoc dst) { Arm32NativeTarget* a = arm_of(t); const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, a->func->fn_type); const ABIArgInfo* ai = p->index < abi->nparams ? &abi->params[p->index] : NULL; int to_reg = dst.kind == NATIVE_LOC_REG; + int pair = ai && arm_arg_is_aligned_pair(ai); u32 i; if (!ai || ai->kind == ABI_ARG_IGNORE) return; if (ai->kind == ABI_ARG_INDIRECT) arm_panic(a, "indirect (by-ref aggregate) params are Phase 2"); + /* AAPCS: round NCRN up to even before an 8-byte aligned pair, and if the pair + * does not fully fit in the remaining core registers it passes ENTIRELY on the + * (8-byte-aligned) stack — it never straddles the r3/stack boundary. */ + if (pair) { + a->next_param_int = align_up_u32(a->next_param_int, 2u); + if (a->next_param_int + 2u > 4u) { + a->next_param_int = 4u; + a->next_param_stack = align_up_u32(a->next_param_stack, 2u * ARM32_GPR_BYTES); + } + } for (i = 0; i < ai->nparts; ++i) { const ABIArgPart* part = &ai->parts[i]; + KitCgTypeId lty = arm_part_scalar_type(part); NativeLoc src; if (a->next_param_int < 4u) { - src = native_loc_reg(p->type, NATIVE_REG_INT, (Reg)(a->next_param_int++)); + src = native_loc_reg(lty, NATIVE_REG_INT, (Reg)(a->next_param_int++)); } else { /* 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); + src = native_loc_reg(lty, 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.base_type = lty; 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)); + arm_emit_mem(a, 1, src, sa, native_mem_for_type(t, lty, part->size)); a->next_param_stack += 4u; } if (dst.kind == NATIVE_LOC_NONE) { @@ -1078,34 +1125,51 @@ static void arm_bind_native_param(NativeTarget* t, const CGParamDesc* p, memset(&home, 0, sizeof home); home.base_kind = NATIVE_ADDR_BASE_FRAME; home.base.frame = dst.v.frame; - home.base_type = p->type; + home.base_type = lty; home.offset = (i32)part->src_offset; - arm_emit_mem(a, 0, src, home, native_mem_for_type(t, p->type, part->size)); + arm_emit_mem(a, 0, src, home, native_mem_for_type(t, lty, part->size)); } } } /* ============================ calls / returns ============================ */ -/* Outgoing stack-argument bytes for a call: int parts beyond r0..r3. */ +/* Advance the AAPCS argument-placement cursor across ONE argument `ai`, updating + * the core-register count *next_int (r0..r3) and the outgoing-stack byte count + * *stack. The single authority for the pair rule, shared by the stack-size + * accounting and the actual plan_call/bind_param emission so they cannot drift: + * - an 8-byte aligned pair rounds NCRN up to even and, if it does not fully + * fit in r0..r3, passes ENTIRELY on the 8-byte-aligned stack (no straddle); + * - every other part takes the next core register, else a 4-byte stack slot. */ +static void arm_arg_advance(const ABIArgInfo* ai, u32* next_int, u32* stack) { + u32 p; + if (ai->kind == ABI_ARG_IGNORE) return; + if (ai->kind == ABI_ARG_INDIRECT) { + if (*next_int < 4u) (*next_int)++; + else *stack += 4u; + return; + } + if (arm_arg_is_aligned_pair(ai)) { + *next_int = align_up_u32(*next_int, 2u); + if (*next_int + 2u > 4u) { + *next_int = 4u; + *stack = align_up_u32(*stack, 2u * ARM32_GPR_BYTES); + } + } + for (p = 0; p < ai->nparts; ++p) { + if (*next_int < 4u) (*next_int)++; + else *stack += 4u; + } +} + +/* Outgoing stack-argument bytes for a call: parts beyond r0..r3 (8-aligned). */ static u32 arm_call_stack_size(NativeTarget* t, const NativeCallDesc* desc) { const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, desc->fn_type); u32 next_int = (abi && abi->has_sret) ? 1u : 0u; - u32 stack = 0, i, p; + u32 stack = 0, i; if (!abi) return 0; - for (i = 0; i < desc->nargs && i < abi->nparams; ++i) { - const ABIArgInfo* ai = &abi->params[i]; - if (ai->kind == ABI_ARG_IGNORE) continue; - if (ai->kind == ABI_ARG_INDIRECT) { - if (next_int < 4u) next_int++; - else stack += 4u; - continue; - } - for (p = 0; p < ai->nparts; ++p) { - if (next_int < 4u) next_int++; - else stack += 4u; - } - } + for (i = 0; i < desc->nargs && i < abi->nparams; ++i) + arm_arg_advance(&abi->params[i], &next_int, &stack); return align_up_u32(stack, 8u); } @@ -1113,23 +1177,12 @@ static u32 arm_signature_stack_bytes(NativeTarget* t, KitCgTypeId fn_type, int* variadic, u32* nparams) { const ABIFuncInfo* abi = abi_cg_func_info(t->c->abi, fn_type); u32 next_int = (abi && abi->has_sret) ? 1u : 0u; - u32 stack = 0, i, p; + u32 stack = 0, i; if (variadic) *variadic = abi && abi->variadic; if (nparams) *nparams = abi ? abi->nparams : 0u; if (!abi) return 0; - for (i = 0; i < abi->nparams; ++i) { - const ABIArgInfo* ai = &abi->params[i]; - if (ai->kind == ABI_ARG_IGNORE) continue; - if (ai->kind == ABI_ARG_INDIRECT) { - if (next_int < 4u) next_int++; - else stack += 4u; - continue; - } - for (p = 0; p < ai->nparts; ++p) { - if (next_int < 4u) next_int++; - else stack += 4u; - } - } + for (i = 0; i < abi->nparams; ++i) + arm_arg_advance(&abi->params[i], &next_int, &stack); return align_up_u32(stack, 8u); } @@ -1239,19 +1292,28 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, if (!ai || ai->kind == ABI_ARG_IGNORE) continue; if (ai->kind == ABI_ARG_INDIRECT) arm_panic(a, "indirect (by-ref aggregate) args are Phase 2"); + /* AAPCS pair rule (mirrors arm_arg_advance / arm_bind_native_param): round + * NCRN to even before an 8-byte aligned pair; if it does not fully fit in + * r0..r3, the whole argument moves to the 8-byte-aligned stack. */ + if (arm_arg_is_aligned_pair(ai)) { + next_int = align_up_u32(next_int, 2u); + if (next_int + 2u > 4u) { + next_int = 4u; + stack = align_up_u32(stack, 2u * ARM32_GPR_BYTES); + } + } for (p = 0; p < ai->nparts; ++p) { const ABIArgPart* part = &ai->parts[p]; + KitCgTypeId lty = arm_part_scalar_type(part); if (next_int < 4u) { NativeArgMove* m = &moves[nmoves++]; - m->dst = native_loc_reg(desc->args[i].type, NATIVE_REG_INT, - (Reg)(next_int++)); + m->dst = native_loc_reg(lty, NATIVE_REG_INT, (Reg)(next_int++)); m->src = desc->args[i]; m->src_offset = part->src_offset; m->size = part->size; m->is_addr = 0; } else { - NativeLoc tmp = native_loc_reg(desc->args[i].type, NATIVE_REG_INT, - ARM_SCRATCH); + NativeLoc tmp = native_loc_reg(lty, NATIVE_REG_INT, ARM_SCRATCH); arm_load_part(t, tmp, desc->args[i], part->src_offset, part->size); stack = align_up_u32(stack, 4u); arm_store_outgoing(t, stack, tmp, part->size); @@ -1271,7 +1333,10 @@ static void arm_plan_call(NativeTarget* t, const NativeCallDesc* desc, u32 nr = 0, ni = 0, p; for (p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; - KitCgTypeId pty = desc->results[0].type; + /* Per-lane type so each NDT-emitted result move is sized to the lane (4), + * not the full 8-byte scalar (which the 32-bit backend cannot single-step + * load/store). The ret pair is always r0:r1 (no even-rounding needed). */ + KitCgTypeId pty = arm_part_scalar_type(part); rets[nr].src = native_loc_reg(pty, NATIVE_REG_INT, (Reg)(ni++)); rets[nr].dst = desc->results[0]; if (rets[nr].dst.kind == NATIVE_LOC_FRAME) @@ -1324,7 +1389,10 @@ static void arm_plan_ret(NativeTarget* t, const CGFuncDesc* fd, u32 ni = 0, p; for (p = 0; p < abi->ret.nparts; ++p) { const ABIArgPart* part = &abi->ret.parts[p]; - KitCgTypeId pty = value->type; + /* Per-lane type: an i64/double return is two i32 lanes in r0:r1; sizing + * each move from the lane type keeps the 32-bit backend off the 8-byte + * memory path. (See arm_part_scalar_type.) */ + KitCgTypeId pty = arm_part_scalar_type(part); rets[nr].src = *value; if (rets[nr].src.kind == NATIVE_LOC_FRAME) rets[nr].src = @@ -1358,16 +1426,59 @@ static void arm_set_loc(NativeTarget* t, SrcLoc loc) { #define ARM_UNIMPL(name) arm_panic(arm_of(t), name " not implemented in Phase 1") +/* Assemble up to 4 little-endian bytes of `c` starting at byte `off` into a u32. */ +static u32 arm_const_lane(ConstBytes c, u32 off) { + u32 v = 0, i; + for (i = 0; i < 4u && off + i < c.size; ++i) + v |= (u32)c.bytes[off + i] << (i * 8u); + return v; +} + static void arm_load_const(NativeTarget* t, NativeLoc dst, ConstBytes c) { Arm32NativeTarget* a = arm_of(t); - u32 v = 0, i; /* Scalar constant <=4 bytes: reinterpret the ABI bytes (little-endian on - * arm32) as a u32 and materialize via arm_load_imm (MOV.W/MVN.W/MOVW+MOVT). - * 8-byte constants are the wide8 track's concern. */ - if (c.size > 4u) - arm_panic(a, "8-byte load_const is the wide8 track"); - for (i = 0; i < c.size && i < 4u; ++i) v |= (u32)c.bytes[i] << (i * 8u); - arm_load_imm(t, dst, (i64)(i32)v); + * arm32) as a u32 and materialize via arm_load_imm (MOV.W/MVN.W/MOVW+MOVT). */ + if (c.size <= 4u) { + arm_load_imm(t, dst, (i64)(i32)arm_const_lane(c, 0u)); + return; + } + /* 8-byte (i64/double) backstop. At -O0 the NDT lowers wide8 constants into two + * 32-bit lanes before they reach the backend (nd_load_const guards it), so + * this is only exercised by paths that hand the full 8-byte constant straight + * to a destination — materialize the low lane then the high lane, little-end + * first. A memory destination (frame/stack home) carries both lanes 4 bytes + * apart; a single 32-bit register can only hold the low lane (the high lane + * has no home in one NativeLoc), which matches a 32-bit reinterpret. */ + if (c.size > 8u) arm_panic(a, "load_const wider than 8 bytes"); + if (dst.kind == NATIVE_LOC_REG) { + arm_load_imm(t, dst, (i64)(i32)arm_const_lane(c, 0u)); + return; + } + { + KitCgTypeId i32t = builtin_id(KIT_CG_BUILTIN_I32); + NativeLoc lane = native_loc_reg(i32t, NATIVE_REG_INT, ARM_TMP); + NativeAddr home; + MemAccess mem = native_mem_for_type(t, i32t, 4u); + u32 off; + memset(&home, 0, sizeof home); + home.base_type = i32t; + if (dst.kind == NATIVE_LOC_FRAME) { + home.base_kind = NATIVE_ADDR_BASE_FRAME; + home.base.frame = dst.v.frame; + } else if (dst.kind == NATIVE_LOC_STACK) { + home.base_kind = NATIVE_ADDR_BASE_FRAME; + home.base.frame = dst.v.stack.slot; + home.offset = dst.v.stack.offset; + } else { + arm_panic(a, "8-byte load_const destination not lowered"); + } + for (off = 0; off < c.size; off += 4u) { + NativeAddr d = home; + d.offset += (i32)off; + arm_load_imm(t, lane, (i64)(i32)arm_const_lane(c, off)); + arm_emit_mem(a, 0, lane, d, mem); + } + } } static void arm_load_label_addr(NativeTarget* t, NativeLoc dst, MCLabel l) { /* `&&label` address-take (computed goto / jump-table base): materialize the