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

Compiler: add short block syntax &(..., &1) #9218

Closed
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
3 changes: 3 additions & 0 deletions spec/compiler/formatter/formatter_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -1650,4 +1650,7 @@ describe Crystal::Formatter do
1 # foo
/ #{1} /
CODE

assert_format "&1"
assert_format "&42"
end
13 changes: 13 additions & 0 deletions spec/compiler/lexer/lexer_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ private def it_lexes_global_match_data_index(globals)
end
end

private def it_lexes_implicit_block_argument(code, number)
it "lexes #{code}" do
lexer = Lexer.new code
token = lexer.next_token
token.type.should eq(:IMPLICIT_BLOCK_ARGUMENT)
token.value.should eq(number)
end
end

describe "Lexer" do
it_lexes "", :EOF
it_lexes " ", :SPACE
Expand Down Expand Up @@ -275,6 +284,10 @@ describe "Lexer" do
it_lexes "$~", :"$~"
it_lexes "$?", :"$?"

it_lexes_implicit_block_argument "&1", 1
it_lexes_implicit_block_argument "&2", 2
it_lexes_implicit_block_argument "&42", 42

assert_syntax_error "128_i8", "128 doesn't fit in an Int8"
assert_syntax_error "-129_i8", "-129 doesn't fit in an Int8"
assert_syntax_error "256_u8", "256 doesn't fit in an UInt8"
Expand Down
5 changes: 5 additions & 0 deletions spec/compiler/parser/parser_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,11 @@ module Crystal
it_parses "foo &.as?(T).bar", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(NilableCast.new(Var.new("__arg0"), "T".path), "bar")))
it_parses "foo(\n &.block\n)", Call.new(nil, "foo", block: Block.new([Var.new("__arg0")], Call.new(Var.new("__arg0"), "block")))

it_parses "&1", ImplicitBlockArgument.new(1)
it_parses "&42", ImplicitBlockArgument.new(42)

it_parses "foo &1", Call.new(nil, "foo", [ImplicitBlockArgument.new(1)] of ASTNode)

it_parses "foo.[0]", Call.new("foo".call, "[]", 0.int32)
it_parses "foo.[0] = 1", Call.new("foo".call, "[]=", [0.int32, 1.int32] of ASTNode)

Expand Down
72 changes: 72 additions & 0 deletions spec/compiler/semantic/implicit_block_argument_spec.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
require "../../spec_helper"

describe "Semantic: implicit block argument" do
it "errors if implicit block argument outside of block" do
assert_error %(
&1
),
"implcit block argument can only be used inside a block"
end

it "uses implicit block argument with call" do
assert_type(%(
def foo
yield 1
end

def bar(x)
x
end

foo &bar(&1)
)) { int32 }
end

it "uses implicit block argument with parentheses" do
assert_type(%(
def foo
yield 1
end

foo &(&1 &+ 1)
)) { int32 }
end

it "uses implicit block argument with tuple syntax" do
assert_type(%(
def foo
yield 1, 'a', true
end

foo &{&1, &2, &3}
)) { tuple_of [int32, char, bool] }
end

it "errors if 'it' variable outside of block (assign)" do
assert_error %(
it = 1
),
"implcit block argument 'it' can only be used inside a block"
end

it "errors if 'it' argless call outside of block (assign)" do
assert_error %(
it
),
"implcit block argument 'it' can only be used inside a block"
end

it "uses implicit block argument 'it' with call" do
assert_type(%(
def foo
yield 1
end

def bar(x)
x
end

foo &bar(it)
)) { int32 }
end
end
6 changes: 3 additions & 3 deletions spec/std/deque_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -579,8 +579,8 @@ describe "Deque" do
it "works while modifying deque" do
a = Deque{1, 2, 3}
count = 0
it = a.each
it.each do
iter = a.each
iter.each do
count += 1
a.clear
end
Expand All @@ -601,7 +601,7 @@ describe "Deque" do
it "works while modifying deque" do
a = Deque{1, 2, 3}
count = 0
it = a.each_index
iter = a.each_index
a.each_index.each do
count += 1
a.clear
Expand Down
99 changes: 99 additions & 0 deletions src/compiler/crystal/semantic/implicit_block_argument.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
module Crystal
class ImplicitBlockArgumentDetector < Visitor
def self.has_implicit_block_arguments?(node : ASTNode)
detector = new
node.accept(detector)
detector.has_implicit_block_arguments?
end

getter? has_implicit_block_arguments = false

def visit(node : ImplicitBlockArgument)
@has_implicit_block_arguments = true
end

def visit(node : ExpandableNode | Call)
if expanded = node.expanded
expanded.accept(self)
false
else
true
end
end

def visit(node : Call)
if node.args.empty? && !node.named_args && node.name == "it"
@has_implicit_block_arguments = true
false
else
true
end
end

def visit(node : ASTNode)
!@has_implicit_block_arguments
end
end

class ImplicitBlockArgumentExpander < Transformer
def self.expand(program : Program, node : ASTNode)
transformer = new(program)
node.transform(transformer)
block = transformer.block
block.body = node
block.at(node)
block
end

getter block : Block

def initialize(@program : Program)
@block = Block.new
end

def transform(node : Call)
if node.is_a?(Call) && node.args.empty? && !node.named_args && !node.block && !node.block_arg && node.name == "it"
return replace(node, 1)
end

expanded = node.expanded
if expanded
node.expanded = expanded.transform(self)
node
else
# TODO: don't go inside Call block_arg
super
end
end

def transform(node : ExpandableNode)
expanded = node.expanded
if expanded
node.expanded = expanded.transform(self)
node
else
# TODO: don't go inside Call block_arg
super
end
end

