Compiler error?

This compiles fine.

Crystal 1.14.0 [dacd97bcc] (2024-10-09)

LLVM: 18.1.6
Default target: x86_64-unknown-linux-gnu

def mpgen(p, steps = 1u64)
  puts "Starting Mp gen exponent = #{p}"
  puts "Starting Mp gen process at step #{steps}"
  r = (1.to_big_i << p) - 1
  modpnk = modpn = 3.to_big_i << p
  (steps-1).times { modpnk = (modpnk << 2) + modpn }
  primes_checked = 0u64
  loop do
    mp = modpnk + r
    print "\rsteps = #{steps}"
    p = mp.bit_length.to_u64
    if prime?(p)
      primes_checked += 1
      break if mp_prime?(p, mp)
    end
    steps += 1
    modpnk = (modpnk << 2) + modpn
  end
  print "\rsteps = #{steps}\n"
  puts "Primes checked = #{primes_checked}"
  puts "Next Mp prime = #{p}"
  p
end

Then I changed the code to this:

def mpgen(n, steps = 1u64)
  puts "Starting Mp gen exponent = #{n}"
  puts "Starting Mp gen process at step #{steps}"
  primes_checked = 0u64
  loop do
    mp = (3.to_big_i << n) + (1.to_big_i << n) - 1
    print "\rsteps = #{steps}"
    p = mp.bit_length.to_u64
    if prime?(p)
      primes_checked += 1
      break if mp_prime?(p, mp)
    end
    steps += 1
    n += 2
  end
  print "\rsteps = #{steps}\n"
  puts "Primes checked = #{primes_checked}"
  puts "Next Mp prime = #{p}"
  p
end

And now get this error.

Showing last frame. Use --error-trace for full trace.

In MPgenerate1.cr:40:25

 40 | mp = (3.to_big_i << n) + (1.to_big_i << n) - 1
                          ^
Error: expected argument #1 to 'BigInt#<<' to be Int, not (Tuple() | UInt64)

Overloads are:
 - BigInt#<<(other : Int)
 - Int#<<(count : Int)

When I change the order of those 2 terms it still points to the first n.

This has to be a compiler error, right?

Do you have a runnable example that reproduces this error?

I put a link to the gist file of all the code in the Issues on this.

The compiler is technically right here. There’s a few things going on, but I don’t really think this is a compiler bug at least. Adding method parameter and return type restrictions makes the problem more clear.

  1. Using loop do creates its own scope within the block, so the compiler has to handle the case where it doesn’t execute I guess, using while true does not suffer from this and works just fine.
  2. Your variable p is basically conflicting with the ::p() method which returns the arguments passed to it, and since its only assigned in the loop do, if that doesn’t execute the compiler will call ::p() with no arguments so it returns an empty tuple, which without a type restriction on the method or n parameter, is allowed to pass thru, turning the type of n in to Tuple() | UInt64.
4 Likes

I changed lopp do to while true and it did compile and gives correct results.

I don’t understand (yet) the other reasoning around the p variable, but I’ll play some more with it to see if I can get it to compile.

Even if it isn’t a formal compiler error, it’s certainly unexpected behavior, and maybe a better error message is warranted.

Thanks for the quick feedback. :slightly_smiling_face:

OK, I got it to work with loop do.

I initialized p before the loop and it works.
But the error message isn’t very helpful because it points to n, which is not the cause of the error (it’s also defined before loop do), it’s apparently an artifact of it.

It’s these subtle differences like between things like loop do and while true that for people coming from dynamic languages like Ruby, et al, can be a PITA.

def mpgen(n, steps = 1u64)
  puts "Starting Mp gen exponent = #{n}"
  puts "Starting Mp gen process at step #{steps}"
  primes_checked = 0u64
  p = 0u64          # <---- need for it to compile
  loop do
    mp = (3.to_big_i << n) + (1.to_big_i << n) - 1
    p = mp.bit_length.to_u64
    print "\rsteps = #{steps}; checking prime #{p}"
    if prime?(p)
      primes_checked += 1
      break if mp_prime?(p, mp)
    end
    steps += 1
    n += 2
  end
  print "\rsteps = #{steps}\n"
  puts "Primes checked = #{primes_checked}"
  puts "Next Mp prime = #{p}"
  p
end

If mpgen is recursive (for example, if mp_prime? calls back into it) or if something else in your program is feeding the return value of one mpgen call to another, then pointing to n makes sense. The method p with no arguments returns an empty Tuple.

p typeof(p)
# => Tuple()

So if you’re doing something like this:

n = 0
while n < some_value
  n = mpgen(n)
  # ...
end

Then the compiler will infer from the fact that you returned a Tuple() from an invocation of mpgen that n inside of mpgen can be a Tuple().

def mpgen(n)
  loop do
    p = n
    break
  end
  p
end

n = 0
3.times do
  puts n
  n = mpgen(n)
end
# => 0
# => {}
# => {}

How so? The scoping rules between loop do and while true in Ruby are the same as in Crystal. This would’ve failed in Ruby, too, except the value would’ve been nil (p with no args returns nil).

def preinitialized
  p = :preinitialized
  loop do
    p = 0
    break
  end
  p
end

def uninitialized
  loop do
    p = 0
    break
  end
  p
end

p preinitialized # => 0
p uninitialized  # => nil
1 Like

The issue for me was the error message was very confusing. As soon as @Blacksmoke16 said the word scope I knew exactly what the problem was, and fixed it.

If the error message had said something like:
p not in scope
or even
variable in loop not in scope

I would have known exactly what the problem was.

These are the specific types of error messages the Rust compiler gives.

Hopefully I’ll be cognizant of these loop differences (again) and won’t repeat this mistake.

This is not the different of loop do with while true, it is the behavior of block.

block always create a new local scope, in fact, are you try it use Ruby? this behavior exactly same as Ruby.

1 Like