Skip to content

Commit

Permalink
Merge pull request #2783 from crystal-lang/feature/block_auto_unpack_…
Browse files Browse the repository at this point in the history
…rule_1

Implement auto-tuple unpacking in block arguments, Rule 1
Ary Borenszweig authored Jun 10, 2016

Verified

This commit was signed with the committer’s verified signature. The key has expired.
2 parents 353eeb1 + ca12dce commit 9c135ad
Showing 16 changed files with 356 additions and 226 deletions.
2 changes: 1 addition & 1 deletion samples/pretty_json.cr
Original file line number Diff line number Diff line change
@@ -53,7 +53,7 @@ class PrettyPrinter
print "[\n"
@indent += 1
i = 0
@pull.read_array do |obj|
@pull.read_array do
if i > 0
print ','
print '\n' if @indent > 0
31 changes: 6 additions & 25 deletions spec/compiler/codegen/block_spec.cr
Original file line number Diff line number Diff line change
@@ -721,26 +721,6 @@ describe "Code gen: block" do
")
end

it "allows yields with less arguments than in block" do
run("
struct Nil
def to_i
0
end
end
def foo
yield 1
end
a = 0
foo do |x, y|
a += x + y.to_i
end
a
").to_i.should eq(1)
end

it "codegens block with nilable type with return (1)" do
run("
def foo
@@ -1421,15 +1401,16 @@ describe "Code gen: block" do
)).to_i.should eq(3)
end

it "uses splat in block argument, but not enough yield expressions" do
it "auto-unpacks tuple" do
run(%(
def foo
yield 42
tup = {1, 2, 4}
yield tup
end
foo do |x, y, z, *w|
x
foo do |x, y, z|
(x + y) * z
end
)).to_i.should eq(42)
)).to_i.should eq((1 + 2) * 4)
end
end
94 changes: 75 additions & 19 deletions spec/compiler/type_inference/block_spec.cr
Original file line number Diff line number Diff line change
@@ -71,16 +71,17 @@ describe "Block inference" do
") { union_of(array_of(int32), array_of(float64)) }
end

it "uses block arg, has nil type" do
assert_type(%(
it "uses block arg, too many arguments" do
assert_error %(
def foo
yield
end
foo do |x|
x
end
)) { nil_type }
),
"too many block arguments (given 1, expected maximum 0)"
end

it "yields with different types" do
@@ -193,17 +194,7 @@ describe "Block inference" do
foo {}
",
"type must be Int32, not (Float64 | Int32)"
end

it "doesn't report error if yields nil but nothing is yielded" do
assert_type("
def foo(&block: Int32, Nil -> )
yield 1
end
foo { |x| x }
") { int32 }
"argument #1 of yield expected to be Int32, not (Float64 | Int32)"
end

it "reports error if missing arguments to yield" do
@@ -214,7 +205,7 @@ describe "Block inference" do
foo { |x| x }
",
"missing argument #2 of yield with type Int32"
"wrong number of yield arguments (given 1, expected 2)"
end

it "reports error if block didn't return expected type" do
@@ -329,7 +320,7 @@ describe "Block inference" do
end

it "errors when using local variable with block argument name" do
assert_error "def foo; yield; end; foo { |a| }; a",
assert_error "def foo; yield 1; end; foo { |a| }; a",
"undefined local variable or method 'a'"
end

@@ -1199,7 +1190,7 @@ describe "Block inference" do
tup = {1, 'a'}
yield *tup
yield true
yield true, nil
end
foo do |x, y|
@@ -1233,15 +1224,16 @@ describe "Block inference" do
end

it "uses splat in block argument, but not enough yield expressions" do
assert_type(%(
assert_error %(
def foo
yield 1
end
foo do |x, y, z, *w|
{x, y, z, w}
end
)) { tuple_of([int32, nil_type, nil_type, tuple_of([] of Type)]) }
),
"too many block arguments (given 3+, expected maximum 1+)"
end

it "errors if splat argument becomes a union" do
@@ -1256,4 +1248,68 @@ describe "Block inference" do
),
"block splat argument must be a tuple type"
end

