Class: Rubycc::IR::Instruction
- Inherits:
-
Object
- Object
- Rubycc::IR::Instruction
- 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
-
#a ⇒ Object
readonly
Returns the value of attribute a.
-
#b ⇒ Object
readonly
Returns the value of attribute b.
-
#dst ⇒ Object
readonly
Returns the value of attribute dst.
-
#op ⇒ Object
readonly
Returns the value of attribute op.
-
#size ⇒ Object
readonly
Returns the value of attribute size.
Instance Method Summary collapse
-
#initialize(op, dst: nil, a: nil, b: nil, size: nil) ⇒ Instruction
constructor
A new instance of Instruction.
- #inspect ⇒ Object
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
#a ⇒ Object (readonly)
Returns the value of attribute a.
282 283 284 |
# File 'lib/rubycc/ir/ir.rb', line 282 def a @a end |
#b ⇒ Object (readonly)
Returns the value of attribute b.
282 283 284 |
# File 'lib/rubycc/ir/ir.rb', line 282 def b @b end |
#dst ⇒ Object (readonly)
Returns the value of attribute dst.
282 283 284 |
# File 'lib/rubycc/ir/ir.rb', line 282 def dst @dst end |
#op ⇒ Object (readonly)
Returns the value of attribute op.
282 283 284 |
# File 'lib/rubycc/ir/ir.rb', line 282 def op @op end |
#size ⇒ Object (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
#inspect ⇒ Object
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 |