# Returns the current execution stack as an array containing strings # usually in the form file:line:column or file:line:column in 'method'. def caller : Array(String) Exception::CallStack.new.printable_backtrace end # :nodoc: struct Exception::CallStack # Compute current directory at the beginning so filenames # are always shown relative to the *starting* working directory. private CURRENT_DIR = Process::INITIAL_PWD.try { |dir| Path[dir] } @@skip = [] of String def self.skip(filename) : Nil @@skip << filename end skip(__FILE__) @@loaded = false # :nodoc: def self.load_debug_info : Nil Crystal.once(pointerof(@@loaded)) do return if ENV["CRYSTAL_LOAD_DEBUG_INFO"]? == "0" begin load_debug_info_impl rescue ex Crystal::System.print_exception "Unable to load debug information", ex end end end @callstack : Array(Void*) @backtrace : Array(String)? def initialize(@callstack : Array(Void*) = CallStack.unwind) end class_getter empty = new([] of Void*) def printable_backtrace : Array(String) @backtrace ||= decode_backtrace end private def decode_backtrace {% if flag?(:wasm32) %} [] of String {% else %} CallStack.load_debug_info show_full_info = ENV["CRYSTAL_CALLSTACK_FULL_INFO"]? == "1" @callstack.compact_map do |ip| CallStack.decode_backtrace_frame(ip, show_full_info) end {% end %} end # :nodoc: def self.decode_backtrace_frame(ip, show_full_info) : String? pc = decode_address(ip) file, line_number, column_number = decode_line_number(pc) if file && file != "??" return if @@skip.includes?(file) # Turn to relative to the current dir, if possible if current_dir = CURRENT_DIR if rel = Path[file].relative_to?(current_dir) rel = rel.to_s file = rel unless rel.starts_with?("..") end end file_line_column = file unless line_number == 0 file_line_column = "#{file_line_column}:#{line_number}" file_line_column = "#{file_line_column}:#{column_number}" unless column_number == 0 end end if name = decode_function_name(pc) function = name elsif frame = decode_frame(ip) _, function, file = frame # Crystal methods (their mangled name) start with `*`, so # we remove that to have less clutter in the output. function = function.lchop('*') else function = "??" end if file_line_column if show_full_info && (frame = decode_frame(ip)) _, sname, _ = frame line = "#{file_line_column} in '#{sname}'" else line = "#{file_line_column} in '#{function}'" end else if file == "??" && function == "??" line = "???" else line = "#{file} in '#{function}'" end end if show_full_info line = "#{line} at 0x#{ip.address.to_s(16)}" end line end end {% if flag?(:interpreted) %} require "./call_stack/interpreter" {% elsif flag?(:win32) && !flag?(:gnu) %} require "./call_stack/stackwalk" {% elsif flag?(:wasm32) %} require "./call_stack/null" {% else %} require "./call_stack/libunwind" {% end %}