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

Add select keyword #3130

Merged
merged 1 commit into from
Aug 10, 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
2 changes: 1 addition & 1 deletion samples/channel_select.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ ch2 = generator(1.5)
ch3 = generator(5)

loop do
index, value = Channel.select(ch1.receive_op, ch2.receive_op, ch3.receive_op)
index, value = Channel.select(ch1.receive_select_action, ch2.receive_select_action, ch3.receive_select_action)
case index
when 0
int = value.as(typeof(ch1.receive))
Expand Down
7 changes: 7 additions & 0 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,13 @@ describe Crystal::Formatter do
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 "select \n when foo \n 2 \n end", "select\nwhen foo\n 2\nend"
assert_format "select \n when foo \n 2 \n when bar \n 3 \n end", "select\nwhen foo\n 2\nwhen bar\n 3\nend"
assert_format "select \n when foo then 2 \n end", "select\nwhen foo then 2\nend"
assert_format "select \n when foo ; 2 \n end", "select\nwhen foo; 2\nend"
assert_format "select \n when foo \n 2 \n else \n 3 \n end", "select\nwhen foo\n 2\nelse\n 3\nend"
assert_format "def foo\nselect \n when foo \n 2 \n else \n 3 \nend\nend", "def foo\n select\n when foo\n 2\n else\n 3\n end\nend"

assert_format "foo.@bar"

assert_format "@[Foo]"
Expand Down
2 changes: 1 addition & 1 deletion spec/compiler/lexer/lexer_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ describe "Lexer" do
it_lexes_keywords [:def, :if, :else, :elsif, :end, :true, :false, :class, :module, :include,
:extend, :while, :until, :nil, :do, :yield, :return, :unless, :next, :break,
:begin, :lib, :fun, :type, :struct, :union, :enum, :macro, :out, :require,
:case, :when, :then, :of, :abstract, :rescue, :ensure, :is_a?, :alias,
:case, :when, :select, :then, :of, :abstract, :rescue, :ensure, :is_a?, :alias,
:pointerof, :sizeof, :instance_sizeof, :ifdef, :as, :as?, :typeof, :for, :in,
:with, :self, :super, :private, :protected, :asm, :uninitialized, :nil?]
it_lexes_idents ["ident", "something", "with_underscores", "with_1", "foo?", "bar!", "fooBar",
Expand Down
68 changes: 68 additions & 0 deletions spec/compiler/normalize/select_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
require "../../spec_helper"

describe "Normalize: case" do
it "normalizes select with call" do
assert_expand "select; when foo; body; when bar; baz; end", <<-CODE
__temp_1, __temp_2 = ::Channel.select({foo_select_action, bar_select_action})
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_for_select or _action, which is it? :)

Not too sure a about either name yet, have to sleep over it.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, good catch. _for_select was our original choice, but _select_action matches the SelectAction "interface". We aren't sure about the name either, but since it's not going to be written manually...

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just conceptually, not referring to the existing Future, can't SelectAction be thought of as a promise/future? Channel.select receives a list of promises and returns the first that could compute a value. Following that line of thought, SelectActon becomes Promise and the suffix becomes _promise, and channel.receive_promise suddenly reads pretty well to me. Going further the whole concept could be decoupled from Channel and Channel.select could become Promise.select.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I must admit it sounds good, but on second thought this is not really a promise. It's one possible action that can be executed in a select branch, but it's not guaranteed that it will be executed.

