Files
Odin/core/rexcode/isa/x86/operands.odin
Flāvius 0f472409c4 rexcode/x86: label addressing for RIP-relative disp and movabs imm
Add mem_rip_label(label_id) so a RIP-relative memory operand can
reference a label: the encoder writes a placeholder disp32 and emits a
REL32 relocation (addend 0) at the field's byte offset, mirroring the
existing .RELATIVE jump/call path. This expresses lea reg, [rip + <label>]
(position-independent data addressing).

Add op_imm_label(label_id) for movabs reg, <label>: the imm stays kind
.IMMEDIATE (form matching unchanged) but is flagged so the encoder emits
an ABS64 relocation for the imm64 instead of a literal; imm_matches_inline
forces the full IMM64 form so a small id can't collapse to mov r64, imm32.

Both labeled forms bypass the contextless recipe fast-path (which cannot
append relocations) and fall back to the interpreter. Flags reuse spare
bits in Memory (disp_is_label) and Operand_Flags (imm_is_label) -- no
struct growth. Section 11 tests cover resolved/unresolved disp, the ABS64
movabs, and an end-to-end executed lea+load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 15:05:16 -04:00

375 lines
13 KiB
Odin

// rexcode · Brendan Punsky (dotbmp@github), original author
package rexcode_x86
// =============================================================================
// SECTION: 2. OPERANDS
// =============================================================================
// -----------------------------------------------------------------------------
// SECTION: 2.1 Operand Kind Enum
// -----------------------------------------------------------------------------
Operand_Kind :: enum u8 {
NONE,
REGISTER, // register operand
MEMORY, // memory operand
IMMEDIATE, // immediate value
RELATIVE, // relative offset (for jumps/calls)
}
// -----------------------------------------------------------------------------
// SECTION: 2.2 Memory Operand - Packed Representation
// -----------------------------------------------------------------------------
MEM_BASE_RIP :: 30
MEM_BASE_NONE :: 31
MEM_INDEX_NONE :: 31
Memory :: bit_field u64 {
base_hw: u8 | 5,
base_ext: bool | 1,
index_hw: u8 | 5,
index_ext: bool | 1,
scale_enc: u8 | 2,
disp: i32 | 32,
segment: u8 | 3,
addr_size_override: bool | 1,
base_class: u8 | 5,
index_class: u8 | 5,
// When set, `disp` holds a label id (not a literal displacement): the
// encoder emits a REL32 relocation at the disp32's byte offset instead of
// the literal, so a RIP-relative operand can reference a label. Uses one of
// Memory's 4 spare bits -- no growth, and one bit-test on the hot path.
disp_is_label: bool | 1,
}
@(require_results)
mem_make :: proc "contextless" (base: Register, index: Register, scale: u8, disp: i32, segment: Register) -> Memory {
mem: Memory = ---
mem.base_hw = MEM_BASE_NONE
mem.base_ext = false
mem.base_class = 0
if base == RIP {
mem.base_hw = MEM_BASE_RIP
} else if base != NONE {
mem.base_hw = reg_hw(base)
mem.base_ext = reg_needs_rex(base)
mem.base_class = u8((u16(base) >> 8) & 0x1F)
}
mem.index_hw = MEM_INDEX_NONE
mem.index_ext = false
mem.index_class = 0
if index != NONE {
mem.index_hw = reg_hw(index)
mem.index_ext = reg_needs_rex(index)
mem.index_class = u8((u16(index) >> 8) & 0x1F)
}
switch scale {
case 1: mem.scale_enc = 0
case 2: mem.scale_enc = 1
case 4: mem.scale_enc = 2
case 8: mem.scale_enc = 3
case: mem.scale_enc = 0
}
mem.segment = 0
if segment != NONE && reg_class(segment) == REG_SEG {
mem.segment = reg_hw(segment) + 1
}
mem.disp = disp
// Must be explicitly cleared: `mem` starts uninitialized (`= ---`) and the
// encoder reads this on the hot path -- garbage here would spuriously turn a
// literal displacement into a label reference. mem_rip_label sets it after.
mem.disp_is_label = false
return mem
}
// -----------------------------------------------------------------------------
// SECTION: 2.3 Memory Utility Functions
// -----------------------------------------------------------------------------
@(require_results)
mem_scale :: #force_inline proc "contextless" (m: Memory) -> u8 {
return 1 << m.scale_enc
}
@(require_results)
mem_is_rip_relative :: #force_inline proc "contextless" (m: Memory) -> bool {
return m.base_hw == MEM_BASE_RIP
}
@(require_results)
mem_has_base :: #force_inline proc "contextless" (m: Memory) -> bool {
return m.base_hw != MEM_BASE_NONE
}
@(require_results)
mem_has_index :: #force_inline proc "contextless" (m: Memory) -> bool {
return m.index_hw != MEM_INDEX_NONE
}
@(require_results)
mem_base :: proc "contextless" (m: Memory) -> Register {
if m.base_hw == MEM_BASE_NONE { return NONE }
if m.base_hw == MEM_BASE_RIP { return RIP }
class := u16(m.base_class) << 8
return Register(class | u16(m.base_hw))
}
@(require_results)
mem_index :: proc "contextless" (m: Memory) -> Register {
if m.index_hw == MEM_INDEX_NONE { return NONE }
class := u16(m.index_class) << 8
return Register(class | u16(m.index_hw))
}
// -----------------------------------------------------------------------------
// SECTION: 2.4 Memory Convenience Constructors
// -----------------------------------------------------------------------------
// Convenience constructors for common memory patterns
@(require_results)
mem_base_only :: #force_inline proc "contextless" (base: Register) -> Memory {
return mem_make(base, NONE, 1, 0, NONE)
}
@(require_results)
mem_base_disp :: #force_inline proc "contextless" (base: Register, disp: i32) -> Memory {
return mem_make(base, NONE, 1, disp, NONE)
}
@(require_results)
mem_base_index :: #force_inline proc "contextless" (base, index: Register, scale: u8) -> Memory {
return mem_make(base, index, scale, 0, NONE)
}
@(require_results)
mem_base_index_disp :: #force_inline proc "contextless" (base, index: Register, scale: u8, disp: i32) -> Memory {
return mem_make(base, index, scale, disp, NONE)
}
@(require_results)
mem_rip_disp :: #force_inline proc "contextless" (disp: i32) -> Memory {
return mem_make(RIP, NONE, 1, disp, NONE)
}
// RIP-relative memory operand whose displacement references a label rather than
// a literal. The encoder writes a placeholder disp32 and emits a REL32
// relocation for `label_id` at that field's byte offset -- the native way to
// express `lea reg, [rip + <label>]` (position-independent data addressing).
// The relocation carries addend 0; any within-section offset is supplied by the
// downstream object/JIT layer when it resolves the label.
@(require_results)
mem_rip_label :: #force_inline proc "contextless" (label_id: u32) -> Memory {
m := mem_make(RIP, NONE, 1, 0, NONE)
m.disp = i32(label_id)
m.disp_is_label = true
return m
}
// -----------------------------------------------------------------------------
// SECTION: 2.5 Operand struct and Flags
// -----------------------------------------------------------------------------
// The unified Operand type
Operand :: struct #packed {
using _: struct #raw_union {
reg: Register, // for Register kind
mem: Memory, // for Memory kind (packed)
immediate: i64, // for Immediate kind
relative: i64, // for Relative kind (offset or label id)
},
kind: Operand_Kind,
size: u8, // operand size in bytes (1, 2, 4, 8, 16, 32, 64)
flags: Operand_Flags,
}
#assert(size_of(Operand) == 12)
// EVEX broadcast mode values for Operand_Flags.broadcast
// Used for EVEX instructions to broadcast a scalar element to all lanes
Broadcast :: enum u8 {
NONE = 0, // No broadcast (full vector load)
B1TO2 = 1, // Broadcast 1 element to 2 (64-bit element to 128-bit vector)
B1TO4 = 2, // Broadcast 1 element to 4 (32-bit to 128-bit, or 64-bit to 256-bit)
B1TO8 = 3, // Broadcast 1 element to 8 (32-bit to 256-bit, or 64-bit to 512-bit)
B1TO16 = 4, // Broadcast 1 element to 16 (32-bit to 512-bit)
}
Operand_Flags :: bit_field u16 {
// EVEX-specific
mask: u8 | 3, // opmask register K1-K7 (0 = no mask)
zeroing: bool | 1, // merge (0) vs zero (1) masking
broadcast: Broadcast | 3, // broadcast mode (see Broadcast enum)
er_sae: u8 | 2, // embedded rounding / SAE (0=none, 1=RN-SAE, 2=RD-SAE, 3=RU-SAE/RZ-SAE)
// When set on a movabs (IQ) immediate operand, `Operand.immediate` holds a
// label id: the encoder writes a placeholder imm64 and emits an ABS64
// relocation at its byte offset. Uses one of Operand_Flags' spare bits.
imm_is_label: bool | 1,
}
// -----------------------------------------------------------------------------
// SECTION: 2.6 Operand Constructors
// -----------------------------------------------------------------------------
// Operand constructors
@(require_results)
op_reg :: #force_inline proc "contextless" (r: Register) -> Operand {
return Operand{reg = r, kind = .REGISTER, size = u8(reg_size(r) / 8)}
}
@(require_results)
op_mem :: #force_inline proc "contextless" (m: Memory, size: u8) -> Operand {
return Operand{mem = m, kind = .MEMORY, size = size}
}
@(require_results)
op_mem_from_parts :: #force_inline proc "contextless" (
base: Register,
index: Register,
scale: u8,
disp: i32,
size: u8,
) -> Operand {
return op_mem(mem_make(base, index, scale, disp, NONE), size)
}
// Generic immediate constructor — value + explicit byte size. The typed
// op_imm8/16/32/64 variants below are convenience wrappers.
@(require_results)
op_imm :: #force_inline proc "contextless" (v: i64, size: u8) -> Operand {
return Operand{immediate = v, kind = .IMMEDIATE, size = size}
}
@(require_results)
op_imm8 :: #force_inline proc "contextless" (v: i8) -> Operand {
return Operand{immediate = i64(v), kind = .IMMEDIATE, size = 1}
}
@(require_results)
op_imm16 :: #force_inline proc "contextless" (v: i16) -> Operand {
return Operand{immediate = i64(v), kind = .IMMEDIATE, size = 2}
}
@(require_results)
op_imm32 :: #force_inline proc "contextless" (v: i32) -> Operand {
return Operand{immediate = i64(v), kind = .IMMEDIATE, size = 4}
}
@(require_results)
op_imm64 :: #force_inline proc "contextless" (v: i64) -> Operand {
return Operand{immediate = v, kind = .IMMEDIATE, size = 8}
}
@(require_results)
op_rel8 :: #force_inline proc "contextless" (offset: i8) -> Operand {
return Operand{relative = i64(offset), kind = .RELATIVE, size = 1}
}
@(require_results)
op_rel32 :: #force_inline proc "contextless" (offset: i32) -> Operand {
return Operand{relative = i64(offset), kind = .RELATIVE, size = 4}
}
// Create a relative operand referencing a label
@(require_results)
op_label :: #force_inline proc "contextless" (label_id: u32, size: u8 = 4) -> Operand {
return Operand{relative = i64(label_id), kind = .RELATIVE, size = size}
}
// A movabs imm64 that references a label: the immediate stays kind .IMMEDIATE
// (so form matching is unchanged) but is flagged as a label, so the encoder
// emits an ABS64 relocation for it instead of a literal. Use for
// `movabs reg, <label>` (absolute addressing; the target address is filled in
// at relocation time).
@(require_results)
op_imm_label :: #force_inline proc "contextless" (label_id: u32) -> Operand {
return Operand{immediate = i64(label_id), kind = .IMMEDIATE, size = 8, flags = {imm_is_label = true}}
}
// -----------------------------------------------------------------------------
// SECTION: 2.7 Typed Operand Constructors (compile-time type safety)
// -----------------------------------------------------------------------------
// These provide compile-time type safety: op_gpr64(.XMM0) is a compile error.
// The enum value IS the hardware number, so we just cast - no table lookup.
@(require_results)
op_gpr64 :: #force_inline proc "contextless" (r: GPR64) -> Operand {
return Operand{reg = Register(REG_GPR64 | u16(r)), kind = .REGISTER, size = 8}
}
@(require_results)
op_gpr32 :: #force_inline proc "contextless" (r: GPR32) -> Operand {
return Operand{reg = Register(REG_GPR32 | u16(r)), kind = .REGISTER, size = 4}
}
@(require_results)
op_gpr16 :: #force_inline proc "contextless" (r: GPR16) -> Operand {
return Operand{reg = Register(REG_GPR16 | u16(r)), kind = .REGISTER, size = 2}
}
@(require_results)
op_gpr8 :: #force_inline proc "contextless" (r: GPR8) -> Operand {
return Operand{reg = Register(REG_GPR8 | u16(r)), kind = .REGISTER, size = 1}
}
@(require_results)
op_gpr8h :: #force_inline proc "contextless" (r: GPR8H) -> Operand {
return Operand{reg = Register(REG_GPR8H | u16(r)), kind = .REGISTER, size = 1}
}
@(require_results)
op_xmm :: #force_inline proc "contextless" (r: XMM) -> Operand {
return Operand{reg = Register(REG_XMM | u16(r)), kind = .REGISTER, size = 16}
}
@(require_results)
op_ymm :: #force_inline proc "contextless" (r: YMM) -> Operand {
return Operand{reg = Register(REG_YMM | u16(r)), kind = .REGISTER, size = 32}
}
@(require_results)
op_zmm :: #force_inline proc "contextless" (r: ZMM) -> Operand {
return Operand{reg = Register(REG_ZMM | u16(r)), kind = .REGISTER, size = 64}
}
@(require_results)
op_kreg :: #force_inline proc "contextless" (r: KREG) -> Operand {
return Operand{reg = Register(REG_K | u16(r)), kind = .REGISTER, size = 8}
}
@(require_results)
op_sreg :: #force_inline proc "contextless" (r: SREG) -> Operand {
return Operand{reg = Register(REG_SEG | u16(r)), kind = .REGISTER, size = 2}
}
@(require_results)
op_mm :: #force_inline proc "contextless" (r: MM) -> Operand {
return Operand{reg = Register(REG_MM | u16(r)), kind = .REGISTER, size = 8}
}
@(require_results)
op_creg :: #force_inline proc "contextless" (r: CREG) -> Operand {
return Operand{reg = Register(REG_CR | u16(r)), kind = .REGISTER, size = 8}
}
@(require_results)
op_dreg :: #force_inline proc "contextless" (r: DREG) -> Operand {
return Operand{reg = Register(REG_DR | u16(r)), kind = .REGISTER, size = 8}
}
@(require_results)
op_st :: #force_inline proc "contextless" (r: ST) -> Operand {
return Operand{reg = Register(REG_ST | u16(r)), kind = .REGISTER, size = 10}
}
@(require_results)
op_bnd :: #force_inline proc "contextless" (r: BND) -> Operand {
return Operand{reg = Register(REG_BND | u16(r)), kind = .REGISTER, size = 16}
}