swift: LLDB missing variables in certain case

For some certain swift code, like:

// Dog.swift
class Dog: NSObject {
  var wang : Bool?
}
// main.swift

let wang = Dog().wang ?? false
if wang { //Break here
  print("this line is never reached") // <--
}

When we debug and stop at the Break here line, the debugger doesn’t show the value of wang.

There are also some details behavior:

  1. If we use that wang variable later (except the if expression), like print(wang). The issue disappears
  2. If we define class Dog inside the same file main.swift (not another file Dog.swift. The issue disappears

We have tried to find out reasons and have one guess: During SIL Canonical stage, the Swift compiler may probably do some optimizations to such unused variables, which leads to the loss of debug value. For example:

%30 = load %8 : $*Bool, loc "/Users/ccc/Desktop/PHITest/PHITest/ViewController.swift":18:27, scope 2 // users: %89, %31, %33
%33 = struct_extract %30 : $Bool, #Bool._value, loc "/Users/ccc/Desktop/PHITest/PHITest/ViewController.swift":20:9, scope 3 // user: %34

This SIL might be optimized as:

%31 = load %30 : $*Builtin.Int1, loc "/Users/ccc/Desktop/PHITest/PHITest/ViewController.swift":18:27, scope 2 // user: %33

Maybe this process leads to the loss of debug value of wang which cause the missing display of value of wang in LLDB?

Steps to reproduce

The demo which can trigger this bug is attached in this PHITest.zip file.

By setting breakpoints at if(wang) in ViewController.swift and debug this project you can see the lldb debugger doesn’t show the value of wang.

PHITest.zip

Expected behavior

The LLDB debugger should show the value of wang, which should be false.

Environment

  • Swift compiler version info: Swift 5.7
  • Xcode version info: Xcode 14.0/14.1
  • Deployment target: iOS15+ (reproducible in lower firmware as well)

About this issue

  • Original URL
  • State: closed
  • Created 2 years ago
  • Comments: 15 (7 by maintainers)

Commits related to this issue

Most upvoted comments

The exclusivity checker verifies that the same variable is not accessed at the same time that it is being modified using a different name (for example via closure captures). People expect to be able to access two fields of the same struct simultaneously though: foo(&s.a, &s.b) Initially, that looks illegal to the checker. Diagnosing that correctly as valid code requires load splitting. The way that we hide the original load from the checker is by wrapping it in an unsafe access scope

%access = begin_access [unsafe] %adr
load %access
end_access %access

Thank you for the report and for the investigation everyone.

I’ve reduced this to the following example:

func f() {
    let wang = Optional<Bool>.none ?? false
    if wang {
      print("wang")
    }
}
f()

It does look like the compiler is dropping debug info for wang, there’s no mentions of wang in the dwarfdump output for the example, but if we add a usage of wang after the if statement (like print(wang)), then it shows up as:

0x00000055:     DW_TAG_subprogram
                  DW_AT_low_pc	(0x0000000100003d0c)
                  DW_AT_high_pc	(0x0000000100003eb0)
                  DW_AT_frame_base	(DW_OP_reg29 W29)
                  DW_AT_linkage_name	("$s1t1fyyF")
                  DW_AT_name	("f")
                  DW_AT_decl_file	("/tmp/t.swift")
                  DW_AT_decl_line	(1)
                  DW_AT_type	(0x0000017e "$sytD")
                  DW_AT_external	(true)

0x00000072:       DW_TAG_lexical_block
                    DW_AT_low_pc	(0x0000000100003d18)
                    DW_AT_high_pc	(0x0000000100003eb0)

0x0000007f:         DW_TAG_variable
                      DW_AT_location	(DW_OP_fbreg -8)
                      DW_AT_name	("wang")
                      DW_AT_decl_file	("/tmp/t.swift")
                      DW_AT_decl_line	(2)
                      DW_AT_type	(0x00000193 "const Swift::Bool")

Here’s a PR the fix I suggested. It needed to be extended to handle general problem, and there were a lot of difficult details to handle.

PR: https://github.com/apple/swift/pull/62672

Thanks for debugging and reducing!

I’m not sure of the status of LLVM’s “debug fragmants” feature. Regardless, I don’t think we want to rely on that feature for debug (Onone) builds.

The split-loads pass “lowers” the load of Bool into a load of Bool._value. We need to run this pass at Onone to avoid breaking source. We can’t move the debug_value to the new load because it’s the wrong type, and only a “fragment” of the original value.

As a very crude hack, we could simply move the debug_value from the loaded value to its address. I’m afraid in some cases that could report an incorrect debug value.

A better hack is probably the one I outlined in the FIXME. Keep both the original and the new lowered load but “hide” the original one from the exclusivity checker.

I can try to implement that very soon, like next week. If someone else wants to go for it, I can review it.

Reopening because the fix was reverted.

It’s mostly related to the compiler side SILGen optimization ? Maybe the dwarf info generated by compiler is wrong.

It would be surprising if it was since we are seeing this issue in debug mode. Although some mandatory SIL passes runs in that case, but I’m no expert so it could be…

Here are some interesting thing I was able to narrow down from this issue: It looks like is related to operator ??, in fact to if m is a result of a call to any generic function

@inline(never)
func hole<T>(_ : T) {}

class A {
  var m : Bool? = true
}

func unwrap<T>(_ optional: T?, _ defaultValue: @autoclosure () throws -> T)
    rethrows -> T {
  switch optional {
  case .some(let value):
    return value
  case .none:
    return try defaultValue()
  }
}

// main.swift
func testFn() {
  let m = unwrap(A().m, false)
  if m { // error: no variable named 'm' found in this frame
    hole("this line is never reached")
  }
}

func testFn1() {
  let m: Bool 
  m = unwrap(A().m, false)
  if m { // OK
    hole("this line is never reached") 
  }
}

testFn()
testFn1()

Also ok if unwrap is not-generic or we define a non-generic ?? operator specialization for Bool. So lldb compiler seems to be having trouble with getting information from generic context, but from that point on wasn’t able to debug further.

Maybe the dwarf info generated by compiler is wrong.

I’m not familiar enough with lldb to affirm anything, so it is possible. But my guess is that the error error: expression failed to parse is something that indicates an issue with lldb compiler because logs aren’t even able to show a type checked AST for lookup the variable, because something is failing when trying to parse source in context, so no sure how could that be related to missing/wrong debug info, but again not an expert in lldb or anything…

cc @adrian-prantl @augusto2112