it "auto-unpacks tuple" do
assert_type(%(
def foo
tup = {1, 'a'}
yield tup
end
foo do |x, y|
{x, y}
end
)) { tuple_of([int32, char]) }
end

it "auto-unpacks tuple, less than max" do
assert_type(%(
def foo
tup = {1, 'a', true}
yield tup
end
foo do |x, y|
{x, y}
end
)) { tuple_of([int32, char]) }
end

it "auto-unpacks with block arg type" do
assert_type(%(
def foo(&block : {Int32, Int32} -> _)
yield({1, 2})
end
foo do |x, y|
x + y
end
)) { int32 }
end

it "doesn't auto-unpacks tuple, more args" do
assert_error %(
def foo
tup = {1, 'a'}
yield tup, true
end
foo do |x, y, z|
end
),
"too many block arguments (given 3, expected maximum 2)"
end

it "auto-unpacks tuple, too many args" do
assert_error %(
def foo
tup = {1, 'a'}
yield tup
end
foo do |x, y, z|
end
),
"too many block arguments (given 3, expected maximum 2)"
end
end
43 changes: 33 additions & 10 deletions spec/std/hash_spec.cr
Original file line number Diff line number Diff line change
@@ -559,33 +559,56 @@ describe "Hash" do
it "pass key, value, index values into block" do
hash = {2 => 4, 5 => 10, 7 => 14}
results = [] of Int32
hash.each_with_index { |k, v, i| results << k + v + i }
{% if Crystal::VERSION == "0.18.0" %}
hash.each_with_index { |(k, v), i| results << k + v + i }
{% else %}
hash.each_with_index { |k, v, i| results << k + v + i }
{% end %}
results.should eq [6, 16, 23]
end

it "can be used with offset" do
hash = {2 => 4, 5 => 10, 7 => 14}
results = [] of Int32
hash.each_with_index(3) { |k, v, i| results << k + v + i }
{% if Crystal::VERSION == "0.18.0" %}
hash.each_with_index(3) { |(k, v), i| results << k + v + i }
{% else %}
hash.each_with_index(3) { |k, v, i| results << k + v + i }
{% end %}
results.should eq [9, 19, 26]
end
end

describe "each_with_object" do
it "passes memo, key and value into block" do
hash = {:a => 'b'}
hash.each_with_object(:memo) do |memo, k, v|
memo.should eq(:memo)
k.should eq(:a)
v.should eq('b')
end
{% if Crystal::VERSION == "0.18.0" %}
hash.each_with_object(:memo) do |(k, v), memo|
memo.should eq(:memo)
k.should eq(:a)
v.should eq('b')
end
{% else %}
hash.each_with_object(:memo) do |memo, k, v|
memo.should eq(:memo)
k.should eq(:a)
v.should eq('b')
end
{% end %}
end

it "reduces the hash to the accumulated value of memo" do
hash = {:a => 'b', :c => 'd', :e => 'f'}
result = hash.each_with_object({} of Char => Symbol) do |memo, k, v|
memo[v] = k
end
result = nil
{% if Crystal::VERSION == "0.18.0" %}
result = hash.each_with_object({} of Char => Symbol) do |(k, v), memo|
memo[v] = k
end
{% else %}
result = hash.each_with_object({} of Char => Symbol) do |memo, k, v|
memo[v] = k
end
{% end %}
result.should eq({'b' => :a, 'd' => :c, 'f' => :e})
end
end
78 changes: 31 additions & 47 deletions src/compiler/crystal/codegen/codegen.cr
Original file line number Diff line number Diff line change
@@ -1285,64 +1285,48 @@ module Crystal

