lib/opal/nodes/rescue.rb
# frozen_string_literal: true
require 'opal/nodes/base'
module Opal
module Nodes
class EnsureNode < Base
handle :ensure
children :begn, :ensr
def compile
push_closure if wrap_in_closure?
push 'try {'
in_ensure do
line stmt(body_sexp)
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 stmt(rescue_else_code)
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 '}'
pop_closure if wrap_in_closure?
if wrap_in_closure?
if scope.await_encountered
wrap '(await (async function() { ', '; })())'
else
wrap '(function() { ', '; })()'
end
end
end
def body_sexp
if wrap_in_closure?
compiler.returns(begn)
else
begn
end
end
def ensr_sexp
ensr || s(:nil)
end
def wrap_in_closure?
recv? || expr? || has_rescue_else?
end
def rescue_else_code
rescue_else_code = scope.rescue_else_sexp
rescue_else_code = compiler.returns(rescue_else_code) unless stmt?
rescue_else_code
end
def has_rescue_else?
@sexp.meta[:has_rescue_else]
end
end
class RescueNode < Base
handle :rescue
children :body
def compile
scope.rescue_else_sexp = children[1..-1].detect { |sexp| sexp && sexp.type != :resbody }
_has_rescue_handlers = false
if handle_rescue_else_manually?
line 'var $no_errors = true;'
end
closure_type = Closure::NONE
closure_type |= Closure::JS_FUNCTION if expr? || recv?
if has_retry?
closure_type |= Closure::JS_LOOP \
| Closure::JS_LOOP_INSIDE \
| Closure::RESCUE_RETRIER
end
push_closure(closure_type) if closure_type != Closure::NONE
in_rescue(self) do
push 'try {'
indent do
line stmt(body_code)
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
next unless child && child.type == :resbody
_has_rescue_handlers = true
push ' else ' unless idx == 0
line process(child, @level)
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 stmt(rescue_else_code)
end
line '}'
end
push '}'
end
end
pop_closure if closure_type != Closure::NONE
wrap 'do { ', ' break; } while(1)' if has_retry?
# Wrap a try{} catch{} into a function
# when it's an expression
# or when there's a method call after begin;rescue;end
if expr? || recv?
if scope.await_encountered
wrap '(await (async function() { ', '})())'
else
wrap '(function() { ', '})()'
end
end
end
def body_code
body_code = (body.nil? || body.type == :resbody ? s(:nil) : body)
body_code = compiler.returns(body_code) unless stmt?
body_code
end
def rescue_else_code
rescue_else_code = scope.rescue_else_sexp
rescue_else_code = compiler.returns(rescue_else_code) unless stmt?
rescue_else_code
end
# Returns true when there's no 'ensure' statement
# wrapping current rescue.
#
def handle_rescue_else_manually?
!in_ensure? && has_rescue_else?
end
def has_retry?
@sexp.meta[:has_retry]
end
end
class ResBodyNode < Base
handle :resbody
children :klasses_sexp, :lvar, :body
def compile
push 'if (Opal.rescue($err, ', expr(klasses), ')) {'
indent do
if lvar
push expr(lvar.updated(nil, [*lvar.children, s(:js_tmp, '$err')]))
end
# Need to ensure we clear the current exception out after the rescue block ends
line 'try {'
indent do
in_resbody do
line stmt(rescue_body)
end
end
line '} finally { Opal.pop_exception($err); }'
end
line '}'
end
def klasses
klasses_sexp || s(:array, s(:const, nil, :StandardError))
end
def rescue_body
body_code = body || s(:nil)
body_code = compiler.returns(body_code) unless stmt?
body_code
end
end
end
end