syntax_tree 5.1.0 → 5.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,624 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "forwardable"
4
+
5
+ module SyntaxTree
6
+ # This module provides an object representation of the YARV bytecode.
7
+ module YARV
8
+ class VM
9
+ class Jump
10
+ attr_reader :label
11
+
12
+ def initialize(label)
13
+ @label = label
14
+ end
15
+ end
16
+
17
+ class Leave
18
+ attr_reader :value
19
+
20
+ def initialize(value)
21
+ @value = value
22
+ end
23
+ end
24
+
25
+ class Frame
26
+ attr_reader :iseq, :parent, :stack_index, :_self, :nesting, :svars
27
+ attr_accessor :line, :pc
28
+
29
+ def initialize(iseq, parent, stack_index, _self, nesting)
30
+ @iseq = iseq
31
+ @parent = parent
32
+ @stack_index = stack_index
33
+ @_self = _self
34
+ @nesting = nesting
35
+
36
+ @svars = {}
37
+ @line = iseq.line
38
+ @pc = 0
39
+ end
40
+ end
41
+
42
+ class TopFrame < Frame
43
+ def initialize(iseq)
44
+ super(iseq, nil, 0, TOPLEVEL_BINDING.eval("self"), [Object])
45
+ end
46
+ end
47
+
48
+ class BlockFrame < Frame
49
+ def initialize(iseq, parent, stack_index)
50
+ super(iseq, parent, stack_index, parent._self, parent.nesting)
51
+ end
52
+ end
53
+
54
+ class MethodFrame < Frame
55
+ attr_reader :name, :block
56
+
57
+ def initialize(iseq, nesting, parent, stack_index, _self, name, block)
58
+ super(iseq, parent, stack_index, _self, nesting)
59
+ @name = name
60
+ @block = block
61
+ end
62
+ end
63
+
64
+ class ClassFrame < Frame
65
+ def initialize(iseq, parent, stack_index, _self)
66
+ super(iseq, parent, stack_index, _self, parent.nesting + [_self])
67
+ end
68
+ end
69
+
70
+ class RescueFrame < Frame
71
+ def initialize(iseq, parent, stack_index)
72
+ super(iseq, parent, stack_index, parent._self, parent.nesting)
73
+ end
74
+ end
75
+
76
+ class ThrownError < StandardError
77
+ attr_reader :value
78
+
79
+ def initialize(value, backtrace)
80
+ super("This error was thrown by the Ruby VM.")
81
+ @value = value
82
+ set_backtrace(backtrace)
83
+ end
84
+ end
85
+
86
+ class ReturnError < ThrownError
87
+ end
88
+
89
+ class BreakError < ThrownError
90
+ end
91
+
92
+ class NextError < ThrownError
93
+ end
94
+
95
+ class FrozenCore
96
+ define_method("core#hash_merge_kwd") { |left, right| left.merge(right) }
97
+
98
+ define_method("core#hash_merge_ptr") do |hash, *values|
99
+ hash.merge(values.each_slice(2).to_h)
100
+ end
101
+
102
+ define_method("core#set_method_alias") do |clazz, new_name, old_name|
103
+ clazz.alias_method(new_name, old_name)
104
+ end
105
+
106
+ define_method("core#set_variable_alias") do |new_name, old_name|
107
+ # Using eval here since there isn't a reflection API to be able to
108
+ # alias global variables.
109
+ eval("alias #{new_name} #{old_name}", binding, __FILE__, __LINE__)
110
+ end
111
+
112
+ define_method("core#set_postexe") { |&block| END { block.call } }
113
+
114
+ define_method("core#undef_method") do |clazz, name|
115
+ clazz.undef_method(name)
116
+ nil
117
+ end
118
+ end
119
+
120
+ # This is the main entrypoint for events firing in the VM, which allows
121
+ # us to implement tracing.
122
+ class NullEvents
123
+ def publish_frame_change(frame)
124
+ end
125
+
126
+ def publish_instruction(iseq, insn)
127
+ end
128
+
129
+ def publish_stack_change(stack)
130
+ end
131
+
132
+ def publish_tracepoint(event)
133
+ end
134
+ end
135
+
136
+ # This is a simple implementation of tracing that prints to STDOUT.
137
+ class STDOUTEvents
138
+ attr_reader :disassembler
139
+
140
+ def initialize
141
+ @disassembler = Disassembler.new
142
+ end
143
+
144
+ def publish_frame_change(frame)
145
+ puts "%-16s %s" % ["frame-change", "#{frame.iseq.file}@#{frame.line}"]
146
+ end
147
+
148
+ def publish_instruction(iseq, insn)
149
+ disassembler.current_iseq = iseq
150
+ puts "%-16s %s" % ["instruction", insn.disasm(disassembler)]
151
+ end
152
+
153
+ def publish_stack_change(stack)
154
+ puts "%-16s %s" % ["stack-change", stack.values.inspect]
155
+ end
156
+
157
+ def publish_tracepoint(event)
158
+ puts "%-16s %s" % ["tracepoint", event.inspect]
159
+ end
160
+ end
161
+
162
+ # This represents the global VM stack. It effectively is an array, but
163
+ # wraps mutating functions with instrumentation.
164
+ class Stack
165
+ attr_reader :events, :values
166
+
167
+ def initialize(events)
168
+ @events = events
169
+ @values = []
170
+ end
171
+
172
+ def concat(...)
173
+ values.concat(...).tap { events.publish_stack_change(self) }
174
+ end
175
+
176
+ def last
177
+ values.last
178
+ end
179
+
180
+ def length
181
+ values.length
182
+ end
183
+
184
+ def push(...)
185
+ values.push(...).tap { events.publish_stack_change(self) }
186
+ end
187
+
188
+ def pop(...)
189
+ values.pop(...).tap { events.publish_stack_change(self) }
190
+ end
191
+
192
+ def slice!(...)
193
+ values.slice!(...).tap { events.publish_stack_change(self) }
194
+ end
195
+
196
+ def [](...)
197
+ values.[](...)
198
+ end
199
+
200
+ def []=(...)
201
+ values.[]=(...).tap { events.publish_stack_change(self) }
202
+ end
203
+ end
204
+
205
+ FROZEN_CORE = FrozenCore.new.freeze
206
+
207
+ extend Forwardable
208
+
209
+ attr_reader :events
210
+
211
+ attr_reader :stack
212
+ def_delegators :stack, :push, :pop
213
+
214
+ attr_reader :frame
215
+
216
+ def initialize(events = NullEvents.new)
217
+ @events = events
218
+ @stack = Stack.new(events)
219
+ @frame = nil
220
+ end
221
+
222
+ ##########################################################################
223
+ # Helper methods for frames
224
+ ##########################################################################
225
+
226
+ def run_frame(frame)
227
+ # First, set the current frame to the given value.
228
+ previous = @frame
229
+ @frame = frame
230
+ events.publish_frame_change(@frame)
231
+
232
+ # Next, set up the local table for the frame. This is actually incorrect
233
+ # as it could use the values already on the stack, but for now we're
234
+ # just doing this for simplicity.
235
+ stack.concat(Array.new(frame.iseq.local_table.size))
236
+
237
+ # Yield so that some frame-specific setup can be done.
238
+ start_label = yield if block_given?
239
+ frame.pc = frame.iseq.insns.index(start_label) if start_label
240
+
241
+ # Finally we can execute the instructions one at a time. If they return
242
+ # jumps or leaves we will handle those appropriately.
243
+ loop do
244
+ case (insn = frame.iseq.insns[frame.pc])
245
+ when Integer
246
+ frame.line = insn
247
+ frame.pc += 1
248
+ when Symbol
249
+ events.publish_tracepoint(insn)
250
+ frame.pc += 1
251
+ when InstructionSequence::Label
252
+ # skip labels
253
+ frame.pc += 1
254
+ else
255
+ begin
256
+ events.publish_instruction(frame.iseq, insn)
257
+ result = insn.call(self)
258
+ rescue ReturnError => error
259
+ raise if frame.iseq.type != :method
260
+
261
+ stack.slice!(frame.stack_index..)
262
+ @frame = frame.parent
263
+ events.publish_frame_change(@frame)
264
+
265
+ return error.value
266
+ rescue BreakError => error
267
+ raise if frame.iseq.type != :block
268
+
269
+ catch_entry =
270
+ find_catch_entry(frame, InstructionSequence::CatchBreak)
271
+ raise unless catch_entry
272
+
273
+ stack.slice!(
274
+ (
275
+ frame.stack_index + frame.iseq.local_table.size +
276
+ catch_entry.restore_sp
277
+ )..
278
+ )
279
+ @frame = frame
280
+ events.publish_frame_change(@frame)
281
+
282
+ frame.pc = frame.iseq.insns.index(catch_entry.exit_label)
283
+ push(result = error.value)
284
+ rescue NextError => error
285
+ raise if frame.iseq.type != :block
286
+
287
+ catch_entry =
288
+ find_catch_entry(frame, InstructionSequence::CatchNext)
289
+ raise unless catch_entry
290
+
291
+ stack.slice!(
292
+ (
293
+ frame.stack_index + frame.iseq.local_table.size +
294
+ catch_entry.restore_sp
295
+ )..
296
+ )
297
+ @frame = frame
298
+ events.publish_frame_change(@frame)
299
+
300
+ frame.pc = frame.iseq.insns.index(catch_entry.exit_label)
301
+ push(result = error.value)
302
+ rescue Exception => error
303
+ catch_entry =
304
+ find_catch_entry(frame, InstructionSequence::CatchRescue)
305
+ raise unless catch_entry
306
+
307
+ stack.slice!(
308
+ (
309
+ frame.stack_index + frame.iseq.local_table.size +
310
+ catch_entry.restore_sp
311
+ )..
312
+ )
313
+ @frame = frame
314
+ events.publish_frame_change(@frame)
315
+
316
+ frame.pc = frame.iseq.insns.index(catch_entry.exit_label)
317
+ push(result = run_rescue_frame(catch_entry.iseq, frame, error))
318
+ end
319
+
320
+ case result
321
+ when Jump
322
+ frame.pc = frame.iseq.insns.index(result.label) + 1
323
+ when Leave
324
+ # this shouldn't be necessary, but is because we're not handling
325
+ # the stack correctly at the moment
326
+ stack.slice!(frame.stack_index..)
327
+
328
+ # restore the previous frame
329
+ @frame = previous || frame.parent
330
+ events.publish_frame_change(@frame) if @frame
331
+
332
+ return result.value
333
+ else
334
+ frame.pc += 1
335
+ end
336
+ end
337
+ end
338
+ end
339
+
340
+ def find_catch_entry(frame, type)
341
+ iseq = frame.iseq
342
+ iseq.catch_table.find do |catch_entry|
343
+ next unless catch_entry.is_a?(type)
344
+
345
+ begin_pc = iseq.insns.index(catch_entry.begin_label)
346
+ end_pc = iseq.insns.index(catch_entry.end_label)
347
+
348
+ (begin_pc...end_pc).cover?(frame.pc)
349
+ end
350
+ end
351
+
352
+ def run_top_frame(iseq)
353
+ run_frame(TopFrame.new(iseq))
354
+ end
355
+
356
+ def run_block_frame(iseq, frame, *args, **kwargs, &block)
357
+ run_frame(BlockFrame.new(iseq, frame, stack.length)) do
358
+ setup_arguments(iseq, args, kwargs, block)
359
+ end
360
+ end
361
+
362
+ def run_class_frame(iseq, clazz)
363
+ run_frame(ClassFrame.new(iseq, frame, stack.length, clazz))
364
+ end
365
+
366
+ def run_method_frame(name, nesting, iseq, _self, *args, **kwargs, &block)
367
+ run_frame(
368
+ MethodFrame.new(
369
+ iseq,
370
+ nesting,
371
+ frame,
372
+ stack.length,
373
+ _self,
374
+ name,
375
+ block
376
+ )
377
+ ) { setup_arguments(iseq, args, kwargs, block) }
378
+ end
379
+
380
+ def run_rescue_frame(iseq, frame, error)
381
+ run_frame(RescueFrame.new(iseq, frame, stack.length)) do
382
+ local_set(0, 0, error)
383
+ nil
384
+ end
385
+ end
386
+
387
+ def setup_arguments(iseq, args, kwargs, block)
388
+ locals = [*args]
389
+ local_index = 0
390
+ start_label = nil
391
+
392
+ # First, set up all of the leading arguments. These are positional and
393
+ # required arguments at the start of the argument list.
394
+ if (lead_num = iseq.argument_options[:lead_num])
395
+ lead_num.times do
396
+ local_set(local_index, 0, locals.shift)
397
+ local_index += 1
398
+ end
399
+ end
400
+
401
+ # Next, set up all of the optional arguments. The opt array contains
402
+ # the labels that the frame should start at if the optional is
403
+ # present. The last element of the array is the label that the frame
404
+ # should start at if all of the optional arguments are present.
405
+ if (opt = iseq.argument_options[:opt])
406
+ opt[0...-1].each do |label|
407
+ if locals.empty?
408
+ start_label = label
409
+ break
410
+ else
411
+ local_set(local_index, 0, locals.shift)
412
+ local_index += 1
413
+ end
414
+
415
+ start_label = opt.last if start_label.nil?
416
+ end
417
+ end
418
+
419
+ # If there is a splat argument, then we'll set that up here. It will
420
+ # grab up all of the remaining positional arguments.
421
+ if (rest_start = iseq.argument_options[:rest_start])
422
+ if (post_start = iseq.argument_options[:post_start])
423
+ length = post_start - rest_start
424
+ local_set(local_index, 0, locals[0...length])
425
+ locals = locals[length..]
426
+ else
427
+ local_set(local_index, 0, locals.dup)
428
+ locals.clear
429
+ end
430
+ local_index += 1
431
+ end
432
+
433
+ # Next, set up any post arguments. These are positional arguments that
434
+ # come after the splat argument.
435
+ if (post_num = iseq.argument_options[:post_num])
436
+ post_num.times do
437
+ local_set(local_index, 0, locals.shift)
438
+ local_index += 1
439
+ end
440
+ end
441
+
442
+ if (keyword_option = iseq.argument_options[:keyword])
443
+ # First, set up the keyword bits array.
444
+ keyword_bits =
445
+ keyword_option.map do |config|
446
+ kwargs.key?(config.is_a?(Array) ? config[0] : config)
447
+ end
448
+
449
+ iseq.local_table.locals.each_with_index do |local, index|
450
+ # If this is the keyword bits local, then set it appropriately.
451
+ if local.name.is_a?(Integer)
452
+ local_set(index, 0, keyword_bits)
453
+ next
454
+ end
455
+
456
+ # First, find the configuration for this local in the keywords
457
+ # list if it exists.
458
+ name = local.name
459
+ config =
460
+ keyword_option.find do |keyword|
461
+ keyword.is_a?(Array) ? keyword[0] == name : keyword == name
462
+ end
463
+
464
+ # If the configuration doesn't exist, then the local is not a
465
+ # keyword local.
466
+ next unless config
467
+
468
+ if !config.is_a?(Array)
469
+ # required keyword
470
+ local_set(index, 0, kwargs.fetch(name))
471
+ elsif !config[1].nil?
472
+ # optional keyword with embedded default value
473
+ local_set(index, 0, kwargs.fetch(name, config[1]))
474
+ else
475
+ # optional keyword with expression default value
476
+ local_set(index, 0, kwargs[name])
477
+ end
478
+ end
479
+ end
480
+
481
+ local_set(local_index, 0, block) if iseq.argument_options[:block_start]
482
+
483
+ start_label
484
+ end
485
+
486
+ ##########################################################################
487
+ # Helper methods for instructions
488
+ ##########################################################################
489
+
490
+ def const_base
491
+ frame.nesting.last
492
+ end
493
+
494
+ def frame_at(level)
495
+ current = frame
496
+ level.times { current = current.parent }
497
+ current
498
+ end
499
+
500
+ def frame_svar
501
+ current = frame
502
+ current = current.parent while current.is_a?(BlockFrame)
503
+ current
504
+ end
505
+
506
+ def frame_yield
507
+ current = frame
508
+ current = current.parent until current.is_a?(MethodFrame)
509
+ current
510
+ end
511
+
512
+ def frozen_core
513
+ FROZEN_CORE
514
+ end
515
+
516
+ def jump(label)
517
+ Jump.new(label)
518
+ end
519
+
520
+ def leave
521
+ Leave.new(pop)
522
+ end
523
+
524
+ def local_get(index, level)
525
+ stack[frame_at(level).stack_index + index]
526
+ end
527
+
528
+ def local_set(index, level, value)
529
+ stack[frame_at(level).stack_index + index] = value
530
+ end
531
+
532
+ ##########################################################################
533
+ # Methods for overriding runtime behavior
534
+ ##########################################################################
535
+
536
+ DLEXT = ".#{RbConfig::CONFIG["DLEXT"]}"
537
+ SOEXT = ".#{RbConfig::CONFIG["SOEXT"]}"
538
+
539
+ def require_resolved(filepath)
540
+ $LOADED_FEATURES << filepath
541
+ iseq = RubyVM::InstructionSequence.compile_file(filepath)
542
+ run_top_frame(InstructionSequence.from(iseq.to_a))
543
+ end
544
+
545
+ def require_internal(filepath, loading: false)
546
+ case (extname = File.extname(filepath))
547
+ when ""
548
+ # search for all the extensions
549
+ searching = filepath
550
+ extensions = ["", ".rb", DLEXT, SOEXT]
551
+ when ".rb", DLEXT, SOEXT
552
+ # search only for the given extension name
553
+ searching = File.basename(filepath, extname)
554
+ extensions = [extname]
555
+ else
556
+ # we don't handle these extensions, raise a load error
557
+ raise LoadError, "cannot load such file -- #{filepath}"
558
+ end
559
+
560
+ if filepath.start_with?("/")
561
+ # absolute path, search only in the given directory
562
+ directories = [File.dirname(searching)]
563
+ searching = File.basename(searching)
564
+ else
565
+ # relative path, search in the load path
566
+ directories = $LOAD_PATH
567
+ end
568
+
569
+ directories.each do |directory|
570
+ extensions.each do |extension|
571
+ absolute_path = File.join(directory, "#{searching}#{extension}")
572
+ next unless File.exist?(absolute_path)
573
+
574
+ if !loading && $LOADED_FEATURES.include?(absolute_path)
575
+ return false
576
+ elsif extension == ".rb"
577
+ require_resolved(absolute_path)
578
+ return true
579
+ elsif loading
580
+ return Kernel.send(:yarv_load, filepath)
581
+ else
582
+ return Kernel.send(:yarv_require, filepath)
583
+ end
584
+ end
585
+ end
586
+
587
+ if loading
588
+ Kernel.send(:yarv_load, filepath)
589
+ else
590
+ Kernel.send(:yarv_require, filepath)
591
+ end
592
+ end
593
+
594
+ def require(filepath)
595
+ require_internal(filepath, loading: false)
596
+ end
597
+
598
+ def require_relative(filepath)
599
+ Kernel.yarv_require_relative(filepath)
600
+ end
601
+
602
+ def load(filepath)
603
+ require_internal(filepath, loading: true)
604
+ end
605
+
606
+ def eval(
607
+ source,
608
+ binding = TOPLEVEL_BINDING,
609
+ filename = "(eval)",
610
+ lineno = 1
611
+ )
612
+ Kernel.yarv_eval(source, binding, filename, lineno)
613
+ end
614
+
615
+ def throw(tag, value = nil)
616
+ Kernel.throw(tag, value)
617
+ end
618
+
619
+ def catch(tag, &block)
620
+ Kernel.catch(tag, &block)
621
+ end
622
+ end
623
+ end
624
+ end