Do/end block behavior differs from Ruby

From the documentation:

The difference between using do ... end and { ... } is that do ... end binds to the left-most call, while { ... } binds to the right-most call

However, this doesn’t match the actual behavior I see:

def a(*args) end
def a(*args)
  puts "a received block"
  yield
end
def b(*args) end
def b(*args)
  puts "b received block"
  yield
end
def c(*args) end
def c(*args)
  puts "c received block"
  yield
end

a b c do
  puts "in block"
end

The output is:

b received block
in block

To demonstrate further: Carcin

Rather than binding to the left-most call, it seems do/end blocks bind to the second-from-the-right call. Is this behavior intended?

For comparison, here is an equivalent Ruby snippet:

def a(*args)
  if block_given?
    puts "a received block"
    yield
  end
end

def b(*args)
  if block_given?
    puts "b received block"
    yield
  end
end

def c(*args)
  if block_given?
    puts "c received block"
    yield
  end
end

a b c do
  puts "in block"
end

The output of the ruby snippet is

a received block
in block
5 Likes

Also, for context, I’m working on GitHub - crystal-lang-tools/tree-sitter-crystal: tree sitter parser for crystal lang, fork of keidax/tree-sitter-crystal. The parser currently assumes Crystal’s block precedence rules match Ruby, and I just realized it doesn’t :scream:

3 Likes

wired behavior:

def a(*args)
  puts "a"
end

def a(*args, &)
  puts "a received block"
  yield
end

def b(*args)
  puts "b"
end

def b(*args, &)
  puts "b received block"
  yield
end

def c(*args)
  puts "c"
end

def c(*args, &)
  puts "c received block"
  yield
end

a b c do
  puts "in block"
end
c
b received block
in block
a

If this code works on ruby, do ... end block should bind to the a, but if replace with {...}, it bind to c.

But, Crystal binding to b.

I’d report it on the github issue tracker, as at the very least it is a bug in the documentation. Though the behavior seems weird so it is probably not intended.

1 Like

I always felt this difference in behaviour is quite extraordinary. And probably more confusing than helpful. :person_shrugging:

It’s a good recommendation to always use parenthesis when there is doubt. Maybe the formatter should do that? :thinking:

Anyway while looking briefly at the parser code, it seems obvious why this is failing: To work correctly as described, this feature would require backtracking through a chain of calls in order to attach do to the leftmost one. But the parser doesn’t do that. It’s just a simple boolean flag that indicates to stop parsing the next call on a do keyword. Then the previous call continues.
So tentatively a fix could turn that boolean into a counter to keep track of multiple nested calls.

Btw. in the source code I noticed another quirk: parentheses breaks the left binding:

a (1; 2), b do
   puts "in block"
end
b received block
in block
5 Likes

Github issue created: Possible bug: binding of do/end blocks is surprising and doesn't match the documentation · Issue #15303 · crystal-lang/crystal · GitHub

3 Likes

What a great find!