Class: Rubycc::IR::Instruction

Inherits:
Object
  • Object
show all
Defined in:
lib/rubycc/ir/ir.rb

Overview

A single three-address instruction over virtual registers.

:const  dst <- a            (a is an immediate Integer; size == 8 loads a
                          full 64-bit immediate for a long/pointer
                          constant, otherwise a 32-bit one)
:copy   dst <- a
:add/:sub/:mul             dst <- a op b
:div/:mod                  dst <- a op b   (signed division/remainder)
:udiv/:umod                dst <- a op b   (unsigned division/remainder;
                          the backend zeroes edx and uses `div`)
:mulhi  dst <- hi64(a * b)  the unsigned high 64 bits of the 128-bit
                          product of two 64-bit values (x86 `mul r64`,
                          REX.W F7 /4: rax*b -> rdx:rax, result taken
                          from rdx). Used only to synthesize a 128-bit
                          (`__int128`) multiply from 64-bit halves; the
                          generator pairs it with ordinary :mul (the low
                          64) and :add. size is always 8
:and/:or/:xor              dst <- a op b   (bitwise)
:shl    dst <- a << b       (logical left shift; b's low byte is the
                          shift count, taken from cl by the backend)
:sar    dst <- a >> b       (arithmetic right shift; b's low byte is the
                          count. A signed left operand's ">>" lowers to
                          :sar so the sign bit is replicated)
:shr    dst <- a >> b       (logical right shift; the unsigned counterpart
                          of :sar, chosen when the left operand is an
                          unsigned type — the split mirrors :div/:udiv,
                          since the machine opcodes differ by sign)
:eq/:ne                    dst <- (a op b) ? 1 : 0   (sign-independent)
:lt/:le/:gt/:ge            dst <- (a op b) ? 1 : 0   (signed compare)
:ult/:ule/:ugt/:uge        dst <- (a op b) ? 1 : 0   (unsigned compare,
                          setb/setbe/seta/setae; also used for pointer
                          ordering, addresses being unsigned)
:neg    dst <- -a
:fadd/:fsub/:fmul/:fdiv    dst <- a op b   (floating arithmetic; size is
                          the operand width, 4 for float / 8 for
                          double, selecting the ss/sd form). A floating
                          "-a" has no op of its own: the generator
                          flips the sign bit with an integer :xor
                          (0x80000000 / 0x8000000000000000, size 8)
:feq/:fne                   dst <- (a op b) ? 1 : 0   (floating equality;
                          size 4/8. NaN-aware: :feq is false and :fne
                          true when either operand is NaN)
:flt/:fle/:fgt/:fge         dst <- (a op b) ? 1 : 0   (floating ordering;
                          size 4/8. Every one is false when either
                          operand is NaN, the backend reversing the
                          ucomis operands for :flt/:fle so an unordered
                          compare clears the flag)
:itof   dst <- (float)a     integer a converted to a floating value.
                          size is the destination float width (4/8);
                          b is the [width, signed?] descriptor of the
                          integer *source* (an unsigned long source is
                          rejected by the generator, so never reaches
                          here)
:ftoi   dst <- (int)a       floating a truncated toward zero to an
                          integer. size is the float *source* width
                          (4/8); b is the [width, signed?] descriptor
                          of the integer destination (an unsigned long
                          destination is likewise rejected upstream)
:ftof   dst <- a            float<->double width change. size is the
                          *source* float width (4 widening to double,
                          8 narrowing to float)
:sext   dst <- a  (size: 1/2/4)  a's low `size` bytes sign-extended to
                          the register's full width. size 4 is a
                          movsxd; size 1/2 a movsx of the low byte/word
:zext   dst <- a  (size: 1/2/4)  a's low `size` bytes zero-extended to
                          the register's full width. size 4 is a plain
                          32-bit mov (which zeroes the upper half);
                          size 1/2 a movzx
