Skip to content

Evaluate YARV bytecode #239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[submodule "mspec"]
path = spec/mspec
url = [email protected]:ruby/mspec.git
[submodule "spec"]
path = spec/ruby
url = [email protected]:ruby/spec.git
5 changes: 4 additions & 1 deletion .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ AllCops:
SuggestExtensions: false
TargetRubyVersion: 2.7
Exclude:
- '{.git,.github,bin,coverage,pkg,test/fixtures,vendor,tmp}/**/*'
- '{.git,.github,bin,coverage,pkg,spec,test/fixtures,vendor,tmp}/**/*'
- test.rb

Layout/LineLength:
Expand Down Expand Up @@ -43,6 +43,9 @@ Lint/NonLocalExitFromIterator:
Lint/RedundantRequireStatement:
Enabled: false

Lint/RescueException:
Enabled: false

Lint/SuppressedException:
Enabled: false

Expand Down
7 changes: 7 additions & 0 deletions Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,10 @@ end

SyntaxTree::Rake::CheckTask.new(&configure)
SyntaxTree::Rake::WriteTask.new(&configure)

desc "Run mspec tests using YARV emulation"
task :spec do
Dir["./spec/ruby/language/**/*_spec.rb"].each do |filepath|
sh "exe/yarv ./spec/mspec/bin/mspec-tag #{filepath}"
end
end
63 changes: 63 additions & 0 deletions exe/yarv
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
#!/usr/bin/env ruby
# frozen_string_literal: true

$:.unshift(File.expand_path("../lib", __dir__))

require "syntax_tree"

# Require these here so that we can run binding.irb without having them require
# anything that we've already patched.
require "irb"
require "irb/completion"
require "irb/color_printer"
require "readline"

# First, create an instance of our virtual machine.
events =
if ENV["DEBUG"]
SyntaxTree::YARV::VM::STDOUTEvents.new
else
SyntaxTree::YARV::VM::NullEvents.new
end

vm = SyntaxTree::YARV::VM.new(events)

# Next, set up a bunch of aliases for methods that we're going to hook into in
# order to set up our virtual machine.
class << Kernel
alias yarv_require require
alias yarv_require_relative require_relative
alias yarv_load load
alias yarv_eval eval
alias yarv_throw throw
alias yarv_catch catch
end

# Next, patch the methods that we just aliased so that they use our virtual
# machine's versions instead. This allows us to load Ruby files and have them
# execute in our virtual machine instead of the runtime environment.
[Kernel, Kernel.singleton_class].each do |klass|
klass.define_method(:require) { |filepath| vm.require(filepath) }

klass.define_method(:load) { |filepath| vm.load(filepath) }

# klass.define_method(:require_relative) do |filepath|
# vm.require_relative(filepath)
# end

# klass.define_method(:eval) do |
# source,
# binding = TOPLEVEL_BINDING,
# filename = "(eval)",
# lineno = 1
# |
# vm.eval(source, binding, filename, lineno)
# end

# klass.define_method(:throw) { |tag, value = nil| vm.throw(tag, value) }

# klass.define_method(:catch) { |tag, &block| vm.catch(tag, &block) }
end

# Finally, require the file that we want to execute.
vm.require_resolved(ARGV.shift)
1 change: 1 addition & 0 deletions lib/syntax_tree.rb
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
require_relative "syntax_tree/yarv/legacy"
require_relative "syntax_tree/yarv/local_table"
require_relative "syntax_tree/yarv/assembler"
require_relative "syntax_tree/yarv/vm"

# Syntax Tree is a suite of tools built on top of the internal CRuby parser. It
# provides the ability to generate a syntax tree from source, as well as the
Expand Down
269 changes: 0 additions & 269 deletions lib/syntax_tree/yarv.rb
Original file line number Diff line number Diff line change
@@ -1,277 +1,8 @@
# frozen_string_literal: true

require "forwardable"

module SyntaxTree
# This module provides an object representation of the YARV bytecode.
module YARV
class VM
class Jump
attr_reader :name

def initialize(name)
@name = name
end
end

class Leave
attr_reader :value

def initialize(value)
@value = value
end
end

class Frame
attr_reader :iseq, :parent, :stack_index, :_self, :nesting, :svars

def initialize(iseq, parent, stack_index, _self, nesting)
@iseq = iseq
@parent = parent
@stack_index = stack_index
@_self = _self
@nesting = nesting
@svars = {}
end
end

class TopFrame < Frame
def initialize(iseq)
super(iseq, nil, 0, TOPLEVEL_BINDING.eval("self"), [Object])
end
end

class BlockFrame < Frame
def initialize(iseq, parent, stack_index)
super(iseq, parent, stack_index, parent._self, parent.nesting)
end
end

class MethodFrame < Frame
attr_reader :name, :block

def initialize(iseq, parent, stack_index, _self, name, block)
super(iseq, parent, stack_index, _self, parent.nesting)
@name = name
@block = block
end
end

