lib/opal/nodes/if.rb
# 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