# Now assign exp values to block arguments
if splat_index
# If there are less expressions than the number of block arguments, we
# can go from left to right, and the argument at the splat index will
# be the empty tuple
if exp_values.size < (block.args.size - 1)
block.args.each_with_index do |arg, i|
block_var = block_context.vars[arg.name]
if i == splat_index
exp_value = allocate_tuple(arg.type.as(TupleInstanceType)) do |tuple_type|
{tuple_type, llvm_nil}
end
exp_type = arg.type
elsif i < exp_values.size
exp_value, exp_type = exp_values[i]
else
exp_value, exp_type = llvm_nil, @mod.nil
end
assign block_var.pointer, block_var.type, exp_type, exp_value
end
else
j = 0
block.args.each_with_index do |arg, i|
block_var = block_context.vars[arg.name]
if i == splat_index
exp_value = allocate_tuple(arg.type.as(TupleInstanceType)) do |tuple_type|
exp_value, exp_type = exp_values[j]
j += 1
{exp_type, exp_value}
end
exp_type = arg.type
else
j = 0
block.args.each_with_index do |arg, i|
block_var = block_context.vars[arg.name]
if i == splat_index
exp_value = allocate_tuple(arg.type.as(TupleInstanceType)) do |tuple_type|
exp_value, exp_type = exp_values[j]
j += 1
{exp_type, exp_value}
end
assign block_var.pointer, block_var.type, exp_type, exp_value
exp_type = arg.type
else
exp_value, exp_type = exp_values[j]
j += 1
end
assign block_var.pointer, block_var.type, exp_type, exp_value
end
else
i = 0
exp_values.each do |(exp_value, exp_type)|
if arg = block.args[i]?
block_var = block_context.vars[arg.name]
assign block_var.pointer, block_var.type, exp_type, exp_value
# Check if tuple unpacking is needed
if exp_values.size == 1 &&
(exp_type = exp_values.first[1]).is_a?(TupleInstanceType) &&
block.args.size > 1
exp_value = exp_values.first[0]
exp_type.tuple_types.each_with_index do |tuple_type, i|
arg = block.args[i]?
if arg
t_type = tuple_type
t_value = codegen_tuple_indexer(exp_type, exp_value, i)
block_var = block_context.vars[arg.name]
assign block_var.pointer, block_var.type, t_type, t_value
end
end
else
exp_values.each_with_index do |(exp_value, exp_type), i|
if arg = block.args[i]?
block_var = block_context.vars[arg.name]
assign block_var.pointer, block_var.type, exp_type, exp_value
end
end
i += 1
end
end
end

# Then assign nil to remaining block args
unless splat_index
while i < block.args.size
arg = block.args[i]
block_var = block_context.vars[arg.name]
assign block_var.pointer, block_var.type, @mod.nil, llvm_nil
i += 1
end
end

Phi.open(self, block, @needs_value) do |phi|
with_cloned_context(block_context) do |old|
# Reset those vars that are declared inside the block and are nilable.
5 changes: 4 additions & 1 deletion src/compiler/crystal/codegen/debug.cr
Original file line number Diff line number Diff line change
@@ -75,13 +75,16 @@ module Crystal
tmp_debug_type = di_builder.temporary_md_node(LLVM::Context.global)
debug_type_cache[type] = tmp_debug_type

ivars.each_with_index do |name, ivar, idx|
# TOOD: use each_with_index
idx = 0
ivars.each do |name, ivar|
if (ivar_type = ivar.type?) && (ivar_debug_type = get_debug_type(ivar_type))
offset = @mod.target_machine.data_layout.offset_of_element(struct_type, idx + (type.struct? ? 0 : 1))
size = @mod.target_machine.data_layout.size_in_bits(llvm_embedded_type(ivar_type))
member = di_builder.create_member_type(nil, name[1..-1], nil, 1, size, size, offset * 8, 0, ivar_debug_type)
element_types << member
end
idx += 1
end

size = @mod.target_machine.data_layout.size_in_bits(struct_type)
97 changes: 59 additions & 38 deletions src/compiler/crystal/semantic/ast.cr
Original file line number Diff line number Diff line change
@@ -911,6 +911,7 @@ module Crystal
# Ficticious node to bind yield expressions to block arguments
class YieldBlockBinder < ASTNode
getter block
property yield_vars : Array(Var)?

def initialize(@mod : Program, @block : Block)
@yields = [] of Yield
@@ -923,8 +924,10 @@ module Crystal

