Skip to content

BUG: no target defs when a closure inside a case branch matching uninstantiated generic types is passed to OptionParser#on #17044

Description

@naqvis

Bug Report

Compiling a program that passes a block to OptionParser#on inside a case … when branch matching uninstantiated generic types crashes with BUG: no target defs during LLVM codegen.

The crash happens when all of these conditions are met:

  1. An abstract base class with Char?, String getters and an abstract method
  2. Two generic subclasses that are never instantiated anywhere in the program
  3. A case statement on the base type with when GENERIC1, GENERIC2 (both uninstantiated)
  4. Inside that branch, a block passed to OptionParser#on that calls a method on the narrowed variable
  5. Local variables extracted from the parameter before the case are referenced inside the block
  6. The method containing the case is reached through an array iteration, preventing compile-time type narrowing

The error goes away if at least one of the generic types in the when clause is instantiated somewhere in the program.

Reproduction:

require "option_parser"

abstract class Flag
  getter short : Char?
  getter long : String
  getter description : String
  def initialize(@short : Char?, @long : String, @description = "")
  end
  abstract def parse(value : String) : Nil
end

class SimpleFlag < Flag
  def parse(value : String) : Nil; end
end

class TypedFlag(T) < Flag
  def parse(value : String) : Nil; end
end

class EnumFlag(T) < Flag
  def parse(value : String) : Nil; end
end

class Registry
  @items = [] of Flag

  def add(short : Char?, long : String)
    @items << SimpleFlag.new(short, long); self
  end

  def run
    parser = OptionParser.new
    @items.each { |item| register(parser, item) }
    parser.parse([] of String)
  end

  private def register(parser : OptionParser, flag : Flag)
    short = flag.short
    long = flag.long
    case flag
    when EnumFlag, TypedFlag
      parser.on("-#{short}", "--#{long}=VALUE", flag.description) { |val| flag.parse(val) }
    end
  end
end

Registry.new.add('v', "verbose").run

Workaround:

Both workarounds work. They just attack the bug differently:

  1. Force instantiation - makes the compiler see concrete instantiations of the generic types so it can build the dispatch table:
_ = EnumFlag(String).new(nil, "")
_ = TypedFlag(String).new(nil, "")
  1. Split when clause — avoids the union entirely so no generic dispatch table is needed:
when EnumFlag
  ...
when TypedFlag
  ...

Crystal Version

Crystal 1.20.2 (2026-05-15)

LLVM: 22.1.6
Default target: aarch64-apple-darwin25.5.0

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:bugA bug in the code. Does not apply to documentation, specs, etc.topic:compiler:semantic

    Type

    Fields

    No fields configured for Bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions