Any feedback on Crystal Debugger is very welcome!

Ladies and gentleman!

Now when debugger changes I’ve done become a part of a Crystal since 0.35 I’d like to have some feedback on it.
I want to know if you are using it and if you have any questions or suggestions and if it helps in your everyday Crystal life.
Does CodeLLVM helps you to simplify debugging and if not what are the issues?

PS (as a aside question): Did anyone tried to debug Amber applications and if so how did you set it up?

1 Like

I’ve tried reading through the other discussions to figure out how to use it, but I still haven’t been able to figure it out. I’m using VSCode, and I installed CodeLLDB. I wrote a small crystal program, clicked the red breakpoint dot, then clicked Run > Start debugging.

Then I get this error:

test.cr doesn’t contain any ‘host’ platform architectures: x86_64h, x86_64, i386

The first error I got said there wasn’t a launch.json, but it generated one. I’m not sure what this file is for, or if I have to edit something…

Is there any step by step guides on how to use the debugger? And I mean from the standpoint of someone that has never used a debugger ever… Also maybe some information on what I should expect to get from using the debugger like "you’ll see variable values, but you won’t see … ".

These are the steps I used to try the debugger https://dev.to/bcardiff/debug-crystal-in-vscode-via-codelldb-3lf

3 Likes

Did you compiled it with -d or --debug parameter?
By default it injects only line numbers info.

Brian,

You’ve made a great tutorial how to set it up and test it!
I think you have to tell somewhere in the article that --debug (or -d for brevity) is necessary to be passed to compiler to be able to use debugger properly. In your case it is implicit in the parameters block.

Also, by some reason etc/lldb/crystal_formatters.py is not part of the distribution. I suspect this is release build process bug.

Ok, so I followed @bcardiff tutorial. I compile my test app crystal build --debug test.cr. Then when I run it with ./test, it just runs and completes. How do I get the breakpoint in there?

When I use VSCode, it looks like the tasks.json file runs "command": "crystal build --debug ${relativeFile} -o bin/${fileBasenameNoExtension}". I then click my breakpoint in the editor, and click “Run > Start Debugging”… It no longer throws any error message, but I don’t see anything happening. When I open the debug menu, it doesn’t show anything.

Also, I saw that the python file isn’t in the codebase when I installed crystal from brew. I had to download that.

Weird!
Can you try to debug that test program in lldb directly?

lldb bin/text

while inside of lldb type:

b __crystal_main
r
v

it should show you at least some debug info for variables.

To exit from lldb just type q

Here is my test script that I used to test during debugging the debugger :slight_smile:

class TestClass
    @@testClassVar = 123_u64
    
    def self.updateTestClassVar (val)
      @@testClassVar = val
    end
end

alias Primitives = (Int8 | UInt8 | Int16 | UInt16 | Int32 | UInt32 | Int64 | UInt64)
alias NullablePrimitives = (Primitives | Nil)

a : Array(NullablePrimitives)? = [1i8, 2u8, 3i16, 4u16, 5i32, 6u32, 7i64, 8u64, nil]
b = StaticArray(UInt8, 256).new(0)
c = {-1_i8, 2_u8}
d = {str: "test", val: 3_u8}
e = Nil

puts "a.class=#{a.class}"
puts "c.class=#{c.class}"
puts "d.class=#{d.class}"
puts a

puts e

a && a.each do |value|
  puts "a = #{value}"
end

TestClass.updateTestClassVar(1379_u64)

a = nil

a.each do |val|
  puts "a = #{var}"
end if a

(1u8..255u8).each_with_index do |val, idx|
#  raise "Error is here" if idx >= 5
  b[idx] = val
  puts "val=#{val}, b[#{idx}] = #{b[idx]}"
end

puts "b = #{b}"

so results looks like here after it runs until the last line where I put a break point:

val=238, b[237] = 238
val=239, b[238] = 239
val=240, b[239] = 240
val=241, b[240] = 241
val=242, b[241] = 242
val=243, b[242] = 243
val=244, b[243] = 244
val=245, b[244] = 245
val=246, b[245] = 246
val=247, b[246] = 247
val=248, b[247] = 248
val=249, b[248] = 249
val=250, b[249] = 250
val=251, b[250] = 251
val=252, b[251] = 252
val=253, b[252] = 253
val=254, b[253] = 254
val=255, b[254] = 255
Process 63890 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 3.1
    frame #0: 0x0000000100005bfe debug_tests`__crystal_main at debug_tests.cr:43:6
   40  	  puts "val=#{val}, b[#{idx}] = #{b[idx]}"
   41  	end
   42
-> 43  	puts "b = #{b}"
Target 0: (debug_tests) stopped.
(lldb) v
((Array(Int16 | Int32 | Int64 | Int8 | UInt16 | UInt32 | UInt64 | UInt8 | Nil) | Nil) *) a = 0x0000000000000000
(unsigned char [256]) b = "\x01\x02\x03\x04\x05\x06\a\b\t\n\v\f\r\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~\x7f\x80\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f������������������������������������������������������������������������������������������������
(Tuple(Int8, UInt8)) c = ([0] = '�', [1] = '\x02')
(NamedTuple(str: String, val: UInt8)) d = (str = "test", val = '\x03')
((Int16 | Int32 | Int64 | Int8 | UInt16 | UInt32 | UInt64 | UInt8 | Nil) *) __temp_487 = 0x000000010194cf00
((Int16 | Int32 | Int64 | Int8 | UInt16 | UInt32 | UInt64 | UInt8 | Nil)) value = {
  type_id = 0
  union = (Int16 = 0, Int32 = 0, Int64 = 0, Int8 = '\0', UInt16 = 0, UInt32 = 0, UInt64 = 0, UInt8 = '\0')
}
(unsigned char) val = '�'
(int) idx = 254