def update(from = nil)
# We compute all the types for each block arguments
block_arg_types = Array(Array(Type)?).new(block.args.size, nil)
args_size = block.args.size
block_arg_types = Array(Array(Type)?).new(args_size, nil)
splat_index = block.splat_index
yield_vars = @yield_vars

@yields.each do |a_yield|
i = 0
@@ -934,19 +937,33 @@ module Crystal
# to split and create tuple types for that case.
exps_types = Array(Type).new(a_yield.exps.size)

a_yield.exps.each do |exp|
break if !splat_index && i >= block.args.size
# Check if there are missing yield expressions to match
# the (optional) block signature
if yield_vars && a_yield.exps.size < yield_vars.size
a_yield.raise "wrong number of yield arguments (given #{a_yield.exps.size}, expected #{yield_vars.size})"
end

a_yield.exps.each do |exp|
exp_type = exp.type?
return unless exp_type

# Check that the expression has the type of the (optional) block signature
if yield_vars
yield_var = yield_vars[i]?
if yield_var && !exp_type.implements?(yield_var.type)
exp.raise "argument ##{i + 1} of yield expected to be #{yield_var.type}, not #{exp_type}"
end
end

break if !splat_index && i >= args_size

if exp.is_a?(Splat)
unless exp_type.is_a?(TupleInstanceType)
exp.raise "expected splat expression to be a tuple type, not #{exp_type}"
end

exp_type.tuple_types.each do |tuple_type|
break if !splat_index && i >= block.args.size
break if !splat_index && i >= args_size

exps_types << tuple_type
i += 1
@@ -959,45 +976,49 @@ module Crystal

# Now move exps_types to block_arg_types
if splat_index
# If there are less expressions than the number of block arguments, we
# can go from left to right, and the argument at the splat index will
# be the empty tuple
if exps_types.size < (block.args.size - 1)
block.args.size.times do |i|
# Error if there are less expressions than the number of block arguments
if exps_types.size < (args_size - 1)
block.raise "too many block arguments (given #{args_size - 1}+, expected maximum #{exps_types.size}+)"
end

j = 0
args_size.times do |i|
types = block_arg_types[i] ||= [] of Type
if i == splat_index
tuple_types = exps_types[i, exps_types.size - (args_size - 1)]
types << @mod.tuple_of(tuple_types)
j += tuple_types.size
else
types << exps_types[j]
j += 1
end
end
else
# Check if tuple unpacking is needed
if exps_types.size == 1 &&
(exp_type = exps_types.first).is_a?(TupleInstanceType) &&
args_size > 1
if block.args.size > exp_type.tuple_types.size
block.raise "too many block arguments (given #{block.args.size}, expected maximum #{exp_type.tuple_types.size})"
end

exp_type.tuple_types.each_with_index do |tuple_type, i|
break if i >= block_arg_types.size

types = block_arg_types[i] ||= [] of Type
if i == splat_index
types << @mod.tuple_of([] of Type)
else
types << (exps_types[i]? || @mod.nil)
end
types << tuple_type
end
else
j = 0
block.args.size.times do |i|
types = block_arg_types[i] ||= [] of Type
if i == splat_index
tuple_types = exps_types[i, exps_types.size - (block.args.size - 1)]
types << @mod.tuple_of(tuple_types)
j += tuple_types.size
else
types << exps_types[j]
j += 1
end
if block.args.size > exps_types.size
block.raise "too many block arguments (given #{block.args.size}, expected maximum #{exps_types.size})"
end
end
else
i = 0
exps_types.each do |exp_type|
types = block_arg_types[i] ||= [] of Type
types << exp_type
i += 1
end

# Remaining block arguments get the Nil type
while i < block.args.size
types = block_arg_types[i] ||= [] of Type
types << @mod.nil
i += 1
exps_types.each_with_index do |exp_type, i|
break if i >= block_arg_types.size

types = block_arg_types[i] ||= [] of Type
types << exp_type
end
end
end
end
19 changes: 16 additions & 3 deletions src/compiler/crystal/semantic/call.cr
Original file line number Diff line number Diff line change
@@ -745,9 +745,22 @@ class Crystal::Call
output_type = mod.nil if output_type.void?
end

