lib/opal/nodes/closure.rb

Summary

Maintainability
C
1 day
Test Coverage
# frozen_string_literal: true

module Opal
  module Nodes
    # This module takes care of providing information about the
    # closure stack that we have for the nodes during compile time.
    # This is not a typical node.
    #
    # Also, while loops are not closures per se, this module also
    # takes a note about them.
    #
    # Then we can use this information for control flow like
    # generating breaks, nexts, returns.
    class Closure
      NONE = 0

      @types = {}

      def self.add_type(name, value)
        const_set(name, value)
        @types[name] = value
      end

      def self.type_inspect(type)
        @types.reject do |_name, value|
          (type & value) == 0
        end.map(&:first).join("|")
      end

      add_type(:JS_FUNCTION,    1 <<  0) # everything that generates an IIFE
      add_type(:JS_LOOP,        1 <<  1) # exerything that generates a JS loop
      add_type(:JS_LOOP_INSIDE, 1 <<  2) # everything that generates an inside of a loop

      add_type(:DEF,            1 <<  3) # def
      add_type(:LAMBDA,         1 <<  4) # lambda
      add_type(:ITER,           1 <<  5) # iter, lambda
      add_type(:MODULE,         1 <<  6)
      add_type(:LOOP,           1 <<  7) # for building a catcher outside a loop
      add_type(:LOOP_INSIDE,    1 <<  8) # for building a catcher inside a loop
      add_type(:SEND,           1 <<  9) # to generate a break catcher after send with a block
      add_type(:TOP,            1 << 10)
      add_type(:RESCUE_RETRIER, 1 << 11) # a virtual loop to catch a retrier

      ANY = 0xffffffff

      def initialize(node, type, parent)
        @node, @type, @parent = node, type, parent
        @catchers = []
        @throwers = {}
      end

      def register_catcher(type = :return)
        @catchers << type unless @catchers.include? type

        "$t_#{type}"
      end

      def register_thrower(type, id)
        @throwers[type] = id
      end

      def is?(type)
        (@type & type) != 0
      end

      def inspect
        "#<Closure #{Closure.type_inspect(type)} #{@node.class}>"
      end

      attr_accessor :node, :type, :parent, :catchers, :throwers

      module NodeSupport
        def push_closure(type = JS_FUNCTION)
          closure = Closure.new(self, type, select_closure)
          @compiler.closure_stack << closure
          @closure = closure
        end

        attr_accessor :closure

        def pop_closure
          compile_catcher
          @compiler.closure_stack.pop
          last = @compiler.closure_stack.last
          @closure = last if last&.node == self
        end

        def in_closure(type = JS_FUNCTION)
          closure = push_closure(type)
          out = yield closure
          pop_closure
          out
        end

        def select_closure(type = ANY, break_after: NONE)
          @compiler.closure_stack.reverse.find do |i|
            break if (i.type & break_after) != 0
            (i.type & type) != 0
          end
        end

        def generate_thrower(type, closure, value)
          id = closure.register_catcher(type)
          closure.register_thrower(type, id)
          push id, '.$throw(', expr_or_nil(value), ', ', scope.identify!, '.$$is_lambda)'
          id
        end

        def generate_thrower_without_catcher(type, closure, value)
          helper :thrower

          if closure.throwers.key? type
            id = closure.throwers[type]
          else
            id = compiler.unique_temp('t_')
            parent_scope = closure.node.scope&.parent || top_scope
            parent_scope.add_scope_temp("#{id} = $thrower('#{type}')")
            closure.register_thrower(type, id)
          end
          push id, '.$throw(', expr_or_nil(value), ', ', scope.identify!, '.$$is_lambda)'
          id
        end

        def thrower(type, value = nil)
          case type
          when :return
            thrower_closure = select_closure(DEF, break_after: MODULE | TOP)
            last_closure = select_closure(JS_FUNCTION)

            if !thrower_closure
              iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
              if iter_closure
                generate_thrower_without_catcher(:return, iter_closure, value)
              elsif compiler.eval?
                push 'Opal.t_eval_return.$throw(', expr_or_nil(value), ', false)'
              else
                error 'Invalid return'
              end
            elsif thrower_closure == last_closure
              push 'return ', expr_or_nil(value)
            else
              id = generate_thrower(:return, thrower_closure, value)
              # Additionally, register our thrower on the surrounding iter, if present
              iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
              iter_closure.register_thrower(:return, id) if iter_closure
            end
          when :eval_return
            thrower_closure = select_closure(DEF | LAMBDA, break_after: MODULE | TOP)

            if thrower_closure
              thrower_closure.register_catcher(:eval_return)
            end
          when :next, :redo
            thrower_closure = select_closure(ITER | LOOP_INSIDE, break_after: DEF | MODULE | TOP)
            last_closure = select_closure(JS_FUNCTION | JS_LOOP_INSIDE)

            if !thrower_closure
              error 'Invalid next'
            elsif thrower_closure == last_closure
              if thrower_closure.is? LOOP_INSIDE
                push 'continue'
              elsif thrower_closure.is? ITER | LAMBDA
                push 'return ', expr_or_nil(value)
              end
            else
              generate_thrower(:next, thrower_closure, value)
            end
          when :break
            thrower_closure = select_closure(SEND | LAMBDA | LOOP, break_after: DEF | MODULE | TOP)
            last_closure = select_closure(JS_FUNCTION | JS_LOOP)

            if !thrower_closure
              iter_closure = select_closure(ITER, break_after: DEF | MODULE | TOP)
              if iter_closure
                generate_thrower_without_catcher(:break, iter_closure, value)
              else
                error 'Invalid break'
              end
            elsif thrower_closure == last_closure
              if thrower_closure.is? JS_FUNCTION | LAMBDA
                push 'return ', expr_or_nil(value)
              elsif thrower_closure.is? LOOP
                push 'break'
              end
            else
              generate_thrower(:break, thrower_closure, value)
            end
          when :retry
            thrower_closure = select_closure(RESCUE_RETRIER, break_after: DEF | MODULE | TOP)
            last_closure = select_closure(JS_LOOP_INSIDE)

            if !thrower_closure
              error 'Invalid retry'
            elsif thrower_closure == last_closure
              push 'continue'
            else
              generate_thrower(:retry, thrower_closure, value)
            end
          end
        end

        def closure_is?(type)
          @closure.is?(type)
        end

        # Generate a catcher if thrower has been used
        def compile_catcher
          catchers = @closure.catchers

          return if catchers.empty?

          helper :thrower

          catchers_without_eval_return = catchers.grep_v(:eval_return)

          push "} catch($e) {"
          indent do
            catchers.each do |type|
              case type
              when :eval_return
                line "if ($e === Opal.t_eval_return) return $e.$v;"
              else
                line "if ($e === $t_#{type}) return $e.$v;"
              end
            end
            line "throw $e;"
          end
          line "}"

          unless catchers_without_eval_return.empty?
            push " finally {", *catchers_without_eval_return.map { |type| "$t_#{type}.is_orphan = true;" }, "}"
          end

          unshift "return " if closure_is? SEND

          unless catchers_without_eval_return.empty?
            unshift "var ", catchers_without_eval_return.map { |type| "$t_#{type} = $thrower('#{type}')" }.join(", "), "; "
          end
          unshift "try { "

          unless closure_is? JS_FUNCTION
            if scope.await_encountered
              wrap "(await (async function(){", "})())"
            else
              wrap "(function(){", "})()"
            end
          end
        end
      end

      module CompilerSupport
        def closure_stack
          @closure_stack ||= []
        end
      end
    end
  end
end