diff --git a/lib/opal/compiler.rb b/lib/opal/compiler.rb index d9a2b40a01..906d206353 100644 --- a/lib/opal/compiler.rb +++ b/lib/opal/compiler.rb @@ -7,6 +7,7 @@ require 'opal/eof_content' require 'opal/errors' require 'opal/magic_comments' +require 'opal/nodes/closure' module Opal # Compile a string of ruby code into javascript. @@ -46,6 +47,8 @@ def self.compile(source, options = {}) # compiler.source_map # => # # class Compiler + include Nodes::Closure::CompilerSupport + # Generated code gets indented with two spaces on each scope INDENT = ' ' diff --git a/lib/opal/nodes.rb b/lib/opal/nodes.rb index 0494a13379..823b328f54 100644 --- a/lib/opal/nodes.rb +++ b/lib/opal/nodes.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'opal/nodes/closure' require 'opal/nodes/base' require 'opal/nodes/literal' require 'opal/nodes/variables' diff --git a/lib/opal/nodes/base.rb b/lib/opal/nodes/base.rb index 1bfb224697..dac267650a 100644 --- a/lib/opal/nodes/base.rb +++ b/lib/opal/nodes/base.rb @@ -1,11 +1,13 @@ # frozen_string_literal: true require 'opal/nodes/helpers' +require 'opal/nodes/closure' module Opal module Nodes class Base include Helpers + include Closure::NodeSupport def self.handlers @handlers ||= {} @@ -126,6 +128,10 @@ def expr_or_nil(sexp) sexp ? expr(sexp) : 'nil' end + def expr_or_empty(sexp) + sexp && sexp.type != :nil ? expr(sexp) : '' + end + def add_local(name) scope.add_scope_local name.to_sym end diff --git a/lib/opal/nodes/call.rb b/lib/opal/nodes/call.rb index 9fae23e69e..12f8ed9a0b 100644 --- a/lib/opal/nodes/call.rb +++ b/lib/opal/nodes/call.rb @@ -3,7 +3,6 @@ require 'set' require 'pathname' require 'opal/nodes/base' -require 'opal/rewriters/break_finder' module Opal module Nodes @@ -64,9 +63,7 @@ def compile def iter_has_break? return false unless iter - finder = Opal::Rewriters::BreakFinder.new - finder.process(iter) - finder.found_break? + iter.meta[:has_break] end # Opal has a runtime helper 'Opal.send_method_name' that assigns @@ -103,6 +100,7 @@ def default_compile scope.await_encountered = true end + push_closure(Closure::SEND) if iter_has_break? if invoke_using_refinement? compile_using_refined_send elsif invoke_using_send? @@ -110,8 +108,7 @@ def default_compile else compile_simple_call_chain end - - compile_break_catcher + pop_closure if iter_has_break? if auto_await? push ')' @@ -188,14 +185,6 @@ def compile_refinements push expr(s(:array, *refinements)), ', ' end - def compile_break_catcher - if iter_has_break? - unshift 'return ' - unshift '(function(){var $brk = Opal.new_brk(); try {' - line '} catch (err) { if (err === $brk) { return err.$v } else { throw err } }})()' - end - end - def compile_simple_call_chain push recv(receiver_sexp), method_jsid, '(', expr(arglist), ')' end @@ -402,6 +391,9 @@ def using_refinement(arg) # This can be refactored in terms of binding, but it would need 'corelib/binding' # to be required in existing code. add_special :eval do |compile_default| + # Catch the return throw coming from eval + thrower(:eval_return) + next compile_default.call if arglist.children.length != 1 || ![s(:self), nil].include?(recvr) scope.nesting diff --git a/lib/opal/nodes/class.rb b/lib/opal/nodes/class.rb index 00b4e47432..e285cce15e 100644 --- a/lib/opal/nodes/class.rb +++ b/lib/opal/nodes/class.rb @@ -24,7 +24,9 @@ def compile line " var self = $klass($base, $super, '#{name}');" in_scope do scope.name = name - compile_body + in_closure(Closure::MODULE | Closure::JS_FUNCTION) do + compile_body + end end if await_encountered diff --git a/lib/opal/nodes/closure.rb b/lib/opal/nodes/closure.rb new file mode 100644 index 0000000000..30686a33d8 --- /dev/null +++ b/lib/opal/nodes/closure.rb @@ -0,0 +1,250 @@ +# 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 + "#" + 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_empty(value), ')' + 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_') + scope = closure.node.scope&.parent || top_scope + scope.add_scope_temp("#{id} = $thrower('#{type}')") + closure.register_thrower(type, id) + end + push id, '.$throw(', expr_or_empty(value), ')' + 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_empty(value), ')' + 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 + + push "} catch($e) {" + indent do + @closure.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 "}" + + unshift "return " if closure_is? SEND + + unshift "var ", catchers.map { |type| "$t_#{type} = $thrower('#{type}')" }.join(", "), "; " + 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 diff --git a/lib/opal/nodes/def.rb b/lib/opal/nodes/def.rb index f38686fd80..ac314e84ed 100644 --- a/lib/opal/nodes/def.rb +++ b/lib/opal/nodes/def.rb @@ -56,22 +56,18 @@ def compile_body inline_params = process(inline_args) - stmt_code = stmt(compiler.returns(stmts)) + in_closure(Closure::DEF | Closure::JS_FUNCTION) do + stmt_code = stmt(compiler.returns(stmts)) - compile_block_arg + compile_block_arg - add_temp 'self = this' if @define_self + add_temp 'self = this' if @define_self - compile_arity_check + compile_arity_check - unshift "\n#{current_indent}", scope.to_vars + unshift "\n#{current_indent}", scope.to_vars - line stmt_code - - if scope.catch_return - unshift "try {\n" - line '} catch ($returner) { if ($returner === Opal.returner) { return $returner.$v }' - push ' throw $returner; }' + line stmt_code end end diff --git a/lib/opal/nodes/definitions.rb b/lib/opal/nodes/definitions.rb index 9456a3807b..c0aaf2c60a 100644 --- a/lib/opal/nodes/definitions.rb +++ b/lib/opal/nodes/definitions.rb @@ -54,7 +54,9 @@ def compile elsif children.size == 1 compile_inline_children(returned_children, @level) else - compile_children(returned_children, @level) + in_closure do + compile_children(returned_children, @level) + end if scope.parent&.await_encountered wrap '(await (async function() {', '})())' diff --git a/lib/opal/nodes/if.rb b/lib/opal/nodes/if.rb index f3f7299cd9..ba8c302c10 100644 --- a/lib/opal/nodes/if.rb +++ b/lib/opal/nodes/if.rb @@ -27,6 +27,8 @@ def compile end def compile_with_if + push_closure if expects_expression? + truthy = self.truthy falsy = self.falsy @@ -60,6 +62,8 @@ def compile_with_if line 'return nil;' if expects_expression? end + pop_closure if expects_expression? + if expects_expression? return_kw = 'return ' if returning_if? diff --git a/lib/opal/nodes/iter.rb b/lib/opal/nodes/iter.rb index b745e5dbc1..3a4d04bc85 100644 --- a/lib/opal/nodes/iter.rb +++ b/lib/opal/nodes/iter.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true require 'opal/nodes/node_with_args' -require 'opal/rewriters/break_finder' module Opal module Nodes @@ -18,7 +17,8 @@ def compile blockopts = [] blockopts << "$$arity: #{arity}" blockopts << "$$s: #{scope.self}" if @define_self - blockopts << "$$brk: $brk" if contains_break? + blockopts << "$$brk: #{@closure.throwers[:break]}" if @closure&.throwers&.key? :break + blockopts << "$$ret: #{@closure.throwers[:return]}" if @closure&.throwers&.key? :return if compiler.arity_check? blockopts << "$$parameters: #{parameters_code}" @@ -66,18 +66,14 @@ def compile_body compile_arity_check - body_code = stmt(returned_body) + in_closure(Closure::JS_FUNCTION | Closure::ITER | (@is_lambda ? Closure::LAMBDA : 0)) do + body_code = stmt(returned_body) - add_temp "self = #{identity}.$$s == null ? this : #{identity}.$$s" if @define_self + add_temp "self = #{identity}.$$s == null ? this : #{identity}.$$s" if @define_self - to_vars = scope.to_vars + to_vars = scope.to_vars - line body_code - - if scope.catch_return - unshift "try {\n" - line '} catch ($returner) { if ($returner === Opal.returner) { return $returner.$v }' - push ' throw $returner; }' + line body_code end end @@ -139,12 +135,6 @@ def has_trailing_comma_in_args? def arity_check_node s(:iter_arity_check, original_args) end - - def contains_break? - finder = Opal::Rewriters::BreakFinder.new - finder.process(@sexp) - finder.found_break? - end end end end diff --git a/lib/opal/nodes/logic.rb b/lib/opal/nodes/logic.rb index 7f3dbcd7b2..b5531c30a6 100644 --- a/lib/opal/nodes/logic.rb +++ b/lib/opal/nodes/logic.rb @@ -8,13 +8,7 @@ class NextNode < Base handle :next def compile - if in_while? - push 'continue;' - elsif scope.iter? - push 'return ', expr_or_nil(value), ';' - else - error 'Invalid next' - end + thrower(:next, value) end def value @@ -35,35 +29,7 @@ class BreakNode < Base children :value def compile - if in_while? - compile_while - elsif scope.iter? - compile_iter - else - error 'void value expression: cannot use break outside of iter/while' - end - end - - def compile_while - if while_loop[:closure] - push 'return ', expr_or_nil(value) - else - push 'break;' - end - end - - def compile_iter - error 'break must be used as a statement' unless stmt? - - line 'Opal.brk(', break_val, ', $brk)' - end - - def break_val - if value.nil? - expr(s(:nil)) - else - expr(value) - end + thrower(:break, value) end end @@ -81,8 +47,8 @@ def compile end def compile_while - while_loop[:use_redo] = true - push "#{while_loop[:redo_var]} = true; continue;" + push "#{while_loop[:redo_var]} = true;" + thrower(:redo) end def compile_iter @@ -110,6 +76,13 @@ def compile end end + class RetryNode < Base + handle :retry + + def compile + thrower(:retry) + end + end class ReturnNode < Base handle :return @@ -118,37 +91,16 @@ class ReturnNode < Base def return_val if value.nil? - expr(s(:nil)) + s(:nil) elsif children.size > 1 - expr(s(:array, *children)) + s(:array, *children) else - expr(value) + value end end - def return_in_iter? - if (scope.iter? && !scope.lambda?) && parent_def = scope.find_parent_def - parent_def - end - end - - def return_expr_in_def? - return scope if expr? && (scope.def? || scope.lambda?) - end - - def scope_to_catch_return - return_in_iter? || return_expr_in_def? - end - def compile - if def_scope = scope_to_catch_return - def_scope.catch_return = true - push 'Opal.ret(', return_val, ')' - elsif stmt? - push 'return ', return_val - else - error 'void value expression: cannot return as an expression' - end + thrower(:return, return_val) end end diff --git a/lib/opal/nodes/module.rb b/lib/opal/nodes/module.rb index 05047e8abb..911533d925 100644 --- a/lib/opal/nodes/module.rb +++ b/lib/opal/nodes/module.rb @@ -24,7 +24,9 @@ def compile line " var self = $module($base, '#{name}');" in_scope do scope.name = name - compile_body + in_closure(Closure::MODULE | Closure::JS_FUNCTION) do + compile_body + end end if await_encountered diff --git a/lib/opal/nodes/rescue.rb b/lib/opal/nodes/rescue.rb index c280414bd1..889c7088b3 100644 --- a/lib/opal/nodes/rescue.rb +++ b/lib/opal/nodes/rescue.rb @@ -10,6 +10,8 @@ class EnsureNode < Base children :begn, :ensr def compile + push_closure if wrap_in_closure? + push 'try {' in_ensure do @@ -49,6 +51,8 @@ def compile line '}' + pop_closure if wrap_in_closure? + if wrap_in_closure? if scope.await_encountered wrap '(await (async function() { ', '; })())' @@ -79,6 +83,10 @@ def rescue_else_code 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 @@ -94,6 +102,15 @@ def compile 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 @@ -133,10 +150,12 @@ def compile end push '}' end - - wrap "#{retry_id}: do { ", ' break; } while(1)' if retry_id 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 @@ -168,11 +187,9 @@ def handle_rescue_else_manually? !in_ensure? && has_rescue_else? end - def gen_retry_id - @retry_id ||= scope.gen_retry_id + def has_retry? + @sexp.meta[:has_retry] end - - attr_reader :retry_id end class ResBodyNode < Base @@ -209,14 +226,5 @@ def rescue_body body_code end end - - class RetryNode < Base - handle :retry - - def compile - error 'Invalid retry' unless in_resbody? - push "continue #{scope.current_rescue.gen_retry_id}" - end - end end end diff --git a/lib/opal/nodes/scope.rb b/lib/opal/nodes/scope.rb index 11d37433f4..cc8372151a 100644 --- a/lib/opal/nodes/scope.rb +++ b/lib/opal/nodes/scope.rb @@ -197,6 +197,12 @@ def add_scope_temp(tmp) @temps.push(tmp) end + def prepend_scope_temp(tmp) + return if has_temp?(tmp) + + @temps.unshift(tmp) + end + def has_temp?(tmp) @temps.include? tmp end diff --git a/lib/opal/nodes/top.rb b/lib/opal/nodes/top.rb index 8e02a38144..87e4c39027 100644 --- a/lib/opal/nodes/top.rb +++ b/lib/opal/nodes/top.rb @@ -33,7 +33,9 @@ def compile in_scope do line '"use strict";' if compiler.use_strict? - body_code = stmt(stmts) + body_code = in_closure(Closure::JS_FUNCTION | Closure::TOP) do + stmt(stmts) + end body_code = [body_code] unless body_code.is_a?(Array) if compiler.eval? @@ -124,7 +126,7 @@ def compile_irb_vars end def add_used_helpers - compiler.helpers.to_a.each { |h| add_temp "$#{h} = Opal.#{h}" } + compiler.helpers.to_a.reverse_each { |h| prepend_scope_temp "$#{h} = Opal.#{h}" } end def compile_method_stubs diff --git a/lib/opal/nodes/while.rb b/lib/opal/nodes/while.rb index 2322756302..10ebc9a527 100644 --- a/lib/opal/nodes/while.rb +++ b/lib/opal/nodes/while.rb @@ -12,20 +12,27 @@ class WhileNode < Base def compile test_code = js_truthy(test) - with_temp do |redo_var| - compiler.in_while do - while_loop[:closure] = true if wrap_in_closure? - while_loop[:redo_var] = redo_var + @redo_var = scope.new_temp if uses_redo? + + compiler.in_while do + while_loop[:closure] = true if wrap_in_closure? + while_loop[:redo_var] = @redo_var + + in_closure(Closure::LOOP | Closure::JS_LOOP | (wrap_in_closure? ? Closure::JS_FUNCTION : 0)) do + in_closure(Closure::LOOP_INSIDE | Closure::JS_LOOP_INSIDE) do + line(indent { stmt(body) }) + end - body_code = indent { stmt(body) } if uses_redo? - compile_with_redo(test_code, body_code, redo_var) + compile_with_redo(test_code) else - compile_without_redo(test_code, body_code) + compile_without_redo(test_code) end end end + scope.queue_temp(@redo_var) if uses_redo? + if wrap_in_closure? if scope.await_encountered wrap '(await (async function() {', '; return nil; })())' @@ -37,26 +44,27 @@ def compile private - def compile_with_redo(test_code, body_code, redo_var) - push "#{redo_var} = false; " - compile_while( - [redo_var, " || ", test_code], - ["#{redo_var} = false;", body_code] - ) + def compile_with_redo(test_code) + compile_while(test_code, "#{@redo_var} = false;") end - def compile_without_redo(test_code, body_code) - compile_while([test_code], [body_code]) + def compile_without_redo(test_code) + compile_while(test_code) end - def compile_while(test_code, body_code) - push while_open, *test_code, while_close - indent { line(*body_code) } + def compile_while(test_code, redo_code = nil) + unshift redo_code if redo_code + unshift while_open, test_code, while_close + unshift redo_code if redo_code line '}' end def while_open - 'while (' + if uses_redo? + redo_part = "#{@redo_var} || " + end + + "while (#{redo_part}" end def while_close @@ -64,7 +72,7 @@ def while_close end def uses_redo? - while_loop[:use_redo] + @sexp.meta[:has_redo] end def wrap_in_closure? @@ -78,7 +86,11 @@ class UntilNode < WhileNode private def while_open - 'while (!(' + if uses_redo? + redo_part = "#{@redo_var} || " + end + + "while (#{redo_part}!(" end def while_close @@ -91,10 +103,10 @@ class WhilePostNode < WhileNode private - def compile_while(test_code, body_code) - push "do {" - indent { line(*body_code) } - line "} ", while_open, *test_code, while_close + def compile_while(test_code, redo_code = nil) + unshift redo_code if redo_code + unshift "do {" + line "} ", while_open, test_code, while_close end def while_close @@ -108,7 +120,11 @@ class UntilPostNode < WhilePostNode private def while_open - 'while(!(' + if uses_redo? + redo_part = "#{@redo_var} || " + end + + "while (#{redo_part}!(" end def while_close diff --git a/lib/opal/rewriter.rb b/lib/opal/rewriter.rb index f93f42ed81..3280257488 100644 --- a/lib/opal/rewriter.rb +++ b/lib/opal/rewriter.rb @@ -15,6 +15,7 @@ require 'opal/rewriters/numblocks' require 'opal/rewriters/returnable_logic' require 'opal/rewriters/forward_args' +require 'opal/rewriters/thrower_finder' module Opal class Rewriter @@ -66,6 +67,7 @@ def rewritter_disabled?(rewriter) use Rewriters::DumpArgs use Rewriters::MlhsArgs use Rewriters::InlineArgs + use Rewriters::ThrowerFinder def initialize(sexp) @sexp = sexp diff --git a/lib/opal/rewriters/break_finder.rb b/lib/opal/rewriters/break_finder.rb deleted file mode 100644 index 778d4debc9..0000000000 --- a/lib/opal/rewriters/break_finder.rb +++ /dev/null @@ -1,36 +0,0 @@ -# frozen_string_literal: true - -require 'opal/rewriter' - -module Opal - module Rewriters - class BreakFinder < Opal::Rewriters::Base - def initialize - @found_break = false - end - - def found_break? - @found_break - end - - def on_break(node) - @found_break = true - node - end - - def stop_lookup(node) - # noop - end - - # regular loops - alias on_for stop_lookup - alias on_while stop_lookup - alias on_while_post stop_lookup - alias on_until stop_lookup - alias on_until_post stop_lookup - - # nested block - alias on_block stop_lookup - end - end -end diff --git a/lib/opal/rewriters/returnable_logic.rb b/lib/opal/rewriters/returnable_logic.rb index dc6c57130b..1393fba552 100644 --- a/lib/opal/rewriters/returnable_logic.rb +++ b/lib/opal/rewriters/returnable_logic.rb @@ -21,6 +21,9 @@ def reset_tmp_counter! def on_if(node) test, = *node.children + + check_control_flow!(test) + # The if_test metadata signifies that we don't care about the return value except if it's # truthy or falsy. And those tests will be carried out by the respective $truthy helper calls. test.meta[:if_test] = true if test @@ -41,6 +44,8 @@ def on_case(node) def on_or(node) lhs, rhs = *node.children + check_control_flow!(lhs) + if node.meta[:if_test] # Let's forward the if_test to the lhs and rhs - since we don't care about the exact return # value of our or, we neither do care about a return value of our lhs or rhs. @@ -58,6 +63,8 @@ def on_or(node) def on_and(node) lhs, rhs = *node.children + check_control_flow!(lhs) + if node.meta[:if_test] lhs.meta[:if_test] = rhs.meta[:if_test] = true out = process(node.updated(:if, [lhs, rhs, s(:false)])) @@ -81,6 +88,13 @@ def on_begin(node) private + def check_control_flow!(node) + case node.type + when :break, :next, :redo, :retry, :return + error 'void value expression' + end + end + def build_if_from_when(node, lhs, lhs_tmp, whens, els) first_when, *next_whens = *whens diff --git a/lib/opal/rewriters/thrower_finder.rb b/lib/opal/rewriters/thrower_finder.rb new file mode 100644 index 0000000000..517d289592 --- /dev/null +++ b/lib/opal/rewriters/thrower_finder.rb @@ -0,0 +1,90 @@ +# frozen_string_literal: true + +# rubocop:disable Layout/EmptyLineBetweenDefs, Style/SingleLineMethods +module Opal + module Rewriters + # ThrowerFinder attempts to track the presence of throwers, like + # break, redo, so we can make an informed guess in the early + # compilation phase before traversing other nodes whether we + # want to track a closure. Tracking a closure is often a deoptimizing + # step, so we want to get that knowledge earlier. + class ThrowerFinder < Opal::Rewriters::Base + def initialize + @break_stack = [] + @redo_stack = [] + @retry_stack = [] + @rescue_else_stack = [] + end + + def on_break(node) + tracking(:break, @break_stack) + super + end + + def on_redo(node) + tracking(:redo, @redo_stack) + super + end + + def on_retry(node) + tracking(:retry, @retry_stack) + super + end + + def on_iter(node) + pushing([@break_stack, node]) { super } + end + + def on_loop(node, &block) + pushing([@redo_stack, node], [@break_stack, nil], &block) + end + + def on_for(node); on_loop(node) { super }; end + def on_while(node); on_loop(node) { super }; end + def on_while_post(node); on_loop(node) { super }; end + def on_until(node); on_loop(node) { super }; end + def on_until_post(node); on_loop(node) { super }; end + + # ignore throwers inside defined + def on_defined(node) + pushing( + [@redo_stack, nil], + [@break_stack, nil], + [@retry_stack, nil] + ) { super } + end + + # In Opal we handle rescue-else either in ensure or in + # rescue. If ensure is present, we handle it in ensure. + # Otherwise we handle it in rescue. ensure is always + # above a rescue. This logic is about tracking if a given + # ensure node should expect a rescue-else inside a + # rescue node. + def on_ensure(node) + pushing([@rescue_else_stack, node]) { super } + end + + def on_rescue(node) + if node.children[1..-1].detect { |sexp| sexp && sexp.type != :resbody } + tracking(:rescue_else, @rescue_else_stack) + end + + pushing([@rescue_else_stack, nil], [@retry_stack, node]) { super } + end + + private + + def pushing(*stacks) + stacks.each { |stack, node| stack.push(node) } + result = yield + stacks.map(&:first).each(&:pop) + result + end + + def tracking(breaker, stack) + stack.last.meta[:"has_#{breaker}"] = true if stack.last + end + end + end +end +# rubocop:enable Layout/EmptyLineBetweenDefs, Style/SingleLineMethods diff --git a/opal/corelib/enumerator/generator.rb b/opal/corelib/enumerator/generator.rb index f85ee79e27..f9d5e2403e 100644 --- a/opal/corelib/enumerator/generator.rb +++ b/opal/corelib/enumerator/generator.rb @@ -1,4 +1,4 @@ -# helpers: breaker, deny_frozen_access +# helpers: deny_frozen_access class Enumerator class Generator @@ -22,8 +22,8 @@ def each(*args, &block) Opal.yieldX(#{@block}, args); } catch (e) { - if (e === $breaker) { - return $breaker.$v; + if (e && e.$thrower_type == "breaker") { + return e.$v; } else { throw e; diff --git a/opal/corelib/enumerator/yielder.rb b/opal/corelib/enumerator/yielder.rb index 8cf35f8beb..760111198b 100644 --- a/opal/corelib/enumerator/yielder.rb +++ b/opal/corelib/enumerator/yielder.rb @@ -1,5 +1,3 @@ -# helpers: breaker - class Enumerator class Yielder def initialize(&block) @@ -13,8 +11,8 @@ def yield(*values) %x{ var value = Opal.yieldX(#{@block}, values); - if (value === $breaker) { - throw $breaker; + if (value && value.$thrower_type == "break") { + throw value; } return value; diff --git a/opal/corelib/module.rb b/opal/corelib/module.rb index 509f7d2c1d..7d5b97150c 100644 --- a/opal/corelib/module.rb +++ b/opal/corelib/module.rb @@ -409,6 +409,9 @@ def define_method(name, method = undefined, &block) target.$$jsid = name; try { return target.apply(self, args); + } catch(e) { + if (e === target.$$brk || e === target.$$ret) return e.$v; + throw e; } finally { target.$$jsid = old_name } diff --git a/opal/corelib/proc.rb b/opal/corelib/proc.rb index 0d895845ee..4556c88b85 100644 --- a/opal/corelib/proc.rb +++ b/opal/corelib/proc.rb @@ -18,9 +18,9 @@ def call(*args, &block) self.$$p = block; } - var result, $brk = self.$$brk; + var result, $brk = self.$$brk, $ret = self.$$ret; - if ($brk) { + if ($brk || ($ret && self.$$is_lambda)) { try { if (self.$$is_lambda) { result = self.apply(null, args); @@ -30,10 +30,13 @@ def call(*args, &block) } } catch (err) { if (err === $brk) { - return $brk.$v + return err.$v; + } + else if (self.$$is_lambda && err === $ret) { + return err.$v; } else { - throw err + throw err; } } } diff --git a/opal/corelib/runtime.js b/opal/corelib/runtime.js index 853e86e790..7307064f59 100644 --- a/opal/corelib/runtime.js +++ b/opal/corelib/runtime.js @@ -1624,30 +1624,6 @@ // @deprecated Opal.find_iter_super_dispatcher = Opal.find_block_super; - // Used to return as an expression. Sometimes, we can't simply return from - // a javascript function as if we were a method, as the return is used as - // an expression, or even inside a block which must "return" to the outer - // method. This helper simply throws an error which is then caught by the - // method. This approach is expensive, so it is only used when absolutely - // needed. - // - Opal.ret = function(val) { - Opal.returner.$v = val; - throw Opal.returner; - }; - - // Used to break out of a block. - Opal.brk = function(val, breaker) { - breaker.$v = val; - throw breaker; - }; - - // Builds a new unique breaker, this is to avoid multiple nested breaks to get - // in the way of each other. - Opal.new_brk = function() { - return new Error('unexpected break'); - }; - // handles yield calls for 1 yielded arg Opal.yield1 = function(block, arg) { if (typeof(block) !== "function") { @@ -2989,9 +2965,19 @@ nil.$$comparable = false; Object.seal(nil); - // Errors - Opal.breaker = new Error('unexpected break (old)'); - Opal.returner = new Error('unexpected return'); + Opal.thrower = function(type) { + var thrower = new Error('unexpected '+type); + thrower.$thrower_type = type; + thrower.$throw = function(value) { + if (value == null) value = nil; + thrower.$v = value; + throw thrower; + }; + return thrower; + }; + + Opal.t_eval_return = Opal.thrower("return"); + TypeError.$$super = Error; // If enable-file-source-embed compiler option is enabled, each module loaded will add its diff --git a/spec/filters/bugs/kernel.rb b/spec/filters/bugs/kernel.rb index 834c6d58ed..848244d5be 100644 --- a/spec/filters/bugs/kernel.rb +++ b/spec/filters/bugs/kernel.rb @@ -316,10 +316,8 @@ fails "Kernel.global_variables finds subset starting with std" fails "Kernel.lambda does not create lambda-style Procs when captured with #method" # Expected true to be false fails "Kernel.lambda raises an ArgumentError when no block is given" - fails "Kernel.lambda returns from the lambda itself, not the creation site of the lambda" fails "Kernel.lambda returns the passed Proc if given an existing Proc through super" # Expected true to be false fails "Kernel.lambda returns the passed Proc if given an existing Proc" # Expected true to be false - fails "Kernel.lambda treats the block as a Proc when lambda is re-defined" # Expected 2 == 1 to be truthy but was false fails "Kernel.loop returns StopIteration#result, the result value of a finished iterator" # requires changes in enumerator.rb fails "Kernel.printf calls write on the first argument when it is not a string" fails "Kernel.printf formatting io is not specified other formats s preserves encoding of the format string" # Expected # == # to be truthy but was false diff --git a/spec/filters/bugs/language.rb b/spec/filters/bugs/language.rb index f58215d97d..8af87e7cff 100644 --- a/spec/filters/bugs/language.rb +++ b/spec/filters/bugs/language.rb @@ -69,7 +69,6 @@ fails "An instance method with a default argument prefers to assign to a default argument before a splat argument" # ArgumentError: [MSpecEnv#foo] wrong number of arguments(0 for -2) fails "Assigning an anonymous module to a constant sets the name of a module scoped by an anonymous module" # NoMethodError: undefined method `end_with?' for nil fails "Executing break from within a block raises LocalJumpError when converted into a proc during a a super call" # Expected LocalJumpError but no exception was raised (1 was returned) - fails "Executing break from within a block returns from the original invoking method even in case of chained calls" fails "Executing break from within a block works when passing through a super call" # Expected to not get Exception fails "Execution variable $: is initialized to an array of strings" fails "Execution variable $: is read-only" @@ -248,15 +247,6 @@ fails "The break statement in a lambda created at the toplevel returns a value when invoking from a block" fails "The break statement in a lambda created at the toplevel returns a value when invoking from a method" fails "The break statement in a lambda created at the toplevel returns a value when invoking from the toplevel" - fails "The break statement in a lambda from a scope that has returned raises a LocalJumpError when yielding to a lambda passed as a block argument" - fails "The break statement in a lambda from a scope that has returned returns a value to the block scope invoking the lambda in a method" # Exception: $brk is not defined - fails "The break statement in a lambda from a scope that has returned returns a value to the method scope invoking the lambda" # Exception: $brk is not defined - fails "The break statement in a lambda returns from the call site if the lambda is passed as a block" # Expected ["before", "unreachable1", "unreachable2", "after"] to equal ["before", "after"] - fails "The break statement in a lambda when the invocation of the scope creating the lambda is still active returns a value to a block scope invoking the lambda in a method below" # Exception: $brk is not defined - fails "The break statement in a lambda when the invocation of the scope creating the lambda is still active returns a value to the method scope below invoking the lambda" # Exception: $brk is not defined - fails "The break statement in a lambda when the invocation of the scope creating the lambda is still active returns a value to the scope creating and calling the lambda" # Exception: $brk is not defined - fails "The break statement in a lambda when the invocation of the scope creating the lambda is still active returns from the lambda" # Exception: unexpected break - fails "The break statement in a lambda when the invocation of the scope creating the lambda is still active returns nil when not passed an argument" # Exception: $brk is not defined fails "The class keyword does not raise a SyntaxError when opening a class without a semicolon" # NameError: uninitialized constant ClassSpecsKeywordWithoutSemicolon fails "The def keyword within a closure looks outside the closure for the visibility" fails "The defined? keyword for a scoped constant returns nil when a constant is defined on top-level but not on the class" # Expected "constant" to be nil @@ -280,7 +270,6 @@ fails "The defined? keyword when called with a method name without a receiver returns 'method' if the method is defined" # Expected false == true to be truthy but was false fails "The if expression with a boolean range ('flip-flop' operator) evaluates the first conditions lazily with exclusive-end range" fails "The if expression with a boolean range ('flip-flop' operator) evaluates the first conditions lazily with inclusive-end range" - fails "The or operator has a lower precedence than 'next' in 'next true or false'" fails "The redo statement in a method is invalid and raises a SyntaxError" # Expected SyntaxError but no exception was raised ("m" was returned) fails "The redo statement triggers ensure block when re-executing a block" fails "The rescue keyword allows rescue in 'do end' block" # NoMethodError: undefined method `call' for nil @@ -294,11 +283,6 @@ fails "The super keyword uses given block even if arguments are passed explicitly" fails "The throw keyword raises an UncaughtThrowError if used to exit a thread" # NotImplementedError: Thread creation not available fails "The unpacking splat operator (*) when applied to a BasicObject coerces it to Array if it respond_to?(:to_a)" # NoMethodError: undefined method `respond_to?' for BasicObject - fails "The until expression restarts the current iteration without reevaluating condition with redo" - fails "The until modifier restarts the current iteration without reevaluating condition with redo" - fails "The until modifier with begin .. end block restart the current iteration without reevaluating condition with redo" # Expected [1] to equal [0, 0, 0, 1, 2] - fails "The while expression stops running body if interrupted by break in a begin ... end element op-assign value" - fails "The while expression stops running body if interrupted by break in a parenthesized element op-assign value" fails "The yield call taking a single argument yielding to a lambda should not destructure an Array into multiple arguments" # Expected ArgumentError but no exception was raised ([1, 2] was returned) fails "The yield call taking no arguments ignores assignment to the explicit block argument and calls the passed block" fails "Using yield in a singleton class literal raises a SyntaxError" # Expected SyntaxError (/Invalid yield/) but got: SyntaxError (undefined method `uses_block!' for nil) @@ -314,8 +298,4 @@ fails_badly "Pattern matching refinements are used for #=== in constant pattern" fails_badly "Pattern matching refinements are used for #deconstruct" fails_badly "Pattern matching refinements are used for #deconstruct_keys" - fails_badly "The while expression stops running body if interrupted by break in a begin ... end attribute op-assign-or value" - fails_badly "The while expression stops running body if interrupted by break in a parenthesized attribute op-assign-or value" - fails_badly "The while expression stops running body if interrupted by break with unless in a begin ... end attribute op-assign-or value" - fails_badly "The while expression stops running body if interrupted by break with unless in a parenthesized attribute op-assign-or value" end diff --git a/spec/lib/compiler_spec.rb b/spec/lib/compiler_spec.rb index 92f9632fa6..4ddf62563c 100644 --- a/spec/lib/compiler_spec.rb +++ b/spec/lib/compiler_spec.rb @@ -479,7 +479,7 @@ def self.exist? path def expect_number_of_warnings(code) warnings_number = 0 - compiler = Opal::Compiler.new(code) + compiler = Opal::Compiler.new(code, eval: true) allow(compiler).to receive(:warning) { warnings_number += 1} compiler.compile expect(warnings_number) diff --git a/stdlib/opal-parser.rb b/stdlib/opal-parser.rb index 248a0929be..814f84c3fb 100644 --- a/stdlib/opal-parser.rb +++ b/stdlib/opal-parser.rb @@ -50,7 +50,7 @@ def require_remote(url) }; Opal['eval'] = function(str, options) { - return eval(Opal.compile(str, options)); + return eval(Opal.compile(str, options)); }; function run_ruby_scripts() { diff --git a/tasks/testing/mspec_special_calls.rb b/tasks/testing/mspec_special_calls.rb index 8316572163..2179af695c 100644 --- a/tasks/testing/mspec_special_calls.rb +++ b/tasks/testing/mspec_special_calls.rb @@ -35,6 +35,7 @@ class Opal::Nodes::CallNode end end +require 'opal/rewriter' require 'opal/rewriters/rubyspec/filters_rewriter' Opal::Rewriter.use Opal::Rubyspec::FiltersRewriter