Search code examples
rubysearchfindblockproc

How does Proc.new finds the block in this code?


I have the following code:

def call_block
  Proc.new.call
  my_local_proc = Proc.new { Proc.new.call }
  my_local_proc.call
end

call_block { p 'block' }

The output is:

block
block

Can someone explain to me how Proc.new found the block I passed to call_block? I guess that Proc.new just searches for the closest block and that it`s implemented entirely in C++.

And I have another question: Can something like this be achieve using only ruby? I mean, can I write a method such that, if not block has been given, takes the block that was passed to the method calling it. Something like:

def bar
  if not block_given?
    #use the block that has been given to the caller
  end
  # some code
end

def foo
  bar
end

foo { :block }

Solution

  • Proc.new will use the method's block if called without one inside a method with one attached. This is documented behavior.

    To find out how YARV does it, let's read the source code. Specifically, the proc_new function:

    block_pointer = rb_vm_control_frame_block_ptr(control_frame_pointer);
    

    This line retrieves a pointer to the block associated with the current control frame.

    I believe these control frames implement Ruby's stack. We are currently inside the Proc.new control frame, so this would retrieve the pointer to the block given to the method.

    if (block_pointer != NULL) {
        /* block found */
    } else {
        /* block not found... */
    }
    

    If the pointer isn't NULL, then Proc.new was passed a block explicitly. What if the pointer is NULL, though?

    /* block not found... */
    control_frame_pointer = RUBY_VM_PREVIOUS_CONTROL_FRAME(control_frame_pointer);
    block_pointer = rb_vm_control_frame_block_ptr(control_frame_pointer);
    

    We move up on the stack and try to get its block. In other words, we move up to the caller's control frame and try to get its block.

    if (block_pointer != NULL) {
        if (is_lambda) {
            rb_warn("tried to create Proc object without a block");
        }
    } else {
        rb_raise(rb_eArgError, "tried to create Proc object without a block");
    }
    

    Now, if it's not NULL, then we pretty much succeeded. If it's still NULL, then we can't create a Proc, so we raise an ArgumentError.

    The algorithm boils down to this:

    1. See if Proc.new was given a block
      1. If so, use it
      2. If not, see if caller was given a block
        1. If so, use it
        2. If not, raise error

    Source code altered for readability. Visit linked source file on GitHub for the original.