Plus, adding more functionality to it is pretty hard, you have to know the runtime internals. I'd say it's extensible so we can add more cases in the standard library, but I wouldn't expect 3rd party libraries to extend this, at least not now. So I prefer it to keep it under Channel and with a name that explicitly states it's for a select expression, and only through a select expression (so the underlying names don't matter much)

Copy link
Member

@jhass jhass Aug 10, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I asked a bit around whether there are existing terms or concepts to this problem.

One interesting note was that it could be a modled as a priority queue with dynamic weights. The weight would denote the readiness state, the queue would hold the operations and pop once. We would need then need to implement a constrained pop that blocks until an element with a high enough weight is available. But all of this doesn't help much with the name for the containers in the queue.

The other note I received was that OCaml calls this "alternatives", or its formal parent CSP calls it "choice". https://en.wikipedia.org/wiki/Communicating_sequential_processes#Algebraic_operators
I think that could actually work here as a generalized construct, Choice.select, Channel#receive_choice, Channel#send_choice, Choice#ready?, Choice#execute, Choice#wait, Choice#unwait.

I'm not sure I follow you on the runtime internals, Choice would define the the interface with how the operations have to behave, things like "wait should ensure the current fiber is rescheduled when value of ready? changes" and "unwait should remove any reschedule setup by wait". We then just have to document Scheduler.enqueue and that's it, the rest is up to the implementer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, but I prefer to wait until we have true parallelism. I don't think any action could be put inside a select, there will be mutexes and locks involved and it will be highly coupled with how the runtime works. I'm not sure there will be many extensions to this, we just did it this way so we could more easily implement all the operations.

In any case, this will probably evolve and change in the future until we stabilize everything in 1.0. But I'm convinced that using these things outside a select expression makes little sense, so the real names aren't very important, and if they are called SelectAction then it's pretty clear it's for them to be used in a select.

case __temp_1
when 0
body
when 1
baz
else
::raise("Bug: invalid select index")
end
CODE
end

it "normalizes select with assign" do
assert_expand "select; when x = foo; x + 1; end", <<-CODE
__temp_1, __temp_2 = ::Channel.select({foo_select_action})
case __temp_1
when 0
x = __temp_2.as(typeof(foo))
x + 1
else
::raise("Bug: invalid select index")
end
CODE
end

it "normalizes select with else" do
assert_expand "select; when foo; body; else; baz; end", <<-CODE
__temp_1, __temp_2 = ::Channel.select({foo_select_action}, true)
case __temp_1
when 0
body
else
baz
end
CODE
end

it "normalizes select with assign and question method" do
assert_expand "select; when x = foo?; x + 1; end", <<-CODE
__temp_1, __temp_2 = ::Channel.select({foo_select_action?})
case __temp_1
when 0
x = __temp_2.as(typeof(foo?))
x + 1
else
::raise("Bug: invalid select index")
end
CODE
end

it "normalizes select with assign and bang method" do
assert_expand "select; when x = foo!; x + 1; end", <<-CODE
__temp_1, __temp_2 = ::Channel.select({foo_select_action!})
case __temp_1
when 0
x = __temp_2.as(typeof(foo!))
x + 1
else
::raise("Bug: invalid select index")
end
CODE
end
end
6 changes: 6 additions & 0 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -902,6 +902,12 @@ describe "Parser" do
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 "select\nwhen foo\n2\nend", Select.new([Select::When.new("foo".call, 2.int32)])
it_parses "select\nwhen foo\n2\nwhen bar\n4\nend", Select.new([Select::When.new("foo".call, 2.int32), Select::When.new("bar".call, 4.int32)])
it_parses "select\nwhen foo\n2\nelse\n3\nend", Select.new([Select::When.new("foo".call, 2.int32)], 3.int32)

assert_syntax_error "select\nwhen 1\n2\nend", "invalid select when expression: must be an assignment or call"

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
9 changes: 7 additions & 2 deletions spec/std/channel_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ describe Channel::Unbuffered do
ch1 = Channel::Unbuffered(Int32).new
ch2 = Channel::Unbuffered(Int32).new
spawn { ch1.send 123 }
Channel.select(ch1.receive_op, ch2.receive_op).should eq({0, 123})
Channel.select(ch1.receive_select_action, ch2.receive_select_action).should eq({0, 123})
end

it "works with select else" do
ch1 = Channel::Unbuffered(Int32).new
Channel.select({ch1.receive_select_action}, true).should eq({1, nil})
end

it "can send and receive nil" do
Expand Down Expand Up @@ -180,7 +185,7 @@ describe Channel::Buffered do
ch1 = Channel::Buffered(Int32).new
ch2 = Channel::Buffered(Int32).new
spawn { ch1.send 123 }
Channel.select(ch1.receive_op, ch2.receive_op).should eq({0, 123})
Channel.select(ch1.receive_select_action, ch2.receive_select_action).should eq({0, 123})
end

it "can send and receive nil" do
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/crystal/semantic/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -519,7 +519,7 @@ module Crystal
ArrayLiteral HashLiteral RegexLiteral RangeLiteral
Case StringInterpolation
MacroExpression MacroIf MacroFor MultiAssign
SizeOf InstanceSizeOf Global Require) %}
SizeOf InstanceSizeOf Global Require Select) %}
class {{name.id}}
include ExpandableNode
end
Expand Down
61 changes: 61 additions & 0 deletions src/compiler/crystal/semantic/literal_expander.cr
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,67 @@ module Crystal
final_exp
end

def expand(node : Select)
index_name = @program.new_temp_var_name
value_name = @program.new_temp_var_name

targets = [Var.new(index_name).at(node), Var.new(value_name).at(node)] of ASTNode
channel = Path.global("Channel").at(node)

tuple_values = [] of ASTNode
case_whens = [] of When

node.whens.each_with_index do |a_when, index|
condition = a_when.condition
case condition
when Call
cloned_call = condition.clone
cloned_call.name = select_action_name(cloned_call.name)
tuple_values << cloned_call

case_whens << When.new([NumberLiteral.new(index).at(node)] of ASTNode, a_when.body.clone)
when Assign
cloned_call = condition.value.as(Call).clone
cloned_call.name = select_action_name(cloned_call.name)
tuple_values << cloned_call