# Bind block arguments to the yield vars, if any, or to nil otherwise
block.args.each_with_index do |arg, i|
arg.bind_to(yield_vars.try(&.[i]?) || mod.nil_var)
if yield_vars
# Check if tuple unpkacing is needed
if yield_vars.size == 1 &&
(yield_var_type = yield_vars.first.type).is_a?(TupleInstanceType) &&
block.args.size > 1
yield_var_type.tuple_types.each_with_index do |tuple_type, i|
arg = block.args[i]?
arg.type = tuple_type if arg
end
else
yield_vars.each_with_index do |yield_var, i|
yield_var_type = yield_var.type
arg = block.args[i]?
arg.bind_to(yield_var || mod.nil_var) if arg
end
end
end

# If the block is used, we convert it to a function pointer
18 changes: 3 additions & 15 deletions src/compiler/crystal/semantic/main_visitor.cr
Original file line number Diff line number Diff line change
@@ -682,24 +682,12 @@ module Crystal
node.scope.try &.accept self
node.exps.each &.accept self

if (yield_vars = @yield_vars) && !node.scope
yield_vars.each_with_index do |var, i|
exp = node.exps[i]?
if exp
if (exp_type = exp.type?) && !exp_type.implements?(var.type)
exp.raise "argument ##{i + 1} of yield expected to be #{var.type}, not #{exp_type}"
end

exp.freeze_type = var.type
elsif !var.type.nil_type?
node.raise "missing argument ##{i + 1} of yield with type #{var.type}"
end
end
end

# We use a binder to support splats and other complex forms
binder = block.binder ||= YieldBlockBinder.new(@mod, block)
binder.add_yield(node)
if (yield_vars = @yield_vars) && !node.scope
binder.yield_vars ||= yield_vars
end
binder.update

unless block.visited
4 changes: 3 additions & 1 deletion src/compiler/crystal/syntax/parser.cr
Original file line number Diff line number Diff line change
@@ -3609,6 +3609,8 @@ module Crystal
end

def parse_block2
location = @token.location

block_args = [] of Var
extra_assigns = nil
block_body = nil
@@ -3716,7 +3718,7 @@ module Crystal
end_location = token_end_location
next_token_skip_space

Block.new(block_args, block_body, splat_index).at_end(end_location)
Block.new(block_args, block_body, splat_index).at(location).at_end(end_location)
end

record CallArgs,
2 changes: 1 addition & 1 deletion src/compiler/crystal/syntax/to_s.cr
Original file line number Diff line number Diff line change
@@ -812,7 +812,7 @@ module Crystal
end

if named_args = node.named_args
named_args.each do |named_arg, i|
named_args.each do |named_arg|
@str << ", " if printed_arg
@str << named_arg.name
@str << ": "
4 changes: 3 additions & 1 deletion src/compiler/crystal/tools/doc/html/methods_inherited.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<% method_groups = methods.group_by { |method| method.name } %>
<% unless method_groups.empty? %>
<h3><%= label %> methods inherited from <%= ancestor.kind %> <code><a href="<%= type.path_to(ancestor) %>"><%= ancestor.full_name %></a></code></h3>
<% method_groups.each_with_index do |method_name, methods, i| %>
<% i = 0 %>
<% method_groups.each do |method_name, methods| %>
<a href="<%= type.path_to(ancestor) %><%= methods[0].anchor %>" class="tooltip">
<span><%= methods.map { |method| method.name + method.args_to_s } .join("<br/>") %></span>
<%= method_name %></a><%= ", " if i != method_groups.size - 1 %>
<% i += 1 %>
<% end %>
<% end %>
10 changes: 9 additions & 1 deletion src/env.cr
Original file line number Diff line number Diff line change
@@ -12,6 +12,10 @@ require "c/stdlib"
# # Later use that env var.
# puts ENV["PORT"].to_i
module ENV
{% if Crystal::VERSION == "0.18.0" %}
extend Enumerable({String, String})
{% end %}

