Skip to content

Commit

Permalink
Merge pull request #2258 from crystal-lang/feature/case_tuple_cond
Browse files Browse the repository at this point in the history
Syntax: special handling of tuple condition in case expression
  • Loading branch information
Ary Borenszweig committed Mar 2, 2016
2 parents 6325463 + 272e293 commit 30a1bee
Show file tree
Hide file tree
Showing 8 changed files with 224 additions and 44 deletions.
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 0...@type.size %}
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

0 comments on commit 30a1bee

Please sign in to comment.