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 = [""]
^~~~~~~~~~~~~~~~~~~~
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.
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?