Crystal: Compiler Error: undefined method for Nil

Created on 26 Feb 2018  路  5Comments  路  Source: crystal-lang/crystal

In some circumstances, the compiler can't seem to differentiate which if branch to use for T based off conditional logic for an instance variable. I get the same result when trying to use an Int32 | Bool type

To explain let me provide a code snippet

class Test
  @test : Int32? = 10

  def test
    if @test.nil?
      0
    else
      @test + 1
    end
  end
end

Test.new.test

When running this snippet you get this result

Error in test.cr:13: instantiating 'Test#test()'

Test.new.test
         ^~~~

in test.cr:8: undefined method '+' for Nil (compile-time type is (Int32 | Nil))

      @test + 1
            ^
question

Most helpful comment

It's not a bug, it's a failsafe!

Nothing prevents you to do:

if t.log
  t.log = nil # We change log here!
  t.log.puts "test" # Crash at runtime :(
  t.log.close
end

Using a local variable, this cannot happen:

if logger = t.log
  t.log = nil # We change log here!
  logger.puts "test" # logger is still valid
  logger.close
end

All 5 comments

Between the condition (@test.nil?) and the branch execution (@test + 1), the instance variable could have changed type (from a concurrent fiber, or maybe later, a thread), and the compiler is not sure that in the branch @test will still be non-nil.

To fix this you need to assign the instance variable to a local variable:

class Test
  @test : Int32?

  def initialize(@test = nil)
  end

  def test
    if test = @test
      test + 1
    else
      0
    end
  end
end

pp Test.new.test # => 0
pp Test.new(10).test # => 11

live at: https://carc.in/#/r/3n28

Looks like same bug - this won't compile:

class Test
  property log = nil
  def initialize()
    @log = File.new("test_.txt", "a")
  end
end

t = Test.new
if t.log
  t.log.puts "test" 
  t.log.close
end

in that way - no problem

class Test
  property log = nil
  def initialize()
    @log = File.new("test_.txt", "a")
  end
end

t = Test.new
tmp = t.log
if tmp
  tmp.puts "test" 
  tmp.close
end

It's not a bug, it's a failsafe!

Nothing prevents you to do:

if t.log
  t.log = nil # We change log here!
  t.log.puts "test" # Crash at runtime :(
  t.log.close
end

Using a local variable, this cannot happen:

if logger = t.log
  t.log = nil # We change log here!
  logger.puts "test" # logger is still valid
  logger.close
end

Not a bug. Should be a stackoverflow question.

Solutions:

  1. avoid nilable types;
  2. make a local copy using the if test = @test pattern 鈥攊nstance variables may be changed concurrently, but a local copy won't;
  3. use a builder accessor, such as def test; @test ||= something; end.

You may alternatively transform the compile-time error into a runtime error, but you that should be limited to times where you're absolutely sure that value can't be nil (hint: almost always a wrong assumption):

  1. force .not_nil!;
  2. use accessors such as property!.

That makes a lot of sense, I will close this issue, thank you.

Was this page helpful?
0 / 5 - 0 ratings