Skip to content
This repository has been archived by the owner on Nov 10, 2017. It is now read-only.
asterite edited this page Aug 28, 2011 · 16 revisions

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
    

Variables outside a block

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.

Nested blocks

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.

Next

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"

Break

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.
Clone this wiki locally