Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Syntax: special handling of tuple condition in case expression #2258

Merged
merged 2 commits into from
Mar 2, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion spec/compiler/normalize/case_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe "Normalize: case" do
end

it "normalizes case with many expressions in when" do
assert_expand_second "x = 1; case x; when 1, 2; 'b'; end", "if 1 === x || 2 === x\n 'b'\nend"
assert_expand_second "x = 1; case x; when 1, 2; 'b'; end", "if (1 === x) || (2 === x)\n 'b'\nend"
end

it "normalizes case with implicit call" do
Expand All @@ -48,4 +48,40 @@ describe "Normalize: case" do
it "normalizes case with nil to is_a?" do
assert_expand_second "x = 1; case x; when nil; 'b'; end", "if x.is_a?(::Nil)\n 'b'\nend"
end

it "normalizes case with multiple expressions" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}; 4; end", "if (2 === x) && (3 === y)\n 4\nend"
end

it "normalizes case with multiple expressions and types" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {Int32, Float64}; 4; end", "if (x.is_a?(Int32)) && (y.is_a?(Float64))\n 4\nend"
end

it "normalizes case with multiple expressions and implicit obj" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {.foo, .bar}; 4; end", "if x.foo && y.bar\n 4\nend"
end

it "normalizes case with multiple expressions and comma" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {2, 3}, {4, 5}; 6; end", "if ((2 === x) && (3 === y)) || ((4 === x) && (5 === y))\n 6\nend"
end

it "normalizes case with multiple expressions with underscore" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {2, _}; 4; end", "if 2 === x\n 4\nend"
end

it "normalizes case with multiple expressions with all underscores" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}; 4; end", "if true\n 4\nend"
end

it "normalizes case with multiple expressions with all underscores twice" do
assert_expand_second "x, y = 1, 2; case {x, y}; when {_, _}, {_, _}; 4; end", "if true\n 4\nend"
end

it "normalizes case with multiple expressions and non-tuple" do
assert_expand_second "x, y = 1, 2; case {x, y}; when 1; 4; end", "if 1 === ({x, y})\n 4\nend"
end

it "normalizes case with single expressions with underscore" do
assert_expand_second "x = 1; case x; when _; 2; end", "if true\n 2\nend"
end
end
10 changes: 5 additions & 5 deletions spec/compiler/normalize/chained_comparisons_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,22 @@ require "../../spec_helper"

describe "Normalize: chained comparisons" do
it "normalizes one comparison with literal" do
assert_normalize "1 <= 2 <= 3", "1 <= 2 && 2 <= 3"
assert_normalize "1 <= 2 <= 3", "(1 <= 2) && (2 <= 3)"
end

it "normalizes one comparison with var" do
assert_normalize "b = 1; 1 <= b <= 3", "b = 1\n1 <= b && b <= 3"
assert_normalize "b = 1; 1 <= b <= 3", "b = 1\n(1 <= b) && (b <= 3)"
end

it "normalizes one comparison with call" do
assert_normalize "1 <= b <= 3", "1 <= (__temp_1 = b) && __temp_1 <= 3"
assert_normalize "1 <= b <= 3", "(1 <= (__temp_1 = b)) && (__temp_1 <= 3)"
end

it "normalizes two comparisons with literal" do
assert_normalize "1 <= 2 <= 3 <= 4", "1 <= 2 && 2 <= 3 && 3 <= 4"
assert_normalize "1 <= 2 <= 3 <= 4", "((1 <= 2) && (2 <= 3)) && (3 <= 4)"
end