typeof_node = TypeOf.new([condition.value.clone] of ASTNode).at(node)
cast = Cast.new(Var.new(value_name).at(node), typeof_node).at(node)
new_assign = Assign.new(condition.target.clone, cast).at(node)
new_body = Expressions.new([new_assign, a_when.body.clone] of ASTNode)
case_whens << When.new([NumberLiteral.new(index).at(node)] of ASTNode, new_body)
else
node.raise "Bug: expected select when expression to be Assign or Call, not #{condition}"
end
end

if node_else = node.else
case_else = node_else.clone
else
case_else = Call.new(nil, "raise", args: [StringLiteral.new("Bug: invalid select index")] of ASTNode, global: true).at(node)
end

call_args = [TupleLiteral.new(tuple_values).at(node)] of ASTNode
call_args << BoolLiteral.new(true) if node.else

call = Call.new(channel, "select", call_args).at(node)
multi = MultiAssign.new(targets, [call] of ASTNode)
case_cond = Var.new(index_name).at(node)
a_case = Case.new(case_cond, case_whens, case_else).at(node)
Expressions.from([multi, a_case] of ASTNode).at(node)
end

def select_action_name(name)
case name
when .ends_with? "!"
name[0...-1] + "_select_action!"
when .ends_with? "?"
name[0...-1] + "_select_action?"
else
name + "_select_action"
end
end

# Transform a multi assign into many assigns.
def expand(node : MultiAssign)
# From:
Expand Down
5 changes: 5 additions & 0 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -2657,6 +2657,11 @@ module Crystal
false
end

def visit(node : Select)
expand(node)
false
end

def visit(node : MultiAssign)
expand(node)
false
Expand Down
24 changes: 24 additions & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1147,6 +1147,30 @@ module Crystal
def_equals_and_hash @cond, @whens, @else
end

class Select < ASTNode
record When, condition : ASTNode, body : ASTNode

property whens : Array(When)
property else : ASTNode?

def initialize(@whens, @else = nil)
end

def accept_children(visitor)
@whens.each do |select_when|
select_when.condition.accept visitor
select_when.body.accept visitor
end
@else.try &.accept visitor
end

def clone_without_location
Select.new(@whens.clone, @else.clone)
end

def_equals_and_hash @whens, @else
end

# Node that represents an implicit obj in:
#
# case foo
Expand Down
11 changes: 9 additions & 2 deletions src/compiler/crystal/syntax/lexer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -926,8 +926,15 @@ module Crystal
when 's'
case next_char
when 'e'
if next_char == 'l' && next_char == 'f'
return check_ident_or_keyword(:self, start)
if next_char == 'l'
case next_char
when 'e'
if next_char == 'c' && next_char == 't'
return check_ident_or_keyword(:select, start)
end
when 'f'
return check_ident_or_keyword(:self, start)
end
end
when 'i'
if next_char == 'z' && next_char == 'e' && next_char == 'o' && next_char == 'f'
Expand Down
72 changes: 72 additions & 0 deletions src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1030,6 +1030,8 @@ module Crystal
check_type_declaration { parse_require }
when :case
check_type_declaration { parse_case }
when :select
check_type_declaration { parse_select }
when :if
check_type_declaration { parse_if }
when :ifdef
Expand Down Expand Up @@ -2483,6 +2485,76 @@ module Crystal
end
end

def parse_select
slash_is_regex!
next_token_skip_space
skip_statement_end

whens = [] of Select::When

while true
case @token.type
when :IDENT
case @token.value
when :when
slash_is_regex!
next_token_skip_space_or_newline

location = @token.location
condition = parse_op_assign_no_control
unless valid_select_when?(condition)
raise "invalid select when expression: must be an assignment or call", location
end

skip_space
unless when_expression_end
unexpected_token @token.to_s, "expecting then, ';' or newline"
end
skip_statement_end

body = parse_expressions
skip_space_or_newline

whens << Select::When.new(condition, body)
when :else
if whens.size == 0
unexpected_token @token.to_s, "expecting when"
end
slash_is_regex!
next_token_skip_statement_end
a_else = parse_expressions
skip_statement_end
check_ident :end
next_token
break
when :end
if whens.empty?
unexpected_token @token.to_s, "expecting when, else or end"
end
next_token
break
else
unexpected_token @token.to_s, "expecting when, else or end"
end
else
unexpected_token @token.to_s, "expecting when, else or end"
end
end

Select.new(whens, a_else)
end

def valid_select_when?(node)
case node
when Assign
node.value.is_a?(Call)
when Call
true
else
false
end
end

def parse_include
parse_include_or_extend Include
end
Expand Down
19 changes: 19 additions & 0 deletions src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1263,6 +1263,25 @@ module Crystal
false
end

def visit(node : Select)
@str << keyword("select")
newline
node.whens.each do |a_when|
@str << "when "
a_when.condition.accept self
newline
accept_with_indent a_when.body
end
if a_else = node.else
@str << "else"
newline
accept_with_indent a_else
end
@str << keyword("end")
newline
false
end

def visit(node : ImplicitObj)
false
end
Expand Down
Loading