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: allow multiple expressions in a case condition #2239

Closed
wants to merge 1 commit into from
Closed
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
1 change: 1 addition & 0 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ describe Crystal::Formatter do
assert_format "case 1 \n when .foo? \n 3 \n end", "case 1\nwhen .foo?\n 3\nend"
assert_format "case 1\nwhen 1 then\n2\nwhen 3\n4\nend", "case 1\nwhen 1\n 2\nwhen 3\n 4\nend"
assert_format "case 1 \n when 2 \n 3 \n else 4 \n end", "case 1\nwhen 2\n 3\nelse 4\nend"
assert_format "case 1 , 2 \n when 3 , 4 \n 5 \n end", "case 1, 2\nwhen 3, 4\n 5\nend"

assert_format "foo.@bar"

Expand Down
4 changes: 2 additions & 2 deletions spec/compiler/macro/macro_methods_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -741,10 +741,10 @@ describe "macro methods" do
end

describe "case methods" do
case_node = Case.new(NumberLiteral.new(1), [When.new([NumberLiteral.new(2), NumberLiteral.new(3)] of ASTNode, NumberLiteral.new(4))], NumberLiteral.new(5))
case_node = Case.new([NumberLiteral.new(1)] of ASTNode, [When.new([NumberLiteral.new(2), NumberLiteral.new(3)] of ASTNode, NumberLiteral.new(4))], NumberLiteral.new(5))

it "executes cond" do
assert_macro "x", %({{x.cond}}), [case_node] of ASTNode, "1"
assert_macro "x", %({{x.conds}}), [case_node] of ASTNode, "[1]"
end

it "executes whens" do
Expand Down
16 changes: 16 additions & 0 deletions spec/compiler/normalize/case_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -48,4 +48,20 @@ 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 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 single expressions with underscore" do
assert_expand_second "x = 1; case x; when _; 2; end", "if true\n 2\nend"
end
end
28 changes: 16 additions & 12 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -844,21 +844,25 @@ describe "Parser" do
it_parses "require \"foo\"", Require.new("foo")
it_parses "require \"foo\"; [1]", [Require.new("foo"), ([1.int32] of ASTNode).array]