it "normalizes two comparisons with calls" do
assert_normalize "1 <= a <= b <= 4", "1 <= (__temp_2 = a) && __temp_2 <= (__temp_1 = b) && __temp_1 <= 4"
assert_normalize "1 <= a <= b <= 4", "((1 <= (__temp_2 = a)) && (__temp_2 <= (__temp_1 = b))) && (__temp_1 <= 4)"
end
end
5 changes: 5 additions & 0 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -859,6 +859,11 @@ describe "Parser" do
it_parses "case 1\nwhen .foo\n2\nend", Case.new(1.int32, [When.new([Call.new(ImplicitObj.new, "foo")] of ASTNode, 2.int32)])
it_parses "case when 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)])
it_parses "case \nwhen 1\n2\nend", Case.new(nil, [When.new([1.int32] of ASTNode, 2.int32)])
it_parses "case {1, 2}\nwhen {3, 4}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode)] of ASTNode, 5.int32)])
it_parses "case {1, 2}\nwhen {3, 4}, {5, 6}\n7\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([3.int32, 4.int32] of ASTNode), TupleLiteral.new([5.int32, 6.int32] of ASTNode)] of ASTNode, 7.int32)])
it_parses "case {1, 2}\nwhen {.foo, .bar}\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new([TupleLiteral.new([Call.new(ImplicitObj.new, "foo"), Call.new(ImplicitObj.new, "bar")] of ASTNode)] of ASTNode, 5.int32)])
it_parses "case {1, 2}\nwhen foo\n5\nend", Case.new(TupleLiteral.new([1.int32, 2.int32] of ASTNode), [When.new(["foo".call] of ASTNode, 5.int32)])
assert_syntax_error "case {1, 2}; when {3}; 4; end", "wrong number of tuple elements (given 1, expected 2)", 1, 19

it_parses "def foo(x); end; x", [Def.new("foo", ["x".arg]), "x".call]
it_parses "def foo; / /; end", Def.new("foo", body: regex(" "))
Expand Down
8 changes: 8 additions & 0 deletions spec/std/tuple_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -219,4 +219,12 @@ describe "Tuple" do

Tuple.new.last?.should be_nil
end

it "does ===" do
({1, 2} === {1, 2}).should be_true
({1, 2} === {1, 3}).should be_false
({1, 2, 3} === {1, 2}).should be_false
({/o+/, "bar"} === {"fox", "bar"}).should be_true
({1, 2} === nil).should be_false
end
end
75 changes: 63 additions & 12 deletions src/compiler/crystal/semantic/literal_expander.cr
Original file line number Diff line number Diff line change
Expand Up @@ -345,18 +345,43 @@ module Crystal
# if temp.is_a?(Bar)
# 1
# end
#
# We also take care to expand multiple conds
#
# From:
#
# case {x, y}
# when {1, 2}, {3, 4}
# 3
# end
#
# To:
#
# if (1 === x && y === 2) || (3 === x && 4 === y)
# 3
# end
def expand(node : Case)
node_cond = node.cond
if node_cond
case node_cond
when Var, InstanceVar
temp_var = node.cond
when Assign
temp_var = node_cond.target
assign = node_cond
if node_cond.is_a?(TupleLiteral)
conds = node_cond.elements
else
temp_var = new_temp_var
assign = Assign.new(temp_var.clone, node_cond)
conds = [node_cond]
end

assigns = [] of ASTNode
temp_vars = conds.map do |cond|
case cond
when Var, InstanceVar
temp_var = cond
when Assign
temp_var = cond.target
assigns << cond
else
temp_var = new_temp_var
assigns << Assign.new(temp_var.clone, cond)
end
temp_var
end
end

Expand All @@ -365,7 +390,30 @@ module Crystal
node.whens.each do |wh|
final_comp = nil
wh.conds.each do |cond|
comp = case_when_comparison(temp_var, cond).at(cond)
next if cond.is_a?(Underscore)

if node_cond.is_a?(TupleLiteral)
if cond.is_a?(TupleLiteral)
comp = nil
cond.elements.zip(temp_vars.not_nil!) do |lh, rh|
next if lh.is_a?(Underscore)

sub_comp = case_when_comparison(rh, lh).at(cond)
if comp
comp = And.new(comp, sub_comp)
else
comp = sub_comp
end
end
else
comp = case_when_comparison(TupleLiteral.new(temp_vars.not_nil!.clone), cond)
end
else
temp_var = temp_vars.try &.first
comp = case_when_comparison(temp_var, cond).at(cond)
end

next unless comp

if final_comp
final_comp = Or.new(final_comp, comp)
Expand All @@ -374,7 +422,9 @@ module Crystal
end
end

wh_if = If.new(final_comp.not_nil!, wh.body)
final_comp ||= BoolLiteral.new(true)

