Crystal: Compiler error when inheriting from String

Created on 10 Jan 2017  路  8Comments  路  Source: crystal-lang/crystal

When creating a class that inherits from String, I get an error on crystal build or crystal run that appears to be internal to crystal.

Code to reproduce (put it in test.cr):

class X < String
end

Then do: crystal run test.cr

I get the error message in /opt/crystal/src/debug/dwarf/line_numbers.cr:153: instance variable '@include_directories' of Debug::DWARF::LineNumbers::Sequence must be Array(String), not Array(String)

(full error output, which is very long, is included at the bottom)

I expected that the code would compile / run without error (or if the intent is that I'm not allowed to subclass String, then I expected an error message to that effect).

Version info:

$ crystal --version
Crystal 0.20.4 [d1f8c42] (2017-01-06)

OS: elementary OS 0.4 (loki), which is based on ubuntu 16.04

$ uname -srvmo
Linux 4.4.0-57-generic #78-Ubuntu SMP Fri Dec 9 23:50:32 UTC 2016 x86_64 GNU/Linux

Full error output from running crystal run test.cr:

Error in test.cr:1: while requiring "prelude"

class X < String
^

in /opt/crystal/src/prelude.cr:47: while requiring "kernel"

require "kernel"
^

in /opt/crystal/src/kernel.cr:193: instantiating 'Signal:Class#setup_default_handlers()'

Signal.setup_default_handlers
       ^~~~~~~~~~~~~~~~~~~~~~

in /opt/crystal/src/signal.cr:117: instantiating 'Signal#reset()'

    Signal::CHLD.reset
                 ^~~~~

in /opt/crystal/src/signal.cr:92: instantiating 'Event::SignalChildHandler#trigger()'

        Event::SignalChildHandler.instance.trigger
                                           ^~~~~~~

in /opt/crystal/src/event/signal_child_handler.cr:28: instantiating 'loop()'

    loop do
    ^~~~

in /opt/crystal/src/event/signal_child_handler.cr:28: instantiating 'loop()'

    loop do
    ^~~~

in /opt/crystal/src/event/signal_child_handler.cr:38: instantiating 'send_pending(Int32, Process::Status)'

        send_pending pid, status
        ^~~~~~~~~~~~

in /opt/crystal/src/event/signal_child_handler.cr:46: instantiating 'Channel::Buffered(Process::Status | Nil)#send(Process::Status)'

      chan.send status
           ^~~~

in /opt/crystal/src/concurrent/channel.cr:183: instantiating 'Scheduler:Class#reschedule()'

      Scheduler.reschedule
                ^~~~~~~~~~

in /opt/crystal/src/concurrent/scheduler.cr:12: instantiating 'loop_fiber()'

      loop_fiber.resume
      ^~~~~~~~~~

in /opt/crystal/src/concurrent/scheduler.cr:18: instantiating 'Fiber:Class#new()'

    @@loop_fiber ||= Fiber.new { @@eb.run_loop }
                           ^~~

in /opt/crystal/src/fiber.cr:29: instantiating 'Fiber#run()'

    fiber_main = ->(f : Fiber) { f.run }
                                   ^~~

in /opt/crystal/src/fiber.cr:122: instantiating 'Exception+#inspect_with_backtrace(IO::FileDescriptor)'

    ex.inspect_with_backtrace STDERR
       ^~~~~~~~~~~~~~~~~~~~~~

in /opt/crystal/src/exception.cr:33: instantiating 'backtrace?()'

    backtrace?.try &.each do |frame|
    ^~~~~~~~~~

in /opt/crystal/src/exception.cr:18: instantiating '(CallStack | Nil)#try()'

    @callstack.try &.printable_backtrace
               ^~~

in /opt/crystal/src/exception.cr:18: instantiating '(CallStack | Nil)#try()'

    @callstack.try &.printable_backtrace
               ^~~

in /opt/crystal/src/exception.cr:18: instantiating 'CallStack#printable_backtrace()'

    @callstack.try &.printable_backtrace
                     ^~~~~~~~~~~~~~~~~~~

in /opt/crystal/src/callstack.cr:42: instantiating 'decode_backtrace()'

    @backtrace ||= decode_backtrace
                   ^~~~~~~~~~~~~~~~

in /opt/crystal/src/callstack.cr:148: instantiating 'Array(Pointer(Void))#compact_map()'

    @callstack.compact_map do |ip|
               ^~~~~~~~~~~

in /opt/crystal/src/callstack.cr:148: instantiating 'Array(Pointer(Void))#compact_map()'

    @callstack.compact_map do |ip|
               ^~~~~~~~~~~

in /opt/crystal/src/callstack.cr:149: instantiating 'CallStack:Class#decode_line_number(Pointer(Void))'

      file, line, column = CallStack.decode_line_number(ip)
                                     ^~~~~~~~~~~~~~~~~~

in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 5:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
>  5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
  21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
  26.           elf.read_section?(".debug_line") do |sh, io|
  27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'dwarf_line_numbers()'
in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 21:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
   5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
> 21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
  26.           elf.read_section?(".debug_line") do |sh, io|
  27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'Debug::ELF:Class#open(String+)'
in /opt/crystal/src/debug/elf.cr:148: instantiating 'File:Class#open(String+, String)'

      File.open(path, "r") do |file|
           ^~~~

in /opt/crystal/src/debug/elf.cr:148: instantiating 'File:Class#open(String+, String)'

      File.open(path, "r") do |file|
           ^~~~

in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 21:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
   5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
> 21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
  26.           elf.read_section?(".debug_line") do |sh, io|
  27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'Debug::ELF:Class#open(String+)'
in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 26:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
   5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
  21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
> 26.           elf.read_section?(".debug_line") do |sh, io|
  27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'Debug::ELF#read_section?(String)'
in /opt/crystal/src/debug/elf.cr:213: instantiating 'IO::FileDescriptor+#seek((UInt32 | UInt64))'

        @io.seek(sh.offset) do
            ^~~~

in /opt/crystal/src/debug/elf.cr:213: instantiating 'IO::FileDescriptor+#seek((UInt32 | UInt64))'

        @io.seek(sh.offset) do
            ^~~~

in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 26:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
   5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
  21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
> 26.           elf.read_section?(".debug_line") do |sh, io|
  27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'Debug::ELF#read_section?(String)'
in /opt/crystal/src/callstack.cr:168: expanding macro

  {% if flag?(:darwin) || flag?(:freebsd) || flag?(:linux) || flag?(:openbsd) %}
  ^

in macro 'macro_69337648' /opt/crystal/src/callstack.cr:168, line 27:

   1. 
   2.     @@dwarf_line_numbers : Debug::DWARF::LineNumbers?
   3. 
   4.     protected def self.decode_line_number(ip)
   5.       if ln = dwarf_line_numbers
   6.         if row = ln.find(decode_address(ip))
   7.           path = ln.files[row.file]?
   8.           if dirname = ln.directories[row.directory]?
   9.             path = "#{dirname}/#{path}"
  10.           end
  11.           return {path, row.line, row.column}
  12.         end
  13.       end
  14.       {"??", 0, 0}
  15.     end
  16. 
  17.     
  18.       @@base_address : UInt64|UInt32|Nil
  19. 
  20.       protected def self.dwarf_line_numbers
  21.         @@dwarf_line_numbers ||= Debug::ELF.open(PROGRAM_NAME) do |elf|
  22.           elf.read_section?(".text") do |sh, _|
  23.             @@base_address = sh.addr - sh.offset
  24.           end
  25. 
  26.           elf.read_section?(".debug_line") do |sh, io|
> 27.             Debug::DWARF::LineNumbers.new(io, sh.size)
  28.           end
  29.         end
  30.       end
  31. 
  32.       # DWARF uses fixed addresses but some platforms (e.g., OpenBSD or Linux
  33.       # with the [PaX patch](https://en.wikipedia.org/wiki/PaX)) load
  34.       # executables at a random address, so we must remove the load offset from
  35.       # the IP to match the addresses in DWARF sections.
  36.       #
  37.       # See https://en.wikipedia.org/wiki/Address_space_layout_randomization
  38.       protected def self.decode_address(ip)
  39.         if LibC.dladdr(ip, out info) != 0
  40.           unless info.dli_fbase.address == @@base_address
  41.             return ip.address - info.dli_fbase.address
  42.           end
  43.         end
  44.         ip.address
  45.       end
  46.     
  47.   

instantiating 'Debug::DWARF::LineNumbers:Class#new(IO::FileDescriptor+, (UInt32 | UInt64))'
in /opt/crystal/src/debug/dwarf/line_numbers.cr:179: instantiating 'decode_sequences((UInt32 | UInt64))'

        decode_sequences(size)
        ^~~~~~~~~~~~~~~~

in /opt/crystal/src/debug/dwarf/line_numbers.cr:212: instantiating 'Debug::DWARF::LineNumbers::Sequence:Class#new()'

          sequence = Sequence.new
                              ^~~

in /opt/crystal/src/debug/dwarf/line_numbers.cr:153: instance variable '@include_directories' of Debug::DWARF::LineNumbers::Sequence must be Array(String), not Array(String)

          @include_directories = [""]
          ^~~~~~~~~~~~~~~~~~~~
bug implemented compiler

Most helpful comment

Ok, thanks for confirming that! (The reason I was trying to do this was as a workaround to get non-substitutable type aliases. I.e. I want types X and Y both to be strings with no added functionality, but for it to be a compiler error to pass X where a Y is expected).

In that case, can we call this issue a feature request for an informative error message when trying to inherit from String?

All 8 comments

Don't inherit from string. Use composition if you must.

String is a special class defined by the compiler, and subclassing it makes it really easy to break things. I agree there probably should be an error though.

Ok, thanks for confirming that! (The reason I was trying to do this was as a workaround to get non-substitutable type aliases. I.e. I want types X and Y both to be strings with no added functionality, but for it to be a compiler error to pass X where a Y is expected).

In that case, can we call this issue a feature request for an informative error message when trying to inherit from String?

Isn't it better to find and fix the real issue here? Preventing to subclass String seems like a poor workaround to me.

@cjfuller What's the scene that you need to inherit from String?

@david50407 I was trying to have multiple different types of plain string that can't be used interchangeably. In my particular case, I was representing protein sequences and DNA sequences using strings, and I wanted the type system to help me enforce that you can't pass a DNA sequence where you expect a protein one and vice-versa. This is conceptually similar to using non-substitutable type aliases to represent numbers with attached units, so that you can't e.g. pass a measure in feet to a function that wants meters, even if they're both just floats.

There are of course other ways to achieve the same thing (I ended up using classes that don't inherit from string that delegate to an actual string using method_missing.), so I don't really need the feature of being able to inherit from string; it's just that the behavior when I tried to do so was surprising.

@cjfuller given #3882 already adds a compiler error to prevent inheriting from string, I'll close this issue. Feel free to open another one about non-substitutable type aliases, I've used them in other languages and find them useful. As a disclaimer, I'm not sure at this time what it takes to support them in the compiler, and whether they will fit well with the language, so there's no guarantees about it getting done :).

Really cool use case and domain, btw :).

Non-substitutable type aliases: there is type Foo = Bar for C bindings (thus only valid under lib), so the logic exists.

Thanks! Opened #3888 as a separate issue for a feature request for non-substitutable type aliases.

Was this page helpful?
0 / 5 - 0 ratings