# frozen_string_literal: true
require 'set'
require 'pathname'
require 'opal/nodes/base'
require 'opal/rewriters/break_finder'
module Opal
module Nodes
class CallNode < Base
handle :send
attr_reader :recvr, :meth, :arglist, :iter
SPECIALS = {}
# Operators that get optimized by compiler
OPERATORS = { :+ => :plus, :- => :minus, :* => :times, :/ => :divide,
:< => :lt, :<= => :le, :> => :gt, :>= => :ge }.freeze
def self.add_special(name, options = {}, &handler)
SPECIALS[name] = options
define_method("handle_#{name}", &handler)
end
def initialize(*)
super
@recvr, @meth, *args = *@sexp
*rest, last_arg = *args
if last_arg && %i[iter block_pass].include?(last_arg.type)
@iter = last_arg
args = rest
else
@iter = nil
end
@arglist = s(:arglist, *args)
end
def compile
# handle some methods specially
# some special methods need to skip compilation, so we pass the default as a block
handle_special do
compiler.method_calls << meth.to_sym if record_method?
# if trying to access an lvar in irb mode
return compile_irb_var if using_irb?
default_compile
end
end
private
def iter_has_break?
return false unless iter
finder = Opal::Rewriters::BreakFinder.new
finder.process(iter)
finder.found_break?
end
# Opal has a runtime helper 'Opal.send_method_name' that assigns
# provided block to a '$$p' property of the method body
# and invokes a method using 'apply'.
#
# We have to compile a method call using this 'Opal.send_method_name' when a method:
# 1. takes a splat
# 2. takes a block
#
# Arguments that contain splat must be handled in a different way.
# @see #compile_arguments
#
# When a method takes a block we have to calculate all arguments
# **before** assigning '$$p' property (that stores a passed block)
# to a method body. This is some kind of protection from method calls
# like 'a(a {}) { 1 }'.
def invoke_using_send?
iter || splat?
end
def default_compile
if invoke_using_send?
compile_using_send
else
compile_simple_call_chain
end
compile_break_catcher
end
# Compiles method call using `Opal.send`
#
# @example
# a.b(c, &block)
#
# Opal.send(a, 'b', [c], block)
#
def compile_using_send
helper :send
push '$send('
compile_receiver
compile_method_name
compile_arguments
compile_block_pass
push ')'
end
def compile_receiver
push recv(receiver_sexp)
end
def compile_method_name
push ", '#{meth}'"
end
def compile_arguments
push ', '
if splat?
push expr(arglist)
elsif arglist.children.empty?
push '[]'
else
push '[', expr(arglist), ']'
end
end
def compile_block_pass
if iter
push ', ', expr(iter)
end
end
def compile_break_catcher
if iter_has_break?
unshift 'return '
unshift '(function(){var $brk = Opal.new_brk(); try {'
line '} catch (err) { if (err === $brk) { return err.$v } else { throw err } }})()'
end
end
def compile_simple_call_chain
push recv(receiver_sexp), method_jsid, '(', expr(arglist), ')'
end
def splat?
arglist.children.any? { |a| a.type == :splat }
end
def receiver_sexp
recvr || s(:self)
end
def method_jsid
mid_to_jsid meth.to_s
end
def record_method?
true
end
# Used to generate the code to use this sexp as an ivar var reference
def compile_irb_var
with_temp do |tmp|
lvar = meth
call = s(:send, s(:self), meth.intern, s(:arglist))
push "((#{tmp} = Opal.irb_vars.#{lvar}) == null ? ", expr(call), " : #{tmp})"
end
end
# a variable reference in irb mode in top scope might be a var ref,
# or it might be a method call
def using_irb?
@compiler.irb? && scope.top? && arglist == s(:arglist) && recvr.nil? && iter.nil?
end
def sexp_with_arglist
@sexp.updated(nil, [recvr, meth, arglist])
end
# Handle "special" method calls, e.g. require(). Subclasses can override
# this method. If this method returns nil, then the method will continue
# to be generated by CallNode.
def handle_special(&compile_default)
if SPECIALS.include? meth
method = method("handle_#{meth}")
method.arity == 1 ? method[compile_default] : method[]
else
yield # i.e. compile_default.call
end
end
OPERATORS.each do |operator, name|
add_special(operator.to_sym) do |compile_default|
if compiler.inline_operators?
compiler.method_calls << operator.to_sym if record_method?
compiler.operator_helpers << operator.to_sym
lhs, rhs = expr(recvr), expr(arglist)
push fragment("$rb_#{name}(")
push lhs
push fragment(', ')
push rhs
push fragment(')')
else
compile_default.call
end
end
end
add_special :require do |compile_default|
str = DependencyResolver.new(compiler, arglist.children[0]).resolve
compiler.requires << str unless str.nil?
compile_default.call
end
add_special :require_relative do
arg = arglist.children[0]
file = compiler.file
if arg.type == :str
dir = File.dirname(file)
compiler.requires << Pathname(dir).join(arg.children[0]).cleanpath.to_s
end
push fragment("self.$require(#{file.inspect}+ '/../' + ")
push process(arglist)
push fragment(')')
end
add_special :autoload do |compile_default|
if scope.class_scope?
str = DependencyResolver.new(compiler, arglist.children[1]).resolve
compiler.requires << str unless str.nil?
compile_default.call
end
end
add_special :require_tree do |compile_default|
first_arg, *rest = *arglist.children
if first_arg.type == :str
relative_path = first_arg.children[0]
compiler.required_trees << relative_path
dir = File.dirname(compiler.file)
full_path = Pathname(dir).join(relative_path).cleanpath.to_s
full_path.force_encoding(relative_path.encoding)
first_arg = first_arg.updated(nil, [full_path])
end
@arglist = arglist.updated(nil, [first_arg] + rest)
compile_default.call
end
add_special :block_given? do
push compiler.handle_block_given_call @sexp
end
add_special :__callee__ do
if scope.def?
push fragment scope.mid.to_s.inspect
else
push fragment 'nil'
end
end
add_special :__method__ do
if scope.def?
push fragment scope.mid.to_s.inspect
else
push fragment 'nil'
end
end
add_special :debugger do
push fragment 'debugger'
end
add_special :__OPAL_COMPILER_CONFIG__ do
push fragment "Opal.hash({ arity_check: #{compiler.arity_check?} })"
end
add_special :nesting do |compile_default|
push_nesting = push_nesting?
push '(Opal.Module.$$nesting = $nesting, ' if push_nesting
compile_default.call
push ')' if push_nesting
end
add_special :constants do |compile_default|
push_nesting = push_nesting?
push '(Opal.Module.$$nesting = $nesting, ' if push_nesting
compile_default.call
push ')' if push_nesting
end
def push_nesting?
recv = children.first
children.size == 2 && ( # only receiver and method
recv.nil? || ( # and no receiver
recv.type == :const && # or receiver
recv.children.last == :Module # is Module
)
)
end
class DependencyResolver
def initialize(compiler, sexp)
@compiler = compiler
@sexp = sexp
end
def resolve
handle_part @sexp
end
def handle_part(sexp)
type = sexp.type
if type == :str
return sexp.children[0]
elsif type == :send
recv, meth, *args = sexp.children
parts = args.map { |s| handle_part s }
if recv.is_a?(::Opal::AST::Node) && recv.type == :const && recv.children.last == :File
if meth == :expand_path
return expand_path(*parts)
elsif meth == :join
return expand_path parts.join('/')
elsif meth == :dirname
return expand_path parts[0].split('/')[0...-1].join('/')
end
end
end
msg = 'Cannot handle dynamic require'
case @compiler.dynamic_require_severity
when :error
@compiler.error msg, @sexp.line
when :warning
@compiler.warning msg, @sexp.line
end
end
def expand_path(path, base = '')
"#{base}/#{path}".split('/').each_with_object([]) do |part, p|
if part == ''
# we had '//', so ignore
elsif part == '..'
p.pop
else
p << part
end
end.join '/'
end
end
end
end
end