class ClassFrame < Frame
def initialize(iseq, parent, stack_index, _self)
super(iseq, parent, stack_index, _self, parent.nesting + [_self])
end
end

class FrozenCore
define_method("core#hash_merge_kwd") { |left, right| left.merge(right) }

define_method("core#hash_merge_ptr") do |hash, *values|
hash.merge(values.each_slice(2).to_h)
end

define_method("core#set_method_alias") do |clazz, new_name, old_name|
clazz.alias_method(new_name, old_name)
end

define_method("core#set_variable_alias") do |new_name, old_name|
# Using eval here since there isn't a reflection API to be able to
# alias global variables.
eval("alias #{new_name} #{old_name}", binding, __FILE__, __LINE__)
end

define_method("core#set_postexe") { |&block| END { block.call } }

define_method("core#undef_method") do |clazz, name|
clazz.undef_method(name)
end
end

FROZEN_CORE = FrozenCore.new.freeze

extend Forwardable

attr_reader :stack
def_delegators :stack, :push, :pop

attr_reader :frame
def_delegators :frame, :_self

def initialize
@stack = []
@frame = nil
end

##########################################################################
# Helper methods for frames
##########################################################################

def run_frame(frame)
# First, set the current frame to the given value.
@frame = frame

# Next, set up the local table for the frame. This is actually incorrect
# as it could use the values already on the stack, but for now we're
# just doing this for simplicity.
frame.iseq.local_table.size.times { push(nil) }

# Yield so that some frame-specific setup can be done.
yield if block_given?

# This hash is going to hold a mapping of label names to their
# respective indices in our instruction list.
labels = {}

# This array is going to hold our instructions.
insns = []

# Here we're going to preprocess the instruction list from the
# instruction sequence to set up the labels hash and the insns array.
frame.iseq.insns.each do |insn|
case insn
when Integer, Symbol
# skip
when InstructionSequence::Label
labels[insn.name] = insns.length
else
insns << insn
end
end

# Finally we can execute the instructions one at a time. If they return
# jumps or leaves we will handle those appropriately.
pc = 0
while pc < insns.length
insn = insns[pc]
pc += 1

case (result = insn.call(self))
when Jump
pc = labels[result.name]
when Leave
return result.value
end
end
ensure
@stack = stack[0...frame.stack_index]
@frame = frame.parent
end

def run_top_frame(iseq)
run_frame(TopFrame.new(iseq))
end

def run_block_frame(iseq, *args, &block)
run_frame(BlockFrame.new(iseq, frame, stack.length)) do
locals = [*args, block]
iseq.local_table.size.times do |index|
local_set(index, 0, locals.shift)
end
end
end

def run_class_frame(iseq, clazz)
run_frame(ClassFrame.new(iseq, frame, stack.length, clazz))
end

def run_method_frame(name, iseq, _self, *args, **kwargs, &block)
run_frame(
MethodFrame.new(iseq, frame, stack.length, _self, name, block)
) do
locals = [*args, block]

if iseq.argument_options[:keyword]
# First, set up the keyword bits array.
keyword_bits =
iseq.argument_options[:keyword].map do |config|
kwargs.key?(config.is_a?(Array) ? config[0] : config)
end

iseq.local_table.locals.each_with_index do |local, index|
# If this is the keyword bits local, then set it appropriately.
if local.name == 2
locals.insert(index, keyword_bits)
next
end

# First, find the configuration for this local in the keywords
# list if it exists.
name = local.name
config =
iseq.argument_options[:keyword].find do |keyword|
keyword.is_a?(Array) ? keyword[0] == name : keyword == name
end

# If the configuration doesn't exist, then the local is not a
# keyword local.
next unless config

if !config.is_a?(Array)
# required keyword
locals.insert(index, kwargs.fetch(name))
elsif !config[1].nil?
# optional keyword with embedded default value
locals.insert(index, kwargs.fetch(name, config[1]))
else
# optional keyword with expression default value
locals.insert(index, nil)
end
end
end

iseq.local_table.size.times do |index|
local_set(index, 0, locals.shift)
end
end
end

##########################################################################
# Helper methods for instructions
##########################################################################

def const_base
frame.nesting.last
end

def frame_at(level)
current = frame
level.times { current = current.parent }
current
end

def frame_svar
current = frame
current = current.parent while current.is_a?(BlockFrame)
current
end

def frame_yield
current = frame
current = current.parent until current.is_a?(MethodFrame)
current
end

def frozen_core
FROZEN_CORE
end

def jump(label)
Jump.new(label.name)
end

def leave
Leave.new(pop)
end

def local_get(index, level)
stack[frame_at(level).stack_index + index]
end

def local_set(index, level, value)
stack[frame_at(level).stack_index + index] = value
end
end

# Compile the given source into a YARV instruction sequence.
def self.compile(source, options = Compiler::Options.new)
SyntaxTree.parse(source).accept(Compiler.new(options))
Expand Down
Loading