lib/opal/nodes/rescue.rb



require 'opal/nodes/base'

module Opal
  module Nodes
    class RescueModNode < Base
      handle :rescue_mod

      children :lhs, :rhs

      def body
        stmt? ? lhs : compiler.returns(lhs)
      end

      def rescue_val
        stmt? ? rhs : compiler.returns(rhs)
      end

      def compile
        line "try {", expr(body), " } catch ($err) { "

        indent do
          line "if (Opal.rescue($err, [", expr(Sexp.new([:const, :StandardError])), "])) {"
          line expr(rescue_val)
          line "} else { throw $err; } }"
        end

        wrap '(function() {', '})()' unless stmt?
      end
    end

    class EnsureNode < Base
      handle :ensure

      children :begn, :ensr

      def compile
        push "try {"

        in_ensure do
          line compiler.process(body_sexp, @level)
        end

        line "} finally {"

        indent do
          if has_rescue_else?
            # $no_errors indicates thate there were no error raised
            unshift "var $no_errors = true; "

            # when there's a begin;rescue;else;ensure;end statement,
            # ruby returns a result of the 'else' branch
            # but invokes it before 'ensure'.
            # so, here we
            # 1. save the result of calling else to $rescue_else_result
            # 2. call ensure
            # 2. return $rescue_else_result
            line "var $rescue_else_result;"
            line "if ($no_errors) { "
            indent do
              line "$rescue_else_result = (function() {"
              indent do
                line compiler.process(compiler.returns(scope.rescue_else_sexp), @level)
              end
              line "})();"
            end
            line "}"
            line compiler.process(ensr_sexp, @level)
            line "if ($no_errors) { return $rescue_else_result; }"
          else
            line compiler.process(ensr_sexp, @level)
          end
        end

        line "}"

        wrap '(function() { ', '; })()' if wrap_in_closure?
      end

      def body_sexp
        if wrap_in_closure?
          sexp = compiler.returns(begn)
          # 'rescue' is an edge case that should be compiled to
          # try { return function(){ ..rescue through try/catch.. }() }
          sexp.type == :rescue ? s(:js_return, sexp) : sexp
        else
          sexp = begn
        end
      end

      def ensr_sexp
        ensr || s(:nil)
      end

      def wrap_in_closure?
        recv? or expr? or has_rescue_else?
      end
    end

    class RescueNode < Base
      handle :rescue

      children :body

      def compile
        scope.rescue_else_sexp = children[1..-1].detect { |sexp| sexp.type != :resbody }
        has_rescue_handlers = false

        if handle_rescue_else_manually?
          line "var $no_errors = true;"
        end

        push "try {"
        indent do
          line process(body_code, @level)
        end
        line "} catch ($err) {"

        indent do
          if has_rescue_else?
            line "$no_errors = false;"
          end

          children[1..-1].each_with_index do |child, idx|
            # counting only rescue, ignoring rescue-else statement
            if child.type == :resbody
              has_rescue_handlers = true

              push " else " unless idx == 0
              line process(child, @level)
            end
          end

          # if no resbodys capture our error, then rethrow
          push " else { throw $err; }"
        end

        line "}"

        if handle_rescue_else_manually?
          # here we must add 'finally' explicitly
          push "finally {"
          indent do
            line "if ($no_errors) { "
            indent do
              line "return (function() {"
              indent do
                line compiler.process(compiler.returns(scope.rescue_else_sexp), @level)
              end
              line "})();"
            end
            line "}"
          end
          push "}"
        end

        # Wrap a try{} catch{} into a function
        # when it's an expression
        # or when there's a method call after begin;rescue;end
        wrap '(function() { ', '})()' if expr? or recv?
      end

      def body_code
        body_code = (body.type == :resbody ? s(:nil) : body)
        body_code = compiler.returns body_code unless stmt?
        body_code
      end

      # Returns true when there's no 'ensure' statement
      #  wrapping current rescue.
      #
      def handle_rescue_else_manually?
        !scope.in_ensure? && scope.has_rescue_else?
      end
    end

    class ResBodyNode < Base
      handle :resbody

      children :args, :body

      def compile
        push "if (Opal.rescue($err, ["
        if rescue_exprs.empty?
          # if no expressions are given, then catch StandardError only
          push expr(Sexp.new([:const, :StandardError]))
        else
          rescue_exprs.each_with_index do |rexpr, idx|
            push ', ' unless idx == 0
            push expr(rexpr)
          end
        end
        push "])) {"
        indent do
          if variable = rescue_variable
            variable[2] = s(:js_tmp, '$err')
            push expr(variable), ';'
          end

          # Need to ensure we clear the current exception out after the rescue block ends
          line "try {"
          indent do
            line process(rescue_body, @level)
          end
          line '} finally { Opal.pop_exception() }'
        end
        line "}"
      end

      def rescue_variable?(variable)
        Sexp === variable and [:lasgn, :iasgn].include?(variable.type)
      end

      def rescue_variable
        rescue_variable?(args.last) ? args.last.dup : nil
      end

      def rescue_exprs
        exprs = args.dup
        exprs.pop if rescue_variable?(exprs.last)
        exprs.children
      end

      def rescue_body
        body_code = (body || s(:nil))
        body_code = compiler.returns(body_code) unless stmt?
        body_code
      end
    end
  end
end