Unexpected behaviour using `select` with `caller`

I was trying to write a method earlier today to grab all of the lines in the call stack matching the name of the file in which the method was defined, using select on the return value of caller, but it was always missing one line that I expected to be there.

To debug, I wrote the following code, derived from the method I was trying to write, into a file named test.cr

def a_method
  puts "Caller:"
  puts "\n"
  puts caller.join("\n")
  puts "\n\n"

  puts "Caller with select:"
  puts "\n"
  puts caller
    .select {|l| puts l; l.includes?("test")}
    .tap { puts "\n"} # just here to separate output of puts in `select` from output of expression
    .join("\n")
  puts "\n\n"

  puts "Caller with map then select:"
  puts "\n"
  puts caller
    .map(&.itself)
    .select {|l| puts l; l.includes?("test")}
    .tap { puts "\n"} # just here to separate output of puts in `select` from output of expression
    .join("\n")
end

a_method

The first chunk (“Caller”) just prints the output of caller so I know what it looks like. I would have expected the second and third chunks (“Caller with select” and “Caller with map then select”) to have the same output, because the .map(&.itself) would just return the same array it started out with.

Instead, the second chunk outputs only one line when I would have expected two. The third chunk gives me the expected output.

I put the putses inside select to see what lines it was working with. To my surprise it was giving me
/usr/local/Cellar/crystal/0.36.1_3/src/enumerable.cr:1313:11 in 'a_method'
instead of something like
test.cr:ln:col in 'a_method'
which is what I got in the 3rd chunk, and it’s also what I was expecting.

Are my expectations off, or is this a bug?

This is the entire screen output when I run the method.

Caller:

test.cr:4:8 in 'a_method'
test.cr:24:1 in '__crystal_main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:110:5 in 'main_user_code'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:96:7 in 'main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:119:3 in 'main'


Caller with select:

/usr/local/Cellar/crystal/0.36.1_3/src/enumerable.cr:1313:11 in 'a_method'
test.cr:24:1 in '__crystal_main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:110:5 in 'main_user_code'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:96:7 in 'main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:119:3 in 'main'

test.cr:24:1 in '__crystal_main'


Caller with map then select:

test.cr:17:8 in 'a_method'
test.cr:24:1 in '__crystal_main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:110:5 in 'main_user_code'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:96:7 in 'main'
/usr/local/Cellar/crystal/0.36.1_3/src/crystal/main.cr:119:3 in 'main'

test.cr:17:8 in 'a_method'
test.cr:24:1 in '__crystal_main'

Reduced reproduction:

def a_method
  p! caller.first                 # => "/eval:2:6 in 'a_method'"
  p! caller.select { true }.first # => "/usr/lib/crystal/enumerable.cr:1313:11 in 'a_method'"
  p! caller.map(&.itself).first   # => "/eval:4:5 in 'a_method'"
end

a_method

For some reason the first call frame of caller.select { true } points to enumerable.cr:1313, that’s the first line of #select: crystal/enumerable.cr at 0.36.1 · crystal-lang/crystal · GitHub

Thanks for the reply. I’m gathering this is not the intended behaviour. Would you recommend submitting an issue?

Yes, this sure looks like a bug.