it_parses "case 1; when 1; 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1; when 0, 1; 2; else; 3; end", Case.new(1.int32, [When.new([0.int32, 1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nelse\n3\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nend", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)])
it_parses "case / /; when / /; / /; else; / /; end", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "))
it_parses "case / /\nwhen / /\n/ /\nelse\n/ /\nend", Case.new(regex(" "), [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "))

it_parses "case 1; when 1 then 2; else; 3; end", Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)]
it_parses "case\n1\nwhen 1\n2\nend\nif a\nend", [Case.new(1.int32, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)]

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 1; when 1; 2; else; 3; end", Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1; when 0, 1; 2; else; 3; end", Case.new([1.int32] of ASTNode, [When.new([0.int32, 1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nelse\n3\nend", Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nend", Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)])
it_parses "case / /; when / /; / /; else; / /; end", Case.new([regex(" ")] of ASTNode, [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "))
it_parses "case / /\nwhen / /\n/ /\nelse\n/ /\nend", Case.new([regex(" ")] of ASTNode, [When.new([regex(" ")] of ASTNode, regex(" "))], regex(" "))

it_parses "case 1; when 1 then 2; else; 3; end", Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)], 3.int32)
it_parses "case 1\nwhen 1\n2\nend\nif a\nend", [Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)]
it_parses "case\n1\nwhen 1\n2\nend\nif a\nend", [Case.new([1.int32] of ASTNode, [When.new([1.int32] of ASTNode, 2.int32)]), If.new("a".call)]

it_parses "case 1\nwhen .foo\n2\nend", Case.new([1.int32] of ASTNode, [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, 3; when 4, 5, 6; 7; else; 8; end", Case.new([1.int32, 2.int32, 3.int32] of ASTNode, [When.new([4.int32, 5.int32, 6.int32] of ASTNode, 7.int32)], 8.int32)
it_parses "case 1; when _; 2; end", Case.new([1.int32] of ASTNode, [When.new([Underscore.new] of ASTNode, 2.int32)])
assert_syntax_error "case 1, 2; when 3; 4; end", "wrong number of when expressions (given 1, expected 2)", 1, 12

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
10 changes: 8 additions & 2 deletions src/compiler/crystal/macros/methods.cr
Original file line number Diff line number Diff line change
Expand Up @@ -969,8 +969,14 @@ module Crystal
class Case
def interpret(method, args, block, interpreter)
case method
when "cond"
interpret_argless_method(method, args) { cond || Nop.new }
when "conds"
interpret_argless_method(method, args) do
if conds = self.conds
ArrayLiteral.new(conds)
else
Nop.new
end
end
when "whens"
interpret_argless_method(method, args) { ArrayLiteral.map whens, &.itself }
when "else"
Expand Down
64 changes: 48 additions & 16 deletions src/compiler/crystal/semantic/literal_expander.cr
Original file line number Diff line number Diff line change
Expand Up @@ -345,36 +345,67 @@ 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
# end
#
# To:
#
# if 1 === x && y === 2
# 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
else
temp_var = new_temp_var
assign = Assign.new(temp_var.clone, node_cond)
conds = node.conds
multi = false

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

a_if = nil
final_if = nil
node.whens.each do |wh|
final_comp = nil
wh.conds.each do |cond|
wh.conds.each_with_index do |cond, i|
next if cond.is_a?(Underscore)

temp_var = temp_vars.try &.[multi ? i : 0]
comp = case_when_comparison(temp_var, cond).at(cond)

if final_comp
final_comp = Or.new(final_comp, comp)
final_comp = if multi
And.new(final_comp, comp)
else
Or.new(final_comp, comp)
end
else
final_comp = comp
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 +419,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
9 changes: 5 additions & 4 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1131,23 +1131,24 @@ module Crystal
end

class Case < ASTNode
property :cond
property conds : Array(ASTNode)?
property :whens
property :else

def initialize(@cond, @whens, @else = nil)
def initialize(@conds : Array(ASTNode)?, @whens, @else = nil)
end

def accept_children(visitor)
@conds.try &.each &.accept visitor
@whens.each &.accept visitor
@else.try &.accept visitor
end

def clone_without_location
Case.new(@cond.clone, @whens.clone, @else.clone)
Case.new(@conds.clone, @whens.clone, @else.clone)
end

def_equals_and_hash @cond, @whens, @else
def_equals_and_hash @conds, @whens, @else
end

# Node that represents an implicit obj in:
Expand Down
28 changes: 25 additions & 3 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2097,8 +2097,22 @@ module Crystal
def parse_case
slash_is_regex!
next_token_skip_space_or_newline

conds = nil

unless @token.keyword?(:when)
cond = parse_op_assign_no_control
conds = [] of ASTNode

while true
conds << parse_op_assign_no_control
skip_space
if @token.type == :","
next_token_skip_space_or_newline
else
break
end
end

skip_statement_end
end

Expand All @@ -2110,15 +2124,19 @@ module Crystal
when :IDENT
case @token.value
when :when
when_location = @token.location

slash_is_regex!
next_token_skip_space_or_newline
when_conds = [] of ASTNode
while true
if cond && @token.type == :"."
if conds && @token.type == :"."
next_token
call = parse_var_or_call(force_call: true) as Call
call.obj = ImplicitObj.new
when_conds << call
elsif conds && @token.type == :UNDERSCORE
when_conds << node_and_next_token(Underscore.new)
else
when_conds << parse_op_assign_no_control
end
Expand All @@ -2143,6 +2161,10 @@ module Crystal
end
end

if conds && conds.size > 1 && conds.size != when_conds.size
raise "wrong number of when expressions (given #{when_conds.size}, expected #{conds.size})", when_location
end

slash_is_regex!
when_body = parse_expressions
skip_space_or_newline
Expand Down Expand Up @@ -2172,7 +2194,7 @@ module Crystal
end
end

Case.new(cond, whens, a_else)
Case.new(conds, whens, a_else)
end

def parse_include
Expand Down
7 changes: 5 additions & 2 deletions src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1145,9 +1145,12 @@ module Crystal

def visit(node : Case)
@str << keyword("case")
if cond = node.cond
if conds = node.conds
@str << " "
cond.accept self
conds.each_with_index do |cond, i|
@str << ", " if i > 0
cond.accept self
end
end
newline
node.whens.each do |wh|
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/syntax/transformer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ module Crystal
end

def transform(node : Case)
node.cond = node.cond.try &.transform(self)
transform_many node.conds
transform_many node.whens

if node_else = node.else
Expand Down
13 changes: 11 additions & 2 deletions src/compiler/crystal/tools/formatter.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2648,9 +2648,18 @@ module Crystal
write_keyword :case
skip_space

if cond = node.cond
if conds = node.conds
write " "
accept cond
conds.each_with_index do |cond, i|
accept cond
unless last?(i, conds)
skip_space
if @token.type == :","
write ", "
next_token_skip_space_or_newline
end
end
end
end

skip_space_write_line
Expand Down