:ret    return a           (a is nil for a void function's "return;"
                         or its implicit fall-off-the-end return,
                         which emits no value-loading code at all).
                         `size` is nil for an integer/pointer return
                         (the value goes in rax/eax) or 4/8 for a
                         floating one, which the backend loads from a's
                         slot into xmm0 with movss/movsd, the System V
                         register a float/double result is returned in.
                         For an in-register struct return `size` is
                         instead an IR::AbiPiece array, one per piece
                         the target's convention cuts the aggregate
                         into (offset, width and kind): a is the address
                         of the struct's buffer, and the backend gathers
                         each piece into its return register — under
                         System V an eightbyte at a time into rax/rdx
                         (:gp) or xmm0/xmm1 (:sse8), under AAPCS64 an
                         HFA member at a time into v0..v3. A struct the
                         convention does not return in registers is not
                         returned this way — its callee copies the
                         result through the hidden pointer parameter and
                         returns that pointer too (a plain size-nil :ret)
:label        a = label id (a jump target; emits no code itself)
:jump         a = label id (unconditional branch)
:jump_if_zero a = condition vreg, b = label id (branch when a == 0)
:call   dst <- f(args)  a = callee name (String),
                      b = array of [arg_vreg, kind] pairs (left to
                      right). `kind` gives each argument's System V
                      AMD64 placement, which the generator has already
                      fixed over the whole argument list (so a Phase B
                      struct can apply the all-or-nothing overflow rule
                      where every argument's type is known): :gp takes
                      the next integer register (edi,esi,edx,ecx,r8d,r9d),
                      :sse4 (float) / :sse8 (double) the next xmm
                      (xmm0..7, loaded from the slot with movss/movsd),
                      and :mem a stack eightbyte. The backend follows the
                      kind verbatim — it assigns registers in order and
                      pushes the :mem arguments in reverse so the first
                      lands at the lowest address (an eightbyte each, the
                      slot's low bits carrying its value). A by-value
                      struct argument fans out into one [vreg, kind] pair
                      per piece its convention cuts it into (a System V
                      eightbyte's class :gp/:sse8, an AAPCS64 HFA
                      member's :sse4/:sse8, or :mem per eightbyte when it
                      spills whole), all placed together so the argument
                      stays in registers or spills as a unit; an
                      aggregate AAPCS64 passes by reference is reduced by
                      the generator to a single :gp pointer to a
                      caller-made copy, so no backend needs a rule of its
                      own for it. A call whose struct result comes back
                      through a hidden pointer also prepends that
                      [vreg, kind] pointer as the first argument. `size` is nil, or a
                      [fixed, ret] pair when either half is non-nil:
                      `fixed` is the callee's fixed parameter count for a
                      variadic call (else nil), which makes the backend
                      set al to the count of xmm registers it used before
                      the call, as the ABI requires; `ret` is :sse4/:sse8
                      when the result is a float/double (loaded from xmm0
                      with movss/movsd into dst's slot), a
                      [buffer_vreg, pieces] pair when the result is a
                      struct returned in registers (dst is nil and the
                      backend scatters each IR::AbiPiece from its return
                      register — rax/rdx or xmm0/xmm1 under System V,
                      x0/x1 or v0..v3 under AAPCS64 — into the buffer
                      buffer_vreg points at), else nil (the result comes
                      back in the integer result register as usual)
:call_indirect dst <- (*a)(args)  a = a vreg holding the function's
                      address (a function pointer value), b = the
                      [arg_vreg, kind] pairs (the same generator-fixed
                      :gp/:sse4/:sse8/:mem placement as :call); `size`
                      carries the same [fixed, ret] pair.
                      The backend calls through a scratch register
:func_addr dst <- &func(a)  dst gets the address of the function named a
                      (a String symbol), the value a function
                      designator decays to (and "&f" yields); resolved
                      by a PC-relative relocation like :global_addr
:addr_of dst <- &slot(a)    dst gets the address of a's stack slot
:object_addr dst <- &object(a)  dst gets the base address of stack
                              object a (an array's first element)
:load   dst <- *a           dst gets `size` bytes read through pointer a,
                          sign-extended (a signed char/short read is a
                          movsx; size 4/8 a plain mov)
:uload  dst <- *a           like :load but zero-extended (an unsigned
                          char/short read is a movzx), for unsigned
                          narrow types and _Bool
:store  *a <- b             `size` bytes of b are written through ptr a
:memcpy *a <- *b (size)     `size` bytes are copied from the address in
                          b to the address in a (a whole-struct
                          assignment "s = t"); both are pointer vregs,
                          `size` the struct's byte width
:string_addr dst <- &string(a)  dst gets the address of read-only string
                              a (an id into the translation unit's
                              string pool), i.e. a decayed char *
:global_addr dst <- &global(a)  dst gets the address of the file-scope
                              variable named a (a String symbol name),
                              the lvalue every global read/write and
                              "&g"/array decay is lowered through
:got_addr dst <- &symbol(a) via GOT  dst gets the address of the
                          file-scope object or function named a (a
                          String symbol), loaded from its Global Offset
                          Table slot rather than formed PC-relatively.
                          The generator emits it in place of
                          :global_addr / :func_addr only under -fPIC,
                          and only for a symbol this translation unit
                          does not define, so a definition in another
                          shared object may interpose; the backend reads
                          the slot with "mov rax, [rip+disp32]" and the
                          linker fills the disp with an
                          R_X86_64_REX_GOTPCRELX relocation against the
                          symbol. A symbol defined here keeps the
                          PC-relative :global_addr / :func_addr form,
                          being always resolved within this DSO
:va_start                   a = a vreg holding the address of a
                          __va_list_tag, b = the enclosing function's
                          fixed (named) parameter count. Initializes the
                          target's va_list fields (the four System V ones
                          gp_offset/fp_offset/overflow_arg_area/reg_save_area,
                          or the five AAPCS64 ones __stack/__gr_top/
                          __vr_top/__gr_offs/__vr_offs) so a later
                          __builtin_va_arg reads the variable arguments;
                          the backend fills them from the register-save
                          area its variadic prologue set up, deriving the
                          named GP and SSE counts (which seed the offsets
                          and the overflow/stack start) from
                          Function.param_kinds rather than from b.
                          va_arg/va_end/va_copy need no IR op of their
                          own — the generator lowers them to ordinary
                          load/store/branch (and, for va_copy, :memcpy)
                          instructions
:alloca dst <- alloca(a)    a = a vreg holding a byte count; dst gets the
                          base address of that many bytes of automatic
                          storage carved from the stack (__builtin_alloca).
                          The backend rounds the count up to a 16-byte
                          multiple and subtracts it from the stack pointer,
                          so that stays 16-aligned and the block is 16-byte
                          aligned; the storage is reclaimed wholesale when
                          the function returns, not at end of scope. What
                          makes the moving stack pointer safe differs by
                          target: x86-64 addresses every other value from
                          rbp already, while aarch64 is sp-relative and so
                          anchors the frame of an alloca-using function in
                          x29 for the duration (see backend/aarch64.rb)
:bit_scan dst <- scan(a)    counts the zero bits of the integer in vreg a,
                          for __builtin_ctz/clz (and their "ll" forms).
                          b is the direction — :forward for a trailing
                          count (ctz), :reverse for a leading count
                          (clz) — and `size` the operand width (4 or 8).
                          x86-64 lowers :forward to `bsf` and :reverse
                          to `bsr` followed by `xor` with (size*8 - 1),
                          so clz = (width-1) - bsr; a size-8 scan takes
                          a REX.W prefix. AArch64 lowers :reverse to a
                          bare `clz` and :forward to `rbit` ahead of
                          one, at the W or X width `size` names. A zero
                          operand is undefined (as in gcc), so no zero
                          case is emitted. The result is an int

The five atomic ops below lower gcc's _atomic* builtins. Every one is sequentially consistent — the IR carries no memory order at all, because the generator lowers every order the source asked for at the strongest one (strengthening an order is always sound; see #gen_builtin_atomic). size is the access width and is only ever 4 or 8: the generator diagnoses every other width, so no backend needs a narrower or wider case.

:atomic_fence                  a sequentially-consistent memory fence
:atomic_load dst <- atomic *a   dst gets `size` bytes read atomically
                          through pointer a, with sequentially
                          consistent ordering. Distinct from :load
                          because the two targets differ: on x86-64 an
                          aligned mov already is a seq_cst load, while
                          aarch64 needs the acquire form (ldar)
:atomic_store *a <- b       `size` bytes of b are written atomically
                          through pointer a, sequentially consistently.
                          x86-64 uses `xchg` (whose implicit lock
                          supplies the trailing fence a seq_cst store
                          needs), aarch64 `stlr`
:atomic_rmw dst <- rmw(a, b)  an atomic read-modify-write through pointer
                          a. b is a [value_vreg, kind] pair; `kind` is
                          :exchange, :fetch_add, :fetch_sub,
                          :add_fetch, :sub_fetch or :or_fetch. dst gets
                          the value the corresponding builtin returns —
                          the value read for :exchange and the
                          :fetch_* forms, the value stored for the
                          :*_fetch ones — and may be nil when the
                          result is discarded
:atomic_cas dst <- cas(a, b)  an atomic compare-and-exchange through
                          pointer a, for __atomic_compare_exchange_n.
                          b is an [expected_ptr_vreg, desired_vreg]
                          pair. If *a equals *expected_ptr, *a becomes
                          desired and dst gets 1; otherwise *a is
                          untouched, the value actually read is stored
                          back through expected_ptr — a side effect
                          callers depend on — and dst gets 0. The
                          write-back happens only on the failing path,
                          so a caller whose expected_ptr aliases a sees
                          the exchanged value rather than a stale one.
                          dst is a _Bool (0/1) and is never nil

dst, a, b are virtual register numbers (Integers) unless noted; unused fields are nil. size is an operand width in bytes. On :load / :uload / :store it is the memory access width (1 char, 2 short, 4 int, 8 pointer/long). On a binary op (the arithmetic, shift and comparison ops) size == 8 selects 64-bit arithmetic for long/unsigned long/pointer values and pointer-offset scaling; a nil (or 4) size means the default 32-bit arithmetic, whose natural wrap-around matches a 4-byte C type. On :sext / :zext, size is instead the source width being extended from. On the floating ops (:fadd..:fge) size is the floating operand width (4 float / 8 double); on :itof it is the destination float width, on :ftoi and :ftof the source float width, with the paired integer width carried in b as a [width, signed?] descriptor for :itof / :ftoi.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(op, dst: nil, a: nil, b: nil, size: nil) ⇒ Instruction

Returns a new instance of Instruction.



284
285
286
287
288
289
290
# File 'lib/rubycc/ir/ir.rb', line 284

def initialize(op, dst: nil, a: nil, b: nil, size: nil)
  @op = op
  @dst = dst
  @a = a
  @b = b
  @size = size
end

Instance Attribute Details

#aObject (readonly)

Returns the value of attribute a.



282
283
284
# File 'lib/rubycc/ir/ir.rb', line 282

def a
  @a
end

#bObject (readonly)

Returns the value of attribute b.



282
283
284
# File 'lib/rubycc/ir/ir.rb', line 282

def b
  @b
end

#dstObject (readonly)

Returns the value of attribute dst.



282
283
284
# File 'lib/rubycc/ir/ir.rb', line 282

def dst
  @dst
end

#opObject (readonly)

Returns the value of attribute op.



282
283
284
# File 'lib/rubycc/ir/ir.rb', line 282

def op
  @op
end

#sizeObject (readonly)

Returns the value of attribute size.



282
283
284
# File 'lib/rubycc/ir/ir.rb', line 282

def size
  @size
end

Instance Method Details

#inspectObject



292
293
294
# File 'lib/rubycc/ir/ir.rb', line 292

def inspect
  "#<IR #{op} dst=#{dst.inspect} a=#{a.inspect} b=#{b.inspect} size=#{size.inspect}>"
end