Why type promotion in following code not work?

# 1.cr

require "option_parser"

module Translater
  enum Browser
    Firefox
  end

  browser = Browser::Firefox

  OptionParser.parse do |parser|
    parser.on(
      "-b BROWSER",
      "--browser=BROWSER",
      "Specify browser used for scrap, only support firefox for now, default is firefox.
") do |b|
      browser = Browser.parse?(b)

      if browser.nil?
        STDERR.puts "Supported options: #{Browser.names.map(&.downcase).join ", "}"
        exit 1
      end
    end
  end

  case browser # Translater::Browser | Nil
  in Browser::Firefox
    puts "Firefox"
  end
end
In src/1.cr:25:3

 25 | case browser # Translater::Browser | Nil
      ^
Error: case is not exhaustive.

Missing cases:
 - Nil

the browser in line 25 case statement is never be nil, right?

Thanks.


Crystal 1.8.0-dev [c6e3116df] (2023-01-13)

LLVM: 14.0.6
Default target: x86_64-pc-linux-gnu

Maybe your code never execute the part ?
do |b|

Maybe have double check, because if it never execute this block, probably it’s possible you value is Nil without know it.

I never use option_parser personnaly.

I think as well about something else: the error say it’s not exhaustive. Actually the enum you made only enum one type. Maybe it’s the main problem here. Can you try to add an another value to your enum ?

The compiler is unable to proof that due to complications with the closure context. browser = Browser.parse?(b) assigns a nilable value. Even if that nilable value afterwards leads to a different code path that never reaches the case statement, it requires the closured variable browser to be nilable.
What you can do to avoid this is not assign the nilable return value from Browser.parse?(b) directly to browser. Instead, use a non-closured local variable and only assign it to the closured browser when the Nil type is eliminated.

1 Like

Should we consider this as a issue? because in fact, browser can never nilable.

Just as you suggested, the expected fix should be introduce a new non-closure variable.

value = Browser.parse?(b)

if value.nil?
  STDERR.puts "Supported options: #{Browser.names.map(&.downcase).join ", "}"
  exit 1
else
  browser = value
end

But, i consider this is not necessary theoretically.

Thanks

If never execute this block, browser value is enum Browser::Firefox, in fact, this issue not involve with OptionParser, add a new enum value, result is same.

No, it’s definitely nilable because you assign a value that’s Browser | Nil. And it can actually have the value nil when Browser.parse? does not find a match. Between the assignment of browser = Browser.parse?(b) and exit 1, it’s nil. Since the assignment happens in a captured block (of parser.on), it’s not easy to disprove that at the point case browser runs, the proc might be exactly in this spot where browser is nil in a different fiber.

Theoretically, the compiler could maybe become sophisticated enough to figure this out. But I’m not sure if it’s feasible. Definitely not an easy feature to implement.
This is certainly not a bug, though.

1 Like

What if the block actually runs inside a spawn and between it gets assigned nil and exiting the block fiber switch happens? Then outside the block it will be nil.

1 Like

Ideally Option Parser doesn’t capture the block. Maybe it’s possible.

1 Like