diff options
Diffstat (limited to 'lib/ruby_vm/rjit')
-rw-r--r-- | lib/ruby_vm/rjit/assembler.rb | 1126 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/block.rb | 11 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/branch_stub.rb | 24 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/c_pointer.rb | 360 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/c_type.rb | 91 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/code_block.rb | 86 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/compiler.rb | 310 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/context.rb | 30 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/exit_compiler.rb | 145 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/hooks.rb | 36 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/insn_compiler.rb | 3970 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/invariants.rb | 144 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/jit_state.rb | 57 | ||||
-rw-r--r-- | lib/ruby_vm/rjit/stats.rb | 102 |
14 files changed, 6492 insertions, 0 deletions
diff --git a/lib/ruby_vm/rjit/assembler.rb b/lib/ruby_vm/rjit/assembler.rb new file mode 100644 index 0000000000..cccd228c00 --- /dev/null +++ b/lib/ruby_vm/rjit/assembler.rb @@ -0,0 +1,1126 @@ +# frozen_string_literal: true +module RubyVM::MJIT + # 8-bit memory access + class BytePtr < Data.define(:reg, :disp); end + + # 32-bit memory access + class DwordPtr < Data.define(:reg, :disp); end + + # SystemV x64 calling convention + C_ARGS = [:rdi, :rsi, :rdx, :rcx, :r8, :r9] + C_RET = :rax + + # https://www.intel.com/content/dam/develop/public/us/en/documents/325383-sdm-vol-2abcd.pdf + # Mostly an x86_64 assembler, but this also has some stuff that is useful for any architecture. + class Assembler + # rel8 jumps are made with labels + class Label < Data.define(:id, :name); end + + # rel32 is inserted as [Rel32, Rel32Pad..] and converted on #resolve_rel32 + class Rel32 < Data.define(:addr); end + Rel32Pad = Object.new + + # A set of ModR/M values encoded on #insn + class ModRM < Data.define(:mod, :reg, :rm); end + Mod00 = 0b00 # Mod 00: [reg] + Mod01 = 0b01 # Mod 01: [reg]+disp8 + Mod10 = 0b10 # Mod 10: [reg]+disp32 + Mod11 = 0b11 # Mod 11: reg + + # REX = 0100WR0B + REX_B = 0b01000001 + REX_R = 0b01000100 + REX_W = 0b01001000 + + def initialize + @bytes = [] + @labels = {} + @label_id = 0 + @comments = Hash.new { |h, k| h[k] = [] } + @blocks = Hash.new { |h, k| h[k] = [] } + @stub_starts = Hash.new { |h, k| h[k] = [] } + @stub_ends = Hash.new { |h, k| h[k] = [] } + @pos_markers = Hash.new { |h, k| h[k] = [] } + end + + def assemble(addr) + set_code_addrs(addr) + resolve_rel32(addr) + resolve_labels + + write_bytes(addr) + + @pos_markers.each do |write_pos, markers| + markers.each { |marker| marker.call(addr + write_pos) } + end + @bytes.size + ensure + @bytes.clear + end + + def size + @bytes.size + end + + # + # Instructions + # + + def add(dst, src) + case [dst, src] + # ADD r/m64, imm8 (Mod 00: [reg]) + in [Array[Symbol => dst_reg], Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /0 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], + imm: imm8(src_imm), + ) + # ADD r/m64, imm8 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /0 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], + imm: imm8(src_imm), + ) + # ADD r/m64 imm32 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm32?(src_imm) + # REX.W + 81 /0 id + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x81, + mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], + imm: imm32(src_imm), + ) + # ADD r/m64, r64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) + # REX.W + 01 /r + # MR: Operand 1: ModRM:r/m (r, w), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x01, + mod_rm: ModRM[mod: Mod11, reg: src_reg, rm: dst_reg], + ) + else + raise NotImplementedError, "add: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def and(dst, src) + case [dst, src] + # AND r/m64, imm8 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /4 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod11, reg: 4, rm: dst_reg], + imm: imm8(src_imm), + ) + # AND r/m64, imm32 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm32?(src_imm) + # REX.W + 81 /4 id + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x81, + mod_rm: ModRM[mod: Mod11, reg: 4, rm: dst_reg], + imm: imm32(src_imm), + ) + # AND r64, r/m64 (Mod 01: [reg]+disp8) + in [Symbol => dst_reg, Array[Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) + # REX.W + 23 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x23, + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: imm8(src_disp), + ) + else + raise NotImplementedError, "and: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def call(dst) + case dst + # CALL rel32 + in Integer => dst_addr + # E8 cd + # D: Operand 1: Offset + insn(opcode: 0xe8, imm: rel32(dst_addr)) + # CALL r/m64 (Mod 11: reg) + in Symbol => dst_reg + # FF /2 + # M: Operand 1: ModRM:r/m (r) + insn( + opcode: 0xff, + mod_rm: ModRM[mod: Mod11, reg: 2, rm: dst_reg], + ) + else + raise NotImplementedError, "call: not-implemented operands: #{dst.inspect}" + end + end + + def cmove(dst, src) + case [dst, src] + # CMOVE r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] + # REX.W + 0F 44 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x44], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmove: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovg(dst, src) + case [dst, src] + # CMOVG r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] + # REX.W + 0F 4F /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x4f], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmovg: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovge(dst, src) + case [dst, src] + # CMOVGE r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] + # REX.W + 0F 4D /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x4d], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmovge: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovl(dst, src) + case [dst, src] + # CMOVL r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] + # REX.W + 0F 4C /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x4c], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmovl: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovle(dst, src) + case [dst, src] + # CMOVLE r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] + # REX.W + 0F 4E /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x4e], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmovle: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovnz(dst, src) + case [dst, src] + # CMOVNZ r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) + # REX.W + 0F 45 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x45], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + else + raise NotImplementedError, "cmovnz: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmovz(dst, src) + case [dst, src] + # CMOVZ r64, r/m64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) + # REX.W + 0F 44 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x44], + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + # CMOVZ r64, r/m64 (Mod 01: [reg]+disp8) + in [Symbol => dst_reg, Array[Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) + # REX.W + 0F 44 /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: [0x0f, 0x44], + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: imm8(src_disp), + ) + else + raise NotImplementedError, "cmovz: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def cmp(left, right) + case [left, right] + # CMP r/m8, imm8 (Mod 01: [reg]+disp8) + in [BytePtr[reg: left_reg, disp: left_disp], Integer => right_imm] if r64?(left_reg) && imm8?(left_disp) && imm8?(right_imm) + # 80 /7 ib + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + opcode: 0x80, + mod_rm: ModRM[mod: Mod01, reg: 7, rm: left_reg], + disp: left_disp, + imm: imm8(right_imm), + ) + # CMP r/m32, imm32 (Mod 01: [reg]+disp8) + in [DwordPtr[reg: left_reg, disp: left_disp], Integer => right_imm] if imm8?(left_disp) && imm32?(right_imm) + # 81 /7 id + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + opcode: 0x81, + mod_rm: ModRM[mod: Mod01, reg: 7, rm: left_reg], + disp: left_disp, + imm: imm32(right_imm), + ) + # CMP r/m64, imm8 (Mod 01: [reg]+disp8) + in [Array[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if r64?(left_reg) && imm8?(left_disp) && imm8?(right_imm) + # REX.W + 83 /7 ib + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod01, reg: 7, rm: left_reg], + disp: left_disp, + imm: imm8(right_imm), + ) + # CMP r/m64, imm8 (Mod 10: [reg]+disp32) + in [Array[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if r64?(left_reg) && imm32?(left_disp) && imm8?(right_imm) + # REX.W + 83 /7 ib + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod10, reg: 7, rm: left_reg], + disp: imm32(left_disp), + imm: imm8(right_imm), + ) + # CMP r/m64, imm8 (Mod 11: reg) + in [Symbol => left_reg, Integer => right_imm] if r64?(left_reg) && imm8?(right_imm) + # REX.W + 83 /7 ib + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod11, reg: 7, rm: left_reg], + imm: imm8(right_imm), + ) + # CMP r/m64, imm32 (Mod 11: reg) + in [Symbol => left_reg, Integer => right_imm] if r64?(left_reg) && imm32?(right_imm) + # REX.W + 81 /7 id + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x81, + mod_rm: ModRM[mod: Mod11, reg: 7, rm: left_reg], + imm: imm32(right_imm), + ) + # CMP r/m64, r64 (Mod 01: [reg]+disp8) + in [Array[Symbol => left_reg, Integer => left_disp], Symbol => right_reg] if r64?(right_reg) + # REX.W + 39 /r + # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x39, + mod_rm: ModRM[mod: Mod01, reg: right_reg, rm: left_reg], + disp: left_disp, + ) + # CMP r/m64, r64 (Mod 11: reg) + in [Symbol => left_reg, Symbol => right_reg] if r64?(left_reg) && r64?(right_reg) + # REX.W + 39 /r + # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x39, + mod_rm: ModRM[mod: Mod11, reg: right_reg, rm: left_reg], + ) + else + raise NotImplementedError, "cmp: not-implemented operands: #{left.inspect}, #{right.inspect}" + end + end + + def jbe(dst) + case dst + # JBE rel8 + in Label => dst_label + # 76 cb + insn(opcode: 0x76, imm: dst_label) + # JBE rel32 + in Integer => dst_addr + # 0F 86 cd + insn(opcode: [0x0f, 0x86], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jbe: not-implemented operands: #{dst.inspect}" + end + end + + def je(dst) + case dst + # JE rel8 + in Label => dst_label + # 74 cb + insn(opcode: 0x74, imm: dst_label) + # JE rel32 + in Integer => dst_addr + # 0F 84 cd + insn(opcode: [0x0f, 0x84], imm: rel32(dst_addr)) + else + raise NotImplementedError, "je: not-implemented operands: #{dst.inspect}" + end + end + + def jl(dst) + case dst + # JL rel32 + in Integer => dst_addr + # 0F 8C cd + insn(opcode: [0x0f, 0x8c], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jl: not-implemented operands: #{dst.inspect}" + end + end + + def jmp(dst) + case dst + # JZ rel8 + in Label => dst_label + # EB cb + insn(opcode: 0xeb, imm: dst_label) + # JMP rel32 + in Integer => dst_addr + # E9 cd + insn(opcode: 0xe9, imm: rel32(dst_addr)) + # JMP r/m64 (Mod 01: [reg]+disp8) + in Array[Symbol => dst_reg, Integer => dst_disp] if imm8?(dst_disp) + # FF /4 + insn(opcode: 0xff, mod_rm: ModRM[mod: Mod01, reg: 4, rm: dst_reg], disp: dst_disp) + # JMP r/m64 (Mod 11: reg) + in Symbol => dst_reg + # FF /4 + insn(opcode: 0xff, mod_rm: ModRM[mod: Mod11, reg: 4, rm: dst_reg]) + else + raise NotImplementedError, "jmp: not-implemented operands: #{dst.inspect}" + end + end + + def jne(dst) + case dst + # JNE rel32 + in Integer => dst_addr + # 0F 85 cd + insn(opcode: [0x0f, 0x85], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jne: not-implemented operands: #{dst.inspect}" + end + end + + def jnz(dst) + case dst + # JE rel8 + in Label => dst_label + # 75 cb + insn(opcode: 0x75, imm: dst_label) + # JNZ rel32 + in Integer => dst_addr + # 0F 85 cd + insn(opcode: [0x0f, 0x85], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jnz: not-implemented operands: #{dst.inspect}" + end + end + + def jo(dst) + case dst + # JO rel32 + in Integer => dst_addr + # 0F 80 cd + insn(opcode: [0x0f, 0x80], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jo: not-implemented operands: #{dst.inspect}" + end + end + + def jz(dst) + case dst + # JZ rel8 + in Label => dst_label + # 74 cb + insn(opcode: 0x74, imm: dst_label) + # JZ rel32 + in Integer => dst_addr + # 0F 84 cd + insn(opcode: [0x0f, 0x84], imm: rel32(dst_addr)) + else + raise NotImplementedError, "jz: not-implemented operands: #{dst.inspect}" + end + end + + def lea(dst, src) + case [dst, src] + # LEA r64,m (Mod 01: [reg]+disp8) + in [Symbol => dst_reg, Array[Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) + # REX.W + 8D /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8d, + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: imm8(src_disp), + ) + # LEA r64,m (Mod 10: [reg]+disp32) + in [Symbol => dst_reg, Array[Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm32?(src_disp) + # REX.W + 8D /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8d, + mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg], + disp: imm32(src_disp), + ) + else + raise NotImplementedError, "lea: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def mov(dst, src) + case dst + in Symbol => dst_reg + case src + # MOV r64, r/m64 (Mod 00: [reg]) + in Array[Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) + # REX.W + 8B /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8b, + mod_rm: ModRM[mod: Mod00, reg: dst_reg, rm: src_reg], + ) + # MOV r64, r/m64 (Mod 01: [reg]+disp8) + in Array[Symbol => src_reg, Integer => src_disp] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) + # REX.W + 8B /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8b, + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: src_disp, + ) + # MOV r64, r/m64 (Mod 10: [reg]+disp16) + in Array[Symbol => src_reg, Integer => src_disp] if r64?(dst_reg) && r64?(src_reg) && imm32?(src_disp) + # REX.W + 8B /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8b, + mod_rm: ModRM[mod: Mod10, reg: dst_reg, rm: src_reg], + disp: imm32(src_disp), + ) + # MOV r64, r/m64 (Mod 11: reg) + in Symbol => src_reg if r64?(dst_reg) && r64?(src_reg) + # REX.W + 8B /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x8b, + mod_rm: ModRM[mod: Mod11, reg: dst_reg, rm: src_reg], + ) + # MOV r32 r/m32 (Mod 01: [reg]+disp8) + in Array[Symbol => src_reg, Integer => src_disp] if r32?(dst_reg) && imm8?(src_disp) + # 8B /r + # RM: Operand 1: ModRM:reg (w), Operand 2: ModRM:r/m (r) + insn( + opcode: 0x8b, + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: src_disp, + ) + # MOV r32, imm32 (Mod 11: reg) + in Integer => src_imm if r32?(dst_reg) && imm32?(src_imm) + # B8+ rd id + # OI: Operand 1: opcode + rd (w), Operand 2: imm8/16/32/64 + insn( + opcode: 0xb8, + rd: dst_reg, + imm: imm32(src_imm), + ) + # MOV r/m64, imm32 (Mod 11: reg) + in Integer => src_imm if r64?(dst_reg) && imm32?(src_imm) + # REX.W + C7 /0 id + # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 + insn( + prefix: REX_W, + opcode: 0xc7, + mod_rm: ModRM[mod: Mod11, reg: 0, rm: dst_reg], + imm: imm32(src_imm), + ) + # MOV r64, imm64 + in Integer => src_imm if r64?(dst_reg) && imm64?(src_imm) + # REX.W + B8+ rd io + # OI: Operand 1: opcode + rd (w), Operand 2: imm8/16/32/64 + insn( + prefix: REX_W, + opcode: 0xb8, + rd: dst_reg, + imm: imm64(src_imm), + ) + else + raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + in Array[Symbol => dst_reg] + case src + # MOV r/m64, imm32 (Mod 00: [reg]) + in Integer => src_imm if r64?(dst_reg) && imm32?(src_imm) + # REX.W + C7 /0 id + # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 + insn( + prefix: REX_W, + opcode: 0xc7, + mod_rm: ModRM[mod: Mod00, reg: 0, rm: dst_reg], + imm: imm32(src_imm), + ) + # MOV r/m64, r64 (Mod 00: [reg]) + in Symbol => src_reg if r64?(dst_reg) && r64?(src_reg) + # REX.W + 89 /r + # MR: Operand 1: ModRM:r/m (w), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x89, + mod_rm: ModRM[mod: Mod00, reg: src_reg, rm: dst_reg], + ) + else + raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + in DwordPtr[reg: dst_reg, disp: dst_disp] + case src + # MOV r/m32, imm32 (Mod 01: [reg]+disp8) + in Integer => src_imm if r64?(dst_reg) && imm8?(dst_disp) && imm32?(src_imm) + # C7 /0 id + # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 + insn( + opcode: 0xc7, + mod_rm: ModRM[mod: Mod01, reg: 0, rm: dst_reg], + disp: dst_disp, + imm: imm32(src_imm), + ) + else + raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + in Array[Symbol => dst_reg, Integer => dst_disp] + # Optimize encoding when disp is 0 + return mov([dst_reg], src) if dst_disp == 0 + + case src + # MOV r/m64, imm32 (Mod 01: [reg]+disp8) + in Integer => src_imm if r64?(dst_reg) && imm8?(dst_disp) && imm32?(src_imm) + # REX.W + C7 /0 id + # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 + insn( + prefix: REX_W, + opcode: 0xc7, + mod_rm: ModRM[mod: Mod01, reg: 0, rm: dst_reg], + disp: dst_disp, + imm: imm32(src_imm), + ) + # MOV r/m64, imm32 (Mod 10: [reg]+disp32) + in Integer => src_imm if r64?(dst_reg) && imm32?(dst_disp) && imm32?(src_imm) + # REX.W + C7 /0 id + # MI: Operand 1: ModRM:r/m (w), Operand 2: imm8/16/32/64 + insn( + prefix: REX_W, + opcode: 0xc7, + mod_rm: ModRM[mod: Mod10, reg: 0, rm: dst_reg], + disp: imm32(dst_disp), + imm: imm32(src_imm), + ) + # MOV r/m64, r64 (Mod 01: [reg]+disp8) + in Symbol => src_reg if r64?(dst_reg) && imm8?(dst_disp) && r64?(src_reg) + # REX.W + 89 /r + # MR: Operand 1: ModRM:r/m (w), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x89, + mod_rm: ModRM[mod: Mod01, reg: src_reg, rm: dst_reg], + disp: dst_disp, + ) + # MOV r/m64, r64 (Mod 10: [reg]+disp32) + in Symbol => src_reg if r64?(dst_reg) && imm32?(dst_disp) && r64?(src_reg) + # REX.W + 89 /r + # MR: Operand 1: ModRM:r/m (w), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x89, + mod_rm: ModRM[mod: Mod10, reg: src_reg, rm: dst_reg], + disp: imm32(dst_disp), + ) + else + raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + else + raise NotImplementedError, "mov: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def or(dst, src) + case [dst, src] + # OR r/m64, imm8 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /1 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod11, reg: 1, rm: dst_reg], + imm: imm8(src_imm), + ) + # OR r/m64, imm32 (Mod 11: reg) + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm32?(src_imm) + # REX.W + 81 /1 id + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x81, + mod_rm: ModRM[mod: Mod11, reg: 1, rm: dst_reg], + imm: imm32(src_imm), + ) + # OR r64, r/m64 (Mod 01: [reg]+disp8) + in [Symbol => dst_reg, Array[Symbol => src_reg, Integer => src_disp]] if r64?(dst_reg) && r64?(src_reg) && imm8?(src_disp) + # REX.W + 0B /r + # RM: Operand 1: ModRM:reg (r, w), Operand 2: ModRM:r/m (r) + insn( + prefix: REX_W, + opcode: 0x0b, + mod_rm: ModRM[mod: Mod01, reg: dst_reg, rm: src_reg], + disp: imm8(src_disp), + ) + else + raise NotImplementedError, "or: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def push(src) + case src + # PUSH r64 + in Symbol => src_reg if r64?(src_reg) + # 50+rd + # O: Operand 1: opcode + rd (r) + insn(opcode: 0x50, rd: src_reg) + else + raise NotImplementedError, "push: not-implemented operands: #{src.inspect}" + end + end + + def pop(dst) + case dst + # POP r64 + in Symbol => dst_reg if r64?(dst_reg) + # 58+ rd + # O: Operand 1: opcode + rd (r) + insn(opcode: 0x58, rd: dst_reg) + else + raise NotImplementedError, "pop: not-implemented operands: #{dst.inspect}" + end + end + + def ret + # RET + # Near return: A return to a procedure within the current code segment + insn(opcode: 0xc3) + end + + def sar(dst, src) + case [dst, src] + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + C1 /7 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8 + insn( + prefix: REX_W, + opcode: 0xc1, + mod_rm: ModRM[mod: Mod11, reg: 7, rm: dst_reg], + imm: imm8(src_imm), + ) + else + raise NotImplementedError, "sar: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def sub(dst, src) + case [dst, src] + # SUB r/m64, imm8 + in [Symbol => dst_reg, Integer => src_imm] if r64?(dst_reg) && imm8?(src_imm) + # REX.W + 83 /5 ib + # MI: Operand 1: ModRM:r/m (r, w), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0x83, + mod_rm: ModRM[mod: Mod11, reg: 5, rm: dst_reg], + imm: imm8(src_imm), + ) + # SUB r/m64, r64 (Mod 11: reg) + in [Symbol => dst_reg, Symbol => src_reg] if r64?(dst_reg) && r64?(src_reg) + # REX.W + 29 /r + # MR: Operand 1: ModRM:r/m (r, w), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x29, + mod_rm: ModRM[mod: Mod11, reg: src_reg, rm: dst_reg], + ) + else + raise NotImplementedError, "sub: not-implemented operands: #{dst.inspect}, #{src.inspect}" + end + end + + def test(left, right) + case [left, right] + # TEST r/m8*, imm8 (Mod 01: [reg]+disp8) + in [BytePtr[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if imm8?(right_imm) && right_imm >= 0 + # REX + F6 /0 ib + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + opcode: 0xf6, + mod_rm: ModRM[mod: Mod01, reg: 0, rm: left_reg], + disp: left_disp, + imm: imm8(right_imm), + ) + # TEST r/m64, imm32 (Mod 01: [reg]+disp8) + in [Array[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if imm8?(left_disp) && imm32?(right_imm) + # REX.W + F7 /0 id + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0xf7, + mod_rm: ModRM[mod: Mod01, reg: 0, rm: left_reg], + disp: left_disp, + imm: imm32(right_imm), + ) + # TEST r/m64, imm32 (Mod 10: [reg]+disp32) + in [Array[Symbol => left_reg, Integer => left_disp], Integer => right_imm] if imm32?(left_disp) && imm32?(right_imm) + # REX.W + F7 /0 id + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0xf7, + mod_rm: ModRM[mod: Mod10, reg: 0, rm: left_reg], + disp: imm32(left_disp), + imm: imm32(right_imm), + ) + # TEST r/m64, imm32 (Mod 11: reg) + in [Symbol => left_reg, Integer => right_imm] if r64?(left_reg) && imm32?(right_imm) + # REX.W + F7 /0 id + # MI: Operand 1: ModRM:r/m (r), Operand 2: imm8/16/32 + insn( + prefix: REX_W, + opcode: 0xf7, + mod_rm: ModRM[mod: Mod11, reg: 0, rm: left_reg], + imm: imm32(right_imm), + ) + # TEST r/m32, r32 (Mod 11: reg) + in [Symbol => left_reg, Symbol => right_reg] if r32?(left_reg) && r32?(right_reg) + # 85 /r + # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) + insn( + opcode: 0x85, + mod_rm: ModRM[mod: Mod11, reg: right_reg, rm: left_reg], + ) + # TEST r/m64, r64 (Mod 11: reg) + in [Symbol => left_reg, Symbol => right_reg] if r64?(left_reg) && r64?(right_reg) + # REX.W + 85 /r + # MR: Operand 1: ModRM:r/m (r), Operand 2: ModRM:reg (r) + insn( + prefix: REX_W, + opcode: 0x85, + mod_rm: ModRM[mod: Mod11, reg: right_reg, rm: left_reg], + ) + else + raise NotImplementedError, "test: not-implemented operands: #{left.inspect}, #{right.inspect}" + end + end + + # + # Utilities + # + + attr_reader :comments + + def comment(message) + @comments[@bytes.size] << message + end + + # Mark the starting address of a block + def block(block) + @blocks[@bytes.size] << block + end + + # Mark the starting/ending addresses of a stub + def stub(stub) + @stub_starts[@bytes.size] << stub + yield + ensure + @stub_ends[@bytes.size] << stub + end + + def pos_marker(&block) + @pos_markers[@bytes.size] << block + end + + def new_label(name) + Label.new(id: @label_id += 1, name:) + end + + # @param [RubyVM::MJIT::Assembler::Label] label + def write_label(label) + @labels[label] = @bytes.size + end + + def incr_counter(name) + if C.mjit_opts.stats + comment("increment counter #{name}") + mov(:rax, C.rb_mjit_counters[name].to_i) + add([:rax], 1) # TODO: lock + end + end + + def imm32?(imm) + (-0x8000_0000..0x7fff_ffff).include?(imm) # TODO: consider uimm + end + + private + + def insn(prefix: 0, opcode:, rd: nil, mod_rm: nil, disp: nil, imm: nil) + # Determine prefix + if rd + prefix |= REX_B if extended_reg?(rd) + opcode += reg_code(rd) + end + if mod_rm + prefix |= REX_R if mod_rm.reg.is_a?(Symbol) && extended_reg?(mod_rm.reg) + prefix |= REX_B if mod_rm.rm.is_a?(Symbol) && extended_reg?(mod_rm.rm) + end + + # Encode insn + if prefix > 0 + @bytes.push(prefix) + end + @bytes.push(*Array(opcode)) + if mod_rm + mod_rm_byte = encode_mod_rm( + mod: mod_rm.mod, + reg: mod_rm.reg.is_a?(Symbol) ? reg_code(mod_rm.reg) : mod_rm.reg, + rm: mod_rm.rm.is_a?(Symbol) ? reg_code(mod_rm.rm) : mod_rm.rm, + ) + @bytes.push(mod_rm_byte) + end + if disp + @bytes.push(*Array(disp)) + end + if imm + @bytes.push(*imm) + end + end + + def reg_code(reg) + reg_code_extended(reg).first + end + + def extended_reg?(reg) + reg_code_extended(reg).last + end + + def reg_code_extended(reg) + case reg + # Not extended + when :al, :ax, :eax, :rax then [0, false] + when :cl, :cx, :ecx, :rcx then [1, false] + when :dl, :dx, :edx, :rdx then [2, false] + when :bl, :bx, :ebx, :rbx then [3, false] + when :ah, :sp, :esp, :rsp then [4, false] + when :ch, :bp, :ebp, :rbp then [5, false] + when :dh, :si, :esi, :rsi then [6, false] + when :bh, :di, :edi, :rdi then [7, false] + # Extended + when :r8b, :r8w, :r8d, :r8 then [0, true] + when :r9b, :r9w, :r9d, :r9 then [1, true] + when :r10b, :r10w, :r10d, :r10 then [2, true] + when :r11b, :r11w, :r11d, :r11 then [3, true] + when :r12b, :r12w, :r12d, :r12 then [4, true] + when :r13b, :r13w, :r13d, :r13 then [5, true] + when :r14b, :r14w, :r14d, :r14 then [6, true] + when :r15b, :r15w, :r15d, :r15 then [7, true] + else raise ArgumentError, "unexpected reg: #{reg.inspect}" + end + end + + # Table 2-2. 32-Bit Addressing Forms with the ModR/M Byte + # + # 7 6 5 4 3 2 1 0 + # +--+--+--+--+--+--+--+--+ + # | Mod | Reg/ | R/M | + # | | Opcode | | + # +--+--+--+--+--+--+--+--+ + # + # The r/m field can specify a register as an operand or it can be combined + # with the mod field to encode an addressing mode. + # + # /0: R/M is 0 (not used) + # /r: R/M is a register + def encode_mod_rm(mod:, reg: 0, rm: 0) + if mod > 0b11 + raise ArgumentError, "too large Mod: #{mod}" + end + if reg > 0b111 + raise ArgumentError, "too large Reg/Opcode: #{reg}" + end + if rm > 0b111 + raise ArgumentError, "too large R/M: #{rm}" + end + (mod << 6) + (reg << 3) + rm + end + + # ib: 1 byte + def imm8(imm) + unless imm8?(imm) + raise ArgumentError, "unexpected imm8: #{imm}" + end + [imm].pack('c').unpack('c*') # TODO: consider uimm + end + + # id: 4 bytes + def imm32(imm) + unless imm32?(imm) + raise ArgumentError, "unexpected imm32: #{imm}" + end + [imm].pack('l').unpack('c*') # TODO: consider uimm + end + + # io: 8 bytes + def imm64(imm) + unless imm64?(imm) + raise ArgumentError, "unexpected imm64: #{imm}" + end + imm_bytes(imm, 8) + end + + def imm_bytes(imm, num_bytes) + bytes = [] + bits = imm + num_bytes.times do + bytes << (bits & 0xff) + bits >>= 8 + end + if bits != 0 + raise ArgumentError, "unexpected imm with #{num_bytes} bytes: #{imm}" + end + bytes + end + + def imm8?(imm) + (-0x80..0x7f).include?(imm) + end + + def imm64?(imm) + (-0x8000_0000_0000_0000..0xffff_ffff_ffff_ffff).include?(imm) + end + + def r32?(reg) + if extended_reg?(reg) + reg.end_with?('d') + else + reg.start_with?('e') + end + end + + def r64?(reg) + if extended_reg?(reg) + reg.match?(/\Ar\d+\z/) + else + reg.start_with?('r') + end + end + + def rel32(addr) + [Rel32.new(addr), Rel32Pad, Rel32Pad, Rel32Pad] + end + + def set_code_addrs(write_addr) + (@bytes.size + 1).times do |index| + @blocks.fetch(index, []).each do |block| + block.start_addr = write_addr + index + end + @stub_starts.fetch(index, []).each do |stub| + stub.start_addr = write_addr + index + end + @stub_ends.fetch(index, []).each do |stub| + stub.end_addr = write_addr + index + end + end + end + + def resolve_rel32(write_addr) + @bytes.each_with_index do |byte, index| + if byte.is_a?(Rel32) + src_addr = write_addr + index + 4 # offset 4 bytes for rel32 itself + dst_addr = byte.addr + rel32 = dst_addr - src_addr + raise "unexpected offset: #{rel32}" unless imm32?(rel32) + imm32(rel32).each_with_index do |rel_byte, rel_index| + @bytes[index + rel_index] = rel_byte + end + end + end + end + + def resolve_labels + @bytes.each_with_index do |byte, index| + if byte.is_a?(Label) + src_index = index + 1 # offset 1 byte for rel8 itself + dst_index = @labels.fetch(byte) + rel8 = dst_index - src_index + raise "unexpected offset: #{rel8}" unless imm8?(rel8) + @bytes[index] = rel8 + end + end + end + + def write_bytes(addr) + Fiddle::Pointer.new(addr)[0, @bytes.size] = @bytes.pack('c*') + end + end +end diff --git a/lib/ruby_vm/rjit/block.rb b/lib/ruby_vm/rjit/block.rb new file mode 100644 index 0000000000..9b0c91ad40 --- /dev/null +++ b/lib/ruby_vm/rjit/block.rb @@ -0,0 +1,11 @@ +class RubyVM::MJIT::Block < Struct.new( + :iseq, # @param `` + :pc, # @param [Integer] Starting PC + :ctx, # @param [RubyVM::MJIT::Context] **Starting** Context (TODO: freeze?) + :start_addr, # @param [Integer] Starting address of this block's JIT code + :entry_exit, # @param [Integer] Address of entry exit (optional) + :incoming, # @param [Array<RubyVM::MJIT::BranchStub>] Incoming branches + :invalidated, # @param [TrueClass,FalseClass] true if already invalidated +) + def initialize(incoming: [], invalidated: false, **) = super +end diff --git a/lib/ruby_vm/rjit/branch_stub.rb b/lib/ruby_vm/rjit/branch_stub.rb new file mode 100644 index 0000000000..0f015e2f72 --- /dev/null +++ b/lib/ruby_vm/rjit/branch_stub.rb @@ -0,0 +1,24 @@ +module RubyVM::MJIT + # Branch shapes + Next0 = :Next0 # target0 is a fallthrough + Next1 = :Next1 # target1 is a fallthrough + Default = :Default # neither targets is a fallthrough + + class BranchStub < Struct.new( + :iseq, # @param [RubyVM::MJIT::CPointer::Struct_rb_iseq_struct] Branch target ISEQ + :shape, # @param [Symbol] Next0, Next1, or Default + :target0, # @param [RubyVM::MJIT::BranchTarget] First branch target + :target1, # @param [RubyVM::MJIT::BranchTarget,NilClass] Second branch target (optional) + :compile, # @param [Proc] A callback to (re-)generate this branch stub + :start_addr, # @param [Integer] Stub source start address to be re-generated + :end_addr, # @param [Integer] Stub source end address to be re-generated + ) + end + + class BranchTarget < Struct.new( + :pc, + :ctx, + :address, + ) + end +end diff --git a/lib/ruby_vm/rjit/c_pointer.rb b/lib/ruby_vm/rjit/c_pointer.rb new file mode 100644 index 0000000000..73cf267482 --- /dev/null +++ b/lib/ruby_vm/rjit/c_pointer.rb @@ -0,0 +1,360 @@ +module RubyVM::MJIT + # Every class under this namespace is a pointer. Even if the type is + # immediate, it shouldn't be dereferenced until `*` is called. + module CPointer + # Note: We'd like to avoid alphabetic method names to avoid a conflict + # with member methods. to_i and to_s are considered an exception. + class Struct + # @param name [String] + # @param sizeof [Integer] + # @param members [Hash{ Symbol => [RubyVM::MJIT::CType::*, Integer, TrueClass] }] + def initialize(addr, sizeof, members) + @addr = addr + @sizeof = sizeof + @members = members + end + + # Get a raw address + def to_i + @addr + end + + # Serialized address for generated code + def to_s + "0x#{@addr.to_s(16)}" + end + + # Pointer diff + def -(struct) + raise ArgumentError if self.class != struct.class + (@addr - struct.to_i) / @sizeof + end + + # Primitive API that does no automatic dereference + # TODO: remove this? + # @param member [Symbol] + def [](member) + type, offset = @members.fetch(member) + type.new(@addr + offset / 8) + end + + private + + # @param member [Symbol] + # @param value [Object] + def []=(member, value) + type, offset = @members.fetch(member) + type[@addr + offset / 8] = value + end + + # @param size [Integer] + # @param members [Hash{ Symbol => [Integer, RubyVM::MJIT::CType::*] }] + def self.define(size, members) + Class.new(self) do + # Return the size of this type + define_singleton_method(:size) { size } + + # Return the offset to a field + define_singleton_method(:offsetof) do |field, *fields| + member, offset = members.fetch(field) + offset /= 8 + unless fields.empty? + offset += member.offsetof(*fields) + end + offset + end + + # Return member names + define_singleton_method(:members) { members.keys } + + define_method(:initialize) do |addr = nil| + if addr.nil? # TODO: get rid of this feature later + addr = Fiddle.malloc(size) + end + super(addr, size, members) + end + + members.each do |member, (type, offset, to_ruby)| + # Intelligent API that does automatic dereference + define_method(member) do + value = self[member] + if value.respond_to?(:*) + value = value.* + end + if to_ruby + value = C.to_ruby(value) + end + value + end + + define_method("#{member}=") do |value| + if to_ruby + value = C.to_value(value) + end + self[member] = value + end + end + end + end + end + + # Note: We'd like to avoid alphabetic method names to avoid a conflict + # with member methods. to_i is considered an exception. + class Union + # @param _name [String] To be used when it starts defining a union pointer class + # @param sizeof [Integer] + # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] + def initialize(addr, sizeof, members) + @addr = addr + @sizeof = sizeof + @members = members + end + + # Get a raw address + def to_i + @addr + end + + # Move addr to access this pointer like an array + def +(index) + raise ArgumentError unless index.is_a?(Integer) + self.class.new(@addr + index * @sizeof) + end + + # Pointer diff + def -(union) + raise ArgumentError if self.class != union.class + (@addr - union.instance_variable_get(:@addr)) / @sizeof + end + + # @param sizeof [Integer] + # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] + def self.define(sizeof, members) + Class.new(self) do + # Return the size of this type + define_singleton_method(:sizeof) { sizeof } + + # Part of Struct's offsetof implementation + define_singleton_method(:offsetof) do |field, *fields| + member = members.fetch(field) + offset = 0 + unless fields.empty? + offset += member.offsetof(*fields) + end + offset + end + + define_method(:initialize) do |addr| + super(addr, sizeof, members) + end + + members.each do |member, type| + # Intelligent API that does automatic dereference + define_method(member) do + value = type.new(@addr) + if value.respond_to?(:*) + value = value.* + end + value + end + end + end + end + end + + class Immediate + # @param addr [Integer] + # @param size [Integer] + # @param pack [String] + def initialize(addr, size, pack) + @addr = addr + @size = size + @pack = pack + end + + # Get a raw address + def to_i + @addr + end + + # Move addr to addess this pointer like an array + def +(index) + Immediate.new(@addr + index * @size, @size, @pack) + end + + # Dereference + def * + self[0] + end + + # Array access + def [](index) + return nil if @addr == 0 + Fiddle::Pointer.new(@addr + index * @size)[0, @size].unpack1(@pack) + end + + # Array set + def []=(index, value) + Fiddle::Pointer.new(@addr + index * @size)[0, @size] = [value].pack(@pack) + end + + # Serialized address for generated code. Used for embedding things like body->iseq_encoded. + def to_s + "0x#{Integer(@addr).to_s(16)}" + end + + # @param fiddle_type [Integer] Fiddle::TYPE_* + def self.define(fiddle_type) + size = Fiddle::PackInfo::SIZE_MAP.fetch(fiddle_type) + pack = Fiddle::PackInfo::PACK_MAP.fetch(fiddle_type) + + Class.new(self) do + define_method(:initialize) do |addr| + super(addr, size, pack) + end + + define_singleton_method(:size) do + size + end + + # Type-level []=: Used by struct fields + define_singleton_method(:[]=) do |addr, value| + Fiddle::Pointer.new(addr)[0, size] = [value].pack(pack) + end + end + end + end + + # -Fiddle::TYPE_CHAR Immediate with special handling of true/false + class Bool < Immediate.define(-Fiddle::TYPE_CHAR) + # Dereference + def * + return nil if @addr == 0 + super != 0 + end + + def self.[]=(addr, value) + super(addr, value ? 1 : 0) + end + end + + class Pointer + attr_reader :type + + # @param addr [Integer] + # @param type [Class] RubyVM::MJIT::CType::* + def initialize(addr, type) + @addr = addr + @type = type + end + + # Move addr to addess this pointer like an array + def +(index) + raise ArgumentError unless index.is_a?(Integer) + Pointer.new(@addr + index * Fiddle::SIZEOF_VOIDP, @type) + end + + # Dereference + def * + return nil if dest_addr == 0 + @type.new(dest_addr) + end + + # Array access + def [](index) + (self + index).* + end + + # Array set + # @param index [Integer] + # @param value [Integer, RubyVM::MJIT::CPointer::Struct] an address itself or an object that return an address with to_i + def []=(index, value) + Fiddle::Pointer.new(@addr + index * Fiddle::SIZEOF_VOIDP)[0, Fiddle::SIZEOF_VOIDP] = + [value.to_i].pack(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) + end + + # Get a raw address + def to_i + @addr + end + + private + + def dest_addr + Fiddle::Pointer.new(@addr)[0, Fiddle::SIZEOF_VOIDP].unpack1(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) + end + + def self.define(block) + Class.new(self) do + define_method(:initialize) do |addr| + super(addr, block.call) + end + + # Type-level []=: Used by struct fields + # @param addr [Integer] + # @param value [Integer, RubyVM::MJIT::CPointer::Struct] an address itself, or an object that return an address with to_i + define_singleton_method(:[]=) do |addr, value| + value = value.to_i + Fiddle::Pointer.new(addr)[0, Fiddle::SIZEOF_VOIDP] = [value].pack(Fiddle::PackInfo::PACK_MAP[Fiddle::TYPE_VOIDP]) + end + end + end + end + + class BitField + # @param addr [Integer] + # @param width [Integer] + # @param offset [Integer] + def initialize(addr, width, offset) + @addr = addr + @width = width + @offset = offset + end + + # Dereference + def * + byte = Fiddle::Pointer.new(@addr)[0, Fiddle::SIZEOF_CHAR].unpack('c').first + if @width == 1 + bit = (1 & (byte >> @offset)) + bit == 1 + elsif @width <= 8 && @offset == 0 + bitmask = @width.times.map { |i| 1 << i }.sum + byte & bitmask + else + raise NotImplementedError.new("not-implemented bit field access: width=#{@width} offset=#{@offset}") + end + end + + # @param width [Integer] + # @param offset [Integer] + def self.define(width, offset) + Class.new(self) do + define_method(:initialize) do |addr| + super(addr, width, offset) + end + end + end + end + + # Give a name to a dynamic CPointer class to see it on inspect + def self.with_class_name(prefix, name, cache: false, &block) + return block.call if name.empty? + + # Use a cached result only if cache: true + class_name = "#{prefix}_#{name}" + klass = + if cache && self.const_defined?(class_name) + self.const_get(class_name) + else + block.call + end + + # Give it a name unless it's already defined + unless self.const_defined?(class_name) + self.const_set(class_name, klass) + end + + klass + end + end +end diff --git a/lib/ruby_vm/rjit/c_type.rb b/lib/ruby_vm/rjit/c_type.rb new file mode 100644 index 0000000000..9e45d8d41c --- /dev/null +++ b/lib/ruby_vm/rjit/c_type.rb @@ -0,0 +1,91 @@ +require 'fiddle' +require 'fiddle/pack' +require_relative 'c_pointer' + +module RubyVM::MJIT + module CType + module Struct + # @param name [String] + # @param members [Hash{ Symbol => [Integer, RubyVM::MJIT::CType::*] }] + def self.new(name, sizeof, **members) + name = members.keys.join('_') if name.empty? + CPointer.with_class_name('Struct', name) do + CPointer::Struct.define(sizeof, members) + end + end + end + + module Union + # @param name [String] + # @param members [Hash{ Symbol => RubyVM::MJIT::CType::* }] + def self.new(name, sizeof, **members) + name = members.keys.join('_') if name.empty? + CPointer.with_class_name('Union', name) do + CPointer::Union.define(sizeof, members) + end + end + end + + module Immediate + # @param fiddle_type [Integer] + def self.new(fiddle_type) + name = Fiddle.constants.find do |const| + const.start_with?('TYPE_') && Fiddle.const_get(const) == fiddle_type.abs + end&.to_s + name.delete_prefix!('TYPE_') + if fiddle_type.negative? + name.prepend('U') + end + CPointer.with_class_name('Immediate', name, cache: true) do + CPointer::Immediate.define(fiddle_type) + end + end + + # @param type [String] + def self.parse(ctype) + new(Fiddle::Importer.parse_ctype(ctype)) + end + + def self.find(size, signed) + fiddle_type = TYPE_MAP.fetch(size) + fiddle_type = -fiddle_type unless signed + new(fiddle_type) + end + + TYPE_MAP = Fiddle::PackInfo::SIZE_MAP.map { |type, size| [size, type.abs] }.to_h + private_constant :TYPE_MAP + end + + module Bool + def self.new + CPointer::Bool + end + end + + class Pointer + # This takes a block to avoid "stack level too deep" on a cyclic reference + # @param block [Proc] + def self.new(&block) + CPointer.with_class_name('Pointer', block.object_id.to_s) do + CPointer::Pointer.define(block) + end + end + end + + module BitField + # @param width [Integer] + # @param offset [Integer] + def self.new(width, offset) + CPointer.with_class_name('BitField', "#{offset}_#{width}") do + CPointer::BitField.define(width, offset) + end + end + end + + # Types that are referenced but not part of code generation targets + Stub = ::Struct.new(:name) + + # Types that it failed to figure out from the header + Unknown = Module.new + end +end diff --git a/lib/ruby_vm/rjit/code_block.rb b/lib/ruby_vm/rjit/code_block.rb new file mode 100644 index 0000000000..21ae2386b7 --- /dev/null +++ b/lib/ruby_vm/rjit/code_block.rb @@ -0,0 +1,86 @@ +module RubyVM::MJIT + class CodeBlock + # @param mem_block [Integer] JIT buffer address + # @param mem_size [Integer] JIT buffer size + # @param outliend [TrueClass,FalseClass] true for outlined CodeBlock + def initialize(mem_block:, mem_size:, outlined: false) + @comments = Hash.new { |h, k| h[k] = [] } + @mem_block = mem_block + @mem_size = mem_size + @write_pos = 0 + @outlined = outlined + end + + # @param asm [RubyVM::MJIT::Assembler] + def write(asm) + return 0 if @write_pos + asm.size >= @mem_size + + start_addr = write_addr + + # Write machine code + C.mjit_mark_writable + @write_pos += asm.assemble(start_addr) + C.mjit_mark_executable + + end_addr = write_addr + + # Convert comment indexes to addresses + asm.comments.each do |index, comments| + @comments[start_addr + index] += comments + end + asm.comments.clear + + # Dump disasm if --mjit-dump-disasm + if C.mjit_opts.dump_disasm && start_addr < end_addr + dump_disasm(start_addr, end_addr) + end + start_addr + end + + def set_write_addr(addr) + @write_pos = addr - @mem_block + @comments.delete(addr) # TODO: clean up old comments for all the overwritten range? + end + + def with_write_addr(addr) + old_write_pos = @write_pos + set_write_addr(addr) + yield + ensure + @write_pos = old_write_pos + end + + def write_addr + @mem_block + @write_pos + end + + def include?(addr) + (@mem_block...(@mem_block + @mem_size)).include?(addr) + end + + private + + def dump_disasm(from, to) + C.dump_disasm(from, to).each do |address, mnemonic, op_str| + @comments.fetch(address, []).each do |comment| + puts colorize(" # #{comment}", bold: true) + end + puts colorize(" 0x#{format("%x", address)}: #{mnemonic} #{op_str}") + end + puts + end + + def colorize(text, bold: false) + buf = +'' + buf << "\e[1m" if bold + buf << "\e[34m" if @outlined + buf << text + buf << "\e[0m" + buf + end + + def bold(text) + "\e[1m#{text}\e[0m" + end + end +end diff --git a/lib/ruby_vm/rjit/compiler.rb b/lib/ruby_vm/rjit/compiler.rb new file mode 100644 index 0000000000..85e6b5fa06 --- /dev/null +++ b/lib/ruby_vm/rjit/compiler.rb @@ -0,0 +1,310 @@ +require 'ruby_vm/mjit/assembler' +require 'ruby_vm/mjit/block' +require 'ruby_vm/mjit/branch_stub' +require 'ruby_vm/mjit/code_block' +require 'ruby_vm/mjit/context' +require 'ruby_vm/mjit/exit_compiler' +require 'ruby_vm/mjit/insn_compiler' +require 'ruby_vm/mjit/instruction' +require 'ruby_vm/mjit/invariants' +require 'ruby_vm/mjit/jit_state' + +module RubyVM::MJIT + # Compilation status + KeepCompiling = :KeepCompiling + CantCompile = :CantCompile + EndBlock = :EndBlock + + # Ruby constants + Qtrue = Fiddle::Qtrue + Qfalse = Fiddle::Qfalse + Qnil = Fiddle::Qnil + Qundef = Fiddle::Qundef + + # Callee-saved registers + # TODO: support using r12/r13 here + EC = :r14 + CFP = :r15 + SP = :rbx + + # Scratch registers: rax, rcx + + # Mark objects in this Array during GC + GC_REFS = [] + + class Compiler + attr_accessor :write_pos + + def self.decode_insn(encoded) + INSNS.fetch(C.rb_vm_insn_decode(encoded)) + end + + # @param mem_block [Integer] JIT buffer address + # @param mem_size [Integer] JIT buffer size + def initialize(mem_block, mem_size) + @cb = CodeBlock.new(mem_block: mem_block, mem_size: mem_size / 2) + @ocb = CodeBlock.new(mem_block: mem_block + mem_size / 2, mem_size: mem_size / 2, outlined: true) + @exit_compiler = ExitCompiler.new + @insn_compiler = InsnCompiler.new(@cb, @ocb, @exit_compiler) + Invariants.initialize(@cb, @ocb, self, @exit_compiler) + + @leave_exit = Assembler.new.then do |asm| + @exit_compiler.compile_leave_exit(asm) + @ocb.write(asm) + end + end + + # Compile an ISEQ from its entry point. + # @param iseq `RubyVM::MJIT::CPointer::Struct_rb_iseq_t` + # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` + def compile(iseq, cfp) + # TODO: Support has_opt + return if iseq.body.param.flags.has_opt + + jit = JITState.new(iseq:, cfp:) + asm = Assembler.new + asm.comment("Block: #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{iseq.body.location.first_lineno}") + compile_prologue(asm) + compile_block(asm, jit:) + iseq.body.jit_func = @cb.write(asm) + rescue Exception => e + $stderr.puts e.full_message + exit 1 + end + + # Compile a branch stub. + # @param branch_stub [RubyVM::MJIT::BranchStub] + # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` + # @param target0_p [TrueClass,FalseClass] + # @return [Integer] The starting address of the compiled branch stub + def branch_stub_hit(branch_stub, cfp, target0_p) + # Update cfp->pc for `jit.at_current_insn?` + target = target0_p ? branch_stub.target0 : branch_stub.target1 + cfp.pc = target.pc + + # Reuse an existing block if it already exists + block = find_block(branch_stub.iseq, target.pc, target.ctx) + + # If the branch stub's jump is the last code, allow overwriting part of + # the old branch code with the new block code. + fallthrough = block.nil? && @cb.write_addr == branch_stub.end_addr + if fallthrough + # If the branch stub's jump is the last code, allow overwriting part of + # the old branch code with the new block code. + @cb.set_write_addr(branch_stub.start_addr) + branch_stub.shape = target0_p ? Next0 : Next1 + Assembler.new.tap do |branch_asm| + branch_stub.compile.call(branch_asm) + @cb.write(branch_asm) + end + end + + # Reuse or generate a block + if block + target.address = block.start_addr + else + jit = JITState.new(iseq: branch_stub.iseq, cfp:) + target.address = Assembler.new.then do |asm| + compile_block(asm, jit:, pc: target.pc, ctx: target.ctx.dup) + @cb.write(asm) + end + block = jit.block + end + block.incoming << branch_stub # prepare for invalidate_block + + # Re-generate the branch code for non-fallthrough cases + unless fallthrough + @cb.with_write_addr(branch_stub.start_addr) do + branch_asm = Assembler.new + branch_stub.compile.call(branch_asm) + @cb.write(branch_asm) + end + end + + return target.address + rescue Exception => e + $stderr.puts e.full_message + exit 1 + end + + # @param iseq `RubyVM::MJIT::CPointer::Struct_rb_iseq_t` + # @param pc [Integer] + def invalidate_blocks(iseq, pc) + list_blocks(iseq, pc).each do |block| + invalidate_block(block) + end + + # If they were the ISEQ's first blocks, re-compile MJIT entry as well + if iseq.body.iseq_encoded.to_i == pc + iseq.body.jit_func = 0 + iseq.body.total_calls = 0 + end + end + + def invalidate_block(block) + iseq = block.iseq + # Avoid touching GCed ISEQs. We assume it won't be re-entered. + return if C.imemo_type(iseq) != C.imemo_iseq + + # Remove this block from the version array + remove_block(iseq, block) + + # Invalidate the block with entry exit + unless block.invalidated + @cb.with_write_addr(block.start_addr) do + asm = Assembler.new + asm.comment('invalidate_block') + asm.jmp(block.entry_exit) + @cb.write(asm) + end + block.invalidated = true + end + + # Re-stub incoming branches + block.incoming.each do |branch_stub| + target = [branch_stub.target0, branch_stub.target1].compact.find do |target| + target.pc == block.pc && target.ctx == block.ctx + end + next if target.nil? + # TODO: Could target.address be a stub address? Is invalidation not needed in that case? + + # If the target being re-generated is currently a fallthrough block, + # the fallthrough code must be rewritten with a jump to the stub. + if target.address == branch_stub.end_addr + branch_stub.shape = Default + end + + target.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(block.ctx, ocb_asm, branch_stub, target == branch_stub.target0) + @ocb.write(ocb_asm) + end + @cb.with_write_addr(branch_stub.start_addr) do + branch_asm = Assembler.new + branch_stub.compile.call(branch_asm) + @cb.write(branch_asm) + end + end + end + + private + + # Callee-saved: rbx, rsp, rbp, r12, r13, r14, r15 + # Caller-saved: rax, rdi, rsi, rdx, rcx, r8, r9, r10, r11 + # + # @param asm [RubyVM::MJIT::Assembler] + def compile_prologue(asm) + asm.comment('MJIT entry point') + + # Save callee-saved registers used by JITed code + asm.push(CFP) + asm.push(EC) + asm.push(SP) + + # Move arguments EC and CFP to dedicated registers + asm.mov(EC, :rdi) + asm.mov(CFP, :rsi) + + # Load sp to a dedicated register + asm.mov(SP, [CFP, C.rb_control_frame_t.offsetof(:sp)]) # rbx = cfp->sp + + # Setup cfp->jit_return + asm.mov(:rax, @leave_exit) + asm.mov([CFP, C.rb_control_frame_t.offsetof(:jit_return)], :rax) + end + + # @param asm [RubyVM::MJIT::Assembler] + def compile_block(asm, jit:, pc: jit.iseq.body.iseq_encoded.to_i, ctx: Context.new) + # Mark the block start address and prepare an exit code storage + block = Block.new(iseq: jit.iseq, pc:, ctx: ctx.dup) + jit.block = block + asm.block(block) + + # Compile each insn + iseq = jit.iseq + index = (pc - iseq.body.iseq_encoded.to_i) / C.VALUE.size + while index < iseq.body.iseq_size + insn = self.class.decode_insn(iseq.body.iseq_encoded[index]) + jit.pc = (iseq.body.iseq_encoded + index).to_i + + # If previous instruction requested to record the boundary + if jit.record_boundary_patch_point + # Generate an exit to this instruction and record it + exit_pos = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_side_exit(jit.pc, ctx, ocb_asm) + @ocb.write(ocb_asm) + end + Invariants.record_global_inval_patch(asm, exit_pos) + jit.record_boundary_patch_point = false + end + + case status = @insn_compiler.compile(jit, ctx, asm, insn) + when KeepCompiling + # For now, reset the chain depth after each instruction as only the + # first instruction in the block can concern itself with the depth. + ctx.chain_depth = 0 + + index += insn.len + when EndBlock + # TODO: pad nops if entry exit exists (not needed for x86_64?) + break + when CantCompile + @exit_compiler.compile_side_exit(jit.pc, ctx, asm) + + # If this is the first instruction, this block never needs to be invalidated. + if block.pc == iseq.body.iseq_encoded.to_i + index * C.VALUE.size + block.invalidated = true + end + + break + else + raise "compiling #{insn.name} returned unexpected status: #{status.inspect}" + end + end + + incr_counter(:compiled_block_count) + set_block(iseq, block) + end + + def incr_counter(name) + if C.mjit_opts.stats + C.rb_mjit_counters[name][0] += 1 + end + end + + def list_blocks(iseq, pc) + mjit_blocks(iseq)[pc].values + end + + # @param [Integer] pc + # @param [RubyVM::MJIT::Context] ctx + # @return [RubyVM::MJIT::Block,NilClass] + def find_block(iseq, pc, ctx) + mjit_blocks(iseq)[pc][ctx] + end + + # @param [RubyVM::MJIT::Block] block + def set_block(iseq, block) + mjit_blocks(iseq)[block.pc][block.ctx] = block + end + + # @param [RubyVM::MJIT::Block] block + def remove_block(iseq, block) + mjit_blocks(iseq)[block.pc].delete(block.ctx) + end + + def mjit_blocks(iseq) + # Guard against ISEQ GC at random moments + if C.imemo_type(iseq) != C.imemo_iseq + return Hash.new { |h, k| h[k] = {} } + end + + unless iseq.body.mjit_blocks + iseq.body.mjit_blocks = Hash.new { |h, k| h[k] = {} } + # For some reason, rb_mjit_iseq_mark didn't protect this Hash + # from being freed. So we rely on GC_REFS to keep the Hash. + GC_REFS << iseq.body.mjit_blocks + end + iseq.body.mjit_blocks + end + end +end diff --git a/lib/ruby_vm/rjit/context.rb b/lib/ruby_vm/rjit/context.rb new file mode 100644 index 0000000000..e834b42999 --- /dev/null +++ b/lib/ruby_vm/rjit/context.rb @@ -0,0 +1,30 @@ +module RubyVM::MJIT + class Context < Struct.new( + :stack_size, # @param [Integer] The number of values on the stack + :sp_offset, # @param [Integer] JIT sp offset relative to the interpreter's sp + :chain_depth, # @param [Integer] jit_chain_guard depth + ) + def initialize(stack_size: 0, sp_offset: 0, chain_depth: 0) = super + + def stack_push(size = 1) + self.stack_size += size + self.sp_offset += size + stack_opnd(0) + end + + def stack_pop(size = 1) + opnd = stack_opnd(0) + self.stack_size -= size + self.sp_offset -= size + opnd + end + + def stack_opnd(depth_from_top) + [SP, C.VALUE.size * (self.sp_offset - 1 - depth_from_top)] + end + + def sp_opnd(offset_bytes = 0) + [SP, (C.VALUE.size * self.sp_offset) + offset_bytes] + end + end +end diff --git a/lib/ruby_vm/rjit/exit_compiler.rb b/lib/ruby_vm/rjit/exit_compiler.rb new file mode 100644 index 0000000000..1d704a8dca --- /dev/null +++ b/lib/ruby_vm/rjit/exit_compiler.rb @@ -0,0 +1,145 @@ +module RubyVM::MJIT + class ExitCompiler + # Used for invalidating a block on entry. + # @param pc [Integer] + # @param asm [RubyVM::MJIT::Assembler] + def compile_entry_exit(pc, ctx, asm, cause:) + # Increment per-insn exit counter + incr_insn_exit(pc, asm) + + # Fix pc/sp offsets for the interpreter + save_pc_and_sp(pc, ctx, asm, reset_sp_offset: false) + + # Restore callee-saved registers + asm.comment("#{cause}: entry exit") + asm.pop(SP) + asm.pop(EC) + asm.pop(CFP) + + asm.mov(C_RET, Qundef) + asm.ret + end + + # Set to cfp->jit_return by default for leave insn + # @param asm [RubyVM::MJIT::Assembler] + def compile_leave_exit(asm) + asm.comment('default cfp->jit_return') + + # Restore callee-saved registers + asm.pop(SP) + asm.pop(EC) + asm.pop(CFP) + + # :rax is written by #leave + asm.ret + end + + # Fire cfunc events on invalidation by TracePoint + # @param asm [RubyVM::MJIT::Assembler] + def compile_full_cfunc_return(asm) + # This chunk of code expects REG_EC to be filled properly and + # RAX to contain the return value of the C method. + + asm.comment('full cfunc return') + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], :rax) + asm.call(C.rb_full_cfunc_return) + + # TODO: count the exit + + # Restore callee-saved registers + asm.pop(SP) + asm.pop(EC) + asm.pop(CFP) + + asm.mov(C_RET, Qundef) + asm.ret + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def compile_side_exit(pc, ctx, asm) + # Increment per-insn exit counter + incr_insn_exit(pc, asm) + + # Fix pc/sp offsets for the interpreter + save_pc_and_sp(pc, ctx.dup, asm) # dup to avoid sp_offset update + + # Restore callee-saved registers + asm.comment("exit to interpreter on #{pc_to_insn(pc).name}") + asm.pop(SP) + asm.pop(EC) + asm.pop(CFP) + + asm.mov(C_RET, Qundef) + asm.ret + end + + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + # @param branch_stub [RubyVM::MJIT::BranchStub] + # @param target0_p [TrueClass,FalseClass] + def compile_branch_stub(ctx, asm, branch_stub, target0_p) + # Call rb_mjit_branch_stub_hit + iseq = branch_stub.iseq + if C.mjit_opts.dump_disasm && C.imemo_type(iseq) == C.imemo_iseq # Guard against ISEQ GC at random moments + asm.comment("branch stub hit: #{iseq.body.location.label}@#{C.rb_iseq_path(iseq)}:#{iseq_lineno(iseq, target0_p ? branch_stub.target0.pc : branch_stub.target1.pc)}") + end + asm.mov(:rdi, to_value(branch_stub)) + asm.mov(:esi, ctx.sp_offset) + asm.mov(:edx, target0_p ? 1 : 0) + asm.call(C.rb_mjit_branch_stub_hit) + + # Jump to the address returned by rb_mjit_stub_hit + asm.jmp(:rax) + end + + private + + def pc_to_insn(pc) + Compiler.decode_insn(C.VALUE.new(pc).*) + end + + # @param pc [Integer] + # @param asm [RubyVM::MJIT::Assembler] + def incr_insn_exit(pc, asm) + if C.mjit_opts.stats + insn = Compiler.decode_insn(C.VALUE.new(pc).*) + asm.comment("increment insn exit: #{insn.name}") + asm.mov(:rax, (C.mjit_insn_exits + insn.bin).to_i) + asm.add([:rax], 1) # TODO: lock + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def save_pc_and_sp(pc, ctx, asm, reset_sp_offset: true) + # Update pc (TODO: manage PC offset?) + asm.comment("save PC#{' and SP' if ctx.sp_offset != 0} to CFP") + asm.mov(:rax, pc) # rax = jit.pc + asm.mov([CFP, C.rb_control_frame_t.offsetof(:pc)], :rax) # cfp->pc = rax + + # Update sp + if ctx.sp_offset != 0 + asm.add(SP, C.VALUE.size * ctx.sp_offset) # sp += stack_size + asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) # cfp->sp = sp + if reset_sp_offset + ctx.sp_offset = 0 + end + end + end + + def to_value(obj) + GC_REFS << obj + C.to_value(obj) + end + + def iseq_lineno(iseq, pc) + C.rb_iseq_line_no(iseq, (pc - iseq.body.iseq_encoded.to_i) / C.VALUE.size) + rescue RangeError # bignum too big to convert into `unsigned long long' (RangeError) + -1 + end + end +end diff --git a/lib/ruby_vm/rjit/hooks.rb b/lib/ruby_vm/rjit/hooks.rb new file mode 100644 index 0000000000..57d4ebc381 --- /dev/null +++ b/lib/ruby_vm/rjit/hooks.rb @@ -0,0 +1,36 @@ +module RubyVM::MJIT + module Hooks # :nodoc: all + def self.on_bop_redefined(_redefined_flag, _bop) + # C.mjit_cancel_all("BOP is redefined") + end + + def self.on_cme_invalidate(cme) + cme = C.rb_callable_method_entry_struct.new(cme) + Invariants.on_cme_invalidate(cme) + end + + def self.on_ractor_spawn + # C.mjit_cancel_all("Ractor is spawned") + end + + # Global constant changes like const_set + def self.on_constant_state_changed(id) + Invariants.on_constant_state_changed(id) + end + + # ISEQ-specific constant invalidation + def self.on_constant_ic_update(iseq, ic, insn_idx) + iseq = C.rb_iseq_t.new(iseq) + ic = C.IC.new(ic) + Invariants.on_constant_ic_update(iseq, ic, insn_idx) + end + + def self.on_tracing_invalidate_all(_new_iseq_events) + Invariants.on_tracing_invalidate_all + end + + def self.on_update_references + Invariants.on_update_references + end + end +end diff --git a/lib/ruby_vm/rjit/insn_compiler.rb b/lib/ruby_vm/rjit/insn_compiler.rb new file mode 100644 index 0000000000..07670daed1 --- /dev/null +++ b/lib/ruby_vm/rjit/insn_compiler.rb @@ -0,0 +1,3970 @@ +module RubyVM::MJIT + class InsnCompiler + # @param ocb [CodeBlock] + # @param exit_compiler [RubyVM::MJIT::ExitCompiler] + def initialize(cb, ocb, exit_compiler) + @ocb = ocb + @exit_compiler = exit_compiler + + @full_cfunc_return = Assembler.new.then do |asm| + @exit_compiler.compile_full_cfunc_return(asm) + @ocb.write(asm) + end + + @cfunc_codegen_table = {} + register_cfunc_codegen_funcs + # freeze # workaround a binding.irb issue. TODO: resurrect this + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + # @param insn `RubyVM::MJIT::Instruction` + def compile(jit, ctx, asm, insn) + asm.incr_counter(:mjit_insns_count) + asm.comment("Insn: #{insn.name}") + + # 72/101 + case insn.name + when :nop then nop(jit, ctx, asm) + when :getlocal then getlocal(jit, ctx, asm) + when :setlocal then setlocal(jit, ctx, asm) + when :getblockparam then getblockparam(jit, ctx, asm) + # setblockparam + when :getblockparamproxy then getblockparamproxy(jit, ctx, asm) + # getspecial + # setspecial + when :getinstancevariable then getinstancevariable(jit, ctx, asm) + when :setinstancevariable then setinstancevariable(jit, ctx, asm) + when :getclassvariable then getclassvariable(jit, ctx, asm) + # setclassvariable + when :opt_getconstant_path then opt_getconstant_path(jit, ctx, asm) + when :getconstant then getconstant(jit, ctx, asm) + # setconstant + # getglobal + # setglobal + when :putnil then putnil(jit, ctx, asm) + when :putself then putself(jit, ctx, asm) + when :putobject then putobject(jit, ctx, asm) + # putspecialobject + when :putstring then putstring(jit, ctx, asm) + when :concatstrings then concatstrings(jit, ctx, asm) + when :anytostring then anytostring(jit, ctx, asm) + # toregexp + # intern + when :newarray then newarray(jit, ctx, asm) + # newarraykwsplat + when :duparray then duparray(jit, ctx, asm) + # duphash + when :expandarray then expandarray(jit, ctx, asm) + when :concatarray then concatarray(jit, ctx, asm) + when :splatarray then splatarray(jit, ctx, asm) + when :newhash then newhash(jit, ctx, asm) + # newrange + when :pop then pop(jit, ctx, asm) + when :dup then dup(jit, ctx, asm) + when :dupn then dupn(jit, ctx, asm) + when :swap then swap(jit, ctx, asm) + # opt_reverse + when :topn then topn(jit, ctx, asm) + when :setn then setn(jit, ctx, asm) + when :adjuststack then adjuststack(jit, ctx, asm) + when :defined then defined(jit, ctx, asm) + # checkmatch + # checkkeyword + # checktype + # defineclass + # definemethod + # definesmethod + when :send then send(jit, ctx, asm) + when :opt_send_without_block then opt_send_without_block(jit, ctx, asm) + when :objtostring then objtostring(jit, ctx, asm) + when :opt_str_freeze then opt_str_freeze(jit, ctx, asm) + when :opt_nil_p then opt_nil_p(jit, ctx, asm) + # opt_str_uminus + # opt_newarray_max + when :opt_newarray_min then opt_newarray_min(jit, ctx, asm) + when :invokesuper then invokesuper(jit, ctx, asm) + # invokeblock + when :leave then leave(jit, ctx, asm) + # throw + when :jump then jump(jit, ctx, asm) + when :branchif then branchif(jit, ctx, asm) + when :branchunless then branchunless(jit, ctx, asm) + when :branchnil then branchnil(jit, ctx, asm) + # once + when :opt_case_dispatch then opt_case_dispatch(jit, ctx, asm) + when :opt_plus then opt_plus(jit, ctx, asm) + when :opt_minus then opt_minus(jit, ctx, asm) + when :opt_mult then opt_mult(jit, ctx, asm) + when :opt_div then opt_div(jit, ctx, asm) + when :opt_mod then opt_mod(jit, ctx, asm) + when :opt_eq then opt_eq(jit, ctx, asm) + when :opt_neq then opt_neq(jit, ctx, asm) + when :opt_lt then opt_lt(jit, ctx, asm) + when :opt_le then opt_le(jit, ctx, asm) + when :opt_gt then opt_gt(jit, ctx, asm) + when :opt_ge then opt_ge(jit, ctx, asm) + when :opt_ltlt then opt_ltlt(jit, ctx, asm) + when :opt_and then opt_and(jit, ctx, asm) + when :opt_or then opt_or(jit, ctx, asm) + when :opt_aref then opt_aref(jit, ctx, asm) + when :opt_aset then opt_aset(jit, ctx, asm) + # opt_aset_with + # opt_aref_with + when :opt_length then opt_length(jit, ctx, asm) + when :opt_size then opt_size(jit, ctx, asm) + when :opt_empty_p then opt_empty_p(jit, ctx, asm) + when :opt_succ then opt_succ(jit, ctx, asm) + when :opt_not then opt_not(jit, ctx, asm) + when :opt_regexpmatch2 then opt_regexpmatch2(jit, ctx, asm) + # invokebuiltin + when :opt_invokebuiltin_delegate then opt_invokebuiltin_delegate(jit, ctx, asm) + when :opt_invokebuiltin_delegate_leave then opt_invokebuiltin_delegate_leave(jit, ctx, asm) + when :getlocal_WC_0 then getlocal_WC_0(jit, ctx, asm) + when :getlocal_WC_1 then getlocal_WC_1(jit, ctx, asm) + when :setlocal_WC_0 then setlocal_WC_0(jit, ctx, asm) + when :setlocal_WC_1 then setlocal_WC_1(jit, ctx, asm) + when :putobject_INT2FIX_0_ then putobject_INT2FIX_0_(jit, ctx, asm) + when :putobject_INT2FIX_1_ then putobject_INT2FIX_1_(jit, ctx, asm) + else CantCompile + end + end + + private + + # + # Insns + # + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def nop(jit, ctx, asm) + # Do nothing + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getlocal(jit, ctx, asm) + idx = jit.operand(0) + level = jit.operand(1) + jit_getlocal_generic(jit, ctx, asm, idx:, level:) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def setlocal(jit, ctx, asm) + idx = jit.operand(0) + level = jit.operand(1) + jit_setlocal_generic(jit, ctx, asm, idx:, level:) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getblockparam(jit, ctx, asm) + # EP level + level = jit.operand(1) + + # Save the PC and SP because we might allocate + jit_prepare_routine_call(jit, ctx, asm) + + # A mirror of the interpreter code. Checking for the case + # where it's pushing rb_block_param_proxy. + side_exit = side_exit(jit, ctx) + + # Load environment pointer EP from CFP + ep_reg = :rax + jit_get_ep(asm, level, reg: ep_reg) + + # Bail when VM_ENV_FLAGS(ep, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM) is non zero + # FIXME: This is testing bits in the same place that the WB check is testing. + # We should combine these at some point + asm.test([ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS], C.VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM) + + # If the frame flag has been modified, then the actual proc value is + # already in the EP and we should just use the value. + frame_flag_modified = asm.new_label('frame_flag_modified') + asm.jnz(frame_flag_modified) + + # This instruction writes the block handler to the EP. If we need to + # fire a write barrier for the write, then exit (we'll let the + # interpreter handle it so it can fire the write barrier). + # flags & VM_ENV_FLAG_WB_REQUIRED + asm.test([ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS], C.VM_ENV_FLAG_WB_REQUIRED) + + # if (flags & VM_ENV_FLAG_WB_REQUIRED) != 0 + asm.jnz(side_exit) + + # Convert the block handler in to a proc + # call rb_vm_bh_to_procval(const rb_execution_context_t *ec, VALUE block_handler) + asm.mov(C_ARGS[0], EC) + # The block handler for the current frame + # note, VM_ASSERT(VM_ENV_LOCAL_P(ep)) + asm.mov(C_ARGS[1], [ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_SPECVAL]) + asm.call(C.rb_vm_bh_to_procval) + + # Load environment pointer EP from CFP (again) + ep_reg = :rcx + jit_get_ep(asm, level, reg: ep_reg) + + # Write the value at the environment pointer + idx = jit.operand(0) + offs = -(C.VALUE.size * idx) + asm.mov([ep_reg, offs], C_RET); + + # Set the frame modified flag + asm.mov(:rax, [ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS]) # flag_check + asm.or(:rax, C.VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM) # modified_flag + asm.mov([ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS], :rax) + + asm.write_label(frame_flag_modified) + + # Push the proc on the stack + stack_ret = ctx.stack_push + ep_reg = :rax + jit_get_ep(asm, level, reg: ep_reg) + asm.mov(:rax, [ep_reg, offs]) + asm.mov(stack_ret, :rax) + + KeepCompiling + end + + # setblockparam + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getblockparamproxy(jit, ctx, asm) + # To get block_handler + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + starting_context = ctx.dup # make a copy for use with jit_chain_guard + + # A mirror of the interpreter code. Checking for the case + # where it's pushing rb_block_param_proxy. + side_exit = side_exit(jit, ctx) + + # EP level + level = jit.operand(1) + + # Peek at the block handler so we can check whether it's nil + comptime_handler = jit.peek_at_block_handler(level) + + # When a block handler is present, it should always be a GC-guarded + # pointer (VM_BH_ISEQ_BLOCK_P) + if comptime_handler != 0 && comptime_handler & 0x3 != 0x1 + asm.incr_counter(:getblockpp_not_gc_guarded) + return CantCompile + end + + # Load environment pointer EP from CFP + ep_reg = :rax + jit_get_ep(asm, level, reg: ep_reg) + + # Bail when VM_ENV_FLAGS(ep, VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM) is non zero + asm.test([ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS], C.VM_FRAME_FLAG_MODIFIED_BLOCK_PARAM) + asm.jnz(counted_exit(side_exit, :getblockpp_block_param_modified)) + + # Load the block handler for the current frame + # note, VM_ASSERT(VM_ENV_LOCAL_P(ep)) + block_handler = :rax + asm.mov(block_handler, [ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_SPECVAL]) + + # Specialize compilation for the case where no block handler is present + if comptime_handler == 0 + # Bail if there is a block handler + asm.cmp(block_handler, 0) + + jit_chain_guard(:jnz, jit, starting_context, asm, counted_exit(side_exit, :getblockpp_block_handler_none)) + + putobject(jit, ctx, asm, val: Qnil) + else + # Block handler is a tagged pointer. Look at the tag. 0x03 is from VM_BH_ISEQ_BLOCK_P(). + asm.and(block_handler, 0x3) + + # Bail unless VM_BH_ISEQ_BLOCK_P(bh). This also checks for null. + asm.cmp(block_handler, 0x1) + + jit_chain_guard(:jnz, jit, starting_context, asm, counted_exit(side_exit, :getblockpp_not_iseq_block)) + + # Push rb_block_param_proxy. It's a root, so no need to use jit_mov_gc_ptr. + top = ctx.stack_push + asm.mov(:rax, C.rb_block_param_proxy) + asm.mov(top, :rax) + end + + jump_to_next_insn(jit, ctx, asm) + + EndBlock + end + + # getspecial + # setspecial + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getinstancevariable(jit, ctx, asm) + # Specialize on a compile-time receiver, and split a block for chain guards + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + id = jit.operand(0) + comptime_obj = jit.peek_at_self + + jit_getivar(jit, ctx, asm, comptime_obj, id) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def setinstancevariable(jit, ctx, asm) + starting_context = ctx.dup # make a copy for use with jit_chain_guard + + # Defer compilation so we can specialize on a runtime `self` + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + ivar_name = jit.operand(0) + comptime_receiver = jit.peek_at_self + + # If the comptime receiver is frozen, writing an IV will raise an exception + # and we don't want to JIT code to deal with that situation. + if C.rb_obj_frozen_p(comptime_receiver) + asm.incr_counter(:setivar_frozen) + return CantCompile + end + + # Check if the comptime receiver is a T_OBJECT + receiver_t_object = C.BUILTIN_TYPE(comptime_receiver) == C.T_OBJECT + + # If the receiver isn't a T_OBJECT, or uses a custom allocator, + # then just write out the IV write as a function call. + # too-complex shapes can't use index access, so we use rb_ivar_get for them too. + if !receiver_t_object || shape_too_complex?(comptime_receiver) || ctx.chain_depth >= 10 + asm.comment('call rb_vm_setinstancevariable') + + ic = jit.operand(1) + + # The function could raise exceptions. + # Note that this modifies REG_SP, which is why we do it first + jit_prepare_routine_call(jit, ctx, asm) + + # Get the operands from the stack + val_opnd = ctx.stack_pop(1) + + # Call rb_vm_setinstancevariable(iseq, obj, id, val, ic); + asm.mov(:rdi, jit.iseq.to_i) + asm.mov(:rsi, [CFP, C.rb_control_frame_t.offsetof(:self)]) + asm.mov(:rdx, ivar_name) + asm.mov(:rcx, val_opnd) + asm.mov(:r8, ic) + asm.call(C.rb_vm_setinstancevariable) + else + # Get the iv index + shape_id = C.rb_shape_get_shape_id(comptime_receiver) + ivar_index = C.rb_shape_get_iv_index(shape_id, ivar_name) + + # Get the receiver + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) + + # Generate a side exit + side_exit = side_exit(jit, ctx) + + # Upgrade type + guard_object_is_heap(asm, :rax, counted_exit(side_exit, :setivar_not_heap)) + + asm.comment('guard shape') + asm.cmp(DwordPtr[:rax, C.rb_shape_id_offset], shape_id) + megamorphic_side_exit = counted_exit(side_exit, :setivar_megamorphic) + jit_chain_guard(:jne, jit, starting_context, asm, megamorphic_side_exit) + + # If we don't have an instance variable index, then we need to + # transition out of the current shape. + if ivar_index.nil? + shape = C.rb_shape_get_shape_by_id(shape_id) + + current_capacity = shape.capacity + new_capacity = current_capacity * 2 + + # If the object doesn't have the capacity to store the IV, + # then we'll need to allocate it. + needs_extension = shape.next_iv_index >= current_capacity + + # We can write to the object, but we need to transition the shape + ivar_index = shape.next_iv_index + + capa_shape = + if needs_extension + # We need to add an extended table to the object + # First, create an outgoing transition that increases the capacity + C.rb_shape_transition_shape_capa(shape, new_capacity) + else + nil + end + + dest_shape = + if capa_shape + C.rb_shape_get_next(capa_shape, comptime_receiver, ivar_name) + else + C.rb_shape_get_next(shape, comptime_receiver, ivar_name) + end + new_shape_id = C.rb_shape_id(dest_shape) + + if new_shape_id == C.OBJ_TOO_COMPLEX_SHAPE_ID + asm.incr_counter(:setivar_too_complex) + return CantCompile + end + + if needs_extension + # Generate the C call so that runtime code will increase + # the capacity and set the buffer. + asm.mov(C_ARGS[0], :rax) + asm.mov(C_ARGS[1], current_capacity) + asm.mov(C_ARGS[2], new_capacity) + asm.call(C.rb_ensure_iv_list_size) + + # Load the receiver again after the function call + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) + end + + write_val = ctx.stack_pop(1) + jit_write_iv(asm, comptime_receiver, :rax, :rcx, ivar_index, write_val, needs_extension) + + # Store the new shape + asm.comment('write shape') + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) # reload after jit_write_iv + asm.mov(DwordPtr[:rax, C.rb_shape_id_offset], new_shape_id) + else + # If the iv index already exists, then we don't need to + # transition to a new shape. The reason is because we find + # the iv index by searching up the shape tree. If we've + # made the transition already, then there's no reason to + # update the shape on the object. Just set the IV. + write_val = ctx.stack_pop(1) + jit_write_iv(asm, comptime_receiver, :rax, :rcx, ivar_index, write_val, false) + end + + skip_wb = asm.new_label('skip_wb') + # If the value we're writing is an immediate, we don't need to WB + asm.test(write_val, C.RUBY_IMMEDIATE_MASK) + asm.jnz(skip_wb) + + # If the value we're writing is nil or false, we don't need to WB + asm.cmp(write_val, Qnil) + asm.jbe(skip_wb) + + asm.comment('write barrier') + asm.mov(C_ARGS[0], [CFP, C.rb_control_frame_t.offsetof(:self)]) # reload after jit_write_iv + asm.mov(C_ARGS[1], write_val) + asm.call(C.rb_gc_writebarrier) + + asm.write_label(skip_wb) + end + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getclassvariable(jit, ctx, asm) + # rb_vm_getclassvariable can raise exceptions. + jit_prepare_routine_call(jit, ctx, asm) + + asm.mov(C_ARGS[0], [CFP, C.rb_control_frame_t.offsetof(:iseq)]) + asm.mov(C_ARGS[1], CFP) + asm.mov(C_ARGS[2], jit.operand(0)) + asm.mov(C_ARGS[3], jit.operand(1)) + asm.call(C.rb_vm_getclassvariable) + + top = ctx.stack_push + asm.mov(top, C_RET) + + KeepCompiling + end + + # setclassvariable + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_getconstant_path(jit, ctx, asm) + # Cut the block for invalidation + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + ic = C.iseq_inline_constant_cache.new(jit.operand(0)) + idlist = ic.segments + + # Make sure there is an exit for this block as the interpreter might want + # to invalidate this block from rb_mjit_constant_ic_update(). + # For now, we always take an entry exit even if it was a side exit. + Invariants.ensure_block_entry_exit(jit, cause: 'opt_getconstant_path') + + # See vm_ic_hit_p(). The same conditions are checked in yjit_constant_ic_update(). + ice = ic.entry + if ice.nil? + # In this case, leave a block that unconditionally side exits + # for the interpreter to invalidate. + asm.incr_counter(:optgetconst_not_cached) + return CantCompile + end + + if ice.ic_cref # with cref + # Cache is keyed on a certain lexical scope. Use the interpreter's cache. + side_exit = side_exit(jit, ctx) + + # Call function to verify the cache. It doesn't allocate or call methods. + asm.mov(C_ARGS[0], ic.to_i) + asm.mov(C_ARGS[1], [CFP, C.rb_control_frame_t.offsetof(:ep)]) + asm.call(C.rb_vm_ic_hit_p) + + # Check the result. SysV only specifies one byte for _Bool return values, + # so it's important we only check one bit to ignore the higher bits in the register. + asm.test(C_RET, 1) + asm.jz(counted_exit(side_exit, :optgetconst_cache_miss)) + + asm.mov(:rax, ic.to_i) # inline_cache + asm.mov(:rax, [:rax, C.iseq_inline_constant_cache.offsetof(:entry)]) # ic_entry + asm.mov(:rax, [:rax, C.iseq_inline_constant_cache_entry.offsetof(:value)]) # ic_entry_val + + # Push ic->entry->value + stack_top = ctx.stack_push + asm.mov(stack_top, :rax) + else # without cref + # TODO: implement this + # Optimize for single ractor mode. + # if !assume_single_ractor_mode(jit, ocb) + # return CantCompile + # end + + # Invalidate output code on any constant writes associated with + # constants referenced within the current block. + Invariants.assume_stable_constant_names(jit, idlist) + + putobject(jit, ctx, asm, val: ice.value) + end + + jump_to_next_insn(jit, ctx, asm) + EndBlock + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getconstant(jit, ctx, asm) + id = jit.operand(0) + + # vm_get_ev_const can raise exceptions. + jit_prepare_routine_call(jit, ctx, asm) + + allow_nil_opnd = ctx.stack_pop(1) + klass_opnd = ctx.stack_pop(1) + + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], klass_opnd) + asm.mov(C_ARGS[2], id) + asm.mov(C_ARGS[3], allow_nil_opnd) + asm.call(C.rb_vm_get_ev_const) + + top = ctx.stack_push + asm.mov(top, C_RET) + + KeepCompiling + end + + # setconstant + # getglobal + # setglobal + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putnil(jit, ctx, asm) + putobject(jit, ctx, asm, val: Qnil) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putself(jit, ctx, asm) + stack_top = ctx.stack_push + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) + asm.mov(stack_top, :rax) + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putobject(jit, ctx, asm, val: jit.operand(0)) + # Push it to the stack + stack_top = ctx.stack_push + if asm.imm32?(val) + asm.mov(stack_top, val) + else # 64-bit immediates can't be directly written to memory + asm.mov(:rax, val) + asm.mov(stack_top, :rax) + end + # TODO: GC offsets? + + KeepCompiling + end + + # putspecialobject + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putstring(jit, ctx, asm) + put_val = jit.operand(0, ruby: true) + + # Save the PC and SP because the callee will allocate + jit_prepare_routine_call(jit, ctx, asm) + + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], to_value(put_val)) + asm.call(C.rb_ec_str_resurrect) + + stack_top = ctx.stack_push + asm.mov(stack_top, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def concatstrings(jit, ctx, asm) + n = jit.operand(0) + + # Save the PC and SP because we are allocating + jit_prepare_routine_call(jit, ctx, asm) + + asm.lea(:rax, ctx.sp_opnd(-C.VALUE.size * n)) + + # call rb_str_concat_literals(size_t n, const VALUE *strings); + asm.mov(C_ARGS[0], n) + asm.mov(C_ARGS[1], :rax) + asm.call(C.rb_str_concat_literals) + + ctx.stack_pop(n) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def anytostring(jit, ctx, asm) + # Save the PC and SP since we might call #to_s + jit_prepare_routine_call(jit, ctx, asm) + + str = ctx.stack_pop(1) + val = ctx.stack_pop(1) + + asm.mov(C_ARGS[0], str) + asm.mov(C_ARGS[1], val) + asm.call(C.rb_obj_as_string_result) + + # Push the return value + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # toregexp + # intern + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def newarray(jit, ctx, asm) + n = jit.operand(0) + + # Save the PC and SP because we are allocating + jit_prepare_routine_call(jit, ctx, asm) + + # If n is 0, then elts is never going to be read, so we can just pass null + if n == 0 + values_ptr = 0 + else + asm.comment('load pointer to array elts') + offset_magnitude = C.VALUE.size * n + values_opnd = ctx.sp_opnd(-(offset_magnitude)) + asm.lea(:rax, values_opnd) + values_ptr = :rax + end + + # call rb_ec_ary_new_from_values(struct rb_execution_context_struct *ec, long n, const VALUE *elts); + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], n) + asm.mov(C_ARGS[2], values_ptr) + asm.call(C.rb_ec_ary_new_from_values) + + ctx.stack_pop(n) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # newarraykwsplat + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def duparray(jit, ctx, asm) + ary = jit.operand(0) + + # Save the PC and SP because we are allocating + jit_prepare_routine_call(jit, ctx, asm) + + # call rb_ary_resurrect(VALUE ary); + asm.comment('call rb_ary_resurrect') + asm.mov(C_ARGS[0], ary) + asm.call(C.rb_ary_resurrect) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # duphash + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def expandarray(jit, ctx, asm) + # Both arguments are rb_num_t which is unsigned + num = jit.operand(0) + flag = jit.operand(1) + + # If this instruction has the splat flag, then bail out. + if flag & 0x01 != 0 + asm.incr_counter(:expandarray_splat) + return CantCompile + end + + # If this instruction has the postarg flag, then bail out. + if flag & 0x02 != 0 + asm.incr_counter(:expandarray_postarg) + return CantCompile + end + + side_exit = side_exit(jit, ctx) + + array_opnd = ctx.stack_pop(1) + + # num is the number of requested values. If there aren't enough in the + # array then we're going to push on nils. + # TODO: implement this + + # Move the array from the stack and check that it's an array. + asm.mov(:rax, array_opnd) + guard_object_is_heap(asm, :rax, counted_exit(side_exit, :expandarray_not_array)) + guard_object_is_array(asm, :rax, :rcx, counted_exit(side_exit, :expandarray_not_array)) + + # If we don't actually want any values, then just return. + if num == 0 + return KeepCompiling + end + + jit_array_len(asm, :rax, :rcx) + + # Only handle the case where the number of values in the array is greater + # than or equal to the number of values requested. + asm.cmp(:rcx, num) + asm.jl(counted_exit(side_exit, :expandarray_rhs_too_small)) + + # Conditionally load the address of the heap array into REG1. + # (struct RArray *)(obj)->as.heap.ptr + #asm.mov(:rax, array_opnd) + asm.mov(:rcx, [:rax, C.RBasic.offsetof(:flags)]) + asm.test(:rcx, C.RARRAY_EMBED_FLAG); + asm.mov(:rcx, [:rax, C.RArray.offsetof(:as, :heap, :ptr)]) + + # Load the address of the embedded array into REG1. + # (struct RArray *)(obj)->as.ary + asm.lea(:rax, [:rax, C.RArray.offsetof(:as, :ary)]) + + asm.cmovnz(:rcx, :rax) + + # Loop backward through the array and push each element onto the stack. + (num - 1).downto(0).each do |i| + top = ctx.stack_push + asm.mov(:rax, [:rcx, i * C.VALUE.size]) + asm.mov(top, :rax) + end + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def concatarray(jit, ctx, asm) + # Save the PC and SP because the callee may allocate + # Note that this modifies REG_SP, which is why we do it first + jit_prepare_routine_call(jit, ctx, asm) + + # Get the operands from the stack + ary2st_opnd = ctx.stack_pop(1) + ary1_opnd = ctx.stack_pop(1) + + # Call rb_vm_concat_array(ary1, ary2st) + asm.mov(C_ARGS[0], ary1_opnd) + asm.mov(C_ARGS[1], ary2st_opnd) + asm.call(C.rb_vm_concat_array) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def splatarray(jit, ctx, asm) + flag = jit.operand(0) + + # Save the PC and SP because the callee may allocate + # Note that this modifies REG_SP, which is why we do it first + jit_prepare_routine_call(jit, ctx, asm) + + # Get the operands from the stack + ary_opnd = ctx.stack_pop(1) + + # Call rb_vm_splat_array(flag, ary) + asm.mov(C_ARGS[0], flag) + asm.mov(C_ARGS[1], ary_opnd) + asm.call(C.rb_vm_splat_array) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def newhash(jit, ctx, asm) + num = jit.operand(0) + + # Save the PC and SP because we are allocating + jit_prepare_routine_call(jit, ctx, asm) + + if num != 0 + # val = rb_hash_new_with_size(num / 2); + asm.mov(C_ARGS[0], num / 2) + asm.call(C.rb_hash_new_with_size) + + # Save the allocated hash as we want to push it after insertion + asm.push(C_RET) + asm.push(C_RET) # x86 alignment + + # Get a pointer to the values to insert into the hash + asm.lea(:rcx, ctx.stack_opnd(num - 1)) + + # rb_hash_bulk_insert(num, STACK_ADDR_FROM_TOP(num), val); + asm.mov(C_ARGS[0], num) + asm.mov(C_ARGS[1], :rcx) + asm.mov(C_ARGS[2], C_RET) + asm.call(C.rb_hash_bulk_insert) + + asm.pop(:rax) + asm.pop(:rax) + + ctx.stack_pop(num) + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + else + # val = rb_hash_new(); + asm.call(C.rb_hash_new) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + end + + KeepCompiling + end + + # newrange + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def pop(jit, ctx, asm) + ctx.stack_pop + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def dup(jit, ctx, asm) + val1 = ctx.stack_opnd(0) + val2 = ctx.stack_push + asm.mov(:rax, val1) + asm.mov(val2, :rax) + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def dupn(jit, ctx, asm) + n = jit.operand(0) + + # In practice, seems to be only used for n==2 + if n != 2 + return CantCompile + end + + opnd1 = ctx.stack_opnd(1) + opnd0 = ctx.stack_opnd(0) + + dst1 = ctx.stack_push + asm.mov(:rax, opnd1) + asm.mov(dst1, :rax) + + dst0 = ctx.stack_push + asm.mov(:rax, opnd0) + asm.mov(dst0, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def swap(jit, ctx, asm) + stack0_mem = ctx.stack_opnd(0) + stack1_mem = ctx.stack_opnd(1) + + asm.mov(:rax, stack0_mem) + asm.mov(:rcx, stack1_mem) + asm.mov(stack0_mem, :rcx) + asm.mov(stack1_mem, :rax) + + KeepCompiling + end + + # opt_reverse + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def topn(jit, ctx, asm) + n = jit.operand(0) + + top_n_val = ctx.stack_opnd(n) + loc0 = ctx.stack_push + asm.mov(:rax, top_n_val) + asm.mov(loc0, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def setn(jit, ctx, asm) + n = jit.operand(0) + + top_val = ctx.stack_pop(0) + dst_opnd = ctx.stack_opnd(n) + asm.mov(:rax, top_val) + asm.mov(dst_opnd, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def adjuststack(jit, ctx, asm) + n = jit.operand(0) + ctx.stack_pop(n) + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def defined(jit, ctx, asm) + op_type = jit.operand(0) + obj = jit.operand(1, ruby: true) + pushval = jit.operand(2, ruby: true) + + # Save the PC and SP because the callee may allocate + # Note that this modifies REG_SP, which is why we do it first + jit_prepare_routine_call(jit, ctx, asm) + + # Get the operands from the stack + v_opnd = ctx.stack_pop(1) + + # Call vm_defined(ec, reg_cfp, op_type, obj, v) + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], CFP) + asm.mov(C_ARGS[2], op_type) + asm.mov(C_ARGS[3], to_value(obj)) + asm.mov(C_ARGS[4], v_opnd) + asm.call(C.rb_vm_defined) + + asm.test(C_RET, 255) + asm.mov(:rax, Qnil) + asm.mov(:rcx, to_value(pushval)) + asm.cmovnz(:rax, :rcx) + + # Push the return value onto the stack + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + + KeepCompiling + end + + # checkmatch + # checkkeyword + # checktype + # defineclass + # definemethod + # definesmethod + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def send(jit, ctx, asm) + # Specialize on a compile-time receiver, and split a block for chain guards + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + cd = C.rb_call_data.new(jit.operand(0)) + blockiseq = jit.operand(1) + + block_handler = jit_caller_setup_arg_block(jit, ctx, asm, cd.ci, blockiseq, false) + if block_handler == CantCompile + return CantCompile + end + + # calling->ci + mid = C.vm_ci_mid(cd.ci) + argc = C.vm_ci_argc(cd.ci) + flags = C.vm_ci_flag(cd.ci) + + # vm_sendish + cme, comptime_recv_klass = jit_search_method(jit, ctx, asm, mid, argc, flags) + if cme == CantCompile + return CantCompile + end + jit_call_general(jit, ctx, asm, mid, argc, flags, cme, block_handler, comptime_recv_klass) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_send_without_block(jit, ctx, asm, cd: C.rb_call_data.new(jit.operand(0))) + # Specialize on a compile-time receiver, and split a block for chain guards + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + # calling->ci + mid = C.vm_ci_mid(cd.ci) + argc = C.vm_ci_argc(cd.ci) + flags = C.vm_ci_flag(cd.ci) + + # vm_sendish + cme, comptime_recv_klass = jit_search_method(jit, ctx, asm, mid, argc, flags) + if cme == CantCompile + return CantCompile + end + jit_call_general(jit, ctx, asm, mid, argc, flags, cme, C.VM_BLOCK_HANDLER_NONE, comptime_recv_klass) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def objtostring(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + recv = ctx.stack_opnd(0) + comptime_recv = jit.peek_at_stack(0) + + if C.RB_TYPE_P(comptime_recv, C.RUBY_T_STRING) + side_exit = side_exit(jit, ctx) + + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_recv), recv, comptime_recv, side_exit) + # No work needed. The string value is already on the top of the stack. + KeepCompiling + else + cd = C.rb_call_data.new(jit.operand(0)) + opt_send_without_block(jit, ctx, asm, cd:) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_str_freeze(jit, ctx, asm) + unless Invariants.assume_bop_not_redefined(jit, C.STRING_REDEFINED_OP_FLAG, C.BOP_FREEZE) + return CantCompile; + end + + str = jit.operand(0, ruby: true) + + # Push the return value onto the stack + stack_ret = ctx.stack_push + asm.mov(:rax, to_value(str)) + asm.mov(stack_ret, :rax) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_nil_p(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # opt_str_uminus + # opt_newarray_max + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_newarray_min(jit, ctx, asm) + num = jit.operand(0) + + # Save the PC and SP because we may allocate + jit_prepare_routine_call(jit, ctx, asm) + + offset_magnitude = C.VALUE.size * num + values_opnd = ctx.sp_opnd(-offset_magnitude) + asm.lea(:rax, values_opnd) + + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], num) + asm.mov(C_ARGS[2], :rax) + asm.call(C.rb_vm_opt_newarray_min) + + ctx.stack_pop(num) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def invokesuper(jit, ctx, asm) + # Specialize on a compile-time receiver, and split a block for chain guards + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + cd = C.rb_call_data.new(jit.operand(0)) + blockiseq = jit.operand(1) + + block_handler = jit_caller_setup_arg_block(jit, ctx, asm, cd.ci, blockiseq, true) + if block_handler == CantCompile + return CantCompile + end + + # calling->ci + mid = C.vm_ci_mid(cd.ci) + argc = C.vm_ci_argc(cd.ci) + flags = C.vm_ci_flag(cd.ci) + + # vm_sendish + cme = jit_search_super_method(jit, ctx, asm, mid, argc, flags) + if cme == CantCompile + return CantCompile + end + jit_call_general(jit, ctx, asm, mid, argc, flags, cme, block_handler, nil) + end + + # invokeblock + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def leave(jit, ctx, asm) + assert_equal(ctx.stack_size, 1) + + jit_check_ints(jit, ctx, asm) + + asm.comment('pop stack frame') + asm.lea(:rax, [CFP, C.rb_control_frame_t.size]) + asm.mov(CFP, :rax) + asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], :rax) + + # Return a value (for compile_leave_exit) + ret_opnd = ctx.stack_pop + asm.mov(:rax, ret_opnd) + + # Set caller's SP and push a value to its stack (for JIT) + asm.mov(SP, [CFP, C.rb_control_frame_t.offsetof(:sp)]) # Note: SP is in the position after popping a receiver and arguments + asm.mov([SP], :rax) + + # Jump to cfp->jit_return + asm.jmp([CFP, -C.rb_control_frame_t.size + C.rb_control_frame_t.offsetof(:jit_return)]) + + EndBlock + end + + # throw + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jump(jit, ctx, asm) + # Check for interrupts, but only on backward branches that may create loops + jump_offset = jit.operand(0, signed: true) + if jump_offset < 0 + jit_check_ints(jit, ctx, asm) + end + + pc = jit.pc + C.VALUE.size * (jit.insn.len + jump_offset) + stub_next_block(jit.iseq, pc, ctx, asm) + EndBlock + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def branchif(jit, ctx, asm) + # Check for interrupts, but only on backward branches that may create loops + jump_offset = jit.operand(0, signed: true) + if jump_offset < 0 + jit_check_ints(jit, ctx, asm) + end + + # TODO: skip check for known truthy + + # This `test` sets ZF only for Qnil and Qfalse, which let jz jump. + val = ctx.stack_pop + asm.test(val, ~Qnil) + + # Set stubs + branch_stub = BranchStub.new( + iseq: jit.iseq, + shape: Default, + target0: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * (jit.insn.len + jump_offset)), # branch target + target1: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * jit.insn.len), # fallthrough + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.target1.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, false) + @ocb.write(ocb_asm) + end + + # Jump to target0 on jnz + branch_stub.compile = proc do |branch_asm| + branch_asm.comment("branchif #{branch_stub.shape}") + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.jnz(branch_stub.target0.address) + branch_asm.jmp(branch_stub.target1.address) + in Next0 + branch_asm.jz(branch_stub.target1.address) + in Next1 + branch_asm.jnz(branch_stub.target0.address) + end + end + end + branch_stub.compile.call(asm) + + EndBlock + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def branchunless(jit, ctx, asm) + # Check for interrupts, but only on backward branches that may create loops + jump_offset = jit.operand(0, signed: true) + if jump_offset < 0 + jit_check_ints(jit, ctx, asm) + end + + # TODO: skip check for known truthy + + # This `test` sets ZF only for Qnil and Qfalse, which let jz jump. + val = ctx.stack_pop + asm.test(val, ~Qnil) + + # Set stubs + branch_stub = BranchStub.new( + iseq: jit.iseq, + shape: Default, + target0: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * (jit.insn.len + jump_offset)), # branch target + target1: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * jit.insn.len), # fallthrough + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.target1.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, false) + @ocb.write(ocb_asm) + end + + # Jump to target0 on jz + branch_stub.compile = proc do |branch_asm| + branch_asm.comment("branchunless #{branch_stub.shape}") + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.jz(branch_stub.target0.address) + branch_asm.jmp(branch_stub.target1.address) + in Next0 + branch_asm.jnz(branch_stub.target1.address) + in Next1 + branch_asm.jz(branch_stub.target0.address) + end + end + end + branch_stub.compile.call(asm) + + EndBlock + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def branchnil(jit, ctx, asm) + # Check for interrupts, but only on backward branches that may create loops + jump_offset = jit.operand(0, signed: true) + if jump_offset < 0 + jit_check_ints(jit, ctx, asm) + end + + # TODO: skip check for known truthy + + val = ctx.stack_pop + asm.cmp(val, Qnil) + + # Set stubs + branch_stub = BranchStub.new( + iseq: jit.iseq, + shape: Default, + target0: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * (jit.insn.len + jump_offset)), # branch target + target1: BranchTarget.new(ctx:, pc: jit.pc + C.VALUE.size * jit.insn.len), # fallthrough + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.target1.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, false) + @ocb.write(ocb_asm) + end + + # Jump to target0 on je + branch_stub.compile = proc do |branch_asm| + branch_asm.comment("branchnil #{branch_stub.shape}") + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.je(branch_stub.target0.address) + branch_asm.jmp(branch_stub.target1.address) + in Next0 + branch_asm.jne(branch_stub.target1.address) + in Next1 + branch_asm.je(branch_stub.target0.address) + end + end + end + branch_stub.compile.call(asm) + + EndBlock + end + + # once + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_case_dispatch(jit, ctx, asm) + # Just go to === branches for now + ctx.stack_pop + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_plus(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + comptime_recv = jit.peek_at_stack(1) + comptime_obj = jit.peek_at_stack(0) + + if fixnum?(comptime_recv) && fixnum?(comptime_obj) + # Generate a side exit before popping operands + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_PLUS) + return CantCompile + end + + obj_opnd = ctx.stack_pop + recv_opnd = ctx.stack_pop + + asm.comment('guard recv is fixnum') # TODO: skip this with type information + asm.test(recv_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.comment('guard obj is fixnum') # TODO: skip this with type information + asm.test(obj_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.mov(:rax, recv_opnd) + asm.sub(:rax, 1) # untag + asm.mov(:rcx, obj_opnd) + asm.add(:rax, :rcx) + asm.jo(side_exit) + + dst_opnd = ctx.stack_push + asm.mov(dst_opnd, :rax) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_minus(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + comptime_recv = jit.peek_at_stack(1) + comptime_obj = jit.peek_at_stack(0) + + if fixnum?(comptime_recv) && fixnum?(comptime_obj) + # Generate a side exit before popping operands + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_MINUS) + return CantCompile + end + + obj_opnd = ctx.stack_pop + recv_opnd = ctx.stack_pop + + asm.comment('guard recv is fixnum') # TODO: skip this with type information + asm.test(recv_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.comment('guard obj is fixnum') # TODO: skip this with type information + asm.test(obj_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.mov(:rax, recv_opnd) + asm.mov(:rcx, obj_opnd) + asm.sub(:rax, :rcx) + asm.jo(side_exit) + asm.add(:rax, 1) # re-tag + + dst_opnd = ctx.stack_push + asm.mov(dst_opnd, :rax) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_mult(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_div(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_mod(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + if two_fixnums_on_stack?(jit) + # Create a side-exit to fall back to the interpreter + # Note: we generate the side-exit before popping operands from the stack + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_MOD) + return CantCompile + end + + # Check that both operands are fixnums + guard_two_fixnums(jit, ctx, asm, side_exit) + + # Get the operands and destination from the stack + arg1 = ctx.stack_pop(1) + arg0 = ctx.stack_pop(1) + + # Check for arg0 % 0 + asm.cmp(arg1, 0) + asm.je(side_exit) + + # Call rb_fix_mod_fix(VALUE recv, VALUE obj) + asm.mov(C_ARGS[0], arg0) + asm.mov(C_ARGS[1], arg1) + asm.call(C.rb_fix_mod_fix) + + # Push the return value onto the stack + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_eq(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + if jit_equality_specialized(jit, ctx, asm, true) + jump_to_next_insn(jit, ctx, asm) + EndBlock + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_neq(jit, ctx, asm) + # opt_neq is passed two rb_call_data as arguments: + # first for ==, second for != + neq_cd = C.rb_call_data.new(jit.operand(1)) + opt_send_without_block(jit, ctx, asm, cd: neq_cd) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_lt(jit, ctx, asm) + jit_fixnum_cmp(jit, ctx, asm, opcode: :cmovl, bop: C.BOP_LT) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_le(jit, ctx, asm) + jit_fixnum_cmp(jit, ctx, asm, opcode: :cmovle, bop: C.BOP_LE) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_gt(jit, ctx, asm) + jit_fixnum_cmp(jit, ctx, asm, opcode: :cmovg, bop: C.BOP_GT) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_ge(jit, ctx, asm) + jit_fixnum_cmp(jit, ctx, asm, opcode: :cmovge, bop: C.BOP_GE) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_ltlt(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_and(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + if two_fixnums_on_stack?(jit) + # Create a side-exit to fall back to the interpreter + # Note: we generate the side-exit before popping operands from the stack + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_AND) + return CantCompile + end + + # Check that both operands are fixnums + guard_two_fixnums(jit, ctx, asm, side_exit) + + # Get the operands and destination from the stack + arg1 = ctx.stack_pop(1) + arg0 = ctx.stack_pop(1) + + asm.comment('bitwise and') + asm.mov(:rax, arg0) + asm.and(:rax, arg1) + + # Push the return value onto the stack + dst = ctx.stack_push + asm.mov(dst, :rax) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_or(jit, ctx, asm) + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + if two_fixnums_on_stack?(jit) + # Create a side-exit to fall back to the interpreter + # Note: we generate the side-exit before popping operands from the stack + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_OR) + return CantCompile + end + + # Check that both operands are fixnums + guard_two_fixnums(jit, ctx, asm, side_exit) + + # Get the operands and destination from the stack + asm.comment('bitwise or') + arg1 = ctx.stack_pop(1) + arg0 = ctx.stack_pop(1) + + # Do the bitwise or arg0 | arg1 + asm.mov(:rax, arg0) + asm.or(:rax, arg1) + + # Push the return value onto the stack + dst = ctx.stack_push + asm.mov(dst, :rax) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_aref(jit, ctx, asm) + cd = C.rb_call_data.new(jit.operand(0)) + argc = C.vm_ci_argc(cd.ci) + + if argc != 1 + asm.incr_counter(:optaref_argc_not_one) + return CantCompile + end + + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + comptime_recv = jit.peek_at_stack(1) + comptime_obj = jit.peek_at_stack(0) + + side_exit = side_exit(jit, ctx) + + if C.rb_class_of(comptime_recv) == Array && fixnum?(comptime_obj) + unless Invariants.assume_bop_not_redefined(jit, C.ARRAY_REDEFINED_OP_FLAG, C.BOP_AREF) + return CantCompile + end + + idx_opnd = ctx.stack_opnd(0) + recv_opnd = ctx.stack_opnd(1) + + not_array_exit = counted_exit(side_exit, :optaref_recv_not_array) + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_recv), recv_opnd, comptime_recv, not_array_exit) + + # Bail if idx is not a FIXNUM + asm.mov(:rax, idx_opnd) + asm.test(:rax, C.RUBY_FIXNUM_FLAG) + asm.jz(counted_exit(side_exit, :optaref_arg_not_fixnum)) + + # Call VALUE rb_ary_entry_internal(VALUE ary, long offset). + # It never raises or allocates, so we don't need to write to cfp->pc. + asm.sar(:rax, 1) # Convert fixnum to int + asm.mov(C_ARGS[0], recv_opnd) + asm.mov(C_ARGS[1], :rax) + asm.call(C.rb_ary_entry_internal) + + # Pop the argument and the receiver + ctx.stack_pop(2) + + # Push the return value onto the stack + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + # Let guard chains share the same successor + jump_to_next_insn(jit, ctx, asm) + EndBlock + elsif C.rb_class_of(comptime_recv) == Hash + unless Invariants.assume_bop_not_redefined(jit, C.HASH_REDEFINED_OP_FLAG, C.BOP_AREF) + return CantCompile + end + + recv_opnd = ctx.stack_opnd(1) + + # Guard that the receiver is a Hash + not_hash_exit = counted_exit(side_exit, :optaref_recv_not_hash) + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_recv), recv_opnd, comptime_recv, not_hash_exit) + + # Prepare to call rb_hash_aref(). It might call #hash on the key. + jit_prepare_routine_call(jit, ctx, asm) + + asm.comment('call rb_hash_aref') + key_opnd = ctx.stack_opnd(0) + recv_opnd = ctx.stack_opnd(1) + asm.mov(:rdi, recv_opnd) + asm.mov(:rsi, key_opnd) + asm.call(C.rb_hash_aref) + + # Pop the key and the receiver + ctx.stack_pop(2) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + # Let guard chains share the same successor + jump_to_next_insn(jit, ctx, asm) + EndBlock + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_aset(jit, ctx, asm) + # Defer compilation so we can specialize on a runtime `self` + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + comptime_recv = jit.peek_at_stack(2) + comptime_key = jit.peek_at_stack(1) + + # Get the operands from the stack + recv = ctx.stack_opnd(2) + key = ctx.stack_opnd(1) + _val = ctx.stack_opnd(0) + + if C.rb_class_of(comptime_recv) == Array && fixnum?(comptime_key) + side_exit = side_exit(jit, ctx) + + # Guard receiver is an Array + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_recv), recv, comptime_recv, side_exit) + + # Guard key is a fixnum + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_key), key, comptime_key, side_exit) + + # We might allocate or raise + jit_prepare_routine_call(jit, ctx, asm) + + asm.comment('call rb_ary_store') + recv = ctx.stack_opnd(2) + key = ctx.stack_opnd(1) + val = ctx.stack_opnd(0) + asm.mov(:rax, key) + asm.sar(:rax, 1) # FIX2LONG(key) + asm.mov(C_ARGS[0], recv) + asm.mov(C_ARGS[1], :rax) + asm.mov(C_ARGS[2], val) + asm.call(C.rb_ary_store) + + # rb_ary_store returns void + # stored value should still be on stack + val = ctx.stack_opnd(0) + + # Push the return value onto the stack + ctx.stack_pop(3) + stack_ret = ctx.stack_push + asm.mov(:rax, val) + asm.mov(stack_ret, :rax) + + jump_to_next_insn(jit, ctx, asm) + EndBlock + elsif C.rb_class_of(comptime_recv) == Hash + side_exit = side_exit(jit, ctx) + + # Guard receiver is a Hash + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_recv), recv, comptime_recv, side_exit) + + # We might allocate or raise + jit_prepare_routine_call(jit, ctx, asm) + + # Call rb_hash_aset + recv = ctx.stack_opnd(2) + key = ctx.stack_opnd(1) + val = ctx.stack_opnd(0) + asm.mov(C_ARGS[0], recv) + asm.mov(C_ARGS[1], key) + asm.mov(C_ARGS[2], val) + asm.call(C.rb_hash_aset) + + # Push the return value onto the stack + ctx.stack_pop(3) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + jump_to_next_insn(jit, ctx, asm) + EndBlock + else + opt_send_without_block(jit, ctx, asm) + end + end + + # opt_aset_with + # opt_aref_with + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_length(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_size(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_empty_p(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_succ(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_not(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_regexpmatch2(jit, ctx, asm) + opt_send_without_block(jit, ctx, asm) + end + + # invokebuiltin + + def opt_invokebuiltin_delegate(jit, ctx, asm) + bf = C.rb_builtin_function.new(jit.operand(0)) + bf_argc = bf.argc + start_index = jit.operand(1) + + # ec, self, and arguments + if bf_argc + 2 > C_ARGS.size + return CantCompile + end + + # If the calls don't allocate, do they need up to date PC, SP? + jit_prepare_routine_call(jit, ctx, asm) + + # Call the builtin func (ec, recv, arg1, arg2, ...) + asm.comment('call builtin func') + asm.mov(C_ARGS[0], EC) + asm.mov(C_ARGS[1], [CFP, C.rb_control_frame_t.offsetof(:self)]) + + # Copy arguments from locals + if bf_argc > 0 + # Load environment pointer EP from CFP + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:ep)]) + + bf_argc.times do |i| + table_size = jit.iseq.body.local_table_size + offs = -table_size - C.VM_ENV_DATA_SIZE + 1 + start_index + i + asm.mov(C_ARGS[2 + i], [:rax, offs * C.VALUE.size]) + end + end + asm.call(bf.func_ptr) + + # Push the return value + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def opt_invokebuiltin_delegate_leave(jit, ctx, asm) + opt_invokebuiltin_delegate(jit, ctx, asm) + # opt_invokebuiltin_delegate is always followed by leave insn + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getlocal_WC_0(jit, ctx, asm) + # Get operands + idx = jit.operand(0) + + # Get EP + asm.mov(:rax, [CFP, C.rb_control_frame_t.offsetof(:ep)]) + + # Get a local variable + asm.mov(:rax, [:rax, -idx * C.VALUE.size]) + + # Push it to the stack + stack_top = ctx.stack_push + asm.mov(stack_top, :rax) + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def getlocal_WC_1(jit, ctx, asm) + idx = jit.operand(0) + jit_getlocal_generic(jit, ctx, asm, idx:, level: 1) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def setlocal_WC_0(jit, ctx, asm) + slot_idx = jit.operand(0) + + # Load environment pointer EP (level 0) from CFP + ep_reg = :rax + jit_get_ep(asm, 0, reg: ep_reg) + + # Write barriers may be required when VM_ENV_FLAG_WB_REQUIRED is set, however write barriers + # only affect heap objects being written. If we know an immediate value is being written we + # can skip this check. + + # flags & VM_ENV_FLAG_WB_REQUIRED + flags_opnd = [ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS] + asm.test(flags_opnd, C.VM_ENV_FLAG_WB_REQUIRED) + + # Create a side-exit to fall back to the interpreter + side_exit = side_exit(jit, ctx) + + # if (flags & VM_ENV_FLAG_WB_REQUIRED) != 0 + asm.jnz(side_exit) + + # Pop the value to write from the stack + stack_top = ctx.stack_pop(1) + + # Write the value at the environment pointer + asm.mov(:rcx, stack_top) + asm.mov([ep_reg, -8 * slot_idx], :rcx) + + KeepCompiling + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def setlocal_WC_1(jit, ctx, asm) + idx = jit.operand(0) + jit_setlocal_generic(jit, ctx, asm, idx:, level: 1) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putobject_INT2FIX_0_(jit, ctx, asm) + putobject(jit, ctx, asm, val: C.to_value(0)) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def putobject_INT2FIX_1_(jit, ctx, asm) + putobject(jit, ctx, asm, val: C.to_value(1)) + end + + # + # C func + # + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_true(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 0 + asm.comment('nil? == true'); + ctx.stack_pop(1) + stack_ret = ctx.stack_push + asm.mov(stack_ret, Qtrue) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_false(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 0 + asm.comment('nil? == false'); + ctx.stack_pop(1) + stack_ret = ctx.stack_push + asm.mov(stack_ret, Qfalse) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_obj_not(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 0 + asm.comment('rb_obj_not') + + recv = ctx.stack_pop + # This `test` sets ZF only for Qnil and Qfalse, which let cmovz set. + asm.test(recv, ~Qnil) + asm.mov(:rax, Qfalse) + asm.mov(:rcx, Qtrue) + asm.cmovz(:rax, :rcx) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_obj_equal(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + asm.comment('equal?') + obj1 = ctx.stack_pop(1) + obj2 = ctx.stack_pop(1) + + asm.mov(:rax, obj1) + asm.mov(:rcx, obj2) + asm.cmp(:rax, :rcx) + asm.mov(:rax, Qfalse) + asm.mov(:rcx, Qtrue) + asm.cmove(:rax, :rcx) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_obj_not_equal(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + jit_equality_specialized(jit, ctx, asm, false) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_mod_eqq(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + + asm.comment('Module#===') + # By being here, we know that the receiver is a T_MODULE or a T_CLASS, because Module#=== can + # only live on these objects. With that, we can call rb_obj_is_kind_of() without + # jit_prepare_routine_call() or a control frame push because it can't raise, allocate, or call + # Ruby methods with these inputs. + # Note the difference in approach from Kernel#is_a? because we don't get a free guard for the + # right hand side. + lhs = ctx.stack_opnd(1) # the module + rhs = ctx.stack_opnd(0) + asm.mov(C_ARGS[0], rhs); + asm.mov(C_ARGS[1], lhs); + asm.call(C.rb_obj_is_kind_of) + + # Return the result + ctx.stack_pop(2) + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + return true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_int_equal(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + return false unless two_fixnums_on_stack?(jit) + + side_exit = side_exit(jit, ctx) + guard_two_fixnums(jit, ctx, asm, side_exit) + + # Compare the arguments + asm.comment('rb_int_equal') + arg1 = ctx.stack_pop(1) + arg0 = ctx.stack_pop(1) + asm.mov(:rax, arg1) + asm.cmp(arg0, :rax) + asm.mov(:rax, Qfalse) + asm.mov(:rcx, Qtrue) + asm.cmove(:rax, :rcx) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_int_mul(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + return false unless two_fixnums_on_stack?(jit) + + side_exit = side_exit(jit, ctx) + guard_two_fixnums(jit, ctx, asm, side_exit) + + asm.comment('rb_int_mul') + y_opnd = ctx.stack_pop + x_opnd = ctx.stack_pop + asm.mov(C_ARGS[0], x_opnd) + asm.mov(C_ARGS[1], y_opnd) + asm.call(C.rb_fix_mul_fix) + + ret_opnd = ctx.stack_push + asm.mov(ret_opnd, C_RET) + true + end + + def jit_rb_int_div(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + return false unless two_fixnums_on_stack?(jit) + + side_exit = side_exit(jit, ctx) + guard_two_fixnums(jit, ctx, asm, side_exit) + + asm.comment('rb_int_div') + y_opnd = ctx.stack_pop + x_opnd = ctx.stack_pop + asm.mov(:rax, y_opnd) + asm.cmp(:rax, C.to_value(0)) + asm.je(side_exit) + + asm.mov(C_ARGS[0], x_opnd) + asm.mov(C_ARGS[1], :rax) + asm.call(C.rb_fix_div_fix) + + ret_opnd = ctx.stack_push + asm.mov(ret_opnd, C_RET) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_int_aref(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + return false unless two_fixnums_on_stack?(jit) + + side_exit = side_exit(jit, ctx) + guard_two_fixnums(jit, ctx, asm, side_exit) + + asm.comment('rb_int_aref') + y_opnd = ctx.stack_pop + x_opnd = ctx.stack_pop + + asm.mov(C_ARGS[0], x_opnd) + asm.mov(C_ARGS[1], y_opnd) + asm.call(C.rb_fix_aref) + + ret_opnd = ctx.stack_push + asm.mov(ret_opnd, C_RET) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_str_to_s(jit, ctx, asm, argc, known_recv_class) + return false if argc != 0 + if known_recv_class == String + asm.comment('to_s on plain string') + # The method returns the receiver, which is already on the stack. + # No stack movement. + return true + end + false + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_str_getbyte(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + asm.comment('rb_str_getbyte') + + index_opnd = ctx.stack_pop + str_opnd = ctx.stack_pop + asm.mov(C_ARGS[0], str_opnd) + asm.mov(C_ARGS[1], index_opnd) + asm.call(C.rb_str_getbyte) + + ret_opnd = ctx.stack_push + asm.mov(ret_opnd, C_RET) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_rb_ary_push(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 1 + asm.comment('rb_ary_push') + + jit_prepare_routine_call(jit, ctx, asm) + + item_opnd = ctx.stack_pop + ary_opnd = ctx.stack_pop + asm.mov(C_ARGS[0], ary_opnd) + asm.mov(C_ARGS[1], item_opnd) + asm.call(C.rb_ary_push) + + ret_opnd = ctx.stack_push + asm.mov(ret_opnd, C_RET) + true + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_thread_s_current(jit, ctx, asm, argc, _known_recv_class) + return false if argc != 0 + asm.comment('Thread.current') + ctx.stack_pop(1) + + # ec->thread_ptr + asm.mov(:rax, [EC, C.rb_execution_context_t.offsetof(:thread_ptr)]) + + # thread->self + asm.mov(:rax, [:rax, C.rb_thread_struct.offsetof(:self)]) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, :rax) + true + end + + # + # Helpers + # + + def register_cfunc_codegen_funcs + # Specialization for C methods. See register_cfunc_method for details. + register_cfunc_method(BasicObject, :!, :jit_rb_obj_not) + + register_cfunc_method(NilClass, :nil?, :jit_rb_true) + register_cfunc_method(Kernel, :nil?, :jit_rb_false) + #register_cfunc_method(Kernel, :is_a?, :jit_rb_kernel_is_a) + #register_cfunc_method(Kernel, :kind_of?, :jit_rb_kernel_is_a) + #register_cfunc_method(Kernel, :instance_of?, :jit_rb_kernel_instance_of) + + register_cfunc_method(BasicObject, :==, :jit_rb_obj_equal) + register_cfunc_method(BasicObject, :equal?, :jit_rb_obj_equal) + register_cfunc_method(BasicObject, :!=, :jit_rb_obj_not_equal) + register_cfunc_method(Kernel, :eql?, :jit_rb_obj_equal) + register_cfunc_method(Module, :==, :jit_rb_obj_equal) + register_cfunc_method(Module, :===, :jit_rb_mod_eqq) + register_cfunc_method(Symbol, :==, :jit_rb_obj_equal) + register_cfunc_method(Symbol, :===, :jit_rb_obj_equal) + register_cfunc_method(Integer, :==, :jit_rb_int_equal) + register_cfunc_method(Integer, :===, :jit_rb_int_equal) + + # rb_str_to_s() methods in string.c + #register_cfunc_method(String, :empty?, :jit_rb_str_empty_p) + register_cfunc_method(String, :to_s, :jit_rb_str_to_s) + register_cfunc_method(String, :to_str, :jit_rb_str_to_s) + #register_cfunc_method(String, :bytesize, :jit_rb_str_bytesize) + #register_cfunc_method(String, :<<, :jit_rb_str_concat) + #register_cfunc_method(String, :+@, :jit_rb_str_uplus) + + # rb_ary_empty_p() method in array.c + #register_cfunc_method(Array, :empty?, :jit_rb_ary_empty_p) + + #register_cfunc_method(Kernel, :respond_to?, :jit_obj_respond_to) + #register_cfunc_method(Kernel, :block_given?, :jit_rb_f_block_given_p) + + # Thread.current + register_cfunc_method(C.rb_singleton_class(Thread), :current, :jit_thread_s_current) + + #--- + register_cfunc_method(Array, :<<, :jit_rb_ary_push) + register_cfunc_method(Integer, :*, :jit_rb_int_mul) + register_cfunc_method(Integer, :/, :jit_rb_int_div) + register_cfunc_method(Integer, :[], :jit_rb_int_aref) + register_cfunc_method(String, :getbyte, :jit_rb_str_getbyte) + end + + def register_cfunc_method(klass, mid_sym, func) + mid = C.rb_intern(mid_sym.to_s) + me = C.rb_method_entry_at(klass, mid) + + assert_equal(false, me.nil?) + + # Only cfuncs are supported + method_serial = me.def.method_serial + + @cfunc_codegen_table[method_serial] = method(func) + end + + def lookup_cfunc_codegen(cme_def) + @cfunc_codegen_table[cme_def.method_serial] + end + + def jit_getlocal_generic(jit, ctx, asm, idx:, level:) + # Load environment pointer EP at level + ep_reg = :rax + jit_get_ep(asm, level, reg: ep_reg) + + # Get a local variable + asm.mov(:rax, [ep_reg, -idx * C.VALUE.size]) + + # Push it to the stack + stack_top = ctx.stack_push + asm.mov(stack_top, :rax) + KeepCompiling + end + + def jit_setlocal_generic(jit, ctx, asm, idx:, level:) + # Load environment pointer EP at level + ep_reg = :rax + jit_get_ep(asm, level, reg: ep_reg) + + # Write barriers may be required when VM_ENV_FLAG_WB_REQUIRED is set, however write barriers + # only affect heap objects being written. If we know an immediate value is being written we + # can skip this check. + + # flags & VM_ENV_FLAG_WB_REQUIRED + flags_opnd = [ep_reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_FLAGS] + asm.test(flags_opnd, C.VM_ENV_FLAG_WB_REQUIRED) + + # Create a side-exit to fall back to the interpreter + side_exit = side_exit(jit, ctx) + + # if (flags & VM_ENV_FLAG_WB_REQUIRED) != 0 + asm.jnz(side_exit) + + # Pop the value to write from the stack + stack_top = ctx.stack_pop(1) + + # Write the value at the environment pointer + asm.mov(:rcx, stack_top) + asm.mov([ep_reg, -(C.VALUE.size * idx)], :rcx) + + KeepCompiling + end + + # Compute the index of a local variable from its slot index + def slot_to_local_idx(iseq, slot_idx) + # Layout illustration + # This is an array of VALUE + # | VM_ENV_DATA_SIZE | + # v v + # low addr <+-------+-------+-------+-------+------------------+ + # |local 0|local 1| ... |local n| .... | + # +-------+-------+-------+-------+------------------+ + # ^ ^ ^ ^ + # +-------+---local_table_size----+ cfp->ep--+ + # | | + # +------------------slot_idx----------------+ + # + # See usages of local_var_name() from iseq.c for similar calculation. + + local_table_size = iseq.body.local_table_size + op = slot_idx - C.VM_ENV_DATA_SIZE + local_table_size - op - 1 + end + + # @param asm [RubyVM::MJIT::Assembler] + def guard_object_is_heap(asm, object_opnd, side_exit) + asm.comment('guard object is heap') + # Test that the object is not an immediate + asm.test(object_opnd, C.RUBY_IMMEDIATE_MASK) + asm.jnz(side_exit) + + # Test that the object is not false + asm.cmp(object_opnd, Qfalse) + asm.je(side_exit) + end + + # @param asm [RubyVM::MJIT::Assembler] + def guard_object_is_array(asm, object_reg, flags_reg, side_exit) + asm.comment('guard object is array') + # Pull out the type mask + asm.mov(flags_reg, [object_reg, C.RBasic.offsetof(:flags)]) + asm.and(flags_reg, C.RUBY_T_MASK) + + # Compare the result with T_ARRAY + asm.cmp(flags_reg, C.RUBY_T_ARRAY) + asm.jne(side_exit) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_chain_guard(opcode, jit, ctx, asm, side_exit, limit: 20) + opcode => :je | :jne | :jnz | :jz + + if ctx.chain_depth < limit + deeper = ctx.dup + deeper.chain_depth += 1 + + branch_stub = BranchStub.new( + iseq: jit.iseq, + shape: Default, + target0: BranchTarget.new(ctx: deeper, pc: jit.pc), + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(deeper, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.compile = proc do |branch_asm| + # Not using `asm.comment` here since it's usually put before cmp/test before this. + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.public_send(opcode, branch_stub.target0.address) + end + end + end + branch_stub.compile.call(asm) + else + asm.public_send(opcode, side_exit) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_guard_known_klass(jit, ctx, asm, known_klass, obj_opnd, comptime_obj, side_exit, limit: 10) + # Only memory operand is supported for now + assert_equal(true, obj_opnd.is_a?(Array)) + + if known_klass == NilClass + asm.comment('guard object is nil') + asm.cmp(obj_opnd, Qnil) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + elsif known_klass == TrueClass + asm.comment('guard object is true') + asm.cmp(obj_opnd, Qtrue) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + elsif known_klass == FalseClass + asm.comment('guard object is false') + asm.cmp(obj_opnd, Qfalse) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + elsif known_klass == Integer && fixnum?(comptime_obj) + asm.comment('guard object is fixnum') + asm.test(obj_opnd, C.RUBY_FIXNUM_FLAG) + jit_chain_guard(:jz, jit, ctx, asm, side_exit, limit:) + elsif known_klass == Symbol && static_symbol?(comptime_obj) + # We will guard STATIC vs DYNAMIC as though they were separate classes + # DYNAMIC symbols can be handled by the general else case below + asm.comment('guard object is static symbol') + assert_equal(8, C.RUBY_SPECIAL_SHIFT) + asm.cmp(BytePtr[*obj_opnd], C.RUBY_SYMBOL_FLAG) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + elsif known_klass == Float && flonum?(comptime_obj) + # We will guard flonum vs heap float as though they were separate classes + asm.comment('guard object is flonum') + asm.mov(:rax, obj_opnd) + asm.and(:rax, C.RUBY_FLONUM_MASK) + asm.cmp(:rax, C.RUBY_FLONUM_FLAG) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + elsif C.FL_TEST(known_klass, C.RUBY_FL_SINGLETON) && comptime_obj == C.rb_class_attached_object(known_klass) + asm.comment('guard known object with singleton class') + asm.mov(:rax, C.to_value(comptime_obj)) + asm.cmp(obj_opnd, :rax) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + else + # Load memory to a register + asm.mov(:rax, obj_opnd) + obj_opnd = :rax + + # Check that the receiver is a heap object + # Note: if we get here, the class doesn't have immediate instances. + asm.comment('guard not immediate') + asm.test(obj_opnd, C.RUBY_IMMEDIATE_MASK) + jit_chain_guard(:jnz, jit, ctx, asm, side_exit, limit:) + asm.cmp(obj_opnd, Qfalse) + jit_chain_guard(:je, jit, ctx, asm, side_exit, limit:) + + # Bail if receiver class is different from known_klass + klass_opnd = [obj_opnd, C.RBasic.offsetof(:klass)] + asm.comment("guard known class #{known_klass}") + asm.mov(:rcx, to_value(known_klass)) + asm.cmp(klass_opnd, :rcx) + jit_chain_guard(:jne, jit, ctx, asm, side_exit, limit:) + end + end + + # @param jit [RubyVM::MJIT::JITState] + def two_fixnums_on_stack?(jit) + comptime_recv = jit.peek_at_stack(1) + comptime_arg = jit.peek_at_stack(0) + return fixnum?(comptime_recv) && fixnum?(comptime_arg) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def guard_two_fixnums(jit, ctx, asm, side_exit) + # Get stack operands without popping them + arg1 = ctx.stack_opnd(0) + arg0 = ctx.stack_opnd(1) + + asm.comment('guard arg0 fixnum') + asm.test(arg0, C.RUBY_FIXNUM_FLAG) + jit_chain_guard(:jz, jit, ctx, asm, side_exit) + # TODO: upgrade type, and skip the check when possible + + asm.comment('guard arg1 fixnum') + asm.test(arg1, C.RUBY_FIXNUM_FLAG) + jit_chain_guard(:jz, jit, ctx, asm, side_exit) + # TODO: upgrade type, and skip the check when possible + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_fixnum_cmp(jit, ctx, asm, opcode:, bop:) + opcode => :cmovl | :cmovle | :cmovg | :cmovge + + unless jit.at_current_insn? + defer_compilation(jit, ctx, asm) + return EndBlock + end + + comptime_recv = jit.peek_at_stack(1) + comptime_obj = jit.peek_at_stack(0) + + if fixnum?(comptime_recv) && fixnum?(comptime_obj) + # Generate a side exit before popping operands + side_exit = side_exit(jit, ctx) + + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, bop) + return CantCompile + end + + obj_opnd = ctx.stack_pop + recv_opnd = ctx.stack_pop + + asm.comment('guard recv is fixnum') # TODO: skip this with type information + asm.test(recv_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.comment('guard obj is fixnum') # TODO: skip this with type information + asm.test(obj_opnd, C.RUBY_FIXNUM_FLAG) + asm.jz(side_exit) + + asm.mov(:rax, obj_opnd) + asm.cmp(recv_opnd, :rax) + asm.mov(:rax, Qfalse) + asm.mov(:rcx, Qtrue) + asm.public_send(opcode, :rax, :rcx) + + dst_opnd = ctx.stack_push + asm.mov(dst_opnd, :rax) + + KeepCompiling + else + opt_send_without_block(jit, ctx, asm) + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_equality_specialized(jit, ctx, asm, gen_eq) + # Create a side-exit to fall back to the interpreter + side_exit = side_exit(jit, ctx) + + a_opnd = ctx.stack_opnd(1) + b_opnd = ctx.stack_opnd(0) + + comptime_a = jit.peek_at_stack(1) + comptime_b = jit.peek_at_stack(0) + + if two_fixnums_on_stack?(jit) + unless Invariants.assume_bop_not_redefined(jit, C.INTEGER_REDEFINED_OP_FLAG, C.BOP_EQ) + return false + end + + guard_two_fixnums(jit, ctx, asm, side_exit) + + asm.comment('check fixnum equality') + asm.mov(:rax, a_opnd) + asm.mov(:rcx, b_opnd) + asm.cmp(:rax, :rcx) + asm.mov(:rax, gen_eq ? Qfalse : Qtrue) + asm.mov(:rcx, gen_eq ? Qtrue : Qfalse) + asm.cmove(:rax, :rcx) + + # Push the output on the stack + ctx.stack_pop(2) + dst = ctx.stack_push + asm.mov(dst, :rax) + + true + elsif C.rb_class_of(comptime_a) == String && C.rb_class_of(comptime_b) == String + unless Invariants.assume_bop_not_redefined(jit, C.STRING_REDEFINED_OP_FLAG, C.BOP_EQ) + # if overridden, emit the generic version + return false + end + + # Guard that a is a String + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_a), a_opnd, comptime_a, side_exit) + + equal_label = asm.new_label(:equal) + ret_label = asm.new_label(:ret) + + # If they are equal by identity, return true + asm.mov(:rax, a_opnd) + asm.mov(:rcx, b_opnd) + asm.cmp(:rax, :rcx) + asm.je(equal_label) + + # Otherwise guard that b is a T_STRING (from type info) or String (from runtime guard) + # Note: any T_STRING is valid here, but we check for a ::String for simplicity + # To pass a mutable static variable (rb_cString) requires an unsafe block + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_b), b_opnd, comptime_b, side_exit) + + asm.comment('call rb_str_eql_internal') + asm.mov(C_ARGS[0], a_opnd) + asm.mov(C_ARGS[1], b_opnd) + asm.call(gen_eq ? C.rb_str_eql_internal : C.rb_str_neq_internal) + + # Push the output on the stack + ctx.stack_pop(2) + dst = ctx.stack_push + asm.mov(dst, C_RET) + asm.jmp(ret_label) + + asm.write_label(equal_label) + asm.mov(dst, gen_eq ? Qtrue : Qfalse) + + asm.write_label(ret_label) + + true + else + false + end + end + + # NOTE: This clobbers :rax + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_prepare_routine_call(jit, ctx, asm) + jit.record_boundary_patch_point = true + jit_save_pc(jit, asm) + jit_save_sp(jit, ctx, asm) + end + + # Note: This clobbers :rax + # @param jit [RubyVM::MJIT::JITState] + # @param asm [RubyVM::MJIT::Assembler] + def jit_save_pc(jit, asm, comment: 'save PC to CFP') + next_pc = jit.pc + jit.insn.len * C.VALUE.size # Use the next one for backtrace and side exits + asm.comment(comment) + asm.mov(:rax, next_pc) + asm.mov([CFP, C.rb_control_frame_t.offsetof(:pc)], :rax) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_save_sp(jit, ctx, asm) + if ctx.sp_offset != 0 + asm.comment('save SP to CFP') + asm.lea(SP, ctx.sp_opnd) + asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) + ctx.sp_offset = 0 + end + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jump_to_next_insn(jit, ctx, asm) + reset_depth = ctx.dup + reset_depth.chain_depth = 0 + + next_pc = jit.pc + jit.insn.len * C.VALUE.size + + # We are at the end of the current instruction. Record the boundary. + if jit.record_boundary_patch_point + exit_pos = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_side_exit(next_pc, ctx, ocb_asm) + @ocb.write(ocb_asm) + end + Invariants.record_global_inval_patch(asm, exit_pos) + jit.record_boundary_patch_point = false + end + + stub_next_block(jit.iseq, next_pc, reset_depth, asm, comment: 'jump_to_next_insn') + end + + # rb_vm_check_ints + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_check_ints(jit, ctx, asm) + asm.comment('RUBY_VM_CHECK_INTS(ec)') + asm.mov(:eax, [EC, C.rb_execution_context_t.offsetof(:interrupt_flag)]) + asm.test(:eax, :eax) + asm.jnz(side_exit(jit, ctx)) + end + + # See get_lvar_level in compile.c + def get_lvar_level(iseq) + level = 0 + while iseq.to_i != iseq.body.local_iseq.to_i + level += 1 + iseq = iseq.body.parent_iseq + end + return level + end + + # GET_LEP + # @param jit [RubyVM::MJIT::JITState] + # @param asm [RubyVM::MJIT::Assembler] + def jit_get_lep(jit, asm, reg:) + level = get_lvar_level(jit.iseq) + jit_get_ep(asm, level, reg:) + end + + # vm_get_ep + # @param asm [RubyVM::MJIT::Assembler] + def jit_get_ep(asm, level, reg:) + asm.mov(reg, [CFP, C.rb_control_frame_t.offsetof(:ep)]) + level.times do + # GET_PREV_EP: ep[VM_ENV_DATA_INDEX_SPECVAL] & ~0x03 + asm.mov(reg, [reg, C.VALUE.size * C.VM_ENV_DATA_INDEX_SPECVAL]) + asm.and(reg, ~0x03) + end + end + + # vm_getivar + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_getivar(jit, ctx, asm, comptime_obj, ivar_id, obj_opnd = nil) + side_exit = side_exit(jit, ctx) + starting_ctx = ctx.dup # copy for jit_chain_guard + + # Guard not special const + if C.SPECIAL_CONST_P(comptime_obj) + asm.incr_counter(:getivar_special_const) + return CantCompile + end + + case C.BUILTIN_TYPE(comptime_obj) + when C.T_OBJECT + # This is the only supported case for now (ROBJECT_IVPTR) + else + # General case. Call rb_ivar_get(). + # VALUE rb_ivar_get(VALUE obj, ID id) + asm.comment('call rb_ivar_get()') + asm.mov(C_ARGS[0], obj_opnd ? obj_opnd : [CFP, C.rb_control_frame_t.offsetof(:self)]) + asm.mov(C_ARGS[1], ivar_id) + + # The function could raise exceptions. + jit_prepare_routine_call(jit, ctx, asm) # clobbers obj_opnd and :rax + + asm.call(C.rb_ivar_get) + + if obj_opnd # attr_reader + ctx.stack_pop + end + + # Push the ivar on the stack + out_opnd = ctx.stack_push + asm.mov(out_opnd, C_RET) + + # Jump to next instruction. This allows guard chains to share the same successor. + jump_to_next_insn(jit, ctx, asm) + return EndBlock + end + + asm.mov(:rax, obj_opnd ? obj_opnd : [CFP, C.rb_control_frame_t.offsetof(:self)]) + guard_object_is_heap(asm, :rax, counted_exit(side_exit, :getivar_not_heap)) + + shape_id = C.rb_shape_get_shape_id(comptime_obj) + if shape_id == C.OBJ_TOO_COMPLEX_SHAPE_ID + asm.incr_counter(:getivar_too_complex) + return CantCompile + end + + asm.comment('guard shape') + asm.cmp(DwordPtr[:rax, C.rb_shape_id_offset], shape_id) + jit_chain_guard(:jne, jit, starting_ctx, asm, counted_exit(side_exit, :getivar_megamorphic)) + + index = C.rb_shape_get_iv_index(shape_id, ivar_id) + if index + asm.comment('ROBJECT_IVPTR') + if C.FL_TEST_RAW(comptime_obj, C.ROBJECT_EMBED) + # Access embedded array + asm.mov(:rax, [:rax, C.RObject.offsetof(:as, :ary) + (index * C.VALUE.size)]) + else + # Pull out an ivar table on heap + asm.mov(:rax, [:rax, C.RObject.offsetof(:as, :heap, :ivptr)]) + # Read the table + asm.mov(:rax, [:rax, index * C.VALUE.size]) + end + val_opnd = :rax + else + val_opnd = Qnil + end + + if obj_opnd + ctx.stack_pop # pop receiver for attr_reader + end + stack_opnd = ctx.stack_push + asm.mov(stack_opnd, val_opnd) + + # Let guard chains share the same successor + jump_to_next_insn(jit, ctx, asm) + EndBlock + end + + def jit_write_iv(asm, comptime_receiver, recv_reg, temp_reg, ivar_index, set_value, needs_extension) + # Compile time self is embedded and the ivar index lands within the object + embed_test_result = C.FL_TEST_RAW(comptime_receiver, C.ROBJECT_EMBED) && !needs_extension + + if embed_test_result + # Find the IV offset + offs = C.RObject.offsetof(:as, :ary) + ivar_index * C.VALUE.size + + # Write the IV + asm.comment('write IV') + asm.mov(temp_reg, set_value) + asm.mov([recv_reg, offs], temp_reg) + else + # Compile time value is *not* embedded. + + # Get a pointer to the extended table + asm.mov(recv_reg, [recv_reg, C.RObject.offsetof(:as, :heap, :ivptr)]) + + # Write the ivar in to the extended table + asm.comment("write IV"); + asm.mov(temp_reg, set_value) + asm.mov([recv_reg, C.VALUE.size * ivar_index], temp_reg) + end + end + + # vm_caller_setup_arg_block + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_caller_setup_arg_block(jit, ctx, asm, ci, blockiseq, is_super) + side_exit = side_exit(jit, ctx) + if C.vm_ci_flag(ci) & C.VM_CALL_ARGS_BLOCKARG != 0 + # TODO: Skip cmp + jne using Context? + block_code = jit.peek_at_stack(0) + block_opnd = ctx.stack_opnd(0) # to be popped after eliminating side exit possibility + if block_code.nil? + asm.cmp(block_opnd, Qnil) + jit_chain_guard(:jne, jit, ctx, asm, counted_exit(side_exit, :send_block_not_nil)) + return C.VM_BLOCK_HANDLER_NONE + elsif C.to_value(block_code) == C.rb_block_param_proxy + asm.mov(:rax, C.rb_block_param_proxy) + asm.cmp(block_opnd, :rax) + jit_chain_guard(:jne, jit, ctx, asm, counted_exit(side_exit, :send_block_not_proxy)) + return C.rb_block_param_proxy + else + asm.incr_counter(:send_blockarg_not_nil_or_proxy) + return CantCompile + end + elsif blockiseq != 0 + return blockiseq + else + if is_super + # GET_BLOCK_HANDLER(); + # Guard no block passed. Only handle that case for now. + asm.comment('guard no block given') + jit_get_lep(jit, asm, reg: :rax) + asm.cmp([:rax, C.VALUE.size * C.VM_ENV_DATA_INDEX_SPECVAL], C.VM_BLOCK_HANDLER_NONE) + asm.jne(counted_exit(side_exit, :send_block_handler)) + return C.VM_BLOCK_HANDLER_NONE + else + # Not implemented yet. Is this even necessary? + asm.incr_counter(:send_block_setup) + return CantCompile + end + end + end + + # vm_search_method + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_search_method(jit, ctx, asm, mid, argc, flags, send_shift: 0) + assert_equal(true, jit.at_current_insn?) + + # Generate a side exit + side_exit = side_exit(jit, ctx) + + # kw_splat is not supported yet + if flags & C.VM_CALL_KW_SPLAT != 0 + asm.incr_counter(:send_kw_splat) + return CantCompile + end + + # Get a compile-time receiver and its class + recv_idx = argc + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0) # blockarg is not popped yet + recv_idx += send_shift + comptime_recv = jit.peek_at_stack(recv_idx + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0)) # this offset is in ctx but not in SP + comptime_recv_klass = C.rb_class_of(comptime_recv) + + # Guard the receiver class (part of vm_search_method_fastpath) + recv_opnd = ctx.stack_opnd(recv_idx) + megamorphic_exit = counted_exit(side_exit, :send_klass_megamorphic) + jit_guard_known_klass(jit, ctx, asm, comptime_recv_klass, recv_opnd, comptime_recv, megamorphic_exit) + + # Do method lookup (vm_cc_cme(cc) != NULL) + cme = C.rb_callable_method_entry(comptime_recv_klass, mid) + if cme.nil? + asm.incr_counter(:send_missing_cme) + return CantCompile # We don't support vm_call_method_name + end + + # Invalidate on redefinition (part of vm_search_method_fastpath) + Invariants.assume_method_lookup_stable(jit, cme) + + return cme, comptime_recv_klass + end + + def jit_search_super_method(jit, ctx, asm, mid, argc, flags) + assert_equal(true, jit.at_current_insn?) + + me = C.rb_vm_frame_method_entry(jit.cfp) + if me.nil? + return CantCompile + end + + # FIXME: We should track and invalidate this block when this cme is invalidated + current_defined_class = me.defined_class + mid = me.def.original_id + + if me.to_i != C.rb_callable_method_entry(current_defined_class, me.called_id).to_i + # Though we likely could generate this call, as we are only concerned + # with the method entry remaining valid, assume_method_lookup_stable + # below requires that the method lookup matches as well + return CantCompile + end + + # vm_search_normal_superclass + rbasic_klass = C.to_ruby(C.RBasic.new(C.to_value(current_defined_class)).klass) + if C.BUILTIN_TYPE(current_defined_class) == C.RUBY_T_ICLASS && C.BUILTIN_TYPE(rbasic_klass) == C.RUBY_T_MODULE && \ + C.FL_TEST_RAW(rbasic_klass, C.RMODULE_IS_REFINEMENT) != 0 + return CantCompile + end + comptime_superclass = C.rb_class_get_superclass(current_defined_class) + + # Don't JIT calls that aren't simple + # Note, not using VM_CALL_ARGS_SIMPLE because sometimes we pass a block. + + if flags & C.VM_CALL_KWARG != 0 + asm.incr_counter(:send_kwarg) + return CantCompile + end + if flags & C.VM_CALL_KW_SPLAT != 0 + asm.incr_counter(:send_kw_splat) + return CantCompile + end + + # Ensure we haven't rebound this method onto an incompatible class. + # In the interpreter we try to avoid making this check by performing some + # cheaper calculations first, but since we specialize on the method entry + # and so only have to do this once at compile time this is fine to always + # check and side exit. + comptime_recv = jit.peek_at_stack(argc) + unless C.obj_is_kind_of(comptime_recv, current_defined_class) + return CantCompile + end + + # Do method lookup + cme = C.rb_callable_method_entry(comptime_superclass, mid) + + if cme.nil? + return CantCompile + end + + # workaround -- TODO: Why does this happen? + if me.to_i == cme.to_i + asm.incr_counter(:invokesuper_same_me) + return CantCompile + end + + # Check that we'll be able to write this method dispatch before generating checks + cme_def_type = cme.def.type + if cme_def_type != C.VM_METHOD_TYPE_ISEQ && cme_def_type != C.VM_METHOD_TYPE_CFUNC + # others unimplemented + return CantCompile + end + + # Guard that the receiver has the same class as the one from compile time + side_exit = side_exit(jit, ctx) + + asm.comment('guard known me') + jit_get_lep(jit, asm, reg: :rax) + + asm.mov(:rcx, me.to_i) + asm.cmp([:rax, C.VALUE.size * C.VM_ENV_DATA_INDEX_ME_CREF], :rcx) + asm.jne(counted_exit(side_exit, :invokesuper_me_changed)) + + # We need to assume that both our current method entry and the super + # method entry we invoke remain stable + Invariants.assume_method_lookup_stable(jit, me) + Invariants.assume_method_lookup_stable(jit, cme) + + return cme + end + + # vm_call_general + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_general(jit, ctx, asm, mid, argc, flags, cme, block_handler, known_recv_class) + jit_call_method(jit, ctx, asm, mid, argc, flags, cme, block_handler, known_recv_class) + end + + # vm_call_method + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + # @param send_shift [Integer] The number of shifts needed for VM_CALL_OPT_SEND + def jit_call_method(jit, ctx, asm, mid, argc, flags, cme, block_handler, known_recv_class, send_shift: 0) + # The main check of vm_call_method before vm_call_method_each_type + case C.METHOD_ENTRY_VISI(cme) + when C.METHOD_VISI_PUBLIC + # You can always call public methods + when C.METHOD_VISI_PRIVATE + # Allow only callsites without a receiver + if flags & C.VM_CALL_FCALL == 0 + asm.incr_counter(:send_private) + return CantCompile + end + when C.METHOD_VISI_PROTECTED + # If the method call is an FCALL, it is always valid + if flags & C.VM_CALL_FCALL == 0 + # otherwise we need an ancestry check to ensure the receiver is valid to be called as protected + jit_protected_callee_ancestry_guard(asm, cme, side_exit(jit, ctx)) + end + else + # TODO: Change them to a constant and use case-in instead + raise 'unreachable' + end + + # Get a compile-time receiver + recv_idx = argc + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0) # blockarg is not popped yet + recv_idx += send_shift + comptime_recv = jit.peek_at_stack(recv_idx + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0)) # this offset is in ctx but not in SP + recv_opnd = ctx.stack_opnd(recv_idx) + + jit_call_method_each_type(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + end + + # Generate ancestry guard for protected callee. + # Calls to protected callees only go through when self.is_a?(klass_that_defines_the_callee). + def jit_protected_callee_ancestry_guard(asm, cme, side_exit) + # See vm_call_method(). + def_class = cme.defined_class + # Note: PC isn't written to current control frame as rb_is_kind_of() shouldn't raise. + # VALUE rb_obj_is_kind_of(VALUE obj, VALUE klass); + + asm.mov(C_ARGS[0], [CFP, C.rb_control_frame_t.offsetof(:self)]) + asm.mov(C_ARGS[1], to_value(def_class)) + asm.call(C.rb_obj_is_kind_of) + asm.test(C_RET, C_RET) + asm.jz(counted_exit(side_exit, :send_protected_check_failed)) + end + + # vm_call_method_each_type + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_method_each_type(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + case cme.def.type + when C.VM_METHOD_TYPE_ISEQ + iseq = def_iseq_ptr(cme.def) + jit_call_iseq_setup(jit, ctx, asm, cme, flags, argc, iseq, block_handler, send_shift:) + when C.VM_METHOD_TYPE_NOTIMPLEMENTED + asm.incr_counter(:send_notimplemented) + return CantCompile + when C.VM_METHOD_TYPE_CFUNC + jit_call_cfunc(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + when C.VM_METHOD_TYPE_ATTRSET + asm.incr_counter(:send_attrset) + return CantCompile + when C.VM_METHOD_TYPE_IVAR + jit_call_ivar(jit, ctx, asm, cme, flags, argc, comptime_recv, recv_opnd, send_shift:) + when C.VM_METHOD_TYPE_MISSING + asm.incr_counter(:send_missing) + return CantCompile + when C.VM_METHOD_TYPE_BMETHOD + jit_call_bmethod(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + when C.VM_METHOD_TYPE_ALIAS + jit_call_alias(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + when C.VM_METHOD_TYPE_OPTIMIZED + jit_call_optimized(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + when C.VM_METHOD_TYPE_UNDEF + asm.incr_counter(:send_undef) + return CantCompile + when C.VM_METHOD_TYPE_ZSUPER + asm.incr_counter(:send_zsuper) + return CantCompile + when C.VM_METHOD_TYPE_REFINED + asm.incr_counter(:send_refined) + return CantCompile + else + asm.incr_counter(:send_unknown_type) + return CantCompile + end + end + + # vm_call_iseq_setup + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_iseq_setup(jit, ctx, asm, cme, flags, argc, iseq, block_handler, send_shift:, frame_type: nil, prev_ep: nil) + opt_pc = jit_callee_setup_arg(jit, ctx, asm, flags, argc, iseq) + if opt_pc == CantCompile + return CantCompile + end + + if flags & C.VM_CALL_TAILCALL != 0 + # We don't support vm_call_iseq_setup_tailcall + asm.incr_counter(:send_tailcall) + return CantCompile + end + jit_call_iseq_setup_normal(jit, ctx, asm, cme, flags, argc, iseq, block_handler, opt_pc, send_shift:, frame_type:, prev_ep:) + end + + # vm_call_iseq_setup_normal (vm_call_iseq_setup_2 -> vm_call_iseq_setup_normal) + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_iseq_setup_normal(jit, ctx, asm, cme, flags, argc, iseq, block_handler, opt_pc, send_shift:, frame_type:, prev_ep:) + # We will not have side exits from here. Adjust the stack. + if flags & C.VM_CALL_OPT_SEND != 0 + jit_call_opt_send_shift_stack(ctx, asm, argc, send_shift:) + end + + # Save caller SP and PC before pushing a callee frame for backtrace and side exits + asm.comment('save SP to caller CFP') + recv_idx = argc + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0) # blockarg is not popped yet + # Skip setting this to SP register. This cfp->sp will be copied to SP on leave insn. + asm.lea(:rax, ctx.sp_opnd(C.VALUE.size * -(1 + recv_idx))) # Pop receiver and arguments to prepare for side exits + asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], :rax) + jit_save_pc(jit, asm, comment: 'save PC to caller CFP') + + frame_type ||= C.VM_FRAME_MAGIC_METHOD | C.VM_ENV_FLAG_LOCAL + jit_push_frame( + jit, ctx, asm, cme, flags, argc, frame_type, block_handler, + iseq: iseq, + local_size: iseq.body.local_table_size - iseq.body.param.size, + stack_max: iseq.body.stack_max, + prev_ep:, + ) + + # Jump to a stub for the callee ISEQ + callee_ctx = Context.new + pc = (iseq.body.iseq_encoded + opt_pc).to_i + stub_next_block(iseq, pc, callee_ctx, asm) + + EndBlock + end + + # vm_call_cfunc + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_cfunc(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + if jit_caller_setup_arg(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + if jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + + jit_call_cfunc_with_frame(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + end + + # jit_call_cfunc_with_frame + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_cfunc_with_frame(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + cfunc = cme.def.body.cfunc + + if argc + 1 > 6 + asm.incr_counter(:send_cfunc_too_many_args) + return CantCompile + end + + frame_type = C.VM_FRAME_MAGIC_CFUNC | C.VM_FRAME_FLAG_CFRAME | C.VM_ENV_FLAG_LOCAL + if flags & C.VM_CALL_KW_SPLAT != 0 + frame_type |= C.VM_FRAME_FLAG_CFRAME_KW + end + + # EXEC_EVENT_HOOK: RUBY_EVENT_C_CALL and RUBY_EVENT_C_RETURN + if C.rb_mjit_global_events & (C.RUBY_EVENT_C_CALL | C.RUBY_EVENT_C_RETURN) != 0 + asm.incr_counter(:send_c_tracing) + return CantCompile + end + + # rb_check_arity + if cfunc.argc >= 0 && argc != cfunc.argc + asm.incr_counter(:send_arity) + return CantCompile + end + if cfunc.argc == -2 + asm.incr_counter(:send_cfunc_ruby_array_varg) + return CantCompile + end + + # Delegate to codegen for C methods if we have it. + if flags & C.VM_CALL_KWARG == 0 && flags & C.VM_CALL_OPT_SEND == 0 + known_cfunc_codegen = lookup_cfunc_codegen(cme.def) + if known_cfunc_codegen&.call(jit, ctx, asm, argc, known_recv_class) + # cfunc codegen generated code. Terminate the block so + # there isn't multiple calls in the same block. + jump_to_next_insn(jit, ctx, asm) + return EndBlock + end + end + + # We will not have side exits from here. Adjust the stack. + if flags & C.VM_CALL_OPT_SEND != 0 + jit_call_opt_send_shift_stack(ctx, asm, argc, send_shift:) + end + + # Check interrupts before SP motion to safely side-exit with the original SP. + jit_check_ints(jit, ctx, asm) + + # Save caller SP and PC before pushing a callee frame for backtrace and side exits + asm.comment('save SP to caller CFP') + sp_index = -(1 + argc + (flags & C.VM_CALL_ARGS_BLOCKARG != 0 ? 1 : 0)) # Pop receiver and arguments for side exits. blockarg is not popped yet + asm.lea(SP, ctx.sp_opnd(C.VALUE.size * sp_index)) + asm.mov([CFP, C.rb_control_frame_t.offsetof(:sp)], SP) + ctx.sp_offset = -sp_index + jit_save_pc(jit, asm, comment: 'save PC to caller CFP') + + # Push a callee frame. SP register and ctx are not modified inside this. + jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler) + + asm.comment('call C function') + case cfunc.argc + in (0..) # Non-variadic method + # Push receiver and args + (1 + argc).times do |i| + asm.mov(C_ARGS[i], ctx.stack_opnd(argc - i)) # TODO: +1 for VM_CALL_ARGS_BLOCKARG + end + in -1 # Variadic method: rb_f_puts(int argc, VALUE *argv, VALUE recv) + asm.mov(C_ARGS[0], argc) + asm.lea(C_ARGS[1], ctx.stack_opnd(argc - 1)) # argv + asm.mov(C_ARGS[2], ctx.stack_opnd(argc)) # recv + end + asm.mov(:rax, cfunc.func) + asm.call(:rax) # TODO: use rel32 if close enough + ctx.stack_pop(1 + argc) + + Invariants.record_global_inval_patch(asm, @full_cfunc_return) + + asm.comment('push the return value') + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + + asm.comment('pop the stack frame') + asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], CFP) + + # Let guard chains share the same successor (ctx.sp_offset == 1) + assert_equal(1, ctx.sp_offset) + jump_to_next_insn(jit, ctx, asm) + EndBlock + end + + # vm_call_ivar (+ part of vm_call_method_each_type) + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_ivar(jit, ctx, asm, cme, flags, argc, comptime_recv, recv_opnd, send_shift:) + if flags & C.VM_CALL_ARGS_SPLAT != 0 + asm.incr_counter(:send_ivar_splat) + return CantCompile + end + + if argc != 0 + asm.incr_counter(:send_arity) + return CantCompile + end + + # We don't support jit_call_opt_send_shift_stack for this yet. + if flags & C.VM_CALL_OPT_SEND != 0 + asm.incr_counter(:send_ivar_opt_send) + return CantCompile + end + + ivar_id = cme.def.body.attr.id + + # Not handling block_handler + if flags & C.VM_CALL_ARGS_BLOCKARG != 0 + asm.incr_counter(:send_ivar_blockarg) + return CantCompile + end + + jit_getivar(jit, ctx, asm, comptime_recv, ivar_id, recv_opnd) + end + + # vm_call_bmethod + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_bmethod(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + proc_addr = cme.def.body.bmethod.proc + + proc_t = C.rb_yjit_get_proc_ptr(proc_addr) + proc_block = proc_t.block + + if proc_block.type != C.block_type_iseq + asm.incr_counter(:send_bmethod_not_iseq) + return CantCompile + end + + capture = proc_block.as.captured + iseq = capture.code.iseq + + # TODO: implement this + # Optimize for single ractor mode and avoid runtime check for + # "defined with an un-shareable Proc in a different Ractor" + # if !assume_single_ractor_mode(jit, ocb) + # return CantCompile; + # end + + # Passing a block to a block needs logic different from passing + # a block to a method and sometimes requires allocation. Bail for now. + if block_handler != C.VM_BLOCK_HANDLER_NONE + asm.incr_counter(:send_bmethod_blockarg) + return CantCompile + end + + frame_type = C.VM_FRAME_MAGIC_BLOCK | C.VM_FRAME_FLAG_BMETHOD | C.VM_FRAME_FLAG_LAMBDA + prev_ep = capture.ep + jit_call_iseq_setup(jit, ctx, asm, cme, flags, argc, iseq, block_handler, send_shift:, frame_type:, prev_ep:) + end + + # vm_call_alias + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_alias(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + cme = C.rb_aliased_callable_method_entry(cme) + jit_call_method_each_type(jit, ctx, asm, argc, flags, cme, comptime_recv, recv_opnd, block_handler, known_recv_class, send_shift:) + end + + # vm_call_optimized + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_optimized(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + if flags & C.VM_CALL_ARGS_BLOCKARG != 0 + # Not working yet + asm.incr_counter(:send_optimized_blockarg) + return CantCompile + end + + case cme.def.body.optimized.type + when C.OPTIMIZED_METHOD_TYPE_SEND + jit_call_opt_send(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + when C.OPTIMIZED_METHOD_TYPE_CALL + jit_call_opt_call(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + when C.OPTIMIZED_METHOD_TYPE_BLOCK_CALL + asm.incr_counter(:send_optimized_block_call) + return CantCompile + when C.OPTIMIZED_METHOD_TYPE_STRUCT_AREF + jit_call_opt_struct_aref(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + when C.OPTIMIZED_METHOD_TYPE_STRUCT_ASET + asm.incr_counter(:send_optimized_struct_aset) + return CantCompile + else + asm.incr_counter(:send_optimized_unknown_type) + return CantCompile + end + end + + # vm_call_opt_send + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_opt_send(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + if jit_caller_setup_arg(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + + if argc == 0 + asm.incr_counter(:send_optimized_send_no_args) + return CantCompile + end + + argc -= 1 + # We aren't handling `send(:send, ...)` yet. This might work, but not tested yet. + if send_shift > 0 + asm.incr_counter(:send_optimized_send_send) + return CantCompile + end + # Ideally, we want to shift the stack here, but it's not safe until you reach the point + # where you never exit. `send_shift` signals to lazily shift the stack by this amount. + send_shift += 1 + + kw_splat = flags & C.VM_CALL_KW_SPLAT != 0 + jit_call_symbol(jit, ctx, asm, cme, C.VM_CALL_FCALL, argc, kw_splat, block_handler, known_recv_class, send_shift:) + end + + # vm_call_opt_call + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_opt_call(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + if block_handler != C.VM_BLOCK_HANDLER_NONE + asm.incr_counter(:send_optimized_call_block) + return CantCompile + end + + if flags & C.VM_CALL_KWARG != 0 + asm.incr_counter(:send_optimized_call_kwarg) + return CantCompile + end + + if flags & C.VM_CALL_ARGS_SPLAT != 0 + asm.incr_counter(:send_optimized_call_splat) + return CantCompile + end + + # TODO: implement this + # Optimize for single ractor mode and avoid runtime check for + # "defined with an un-shareable Proc in a different Ractor" + # if !assume_single_ractor_mode(jit, ocb) + # return CantCompile + # end + + # If this is a .send call we need to adjust the stack + if flags & C.VM_CALL_OPT_SEND != 0 + jit_call_opt_send_shift_stack(ctx, asm, argc, send_shift:) + end + + # About to reset the SP, need to load this here + recv_idx = argc # blockarg is not supported. send_shift is already handled. + asm.mov(:rcx, ctx.stack_opnd(recv_idx)) # recv + + # Save the PC and SP because the callee can make Ruby calls + jit_prepare_routine_call(jit, ctx, asm) # NOTE: clobbers rax + + asm.lea(:rax, ctx.sp_opnd(0)) # sp + + kw_splat = flags & C.VM_CALL_KW_SPLAT + + asm.mov(C_ARGS[0], :rcx) + asm.mov(C_ARGS[1], EC) + asm.mov(C_ARGS[2], argc) + asm.lea(C_ARGS[3], [:rax, -argc * C.VALUE.size]) # stack_argument_pointer. NOTE: C_ARGS[3] is rcx + asm.mov(C_ARGS[4], kw_splat) + asm.mov(C_ARGS[5], C.VM_BLOCK_HANDLER_NONE) + asm.call(C.rb_optimized_call) + + ctx.stack_pop(argc + 1) + + stack_ret = ctx.stack_push + asm.mov(stack_ret, C_RET) + return KeepCompiling + end + + # vm_call_opt_struct_aref + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_opt_struct_aref(jit, ctx, asm, cme, flags, argc, block_handler, known_recv_class, send_shift:) + if argc != 0 + asm.incr_counter(:send_optimized_struct_aref_error) + return CantCompile + end + + off = cme.def.body.optimized.index + + recv_idx = argc # blockarg is not supported + recv_idx += send_shift + comptime_recv = jit.peek_at_stack(recv_idx) + + # This is a .send call and we need to adjust the stack + if flags & C.VM_CALL_OPT_SEND != 0 + jit_call_opt_send_shift_stack(ctx, asm, argc, send_shift:) + end + + # All structs from the same Struct class should have the same + # length. So if our comptime_recv is embedded all runtime + # structs of the same class should be as well, and the same is + # true of the converse. + embedded = C.FL_TEST_RAW(comptime_recv, C.RSTRUCT_EMBED_LEN_MASK) + + asm.comment('struct aref') + asm.mov(:rax, ctx.stack_pop(1)) # recv + + if embedded + asm.mov(:rax, [:rax, C.RStruct.offsetof(:as, :ary) + (C.VALUE.size * off)]) + else + asm.mov(:rax, [:rax, C.RStruct.offsetof(:as, :heap, :ptr)]) + asm.mov(:rax, [:rax, C.VALUE.size * off]) + end + + ret = ctx.stack_push + asm.mov(ret, :rax) + + jump_to_next_insn(jit, ctx, asm) + EndBlock + end + + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_opt_send_shift_stack(ctx, asm, argc, send_shift:) + # We don't support `send(:send, ...)` for now. + assert_equal(1, send_shift) + + asm.comment('shift stack') + (0...argc).reverse_each do |i| + opnd = ctx.stack_opnd(i) + opnd2 = ctx.stack_opnd(i + 1) + asm.mov(:rax, opnd) + asm.mov(opnd2, :rax) + end + + ctx.stack_pop(1) + end + + # vm_call_symbol + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_call_symbol(jit, ctx, asm, cme, flags, argc, kw_splat, block_handler, known_recv_class, send_shift:) + flags |= C.VM_CALL_OPT_SEND | (kw_splat ? C.VM_CALL_KW_SPLAT : 0) + + comptime_symbol = jit.peek_at_stack(argc) + if comptime_symbol.class != String && !static_symbol?(comptime_symbol) + asm.incr_counter(:send_optimized_send_not_sym_or_str) + return CantCompile + end + + mid = C.get_symbol_id(comptime_symbol) + if mid == 0 + asm.incr_counter(:send_optimized_send_null_mid) + return CantCompile + end + + asm.comment("Guard #{comptime_symbol.inspect} is on stack") + class_changed_exit = counted_exit(side_exit(jit, ctx), :send_optimized_send_mid_class_changed) + jit_guard_known_klass(jit, ctx, asm, C.rb_class_of(comptime_symbol), ctx.stack_opnd(argc), comptime_symbol, class_changed_exit) + asm.mov(C_ARGS[0], ctx.stack_opnd(argc)) + asm.call(C.rb_get_symbol_id) + asm.cmp(C_RET, mid) + id_changed_exit = counted_exit(side_exit(jit, ctx), :send_optimized_send_mid_id_changed) + jit_chain_guard(:jne, jit, ctx, asm, id_changed_exit) + + # rb_callable_method_entry_with_refinements + cme, _ = jit_search_method(jit, ctx, asm, mid, argc, flags, send_shift:) + if cme == CantCompile + return CantCompile + end + + if flags & C.VM_CALL_FCALL != 0 + return jit_call_method(jit, ctx, asm, mid, argc, flags, cme, block_handler, known_recv_class, send_shift:) + end + + raise NotImplementedError # unreachable for now + end + + # vm_push_frame + # + # Frame structure: + # | args | locals | cme/cref | block_handler/prev EP | frame type (EP here) | stack bottom (SP here) + # + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_push_frame(jit, ctx, asm, cme, flags, argc, frame_type, block_handler, iseq: nil, local_size: 0, stack_max: 0, prev_ep: nil) + # CHECK_VM_STACK_OVERFLOW0: next_cfp <= sp + (local_size + stack_max) + asm.comment('stack overflow check') + asm.lea(:rax, ctx.sp_opnd(C.rb_control_frame_t.size + C.VALUE.size * (local_size + stack_max))) + asm.cmp(CFP, :rax) + asm.jbe(counted_exit(side_exit(jit, ctx), :send_stackoverflow)) + + # Pop blockarg after all side exits + if flags & C.VM_CALL_ARGS_BLOCKARG != 0 + ctx.stack_pop(1) + end + + if iseq + # This was not handled in jit_callee_setup_arg + opts_filled = argc - iseq.body.param.lead_num # TODO: kwarg + opts_missing = iseq.body.param.opt_num - opts_filled + local_size += opts_missing + end + local_size.times do |i| + asm.comment('set local variables') if i == 0 + local_index = ctx.sp_offset + i + asm.mov([SP, C.VALUE.size * local_index], Qnil) + end + + asm.comment('set up EP with managing data') + ep_offset = ctx.sp_offset + local_size + 2 + # ep[-2]: cref_or_me + asm.mov(:rax, cme.to_i) + asm.mov([SP, C.VALUE.size * (ep_offset - 2)], :rax) + # ep[-1]: block handler or prev env ptr + if prev_ep + asm.mov(:rax, prev_ep.to_i | 1) # tagged prev ep + asm.mov([SP, C.VALUE.size * (ep_offset - 1)], :rax) + elsif block_handler == C.VM_BLOCK_HANDLER_NONE + asm.mov([SP, C.VALUE.size * (ep_offset - 1)], C.VM_BLOCK_HANDLER_NONE) + elsif block_handler == C.rb_block_param_proxy + # vm_caller_setup_arg_block: + # VALUE handler = VM_CF_BLOCK_HANDLER(reg_cfp); + # reg_cfp->block_code = (const void *) handler; + jit_get_lep(jit, asm, reg: :rax) + asm.mov(:rax, [:rax, C.VALUE.size * C.VM_ENV_DATA_INDEX_SPECVAL]) # handler + asm.mov([CFP, C.rb_control_frame_t.offsetof(:block_code)], :rax) + + asm.mov(:rax, C.rb_block_param_proxy) + asm.mov([SP, C.VALUE.size * (ep_offset - 1)], :rax) + else # assume blockiseq + asm.mov(:rax, block_handler) + asm.mov([CFP, C.rb_control_frame_t.offsetof(:block_code)], :rax) + asm.lea(:rax, [CFP, C.rb_control_frame_t.offsetof(:self)]) # VM_CFP_TO_CAPTURED_BLOCK + asm.or(:rax, 1) # VM_BH_FROM_ISEQ_BLOCK + asm.mov([SP, C.VALUE.size * (ep_offset - 1)], :rax) + end + # ep[-0]: ENV_FLAGS + asm.mov([SP, C.VALUE.size * (ep_offset - 0)], frame_type) + + asm.comment('set up new frame') + cfp_offset = -C.rb_control_frame_t.size # callee CFP + # For ISEQ, JIT code will set it as needed. However, C func needs 0 there for svar frame detection. + if iseq.nil? + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:pc)], 0) + end + asm.mov(:rax, iseq.to_i) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:iseq)], :rax) + self_index = ctx.sp_offset - (1 + argc) # blockarg has been popped + asm.mov(:rax, [SP, C.VALUE.size * self_index]) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:self)], :rax) + asm.lea(:rax, [SP, C.VALUE.size * ep_offset]) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:ep)], :rax) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:block_code)], 0) + # Update SP register only for ISEQ calls. SP-relative operations should be done above this. + sp_reg = iseq ? SP : :rax + asm.lea(sp_reg, [SP, C.VALUE.size * (ctx.sp_offset + local_size + 3)]) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:sp)], sp_reg) + asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:__bp__)], sp_reg) # TODO: get rid of this!! + + # cfp->jit_return is used only for ISEQs + if iseq + # Stub cfp->jit_return + return_ctx = ctx.dup + return_ctx.stack_size -= argc # Pop args. blockarg has been popped + return_ctx.sp_offset = 1 # SP is in the position after popping a receiver and arguments + return_ctx.chain_depth = 0 + branch_stub = BranchStub.new( + iseq: jit.iseq, + shape: Default, + target0: BranchTarget.new(ctx: return_ctx, pc: jit.pc + jit.insn.len * C.VALUE.size), + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(return_ctx, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.compile = proc do |branch_asm| + branch_asm.comment('set jit_return to callee CFP') + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.mov(:rax, branch_stub.target0.address) + branch_asm.mov([CFP, cfp_offset + C.rb_control_frame_t.offsetof(:jit_return)], :rax) + end + end + end + branch_stub.compile.call(asm) + end + + asm.comment('switch to callee CFP') + # Update CFP register only for ISEQ calls + cfp_reg = iseq ? CFP : :rax + asm.lea(cfp_reg, [CFP, cfp_offset]) + asm.mov([EC, C.rb_execution_context_t.offsetof(:cfp)], cfp_reg) + end + + # vm_callee_setup_arg: Set up args and return opt_pc (or CantCompile) + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_callee_setup_arg(jit, ctx, asm, flags, argc, iseq) + if flags & C.VM_CALL_KW_SPLAT == 0 + if C.rb_simple_iseq_p(iseq) + if jit_caller_setup_arg(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + if jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + + if argc != iseq.body.param.lead_num + # argument_arity_error + return CantCompile + end + + return 0 + elsif C.rb_iseq_only_optparam_p(iseq) + if jit_caller_setup_arg(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + if jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) == CantCompile + return CantCompile + end + + lead_num = iseq.body.param.lead_num + opt_num = iseq.body.param.opt_num + opt = argc - lead_num + + if opt < 0 || opt > opt_num + asm.incr_counter(:send_arity) + return CantCompile + end + + # Qnil push is handled in jit_push_frame + + return iseq.body.param.opt_table[opt] + elsif C.rb_iseq_only_kwparam_p(iseq) && (flags & C.VM_CALL_ARGS_SPLAT) == 0 + asm.incr_counter(:send_iseq_kwparam) + return CantCompile + end + end + + # We don't support setup_parameters_complex + asm.incr_counter(:send_iseq_kw_splat) + return CantCompile + end + + # CALLER_SETUP_ARG: Return CantCompile if not supported + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_caller_setup_arg(jit, ctx, asm, flags) + if flags & C.VM_CALL_ARGS_SPLAT != 0 + # We don't support vm_caller_setup_arg_splat + asm.incr_counter(:send_args_splat) + return CantCompile + end + if flags & (C.VM_CALL_KWARG | C.VM_CALL_KW_SPLAT) != 0 + # We don't support keyword args either + asm.incr_counter(:send_kwarg) + return CantCompile + end + end + + # CALLER_REMOVE_EMPTY_KW_SPLAT: Return CantCompile if not supported + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def jit_caller_remove_empty_kw_splat(jit, ctx, asm, flags) + if (flags & C.VM_CALL_KW_SPLAT) > 0 + # We don't support removing the last Hash argument + asm.incr_counter(:send_kw_splat) + return CantCompile + end + end + + # Generate RARRAY_LEN. For array_opnd, use Opnd::Reg to reduce memory access, + # and use Opnd::Mem to save registers. + def jit_array_len(asm, array_reg, len_reg) + asm.comment('get array length for embedded or heap') + + # Pull out the embed flag to check if it's an embedded array. + asm.mov(len_reg, [array_reg, C.RBasic.offsetof(:flags)]) + + # Get the length of the array + asm.and(len_reg, C.RARRAY_EMBED_LEN_MASK) + asm.sar(len_reg, C.RARRAY_EMBED_LEN_SHIFT) + + # Conditionally move the length of the heap array + asm.test([array_reg, C.RBasic.offsetof(:flags)], C.RARRAY_EMBED_FLAG) + + # Select the array length value + asm.cmovz(len_reg, [array_reg, C.RArray.offsetof(:as, :heap, :len)]) + end + + def assert_equal(left, right) + if left != right + raise "'#{left.inspect}' was not '#{right.inspect}'" + end + end + + def fixnum?(obj) + (C.to_value(obj) & C.RUBY_FIXNUM_FLAG) == C.RUBY_FIXNUM_FLAG + end + + def flonum?(obj) + (C.to_value(obj) & C.RUBY_FLONUM_MASK) == C.RUBY_FLONUM_FLAG + end + + def static_symbol?(obj) + (C.to_value(obj) & 0xff) == C.RUBY_SYMBOL_FLAG + end + + def shape_too_complex?(obj) + C.rb_shape_get_shape_id(obj) == C.OBJ_TOO_COMPLEX_SHAPE_ID + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + # @param asm [RubyVM::MJIT::Assembler] + def defer_compilation(jit, ctx, asm) + # Make a stub to compile the current insn + stub_next_block(jit.iseq, jit.pc, ctx, asm, comment: 'defer_compilation') + end + + def stub_next_block(iseq, pc, ctx, asm, comment: 'stub_next_block') + branch_stub = BranchStub.new( + iseq:, + shape: Default, + target0: BranchTarget.new(ctx:, pc:), + ) + branch_stub.target0.address = Assembler.new.then do |ocb_asm| + @exit_compiler.compile_branch_stub(ctx, ocb_asm, branch_stub, true) + @ocb.write(ocb_asm) + end + branch_stub.compile = proc do |branch_asm| + branch_asm.comment(comment) + branch_asm.stub(branch_stub) do + case branch_stub.shape + in Default + branch_asm.jmp(branch_stub.target0.address) + in Next0 + # Just write the block without a jump + end + end + end + branch_stub.compile.call(asm) + end + + # @param jit [RubyVM::MJIT::JITState] + # @param ctx [RubyVM::MJIT::Context] + def side_exit(jit, ctx) + if side_exit = jit.side_exits[jit.pc] + return side_exit + end + asm = Assembler.new + @exit_compiler.compile_side_exit(jit.pc, ctx, asm) + jit.side_exits[jit.pc] = @ocb.write(asm) + end + + def counted_exit(side_exit, name) + asm = Assembler.new + asm.incr_counter(name) + asm.jmp(side_exit) + @ocb.write(asm) + end + + def def_iseq_ptr(cme_def) + C.rb_iseq_check(cme_def.body.iseq.iseqptr) + end + + def to_value(obj) + GC_REFS << obj + C.to_value(obj) + end + end +end diff --git a/lib/ruby_vm/rjit/invariants.rb b/lib/ruby_vm/rjit/invariants.rb new file mode 100644 index 0000000000..127e446ed9 --- /dev/null +++ b/lib/ruby_vm/rjit/invariants.rb @@ -0,0 +1,144 @@ +require 'set' + +module RubyVM::MJIT + class Invariants + class << self + # Called by RubyVM::MJIT::Compiler to lazily initialize this + # @param cb [CodeBlock] + # @param ocb [CodeBlock] + # @param compiler [RubyVM::MJIT::Compiler] + # @param exit_compiler [RubyVM::MJIT::ExitCompiler] + def initialize(cb, ocb, compiler, exit_compiler) + @cb = cb + @ocb = ocb + @compiler = compiler + @exit_compiler = exit_compiler + @bop_blocks = Set.new # TODO: actually invalidate this + @cme_blocks = Hash.new { |h, k| h[k] = Set.new } + @const_blocks = Hash.new { |h, k| h[k] = Set.new } + @patches = {} + + # freeze # workaround a binding.irb issue. TODO: resurrect this + end + + # @param jit [RubyVM::MJIT::JITState] + # @param klass [Integer] + # @param op [Integer] + def assume_bop_not_redefined(jit, klass, op) + return false unless C.BASIC_OP_UNREDEFINED_P(klass, op) + + ensure_block_entry_exit(jit, cause: 'assume_bop_not_redefined') + @bop_blocks << jit.block + true + end + + # @param jit [RubyVM::MJIT::JITState] + def assume_method_lookup_stable(jit, cme) + ensure_block_entry_exit(jit, cause: 'assume_method_lookup_stable') + @cme_blocks[cme.to_i] << jit.block + end + + def assume_stable_constant_names(jit, idlist) + (0..).each do |i| + break if (id = idlist[i]) == 0 + @const_blocks[id] << jit.block + end + end + + # @param asm [RubyVM::MJIT::Assembler] + def record_global_inval_patch(asm, target) + asm.pos_marker do |address| + if @patches.key?(address) + raise 'multiple patches in the same address' + end + @patches[address] = target + end + end + + def on_cme_invalidate(cme) + @cme_blocks.fetch(cme.to_i, []).each do |block| + @cb.with_write_addr(block.start_addr) do + asm = Assembler.new + asm.comment('on_cme_invalidate') + asm.jmp(block.entry_exit) + @cb.write(asm) + end + # TODO: re-generate branches that refer to this block + end + @cme_blocks.delete(cme.to_i) + end + + def on_constant_ic_update(iseq, ic, insn_idx) + # TODO: check multi ractor as well + if ic.entry.ic_cref + # No need to recompile the slowpath + return + end + + pc = iseq.body.iseq_encoded + insn_idx + insn_name = Compiler.decode_insn(pc.*).name + if insn_name != :opt_getconstant_path && insn_name != :trace_opt_getconstant_path + raise 'insn_idx was not at opt_getconstant_path' + end + if ic.to_i != pc[1] + raise 'insn_idx + 1 was not at the updated IC' + end + @compiler.invalidate_blocks(iseq, pc.to_i) + end + + def on_constant_state_changed(id) + @const_blocks.fetch(id, []).each do |block| + @compiler.invalidate_block(block) + end + end + + def on_tracing_invalidate_all + invalidate_all + end + + def on_update_references + # Give up. In order to support GC.compact, you'd have to update ISEQ + # addresses in BranchStub, etc. Ideally, we'd need to update moved + # pointers in JITed code here, but we just invalidate all for now. + invalidate_all + end + + # @param jit [RubyVM::MJIT::JITState] + # @param block [RubyVM::MJIT::Block] + def ensure_block_entry_exit(jit, cause:) + block = jit.block + if block.entry_exit.nil? + block.entry_exit = Assembler.new.then do |asm| + @exit_compiler.compile_entry_exit(block.pc, block.ctx, asm, cause:) + @ocb.write(asm) + end + end + end + + private + + def invalidate_all + # On-Stack Replacement + @patches.each do |address, target| + # TODO: assert patches don't overlap each other + @cb.with_write_addr(address) do + asm = Assembler.new + asm.comment('on_tracing_invalidate_all') + asm.jmp(target) + @cb.write(asm) + end + end + @patches.clear + + C.mjit_for_each_iseq do |iseq| + # Avoid entering past code + iseq.body.jit_func = 0 + # Avoid reusing past code + iseq.body.mjit_blocks.clear if iseq.body.mjit_blocks + # Compile this again if not converted to trace_* insns + iseq.body.total_calls = 0 + end + end + end + end +end diff --git a/lib/ruby_vm/rjit/jit_state.rb b/lib/ruby_vm/rjit/jit_state.rb new file mode 100644 index 0000000000..decafee579 --- /dev/null +++ b/lib/ruby_vm/rjit/jit_state.rb @@ -0,0 +1,57 @@ +module RubyVM::MJIT + class JITState < Struct.new( + :iseq, # @param `RubyVM::MJIT::CPointer::Struct_rb_iseq_t` + :pc, # @param [Integer] The JIT target PC + :cfp, # @param `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t` The JIT source CFP (before MJIT is called) + :block, # @param [RubyVM::MJIT::Block] + :side_exits, # @param [Hash{ Integer => Integer }] { PC => address } + :record_boundary_patch_point, # @param [TrueClass,FalseClass] + ) + def initialize(side_exits: {}, record_boundary_patch_point: false, **) = super + + def insn + Compiler.decode_insn(C.VALUE.new(pc).*) + end + + def operand(index, signed: false, ruby: false) + addr = pc + (index + 1) * Fiddle::SIZEOF_VOIDP + value = Fiddle::Pointer.new(addr)[0, Fiddle::SIZEOF_VOIDP].unpack(signed ? 'q' : 'Q')[0] + if ruby + value = C.to_ruby(value) + end + value + end + + def at_current_insn? + pc == cfp.pc.to_i + end + + def peek_at_stack(depth_from_top) + raise 'not at current insn' unless at_current_insn? + offset = -(1 + depth_from_top) + # rb_mjit_branch_stub_hit updates SP, so you don't need to worry about sp_offset + value = (cfp.sp + offset).* + C.to_ruby(value) + end + + def peek_at_self + C.to_ruby(cfp.self) + end + + def peek_at_block_handler(level) + ep = ep_at_level(cfp, level:) + ep[C.VM_ENV_DATA_INDEX_SPECVAL] + end + + private + + def ep_at_level(cfp, level:) + ep = cfp.ep + level.times do + # VM_ENV_PREV_EP + ep = C.VALUE.new(ep[C.VM_ENV_DATA_INDEX_SPECVAL] & ~0x03) + end + ep + end + end +end diff --git a/lib/ruby_vm/rjit/stats.rb b/lib/ruby_vm/rjit/stats.rb new file mode 100644 index 0000000000..b605f3c1ad --- /dev/null +++ b/lib/ruby_vm/rjit/stats.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true +module RubyVM::MJIT + def self.runtime_stats + stats = {} + + # Insn exits + INSNS.each_value do |insn| + exits = C.mjit_insn_exits[insn.bin] + if exits > 0 + stats[:"exit_#{insn.name}"] = exits + end + end + + # Runtime stats + C.rb_mjit_runtime_counters.members.each do |member| + stats[member] = C.rb_mjit_counters.public_send(member) + end + + # Other stats are calculated here + stats[:side_exit_count] = stats.select { |name, _count| name.start_with?('exit_') }.sum(&:last) + if stats[:vm_insns_count] > 0 + retired_in_mjit = stats[:mjit_insns_count] - stats[:side_exit_count] + stats[:total_insns_count] = retired_in_mjit + stats[:vm_insns_count] + stats[:ratio_in_mjit] = 100.0 * retired_in_mjit / stats[:total_insns_count] + end + + stats + end + + class << self + private + + def print_stats + stats = runtime_stats + $stderr.puts("***MJIT: Printing MJIT statistics on exit***") + + print_counters(stats, prefix: 'send_', prompt: 'method call exit reasons') + print_counters(stats, prefix: 'invokesuper_', prompt: 'invokesuper exit reasons') + print_counters(stats, prefix: 'getblockpp_', prompt: 'getblockparamproxy exit reasons') + print_counters(stats, prefix: 'getivar_', prompt: 'getinstancevariable exit reasons') + print_counters(stats, prefix: 'setivar_', prompt: 'setinstancevariable exit reasons') + print_counters(stats, prefix: 'optaref_', prompt: 'opt_aref exit reasons') + print_counters(stats, prefix: 'optgetconst_', prompt: 'opt_getconstant_path exit reasons') + print_counters(stats, prefix: 'expandarray_', prompt: 'expandarray exit reasons') + + $stderr.puts "compiled_block_count: #{format_number(13, stats[:compiled_block_count])}" + $stderr.puts "side_exit_count: #{format_number(13, stats[:side_exit_count])}" + $stderr.puts "total_insns_count: #{format_number(13, stats[:total_insns_count])}" if stats.key?(:total_insns_count) + $stderr.puts "vm_insns_count: #{format_number(13, stats[:vm_insns_count])}" if stats.key?(:vm_insns_count) + $stderr.puts "mjit_insns_count: #{format_number(13, stats[:mjit_insns_count])}" + $stderr.puts "ratio_in_mjit: #{format('%12.1f', stats[:ratio_in_mjit])}%" if stats.key?(:ratio_in_mjit) + + print_exit_counts(stats) + end + + def print_counters(stats, prefix:, prompt:) + $stderr.puts("#{prompt}: ") + counters = stats.filter { |key, _| key.start_with?(prefix) } + counters.filter! { |_, value| value != 0 } + counters.transform_keys! { |key| key.to_s.delete_prefix(prefix) } + + if counters.empty? + $stderr.puts(" (all relevant counters are zero)") + return + end + + counters = counters.to_a + counters.sort_by! { |(_, counter_value)| counter_value } + longest_name_length = counters.max_by { |(name, _)| name.length }.first.length + total = counters.sum { |(_, counter_value)| counter_value } + + counters.reverse_each do |(name, value)| + percentage = value.fdiv(total) * 100 + $stderr.printf(" %*s %s (%4.1f%%)\n", longest_name_length, name, format_number(10, value), percentage) + end + end + + def print_exit_counts(stats, how_many: 20, padding: 2) + exits = stats.filter_map { |name, count| [name.to_s.delete_prefix('exit_'), count] if name.start_with?('exit_') }.to_h + return if exits.empty? + + top_exits = exits.sort_by { |_name, count| -count }.first(how_many).to_h + total_exits = exits.values.sum + $stderr.puts "Top-#{top_exits.size} most frequent exit ops (#{format("%.1f", 100.0 * top_exits.values.sum / total_exits)}% of exits):" + + name_width = top_exits.map { |name, _count| name.length }.max + padding + count_width = top_exits.map { |_name, count| format_number(10, count).length }.max + padding + top_exits.each do |name, count| + ratio = 100.0 * count / total_exits + $stderr.puts "#{format("%#{name_width}s", name)}: #{format_number(count_width, count)} (#{format('%4.1f', ratio)}%)" + end + end + + # Format large numbers with comma separators for readability + def format_number(pad, number) + integer, decimal = number.to_s.split('.') + d_groups = integer.chars.reverse.each_slice(3) + with_commas = d_groups.map(&:join).join(',').reverse + [with_commas, decimal].compact.join('.').rjust(pad, ' ') + end + end +end |