def transform(node : Block)
# Don't go inside nested blocks
node
end

def transform(node : ImplicitBlockArgument)
replace(node, node.number)
end

def replace(node, number)
args = @block.args ||= [] of Var

(number - args.size).times do |i|
args << @program.new_temp_var
end

args[number - 1].clone.at(node)
end
end
end
32 changes: 31 additions & 1 deletion src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
Expand Up @@ -753,6 +753,10 @@ module Crystal
end

def type_assign(target : Var, value, node, restriction = nil)
if target.name == "it"
node.raise "implcit block argument 'it' can only be used inside a block"
end

value.accept self

var_name = target.name
Expand Down Expand Up @@ -1154,6 +1158,18 @@ module Crystal
false
end

def visit(node : ImplicitBlockArgument)
node.raise "implcit block argument can only be used inside a block"
end

def expand_implicit_block_arguments(node : ASTNode)
if ImplicitBlockArgumentDetector.has_implicit_block_arguments?(node)
ImplicitBlockArgumentExpander.expand(@program, node)
else
nil
end
end

def bind_block_var(node, target, meta_vars, before_block_vars)
meta_var = new_meta_var(target.name, context: node)
meta_var.bind_to(target)
Expand Down Expand Up @@ -1279,6 +1295,10 @@ module Crystal
end

def visit(node : Call)
if node.args.empty? && !node.named_args && !node.block && !node.block_arg && node.name == "it"
node.raise "implcit block argument 'it' can only be used inside a block"
end

prepare_call(node)

if expand_macro(node)
Expand All @@ -1303,9 +1323,19 @@ module Crystal

obj = node.obj
args = node.args
block = node.block
block_arg = node.block_arg
named_args = node.named_args

if block_arg
expanded_block = expand_implicit_block_arguments(block_arg)
if expanded_block
expanded_block.call = node
node.block_arg = block_arg = nil
node.block = block = expanded_block
end
end

ignoring_type_filters do
if obj
obj.accept(self)
Expand Down Expand Up @@ -1333,7 +1363,7 @@ module Crystal
# and bind them to the current variables. Then, when visiting
# the block we will bind more variables to these ones if variables
# are reassigned.
if node.block || block_arg
if block || block_arg
before_vars = MetaVars.new
after_vars = MetaVars.new

Expand Down
13 changes: 13 additions & 0 deletions src/compiler/crystal/syntax/ast.cr
Original file line number Diff line number Diff line change
Expand Up @@ -526,6 +526,19 @@ module Crystal
def_equals_and_hash args, body, splat_index
end

class ImplicitBlockArgument < ASTNode
property number : Int32

def initialize(@number : Int32)
end

def clone_without_location
ImplicitBlockArgument.new(@number)
end

def_equals_and_hash number
end

# A method call.
#
# [ obj '.' ] name '(' ')' [ block ]
Expand Down
19 changes: 16 additions & 3 deletions src/compiler/crystal/syntax/lexer.cr
Original file line number Diff line number Diff line change
Expand Up @@ -648,7 +648,8 @@ module Crystal
@token.type = :"."
end
when '&'
case next_char
char = next_char
case char
when '&'
case next_char
when '='
Expand Down Expand Up @@ -689,7 +690,18 @@ module Crystal
@token.type = :"&*"
end
else
@token.type = :"&"
if char.ascii_number?
number = 0
while char.ascii_number?
number = number * 10 + char.to_i
char = next_char
end
@token.type = :IMPLICIT_BLOCK_ARGUMENT
@token.value = number
return @token
else
@token.type = :"&"
end
end
when '|'
case next_char
Expand Down Expand Up @@ -1237,7 +1249,8 @@ module Crystal
end
scan_ident(start)
when '_'
case next_char
char = next_char
case char
when '_'
case next_char
when 'D'
Expand Down
12 changes: 11 additions & 1 deletion src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
Expand Up @@ -993,6 +993,10 @@ module Crystal
end
location = @token.location
node_and_next_token Call.new(Global.new("$~").at(location), method, NumberLiteral.new(value.to_i))
when :IMPLICIT_BLOCK_ARGUMENT
number = @token.value.as(Int32)
location = @token.location
node_and_next_token ImplicitBlockArgument.new(number).at(location)
when :__LINE__
node_and_next_token MagicConstant.expand_line_node(@token.location)
when :__END_LINE__
Expand Down Expand Up @@ -4365,7 +4369,13 @@ module Crystal
end
when :"{"
return nil unless allow_curly
when :CHAR, :STRING, :DELIMITER_START, :STRING_ARRAY_START, :SYMBOL_ARRAY_START, :NUMBER, :IDENT, :SYMBOL, :INSTANCE_VAR, :CLASS_VAR, :CONST, :GLOBAL, :"$~", :"$?", :GLOBAL_MATCH_DATA_INDEX, :REGEX, :"(", :"!", :"[", :"[]", :"~", :"->", :"{{", :__LINE__, :__END_LINE__, :__FILE__, :__DIR__, :UNDERSCORE
when :CHAR, :STRING, :DELIMITER_START, :STRING_ARRAY_START,
:SYMBOL_ARRAY_START, :NUMBER, :IDENT, :SYMBOL, :INSTANCE_VAR,
:CLASS_VAR, :CONST, :GLOBAL, :"$~", :"$?",
:GLOBAL_MATCH_DATA_INDEX, :REGEX,
:"(", :"!", :"[", :"[]", :"~", :"->", :"{{",
:__LINE__, :__END_LINE__, :__FILE__, :__DIR__,
:UNDERSCORE, :IMPLICIT_BLOCK_ARGUMENT
# Nothing
when :"*", :"**"
if current_char.ascii_whitespace?
Expand Down
Loading