kit

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

commit 0f8f6f11206cb62dc958185dd0310b3135f15aa9
parent 76f33b88a9245f1f03ef8d8eb7b5406ae06663a2
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Mon, 15 Jun 2026 17:41:06 -0700

aa64: address-build fallback for far slots in any size/large frame

The deferred far-slot patch resolves to a base-relative scaled ldr/str
whose immediate must fit the access-size-scaled reach (4KB/8KB/16KB/32KB
for byte/half/word/dword). The one-word patch was kept for word/dword on
the assumption that the 16KB/32KB reach covers any frame; only byte/half
reserved a second word for the address-build fallback.

That assumption breaks on kit's own large frames: src/api/package.c has a
~69KB frame, where a top-of-frame 8-byte slot (small slot offset, so a
base offset near frame_size) resolved to ~68KB -- past the 32KB dword
reach -- and tripped "far slot offset out of positive scaled range".
The final base offset is frame_size - 16 - slot_off, unknown during the
single-pass body emit (the frame is still growing), so no emit-time test
can prove a one-word patch is safe.

Reserve the second word for every far slot regardless of size, and let
the resolver emit the address-build fallback (add x17,base,#hi ;
ldr/str rt,[x17,#lo], correct for any frame < 16MB) for any size whose
scaled offset overflows; it stays a nop when the one-word form fits.
Also routes two raw 0xd503201f nop encodings through aa64_nop().

Regression test test/parse/cases/far_slot_large_frame.c exercises 4-byte
and 8-byte far slots in a >32KB frame; passes E on aa64/x64/rv64 at
O0/O1.

Diffstat:
Msrc/arch/aa64/native.c | 51++++++++++++++++++++++++++-------------------------
Atest/parse/cases/far_slot_large_frame.c | 32++++++++++++++++++++++++++++++++
Atest/parse/cases/far_slot_large_frame.expected | 1+
3 files changed, 59 insertions(+), 25 deletions(-)

diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -884,14 +884,14 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, * final, aa_apply_patches rewrites it to `[base, #frame_size-...]`, base = sp * (stable) or AA_FRAME_BASE (alloca). * - * A 4/8-byte slot (sz>=2) always fits the scaled imm12 (reach 16 KB/32 KB >= - * any frame), so it is a one-word patch. A 1/2-byte slot (sz<2, Lever 3) has - * a smaller scaled reach (4 KB/8 KB) that the largest frames (sqlite VdbeExec - * ~6.5 KB) exceed, so it reserves TWO words: when the resolved offset fits it - * is `ldrb/strb [base,#scaled] ; nop`, and when it overflows the resolver - * emits the address-build fallback `add x17,base,#hi ; ldrb/strb [x17,#lo]` - * (correct for any frame). byte/half are always integer (vbit=0; FP was - * widened to sz>=2 above). + * The base-relative offset is frame_size - 16 - slot_off, unknown here + * (single-pass: the frame is still growing) and able to exceed the scaled + * ldr/str reach (4 KB/8 KB/16 KB/32 KB by size) for ANY access size — a + * top-of-frame 8-byte slot sits ~frame_size off sp, and kit's own + * src/api/package.c reaches a ~69 KB frame. So every size reserves TWO words: + * when the resolved offset fits it is `ldr/str [base,#scaled] ; nop`, and when + * it overflows the resolver emits the address-build fallback `add x17,base,#hi + * ; ldr/str [x17,#lo]` (correct for any frame < 16 MB). * * Lever 3/4 interaction: a far SIGNED narrow load (ld_opc==LDRS_X) is * excluded from this scaled-slot path — the slot patch records only a plain @@ -928,9 +928,14 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, p->u.slot.rt = (u8)rt; aa_emit32(mc, load ? aa_ldr_uimm_v(sz, vbit, rt, AA_SP, 0) : aa_str_uimm_v(sz, vbit, rt, AA_SP, 0)); - /* Reserve the fallback's second word for byte/half (filled at resolve). - */ - if (sz < 2u) aa_emit32(mc, 0xd503201fu /* nop */); + /* Reserve a second word for the address-build fallback. The final + * base-relative offset is frame_size - 16 - slot_off, which is unknown + * here (single-pass: the frame keeps growing) and can exceed the scaled + * ldr/str reach for ANY access size in a large frame (a top-of-frame + * 8-byte slot sits ~frame_size off sp; kit's own src/api/package.c has a + * ~69 KB frame). So every size reserves the fallback word; aa_apply_patches + * leaves it a nop when the one-word scaled form fits. */ + aa_emit32(mc, aa64_nop()); return; } } @@ -1352,7 +1357,7 @@ static void aa_func_begin(NativeTarget* t, const CGFuncDesc* fd) { a->prologue_region_words = region; { u8 nops[AA_NDT_SUB_WORDS * 4u]; - for (u32 i = 0; i < region; ++i) wr_u32_le(nops + i * 4u, 0xd503201fu); + for (u32 i = 0; i < region; ++i) wr_u32_le(nops + i * 4u, aa64_nop()); if (mc->debug) { u32 ofs = obj_pos(mc->obj, mc->section_id); mc_emit_bytes(mc, nops, region * 4u); @@ -1844,19 +1849,19 @@ static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) { if (base_off < 0 || (ubo & ((1u << sl->sz) - 1u)) != 0u) aa_panic(a, "far slot offset negative or misaligned"); if ((ubo >> sl->sz) <= 0xfffu) { - /* Scaled immediate fits: one-word ldr/str. For byte/half the reserved - * second placeholder word stays the nop emitted at body time. */ + /* Scaled immediate fits: one-word ldr/str; the reserved second + * placeholder word stays the nop emitted at body time. */ aa_patch32(a->base.obj, sec, p->pos, sl->load ? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo) : aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, base, ubo)); - } else if (sl->sz < 2u) { - /* Lever 3 overflow fallback. byte/half scaled reach (4 KB/8 KB) is - * below the largest frames, so build the address into the reserved - * second word instead: `add x17,base,#(off & ~0xfff) ; ldrb/strb - * rt,[x17,#(off & 0xfff)]`. The high part is a multiple of 4096 (fits - * the shift-12 addsub imm for any frame < 16 MB); the low part is <4096 - * and, for a half, even (base_off is 2-aligned), so both scale-divide + } else { + /* The scaled reach (4 KB/8 KB/16 KB/32 KB by size) is below the largest + * frames, so build the address into the reserved second word instead: + * `add x17,base,#(off & ~0xfff) ; ldr/str rt,[x17,#(off & 0xfff)]`. The + * high part is a multiple of 4096 (fits the shift-12 addsub imm for any + * frame < 16 MB); the low part is <4096 and, being naturally aligned to + * the access size (base_off is, and hi is 4096-aligned), scale-divides * cleanly. */ u32 hi = ubo & ~0xfffu; u32 lo = ubo & 0xfffu; @@ -1868,10 +1873,6 @@ static void aa_apply_patches(AANativeTarget* a, const AAFrameLayout* L) { sl->load ? aa_ldr_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo) : aa_str_uimm_v(sl->sz, sl->vbit, sl->rt, AA_TMP1, lo)); - } else { - /* 4/8-byte scaled reach (16 KB/32 KB) covers any frame, so this is - * unreachable; a one-word patch has no fallback room. */ - aa_panic(a, "far slot offset out of positive scaled range"); } } else { /* AA_PATCH_TAIL */ NativeLoc callee = p->u.callee; diff --git a/test/parse/cases/far_slot_large_frame.c b/test/parse/cases/far_slot_large_frame.c @@ -0,0 +1,32 @@ +/* Far frame slots in a very large (>32KB) frame. `x` (8 bytes) and `y` (4 + * bytes) are declared before the big array, so they get small slot offsets and + * thus large base-relative offsets (near frame_size off sp) -- past the scaled + * ldr/str reach (32KB for 8-byte, 16KB for 4-byte) on aarch64. The far-slot + * fast path must resolve these via the address-build fallback rather than an + * out-of-range scaled immediate (which previously tripped "far slot offset out + * of positive scaled range" compiling src/api/package.c, a ~69KB frame). + * x64/rv64 have no scaled-offset constraint, so this just confirms correctness + * everywhere. Returns the count of passing checks (expect 7). */ + +int test_main(void) { + volatile long x; + volatile int y; + volatile char huge[40000]; + int i; + int ok = 0; + long s = 0; + + x = 0x1122334455667788L; + y = 0x0a0b0c0d; + for (i = 0; i < 40000; i += 997) huge[i] = (char)(i & 0x7f); + + /* Read the far slots back. */ + if (x == 0x1122334455667788L) ok += 3; + if (y == 0x0a0b0c0d) ok += 4; + + /* Keep `huge` live so the big frame is not elided. */ + for (i = 0; i < 40000; i += 997) s += huge[i]; + if (s < 0) ok = -1; + + return ok; +} diff --git a/test/parse/cases/far_slot_large_frame.expected b/test/parse/cases/far_slot_large_frame.expected @@ -0,0 +1 @@ +7