kit

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

commit 99899453fcf7fad2a7ba0eed9e341c78298e86cf
parent 942c3090611c0ac05b3c024bb3a5b8c380863d86
Author: Ryan Sepassi <rsepassi@gmail.com>
Date:   Sat, 13 Jun 2026 17:01:31 -0700

perf(cg): -O0 signed load-with-extend (ldrsb/ldrsh) via a frontend widening signed load (Lever 4)

A signed narrow load was `ldrb/ldrh ; sxtb/sxth` (two insns) where the
aarch64 `ldrsb`/`ldrsh` does it in one. kit's CG integer types are
sign-agnostic (signedness lives in the CV_SEXT convert, not the type), so
`nd_load` cannot know the loaded value feeds a sign-extend and must emit a
zero-extending load. L3 already handled the *unsigned* half (a zero-extending
load fills the register, so a following CV_ZEXT is elided). This adds the
signed mirror: a frontend widening signed load.

Seam (mirror of L3, gated by a backend capability):
- A signedness rider on the load: new MF_SEXT_LOAD bit (internal MemFlag,
  backed by public KIT_CG_MEM_SEXT_LOAD; translated in api_mem_from_access).
- Frontend (pcg_load): a non-bit-field signed byte/short lvalue read sets the
  flag. Such a value is only ever widened by CV_SEXT (pcg_convert emits sext
  for signed sources, zext for unsigned), so the rider is unconditional and
  the L3 zext-elision can never misfire on it.
- aa64 (aa_emit_mem): for an MF_SEXT_LOAD byte/half integer load, emit the
  load with opc=10 (ldrsb/ldrsh into the X register, sign-extending the whole
  register) across every addressing mode. New aa_*_op_v encoder variants carry
  the load opcode; the existing helpers stay plain-LDR wrappers.
- Shared NDT: nd_load records load_sext (instead of load_zext) when the access
  is MF_SEXT_LOAD and the backend advertises the new NativeRegInfo.ndt_load_sext
  capability; nd_convert then drops a CV_SEXT whose source is a load_sext local,
  exactly as it drops a CV_ZEXT after load_zext. Cleared on spill/invalidate/
  overwrite (a plain reload zero-extends, so the real extend runs on reload).

Cross-arch correctness: the capability is on for aa64 (honors the flag) and
off (default) for x86-64/riscv64 — there the frontend flag is a no-op, the
backend emits a plain zero-extending load, and the CV_SEXT convert runs
normally. Other backends (opt recorder, c_target, wasm, check) ignore the bit.

sqlite3.c (-c, aa64-macos), vs base 942c3090, byte-deterministic:
  .text 0x16d170 -> 0x16c478 (-3,832 B, -958 insns, -0.256%)
  sxtb -449, sxth -497 (946 converts elided); ldrsb +421, ldrsh +493,
  ldursb +15 (929 sign-extending loads); ldrb -735, ldrh -586.
  No change to mov/stur/ldur/sub/movk — purely the targeted lever.

Gates: test-toy 1392/0; test-cg-api/test-isa/test-asm/test-opt/test-parse
(3920+129)/0; smoke-x64/smoke-rv64 3/0 (capability-off arches correct).
Differential vs /usr/bin/cc -O0 on signed+unsigned char/short arrays/fields
with most-negative values (-1/-128/-32768), promotion to int/long, sign-
dependent arithmetic and comparisons, and (unsigned char) truncation of a
sign-extended register: MATCH at kit -O0 and -O1.

Diffstat:
Minclude/kit/cg.h | 6++++++
Mlang/c/parse/cg_adapter.c | 9+++++++++
Msrc/arch/aa64/isa.h | 4++++
Msrc/arch/aa64/native.c | 86++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------
Msrc/arch/native_target.h | 8++++++++
Msrc/cg/cgir.h | 10++++++++++
Msrc/cg/native_direct_target.c | 42++++++++++++++++++++++++++++--------------
Msrc/cg/native_direct_target.h | 8++++++++
Msrc/cg/value.c | 1+
9 files changed, 137 insertions(+), 37 deletions(-)

