# frozen_string_literal: true
module SyntaxTree
module YARV
# This class is an experiment in transforming Syntax Tree nodes into their
# corresponding YARV instruction sequences. It attempts to mirror the
# behavior of RubyVM::InstructionSequence.compile.
#
# You use this as with any other visitor. First you parse code into a tree,
# then you visit it with this compiler. Visiting the root node of the tree
# will return a SyntaxTree::YARV::Compiler::InstructionSequence object.
# With that object you can call #to_a on it, which will return a serialized
# form of the instruction sequence as an array. This array _should_ mirror
# the array given by RubyVM::InstructionSequence#to_a.
#
# As an example, here is how you would compile a single expression:
#
# program = SyntaxTree.parse("1 + 2")
# program.accept(SyntaxTree::YARV::Compiler.new).to_a
#
# [
# "YARVInstructionSequence/SimpleDataFormat",
# 3,
# 1,
# 1,
# {:arg_size=>0, :local_size=>0, :stack_max=>2},
# "<compiled>",
# "<compiled>",
# "<compiled>",
# 1,
# :top,
# [],
# {},
# [],
# [
# [:putobject_INT2FIX_1_],
# [:putobject, 2],
# [:opt_plus, {:mid=>:+, :flag=>16, :orig_argc=>1}],
# [:leave]
# ]
# ]
#
# Note that this is the same output as calling:
#
# RubyVM::InstructionSequence.compile("1 + 2").to_a
#
class Compiler < BasicVisitor
# This represents a set of options that can be passed to the compiler to
# control how it compiles the code. It mirrors the options that can be
# passed to RubyVM::InstructionSequence.compile, except it only includes
# options that actually change the behavior.
class Options
def initialize(
frozen_string_literal: false,
inline_const_cache: true,
operands_unification: true,
peephole_optimization: true,
specialized_instruction: true,
tailcall_optimization: false
)
@frozen_string_literal = frozen_string_literal
@inline_const_cache = inline_const_cache
@operands_unification = operands_unification
@peephole_optimization = peephole_optimization
@specialized_instruction = specialized_instruction
@tailcall_optimization = tailcall_optimization
end
def to_hash
{
frozen_string_literal: @frozen_string_literal,
inline_const_cache: @inline_const_cache,
operands_unification: @operands_unification,
peephole_optimization: @peephole_optimization,
specialized_instruction: @specialized_instruction,
tailcall_optimization: @tailcall_optimization
}
end
def frozen_string_literal!
@frozen_string_literal = true
end
def frozen_string_literal?
@frozen_string_literal
end
def inline_const_cache?
@inline_const_cache
end
def operands_unification?
@operands_unification
end
def peephole_optimization?
@peephole_optimization
end
def specialized_instruction?
@specialized_instruction
end
def tailcall_optimization?
@tailcall_optimization
end
end
# This visitor is responsible for converting Syntax Tree nodes into their
# corresponding Ruby structures. This is used to convert the operands of
# some instructions like putobject that push a Ruby object directly onto
# the stack. It is only used when the entire structure can be represented
# at compile-time, as opposed to constructed at run-time.
class RubyVisitor < BasicVisitor
# This error is raised whenever a node cannot be converted into a Ruby
# object at compile-time.
class CompilationError < StandardError
end
# This will attempt to compile the given node. If it's possible, then
# it will return the compiled object. Otherwise it will return nil.
def self.compile(node)
node.accept(new)
rescue CompilationError
end
visit_methods do
def visit_array(node)
node.contents ? visit_all(node.contents.parts) : []
end
def visit_bare_assoc_hash(node)
node.assocs.to_h do |assoc|
# We can only convert regular key-value pairs. A double splat **
# operator means it has to be converted at run-time.
raise CompilationError unless assoc.is_a?(Assoc)
[visit(assoc.key), visit(assoc.value)]
end
end
def visit_float(node)
node.value.to_f
end
alias visit_hash visit_bare_assoc_hash
def visit_imaginary(node)
node.value.to_c
end
def visit_int(node)
case (value = node.value)
when /^0b/
value[2..].to_i(2)
when /^0o/
value[2..].to_i(8)
when /^0d/
value[2..].to_i
when /^0x/
value[2..].to_i(16)
else
value.to_i
end
end
def visit_label(node)
node.value.chomp(":").to_sym
end
def visit_mrhs(node)
visit_all(node.parts)
end
def visit_qsymbols(node)
node.elements.map { |element| visit(element).to_sym }
end
def visit_qwords(node)
visit_all(node.elements)
end
def visit_range(node)
left, right = [visit(node.left), visit(node.right)]
node.operator.value === ".." ? left..right : left...right
end
def visit_rational(node)
node.value.to_r
end
def visit_regexp_literal(node)
if node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
Regexp.new(
node.parts.first.value,
visit_regexp_literal_flags(node)
)
else
# Any interpolation of expressions or variables will result in the
# regular expression being constructed at run-time.
raise CompilationError
end
end
def visit_symbol_literal(node)
node.value.value.to_sym
end
def visit_symbols(node)
node.elements.map { |element| visit(element).to_sym }
end
def visit_tstring_content(node)
node.value
end
def visit_var_ref(node)
raise CompilationError unless node.value.is_a?(Kw)
case node.value.value
when "nil"
nil
when "true"
true
when "false"
false
else
raise CompilationError
end
end
def visit_word(node)
if node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
node.parts.first.value
else
# Any interpolation of expressions or variables will result in the
# string being constructed at run-time.
raise CompilationError
end
end
def visit_words(node)
visit_all(node.elements)
end
end
# This isn't actually a visit method, though maybe it should be. It is
# responsible for converting the set of string options on a regular
# expression into its equivalent integer.
def visit_regexp_literal_flags(node)
node
.options
.chars
.inject(0) do |accum, option|
accum |
case option
when "i"
Regexp::IGNORECASE
when "x"
Regexp::EXTENDED
when "m"
Regexp::MULTILINE
else
raise "Unknown regexp option: #{option}"
end
end
end
def visit_unsupported(_node)
raise CompilationError
end
# Please forgive the metaprogramming here. This is used to create visit
# methods for every node that we did not explicitly handle. By default
# each of these methods will raise a CompilationError.
handled = instance_methods(false)
(Visitor.instance_methods(false) - handled).each do |method|
alias_method method, :visit_unsupported
end
end
# These options mirror the compilation options that we currently support
# that can be also passed to RubyVM::InstructionSequence.compile.
attr_reader :options
# The current instruction sequence that is being compiled.
attr_reader :iseq
# A boolean to track if we're currently compiling the last statement
# within a set of statements. This information is necessary to determine
# if we need to return the value of the last statement.
attr_reader :last_statement
def initialize(options = Options.new)
@options = options
@iseq = nil
@last_statement = false
end
def visit_BEGIN(node)
visit(node.statements)
end
def visit_CHAR(node)
if options.frozen_string_literal?
iseq.putobject(node.value[1..])
else
iseq.putstring(node.value[1..])
end
end
def visit_END(node)
start_line = node.location.start_line
once_iseq =
with_child_iseq(iseq.block_child_iseq(start_line)) do
postexe_iseq =
with_child_iseq(iseq.block_child_iseq(start_line)) do
iseq.event(:RUBY_EVENT_B_CALL)
*statements, last_statement = node.statements.body
visit_all(statements)
with_last_statement { visit(last_statement) }
iseq.event(:RUBY_EVENT_B_RETURN)
iseq.leave
end
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.send(
YARV.calldata(:"core#set_postexe", 0, CallData::CALL_FCALL),
postexe_iseq
)
iseq.leave
end
iseq.once(once_iseq, iseq.inline_storage)
iseq.pop
end
def visit_alias(node)
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.putspecialobject(PutSpecialObject::OBJECT_CBASE)
visit(node.left)
visit(node.right)
iseq.send(YARV.calldata(:"core#set_method_alias", 3))
end
def visit_aref(node)
calldata = YARV.calldata(:[], 1)
visit(node.collection)
if !options.frozen_string_literal? &&
options.specialized_instruction? && (node.index.parts.length == 1)
arg = node.index.parts.first
if arg.is_a?(StringLiteral) && (arg.parts.length == 1)
string_part = arg.parts.first
if string_part.is_a?(TStringContent)
iseq.opt_aref_with(string_part.value, calldata)
return
end
end
end
visit(node.index)
iseq.send(calldata)
end
def visit_arg_block(node)
visit(node.value)
end
def visit_arg_paren(node)
visit(node.arguments)
end
def visit_arg_star(node)
visit(node.value)
iseq.splatarray(false)
end
def visit_args(node)
visit_all(node.parts)
end
def visit_array(node)
if (compiled = RubyVisitor.compile(node))
iseq.duparray(compiled)
elsif node.contents && node.contents.parts.length == 1 &&
node.contents.parts.first.is_a?(BareAssocHash) &&
node.contents.parts.first.assocs.length == 1 &&
node.contents.parts.first.assocs.first.is_a?(AssocSplat)
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.newhash(0)
visit(node.contents.parts.first)
iseq.send(YARV.calldata(:"core#hash_merge_kwd", 2))
iseq.newarraykwsplat(1)
else
length = 0
node.contents.parts.each do |part|
if part.is_a?(ArgStar)
if length > 0
iseq.newarray(length)
length = 0
end
visit(part.value)
iseq.concatarray
else
visit(part)
length += 1
end
end
iseq.newarray(length) if length > 0
iseq.concatarray if length > 0 && length != node.contents.parts.length
end
end
def visit_aryptn(node)
end
def visit_assign(node)
case node.target
when ARefField
calldata = YARV.calldata(:[]=, 2)
if !options.frozen_string_literal? &&
options.specialized_instruction? &&
(node.target.index.parts.length == 1)
arg = node.target.index.parts.first
if arg.is_a?(StringLiteral) && (arg.parts.length == 1)
string_part = arg.parts.first
if string_part.is_a?(TStringContent)
visit(node.target.collection)
visit(node.value)
iseq.swap
iseq.topn(1)
iseq.opt_aset_with(string_part.value, calldata)
iseq.pop
return
end
end
end
iseq.putnil
visit(node.target.collection)
visit(node.target.index)
visit(node.value)
iseq.setn(3)
iseq.send(calldata)
iseq.pop
when ConstPathField
names = constant_names(node.target)
name = names.pop
if RUBY_VERSION >= "3.2"
iseq.opt_getconstant_path(names)
visit(node.value)
iseq.swap
iseq.topn(1)
iseq.swap
iseq.setconstant(name)
else
visit(node.value)
iseq.dup if last_statement?
iseq.opt_getconstant_path(names)
iseq.setconstant(name)
end
when Field
iseq.putnil
visit(node.target)
visit(node.value)
iseq.setn(2)
iseq.send(YARV.calldata(:"#{node.target.name.value}=", 1))
iseq.pop
when TopConstField
name = node.target.constant.value.to_sym
if RUBY_VERSION >= "3.2"
iseq.putobject(Object)
visit(node.value)
iseq.swap
iseq.topn(1)
iseq.swap
iseq.setconstant(name)
else
visit(node.value)
iseq.dup if last_statement?
iseq.putobject(Object)
iseq.setconstant(name)
end
when VarField
visit(node.value)
iseq.dup if last_statement?
case node.target.value
when Const
iseq.putspecialobject(PutSpecialObject::OBJECT_CONST_BASE)
iseq.setconstant(node.target.value.value.to_sym)
when CVar
iseq.setclassvariable(node.target.value.value.to_sym)
when GVar
iseq.setglobal(node.target.value.value.to_sym)
when Ident
lookup = visit(node.target)
if lookup.local.is_a?(LocalTable::BlockLocal)
iseq.setblockparam(lookup.index, lookup.level)
else
iseq.setlocal(lookup.index, lookup.level)
end
when IVar
iseq.setinstancevariable(node.target.value.value.to_sym)
end
end
end
def visit_assoc(node)
visit(node.key)
visit(node.value)
end
def visit_assoc_splat(node)
visit(node.value)
end
def visit_backref(node)
iseq.getspecial(GetSpecial::SVAR_BACKREF, node.value[1..].to_i << 1)
end
def visit_bare_assoc_hash(node)
if (compiled = RubyVisitor.compile(node))
iseq.duphash(compiled)
else
visit_all(node.assocs)
end
end
def visit_begin(node)
end
def visit_binary(node)
case node.operator
when :"&&"
done_label = iseq.label
visit(node.left)
iseq.dup
iseq.branchunless(done_label)
iseq.pop
visit(node.right)
iseq.push(done_label)
when :"||"
visit(node.left)
iseq.dup
skip_right_label = iseq.label
iseq.branchif(skip_right_label)
iseq.pop
visit(node.right)
iseq.push(skip_right_label)
else
visit(node.left)
visit(node.right)
iseq.send(YARV.calldata(node.operator, 1))
end
end
def visit_block(node)
with_child_iseq(iseq.block_child_iseq(node.location.start_line)) do
iseq.event(:RUBY_EVENT_B_CALL)
visit(node.block_var)
visit(node.bodystmt)
iseq.event(:RUBY_EVENT_B_RETURN)
iseq.leave
end
end
def visit_block_var(node)
params = node.params
if params.requireds.length == 1 && params.optionals.empty? &&
!params.rest && params.posts.empty? && params.keywords.empty? &&
!params.keyword_rest && !params.block
iseq.argument_options[:ambiguous_param0] = true
end
visit(node.params)
node.locals.each { |local| iseq.local_table.plain(local.value.to_sym) }
end
def visit_blockarg(node)
iseq.argument_options[:block_start] = iseq.argument_size
iseq.local_table.block(node.name.value.to_sym)
iseq.argument_size += 1
end
def visit_bodystmt(node)
visit(node.statements)
end
def visit_break(node)
end
def visit_call(node)
if node.is_a?(CallNode)
return(
visit_call(
CommandCall.new(
receiver: node.receiver,
operator: node.operator,
message: node.message,
arguments: node.arguments,
block: nil,
location: node.location
)
)
)
end
# Track whether or not this is a method call on a block proxy receiver.
# If it is, we can potentially do tailcall optimizations on it.
block_receiver = false
if node.receiver
if node.receiver.is_a?(VarRef)
lookup = iseq.local_variable(node.receiver.value.value.to_sym)
if lookup.local.is_a?(LocalTable::BlockLocal)
iseq.getblockparamproxy(lookup.index, lookup.level)
block_receiver = true
else
visit(node.receiver)
end
else
visit(node.receiver)
end
else
iseq.putself
end
after_call_label = nil
if node.operator&.value == "&."
iseq.dup
after_call_label = iseq.label
iseq.branchnil(after_call_label)
end
arg_parts = argument_parts(node.arguments)
argc = arg_parts.length
flag = 0
arg_parts.each do |arg_part|
case arg_part
when ArgBlock
argc -= 1
flag |= CallData::CALL_ARGS_BLOCKARG
visit(arg_part)
when ArgStar
flag |= CallData::CALL_ARGS_SPLAT
visit(arg_part)
when ArgsForward
flag |= CallData::CALL_TAILCALL if options.tailcall_optimization?
flag |= CallData::CALL_ARGS_SPLAT
lookup = iseq.local_table.find(:*)
iseq.getlocal(lookup.index, lookup.level)
iseq.splatarray(arg_parts.length != 1)
flag |= CallData::CALL_ARGS_BLOCKARG
lookup = iseq.local_table.find(:&)
iseq.getblockparamproxy(lookup.index, lookup.level)
when BareAssocHash
flag |= CallData::CALL_KW_SPLAT
visit(arg_part)
else
visit(arg_part)
end
end
block_iseq = visit(node.block) if node.block
# If there's no block and we don't already have any special flags set,
# then we can safely call this simple arguments. Note that has to be the
# first flag we set after looking at the arguments to get the flags
# correct.
flag |= CallData::CALL_ARGS_SIMPLE if block_iseq.nil? && flag == 0
# If there's no receiver, then this is an "fcall".
flag |= CallData::CALL_FCALL if node.receiver.nil?
# If we're calling a method on the passed block object and we have
# tailcall optimizations turned on, then we can set the tailcall flag.
if block_receiver && options.tailcall_optimization?
flag |= CallData::CALL_TAILCALL
end
iseq.send(
YARV.calldata(node.message.value.to_sym, argc, flag),
block_iseq
)
iseq.event(after_call_label) if after_call_label
end
def visit_case(node)
visit(node.value) if node.value
clauses = []
else_clause = nil
current = node.consequent
while current
clauses << current
if (current = current.consequent).is_a?(Else)
else_clause = current
break
end
end
branches =
clauses.map do |clause|
visit(clause.arguments)
iseq.topn(1)
iseq.send(
YARV.calldata(
:===,
1,
CallData::CALL_FCALL | CallData::CALL_ARGS_SIMPLE
)
)
label = iseq.label
iseq.branchif(label)
[clause, label]
end
iseq.pop
else_clause ? visit(else_clause) : iseq.putnil
iseq.leave
branches.each_with_index do |(clause, label), index|
iseq.leave if index != 0
iseq.push(label)
iseq.pop
visit(clause)
end
end
def visit_class(node)
name = node.constant.constant.value.to_sym
class_iseq =
with_child_iseq(
iseq.class_child_iseq(name, node.location.start_line)
) do
iseq.event(:RUBY_EVENT_CLASS)
visit(node.bodystmt)
iseq.event(:RUBY_EVENT_END)
iseq.leave
end
flags = DefineClass::TYPE_CLASS
case node.constant
when ConstPathRef
flags |= DefineClass::FLAG_SCOPED
visit(node.constant.parent)
when ConstRef
iseq.putspecialobject(PutSpecialObject::OBJECT_CONST_BASE)
when TopConstRef
flags |= DefineClass::FLAG_SCOPED
iseq.putobject(Object)
end
if node.superclass
flags |= DefineClass::FLAG_HAS_SUPERCLASS
visit(node.superclass)
else
iseq.putnil
end
iseq.defineclass(name, class_iseq, flags)
end
def visit_command(node)
visit_call(
CommandCall.new(
receiver: nil,
operator: nil,
message: node.message,
arguments: node.arguments,
block: node.block,
location: node.location
)
)
end
def visit_command_call(node)
visit_call(
CommandCall.new(
receiver: node.receiver,
operator: node.operator,
message: node.message,
arguments: node.arguments,
block: node.block,
location: node.location
)
)
end
def visit_const_path_field(node)
visit(node.parent)
end
def visit_const_path_ref(node)
names = constant_names(node)
iseq.opt_getconstant_path(names)
end
def visit_def(node)
name = node.name.value.to_sym
method_iseq =
iseq.method_child_iseq(name.to_s, node.location.start_line)
with_child_iseq(method_iseq) do
visit(node.params) if node.params
iseq.event(:RUBY_EVENT_CALL)
visit(node.bodystmt)
iseq.event(:RUBY_EVENT_RETURN)
iseq.leave
end
if node.target
visit(node.target)
iseq.definesmethod(name, method_iseq)
else
iseq.definemethod(name, method_iseq)
end
iseq.putobject(name)
end
def visit_defined(node)
case node.value
when Assign
# If we're assigning to a local variable, then we need to make sure
# that we put it into the local table.
if node.value.target.is_a?(VarField) &&
node.value.target.value.is_a?(Ident)
iseq.local_table.plain(node.value.target.value.value.to_sym)
end
iseq.putobject("assignment")
when VarRef
value = node.value.value
name = value.value.to_sym
case value
when Const
iseq.putnil
iseq.defined(Defined::TYPE_CONST, name, "constant")
when CVar
iseq.putnil
iseq.defined(Defined::TYPE_CVAR, name, "class variable")
when GVar
iseq.putnil
iseq.defined(Defined::TYPE_GVAR, name, "global-variable")
when Ident
iseq.putobject("local-variable")
when IVar
iseq.definedivar(name, iseq.inline_storage, "instance-variable")
when Kw
case name
when :false
iseq.putobject("false")
when :nil
iseq.putobject("nil")
when :self
iseq.putobject("self")
when :true
iseq.putobject("true")
end
end
when VCall
iseq.putself
name = node.value.value.value.to_sym
iseq.defined(Defined::TYPE_FUNC, name, "method")
when YieldNode
iseq.putnil
iseq.defined(Defined::TYPE_YIELD, false, "yield")
when ZSuper
iseq.putnil
iseq.defined(Defined::TYPE_ZSUPER, false, "super")
else
iseq.putobject("expression")
end
end
def visit_dyna_symbol(node)
if node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
iseq.putobject(node.parts.first.value.to_sym)
end
end
def visit_else(node)
visit(node.statements)
iseq.pop unless last_statement?
end
def visit_elsif(node)
visit_if(
IfNode.new(
predicate: node.predicate,
statements: node.statements,
consequent: node.consequent,
location: node.location
)
)
end
def visit_ensure(node)
end
def visit_field(node)
visit(node.parent)
end
def visit_float(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_fndptn(node)
end
def visit_for(node)
visit(node.collection)
name = node.index.value.value.to_sym
iseq.local_table.plain(name)
block_iseq =
with_child_iseq(
iseq.block_child_iseq(node.statements.location.start_line)
) do
iseq.argument_options[:lead_num] ||= 0
iseq.argument_options[:lead_num] += 1
iseq.argument_options[:ambiguous_param0] = true
iseq.argument_size += 1
iseq.local_table.plain(2)
iseq.getlocal(0, 0)
local_variable = iseq.local_variable(name)
iseq.setlocal(local_variable.index, local_variable.level)
iseq.event(:RUBY_EVENT_B_CALL)
iseq.nop
visit(node.statements)
iseq.event(:RUBY_EVENT_B_RETURN)
iseq.leave
end
iseq.send(YARV.calldata(:each, 0, 0), block_iseq)
end
def visit_hash(node)
if (compiled = RubyVisitor.compile(node))
iseq.duphash(compiled)
else
visit_all(node.assocs)
iseq.newhash(node.assocs.length * 2)
end
end
def visit_hshptn(node)
end
def visit_heredoc(node)
if node.beginning.value.end_with?("`")
visit_xstring_literal(node)
elsif node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
visit(node.parts.first)
else
length = visit_string_parts(node)
iseq.concatstrings(length)
end
end
def visit_if(node)
if node.predicate.is_a?(RangeNode)
true_label = iseq.label
false_label = iseq.label
end_label = iseq.label
iseq.getspecial(GetSpecial::SVAR_FLIPFLOP_START, 0)
iseq.branchif(true_label)
visit(node.predicate.left)
iseq.branchunless(end_label)
iseq.putobject(true)
iseq.setspecial(GetSpecial::SVAR_FLIPFLOP_START)
iseq.push(true_label)
visit(node.predicate.right)
iseq.branchunless(false_label)
iseq.putobject(false)
iseq.setspecial(GetSpecial::SVAR_FLIPFLOP_START)
iseq.push(false_label)
visit(node.statements)
iseq.leave
iseq.push(end_label)
iseq.putnil
else
consequent_label = iseq.label
visit(node.predicate)
iseq.branchunless(consequent_label)
visit(node.statements)
if last_statement?
iseq.leave
iseq.push(consequent_label)
node.consequent ? visit(node.consequent) : iseq.putnil
else
iseq.pop
if node.consequent
done_label = iseq.label
iseq.jump(done_label)
iseq.push(consequent_label)
visit(node.consequent)
iseq.push(done_label)
else
iseq.push(consequent_label)
end
end
end
end
def visit_if_op(node)
visit_if(
IfNode.new(
predicate: node.predicate,
statements:
Statements.new(body: [node.truthy], location: Location.default),
consequent:
Else.new(
keyword: Kw.new(value: "else", location: Location.default),
statements:
Statements.new(
body: [node.falsy],
location: Location.default
),
location: Location.default
),
location: Location.default
)
)
end
def visit_imaginary(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_int(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_kwrest_param(node)
iseq.argument_options[:kwrest] = iseq.argument_size
iseq.argument_size += 1
iseq.local_table.plain(node.name.value.to_sym)
end
def visit_label(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_lambda(node)
lambda_iseq =
with_child_iseq(iseq.block_child_iseq(node.location.start_line)) do
iseq.event(:RUBY_EVENT_B_CALL)
visit(node.params)
visit(node.statements)
iseq.event(:RUBY_EVENT_B_RETURN)
iseq.leave
end
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.send(YARV.calldata(:lambda, 0, CallData::CALL_FCALL), lambda_iseq)
end
def visit_lambda_var(node)
visit_block_var(node)
end
def visit_massign(node)
visit(node.value)
iseq.dup
visit(node.target)
end
def visit_method_add_block(node)
visit_call(
CommandCall.new(
receiver: node.call.receiver,
operator: node.call.operator,
message: node.call.message,
arguments: node.call.arguments,
block: node.block,
location: node.location
)
)
end
def visit_mlhs(node)
lookups = []
node.parts.each do |part|
case part
when VarField
lookups << visit(part)
end
end
iseq.expandarray(lookups.length, 0)
lookups.each { |lookup| iseq.setlocal(lookup.index, lookup.level) }
end
def visit_module(node)
name = node.constant.constant.value.to_sym
module_iseq =
with_child_iseq(
iseq.module_child_iseq(name, node.location.start_line)
) do
iseq.event(:RUBY_EVENT_CLASS)
visit(node.bodystmt)
iseq.event(:RUBY_EVENT_END)
iseq.leave
end
flags = DefineClass::TYPE_MODULE
case node.constant
when ConstPathRef
flags |= DefineClass::FLAG_SCOPED
visit(node.constant.parent)
when ConstRef
iseq.putspecialobject(PutSpecialObject::OBJECT_CONST_BASE)
when TopConstRef
flags |= DefineClass::FLAG_SCOPED
iseq.putobject(Object)
end
iseq.putnil
iseq.defineclass(name, module_iseq, flags)
end
def visit_mrhs(node)
if (compiled = RubyVisitor.compile(node))
iseq.duparray(compiled)
else
visit_all(node.parts)
iseq.newarray(node.parts.length)
end
end
def visit_next(node)
end
def visit_not(node)
visit(node.statement)
iseq.send(YARV.calldata(:!))
end
def visit_opassign(node)
flag = CallData::CALL_ARGS_SIMPLE
if node.target.is_a?(ConstPathField) || node.target.is_a?(TopConstField)
flag |= CallData::CALL_FCALL
end
case (operator = node.operator.value.chomp("=").to_sym)
when :"&&"
done_label = iseq.label
with_opassign(node) do
iseq.dup
iseq.branchunless(done_label)
iseq.pop
visit(node.value)
end
case node.target
when ARefField
iseq.leave
iseq.push(done_label)
iseq.setn(3)
iseq.adjuststack(3)
when ConstPathField, TopConstField
iseq.push(done_label)
iseq.swap
iseq.pop
else
iseq.push(done_label)
end
when :"||"
if node.target.is_a?(ConstPathField) ||
node.target.is_a?(TopConstField)
opassign_defined(node)
iseq.swap
iseq.pop
elsif node.target.is_a?(VarField) &&
[Const, CVar, GVar].include?(node.target.value.class)
opassign_defined(node)
else
skip_value_label = iseq.label
with_opassign(node) do
iseq.dup
iseq.branchif(skip_value_label)
iseq.pop
visit(node.value)
end
if node.target.is_a?(ARefField)
iseq.leave
iseq.push(skip_value_label)
iseq.setn(3)
iseq.adjuststack(3)
else
iseq.push(skip_value_label)
end
end
else
with_opassign(node) do
visit(node.value)
iseq.send(YARV.calldata(operator, 1, flag))
end
end
end
def visit_params(node)
if node.requireds.any?
iseq.argument_options[:lead_num] = 0
node.requireds.each do |required|
iseq.local_table.plain(required.value.to_sym)
iseq.argument_size += 1
iseq.argument_options[:lead_num] += 1
end
end
node.optionals.each do |(optional, value)|
index = iseq.local_table.size
name = optional.value.to_sym
iseq.local_table.plain(name)
iseq.argument_size += 1
unless iseq.argument_options.key?(:opt)
start_label = iseq.label
iseq.push(start_label)
iseq.argument_options[:opt] = [start_label]
end
visit(value)
iseq.setlocal(index, 0)
arg_given_label = iseq.label
iseq.push(arg_given_label)
iseq.argument_options[:opt] << arg_given_label
end
visit(node.rest) if node.rest
if node.posts.any?
iseq.argument_options[:post_start] = iseq.argument_size
iseq.argument_options[:post_num] = 0
node.posts.each do |post|
iseq.local_table.plain(post.value.to_sym)
iseq.argument_size += 1
iseq.argument_options[:post_num] += 1
end
end
if node.keywords.any?
iseq.argument_options[:kwbits] = 0
iseq.argument_options[:keyword] = []
keyword_bits_name = node.keyword_rest ? 3 : 2
iseq.argument_size += 1
keyword_bits_index = iseq.local_table.locals.size + node.keywords.size
node.keywords.each_with_index do |(keyword, value), keyword_index|
name = keyword.value.chomp(":").to_sym
index = iseq.local_table.size
iseq.local_table.plain(name)
iseq.argument_size += 1
iseq.argument_options[:kwbits] += 1
if value.nil?
iseq.argument_options[:keyword] << name
elsif (compiled = RubyVisitor.compile(value))
iseq.argument_options[:keyword] << [name, compiled]
else
skip_value_label = iseq.label
iseq.argument_options[:keyword] << [name]
iseq.checkkeyword(keyword_bits_index, keyword_index)
iseq.branchif(skip_value_label)
visit(value)
iseq.setlocal(index, 0)
iseq.push(skip_value_label)
end
end
iseq.local_table.plain(keyword_bits_name)
end
if node.keyword_rest.is_a?(ArgsForward)
if RUBY_VERSION >= "3.2"
iseq.local_table.plain(:*)
iseq.local_table.plain(:&)
iseq.local_table.plain(:"...")
iseq.argument_options[:rest_start] = iseq.argument_size
iseq.argument_options[:block_start] = iseq.argument_size + 1
iseq.argument_size += 2
else
iseq.local_table.plain(:*)
iseq.local_table.plain(:&)
iseq.argument_options[:rest_start] = iseq.argument_size
iseq.argument_options[:block_start] = iseq.argument_size + 1
iseq.argument_size += 2
end
elsif node.keyword_rest
visit(node.keyword_rest)
end
visit(node.block) if node.block
end
def visit_paren(node)
visit(node.contents)
end
def visit_pinned_begin(node)
end
def visit_pinned_var_ref(node)
end
def visit_program(node)
node.statements.body.each do |statement|
break unless statement.is_a?(Comment)
if statement.value == "# frozen_string_literal: true"
options.frozen_string_literal!
end
end
preexes = []
statements = []
node.statements.body.each do |statement|
case statement
when Comment, EmbDoc, EndContent, VoidStmt
# ignore
when BEGINBlock
preexes << statement
else
statements << statement
end
end
top_iseq =
InstructionSequence.new(
"<compiled>",
"<compiled>",
1,
:top,
nil,
options
)
with_child_iseq(top_iseq) do
visit_all(preexes)
if statements.empty?
iseq.putnil
else
*statements, last_statement = statements
visit_all(statements)
with_last_statement { visit(last_statement) }
end
iseq.leave
end
top_iseq.compile!
top_iseq
end
def visit_qsymbols(node)
iseq.duparray(node.accept(RubyVisitor.new))
end
def visit_qwords(node)
if options.frozen_string_literal?
iseq.duparray(node.accept(RubyVisitor.new))
else
visit_all(node.elements)
iseq.newarray(node.elements.length)
end
end
def visit_range(node)
if (compiled = RubyVisitor.compile(node))
iseq.putobject(compiled)
else
visit(node.left)
visit(node.right)
iseq.newrange(node.operator.value == ".." ? 0 : 1)
end
end
def visit_rassign(node)
iseq.putnil
if node.operator.is_a?(Kw)
match_label = iseq.label
visit(node.value)
iseq.dup
visit_pattern(node.pattern, match_label)
iseq.pop
iseq.pop
iseq.putobject(false)
iseq.leave
iseq.push(match_label)
iseq.adjuststack(2)
iseq.putobject(true)
else
no_key_label = iseq.label
end_leave_label = iseq.label
end_label = iseq.label
iseq.putnil
iseq.putobject(false)
iseq.putnil
iseq.putnil
visit(node.value)
iseq.dup
visit_pattern(node.pattern, end_label)
# First we're going to push the core onto the stack, then we'll check
# if the value to match is truthy. If it is, we'll jump down to raise
# NoMatchingPatternKeyError. Otherwise we'll raise
# NoMatchingPatternError.
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.topn(4)
iseq.branchif(no_key_label)
# Here we're going to raise NoMatchingPatternError.
iseq.putobject(NoMatchingPatternError)
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.putobject("%p: %s")
iseq.topn(4)
iseq.topn(7)
iseq.send(YARV.calldata(:"core#sprintf", 3))
iseq.send(YARV.calldata(:"core#raise", 2))
iseq.jump(end_leave_label)
# Here we're going to raise NoMatchingPatternKeyError.
iseq.push(no_key_label)
iseq.putobject(NoMatchingPatternKeyError)
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.putobject("%p: %s")
iseq.topn(4)
iseq.topn(7)
iseq.send(YARV.calldata(:"core#sprintf", 3))
iseq.topn(7)
iseq.topn(9)
iseq.send(
YARV.calldata(:new, 1, CallData::CALL_KWARG, %i[matchee key])
)
iseq.send(YARV.calldata(:"core#raise", 1))
iseq.push(end_leave_label)
iseq.adjuststack(7)
iseq.putnil
iseq.leave
iseq.push(end_label)
iseq.adjuststack(6)
iseq.putnil
end
end
def visit_rational(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_redo(node)
end
def visit_regexp_literal(node)
if (compiled = RubyVisitor.compile(node))
iseq.putobject(compiled)
else
flags = RubyVisitor.new.visit_regexp_literal_flags(node)
length = visit_string_parts(node)
iseq.toregexp(flags, length)
end
end
def visit_rescue(node)
end
def visit_rescue_ex(node)
end
def visit_rescue_mod(node)
end
def visit_rest_param(node)
iseq.local_table.plain(node.name.value.to_sym)
iseq.argument_options[:rest_start] = iseq.argument_size
iseq.argument_size += 1
end
def visit_retry(node)
end
def visit_return(node)
end
def visit_sclass(node)
visit(node.target)
iseq.putnil
singleton_iseq =
with_child_iseq(
iseq.singleton_class_child_iseq(node.location.start_line)
) do
iseq.event(:RUBY_EVENT_CLASS)
visit(node.bodystmt)
iseq.event(:RUBY_EVENT_END)
iseq.leave
end
iseq.defineclass(
:singletonclass,
singleton_iseq,
DefineClass::TYPE_SINGLETON_CLASS
)
end
def visit_statements(node)
statements =
node.body.select do |statement|
case statement
when Comment, EmbDoc, EndContent, VoidStmt
false
else
true
end
end
statements.empty? ? iseq.putnil : visit_all(statements)
end
def visit_string_concat(node)
value = node.left.parts.first.value + node.right.parts.first.value
visit_string_literal(
StringLiteral.new(
parts: [TStringContent.new(value: value, location: node.location)],
quote: node.left.quote,
location: node.location
)
)
end
def visit_string_embexpr(node)
visit(node.statements)
end
def visit_string_literal(node)
if node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
visit(node.parts.first)
else
length = visit_string_parts(node)
iseq.concatstrings(length)
end
end
def visit_super(node)
iseq.putself
visit(node.arguments)
iseq.invokesuper(
YARV.calldata(
nil,
argument_parts(node.arguments).length,
CallData::CALL_FCALL | CallData::CALL_ARGS_SIMPLE |
CallData::CALL_SUPER
),
nil
)
end
def visit_symbol_literal(node)
iseq.putobject(node.accept(RubyVisitor.new))
end
def visit_symbols(node)
if (compiled = RubyVisitor.compile(node))
iseq.duparray(compiled)
else
node.elements.each do |element|
if element.parts.length == 1 &&
element.parts.first.is_a?(TStringContent)
iseq.putobject(element.parts.first.value.to_sym)
else
length = visit_string_parts(element)
iseq.concatstrings(length)
iseq.intern
end
end
iseq.newarray(node.elements.length)
end
end
def visit_top_const_ref(node)
iseq.opt_getconstant_path(constant_names(node))
end
def visit_tstring_content(node)
if options.frozen_string_literal?
iseq.putobject(node.accept(RubyVisitor.new))
else
iseq.putstring(node.accept(RubyVisitor.new))
end
end
def visit_unary(node)
method_id =
case node.operator
when "+", "-"
"#{node.operator}@"
else
node.operator
end
visit_call(
CommandCall.new(
receiver: node.statement,
operator: nil,
message: Ident.new(value: method_id, location: Location.default),
arguments: nil,
block: nil,
location: Location.default
)
)
end
def visit_undef(node)
node.symbols.each_with_index do |symbol, index|
iseq.pop if index != 0
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.putspecialobject(PutSpecialObject::OBJECT_CBASE)
visit(symbol)
iseq.send(YARV.calldata(:"core#undef_method", 2))
end
end
def visit_unless(node)
statements_label = iseq.label
visit(node.predicate)
iseq.branchunless(statements_label)
node.consequent ? visit(node.consequent) : iseq.putnil
if last_statement?
iseq.leave
iseq.push(statements_label)
visit(node.statements)
else
iseq.pop
if node.consequent
done_label = iseq.label
iseq.jump(done_label)
iseq.push(statements_label)
visit(node.consequent)
iseq.push(done_label)
else
iseq.push(statements_label)
end
end
end
def visit_until(node)
predicate_label = iseq.label
statements_label = iseq.label
iseq.jump(predicate_label)
iseq.putnil
iseq.pop
iseq.jump(predicate_label)
iseq.push(statements_label)
visit(node.statements)
iseq.pop
iseq.push(predicate_label)
visit(node.predicate)
iseq.branchunless(statements_label)
iseq.putnil if last_statement?
end
def visit_var_field(node)
case node.value
when CVar, IVar
name = node.value.value.to_sym
iseq.inline_storage_for(name)
when Ident
name = node.value.value.to_sym
if (local_variable = iseq.local_variable(name))
local_variable
else
iseq.local_table.plain(name)
iseq.local_variable(name)
end
end
end
def visit_var_ref(node)
case node.value
when Const
iseq.opt_getconstant_path(constant_names(node))
when CVar
name = node.value.value.to_sym
iseq.getclassvariable(name)
when GVar
iseq.getglobal(node.value.value.to_sym)
when Ident
lookup = iseq.local_variable(node.value.value.to_sym)
case lookup.local
when LocalTable::BlockLocal
iseq.getblockparam(lookup.index, lookup.level)
when LocalTable::PlainLocal
iseq.getlocal(lookup.index, lookup.level)
end
when IVar
name = node.value.value.to_sym
iseq.getinstancevariable(name)
when Kw
case node.value.value
when "false"
iseq.putobject(false)
when "nil"
iseq.putnil
when "self"
iseq.putself
when "true"
iseq.putobject(true)
end
end
end
def visit_vcall(node)
iseq.putself
iseq.send(
YARV.calldata(
node.value.value.to_sym,
0,
CallData::CALL_FCALL | CallData::CALL_VCALL |
CallData::CALL_ARGS_SIMPLE
)
)
end
def visit_when(node)
visit(node.statements)
end
def visit_while(node)
predicate_label = iseq.label
statements_label = iseq.label
iseq.jump(predicate_label)
iseq.putnil
iseq.pop
iseq.jump(predicate_label)
iseq.push(statements_label)
visit(node.statements)
iseq.pop
iseq.push(predicate_label)
visit(node.predicate)
iseq.branchif(statements_label)
iseq.putnil if last_statement?
end
def visit_word(node)
if node.parts.length == 1 && node.parts.first.is_a?(TStringContent)
visit(node.parts.first)
else
length = visit_string_parts(node)
iseq.concatstrings(length)
end
end
def visit_words(node)
if options.frozen_string_literal? &&
(compiled = RubyVisitor.compile(node))
iseq.duparray(compiled)
else
visit_all(node.elements)
iseq.newarray(node.elements.length)
end
end
def visit_xstring_literal(node)
iseq.putself
length = visit_string_parts(node)
iseq.concatstrings(node.parts.length) if length > 1
iseq.send(
YARV.calldata(
:`,
1,
CallData::CALL_FCALL | CallData::CALL_ARGS_SIMPLE
)
)
end
def visit_yield(node)
parts = argument_parts(node.arguments)
visit_all(parts)
iseq.invokeblock(YARV.calldata(nil, parts.length))
end
def visit_zsuper(_node)
iseq.putself
iseq.invokesuper(
YARV.calldata(
nil,
0,
CallData::CALL_FCALL | CallData::CALL_ARGS_SIMPLE |
CallData::CALL_SUPER | CallData::CALL_ZSUPER
),
nil
)
end
private
# This is a helper that is used in places where arguments may be present
# or they may be wrapped in parentheses. It's meant to descend down the
# tree and return an array of argument nodes.
def argument_parts(node)
case node
when nil
[]
when Args
node.parts
when ArgParen
if node.arguments.is_a?(ArgsForward)
[node.arguments]
else
node.arguments.parts
end
when Paren
node.contents.parts
end
end
# Constant names when they are being assigned or referenced come in as a
# tree, but it's more convenient to work with them as an array. This
# method converts them into that array. This is nice because it's the
# operand that goes to opt_getconstant_path in Ruby 3.2.
def constant_names(node)
current = node
names = []
while current.is_a?(ConstPathField) || current.is_a?(ConstPathRef)
names.unshift(current.constant.value.to_sym)
current = current.parent
end
case current
when VarField, VarRef
names.unshift(current.value.value.to_sym)
when TopConstRef
names.unshift(current.constant.value.to_sym)
names.unshift(:"")
end
names
end
# For the most part when an OpAssign (operator assignment) node with a ||=
# operator is being compiled it's a matter of reading the target, checking
# if the value should be evaluated, evaluating it if so, and then writing
# the result back to the target.
#
# However, in certain kinds of assignments (X, ::X, X::Y, @@x, and $x) we
# first check if the value is defined using the defined instruction. I
# don't know why it is necessary, and suspect that it isn't.
def opassign_defined(node)
value_label = iseq.label
skip_value_label = iseq.label
case node.target
when ConstPathField
visit(node.target.parent)
name = node.target.constant.value.to_sym
iseq.dup
iseq.defined(Defined::TYPE_CONST_FROM, name, true)
when TopConstField
name = node.target.constant.value.to_sym
iseq.putobject(Object)
iseq.dup
iseq.defined(Defined::TYPE_CONST_FROM, name, true)
when VarField
name = node.target.value.value.to_sym
iseq.putnil
case node.target.value
when Const
iseq.defined(Defined::TYPE_CONST, name, true)
when CVar
iseq.defined(Defined::TYPE_CVAR, name, true)
when GVar
iseq.defined(Defined::TYPE_GVAR, name, true)
end
end
iseq.branchunless(value_label)
case node.target
when ConstPathField, TopConstField
iseq.dup
iseq.putobject(true)
iseq.getconstant(name)
when VarField
case node.target.value
when Const
iseq.opt_getconstant_path(constant_names(node.target))
when CVar
iseq.getclassvariable(name)
when GVar
iseq.getglobal(name)
end
end
iseq.dup
iseq.branchif(skip_value_label)
iseq.pop
iseq.push(value_label)
visit(node.value)
case node.target
when ConstPathField, TopConstField
iseq.dupn(2)
iseq.swap
iseq.setconstant(name)
when VarField
iseq.dup
case node.target.value
when Const
iseq.putspecialobject(PutSpecialObject::OBJECT_CONST_BASE)
iseq.setconstant(name)
when CVar
iseq.setclassvariable(name)
when GVar
iseq.setglobal(name)
end
end
iseq.push(skip_value_label)
end
# Whenever a value is interpolated into a string-like structure, these
# three instructions are pushed.
def push_interpolate
iseq.dup
iseq.objtostring(
YARV.calldata(
:to_s,
0,
CallData::CALL_FCALL | CallData::CALL_ARGS_SIMPLE
)
)
iseq.anytostring
end
# Visit a type of pattern in a pattern match.
def visit_pattern(node, end_label)
case node
when AryPtn
length_label = iseq.label
match_failure_label = iseq.label
match_error_label = iseq.label
# If there's a constant, then check if we match against that constant
# or not first. Branch to failure if we don't.
if node.constant
iseq.dup
visit(node.constant)
iseq.checkmatch(CheckMatch::VM_CHECKMATCH_TYPE_CASE)
iseq.branchunless(match_failure_label)
end
# First, check if the #deconstruct cache is nil. If it is, we're going
# to call #deconstruct on the object and cache the result.
iseq.topn(2)
deconstruct_label = iseq.label
iseq.branchnil(deconstruct_label)
# Next, ensure that the cached value was cached correctly, otherwise
# fail the match.
iseq.topn(2)
iseq.branchunless(match_failure_label)
# Since we have a valid cached value, we can skip past the part where
# we call #deconstruct on the object.
iseq.pop
iseq.topn(1)
iseq.jump(length_label)
# Check if the object responds to #deconstruct, fail the match
# otherwise.
iseq.event(deconstruct_label)
iseq.dup
iseq.putobject(:deconstruct)
iseq.send(YARV.calldata(:respond_to?, 1))
iseq.setn(3)
iseq.branchunless(match_failure_label)
# Call #deconstruct and ensure that it's an array, raise an error
# otherwise.
iseq.send(YARV.calldata(:deconstruct))
iseq.setn(2)
iseq.dup
iseq.checktype(CheckType::TYPE_ARRAY)
iseq.branchunless(match_error_label)
# Ensure that the deconstructed array has the correct size, fail the
# match otherwise.
iseq.push(length_label)
iseq.dup
iseq.send(YARV.calldata(:length))
iseq.putobject(node.requireds.length)
iseq.send(YARV.calldata(:==, 1))
iseq.branchunless(match_failure_label)
# For each required element, check if the deconstructed array contains
# the element, otherwise jump out to the top-level match failure.
iseq.dup
node.requireds.each_with_index do |required, index|
iseq.putobject(index)
iseq.send(YARV.calldata(:[], 1))
case required
when VarField
lookup = visit(required)
iseq.setlocal(lookup.index, lookup.level)
else
visit(required)
iseq.checkmatch(CheckMatch::VM_CHECKMATCH_TYPE_CASE)
iseq.branchunless(match_failure_label)
end
if index < node.requireds.length - 1
iseq.dup
else
iseq.pop
iseq.jump(end_label)
end
end
# Set up the routine here to raise an error to indicate that the type
# of the deconstructed array was incorrect.
iseq.push(match_error_label)
iseq.putspecialobject(PutSpecialObject::OBJECT_VMCORE)
iseq.putobject(TypeError)
iseq.putobject("deconstruct must return Array")
iseq.send(YARV.calldata(:"core#raise", 2))
iseq.pop
# Patch all of the match failures to jump here so that we pop a final
# value before returning to the parent node.
iseq.push(match_failure_label)
iseq.pop
when VarField
lookup = visit(node)
iseq.setlocal(lookup.index, lookup.level)
iseq.jump(end_label)
end
end
# There are a lot of nodes in the AST that act as contains of parts of
# strings. This includes things like string literals, regular expressions,
# heredocs, etc. This method will visit all the parts of a string within
# those containers.
def visit_string_parts(node)
length = 0
unless node.parts.first.is_a?(TStringContent)
iseq.putobject("")
length += 1
end
node.parts.each do |part|
case part
when StringDVar
visit(part.variable)
push_interpolate
when StringEmbExpr
visit(part)
push_interpolate
when TStringContent
iseq.putobject(part.accept(RubyVisitor.new))
end
length += 1
end
length
end
# The current instruction sequence that we're compiling is always stored
# on the compiler. When we descend into a node that has its own
# instruction sequence, this method can be called to temporarily set the
# new value of the instruction sequence, yield, and then set it back.
def with_child_iseq(child_iseq)
parent_iseq = iseq
begin
@iseq = child_iseq
yield
child_iseq
ensure
@iseq = parent_iseq
end
end
# When we're compiling the last statement of a set of statements within a
# scope, the instructions sometimes change from pops to leaves. These
# kinds of peephole optimizations can reduce the overall number of
# instructions. Therefore, we keep track of whether we're compiling the
# last statement of a scope and allow visit methods to query that
# information.
def with_last_statement
previous = @last_statement
@last_statement = true
begin
yield
ensure
@last_statement = previous
end
end
def last_statement?
@last_statement
end
# OpAssign nodes can have a number of different kinds of nodes as their
# "target" (i.e., the left-hand side of the assignment). When compiling
# these nodes we typically need to first fetch the current value of the
# variable, then perform some kind of action, then store the result back
# into the variable. This method handles that by first fetching the value,
# then yielding to the block, then storing the result.
def with_opassign(node)
case node.target
when ARefField
iseq.putnil
visit(node.target.collection)
visit(node.target.index)
iseq.dupn(2)
iseq.send(YARV.calldata(:[], 1))
yield
iseq.setn(3)
iseq.send(YARV.calldata(:[]=, 2))
iseq.pop
when ConstPathField
name = node.target.constant.value.to_sym
visit(node.target.parent)
iseq.dup
iseq.putobject(true)
iseq.getconstant(name)
yield
if node.operator.value == "&&="
iseq.dupn(2)
else
iseq.swap
iseq.topn(1)
end
iseq.swap
iseq.setconstant(name)
when TopConstField
name = node.target.constant.value.to_sym
iseq.putobject(Object)
iseq.dup
iseq.putobject(true)
iseq.getconstant(name)
yield
if node.operator.value == "&&="
iseq.dupn(2)
else
iseq.swap
iseq.topn(1)
end
iseq.swap
iseq.setconstant(name)
when VarField
case node.target.value
when Const
names = constant_names(node.target)
iseq.opt_getconstant_path(names)
yield
iseq.dup
iseq.putspecialobject(PutSpecialObject::OBJECT_CONST_BASE)
iseq.setconstant(names.last)
when CVar
name = node.target.value.value.to_sym
iseq.getclassvariable(name)
yield
iseq.dup
iseq.setclassvariable(name)
when GVar
name = node.target.value.value.to_sym
iseq.getglobal(name)
yield
iseq.dup
iseq.setglobal(name)
when Ident
local_variable = visit(node.target)
iseq.getlocal(local_variable.index, local_variable.level)
yield
iseq.dup
iseq.setlocal(local_variable.index, local_variable.level)
when IVar
name = node.target.value.value.to_sym
iseq.getinstancevariable(name)
yield
iseq.dup
iseq.setinstancevariable(name)
end
end
end
end
end
end