# frozen_string_literal: true require 'opal/nodes/base' require 'opal/ast/matcher' module Opal module Nodes class IfNode < Base handle :if children :test, :true_body, :false_body def compile if should_compile_as_simple_expression? if true_body == s(:true) compile_with_binary_or elsif false_body == s(:false) compile_with_binary_and else compile_with_ternary end elsif could_become_switch? compile_with_switch else compile_with_if end end def compile_with_if push_closure if expects_expression? truthy = self.truthy falsy = self.falsy if falsy && !truthy # Let's optimize a little bit `unless` calls. push 'if (!', js_truthy(test), ') {' falsy, truthy = truthy, falsy else push 'if (', js_truthy(test), ') {' end # skip if-body if no truthy sexp indent { line stmt(truthy) } if truthy if falsy if falsy.type == :if line '} else ', stmt(falsy) else line '} else {' indent do line stmt(falsy) end line '}' end else line '}' # This resolution isn't finite. Let's ensure this block # always return something if we expect a return line 'return nil;' if expects_expression? end pop_closure if expects_expression? if expects_expression? return_kw = 'return ' if returning_if? if scope.await_encountered wrap "#{return_kw}(await (async function() {", '})())' else wrap "#{return_kw}(function() {", '})()' end end end def returning_if? @sexp.meta[:returning] end def truthy returnify(true_body) end def falsy returnify(false_body) end def returnify(body) if expects_expression? && body compiler.returns(body) else body end end def expects_expression? expr? || recv? end # There was a particular case in the past, that when we # expected an expression from if, we always had to closure # it. This produced an ugly code that was hard to minify. # This addition tries to make a few cases compiled with # a ternary operator instead and possibly a binary operator # even? def should_compile_as_simple_expression? expects_expression? && simple?(true_body) && simple?(false_body) end def compile_with_ternary truthy = true_body falsy = false_body push '(' push js_truthy(test), ' ? ' push '(', expr(truthy || s(:nil)), ') : ' if !falsy || falsy.type == :if push expr(falsy || s(:nil)) else push '(', expr(falsy || s(:nil)), ')' end push ')' end def compile_with_binary_and if sexp.meta[:do_js_truthy_on_true_body] truthy = js_truthy(true_body || s(:nil)) else truthy = expr(true_body || s(:nil)) end push '(' push js_truthy(test), ' && ' push '(', truthy, ')' push ')' end def compile_with_binary_or if sexp.meta[:do_js_truthy_on_false_body] falsy = js_truthy(false_body || s(:nil)) else falsy = expr(false_body || s(:nil)) end push '(' push js_truthy(test), ' || ' push '(', falsy, ')' push ')' end # Let's ensure there are no control flow statements inside. def simple?(body) case body when AST::Node case body.type when :return, :js_return, :break, :next, :redo, :retry false when :xstr XStringNode.single_line?( XStringNode.strip_empty_children(body.children) ) else body.children.all? { |i| simple?(i) } end else true end end # NOTE: all following matcher will act on case/when statements in their rewitten form: # # bin/opal --sexp -e'case some_value_or_expression; when 123; when 456, 789; end' # # s(:top, # s(:if, # s(:send, # s(:int, 123), :===, # s(:lvasgn, "$ret_or_1", # s(:send, nil, :some_value_or_expression))), nil, # s(:if, # s(:if, # s(:send, # s(:int, 456), :===, # s(:js_tmp, "$ret_or_1")), # s(:true), # s(:send, # s(:int, 789), :===, # s(:js_tmp, "$ret_or_1"))), nil, # s(:nil)))) # # Matches: `case some_value_or_expression; when 123` # Captures: [s(:int, 123), "$ret_or_1", s(:send, nil, :some_value_or_expression))] SWITCH_TEST_MATCH = AST::Matcher.new do s(:send, cap(s(%i[float int sym str true false nil], :*)), :===, s(:lvasgn, cap(:*), cap(:*)) ) end # Matches: case some_value_or_expression; when 123, 456; end # Captures: [ # s(:int, 123), # "$ret_or_1", # s(:send, nil, :some_value_or_expression)), # …here we delegate to either SWITCH_BRANCH_TEST_MATCH or SWITCH_BRANCH_TEST_MATCH_CONTINUED # ] SWITCH_TEST_MATCH_CONTINUED = AST::Matcher.new do s(:if, s(:send, cap(s(%i[float int sym str true false nil], :*)), :===, s(:lvasgn, cap(:*), cap(:*)) ), s(:true), cap(:*) ) end # Matches: `when 456` (from `case foo; when 123; when 456; end`) # Captures: [s(:int, 456), "$ret_or_1"] SWITCH_BRANCH_TEST_MATCH = AST::Matcher.new do s(:send, cap(s(%i[float int sym str true false nil], :*)), :===, s(:js_tmp, cap(:*)) ) end # Matches: `when 456` # Captures: [ # s(:int, 789), # "$ret_or_1", # …here we delegate to either SWITCH_BRANCH_TEST_MATCH or SWITCH_BRANCH_TEST_MATCH_CONTINUED # ] SWITCH_BRANCH_TEST_MATCH_CONTINUED = AST::Matcher.new do s(:if, s(:send, cap(s(%i[float int sym str true false nil], :*)), :===, s(:js_tmp, cap(:*)) ), s(:true), cap(:*) ) end def could_become_switch? return false if expects_expression? return true if sexp.meta[:switch_child] test_match = SWITCH_TEST_MATCH.match(test) || SWITCH_TEST_MATCH_CONTINUED.match(test) return false unless test_match @switch_test, @switch_variable, @switch_first_test, additional_rules = *test_match additional_rules = handle_additional_switch_rules(additional_rules) return false unless additional_rules # It's ok for them to be empty, but false denotes a mismatch @switch_additional_rules = additional_rules return false unless valid_switch_body?(true_body) could_become_switch_branch?(false_body) end def handle_additional_switch_rules(additional_rules) switch_additional_rules = [] while additional_rules match = SWITCH_BRANCH_TEST_MATCH.match(additional_rules) || SWITCH_BRANCH_TEST_MATCH_CONTINUED.match(additional_rules) return false unless match switch_test, switch_variable, additional_rules = *match return false unless switch_variable == @switch_variable switch_additional_rules << switch_test end switch_additional_rules end def could_become_switch_branch?(body) if !body return true elsif body.type != :if if valid_switch_body?(body) body.meta[:switch_default] = true return true end return false end test, true_body, false_body = *body test_match = SWITCH_BRANCH_TEST_MATCH.match(test) || SWITCH_BRANCH_TEST_MATCH_CONTINUED.match(test) unless test_match if valid_switch_body?(body, true) body.meta[:switch_default] = true return true end end switch_test, switch_variable, additional_rules = *test_match switch_additional_rules = handle_additional_switch_rules(additional_rules) return false unless switch_additional_rules # It's ok for them to be empty, but false denotes a mismatch return false unless switch_variable == @switch_variable return false unless valid_switch_body?(true_body) return false unless could_become_switch_branch?(false_body) body.meta.merge!(switch_child: true, switch_test: switch_test, switch_variable: @switch_variable, switch_additional_rules: switch_additional_rules ) true end def valid_switch_body?(body, check_variable = false) case body when AST::Node case body.type when :break, :redo, :retry false when :iter, :while # Don't traverse the iters or whiles! true else body.children.all? { |i| valid_switch_body?(i, check_variable) } end when @switch_variable # Perhaps we ended abruptly and we lack a $ret_or variable... but sometimes # we can ignore this. !check_variable else true end end def compile_with_switch if sexp.meta[:switch_child] @switch_variable = sexp.meta[:switch_variable] @switch_additional_rules = sexp.meta[:switch_additional_rules] compile_switch_case(sexp.meta[:switch_test]) else line "switch (", expr(@switch_first_test), ".valueOf()) {" indent do compile_switch_case(@switch_test) end line "}" end end def returning?(body) %i[return js_return next].include?(body.type) || (body.type == :begin && %i[return js_return next].include?(body.children.last.type)) end def compile_switch_case(test) line "case ", expr(test), ":" if @switch_additional_rules @switch_additional_rules.each do |rule| line "case ", expr(rule), ":" end end indent do line stmt(true_body) line "break;" if !true_body || !returning?(true_body) end if false_body if false_body.meta[:switch_default] compile_switch_default elsif false_body.meta[:switch_child] push stmt(false_body) end else push stmt(s(:nil)) end end def compile_switch_default line "default:" indent do line stmt(false_body) end end end class BaseFlipFlop < Base children :start_condition, :end_condition def compile helper :truthy func_name = top_scope.new_temp flip_flop_state = "#{func_name}.$$ff" # Start function definition, checking and initializing it if necessary push "(#{func_name} = #{func_name} || function(_start_func, _end_func){" # If flip flop state is not defined, set it to false push " var flip_flop = #{flip_flop_state} || false;" # If flip flop state is false, call the 'start_condition' function and store its truthy result into flip flop state push " if (!flip_flop) #{flip_flop_state} = flip_flop = $truthy(_start_func());" # If flip flop state is true, call the 'end_condition' function and set flip flop state to false if 'end_condition' is truthy push " #{excl}if (flip_flop && $truthy(_end_func())) #{flip_flop_state} = false;" # Return current state of flip flop push " return flip_flop;" # End function definition push "})(" # Call the function with 'start_condition' and 'end_condition' arguments wrapped in functions to ensure correct binding and delay evaluation push " function() { ", stmt(compiler.returns(start_condition)), " }," push " function() { ", stmt(compiler.returns(end_condition)), " }" push ")" end end class IFlipFlop < BaseFlipFlop handle :iflipflop # Inclusive flip flop, check 'end_condition' in the same iteration when 'start_condition' is truthy def excl "" end end class EFlipFlop < BaseFlipFlop handle :eflipflop # Exclusive flip flop, check 'end_condition' in the next iteration after 'start_condition' is truthy def excl "else " end end end end