It is working fine under CodeLLDB and it respects breakpoints.

Update: Not anymore. Looks like CodeLLDB having some issues with python on macOS Catalina.
It gives me following error that I have to check how to handle it:

2020-07-14T15:58:38Z ERROR codelldb::find_python] "dlopen(/usr/local/Cellar/python@3.8/3.8.3_2/Frameworks/Python.framework/Versions/3.8/Python3, 9): image not found"
Listening on port 49381
Received signal: SIGSEGV
   0: backtrace::backtrace::trace
   1: backtrace::capture::Backtrace::new
   2: codelldb::hook_crashes::handler
   3: __sigtramp
   4: __PyErr_Restore
   5: __PyErr_FormatV
   6: __PyErr_Format
   7: _PyModule_GetDict
   8: _init_locale
... skipped for brevity ...
  53: _PyImport_ImportModule
  54: _Py_FatalError
  55: _Py_InitializeEx

Debug adapter exit code=255, signal=null.

If I create a link to map Python to Python3 in that path then it just dies with SIGSEGV.

Ok, doing those steps worked.

[09:19AM] sandbox$ crystal build --debug a.cr
[09:20AM] sandbox$ lldb a
(lldb) target create "a"
Current executable set to '/Users/jeremywoertink/Development/crystal/sandbox/a' (x86_64).
(lldb) b __crystal_main
Breakpoint 1: where = a`__crystal_main + 38 at a.cr:1:1, address = 0x00000001000017b6
(lldb) r
Process 9413 launched: '/Users/jeremywoertink/Development/crystal/sandbox/a' (x86_64)
Process 9413 stopped
* thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.1
    frame #0: 0x00000001000017b6 a`__crystal_main at a.cr:1:1
-> 1   	require "lexbor"
   2   	require "crystagiri"
   3
   4   	input = <<-HTML
   5   	<!DOCTYPE html>
   6   	<html lang="en">
   7   	<head>
(lldb) v
(String *) input = 0x0000000000000000
(Crystagiri::HTML *) document = 0x0000000000000000
(lldb)

As for VSCode, I guess there’s still issues with it?

Thanks for the help!

2 Likes

You are very welcome!

As of CodeLLDB I afraid they stuck somehow with this issue. We can try NativeDebug but their support of Python extensions are non-existent.
I may end up with cloning NativeDebug or CodeLLVM and modify it to my liking to allow to work with Crystal debugger smoothly.

1 Like

@ComputerMage I’m using it in visual studio code and it is very helpful.
I used it with steps that @bcardiff has mentioned.
but to get it worked I also made settings.json like this:

{
    "gulp.autoDetect": "off",
    "typescript.tsc.autoDetect": "off",
    "npm.autoDetect": "off",
    "jake.autoDetect": "off",
    "grunt.autoDetect": "off",
    "task.autoDetect": "off"
}

but sometimes I still need to use pp my_var for example this sample code:

def my_def(val)
    if val ==1
        1
    else
        true
    end
end
my_val =my_def(1)
pp my_val

If you put a breakpoint on line 9 and you will see something like this:

my_val: (163, (true,1))

so I could not find the current value of “my_value” and I use pp for that,
and If you change the condition you would see:

my_val: (174, (true,1))

1 Like

The issue here is how crystal stores a union types like in your case when you have {UInt64, Bool}.
So first value in the response is the id that is associated with Union type (will be the same within the same program), and then values for types associated with this union type.
So now you know how to read it it will be easier for you to understand the values in the lldb, CodeLLDB or similar.

How could I know the value of my_val?
In both case shows (true,1).
But the real value is 1 or true.
If I use

> puts my_val

it prints the correct value not both.

There’s no way you can know that. The runtime knows that 163 is Bool and 174 is Int32, but you don’t have that information.

I think 174 is bool and 163 is Int32.
@asterite @ComputerMage
can we change this?
showing TYPE of value instead of showing the the ID of value
for example:
my_val: (bool, (true,1))

It can’t be done without adding a runtime table that holds all these ids and their corresponding string representation But even then, ideally you would see my_val: 1 or my_value: true because that’s what you care about, but there’s no way to implement that.

My suggestion: use puts, p, p! and pp to debug things.

1 Like

I was thinking about exposing that info to DWARF.
But at the same time I am exposing types as a name of the union element, so you can see it in the debugger what type that data belongs but not the current type it behaves. If I expose all TypeID to the runtime and then will make a helper to show it correctly then yes, it will be shown that way. Still thinking how to do it correctly.

The compiler should probably generate this table, then expose it via, for example Crystal::Runtime.types_hash or something like that. That will probably have a well-known name in the generated code so that a debugger tool could use it.

I was thinking to store them as a DIEs in DWARF but your approach is good too.
It will not be injected if it will be compiled as release or as just line number info so it should not fatten the application binary code.

Hi,
My dev environment is Vim, and till now, essentially use puts to debug my code !
As of Vim 8.0, there is an integrated debugger (:Termdebug) in Vim, but which relies on gdb exclusively.
I tried it on my Crystal code, but hardly could use it, probably because of my lack of experience, or perhaps also because this environment is not yet ready for Crystal debugging ?
Thanks for more infos on this.

Gdb should okay as well. There type helpers are not ready for gdb though but you should see the debug info for variables given that you compiled program with --debug flag.