# Retrieves the value for environment variable named `key` as a `String`.
# Raises `KeyError` if the named variable does not exist.
def self.[](key : String) : String
@@ -106,7 +110,11 @@ module ENV
key_value = String.new(environ_value).split('=', 2)
key = key_value[0]
value = key_value[1]? || ""
yield key, value
{% if Crystal::VERSION == "0.18.0" %}
yield({key, value})
{% else %}
yield key, value
{% end %}
environ_ptr += 1
else
break
144 changes: 86 additions & 58 deletions src/hash.cr
Original file line number Diff line number Diff line change
@@ -2,6 +2,10 @@
#
# See the [official docs](http://crystal-lang.org/docs/syntax_and_semantics/literals/hash.html) for the basics.
class Hash(K, V)
{% if Crystal::VERSION == "0.18.0" %}
include Enumerable({K, V})
{% end %}

getter size : Int32
@buckets_size : Int32
@first : Entry(K, V)?
@@ -263,15 +267,24 @@ class Hash(K, V)
#
# ```
# h = {"foo" => "bar"}
#
# h.each do |key, value|
# key # => "foo"
# value # => "bar"
# end
#
# h.each do |key_and_value|
# key_and_value # => {"foo", "bar"}
# end
# ```
def each
current = @first
while current
yield current.key, current.value
{% if Crystal::VERSION == "0.18.0" %}
yield({current.key, current.value})
{% else %}
yield current.key, current.value
{% end %}
current = current.fore
end
self
@@ -358,43 +371,45 @@ class Hash(K, V)
ValueIterator(K, V).new(self, @first)
end

# Calls the given block for each key-value pair and passes in the key, value, and index.
#
# ```
# h = {"foo" => "bar"}
#
# h.each_with_index do |key, value, index|
# key # => "foo"
# value # => "bar"
# index # => 0
# end
#
# h.each_with_index(3) do |key, value, index|
# key # => "foo"
# value # => "bar"
# index # => 3
# end
# ```
def each_with_index(offset = 0)
i = offset
each do |key, value|
yield key, value, i
i += 1
{% if Crystal::VERSION != "0.18.0" %}
# Calls the given block for each key-value pair and passes in the key, value, and index.
#
# ```
# h = {"foo" => "bar"}
#
# h.each_with_index do |key, value, index|
# key # => "foo"
# value # => "bar"
# index # => 0
# end
#
# h.each_with_index(3) do |key, value, index|
# key # => "foo"
# value # => "bar"
# index # => 3
# end
# ```
def each_with_index(offset = 0)
i = offset
each do |key, value|
yield key, value, i
i += 1
end
self
end
self
end

# Iterates the given block for each element with an arbitrary object given, and returns the initially given object.
# ```
# evens = (1..10).each_with_object([] of Int32) { |i, a| a << i*2 }
# # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# ```
def each_with_object(memo)
each do |k, v|
yield(memo, k, v)
# Iterates the given block for each element with an arbitrary object given, and returns the initially given object.
# ```
# evens = (1..10).each_with_object([] of Int32) { |i, a| a << i*2 }
# # => [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
# ```
def each_with_object(memo)
each do |k, v|
yield(memo, k, v)
end
memo
end
memo
end
{% end %}

# Returns a new `Array` with all the keys.
#
@@ -404,7 +419,7 @@ class Hash(K, V)
# ```
def keys
keys = Array(K).new(@size)
each { |key| keys << key }
each_key { |key| keys << key }
keys
end

@@ -416,7 +431,7 @@ class Hash(K, V)
# ```
def values
values = Array(V).new(@size)
each { |key, value| values << value }
each_value { |value| values << value }
values
end

@@ -443,25 +458,30 @@ class Hash(K, V)
# h.key_index("qux") # => nil
# ```
def key_index(key)
each_with_index do |my_key, my_value, i|
# TODO: use each_with_index
i = 0
each do |my_key, my_value|
return i if key == my_key
i += 1
end
nil
end

