module Rubinius class CompiledCode < Executable def self.allocate Rubinius.primitive :compiledcode_allocate raise PrimitiveFailure, "CompiledCode.allocate primitive failed" end def dup Rubinius.primitive :compiledcode_dup raise PrimitiveFailure, "CompiledCode#dup primitive failed" end def call_sites Rubinius.primitive :compiledcode_call_sites raise PrimitiveFailure, "CompiledCode#call_sites primitive failed" end def code_id @code_id || stamp_id end def constant_caches Rubinius.primitive :compiledcode_constant_caches raise PrimitiveFailure, "CompiledCode#constant_caches primitive failed" end # Return the CompiledCode for caller of the method that called # .of_sender. # # For example: # # def g # f_code = Rubinius::CompiledCode.of_sender # end # # def f # g # end # # f_code is the CompiledCode of f as requested by g. # def self.of_sender Rubinius.primitive :compiledcode_of_sender raise PrimitiveFailure, "CompiledCode.of_sender primitive failed" end def jitted? Rubinius.primitive :compiledcode_jitted_p raise PrimitiveFailure, "CompiledCode#jitted? primitive failed" end def sample_count Rubinius.primitive :compiledcode_sample_count raise PrimitiveFailure, "CompiledCode#sample_count primitive failed" end # Returns the CompiledCode object for the currently executing Ruby # method. For example: # # def m # p Rubinius::CompiledCode.current.name # end # def self.current Rubinius.primitive :compiledcode_current raise PrimitiveFailure, "CompiledCode.current primitive failed" end ## # Any CompiledCode with this value in it's serial slot is expected to be # the default, core library version. KernelMethodSerial = 47 ## # Ivars: instance_variables, primitive, serial, name, iseq, stack_size, # local_count, required_args, total_args, splat, literals, exceptions, # lines, file, compiled, scope ## # This is runtime hints, added to the method by the VM to indicate how it's # being used. attr_accessor :name attr_accessor :hints # added by the VM to indicate how it's being used. attr_accessor :metadata # [Tuple] extra data attr_accessor :name # [Symbol] name of the method attr_accessor :iseq # [Tuple] instructions to execute attr_accessor :stack_size # [Integer] size of stack at compile time attr_accessor :local_count # [Integer] number of local vars attr_accessor :required_args # [Integer] number of required args attr_accessor :post_args # [Integer] number of args after splat attr_accessor :total_args # [Integer] number of total args attr_accessor :splat # [Integer] POSITION of the splat arg attr_accessor :literals # [Tuple] tuple of the literals attr_accessor :lines # [Tuple] tuple of the lines where its found attr_accessor :file # [Symbol] the file where this comes from attr_accessor :local_names # [Array] names of the local vars attr_accessor :scope # [LexicalScope] scope for looking up constants attr_accessor :keywords # [Tuple] pairs of Symbol name, required flag attr_accessor :arity # [Integer] number of arguments, negative if variadic. attr_accessor :registers # [Integer] number of registers used. ## # Compare this method with +other+. Instead of bugging out if +other+ # isn't a {CompiledCode}, this returns +false+ immediately unless # we're comparing two apples, AKA, {CompiledCode}s. The methods have # to be the exact same in implementation, but their scoping (location) # can differ. # # For instance: # # (module A; def m; 5; end; end) == (def m; 5; end) # # and # # def monkey; 5; end # # and # # module B # def monkey; 5; end # end # # would all be the same, despite their access to different ivars and scopes # (which {CompiledCode}s DO keep track of) # # @todo Make example (in method documentation) match reality # @param [Rubinius::CompiledCode] other the other part to compare # @param [Boolean] # def ==(other) return false unless other.kind_of? CompiledCode equivalent_body?(other) and @lines == other.lines and @name == other.name end def equivalent_body?(other) @tags == other.tags and @primitive == other.primitive and @iseq == other.iseq and @stack_size == other.stack_size and @local_count == other.local_count and @required_args == other.required_args and @total_args == other.total_args and @splat == other.splat and block_index == other.block_index and @literals == other.literals and @file == other.file and @local_names == other.local_names end ## # Returns the index of local +name+ or nil if there is no local. # def local_slot(name) slot = 0 while slot < @local_count return slot if @local_names[slot] == name slot += 1 end end ## # Stores the index of the block argument in the metadata storage. # # @param [Fixnum] position # def block_index=(position) if position add_metadata(:block_index, position) end end ## # @return [Fixnum|NilClass] # def block_index return get_metadata(:block_index) end ## # Return a human readable interpretation of this method. # # @return [String] def inspect "#<#{self.class.name}:0x#{object_id.to_s(16)} name=#{@name} file=#{@file} line=#{defined_line}>" end alias_method :to_s, :inspect ## # Make the method change its scope so that it can act as though # it's from somewhere else. You can pass in a method and +self+ # will borrow its scope. # # @param [#scope] other the other method that has a scope we can borrow def inherit_scope(other) @scope = other.scope end ## # Set a breakpoint on +ip+. +obj+ can be any object. When the breakpoint # is hit +obj+ is sent to the debugger so it can distinguish which breakpoint # this is. # def set_breakpoint(ip, obj) Rubinius.primitive :compiledcode_set_breakpoint raise ArgumentError, "Unable to set breakpoint on #{inspect} at invalid bytecode address #{ip}" end ## # Erase a breakpoint at +ip+ # def clear_breakpoint(ip) Rubinius.primitive :compiledcode_clear_breakpoint raise ArgumentError, "Unable to clear breakpoint on #{inspect} at invalid bytecode address #{ip}" end ## # Indicate if there is a breakpoint set at +ip+ # def breakpoint?(ip) Rubinius.primitive :compiledcode_is_breakpoint raise ArgumentError, "Unable to retrieve breakpoint status on #{inspect} at bytecode address #{ip}" end def stamp_id Rubinius.primitive :compiledcode_stamp_id raise ArgumentError, "CompiledCode#stamp_id primitive failed" end class Script attr_accessor :compiled_code attr_accessor :file_path attr_accessor :data_path attr_accessor :eval_source def initialize(method, path=nil, for_eval=false) @compiled_code = method @file_path = path @for_eval = for_eval @eval_source = nil @main = false end private :initialize def eval? @for_eval end def make_main! @main = true end def main? @main end end # Creates the Script instance for a toplevel compiled method. def create_script(wrap=false) script = CompiledCode::Script.new(self) # Setup the scoping. cs = LexicalScope.new(Object) cs.script = script if wrap @scope = LexicalScope.new(Module.new, cs) else @scope = cs end sc = Rubinius::Type.object_singleton_class(MAIN) VM.reset_method_cache sc, :__script__ sc.method_table.store :__script__, nil, self, @scope, 0, :public script end def active_path if @scope and script = @scope.current_script if fp = script.file_path return fp.dup end end @file.to_s end def eval_source if @scope and script = @scope.current_script return script.eval_source end return nil end ## # Return the line of source code at +ip+. # # @param [Fixnum] ip # @return [Fixnum] line def line_from_ip(ip) return -1 unless @lines return 0 if @lines.size < 2 low = 0 high = @lines.size / 2 - 1 while low <= high # the chance that we're going from a fixnum to a bignum # here is low, but we still try to prevent that. mid = low + ((high - low) / 2) line_index = mid * 2 + 1 if ip < @lines.at(line_index - 1) high = mid - 1 elsif ip >= @lines.at(line_index + 1) low = mid + 1 else return @lines.at(line_index) end end @lines.at(@lines.size - 2) end ## # Returns the address (IP) of the first instruction in this CompiledCode # that is on the specified line, or the address of the first instruction on # the next code line after the specified line if there are no instructions # on the requested line. # This method only looks at instructions within the current CompiledCode; # see #locate_line for an alternate method that also searches inside the child # CompiledCodes. # # Optionally only consider ip's greater than +start+ # # @return [Fixnum] the address of the first instruction # OR nil if there is no ip for the given line def first_ip_on_line(line, start=nil) i = 1 total = @lines.size while i < total cur_line = @lines.at(i) if cur_line >= line ip = @lines.at(i-1) if !start or ip > start # matched the definition line, return 0 return 0 if ip == -1 return ip end end i += 2 end nil end ## # The first line where instructions are located. # # @return [Fixnum] def first_line line_from_ip(0) end ## # Indicate the line in the source code that this # was defined on. # def defined_line # Detect a -1 ip, which indicates a definition entry. return @lines[1] if @lines[0] == -1 first_line end ## # # Given all CompiledCodes in the system, find one that # was defined in file +file+ and encompasses +line+ # def self.locate(file, line=nil) file = StringValue file if line line = Integer line elsif m = /\A(.*):(\d+)\Z/.match(file) file = m[1] line = m[2].to_i end ary = [] ObjectSpace.find_object([:kind_of, Rubinius::CompiledCode], ary) methods = ary.find_all do |x| x.scope and path = x.scope.absolute_active_path and \ path.suffix?(file) end return methods unless line methods.find_all { |x| x.first_ip_on_line(line) } end ## # Is this actually a block of code? # # @return [Boolean] def is_block? get_metadata(:for_block) end def for_eval? get_metadata(:for_eval) end def for_module_body? get_metadata(:for_module_body) end def describe str = "method #{@name}: #{@total_args} arg(s), #{@required_args} required" if @splat str << ", splatted." end str end ## # Convenience method to return an array of the child CompiledCodes from # this CompiledCode's literals. # # @return [Tuple] def child_methods literals.select { |lit| lit.kind_of? CompiledCode } end def change_name(name) code = dup code.name = name lits = Tuple.new(code.literals.size) code.literals.each_with_index do |lit, idx| if lit.kind_of? CompiledCode and lit.is_block? lit = lit.change_name name end lits[idx] = lit end code.literals = lits return code end ## # Locates the CompiledCode and instruction address (IP) of the first # instruction on the specified line. This method recursively examines child # compiled methods until an exact match for the searched line is found. # It returns both the matching CompiledCode and the IP of the first # instruction on the requested line, or nil if no match for the specified line # is found. # # @return [(Rubinius::CompiledCode, Fixnum), NilClass] returns # nil if nothing is found, else an array of size 2 containing the method # the line was found in and the IP pointing there. def locate_line(line) i = 1 total = @lines.size while i < total cur_line = @lines.at(i) if cur_line == line ip = @lines.at(i-1) return nil if ip < 0 return [self, ip] elsif cur_line > line break end i += 2 end # Didn't find line in this CM, so check if a contained # CM encompasses the line searched for child_methods.each do |child| if res = child.locate_line(line) return res end end # No child method is a match - fail return nil end ## # Decodes the instruction sequence that is represented by this compileed # method. Delegates to InstructionSequence to do the instruction decoding, # but then converts opcode literal arguments to their actual values by looking # them up in the literals tuple. # Takes an optional bytecodes argument representing the bytecode that is to # be decoded using this CompiledCode's locals and literals. This is provided # for use by the debugger, where the bytecode sequence to be decoded may not # exactly match the bytecode currently held by the CompiledCode, typically # as a result of substituting yield_debugger instructions into the bytecode. def decode(bytecodes = @iseq) decoder = Rubinius::InstructionDecoder.new(bytecodes) stream = decoder.decode(false) ip = 0 stream.map do |inst| instruct = Instruction.new(inst, self, ip) ip += instruct.size instruct end end def add_metadata(key, val) raise TypeError, "key must be a symbol" unless key.kind_of? Symbol case val when true, false, Symbol, Fixnum, String # ok else raise TypeError, "invalid type of value" end @metadata ||= nil # to deal with MRI seeing @metadata as not set unless @metadata @metadata = Tuple.new(2) @metadata[0] = key @metadata[1] = val return val end i = 0 fin = @metadata.size while i < fin if @metadata[i] == key @metadata[i + 1] = val return val end i += 2 end tup = Tuple.new(fin + 2) tup.copy_from @metadata, 0, fin, 0 tup[fin] = key tup[fin + 1] = val @metadata = tup return val end def get_metadata(key) return nil unless @metadata.kind_of? Tuple i = 0 while i < @metadata.size if @metadata[i] == key return @metadata[i + 1] end i += 2 end return nil end ## # For Method#parameters def parameters params = [] return params unless respond_to?(:local_names) m = required_args - post_args o = m + total_args - required_args p = o + post_args p += 1 if splat local_names.each_with_index do |name, i| if i < m params << parameter(:req, name) elsif i < o params << [:opt, name] elsif splat == i params << parameter(:rest, name) elsif i < p params << parameter(:req, name) elsif block_index == i params << [:block, name] end end params end def parameter(kind, param) array = [kind] array << param unless param == :* or param[0, 2] == "_:" array end private :parameter ## # Represents virtual machine's CPU instruction. # Instructions are organized into instruction # sequences known as iSeq, forming body # of CompiledCodes. # # To generate VM opcodes documentation # use rake doc:vm task. class Instruction class Association def initialize(index) @index = index end private :initialize def inspect "literals[#{@index}]" end end class Location FORMAT = "%04d:" def initialize(location) @location = location end private :initialize def inspect FORMAT % @location end end def initialize(inst, code, ip) @instruction = inst[0] @args = inst[1..-1] @comment = nil @args.each_index do |i| case @instruction.args[i] when :literal @args[i] = code.literals[@args[i]] when :local # TODO: Blocks should be able to retrieve local names as well, # but need access to method corresponding to home context if code.local_names and !code.is_block? @comment = code.local_names[args[i]].to_s end when :association @args[i] = Association.new(args[i]) when :location @args[i] = Location.new(args[i]) end end @compiled_code = code @ip = ip end private :initialize # Instruction pointer attr_reader :ip ## # Return the line that this instruction is on in the method # def line @compiled_code.line_from_ip(ip) end ## # Returns the OpCode object # attr_reader :instruction ## # Returns the symbol representing the opcode for this instruction. # def opcode @instruction.opcode end ## # Returns an array of 0 to 2 arguments, depending on the opcode. # attr_reader :args ## # Returns a Fixnum indicating how wide the instruction takes up # in the instruction stream # def size @args.size + 1 end ## # A nice human readable interpretation of this set of instructions def to_s str = "#{Location::FORMAT} %-27s" % [@ip, opcode] str << @args.map{ |a| a.inspect }.join(', ') if @comment str << " # #{@comment}" end return str end end end end