-
Notifications
You must be signed in to change notification settings - Fork 610
/
compiled_method.rb
566 lines (481 loc) · 14.9 KB
/
compiled_method.rb
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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
# -*- encoding: us-ascii -*-
module Rubinius
class CompiledMethod < Executable
##
# Any CompiledMethod with this value in it's serial slot
# is expected to be the default, kernel 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 :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<Symbol>] names of the local vars
attr_accessor :scope
##
# Compare this method with +other+. Instead of bugging out if +other+
# isn't a {CompiledMethod}, this returns +false+ immediately unless
# we're comparing two apples, AKA, {CompiledMethod}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 {CompiledMethod}s DO keep track of)
#
# @todo Make example (in method documentation) match reality
# @param [Rubinius::CompiledMethod] other the other part to compare
# @param [Boolean]
#
def ==(other)
return false unless other.kind_of? CompiledMethod
@primitive == other.primitive and
@name == other.name 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
@literals == other.literals and
@lines == other.lines 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
##
# Return a human readable interpretation of this method.
#
# @return [String]
def inspect
"#<#{self.class.name} #{@name} file=#{@file}>"
end
##
# 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 :compiledmethod_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 :compiledmethod_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 :compiledmethod_is_breakpoint
raise ArgumentError, "Unable to retrieve breakpoint status on #{inspect} at bytecode address #{ip}"
end
class Script
attr_accessor :compiled_method
attr_accessor :file_path
attr_accessor :data_path
attr_accessor :eval_binding
attr_accessor :eval_source
def initialize(method, path=nil, for_eval=false)
@compiled_method = method
@file_path = path
@for_eval = for_eval
@eval_binding = nil
@eval_source = nil
@main = false
end
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 = CompiledMethod::Script.new(self)
# Setup the scoping.
ss = StaticScope.new(Object)
ss.script = script
if wrap
@scope = StaticScope.new(Module.new, ss)
else
@scope = ss
end
sc = Rubinius::Type.object_singleton_class(MAIN)
sc.method_table.store :__script__, self, :public
VM.reset_method_cache :__script__
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 CompiledMethod
# 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 CompiledMethod;
# see #locate_line for an alternate method that also searches inside the child
# CompiledMethods.
#
# 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 CompiledMethods 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::CompiledMethod], 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 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 CompiledMethods from
# this CompiledMethod's literals.
#
# @return [Tuple]
def child_methods
literals.select { |lit| lit.kind_of? CompiledMethod }
end
def change_name(name)
cm = dup
cm.name = name
lits = Tuple.new(cm.literals.size)
cm.literals.each_with_index do |lit, idx|
if lit.kind_of? CompiledMethod and lit.is_block?
lit = lit.change_name name
end
lits[idx] = lit
end
cm.literals = lits
return cm
end
##
# Locates the CompiledMethod 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 CompiledMethod and the IP of the first
# instruction on the requested line, or nil if no match for the specified line
# is found.
#
# @return [(Rubinius::CompiledMethod, 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 CompiledMethod'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 CompiledMethod, typically
# as a result of substituting yield_debugger instructions into the bytecode.
def decode(bytecodes = @iseq)
require 'compiler/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 arity
if @required_args == @total_args and (@splat.nil? or @splat == -3)
@required_args
else
-(@required_args + 1)
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
##
# Represents virtual machine's CPU instruction.
# Instructions are organized into instruction
# sequences known as iSeq, forming body
# of CompiledMethods.
#
# To generate VM opcodes documentation
# use rake doc:vm task.
class Instruction
def initialize(inst, cm, ip)
@instruction = inst[0]
@args = inst[1..-1]
@comment = nil
@args.each_index do |i|
case @instruction.args[i]
when :literal
@args[i] = cm.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 cm.local_names and !cm.is_block?
@comment = cm.local_names[args[i]].to_s
end
end
end
@cm = cm
@ip = ip
end
# Instruction pointer
attr_reader :ip
##
# Return the line that this instruction is on in the method
#
def line
@cm.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 = "%04d: %-27s" % [@ip, opcode]
str << @args.map{ |a| a.inspect }.join(', ')
if @comment
str << " # #{@comment}"
end
return str
end
end
end
end