# Returns an `Array` populated with the results of each iteration in the given block.
#
# ```
# h = {"foo" => "bar", "baz" => "qux"}
# h.map { |k, v| v } # => ["bar", "qux"]
# ```
def map(&block : K, V -> U)
array = Array(U).new(@size)
each do |k, v|
array.push yield k, v
{% if Crystal::VERSION != "0.18.0" %}
# Returns an `Array` populated with the results of each iteration in the given block.
#
# ```
# h = {"foo" => "bar", "baz" => "qux"}
# h.map { |k, v| v } # => ["bar", "qux"]
# ```
def map(&block : K, V -> U)
array = Array(U).new(@size)
each do |k, v|
array.push yield k, v
end
array
end
array
end
{% end %}

# Returns a new `Hash` with the keys and values of this hash and *other* combined.
# A value in *other* takes precedence over the one in this hash.
@@ -534,9 +554,15 @@ class Hash(K, V)
# h.reject { |k, v| v < 200 } # => {"b" => 200, "c" => 300}
# ```
def reject(&block : K, V -> U)
each_with_object({} of K => V) do |memo, k, v|
memo[k] = v unless yield k, v
end
{% if Crystal::VERSION == "0.18.0" %}
each_with_object({} of K => V) do |(k, v), memo|
memo[k] = v unless yield k, v
end
{% else %}
each_with_object({} of K => V) do |memo, k, v|
memo[k] = v unless yield k, v
end
{% end %}
end

# Equivalent to `Hash#reject`, but makes modification on the current object rather that returning a new one. Returns nil if no changes were made.
@@ -617,11 +643,13 @@ class Hash(K, V)
hash
end

# Returns a `Tuple` of the first key-value pair in the hash.
def first
first = @first.not_nil!
{first.key, first.value}
end
{% if Crystal::VERSION != "0.18.0" %}
# Returns a `Tuple` of the first key-value pair in the hash.
def first
first = @first.not_nil!
{first.key, first.value}
end
{% end %}

# Returns the first key in the hash.
def first_key
21 changes: 17 additions & 4 deletions src/http/headers.cr
Original file line number Diff line number Diff line change
@@ -3,6 +3,10 @@
# Two headers are considered the same if their downcase representation is the same
# (in which `_` is the downcase version of `-`).
struct HTTP::Headers
{% if Crystal::VERSION == "0.18.0" %}
include Enumerable({String, Array(String)})
{% end %}

# :nodoc:
record Key, name : String do
forward_missing_to @name
@@ -177,9 +181,15 @@ struct HTTP::Headers
end

def each
@hash.each do |key, value|
yield key.name, value
end
{% if Crystal::VERSION == "0.18.0" %}
@hash.each do |key, value|
yield({key.name, value})
end
{% else %}
@hash.each do |key, value|
yield key.name, value
end
{% end %}
end

def get(key)
@@ -208,7 +218,9 @@ struct HTTP::Headers

def to_s(io : IO)
io << "HTTP::Headers{"
@hash.each_with_index do |key, values, index|
# TODO: use each_with_index
index = 0
@hash.each do |key, values|
io << ", " if index > 0
key.name.inspect(io)
io << " => "
@@ -217,6 +229,7 @@ struct HTTP::Headers
else
values.inspect(io)
end
index += 1
end
io << "}"
end
10 changes: 9 additions & 1 deletion src/http/params.cr
Original file line number Diff line number Diff line change
@@ -3,6 +3,10 @@ require "uri"
module HTTP
# Represents a collection of http parameters and their respective values.
struct Params
{% if Crystal::VERSION == "0.18.0" %}
include Enumerable({String, String})
{% end %}

# Parses an HTTP query string into a `HTTP::Params`
#
# HTTP::Params.parse("foo=bar&foo=baz&qux=zoo")
@@ -214,7 +218,11 @@ module HTTP
def each
raw_params.each do |name, values|
values.each do |value|
yield(name, value)
{% if Crystal::VERSION == "0.18.0" %}
yield({name, value})
{% else %}
yield(name, value)
{% end %}
end
end
end

0 comments on commit 9c135ad

Please sign in to comment.