summaryrefslogtreecommitdiff
path: root/lib/ruby_vm/mjit/compiler.rb
blob: 3fc56dac5ecc0371cc6a4bdd71fcf1d558d72b33 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
require 'ruby_vm/mjit/assembler'
require 'ruby_vm/mjit/block'
require 'ruby_vm/mjit/block_stub'
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

  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(@ocb, @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

      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: JITState.new(iseq:, cfp:))
      iseq.body.jit_func = @cb.write(asm)
    rescue Exception => e
      $stderr.puts e.full_message # TODO: check verbose
    end

    # Continue compilation from a block stub.
    # @param block_stub [RubyVM::MJIT::BlockStub]
    # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t`
    # @return [Integer] The starting address of the compiled block stub
    def block_stub_hit(block_stub, cfp)
      # Update cfp->pc for `jit.at_current_insn?`
      cfp.pc = block_stub.pc

      # Prepare the jump target
      new_asm = Assembler.new.tap do |asm|
        jit = JITState.new(iseq: block_stub.iseq, cfp:)
        compile_block(asm, jit:, pc: block_stub.pc, ctx: block_stub.ctx)
      end

      # Rewrite the block stub
      if @cb.write_addr == block_stub.end_addr
        # If the block stub's jump is the last code, overwrite the jump with the new code.
        @cb.set_write_addr(block_stub.start_addr)
        @cb.write(new_asm)
      else
        # If the block stub's jump is old code, change the jump target to the new code.
        new_addr = @cb.write(new_asm)
        @cb.with_write_addr(block_stub.start_addr) do
          asm = Assembler.new
          block_stub.change_block.call(asm, new_addr)
          @cb.write(asm)
        end
        new_addr
      end
    end

    # Compile a branch stub.
    # @param branch_stub [RubyVM::MJIT::BranchStub]
    # @param cfp `RubyVM::MJIT::CPointer::Struct_rb_control_frame_t`
    # @param branch_target_p [TrueClass,FalseClass]
    # @return [Integer] The starting address of the compiled branch stub
    def branch_stub_hit(branch_stub, cfp, branch_target_p)
      # Update cfp->pc for `jit.at_current_insn?`
      pc = branch_target_p ? branch_stub.branch_target_pc : branch_stub.fallthrough_pc
      cfp.pc = pc

      # Prepare the jump target
      new_asm = Assembler.new.tap do |asm|
        jit = JITState.new(iseq: branch_stub.iseq, cfp:)
        compile_block(asm, jit:, pc:, ctx: branch_stub.ctx.dup)
      end

      # Rewrite the branch stub
      if @cb.write_addr == branch_stub.end_addr
        # If the branch stub's jump is the last code, overwrite the jump with the new code.
        @cb.set_write_addr(branch_stub.start_addr)
        Assembler.new.tap do |branch_asm|
          if branch_target_p
            branch_stub.branch_target_next.call(branch_asm)
          else
            branch_stub.fallthrough_next.call(branch_asm)
          end
          @cb.write(branch_asm)
        end

        # Compile a fallthrough over the jump
        if branch_target_p
          branch_stub.branch_target_addr = @cb.write(new_asm)
        else
          branch_stub.fallthrough_addr = @cb.write(new_asm)
        end
      else
        # Otherwise, just prepare the new code somewhere
        if branch_target_p
          unless @cb.include?(branch_stub.branch_target_addr)
            branch_stub.branch_target_addr = @cb.write(new_asm)
          end
        else
          unless @cb.include?(branch_stub.fallthrough_addr)
            branch_stub.fallthrough_addr = @cb.write(new_asm)
          end
        end

        # Update jump destinations
        branch_asm = Assembler.new
        if branch_stub.end_addr == branch_stub.branch_target_addr # branch_target_next has been used
          branch_stub.branch_target_next.call(branch_asm)
        elsif branch_stub.end_addr == branch_stub.fallthrough_addr # fallthrough_next has been used
          branch_stub.fallthrough_next.call(branch_asm)
        else
          branch_stub.neither_next.call(branch_asm)
        end
        @cb.with_write_addr(branch_stub.start_addr) do
          @cb.write(branch_asm)
        end
      end

      if branch_target_p
        branch_stub.branch_target_addr
      else
        branch_stub.fallthrough_addr
      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
      jit.block = Block.new(pc:)
      asm.block(jit.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

        case status = @insn_compiler.compile(jit, ctx, asm, insn)
        when KeepCompiling
          index += insn.len
        when EndBlock
          # TODO: pad nops if entry exit exists
          break
        when CantCompile
          @exit_compiler.compile_side_exit(jit, ctx, asm)
          break
        else
          raise "compiling #{insn.name} returned unexpected status: #{status.inspect}"
        end
      end
    end
  end
end