lib/opal/nodes/iter.rb



require 'opal/nodes/node_with_args'

module Opal
  module Nodes
    class IterNode < NodeWithArgs
      handle :iter

      children :args_sexp, :body_sexp

      attr_accessor :block_arg, :shadow_args

      def compile
        inline_params = nil
        extract_block_arg
        extract_shadow_args
        split_args

        to_vars = identity = body_code = nil

        in_scope do
          inline_params = process(inline_args_sexp)

          identity = scope.identify!
          add_temp "self = #{identity}.$$s || this"

          compile_block_arg
          compile_shadow_args
          compile_inline_args
          compile_post_args
          compile_norm_args

          if compiler.arity_check?
            compile_arity_check
          end

          body_code = stmt(body)
          to_vars = scope.to_vars
        end

        line body_code

        unshift to_vars

        unshift "(#{identity} = function(", inline_params, "){"
        push "}, #{identity}.$$s = self,"
        push " #{identity}.$$brk = $brk," if compiler.has_break?
        push " #{identity}.$$arity = #{arity},"

        if compiler.arity_check?
          push " #{identity}.$$parameters = #{parameters_code},"
        end

        # MRI expands a passed argument if the block:
        # 1. takes a single argument that is an array
        # 2. has more that one argument
        # With a few exceptions:
        # 1. mlhs arg: if a block takes |(a, b)| argument
        # 2. trailing ',' in the arg list (|a, |)
        # This flag on the method indicates that a block has a top level mlhs argument
        # which means that we have to expand passed array explicitly in runtime.
        if has_top_level_mlhs_arg?
          push " #{identity}.$$has_top_level_mlhs_arg = true,"
        end

        if has_trailing_comma_in_args?
          push " #{identity}.$$has_trailing_comma_in_args = true,"
        end

        push " #{identity})"
      end

      def norm_args
        @norm_args ||= args[1..-1].select { |arg| arg.type == :arg }
      end

      def compile_norm_args
        norm_args.each do |arg|
          arg = variable(arg[1])
          push "if (#{arg} == null) #{arg} = nil;"
        end
      end

      def compile_block_arg
        if block_arg
          block_arg = variable(self.block_arg.to_s)
          scope.block_name = block_arg
          scope.add_temp block_arg
          scope_name = scope.identify!

          line "#{block_arg} = #{scope_name}.$$p || nil, #{scope_name}.$$p = null;"
        end
      end

      def extract_block_arg
        if args.is_a?(Sexp) && args.last.is_a?(Sexp) and args.last.type == :block_pass
          self.block_arg = args.pop[1][1].to_sym
        end
      end

      def compile_shadow_args
        shadow_args.each do |shadow_arg|
          scope.add_local(shadow_arg.last)
        end
      end

      def extract_shadow_args
        if args.is_a?(Sexp)
          @shadow_args = []
          args.children.each_with_index do |arg, idx|
            if arg.type == :shadowarg
              @shadow_args << args.delete(arg)
            end
          end
        end
      end

      def args
        sexp = if Fixnum === args_sexp or args_sexp.nil?
          s(:args)
        elsif args_sexp.is_a?(Sexp) && args_sexp.type == :lasgn
          s(:args, s(:arg, *args_sexp[1]))
        else
          args_sexp[1]
        end

        # compacting _ arguments into a single one (only the first one leaves in the sexp)
        caught_blank_argument = false

        sexp.each_with_index do |part, idx|
          if part.is_a?(Sexp) && part.last == :_
            if caught_blank_argument
              sexp.delete_at(idx)
            end
            caught_blank_argument = true
          end
        end

        sexp
      end

      def body
        compiler.returns(body_sexp || s(:nil))
      end

      def mlhs_args
        scope.mlhs_mapping.keys
      end

      def has_top_level_mlhs_arg?
        args.children.any? { |arg| arg.type == :mlhs }
      end

      def has_trailing_comma_in_args?
        args.meta[:has_trailing_comma]
      end

      # Returns code used in debug mode to check arity of method call
      def compile_arity_check
        if arity_checks.size > 0
          parent_scope = scope
          while !(parent_scope.top? || parent_scope.def? || parent_scope.class_scope?)
            parent_scope = parent_scope.parent
          end

          context = if parent_scope.top?
            "'<main>'"
          elsif parent_scope.def?
            "'#{parent_scope.mid}'"
          elsif parent_scope.class?
            "'<class:#{parent_scope.name}>'"
          elsif parent_scope.module?
            "'<module:#{parent_scope.name}>'"
          end

          identity = scope.identity

          line "if (#{identity}.$$is_lambda || #{identity}.$$define_meth) {"
          line "  var $arity = arguments.length;"
          line "  if (#{arity_checks.join(' || ')}) { Opal.block_ac($arity, #{arity}, #{context}); }"
          line "}"
        end
      end
    end
  end
end