require 'set'
require 'pathname'
require 'opal/nodes/base'
require 'opal/nodes/runtime_helpers'
module Opal
module Nodes
class CallNode < Base
handle :call
children :recvr, :meth, :arglist, :iter
SPECIALS = {}
# Operators that get optimized by compiler
OPERATORS = { :+ => :plus, :- => :minus, :* => :times, :/ => :divide,
:< => :lt, :<= => :le, :> => :gt, :>= => :ge }
def self.add_special(name, options = {}, &handler)
SPECIALS[name] = options
define_method("handle_#{name}", &handler)
end
def compile
# handle some methods specially
handle_special
# some special methods need to skip compilation
return unless compile_default?
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
def record_method?
true
end
def default_compile
mid = mid_to_jsid meth.to_s
splat = arglist[1..-1].any? { |a| a.first == :splat }
if Sexp === arglist.last and arglist.last.type == :block_pass
block = arglist.pop
elsif iter
block = iter
end
blktmp = scope.new_temp if block
tmprecv = scope.new_temp if splat || blktmp
# must do this after assigning temp variables
block = expr(block) if block
recv_code = recv(recv_sexp)
call_recv = s(:js_tmp, tmprecv || recv_code)
if blktmp and !splat
arglist.insert 1, call_recv
end
args = expr(arglist)
if tmprecv
push "(#{tmprecv} = ", recv_code, ")#{mid}"
else
push recv_code, mid
end
if blktmp
unshift "(#{blktmp} = "
push ", #{blktmp}.$$p = ", block, ", #{blktmp})"
end
if splat
push ".apply(", (tmprecv || recv_code), ", ", args, ")"
elsif blktmp
push ".call(", args, ")"
else
push "(", args, ")"
end
scope.queue_temp blktmp if blktmp
end
def recv_sexp
recvr || s(:self)
end
def attr_assignment?
@assignment ||= meth.to_s =~ /#{REGEXP_START}[\da-z]+\=#{REGEXP_END}/i
end
# Used to generate the code to use this sexp as an ivar var reference
def compile_irb_var
with_temp do |tmp|
lvar = variable(meth)
call = s(:call, s(:self), meth.intern, s(:arglist))
push "((#{tmp} = Opal.irb_vars.#{lvar}) == null ? ", expr(call), " : #{tmp})"
end
end
def compile_assignment
with_temp do |args_tmp|
with_temp do |recv_tmp|
args = expr(arglist)
mid = mid_to_jsid meth.to_s
push "((#{args_tmp} = [", args, "]), "+
"#{recv_tmp} = ", recv(recv_sexp), ", ",
recv_tmp, mid, ".apply(#{recv_tmp}, #{args_tmp}), "+
"#{args_tmp}[#{args_tmp}.length-1])"
end
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? and scope.top? and arglist == s(:arglist) and recvr.nil? and iter.nil?
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 = true
if SPECIALS.include? meth
@compile_default = false
__send__("handle_#{meth}")
elsif RuntimeHelpers.compatible?(recvr, meth, arglist)
@compile_default = false
push(RuntimeHelpers.new(@sexp, @level, @compiler).compile)
end
end
def compile_default!
@compile_default = true
end
def compile_default?
@compile_default
end
OPERATORS.each do |operator, name|
add_special(operator.to_sym) do
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[1])
push fragment("$rb_#{name}(")
push lhs
push fragment(", ")
push rhs
push fragment(")")
else
compile_default!
end
end
end
add_special :require do
compile_default!
str = DependencyResolver.new(compiler, arglist[1]).resolve
compiler.requires << str unless str.nil?
push fragment('')
end
add_special :require_relative do
arg = arglist[1]
file = compiler.file
if arg[0] == :str
dir = File.dirname(file)
compiler.requires << Pathname(dir).join(arg[1]).cleanpath.to_s
end
push fragment("self.$require(#{file.inspect}+ '/../' + ")
push process(arglist)
push fragment(')')
end
add_special :autoload do
if scope.class_scope?
compile_default!
str = DependencyResolver.new(compiler, arglist[2]).resolve
compiler.requires << str unless str.nil?
push fragment('')
end
end
add_special :require_tree do
arg = arglist[1]
if arg[0] == :str
relative_path = arg[1]
compiler.required_trees << relative_path
dir = File.dirname(compiler.file)
full_path = Pathname(dir).join(relative_path).cleanpath.to_s
arg[1] = full_path
end
compile_default!
push fragment('')
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
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[1]
elsif type == :call
_, recv, meth, args = sexp
parts = args[1..-1].map { |s| handle_part s }
if recv == [:const, :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("/").inject([]) do |p, part|
if part == ''
# we had '//', so ignore
elsif part == '..'
p.pop
else
p << part
end
p
end.join "/"
end
end
end
end
end