-
Notifications
You must be signed in to change notification settings - Fork 7
Blocks
Crystal supports blocks and yields like Ruby. This page describes how they are implemented.
This basic code:
def foo
yield 1
yield 2
end
foo do |x|
x + 2
end
is implemented as following:
-
The block given to foo is defined as a new function, say block1:
def block1(x) x + 2 end
-
Since foo yields Ints, the type of block1 is inferred to be an Int since x is an Int and Int#+(Int) is of type Int.
-
An extra parameter is added to function: a pointer to a function, and every yield just invokes that function (written in pseudo language):
def foo(&block) block.call 1 block.call 2 end
-
foo's invocation is passed a pointer to block1:
foo &block1
The above is not entirely correct. Take a look at this function:
def bar
a = 10
foo do |x|
a += x
end
end
If the block is extracted to a function named block1, how does it accesses and modifies the variable "a"?
For this reason, foo is added yet another extra parameter, a context that will hold pointers to the referenced variables inside the block. Before invoking foo, a context is prepared, like this (in pseudo language):
def foo(context, &block)
block.call 1, context
block.call 2, context
end
def bar
a = 10
context = alloc ...
context.a = &a
foo context, &block1
end
def block1(x, context)
context.a += x
end
Note that foo doesn't use the context, it just passes it to the block call. So foo's context type in LLVM is just i8* (void* in C) and we don't end up having too many instantiations of foo.
So basically a context holds pointers to each variable accessed in the block.
And still, the above is still not correct. Consider this case:
def bar
a = 10
foo do |x|
b = 20
foo do |y|
a += b + x + y
end
end
end
Let's try to write it in pseudo language:
def foo(context, &block)
block.call 1, context
block.call 2, context
end
def bar
a = 10
context = alloc ...
context.a = &a
foo context, &block1
end
def block1(x, context)
b = 20
context2 = alloc ...
context2.b = &b
context2.x = &x
context2.a = context.a
foo context2, &block2
end
def block2(y, context)
context.a += context.b + context.x + y
end
Note that in block1 we copied variable's a pointer from context to context2. Since many more variables might be needed to be passed along nested blocks, instead of copying each of them (which might make the context a lot bigger), we just keep a pointer to the parent context, like so:
def block1(x, context)
b = 20
context2 = alloc ...
context2.b = &b
context2.x = &x
context2.parent = &context
foo context2, &block2
end
def block2(y, context)
context.parent.a += context.b + context.x + y
end
At this point, a context holds the following:
- A pointer to the parent context, if needed.
- Pointers to each variable accessed in the block in the immediately superior block or def.
What if there's a next inside a block?
def foo
yield 1
yield 2
end
def bar
foo do |x|
next
puts 1
end
end
Well, a next poses no big problems: it's just a return inside of the block:
def block1(x)
return
puts 1
end
Note: we do "next 3"
What if there's a break inside a block?
def foo
yield 1
puts 1
yield 2
end
def bar
foo do |x|
break 3
end
end
Since the block given to foo is breaking, the "puts 1" in foo should never be executed: after the first yield, foo should just return. How to implement this?
We need to yet again extend the context: we will add an exit flag that will tell us if a block was exited prematurely:
def foo(context, &block)
context.exit_flag = false
block.call 1, context
return result if context.exit_flag
context.exit_flag = false
result = block.call 2, context
return result if context.exit_flag
result
end
def bar
foo context, &block1
end
def block1(x, context)
context.exit_flag = true
3
end
Implementation note: before this point foo didn't need to know anything about the context. Now it must set and check a flag in it. We can store this flag in the first position of the context and just cast it to the exit flag.
At this point, a context holds the following:
- An exit flag
- A pointer to the parent context, if needed.
- Pointers to each variable accessed in the block in the immediately superior block or def.