lib/opal/nodes/def.rb
require 'opal/nodes/scope'
module Opal
module Nodes
# FIXME: needs rewrite
class DefNode < ScopeNode
handle :def
children :recvr, :mid, :args, :stmts
def opt_args
@opt_args ||= args[1..-1].select { |arg| arg.first == :optarg }
end
def rest_arg
@rest_arg ||= args[1..-1].find { |arg| arg.first == :restarg }
end
def keyword_args
@keyword_args ||= args[1..-1].select do |arg|
[:kwarg, :kwoptarg, :kwrestarg].include? arg.first
end
end
def block_arg
@block_arg ||= args[1..-1].find { |arg| arg.first == :blockarg }
end
def argc
return @argc if @argc
@argc = args.length - 1
@argc -= 1 if block_arg
@argc -= 1 if rest_arg
@argc -= keyword_args.size
@argc
end
def compile
jsid = mid_to_jsid mid.to_s
params = nil
scope_name = nil
# block name (&block)
if block_arg
block_name = variable(block_arg[1]).to_sym
end
if compiler.arity_check?
arity_code = arity_check(args, opt_args, rest_arg, keyword_args, block_name, mid)
end
in_scope do
scope.mid = mid
scope.defs = true if recvr
if block_name
scope.uses_block!
scope.add_arg block_name
end
scope.block_name = block_name || '$yield'
params = process(args)
stmt_code = stmt(compiler.returns(stmts))
add_temp 'self = this'
compile_rest_arg
compile_opt_args
compile_keyword_args
# must do this after opt args incase opt arg uses yield
scope_name = scope.identity
compile_block_arg
unshift "\n#{current_indent}", scope.to_vars
line stmt_code
unshift arity_code if arity_code
unshift "var $zuper = $slice.call(arguments, 0);" if scope.uses_zuper
if scope.catch_return
unshift "try {\n"
line "} catch ($returner) { if ($returner === Opal.returner) { return $returner.$v }"
push " throw $returner; }"
end
end
unshift ") {"
unshift(params)
unshift "function("
unshift "#{scope_name} = " if scope_name
line "}"
if recvr
unshift 'Opal.defs(', recv(recvr), ", '$#{mid}', "
push ')'
elsif uses_defn?(scope)
wrap "Opal.defn(self, '$#{mid}', ", ')'
elsif scope.class?
unshift "#{scope.proto}#{jsid} = "
elsif scope.sclass?
unshift "self.$$proto#{jsid} = "
elsif scope.top?
unshift "Opal.Object.$$proto#{jsid} = "
else
unshift "def#{jsid} = "
end
wrap '(', ", nil) && '#{mid}'" if expr?
end
def compile_block_arg
if scope.uses_block?
scope_name = scope.identity
yielder = scope.block_name
add_temp "$iter = #{scope_name}.$$p"
add_temp "#{yielder} = $iter || nil"
line "#{scope_name}.$$p = null;"
end
end
def compile_rest_arg
if rest_arg and rest_arg[1]
splat = variable(rest_arg[1].to_sym)
line "#{splat} = $slice.call(arguments, #{argc});"
end
end
def compile_opt_args
opt_args.each do |arg|
next if arg[2][2] == :undefined
line "if (#{variable(arg[1])} == null) {"
line " #{variable(arg[1])} = ", expr(arg[2])
line "}"
end
end
def compile_keyword_args
return if keyword_args.empty?
helper :hash2
if rest_arg
with_temp do |tmp|
rest_arg_name = variable(rest_arg[1].to_sym)
line "#{tmp} = #{rest_arg_name}[#{rest_arg_name}.length - 1];"
line "if (#{tmp} == null || !#{tmp}.$$is_hash) {"
line " $kwargs = $hash2([], {});"
line "} else {"
line " $kwargs = #{rest_arg_name}.pop();"
line "}"
end
elsif last_opt_arg = opt_args.last
opt_arg_name = variable(last_opt_arg[1])
line "if (#{opt_arg_name} == null) {"
line " $kwargs = $hash2([], {});"
line "}"
line "else if (#{opt_arg_name}.$$is_hash) {"
line " $kwargs = #{opt_arg_name};"
line " #{opt_arg_name} = ", expr(last_opt_arg[2]), ";"
line "}"
else
line "if ($kwargs == null) {"
line " $kwargs = $hash2([], {});"
line "}"
end
line "if (!$kwargs.$$is_hash) {"
line " throw Opal.ArgumentError.$new('expecting keyword args');"
line "}"
keyword_args.each do |kwarg|
case kwarg.first
when :kwoptarg
arg_name = kwarg[1]
var_name = variable(arg_name.to_s)
add_local var_name
line "if ((#{var_name} = $kwargs.smap['#{arg_name}']) == null) {"
line " #{var_name} = ", expr(kwarg[2])
line "}"
when :kwarg
arg_name = kwarg[1]
var_name = variable(arg_name.to_s)
add_local var_name
line "if ((#{var_name} = $kwargs.smap['#{arg_name}']) == null) {"
line " throw new Error('expecting keyword arg: #{arg_name}')"
line "}"
when :kwrestarg
arg_name = kwarg[1]
var_name = variable(arg_name.to_s)
add_local var_name
kwarg_names = keyword_args.select do |kw|
[:kwoptarg, :kwarg].include? kw.first
end.map { |kw| "#{kw[1].to_s.inspect}: true" }
used_args = "{#{kwarg_names.join ','}}"
line "#{var_name} = Opal.kwrestargs($kwargs, #{used_args});"
else
raise "unknown kwarg type #{kwarg.first}"
end
end
end
# Simple helper to check whether this method should be defined through
# `Opal.defn()` runtime helper.
#
# @param [Opal::Scope] scope
# @returns [Boolean]
#
def uses_defn?(scope)
if scope.iter? or scope.module?
true
elsif scope.class? and %w(Object BasicObject).include?(scope.name)
true
else
false
end
end
# Returns code used in debug mode to check arity of method call
def arity_check(args, opt, splat, kwargs, block_name, mid)
meth = mid.to_s.inspect
arity = args.size - 1
arity -= (opt.size)
arity -= 1 if splat
arity -= (kwargs.size)
arity -= 1 if block_name
arity = -arity - 1 if !opt.empty? or !kwargs.empty? or splat
# $arity will point to our received arguments count
aritycode = "var $arity = arguments.length;"
if arity < 0 # splat or opt args
aritycode + "if ($arity < #{-(arity + 1)}) { Opal.ac($arity, #{arity}, this, #{meth}); }"
else
aritycode + "if ($arity !== #{arity}) { Opal.ac($arity, #{arity}, this, #{meth}); }"
end
end
end
# def args list
class ArgsNode < Base
handle :args
def compile
done_kwargs = false
children.each_with_index do |child, idx|
next if :blockarg == child.first
next if :restarg == child.first and child[1].nil?
case child.first
when :kwarg, :kwoptarg, :kwrestarg
unless done_kwargs
done_kwargs = true
push ', ' unless idx == 0
scope.add_arg '$kwargs'
push '$kwargs'
end
else
child = child[1].to_sym
push ', ' unless idx == 0
child = variable(child)
scope.add_arg child.to_sym
push child.to_s
end
end
end
end
end
end