wh_if = If.new(final_comp, wh.body)
if a_if
a_if.else = wh_if
else
Expand All @@ -388,8 +438,9 @@ module Crystal
end

final_if = final_if.not_nil!
final_exp = if assign
Expressions.new([assign, final_if] of ASTNode)
final_exp = if assigns && !assigns.empty?
assigns << final_if
Expressions.new(assigns)
else
final_if
end
Expand Down
95 changes: 71 additions & 24 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2113,33 +2113,47 @@ module Crystal
slash_is_regex!
next_token_skip_space_or_newline
when_conds = [] of ASTNode
while true
if cond && @token.type == :"."
next_token
call = parse_var_or_call(force_call: true) as Call
call.obj = ImplicitObj.new
when_conds << call
else
when_conds << parse_op_assign_no_control
end
skip_space
if @token.keyword?(:then)
next_token_skip_space_or_newline
break
else
slash_is_regex!
case @token.type
when :","

if cond.is_a?(TupleLiteral)
while true
if @token.type == :"{"
curly_location = @token.location

next_token_skip_space_or_newline
when :NEWLINE
skip_space_or_newline
break
when :";"
skip_statement_end
break

tuple_elements = [] of ASTNode

while true
tuple_elements << parse_when_expression(cond)
skip_space
if @token.type == :","
next_token_skip_space_or_newline
else
break
end
end

if tuple_elements.size != cond.elements.size
raise "wrong number of tuple elements (given #{tuple_elements.size}, expected #{cond.elements.size})", curly_location
end

tuple = TupleLiteral.new(tuple_elements)
when_conds << tuple

check :"}"
next_token_skip_space
else
unexpected_token @token.to_s, "expecting ',', ';' or '\n'"
when_conds << parse_when_expression(cond)
skip_space
end

break if when_expression_end
end
else
while true
when_conds << parse_when_expression(cond)
skip_space
break if when_expression_end
end
end

Expand Down Expand Up @@ -2175,6 +2189,39 @@ module Crystal
Case.new(cond, whens, a_else)
end

def when_expression_end
if @token.keyword?(:then)
next_token_skip_space_or_newline
return true
else
slash_is_regex!
case @token.type
when :","
next_token_skip_space_or_newline
when :NEWLINE
skip_space_or_newline
return true
when :";"
skip_statement_end
return true
else
unexpected_token @token.to_s, "expecting ',', ';' or '\n'"
end
end
false
end

def parse_when_expression(cond)
if cond && @token.type == :"."
next_token
call = parse_var_or_call(force_call: true) as Call
call.obj = ImplicitObj.new
call
else
parse_op_assign_no_control
end
end

def parse_include
parse_include_or_extend Include
end
Expand Down
4 changes: 2 additions & 2 deletions src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
Expand Up @@ -957,14 +957,14 @@ module Crystal
end

def to_s_binary(node, op)
left_needs_parens = node.left.is_a?(Assign) || node.left.is_a?(Expressions)
left_needs_parens = need_parens(node.left)
in_parenthesis(left_needs_parens, node.left)

@str << " "
@str << op
@str << " "

right_needs_parens = node.right.is_a?(Assign) || node.right.is_a?(Expressions)
right_needs_parens = need_parens(node.right)
in_parenthesis(right_needs_parens, node.right)
false
end
Expand Down
33 changes: 33 additions & 0 deletions src/tuple.cr
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,39 @@ struct Tuple
true
end

# Returns `true` if case equality holds for the elements in `self` and *other*.
#
# ```
# {1, 2} === {1, 2} # => true
# {1, 2} === {1, 3} # => false
# ```
#
# See `Object#===`
def ===(other : self)
{% for i in [email protected] %}
return false unless self[{{i}}] === other[{{i}}]
{% end %}
true
end

# Returns `true` if `self` and *other* have the same size and case equality holds
# for the elements in `self` and *other*.
#
# ```
# {1, 2} === {1, 2, 3} # => false
# {/o+/, "bar"} === {"foo", "bar"} # => true
# ```
#
# See `Object#===`
def ===(other : Tuple)
return false unless size == other.size

size.times do |i|
return false unless self[i] === other[i]
end
true
end

# Implements the comparison operator.
#
# Each object in each tuple is compared (using the <=> operator).
Expand Down