kit

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

native_target.h (52459B)


      1 #ifndef KIT_ARCH_NATIVE_TARGET_H
      2 #define KIT_ARCH_NATIVE_TARGET_H
      3 
      4 #include <string.h>
      5 
      6 #include "arch/mc.h"
      7 #include "cg/cgir.h"
      8 #include "cg/type.h"
      9 #include "core/core.h"
     10 #include "core/slice.h" /* Slice, for resolve_name */
     11 
     12 /* NativeTarget is the physical native-emission contract. It is driven after
     13  * semantic CG has been either direct-lowered by NativeDirectTarget or recorded,
     14  * optimized, machinized, and allocated. It must not speak in semantic CGLocal
     15  * ids except where a descriptor is carried for diagnostics or ABI queries. */
     16 
     17 typedef u32 NativeFrameSlot;
     18 #define NATIVE_FRAME_SLOT_NONE 0u
     19 
     20 typedef enum NativeFrameSlotKind {
     21   NATIVE_FRAME_SLOT_LOCAL,
     22   NATIVE_FRAME_SLOT_PARAM,
     23   NATIVE_FRAME_SLOT_SPILL,
     24   NATIVE_FRAME_SLOT_ALLOCA,
     25   NATIVE_FRAME_SLOT_OUTGOING,
     26   NATIVE_FRAME_SLOT_SAVE,
     27 } NativeFrameSlotKind;
     28 
     29 typedef enum NativeFrameSlotFlag {
     30   NATIVE_FRAME_SLOT_NONE_FLAG = 0,
     31   NATIVE_FRAME_SLOT_ADDR_TAKEN = 1u << 0,
     32   NATIVE_FRAME_SLOT_MEMORY_REQUIRED = 1u << 1,
     33   NATIVE_FRAME_SLOT_FIXED_OFFSET = 1u << 2,
     34   /* A recyclable single-pass temp slot. native_frame_slot_alloc serves such a
     35    * request from the per-(size,align) free list first, before bumping cum_off;
     36    * native_frame_release_slot returns the slot there when its temp dies at a
     37    * statement boundary. Bounds the -O0 frame to max-simultaneous-live temps. */
     38   NATIVE_FRAME_SLOT_TRANSIENT = 1u << 3,
     39 } NativeFrameSlotFlag;
     40 
     41 typedef struct NativeFrameSlotDesc {
     42   KitCgTypeId type;
     43   Sym name;
     44   SrcLoc loc;
     45   u32 size;
     46   u32 align;
     47   i32 fixed_offset;
     48   /* Spill-traffic priority (O1.md W1.0), copied from IRFrameSlot.priority. The
     49    * known-frame emitter (opt_emit_native) presents slot descs to the backend in
     50    * descending-priority order, so the bump allocator (native_frame_slot_alloc)
     51    * gives the hottest spills the smallest final displacement. The backend itself
     52    * ignores this field — it is purely an ordering signal for the emitter. */
     53   u32 priority;
     54   u8 kind; /* NativeFrameSlotKind */
     55   u8 pad;
     56   u16 flags; /* NativeFrameSlotFlag */
     57 } NativeFrameSlotDesc;
     58 
     59 typedef struct NativeKnownFrameDesc {
     60   const NativeFrameSlotDesc* slots;
     61   u32 nslots;
     62   u32 max_outgoing;
     63   u32 align;
     64   /* Callee-saved hard registers the allocator assigned, one bitmask per
     65    * NativeAllocClass (indexed by class id). The backend reserves a save slot
     66    * and emits the prologue save / epilogue restore for each — equivalent to a
     67    * reserve_callee_saves() call, but folded into the known-frame setup so the
     68    * full frame is fixed before the prologue is emitted. NULL / 0 means none. */
     69   const u32* callee_saved_used;
     70   u32 ncallee_classes;
     71   /* Union of the clobber register names of every inline-asm block in the body.
     72    * Inline-asm clobbers are invisible to the operand scan that builds
     73    * callee_saved_used, so the optimizer forwards the raw names here and the
     74    * backend resolves them with its own clobber parser, folding the callee-saved
     75    * ones into its save set (applying its ABI predicate, which excludes the
     76    * frame pointer and keeps any reserved-but-callee-saved scratch such as x64
     77    * rbx). The prologue/epilogue then preserve them, so the asm hook needs no
     78    * per-block spill — which on the known-frame path would request a frame slot
     79    * after the frame is already final. NULL / 0 when the body contains no inline
     80    * asm. */
     81   const Sym* asm_clobbers;
     82   u32 nasm_clobbers;
     83   /* Union of KitCgAsmClobberAbiSet bits over the body's inline-asm blocks: an
     84    * arch-neutral "clobbers the whole caller/callee-saved set" the backend
     85    * expands against its own register file, alongside the named asm_clobbers. */
     86   u32 asm_clobber_abi_sets;
     87   /* Whether the function body contains a dynamic alloca. The backend needs this
     88    * up front (before the body) to decide prologue/epilogue form, since with a
     89    * known frame the slim-epilogue eligibility is settled at func_begin. */
     90   u8 has_alloca;
     91   /* Whether the body has an operation that needs a backend-internal scratch
     92    * spill slot — on aa64, an atomic read-modify-write, whose retry loop spills
     93    * one scratch register. The backend reserves the slot up front so the body
     94    * never grows the frame after the prologue. */
     95   u8 needs_scratch_spill;
     96   /* Whether the function is a leaf — its body contains no call of any kind
     97    * (regular or sibling/tail). A leaf does not clobber the return-address
     98    * register or the stack below sp through a call, so backends can omit the
     99    * saved-frame record entirely (rv64 leaf tier) or skip the stack reservation
    100    * and keep locals in the red zone (x64 SysV red-zone tier) — but ONLY when
    101    * `has_asm` is also clear (see below). Conservatively false whenever any
    102    * IR_CALL is present. */
    103   u8 is_leaf;
    104   /* Whether the body contains an inline-asm block. Inline asm can clobber the
    105    * return-address register (rv64 ra) or write into the red zone / make a call
    106    * (x64) without the optimizer modelling it, so the frame-eliding
    107    * leaf/red-zone tiers must NOT fire when this is set — even for an
    108    * otherwise-leaf function. The single-pass and fat known-frame shapes always
    109    * save the return address and reserve their stack, so they are unaffected. */
    110   u8 has_asm;
    111   /* Whether the body reads its own frame-pointer chain via
    112    * __builtin_frame_address / __builtin_return_address (INTRIN_FRAME_ADDRESS /
    113    * INTRIN_RETURN_ADDRESS). Such a function must keep a valid frame record and
    114    * frame pointer, so the frameless-leaf tier (rv64 slim_prologue, which emits
    115    * no prologue and never anchors s0) must NOT fire. aa64/x64 keep the frame
    116    * record in every prologue shape, so they ignore this flag. */
    117   u8 reads_frame;
    118 } NativeKnownFrameDesc;
    119 
    120 typedef enum NativeAllocClass {
    121   NATIVE_REG_INT,
    122   NATIVE_REG_FP,
    123   NATIVE_REG_VEC,
    124   NATIVE_REG_CLASS_COUNT,
    125 } NativeAllocClass;
    126 
    127 #define NATIVE_MAX_HARD_REGS 32u
    128 
    129 typedef u32 NativeRegMaskSet[NATIVE_REG_CLASS_COUNT];
    130 
    131 typedef enum NativeRegFlag {
    132   NATIVE_REG_NONE = 0,
    133   NATIVE_REG_ALLOCABLE = 1u << 0,
    134   NATIVE_REG_CALLER_SAVED = 1u << 1,
    135   NATIVE_REG_CALLEE_SAVED = 1u << 2,
    136   NATIVE_REG_ARG = 1u << 3,
    137   NATIVE_REG_RET = 1u << 4,
    138   NATIVE_REG_RESERVED = 1u << 5,
    139   NATIVE_REG_TEMP_PREFERRED = 1u << 6,
    140 } NativeRegFlag;
    141 
    142 typedef struct NativePhysRegInfo {
    143   Reg reg;
    144   u8 cls;       /* NativeAllocClass */
    145   u8 abi_index; /* 0xff when not an ordered ABI arg/ret register */
    146   u16 flags;    /* NativeRegFlag */
    147   u16 spill_cost;
    148   u16 copy_cost;
    149 } NativePhysRegInfo;
    150 
    151 typedef struct NativeAllocClassInfo {
    152   u8 cls; /* NativeAllocClass */
    153   u8 pad[3];
    154 
    155   /* NativeDirectTarget (-O0) value-cache register policy. Optimized
    156    * allocation is described independently by NATIVE_REG_ALLOCABLE rows in
    157    * `phys`; neither path may infer its value bank from the other. */
    158   const Reg* ndt_allocable;
    159   u32 ndt_allocable_count;
    160 
    161   /* NativeDirectTarget (-O0) value-cache temporary policy only. Optimized
    162    * emission must not infer operand registers from this list; it owns the
    163    * independently declared, instruction-scoped `emit_temps` bank below. */
    164   const Reg* scratch;
    165   u32 nscratch;
    166 
    167   /* Registers owned exclusively by an -O1 native-emission instruction scope.
    168    * They are operand-facing temporaries, not MIR values and not backend-private
    169    * implementation registers. The set is explicit rather than inherited from
    170    * the -O0 cache's `scratch` policy: those two clients have different
    171    * lifetimes and may require disjoint banks (notably ARM32). */
    172   const Reg* emit_temps;
    173   u32 nemit_temps;
    174 
    175   /* Backend-internal registers that may be loaned specifically while binding
    176    * a register-only inline-asm block. They are otherwise unavailable to the
    177    * optimizer emitter. The asm staging planner uses them only when the block
    178    * has no memory constraint (whose address binder may need the same bank). */
    179   const Reg* asm_temps;
    180   u32 nasm_temps;
    181 
    182   /* Ordered register policy for operands self-allocated by the O0 direct
    183    * inline-asm binder. This is distinct from ndt_allocable and scratch: values
    184    * live for the complete asm block, and the order is an architecture policy.
    185    * Explicit hard-register pins are validated separately and need not appear
    186    * here. Selection itself is shared in cg/native_asm.c. */
    187   const Reg* direct_asm_allocable;
    188   u32 ndirect_asm_allocable;
    189 
    190   /* Subset of reserved optimizer emitter temps whose contents survive ordinary
    191    * backend hooks when they are not passed as operands or named by a clobber
    192    * effect. The native emitter may retain clean FS_SPILL values in these and
    193    * in dead O1 allocation registers whose ABI preservation is established
    194    * (caller-saved, or a callee-saved register present in the known frame)
    195    * across adjacent instructions; O1 allocation-register preservation is
    196    * governed by machine_op_clobbers below. */
    197   u32 emit_cache_mask;
    198 
    199   const NativePhysRegInfo* phys;
    200   u32 nphys;
    201 } NativeAllocClassInfo;
    202 
    203 typedef struct NativeRegInfo NativeRegInfo;
    204 struct NativeRegInfo {
    205   const NativeAllocClassInfo* classes;
    206   u32 nclasses;
    207 
    208   /* True when this register model's scratch + caller-saved NDT cache pool is
    209    * rich enough that the single-pass (-O0) NativeDirectTarget never needs to
    210    * fall back to a callee-saved register under pressure. When set, NDT is
    211    * enforced caller-saved-only (nd_scratch_acquire skips callee-saved regs;
    212    * nd_func_end asserts none were used), which lets the backend reserve only a
    213    * tiny tcc-style deferred-`sub` prologue region instead of a worst-case
    214    * callee-save area. When clear (the default), NDT keeps its historical
    215    * behavior: it may use callee-saved registers as extra scratch and the
    216    * prologue preserves them. Independent of the optimizer/-O1 path, which always
    217    * has full callee-save freedom. */
    218   u8 ndt_caller_saved_only;
    219 
    220   /* True when the ABI scalar-result register is clobbered ONLY at call
    221    * boundaries (where the -O0 cache already flushes), so a scalar call result
    222    * can be left cached in it across following ops instead of being moved into a
    223    * general cache register (saving the post-call `mov cachereg, retreg`). Set on
    224    * aarch64 (x0) and riscv64 (a0), whose div/mul/etc. take explicit operands.
    225    * Clear on x86-64, where RAX is an implicit div/mul operand and a result left
    226    * in it would be silently clobbered before its consumer. */
    227   u8 ndt_result_reg_stable;
    228 
    229   /* True when this backend honors MF_SEXT_LOAD by emitting a sign-extending
    230    * load that fills the whole register (aa64 ldrsb/ldrsh -> X), so the shared
    231    * Native codegen may record load_sext on the loaded local and elide the following
    232    * CV_SEXT. Clear (the default) on backends whose narrow load zero-extends
    233    * regardless of the flag (x86-64, riscv64 today): there the convert must run,
    234    * so the flag is a no-op and the extend is emitted normally. */
    235   u8 load_sext;
    236 
    237   /* Map a register name to its (Reg, class). `name` is the raw spelling
    238    * ("rax", "x8", "a7"); the caller resolves any Sym to its bytes first so this
    239    * stays pool-free. Returns 0 on success, non-zero for a non-register name. */
    240   int (*resolve_name)(const NativeRegInfo*, Slice name, Reg* out,
    241                       NativeAllocClass* cls_out);
    242   /* True when (cls, reg) is a valid hard-register home for an inline-asm value
    243    * operand. This is intentionally separate from allocator availability:
    244    * syscall idioms need ABI registers such as x8/a7, while stack/frame, zero,
    245    * link, platform, and backend scratch registers must stay unavailable even if
    246    * the assembler can name them. */
    247   int (*asm_operand_reg_ok)(const NativeRegInfo*, NativeAllocClass cls,
    248                             Reg reg);
    249   /* Optional target-specific register-constraint parser for inline asm. The
    250    * input is the constraint body after generic modifiers ('=', '+', '&') have
    251    * been stripped. Return non-zero only for constraints that name a register
    252    * class; set fixed_out to REG_NONE for a free class or to a physical register
    253    * when the constraint hard-wires the operand (x86 "a" -> rax). Set
    254    * allowed_mask_out to 0 for the whole class, or a physical-register bitmask
    255    * when the constraint names a restricted class subset. */
    256   int (*asm_constraint_reg)(const NativeRegInfo*, const char* body,
    257                             NativeAllocClass* cls_out, Reg* fixed_out,
    258                             u32* allowed_mask_out);
    259   const char* (*debug_name)(const NativeRegInfo*, NativeAllocClass, Reg);
    260   u32 (*dwarf_reg)(const NativeRegInfo*, NativeAllocClass, Reg);
    261 };
    262 
    263 /* Validate the target-independent NativeRegInfo contract. Both the direct and
    264  * optimized adapters call this at their boundary, so malformed physical rows,
    265  * role flags, or temporary banks fail identically at O0 and O1. */
    266 void native_reg_info_validate(Compiler* c, const NativeRegInfo* ri);
    267 
    268 /* Classes are identified by NativeAllocClass, not by their position in the
    269  * target's table. Keep the lookup policy shared by every register consumer. */
    270 const NativeAllocClassInfo* native_reg_info_class_info(
    271     const NativeRegInfo* ri, NativeAllocClass cls);
    272 
    273 /* Static register roles have one authority: NativePhysRegInfo.flags. Targets
    274  * with an OS-dependent preservation ABI override caller/callee masks through
    275  * NativeTarget below; all other role consumers derive their masks here. */
    276 u32 native_reg_info_flag_mask(const NativeRegInfo* ri, NativeAllocClass cls,
    277                               u16 flag);
    278 
    279 typedef enum NativeLocKind {
    280   NATIVE_LOC_NONE,
    281   NATIVE_LOC_REG,
    282   NATIVE_LOC_FRAME,
    283   NATIVE_LOC_STACK,
    284   NATIVE_LOC_IMM,
    285   NATIVE_LOC_GLOBAL,
    286   /* Address value of a concrete frame slot. Distinct from NATIVE_LOC_FRAME,
    287    * which is the scalar value stored in the slot, and NATIVE_LOC_ADDR, which
    288    * is the scalar value addressed by an effective-address expression. */
    289   NATIVE_LOC_FRAME_ADDR,
    290   NATIVE_LOC_ADDR,
    291 } NativeLocKind;
    292 
    293 typedef enum NativeAddrBaseKind {
    294   NATIVE_ADDR_BASE_NONE,
    295   /* Pointer value already held in a physical register. */
    296   NATIVE_ADDR_BASE_REG,
    297   /* Address of the frame slot itself: FP + slot offset. */
    298   NATIVE_ADDR_BASE_FRAME,
    299   /* Scalar pointer value stored in a frame slot. base_type is mandatory and
    300    * is the exact value type, independent of the colored slot descriptor. */
    301   NATIVE_ADDR_BASE_FRAME_VALUE,
    302   NATIVE_ADDR_BASE_GLOBAL,
    303 } NativeAddrBaseKind;
    304 
    305 typedef enum NativeAddrIndexKind {
    306   NATIVE_ADDR_INDEX_NONE,
    307   NATIVE_ADDR_INDEX_REG,
    308   /* Scalar index value stored in a frame slot. index_type is mandatory and is
    309    * the exact value type, independent of the colored slot descriptor. */
    310   NATIVE_ADDR_INDEX_FRAME_VALUE,
    311 } NativeAddrIndexKind;
    312 
    313 typedef enum NativeImmUse {
    314   NATIVE_IMM_MOVE,
    315   NATIVE_IMM_BINOP,
    316   NATIVE_IMM_CMP,
    317   NATIVE_IMM_ADDR_OFFSET,
    318 } NativeImmUse;
    319 
    320 /* Addressing-mode index extend (O1-PATTERNS L8). A backend that advertises
    321  * can_fold_extend_into_addr may receive a NATIVE_ADDR_INDEX_REG whose register
    322  * names a 32-bit value the addressing mode widens to 64 bits with this extend
    323  * (aa64 `[Xbase, Wm, sxtw/uxtw #log2_scale]`). NONE = full-width index, the
    324  * default; every other backend only ever sees NONE. */
    325 typedef enum NativeAddrIndexExt {
    326   NATIVE_ADDR_IDX_EXT_NONE = 0,
    327   NATIVE_ADDR_IDX_EXT_SXTW = 1,
    328   NATIVE_ADDR_IDX_EXT_UXTW = 2,
    329 } NativeAddrIndexExt;
    330 
    331 typedef struct NativeAddr {
    332   u8 base_kind;  /* NativeAddrBaseKind */
    333   u8 cls;        /* NativeAllocClass for base value */
    334   u8 index_kind; /* NativeAddrIndexKind */
    335   u8 index_cls;  /* NativeAllocClass for index value */
    336   u8 log2_scale;
    337   u8 index_ext; /* NativeAddrIndexExt */
    338   u8 pad[2];
    339   KitCgTypeId base_type;
    340   KitCgTypeId index_type;
    341   union {
    342     Reg reg;
    343     NativeFrameSlot frame;
    344     struct {
    345       ObjSymId sym;
    346       i64 addend;
    347     } global;
    348   } base;
    349   union {
    350     Reg reg;
    351     NativeFrameSlot frame;
    352   } index;
    353   i32 offset;
    354 } NativeAddr;
    355 
    356 typedef struct NativeLoc {
    357   u8 kind; /* NativeLocKind */
    358   u8 cls;  /* NativeAllocClass for register-like locations */
    359   /* Cached scalar-width descriptor, stamped once by the NDT at the
    360    * register-NativeLoc choke point (nd_loc_reg) so the per-arch binop/move/
    361    * convert/cmp hot path reads a byte instead of re-crossing the type bridge
    362    * via cg_type_size on `type`. Layout:
    363    *   bit0      SZINFO_VALID — set => the size field is authoritative
    364    *   bits 1..3 SZINFO_SIZE_LOG2 — log2(byte_size) clamped: 0=1,1=2,2=4,3=8,4=16
    365    * Each arch derives its own `is64` predicate from this cached size plus its
    366    * pointer/xlen rule (so the rv32 4-byte-pointer and i128 distinctions stay
    367    * correct per-arch) — the descriptor caches only the size, never a baked
    368    * width class. Left 0 (invalid) for cold locs (immediates, memory, unstamped
    369    * producers); the arch then falls back to the live cg_type_size query, so
    370    * partial adoption stays byte-identical. */
    371   u8 szinfo;
    372   /* L7 shift rider (O1-PATTERNS): when nonzero on a register NativeLoc passed
    373    * as the second source of a binop, the value is pre-shifted left by `shift`
    374    * (1..4) and the backend emits the shifted-register ALU form
    375    * (`add xD,xA,xS,lsl #shift`). 0 = use the register as-is. Only stamped when
    376    * the target advertised can_fold_shift_into_alu, so a backend without the
    377    * capability never sees it. */
    378   u8 shift;
    379   KitCgTypeId type;
    380   union {
    381     Reg reg;
    382     NativeFrameSlot frame;
    383     struct {
    384       NativeFrameSlot slot;
    385       i32 offset;
    386     } stack;
    387     i64 imm;
    388     struct {
    389       ObjSymId sym;
    390       i64 addend;
    391     } global;
    392     NativeAddr addr;
    393   } v;
    394 } NativeLoc;
    395 
    396 /* Narrow register-only operand descriptor for the -O0 NativeDirectTarget hot
    397  * path. NDT resolves every post-materialization binop/move/cmp/convert operand
    398  * to a hard register (or, for an arithmetic/compare RHS, a target-legal
    399  * immediate), then crosses into the per-arch NativeTarget. The fat 48 B
    400  * NativeLoc carries a union wide enough for memory/global/addr forms that these
    401  * ops never receive; passing it by value copies 48 B per operand (144 B/op)
    402  * only for the arch to unwrap a register number + the cached width. NativeRegLoc
    403  * carries exactly the fields the arch reads — register/immediate, class, the
    404  * §E.2 cached width descriptor, and the type for the cold width-fallback — in
    405  * 16 B. The narrow *_rr hooks below take it; they are byte-identical to the fat
    406  * hooks because native_loc_from_reg reconstructs the same NativeLoc the fat path
    407  * would have received. */
    408 typedef struct NativeRegLoc {
    409   u8 cls;    /* NativeAllocClass for the register/value */
    410   u8 szinfo; /* cached scalar-width descriptor, same encoding as NativeLoc */
    411   u8 is_imm; /* 1 => v.imm holds a target-legal immediate (RHS only); else reg */
    412   u8 pad;
    413   KitCgTypeId type;
    414   union {
    415     Reg reg;
    416     i64 imm;
    417   } v;
    418 } NativeRegLoc;
    419 
    420 typedef struct NativeInst NativeInst;
    421 
    422 typedef enum NativePatchKind {
    423   NATIVE_PATCH_FRAME_SIZE,
    424   NATIVE_PATCH_MAX_OUTGOING,
    425   NATIVE_PATCH_ARCH = 0x1000,
    426 } NativePatchKind;
    427 
    428 typedef struct NativePatch {
    429   u32 kind; /* NativePatchKind or arch-private */
    430   u32 section_id;
    431   u32 offset;
    432   u32 width;
    433   i64 addend;
    434   u64 value;
    435 } NativePatch;
    436 
    437 typedef struct NativeFramePatchState {
    438   u32 max_outgoing;
    439   u32 max_align;
    440 } NativeFramePatchState;
    441 
    442 /* A semantic machine operation, enough for the target to report the physical
    443  * registers its encoding clobbers as a side effect (e.g. x86 idiv writes
    444  * rax/rdx, variable shifts use cl, and a bitfield read-modify-write borrows
    445  * fixed temporaries). Built by the optimizer from an instruction; the
    446  * descriptor keeps the backend from depending on the optimizer IR. */
    447 typedef enum NativeMachineOpKind {
    448   NATIVE_MOP_BINOP,
    449   NATIVE_MOP_BITFIELD_LOAD,
    450   NATIVE_MOP_BITFIELD_STORE,
    451   NATIVE_MOP_VA_START,
    452   NATIVE_MOP_VA_ARG,
    453   NATIVE_MOP_ATOMIC_CAS,
    454   NATIVE_MOP_ATOMIC_RMW,
    455   NATIVE_MOP_INTRINSIC,
    456   /* A thread-local address materialization (IR_TLS_ADDR_OF). On targets whose
    457    * TLS access model uses fixed scratch/result registers or a resolver-thunk
    458    * call (e.g. Mach-O TLV descriptors → x0/x16/x17/lr), the encoding clobbers
    459    * those regs even though the IR op only declares its destination. Targets
    460    * whose TLS sequence touches only the destination register (ELF Local-Exec)
    461    * report no clobbers. */
    462   NATIVE_MOP_TLS_ADDR,
    463   NATIVE_MOP_COUNT,
    464 } NativeMachineOpKind;
    465 
    466 typedef struct NativeMachineOp {
    467   u8 kind;          /* NativeMachineOpKind */
    468   u8 binop;         /* BinOp, when kind == NATIVE_MOP_BINOP */
    469   u8 intrin;        /* IntrinKind, when kind == NATIVE_MOP_INTRINSIC */
    470   u8 second_is_reg; /* binop's second operand is a register (not an immediate)
    471                      */
    472   u8 result_is_fp;  /* result lands in an FP register (e.g. va_arg of a double)
    473                      */
    474 } NativeMachineOp;
    475 
    476 typedef struct NativeCallDesc {
    477   KitCgTypeId fn_type;
    478   NativeLoc callee;
    479   /* Each argument is a complete semantic value. REG/IMM carry scalar bits;
    480    * FRAME/STACK/ADDR name dereferenceable storage; GLOBAL/FRAME_ADDR are
    481    * scalar address values and must classify as one pointer-sized ABI part.
    482    * Backends use native_loc_addr_role plus the checked converters below rather
    483    * than inferring value-vs-storage semantics from union fields. */
    484   const NativeLoc* args;
    485   const NativeLoc* results;
    486   u32 nargs;
    487   u32 nresults;
    488   u16 flags;      /* CGCallFlag */
    489   u8 tail_policy; /* KitCgTailPolicy */
    490   u8 pad;
    491   KitCgInlinePolicy inline_policy;
    492 } NativeCallDesc;
    493 
    494 typedef enum NativeCallPhaseMoveKind {
    495   NATIVE_CALL_MOVE_NONE,
    496   NATIVE_CALL_MOVE_VALUE,
    497   NATIVE_CALL_MOVE_ADDR,
    498 } NativeCallPhaseMoveKind;
    499 
    500 typedef struct NativeCallPhaseMove {
    501   NativeLoc src;
    502   NativeLoc dst;
    503   MemAccess mem;
    504   u8 src_kind; /* NativeCallPhaseMoveKind */
    505   u8 dst_kind; /* NativeLocKind */
    506   u8 pad[2];
    507 } NativeCallPhaseMove;
    508 
    509 typedef struct NativeCallPhaseRet {
    510   NativeLoc src;
    511   NativeLoc dst;
    512   MemAccess mem;
    513 } NativeCallPhaseRet;
    514 
    515 typedef struct NativeCallPhase {
    516   NativeLoc callee;
    517   NativeCallPhaseMove* args;
    518   NativeCallPhaseRet* rets;
    519   u32 nargs;
    520   u32 nrets;
    521   u32 stack_arg_size;
    522   u16 flags; /* CGCallFlag */
    523   u8 has_sret;
    524   u8 is_variadic;
    525 } NativeCallPhase;
    526 
    527 typedef struct NativeTarget NativeTarget;
    528 struct NativeTarget {
    529   Compiler* c;
    530   ObjBuilder* obj;
    531   MCEmitter* mc;
    532   const NativeRegInfo* regs;
    533   u64 disabled_backend_features;
    534 
    535   NativeAllocClass (*class_for_type)(NativeTarget*, KitCgTypeId);
    536   int (*imm_legal)(NativeTarget*, NativeImmUse, u32 op, KitCgTypeId, i64);
    537   int (*addr_legal)(NativeTarget*, const NativeAddr*, MemAccess);
    538   /* Optional O1-PATTERNS rider capabilities. The target-agnostic recognition
    539    * passes (pass_combine / pass_addr_fold) only stamp an operand rider after
    540    * the consuming backend advertises the matching capability here, so a
    541    * backend that cannot emit the folded form never receives a rider it would
    542    * have to silently drop. NULL == not supported (the default for every
    543    * backend that has not opted in).
    544    *
    545    * can_fold_shift_into_alu (L7): the backend emits a single-use left shift
    546    * folded into a consuming IADD/ISUB/AND/ORR/EOR as the shifted-register form
    547    * (aa64 `add xD,xA,xS,lsl #k`), honoring OptOperand.shift on a binop's
    548    * second source operand.
    549    *
    550    * can_fold_extend_into_addr (L8): the backend emits a single-use sxtw/uxtw
    551    * of a load/store index folded into the addressing mode (aa64 `[Xb, Wm,
    552    * sxtw/uxtw #scale]`), honoring NativeAddr.index_ext. */
    553   int (*can_fold_shift_into_alu)(NativeTarget*);
    554   int (*can_fold_extend_into_addr)(NativeTarget*);
    555   /* Optional, but exhaustive when present. Report every optimizer-visible
    556    * physical register the target's encoding of `op` clobbers as a side effect
    557    * (not its declared operands/results), one bitmask per NativeAllocClass.
    558    * This includes registers that can be MIR hard homes and cache-enabled emit
    559    * temps whose retained spill value must be invalidated. Pure backend-private
    560    * temporaries are reserved, never cached, and do not appear here. This single
    561    * contract lets regalloc keep live values out of fixed clobbers and lets the
    562    * emitter retain clean spill values safely. Examples include x86 idiv
    563    * (rax/rdx), variable shift (rcx), bitfield RMW and atomics (rax/rcx/rdx),
    564    * and format-dependent TLS sequences. Return non-zero if any register is
    565    * clobbered, 0 otherwise. NULL promises no optimizer-visible fixed
    566    * clobbers. */
    567   int (*machine_op_clobbers)(NativeTarget*, const NativeMachineOp* op,
    568                              NativeRegMaskSet clobber_mask);
    569 
    570   void (*func_begin)(NativeTarget*, const CGFuncDesc*);
    571   void (*func_begin_known_frame)(NativeTarget*, const CGFuncDesc*,
    572                                  const NativeKnownFrameDesc*,
    573                                  NativeFrameSlot* out_slots);
    574   void (*note_frame_state)(NativeTarget*, const NativeFramePatchState*);
    575   /* Optional. Called once after func_begin and before frame-slot mapping, with
    576    * the set of callee-saved hard registers the allocator assigned (one bitmask
    577    * per NativeAllocClass, indexed by class id). The target reserves save slots
    578    * and emits the prologue save / epilogue restore for each. Register
    579    * allocation is complete before emission, so the caller knows the full set
    580    * up front. */
    581   void (*reserve_callee_saves)(NativeTarget*, const u32* used_by_class,
    582                                u32 nclasses);
    583   /* Optional live-ABI caller/callee-saved register masks for a class. Static
    584    * NativeAllocClassInfo masks describe the target register file, but some
    585    * targets vary preservation rules by OS ABI (x64 SysV vs Win64 XMM regs).
    586    * The optimizer and direct emission use these to keep allocation, call
    587    * clobbers, and prologue save sets aligned with the selected ABI. NULL falls
    588    * back to NativeAllocClassInfo.{caller,callee}_saved_mask. */
    589   u32 (*caller_saved_mask)(NativeTarget*, NativeAllocClass);
    590   u32 (*callee_saved_mask)(NativeTarget*, NativeAllocClass);
    591   /* Bytes of stack-passed arguments the fixed parameters of this function
    592    * signature use (the part beyond the register arg pools). Sets *variadic to
    593    * whether the signature is variadic and *nparams to the fixed parameter
    594    * count. Used to decide tail-call (sibling) realizability: the callee's
    595    * outgoing stack args must fit the area the caller itself received. Either
    596    * out-pointer may be NULL. May itself be NULL. */
    597   u32 (*signature_stack_bytes)(NativeTarget*, KitCgTypeId fn_type,
    598                                int* variadic, u32* nparams);
    599   /* Pure query: the outgoing stack-argument bytes a call with this descriptor
    600    * uses, rounded to the ABI's outgoing-area alignment. Reads only fn_type,
    601    * flags, nargs, and each args[i].type — never argument *locations* — so the
    602    * optimizer can call it in a frame-planning pre-pass, before any argument
    603    * marshalling is emitted, to size the outgoing area. Must equal the
    604    * stack_arg_size marshal_call computes for the same descriptor. May be NULL. */
    605   u32 (*call_stack_bytes)(NativeTarget*, const NativeCallDesc*);
    606   /* Integer hardware zero register, if the ISA has one (aa64 wzr/xzr, rv64
    607    * x0). When `has_store_zero_reg` is set, the emit path stores a constant 0
    608    * straight from `store_zero_reg` instead of materializing 0 into a scratch
    609    * with a mov/movz first. */
    610   u8 has_store_zero_reg;
    611   Reg store_zero_reg;
    612   void (*func_end)(NativeTarget*);
    613 
    614   NativeFrameSlot (*frame_slot)(NativeTarget*, const NativeFrameSlotDesc*);
    615   /* Optional. Return a recyclable single-pass temp slot (one previously handed
    616    * out for a NATIVE_FRAME_SLOT_TRANSIENT desc) to the frame's free list so a
    617    * later transient allocation of the same (size,align) reuses it instead of
    618    * growing the frame. Called by NativeDirectTarget's reclaim_temps at statement
    619    * boundaries. NULL means the backend does not recycle (frame still correct,
    620    * just larger). */
    621   void (*release_frame_slot)(NativeTarget*, NativeFrameSlot);
    622   /* Optional post-finalization query for a native frame slot's debug location.
    623    * Each arch owns the frame layout math and returns the coordinate system its
    624    * debugger/unwinder path can materialize. */
    625   int (*frame_slot_debug_loc)(NativeTarget*, NativeFrameSlot, CGDebugLoc*);
    626   /* Place the incoming parameter into `dst`. The caller (which has run register
    627    * allocation) chooses the destination: a hard register (NATIVE_LOC_REG) for a
    628    * register-allocated scalar param, a frame slot (NATIVE_LOC_FRAME) for an
    629    * address-taken / spilled / aggregate param. NATIVE_LOC_NONE means the param
    630    * is unused and only the ABI register/stack cursor must advance. Incoming arg
    631    * registers are never allocable, so reg destinations never alias an incoming
    632    * arg register and ordering across params is unconstrained. */
    633   void (*bind_param)(NativeTarget*, const CGParamDesc*, NativeLoc dst);
    634   /* Optional. Called once by the optimizer emit path after the last bind_param,
    635    * before the body. Lets a backend that defers register-destination param
    636    * binds (to resolve them as a parallel copy, since the allocator may rotate
    637    * params across the incoming arg registers — a permutation the naive
    638    * per-param move order cannot realize) flush them now. Backends that bind
    639    * eagerly leave this NULL. */
    640   void (*bind_params_end)(NativeTarget*);
    641 
    642   MCLabel (*label_new)(NativeTarget*);
    643   void (*label_place)(NativeTarget*, MCLabel);
    644   void (*jump)(NativeTarget*, MCLabel);
    645   void (*cmp_branch)(NativeTarget*, CmpOp, NativeLoc a, NativeLoc b,
    646                      MCLabel target);
    647   void (*indirect_branch)(NativeTarget*, NativeLoc addr,
    648                           const MCLabel* valid_targets, u32 ntargets);
    649   void (*load_label_addr)(NativeTarget*, NativeLoc dst, MCLabel target);
    650 
    651   void (*emit)(NativeTarget*, const NativeInst*);
    652   /* All instruction-emission hooks require caller-selected legal physical
    653    * value operands. In particular, dst values are NATIVE_LOC_REG and
    654    * arithmetic sources are NATIVE_LOC_REG or target-legal immediates.
    655    * NativeAddr may retain typed FRAME_VALUE base/index components; the backend
    656    * materializes those exact-width values only through its declared internal
    657    * registers. It must not allocate or expose a new IR-visible register. */
    658   void (*move)(NativeTarget*, NativeLoc dst_reg, NativeLoc src_reg);
    659   void (*load_imm)(NativeTarget*, NativeLoc dst_reg, i64 imm);
    660   void (*load_const)(NativeTarget*, NativeLoc dst_reg, ConstBytes);
    661   /* Address materialization must accept dst_reg aliasing a BASE_REG component
    662    * while still consuming any distinct index before the destination write.
    663    * The emitter never requests dst_reg aliasing the index itself. */
    664   void (*load_addr)(NativeTarget*, NativeLoc dst_reg, NativeAddr addr);
    665   /* A one-register scalar load from a plain BASE_REG address must accept
    666    * dst_reg aliasing that base: the effective address is consumed before the
    667    * destination is written. This lets the target-neutral emitter reuse an
    668    * instruction-leased address register under maximal temporary pressure. */
    669   void (*load)(NativeTarget*, NativeLoc dst_reg, NativeAddr addr, MemAccess);
    670   void (*store)(NativeTarget*, NativeAddr addr, NativeLoc src_reg, MemAccess);
    671   void (*tls_addr_of)(NativeTarget*, NativeLoc dst_reg, ObjSymId sym,
    672                       i64 addend);
    673   void (*copy_bytes)(NativeTarget*, NativeAddr dst, NativeAddr src,
    674                      AggregateAccess);
    675   void (*set_bytes)(NativeTarget*, NativeAddr dst, NativeLoc byte_value,
    676                     AggregateAccess);
    677   /* Like load(), a one-register bitfield load must consume a plain BASE_REG
    678    * address before writing dst_reg and accept those registers aliasing. */
    679   void (*bitfield_load)(NativeTarget*, NativeLoc dst_reg,
    680                         NativeAddr record_addr, BitFieldAccess);
    681   void (*bitfield_store)(NativeTarget*, NativeAddr record_addr,
    682                          NativeLoc src_reg, BitFieldAccess);
    683   void (*binop)(NativeTarget*, BinOp, NativeLoc dst_reg, NativeLoc a_reg,
    684                 NativeLoc b_reg_or_imm);
    685   void (*unop)(NativeTarget*, UnOp, NativeLoc dst_reg, NativeLoc src_reg);
    686   void (*cmp)(NativeTarget*, CmpOp, NativeLoc dst_reg, NativeLoc a_reg,
    687               NativeLoc b_reg_or_imm);
    688   void (*convert)(NativeTarget*, ConvKind, NativeLoc dst_reg,
    689                   NativeLoc src_reg);
    690   /* Narrow register-only entry points for the -O0 NativeDirectTarget hot path
    691    * (§E.3). Same semantics and byte output as the fat binop/move/cmp/convert,
    692    * but the operands cross as 16 B NativeRegLoc instead of 48 B NativeLoc. NDT
    693    * calls these when installed (always, for the native arch backends); the
    694    * fat hooks above remain for the opt/-O1 replay path, which is unchanged.
    695    * Optional: a backend that leaves them NULL keeps the fat path. */
    696   void (*binop_rr)(NativeTarget*, BinOp, NativeRegLoc dst, NativeRegLoc a,
    697                    NativeRegLoc b_reg_or_imm);
    698   void (*move_rr)(NativeTarget*, NativeRegLoc dst, NativeRegLoc src);
    699   void (*cmp_rr)(NativeTarget*, CmpOp, NativeRegLoc dst, NativeRegLoc a,
    700                  NativeRegLoc b_reg_or_imm);
    701   void (*convert_rr)(NativeTarget*, ConvKind, NativeRegLoc dst,
    702                      NativeRegLoc src);
    703   void (*alloca_)(NativeTarget*, NativeLoc dst_reg, NativeLoc size_reg,
    704                   u32 align);
    705 
    706   void (*spill)(NativeTarget*, NativeLoc src_reg, NativeFrameSlot, MemAccess);
    707   void (*reload)(NativeTarget*, NativeLoc dst_reg, NativeFrameSlot, MemAccess);
    708 
    709   /* Mutating ABI-marshalling phase. It may stage the callee, emit stack
    710    * arguments and register shuffles, and fill any remaining generic
    711    * moves/results in NativeCallPhase. The phase owns every populated ABI
    712    * argument destination and any staged callee until emit_call consumes it. */
    713   void (*marshal_call)(NativeTarget*, const NativeCallDesc*, NativeCallPhase*);
    714   void (*emit_call)(NativeTarget*, const NativeCallPhase*);
    715   /* The corresponding mutating return-marshalling phase. `value` is the
    716    * single returned local's location, or NULL for void; the hook may emit an
    717    * indirect-result copy and returns the ordered register moves in
    718    * out_rets/out_nrets. Completed destinations remain live through the phase. */
    719   void (*marshal_ret)(NativeTarget*, const CGFuncDesc*, const NativeLoc* value,
    720                       NativeCallPhaseRet** out_rets, u32* out_nrets);
    721   void (*ret)(NativeTarget*);
    722 
    723   /* Like load(), a one-register atomic load must consume a plain BASE_REG
    724    * address before writing dst and accept those registers aliasing. Backends
    725    * already materialize any stronger atomic addressing constraint into their
    726    * private scratch before the value load. */
    727   void (*atomic_load)(NativeTarget*, NativeLoc dst, NativeAddr addr, MemAccess,
    728                       KitCgMemOrder);
    729   void (*atomic_store)(NativeTarget*, NativeAddr addr, NativeLoc src, MemAccess,
    730                        KitCgMemOrder);
    731   void (*atomic_rmw)(NativeTarget*, KitCgAtomicOp, NativeLoc dst,
    732                      NativeAddr addr, NativeLoc val, MemAccess, KitCgMemOrder);
    733   void (*atomic_cas)(NativeTarget*, NativeLoc prior, NativeLoc ok,
    734                      NativeAddr addr, NativeLoc expected, NativeLoc desired,
    735                      MemAccess, KitCgMemOrder success, KitCgMemOrder failure);
    736   void (*fence)(NativeTarget*, KitCgMemOrder);
    737   /* Variadic support. The optimizer passes the va_list pointer opaquely as a
    738    * NativeLoc (a register or memory location holding the address of the
    739    * va_list object); va_arg additionally receives the argument type and a
    740    * destination location for the fetched value. All va_list layout knowledge
    741    * (pointer ABI vs register-save-area ABI, field offsets, sizes) lives behind
    742    * these hooks, which query the target ABI -- the optimizer makes no layout
    743    * assumptions. */
    744   void (*va_start_)(NativeTarget*, NativeLoc ap_ptr);
    745   void (*va_arg_)(NativeTarget*, NativeLoc dst, NativeLoc ap_ptr,
    746                   KitCgTypeId type);
    747   void (*va_end_)(NativeTarget*, NativeLoc ap_ptr);
    748   void (*va_copy_)(NativeTarget*, NativeLoc dst_ap_ptr, NativeLoc src_ap_ptr);
    749   /* Value arguments reach the backend as NATIVE_LOC_REG unless the backend
    750    * explicitly accepts an immediate for that intrinsic operand. The shared
    751    * target-sequence/control operands recognized by
    752    * native_intrinsic_arg_accepts_imm below also remain NATIVE_LOC_IMM when the
    753    * IR supplied a constant. Keeping this capability explicit prevents a
    754    * backend from accidentally decoding an immediate's union payload as a
    755    * register while allowing targets with private materialization registers to
    756    * avoid unnecessary pressure on the optimizer's operand-temp bank. */
    757   int (*intrinsic_arg_accepts_imm)(NativeTarget*, IntrinKind, u32 arg_index);
    758   void (*intrinsic)(NativeTarget*, IntrinKind, const NativeLoc* dsts, u32 ndst,
    759                     const NativeLoc* args, u32 narg);
    760   void (*asm_block)(NativeTarget*, const char* tmpl, const AsmConstraint* outs,
    761                     u32 nout, NativeLoc* out_locs, const AsmConstraint* ins,
    762                     u32 nin, const NativeLoc* in_locs, const Sym* clobbers,
    763                     u32 nclob);
    764   void (*file_scope_asm)(NativeTarget*, const char* src, size_t len);
    765   void (*patch_add)(NativeTarget*, const NativePatch*);
    766   void (*patch_apply)(NativeTarget*);
    767   void (*trap)(NativeTarget*);
    768   void (*set_loc)(NativeTarget*, SrcLoc);
    769   void (*finalize)(NativeTarget*);
    770   void (*destroy)(NativeTarget*);
    771 };
    772 
    773 static inline const NativeAllocClassInfo* native_target_class_info(
    774     const NativeTarget* t, NativeAllocClass cls) {
    775   return t ? native_reg_info_class_info(t->regs, cls) : NULL;
    776 }
    777 
    778 static inline u32 native_target_caller_saved_mask(NativeTarget* t,
    779                                                   NativeAllocClass cls) {
    780   if (t && t->caller_saved_mask) return t->caller_saved_mask(t, cls);
    781   return t ? native_reg_info_flag_mask(t->regs, cls,
    782                                        NATIVE_REG_CALLER_SAVED)
    783            : 0u;
    784 }
    785 
    786 static inline u32 native_target_callee_saved_mask(NativeTarget* t,
    787                                                   NativeAllocClass cls) {
    788   if (t && t->callee_saved_mask) return t->callee_saved_mask(t, cls);
    789   return t ? native_reg_info_flag_mask(t->regs, cls,
    790                                        NATIVE_REG_CALLEE_SAVED)
    791            : 0u;
    792 }
    793 
    794 static inline u32 native_target_arg_mask(const NativeTarget* t,
    795                                          NativeAllocClass cls) {
    796   return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_ARG) : 0u;
    797 }
    798 
    799 static inline u32 native_target_ret_mask(const NativeTarget* t,
    800                                          NativeAllocClass cls) {
    801   return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_RET) : 0u;
    802 }
    803 
    804 static inline u32 native_target_reserved_mask(const NativeTarget* t,
    805                                               NativeAllocClass cls) {
    806   return t ? native_reg_info_flag_mask(t->regs, cls, NATIVE_REG_RESERVED) : 0u;
    807 }
    808 
    809 /* Intrinsic operands whose immediate form is shared across every NativeTarget.
    810  * Other immediates are scalar values: the optimized emitter materializes them
    811  * unless the target explicitly advertises support through its capability
    812  * hook. */
    813 static inline int native_intrinsic_arg_accepts_imm(NativeTarget* t,
    814                                                    IntrinKind kind,
    815                                                    u32 arg_index) {
    816   switch (kind) {
    817     case INTRIN_MEMMOVE:
    818       return arg_index == 2u; /* constant byte count */
    819     case INTRIN_PREFETCH:
    820       return arg_index >= 1u; /* rw, locality */
    821     case INTRIN_ASSUME_ALIGNED:
    822       return arg_index >= 1u; /* alignment, offset */
    823     case INTRIN_EXPECT:
    824       return arg_index == 1u; /* expected value hint */
    825     case INTRIN_SYSCALL:
    826       /* Syscall hooks already use their target's parallel ABI-argument mover,
    827        * which accepts both registers and immediates and avoids requiring up to
    828        * seven simultaneous emitter-temp leases. */
    829       return 1;
    830     case INTRIN_DMB:
    831     case INTRIN_DSB:
    832       return arg_index == 0u; /* KitCgBarrierScope */
    833     case INTRIN_FRAME_ADDRESS:
    834     case INTRIN_RETURN_ADDRESS:
    835       return arg_index == 0u; /* constant frame-chain level */
    836     default:
    837       return t && t->intrinsic_arg_accepts_imm
    838                  ? t->intrinsic_arg_accepts_imm(t, kind, arg_index)
    839                  : 0;
    840   }
    841 }
    842 
    843 /* Location constructors. A designated compound literal initializes the named
    844  * fields and zero-fills the rest (so it is value-identical to the former
    845  * memset + field stores) but lets the compiler emit only the needed stores
    846  * instead of an out-of-line memset of the ~64-byte struct — these run on the
    847  * hottest per-operand codegen path. */
    848 static inline NativeLoc native_loc_none(void) {
    849   return (NativeLoc){.kind = NATIVE_LOC_NONE};
    850 }
    851 
    852 /* Target-neutral location constructors and scalar queries. These are
    853  * byte-identical across the native backends, so they live here as the single
    854  * source of truth. (loc_reg's register mask differs per arch and stays
    855  * per-backend.) */
    856 static inline NativeLoc native_loc_reg(KitCgTypeId type, NativeAllocClass cls,
    857                                        Reg reg) {
    858   return (NativeLoc){
    859       .kind = NATIVE_LOC_REG, .cls = (u8)cls, .type = type, .v.reg = reg};
    860 }
    861 
    862 static inline NativeLoc native_loc_stack(KitCgTypeId type, NativeFrameSlot slot,
    863                                          i32 offset) {
    864   return (NativeLoc){.kind = NATIVE_LOC_STACK,
    865                      .cls = NATIVE_REG_INT,
    866                      .type = type,
    867                      .v.stack = {.slot = slot, .offset = offset}};
    868 }
    869 
    870 /* A NativeLoc that structurally contains an address has one of two disjoint
    871  * semantic roles.  Storage locations are dereferenced to obtain a value;
    872  * address recipes are themselves scalar pointer values.  Keep the role in the
    873  * shared contract so ABI marshallers cannot accidentally turn `&slot` into a
    874  * load from slot (or turn a stored scalar into its address). */
    875 typedef enum NativeLocAddrRole {
    876   NATIVE_LOC_ADDR_ROLE_NONE,
    877   NATIVE_LOC_ADDR_ROLE_STORAGE,
    878   NATIVE_LOC_ADDR_ROLE_VALUE,
    879 } NativeLocAddrRole;
    880 
    881 static inline NativeLocAddrRole native_loc_addr_role(NativeLoc loc) {
    882   switch ((NativeLocKind)loc.kind) {
    883     case NATIVE_LOC_FRAME:
    884     case NATIVE_LOC_STACK:
    885     case NATIVE_LOC_ADDR:
    886       return NATIVE_LOC_ADDR_ROLE_STORAGE;
    887     case NATIVE_LOC_GLOBAL:
    888     case NATIVE_LOC_FRAME_ADDR:
    889       return NATIVE_LOC_ADDR_ROLE_VALUE;
    890     default:
    891       return NATIVE_LOC_ADDR_ROLE_NONE;
    892   }
    893 }
    894 
    895 /* Resolve storage that may be dereferenced. `offset` selects bytes within the
    896  * stored object (including an existing STACK/ADDR displacement). */
    897 static inline int native_loc_storage_addr(NativeLoc loc, i32 offset,
    898                                           NativeAddr* out) {
    899   NativeAddr addr = {0};
    900   if (!out || native_loc_addr_role(loc) != NATIVE_LOC_ADDR_ROLE_STORAGE)
    901     return 0;
    902   switch ((NativeLocKind)loc.kind) {
    903     case NATIVE_LOC_FRAME:
    904       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    905       addr.base.frame = loc.v.frame;
    906       addr.base_type = loc.type;
    907       break;
    908     case NATIVE_LOC_STACK:
    909       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    910       addr.base.frame = loc.v.stack.slot;
    911       addr.base_type = loc.type;
    912       addr.offset = loc.v.stack.offset;
    913       break;
    914     case NATIVE_LOC_ADDR:
    915       addr = loc.v.addr;
    916       break;
    917     default:
    918       return 0;
    919   }
    920   addr.offset += offset;
    921   *out = addr;
    922   return 1;
    923 }
    924 
    925 /* Resolve a scalar address recipe.  There is intentionally no part offset:
    926  * GLOBAL/FRAME_ADDR are one pointer value, not byte-addressable aggregate
    927  * storage.  A call marshaller that tries to split one must reject the shape. */
    928 static inline int native_loc_address_value(NativeLoc loc, NativeAddr* out) {
    929   NativeAddr addr = {0};
    930   if (!out || native_loc_addr_role(loc) != NATIVE_LOC_ADDR_ROLE_VALUE)
    931     return 0;
    932   switch ((NativeLocKind)loc.kind) {
    933     case NATIVE_LOC_FRAME_ADDR:
    934       addr.base_kind = NATIVE_ADDR_BASE_FRAME;
    935       addr.base.frame = loc.v.frame;
    936       addr.base_type = loc.type;
    937       break;
    938     case NATIVE_LOC_GLOBAL:
    939       addr.base_kind = NATIVE_ADDR_BASE_GLOBAL;
    940       addr.base.global.sym = loc.v.global.sym;
    941       addr.base.global.addend = loc.v.global.addend;
    942       addr.base_type = loc.type;
    943       break;
    944     default:
    945       return 0;
    946   }
    947   *out = addr;
    948   return 1;
    949 }
    950 
    951 /* Iterate the exact bytes carried by one ABI part as native load/store widths.
    952  * ABIArgPart.size is a semantic byte count, not the rounded size of its
    953  * register or stack carrier: a six-byte aggregate part must transfer 4+2
    954  * bytes, never one 8-byte access that can cross the source/destination object.
    955  *
    956  * Chunks are returned from low to high byte offsets, greedily choosing the
    957  * largest power-of-two width no greater than max_chunk. All native targets are
    958  * little-endian today, so the byte offset is also the register shift in bytes
    959  * when packing/unpacking a part. Physical ABI stack-slot sizing remains the
    960  * backend's separate responsibility. */
    961 typedef struct NativePartChunkIter {
    962   u32 total;
    963   u32 offset;
    964   u32 max_chunk;
    965 } NativePartChunkIter;
    966 
    967 static inline NativePartChunkIter native_part_chunks(u32 total,
    968                                                       u32 max_chunk) {
    969   NativePartChunkIter it;
    970   it.total = total;
    971   it.offset = 0;
    972   it.max_chunk = max_chunk;
    973   return it;
    974 }
    975 
    976 static inline int native_part_chunk_next(NativePartChunkIter* it,
    977                                          u32* offset_out, u32* size_out) {
    978   u32 chunk = 1u;
    979   u32 remaining;
    980   if (!it || !offset_out || !size_out || it->offset >= it->total ||
    981       it->max_chunk == 0u)
    982     return 0;
    983   remaining = it->total - it->offset;
    984   while (chunk <= remaining / 2u && chunk <= it->max_chunk / 2u) chunk <<= 1;
    985   *offset_out = it->offset;
    986   *size_out = chunk;
    987   it->offset += chunk;
    988   return 1;
    989 }
    990 
    991 /* Select the raw byte lane used for one ABI part of an immediate scalar.  The
    992  * complete value is only i64-wide, so bounds are explicit.  Offset zero keeps
    993  * the producer's signed numeric value (preserving established narrow-scalar
    994  * extension behavior); later parts are bit slices and are zero-extended. */
    995 static inline int native_loc_imm_part(NativeLoc loc, u32 offset, u32 size,
    996                                       i64* out) {
    997   u64 bits, mask;
    998   if (!out || loc.kind != NATIVE_LOC_IMM || size == 0u || size > 8u ||
    999       offset >= 8u || size > 8u - offset)
   1000     return 0;
   1001   if (offset == 0u) {
   1002     *out = loc.v.imm;
   1003     return 1;
   1004   }
   1005   bits = (u64)loc.v.imm >> (offset * 8u);
   1006   mask = size == 8u ? ~(u64)0 : (((u64)1u << (size * 8u)) - 1u);
   1007   *out = (i64)(bits & mask);
   1008   return 1;
   1009 }
   1010 
   1011 static inline int native_loc_is_fp(NativeLoc loc) {
   1012   return (NativeAllocClass)loc.cls == NATIVE_REG_FP;
   1013 }
   1014 
   1015 /* Narrow a register-or-immediate NativeLoc to the 16 B NativeRegLoc the §E.3
   1016  * *_rr hooks take. Only NATIVE_LOC_REG / NATIVE_LOC_IMM forms reach these hooks
   1017  * (the post-materialization contract), so this carries the register/immediate,
   1018  * class, the cached width descriptor, and the type for the cold fallback. */
   1019 static inline NativeRegLoc native_reg_loc_of(NativeLoc loc) {
   1020   NativeRegLoc r;
   1021   r.cls = loc.cls;
   1022   r.szinfo = loc.szinfo;
   1023   r.is_imm = (u8)(loc.kind == NATIVE_LOC_IMM);
   1024   r.pad = 0;
   1025   r.type = loc.type;
   1026   if (loc.kind == NATIVE_LOC_IMM)
   1027     r.v.imm = loc.v.imm;
   1028   else
   1029     r.v.reg = loc.v.reg;
   1030   return r;
   1031 }
   1032 
   1033 /* Reconstruct the full NativeLoc a fat binop/move/cmp/convert hook would have
   1034  * received from a NativeRegLoc. Byte-identical by construction: the arch reads
   1035  * only kind / cls / szinfo / type / v.{reg,imm}, all preserved here, and the
   1036  * rest of the 48 B union is never consulted on this path. */
   1037 static inline NativeLoc native_loc_from_reg(NativeRegLoc r) {
   1038   NativeLoc loc;
   1039   loc.kind = (u8)(r.is_imm ? NATIVE_LOC_IMM : NATIVE_LOC_REG);
   1040   loc.cls = r.cls;
   1041   loc.szinfo = r.szinfo;
   1042   loc.shift = 0; /* O1-only L7 rider; the -O0 NDT fast path never sets it */
   1043   loc.type = r.type;
   1044   if (r.is_imm)
   1045     loc.v.imm = r.v.imm;
   1046   else
   1047     loc.v.reg = r.v.reg;
   1048   return loc;
   1049 }
   1050 
   1051 /* NativeLoc.szinfo layout (see the field comment on NativeLoc). */
   1052 #define NATIVE_SZINFO_VALID 0x01u
   1053 #define NATIVE_SZINFO_SIZE_LOG2_SHIFT 1u
   1054 #define NATIVE_SZINFO_SIZE_LOG2_MASK 0x07u
   1055 
   1056 /* size (bytes, 1..16) -> log2 bucket (0=1,1=2,2=4,3=8,4=16). */
   1057 static inline u8 native_szinfo_log2(u32 size) {
   1058   if (size <= 1u) return 0u;
   1059   if (size <= 2u) return 1u;
   1060   if (size <= 4u) return 2u;
   1061   if (size <= 8u) return 3u;
   1062   return 4u;
   1063 }
   1064 
   1065 /* Stamp a register NativeLoc's cached width descriptor from its type, computing
   1066  * cg_type_size exactly once. Called by the NDT at its register-loc choke point;
   1067  * the arch backends then read loc.szinfo instead of re-querying. Only meaningful
   1068  * for scalar (<=16B) reg locs; left invalid for a 0 type or an over-wide size so
   1069  * the arch's own (panicking) query path runs. The mapped size is byte-identical
   1070  * to native_type_size / aarch64's type_size32 for in-range scalars: a 0 type
   1071  * maps to 8, a 0 size to 8 (the register-sized default both helpers apply). */
   1072 static inline void native_loc_stamp_size(NativeTarget* t, NativeLoc* loc) {
   1073   u64 n = loc->type ? cg_type_size(t->c, loc->type) : 8u;
   1074   if (n == 0) n = 8u;
   1075   /* Cache only exact-power-of-two scalar widths (1/2/4/8/16): the szinfo bucket
   1076    * recovers the size exactly, so the cached value is bit-for-bit what
   1077    * cg_type_size / native_type_size would return. Anything else (over-wide, or a
   1078    * non-power-of-two aggregate that should never reach a register loc) is left
   1079    * invalid and falls back to the live query. */
   1080   if (n != 1u && n != 2u && n != 4u && n != 8u && n != 16u) return;
   1081   loc->szinfo = (u8)(NATIVE_SZINFO_VALID | ((u32)native_szinfo_log2((u32)n)
   1082                                             << NATIVE_SZINFO_SIZE_LOG2_SHIFT));
   1083 }
   1084 
   1085 /* Byte size (1/2/4/8/16) recovered from a stamped szinfo descriptor. Only valid
   1086  * when (szinfo & NATIVE_SZINFO_VALID). */
   1087 static inline u32 native_szinfo_size(u8 szinfo) {
   1088   u32 lg =
   1089       ((u32)szinfo >> NATIVE_SZINFO_SIZE_LOG2_SHIFT) & NATIVE_SZINFO_SIZE_LOG2_MASK;
   1090   return 1u << lg;
   1091 }
   1092 
   1093 /* Scalar size/align, clamped to a usable register-sized default. Shared by the
   1094  * backends whose scalars are at most pointer-width (x64, rv64); aa64 keeps its
   1095  * own size query because it asserts on over-wide scalars. */
   1096 /* Round v up to the next multiple of align (a power of two, or 0 → no-op).
   1097  * Shared by every native backend's frame-layout math. */
   1098 static inline u32 align_up_u32(u32 v, u32 align) {
   1099   u32 mask = align ? align - 1u : 0u;
   1100   return (v + mask) & ~mask;
   1101 }
   1102 
   1103 static inline u32 native_type_size(NativeTarget* t, KitCgTypeId type) {
   1104   u64 n = type ? cg_type_size(t->c, type) : 8u;
   1105   if (n == 0) n = 8u;
   1106   return (u32)n;
   1107 }
   1108 
   1109 static inline u32 native_type_align(NativeTarget* t, KitCgTypeId type) {
   1110   u64 n = type ? cg_type_align(t->c, type) : 8u;
   1111   if (n == 0) n = 1u;
   1112   if (n > 16u) n = 16u;
   1113   return (u32)n;
   1114 }
   1115 
   1116 static inline MemAccess native_mem_for_type(NativeTarget* t, KitCgTypeId type,
   1117                                             u32 size) {
   1118   MemAccess m;
   1119   memset(&m, 0, sizeof m);
   1120   m.type = type;
   1121   m.size = size ? size : native_type_size(t, type);
   1122   m.align = native_type_align(t, type);
   1123   return m;
   1124 }
   1125 
   1126 /* FP register class for a scalar type: a float value lives in an FP register
   1127  * only when the hardware float ABI has a register that wide. flen comes from
   1128  * the target float ABI (SINGLE->4, DOUBLE->8, SOFT->0); the DEFAULT/unset
   1129  * sentinel maps to the pointer width, which preserves the historical "FP iff
   1130  * float and <= 8 bytes" behavior for lp64d / x86-64 and yields the correct
   1131  * rv32 soft-double result (double is 8 bytes > flen=4 on ilp32f, > 0 on ilp32,
   1132  * so it is INT-class and never bit-cast through an FP register via fmv.d.x).
   1133  * aa64 keeps its own (same predicate, distinct mem helper). */
   1134 static inline NativeAllocClass native_class_for_type_fp_le8(NativeTarget* t,
   1135                                                             KitCgTypeId type) {
   1136   u32 flen;
   1137   switch (t->c->target.float_abi) {
   1138     case KIT_FLOAT_ABI_SINGLE:
   1139       flen = 4u;
   1140       break;
   1141     case KIT_FLOAT_ABI_DOUBLE:
   1142       flen = 8u;
   1143       break;
   1144     case KIT_FLOAT_ABI_SOFT:
   1145       flen = 0u;
   1146       break;
   1147     default:
   1148       flen = t->c->target.ptr_size;
   1149       break; /* DEFAULT: historical */
   1150   }
   1151   if (type && flen && cg_type_is_float(t->c, type) &&
   1152       cg_type_size(t->c, type) <= flen)
   1153     return NATIVE_REG_FP;
   1154   return NATIVE_REG_INT;
   1155 }
   1156 
   1157 #endif