diff --git a/include/kit/cg.h b/include/kit/cg.h @@ -295,6 +295,12 @@ typedef enum KitCgMemAccessFlag { /* Access is an externally observable side effect and must not be merged, * removed, or reordered across other volatile accesses. */ KIT_CG_MEM_VOLATILE = 1u << 0, + /* Codegen hint (loads only): the loaded value is a signed narrow integer that + * the frontend will only ever widen by sign-extension, so the backend is free + * to emit a single sign-extending load and the -O0 path may then elide the + * following sign-extend convert. A backend that does not exploit it just emits + * a plain load; semantics are unchanged. Ignored on stores and bit-fields. */ + KIT_CG_MEM_SEXT_LOAD = 1u << 1, } KitCgMemAccessFlag; typedef struct KitCgMemAccess { diff --git a/lang/c/parse/cg_adapter.c b/lang/c/parse/cg_adapter.c @@ -528,6 +528,15 @@ void pcg_load(Parser* p) { KitCgMemAccess access = pcg_mem_id(p, pcg_top_cg_id(p), ty); /* Snapshot bit-field geometry before materialize clears the aux. */ PcgLvAux bf = lv ? *lv : (PcgLvAux){0}; + /* Widening signed load (Lever 4): a non-bit-field signed byte/short load is + * only ever widened by sign-extension (the integer-promotion CV_SEXT), so + * mark the access so the backend can fold the load + sign-extend into one + * ldrsb/ldrsh and the -O0 path can drop the redundant CV_SEXT. Plain hint; + * a backend that ignores it stays correct. */ + if (bf.bit_width == 0 && type_is_int(ty) && pcg_type_is_signed(ty) && + pcg_sizeof(p, ty) < 4u) { + access.flags |= KIT_CG_MEM_SEXT_LOAD; + } /* Build the PLACE the strict load requires. A trivial local already has its * PLACE on the CG stack (push_local) and loads directly; anything else is * reduced to a single pointer (materialize, or a pointer-rvalue base) and diff --git a/src/arch/aa64/isa.h b/src/arch/aa64/isa.h @@ -751,6 +751,10 @@ static inline int aa64_addsub_imm_fits(i64 imm, u32* imm12_out, u32* sh_out) { #define AA64_LDST_SIZE_64 3u #define AA64_LDST_OPC_STR 0u #define AA64_LDST_OPC_LDR 1u +/* opc=10 -> sign-extending load into the 64-bit X register (LDRSB/LDRSH/LDRSW + * Xt); opc=11 is the 32-bit W form. Codegen uses the X form so a narrow signed + * load fills the whole register (the -O0 widening-signed-load lever). */ +#define AA64_LDST_OPC_LDRS_X 2u #define AA64_LDST_UIMM_FAMILY_MATCH 0x39000000u #define AA64_LDST_UIMM_FAMILY_MASK 0x3B000000u /* bits 29:27 + bits 25:24 */ diff --git a/src/arch/aa64/native.c b/src/arch/aa64/native.c @@ -501,15 +501,22 @@ static void aa_emit_add_i64(AANativeTarget* a, u32 rd, u32 rn, i64 off) { aa_emit32(mc, aa64_add(1, rd, rn, rd)); } -static u32 aa_ldur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) { +/* Unscaled load with an explicit load opcode (AA64_LDST_OPC_LDR for a plain + * zero-extending load, AA64_LDST_OPC_LDRS_X for a sign-extending ldursb/ldursh + * into the X register). */ +static u32 aa_ldur_op_v(u32 size, u32 v, u32 ld_opc, u32 rt, u32 rn, i32 simm9) { return aa64_ldst_simm9_pack((AA64LdStSimm9){.size = size, .V = v, - .opc = AA64_LDST_OPC_LDR, + .opc = ld_opc, .imm9 = (u32)simm9 & 0x1ffu, .Rn = rn, .Rt = rt}); } +static u32 aa_ldur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) { + return aa_ldur_op_v(size, v, AA64_LDST_OPC_LDR, rt, rn, simm9); +} + static u32 aa_stur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) { return aa64_ldst_simm9_pack((AA64LdStSimm9){.size = size, .V = v, @@ -519,16 +526,24 @@ static u32 aa_stur_v(u32 size, u32 v, u32 rt, u32 rn, i32 simm9) { .Rt = rt}); } -static u32 aa_ldr_uimm_v(u32 size, u32 v, u32 rt, u32 rn, u32 byte_off) { +/* Scaled load with an explicit load opcode (see aa_ldur_op_v). The scale shift + * is the access size for plain LDR; a sign-extending narrow load (opc=LDRS_X) + * uses the same byte-size scale (the encoded imm12 is byte_off >> size). */ +static u32 aa_ldr_uimm_op_v(u32 size, u32 v, u32 ld_opc, u32 rt, u32 rn, + u32 byte_off) { u32 sc = byte_off >> size; return aa64_ldst_uimm_pack((AA64LdStUimm){.size = size, .V = v, - .opc = AA64_LDST_OPC_LDR, + .opc = ld_opc, .imm12 = sc, .Rn = rn, .Rt = rt}); } +static u32 aa_ldr_uimm_v(u32 size, u32 v, u32 rt, u32 rn, u32 byte_off) { + return aa_ldr_uimm_op_v(size, v, AA64_LDST_OPC_LDR, rt, rn, byte_off); +} + static u32 aa_str_uimm_v(u32 size, u32 v, u32 rt, u32 rn, u32 byte_off) { u32 sc = byte_off >> size; return aa64_ldst_uimm_pack((AA64LdStUimm){.size = size, @@ -547,12 +562,21 @@ static u32 aa_str_uimm(u32 size, u32 rt, u32 rn, u32 byte_off) { return aa_str_uimm_v(size, 0, rt, rn, byte_off); } +/* Register-offset load/store with an explicit load opcode (ld_opc is consulted + * only when load != 0; AA64_LDST_OPC_LDRS_X gives a sign-extending ldrsb/ldrsh + * into the X register). */ +static u32 aa_ldst_regoff_op_v(u32 size, u32 v, u32 load, u32 ld_opc, u32 rt, + u32 rn, u32 rm, u32 scaled) { + return ((size & 3u) << 30) | 0x38200800u | ((v & 1u) << 26) | + ((load ? ld_opc : AA64_LDST_OPC_STR) << 22) | ((rm & 0x1fu) << 16) | + (3u << 13) | ((scaled & 1u) << 12) | ((rn & 0x1fu) << 5) | + (rt & 0x1fu); +} + static u32 aa_ldst_regoff_v(u32 size, u32 v, u32 load, u32 rt, u32 rn, u32 rm, u32 scaled) { - return ((size & 3u) << 30) | 0x38200800u | ((v & 1u) << 26) | - ((load ? AA64_LDST_OPC_LDR : AA64_LDST_OPC_STR) << 22) | - ((rm & 0x1fu) << 16) | (3u << 13) | ((scaled & 1u) << 12) | - ((rn & 0x1fu) << 5) | (rt & 0x1fu); + return aa_ldst_regoff_op_v(size, v, load, AA64_LDST_OPC_LDR, rt, rn, rm, + scaled); } static u32 aa_mrs_tpidr_el0(u32 rt) { return 0xd53bd040u | (rt & 0x1fu); } @@ -827,7 +851,7 @@ static void aa_emit_mem_q(AANativeTarget* a, int load, NativeLoc reg, static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, NativeAddr addr, MemAccess mem) { - u32 base, rt, sz; + u32 base, rt, sz, ld_opc; i32 off; MCEmitter* mc = a->base.mc; rt = loc_reg(reg); @@ -842,6 +866,15 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, return; } if (native_loc_is_fp(reg) && sz < 2u) sz = 2u; + /* Lever 4: a signed narrow integer load (MF_SEXT_LOAD, byte/half, integer + * register) becomes a sign-extending load into the X register (ldrsb/ldrsh, + * opc=10) so it fills the whole register in one instruction — the cg layer + * then drops the redundant CV_SEXT. Every other load (incl. fp, word, dword) + * keeps the plain zero-extending LDR. */ + ld_opc = (load && (mem.flags & MF_SEXT_LOAD) && !native_loc_is_fp(reg) && + sz <= 1u) + ? AA64_LDST_OPC_LDRS_X + : AA64_LDST_OPC_LDR; /* Far fixed-slot fast path (Lever 1 / Fix B). A plain 4/8-byte frame slot * whose top-record fp offset is past stur's ±256 range would otherwise cost * `sub xN,x29,#off (+movk) ; ldur` (2-3 insns). Instead emit a one-word @@ -883,18 +916,19 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_LD64_GOT_LO12_NC, addr.base.global.sym, 0, 0, 0); if (addend) aa_emit_add_i64(a, scratch, scratch, addend); - aa_emit32(mc, load - ? aa_ldur_v(sz, native_loc_is_fp(reg), rt, scratch, 0) - : aa_stur_v(sz, native_loc_is_fp(reg), rt, scratch, 0)); + aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + scratch, 0) + : aa_stur_v(sz, native_loc_is_fp(reg), rt, scratch, 0)); return; } aa_emit32(mc, aa64_adrp(scratch, 0, 0)); mc_emit_reloc_at(mc, mc->section_id, pos, R_AARCH64_ADR_PREL_PG_HI21, addr.base.global.sym, addend, 0, 0); pos = mc_pos(mc); - aa_emit32(mc, - load ? aa_ldr_uimm_v(sz, native_loc_is_fp(reg), rt, scratch, 0) - : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, scratch, 0)); + aa_emit32(mc, load ? aa_ldr_uimm_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + scratch, 0) + : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, scratch, + 0)); mc_emit_reloc_at(mc, mc->section_id, pos, aa_ldst_reloc_for_size(sz), addr.base.global.sym, addend, 0, 0); return; @@ -916,25 +950,27 @@ static void aa_emit_mem(AANativeTarget* a, int load, NativeLoc reg, } else { aa_panic(a, "unsupported memory address scale"); } - aa_emit32(mc, aa_ldst_regoff_v(sz, native_loc_is_fp(reg), load, rt, - use_base, addr.index.reg, scaled)); + aa_emit32(mc, aa_ldst_regoff_op_v(sz, native_loc_is_fp(reg), load, ld_opc, + rt, use_base, addr.index.reg, scaled)); return; } if (off >= 0 && (((u32)off & ((1u << sz) - 1u)) == 0) && ((u32)off >> sz) <= 0xfffu) { - aa_emit32( - mc, load - ? aa_ldr_uimm_v(sz, native_loc_is_fp(reg), rt, base, (u32)off) - : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, base, (u32)off)); + aa_emit32(mc, load ? aa_ldr_uimm_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + base, (u32)off) + : aa_str_uimm_v(sz, native_loc_is_fp(reg), rt, base, + (u32)off)); return; } if (off >= -256 && off <= 255) { - aa_emit32(mc, load ? aa_ldur_v(sz, native_loc_is_fp(reg), rt, base, off) + aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + base, off) : aa_stur_v(sz, native_loc_is_fp(reg), rt, base, off)); return; } aa_emit_add_imm(a, AA_TMP1, base, off); - aa_emit32(mc, load ? aa_ldur_v(sz, native_loc_is_fp(reg), rt, AA_TMP1, 0) + aa_emit32(mc, load ? aa_ldur_op_v(sz, native_loc_is_fp(reg), ld_opc, rt, + AA_TMP1, 0) : aa_stur_v(sz, native_loc_is_fp(reg), rt, AA_TMP1, 0)); } @@ -4228,6 +4264,10 @@ static const NativeRegInfo aa_reg_info = { /* x0 is clobbered only by calls (div/mul take explicit operands), so a * scalar call result can stay cached in it — see L4 Phase 1 in nd_call. */ .ndt_result_reg_stable = 1u, + /* aa_emit_mem emits ldrsb/ldrsh (opc=10, X form) for an MF_SEXT_LOAD narrow + * integer load, filling the whole register — so the -O0 NDT may drop the + * following CV_SEXT (Lever 4). */ + .ndt_load_sext = 1u, .resolve_name = aa_resolve_name, .asm_operand_reg_ok = aa_asm_operand_reg_ok, .asm_constraint_reg = aa_asm_constraint_reg, diff --git a/src/arch/native_target.h b/src/arch/native_target.h @@ -183,6 +183,14 @@ struct NativeRegInfo { * in it would be silently clobbered before its consumer. */ u8 ndt_result_reg_stable; + /* True when this backend honors MF_SEXT_LOAD by emitting a sign-extending + * load that fills the whole register (aa64 ldrsb/ldrsh -> X), so the shared + * -O0 NDT may record load_sext on the loaded local and elide the following + * CV_SEXT. Clear (the default) on backends whose narrow load zero-extends + * regardless of the flag (x86-64, riscv64 today): there the convert must run, + * so the flag is a no-op and the extend is emitted normally. */ + u8 ndt_load_sext; + /* Map a register name to its (Reg, class). `name` is the raw spelling * ("rax", "x8", "a7"); the caller resolves any Sym to its bytes first so this * stays pool-free. Returns 0 on success, non-zero for a non-register name. */ diff --git a/src/cg/cgir.h b/src/cg/cgir.h @@ -246,6 +246,16 @@ typedef enum MemFlag { MF_READONLY = 1u << 3, MF_WRITEONLY = 1u << 4, MF_UNALIGNED = 1u << 5, + /* Sign-extending narrow load (the C frontend's widening-signed-load lever): + * the value comes from a signed char/short/int lvalue whose only widening is a + * sign-extension to a wider type, so a backend that honors this flag emits a + * single sign-extending load (aa64 ldrsb/ldrsh into the X register, filling + * the whole register) instead of a zero-extending load plus a CV_SEXT convert. + * Non-bitfield integer loads only. A backend that does not honor it ignores + * the bit and emits a plain load; the shared -O0 NDT only elides the following + * CV_SEXT when the backend advertises NativeRegInfo.ndt_load_sext, so leaving + * the convert in place stays correct there. */ + MF_SEXT_LOAD = 1u << 6, } MemFlag; typedef enum AliasKind { diff --git a/src/cg/native_direct_target.c b/src/cg/native_direct_target.c @@ -704,6 +704,7 @@ static void nd_flush_local(NativeDirectTarget* d, CGLocal local) { l->reg = REG_NONE; l->dirty = 0; l->load_zext = 0; + l->load_sext = 0; } /* Drop a cache entry without writing it back, for when a store supersedes the @@ -716,6 +717,7 @@ static void nd_invalidate_local(NativeDirectTarget* d, CGLocal local) { l->reg = REG_NONE; l->dirty = 0; l->load_zext = 0; + l->load_sext = 0; } /* Spill the whole cache to memory and empty it. The list is sorted ascending, @@ -1046,6 +1048,7 @@ static void nd_dst_writeback(NativeDirectTarget* d, Operand dst, NativeLoc dr) { dst.type == l->type && nd_local_cacheable(d, l)) { l->dirty = 1; l->load_zext = 0; /* a fresh value; nd_load re-sets this for narrow loads */ + l->load_sext = 0; /* "" (re-set only by an MF_SEXT_LOAD load) */ d->scratch_used[l->cls] &= ~(1u << dr.v.reg); /* unpin, keep cached */ return; } @@ -1628,15 +1631,23 @@ static void nd_load(CgTarget* t, Operand dst, Operand addr, MemAccess mem) { ND_REQUIRE_NATIVE(d, load, "target does not emit loads"); d->native->load(d->native, reg, naddr, mem); nd_dst_writeback(d, dst, reg); - /* A narrow integer load zero-extends the whole register (ldrb/ldrh/ldr-w - * clear the upper bits), so a subsequent CV_ZEXT that widens this value is a - * no-op. Record it when the result stayed register-resident so nd_convert can - * drop the extend. (nd_dst_writeback just cleared the flag for this local.) */ + /* A narrow integer load fills the whole register: a plain ldrb/ldrh/ldr-w + * zero-extends, and (when the backend advertises ndt_load_sext and the access + * is MF_SEXT_LOAD) a sign-extending ldrsb/ldrsh sign-extends. Either way a + * subsequent matching widen (CV_ZEXT / CV_SEXT) is a no-op, so record which + * one this load produced — register-resident only — so nd_convert can drop the + * extend. (nd_dst_writeback just cleared both flags for this local.) */ if (dst.kind == OPK_LOCAL && size < (u64)t->c->target.ptr_size && nd_class_for_type(d, dst.type) == NATIVE_REG_INT && reg.kind == NATIVE_LOC_REG) { NativeDirectLocal* l = nd_local(d, dst.v.local); - if (l->reg == reg.v.reg) l->load_zext = 1; + if (l->reg == reg.v.reg) { + if ((mem.flags & MF_SEXT_LOAD) && d->reg_info && + d->reg_info->ndt_load_sext) + l->load_sext = 1; + else + l->load_zext = 1; + } } nd_addr_temps_release(d, &temps); } @@ -1943,13 +1954,16 @@ static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) { * collapse to nothing in the arch move/convert path. A class-crossing convert * (GPR<->FPR fmov) is excluded by nd_rename_killed_to_dst's same-class guard * and falls through below. */ - /* A CV_ZEXT whose source came straight from a narrow integer load is a no-op: - * the load already zero-extended the whole register (load_zext). Detect it - * before the rename, which transfers (and would clear) the source's flag. */ - int zext_noop = 0; - if (op == CV_ZEXT && src.kind == OPK_LOCAL) { + /* A CV_ZEXT (resp. CV_SEXT) whose source came straight from a narrow integer + * load is a no-op: the load already zero-extended (resp. sign-extended, via an + * MF_SEXT_LOAD ldrsb/ldrsh that filled the whole register) the value, so the + * widen is redundant for any wider destination. Detect it before the rename, + * which transfers (and would clear) the source's flag. */ + int ext_noop = 0; + if (src.kind == OPK_LOCAL && (op == CV_ZEXT || op == CV_SEXT)) { NativeDirectLocal* sl = nd_local(d, src.v.local); - zext_noop = sl->reg != REG_NONE && sl->load_zext && src.type == sl->type; + u8 has = op == CV_ZEXT ? sl->load_zext : sl->load_sext; + ext_noop = sl->reg != REG_NONE && has && src.type == sl->type; } { Reg r = nd_rename_killed_to_dst(d, dst, src); @@ -1958,9 +1972,9 @@ static void nd_convert(CgTarget* t, ConvKind op, Operand dst, Operand src) { sr = nd_loc_reg(d, src.type, cls, r); dr = nd_loc_reg(d, dst.type, cls, r); /* Renaming the source register to the destination already placed the - * zero-extended value where the result wants it — skip the redundant - * extend; the upper bits are provably clear. */ - if (!zext_noop) { + * extended value where the result wants it — skip the redundant extend; + * the upper bits are provably the correct zero/sign fill. */ + if (!ext_noop) { if (d->native->convert_rr) d->native->convert_rr(d->native, op, native_reg_loc_of(dr), native_reg_loc_of(sr)); diff --git a/src/cg/native_direct_target.h b/src/cg/native_direct_target.h @@ -48,6 +48,14 @@ typedef struct NativeDirectLocal { * (nd_dst_writeback) and when the register is dropped (flush/invalidate). * Meaningful only while reg != REG_NONE. */ u8 load_zext; + /* The cached register holds this local's value SIGN-extended to the full + * register, from an MF_SEXT_LOAD narrow load that the backend honored with a + * sign-extending load (aa64 ldrsb/ldrsh, gated on ndt_load_sext). While set, a + * CV_SEXT widening it to any wider integer is a no-op and is elided by + * nd_convert. Set only in nd_load; cleared exactly like load_zext (a spill + + * plain reload would zero-extend, so clearing it forces the real extend to run + * on the reloaded value — see nd_flush_local / nd_invalidate_local). */ + u8 load_sext; u32 last_use; /* d->use_tick at the most recent cache touch (LRU victim key) */ /* Intrusive doubly-linked list of currently-cached locals, in insertion diff --git a/src/cg/value.c b/src/cg/value.c @@ -280,6 +280,7 @@ MemAccess api_mem_from_access(KitCg* g, const Operand* lv, if (access.align) m.align = access.align; m.addr_space = (u16)access.address_space; if (access.flags & KIT_CG_MEM_VOLATILE) m.flags |= MF_VOLATILE; + if (access.flags & KIT_CG_MEM_SEXT_LOAD) m.flags |= MF_SEXT_LOAD; if (!access.align || (ty && access.align < abi_cg_alignof(g->c->abi, ty))) { m.flags |= MF_UNALIGNED; }