Skip to content

[MLIR][LLVM] CallSiteLoc without caller scope not translatable #90759

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

zyx-billy
Copy link
Contributor

If a CallSiteLoc's caller cannot be translated (returns null), we cannot simply return the translation of the callee location. This is because the resulting DILocation will not have any InlinedAt field for the function it is in, and will fail the LLVM IR verifier.


Example:

This situation occurs when we have a nested callsite loc, where the middle function in the stack does not have a debug scope

func A {
  foo loc(fused<#A>["a":1:1])
}
func B {
  call @A loc("b":1:1)
}
func C {
  call @B loc(fused<#C>["c":1:1])
}

When A is inlined into B, and B is inlined into C, we get

func C {
  foo loc(callsite(
            callsite(fused<#A>["a":1:1] at loc("b":1:1))
           at
            fused<#C>["c":1:1]))
}

We do not want to translate this into just a !DILocation(line: 1, column: 1, scope: !A), as that would not be legal in func C, so this is not something we can represent in LLVM IR.

@zyx-billy zyx-billy marked this pull request as ready for review May 1, 2024 20:28
@zyx-billy zyx-billy requested review from gysit and Dinistro May 1, 2024 20:28
@llvmbot
Copy link
Member

llvmbot commented May 1, 2024

@llvm/pr-subscribers-mlir-llvm

@llvm/pr-subscribers-mlir

Author: Billy Zhu (zyx-billy)

Changes

If a CallSiteLoc's caller cannot be translated (returns null), we cannot simply return the translation of the callee location. This is because the resulting DILocation will not have any InlinedAt field for the function it is in, and will fail the LLVM IR verifier.


Example:

This situation occurs when we have a nested callsite loc, where the middle function in the stack does not have a debug scope

func A {
  foo loc(fused&lt;#A&gt;["a":1:1])
}
func B {
  call @<!-- -->A loc("b":1:1)
}
func C {
  call @<!-- -->B loc(fused&lt;#C&gt;["c":1:1])
}

When A is inlined into B, and B is inlined into C, we get

func C {
  foo loc(callsite(
            callsite(fused&lt;#A&gt;["a":1:1] at loc("b":1:1))
           at
            fused&lt;#C&gt;["c":1:1]))
}

We do not want to translate this into just a !DILocation(line: 1, column: 1, scope: !A), as that would not be legal in func C, so this is not something we can represent in LLVM IR.


Full diff: https://github.com/llvm/llvm-project/pull/90759.diff

2 Files Affected:

  • (modified) mlir/lib/Target/LLVMIR/DebugTranslation.cpp (+4)
  • (modified) mlir/test/Target/LLVMIR/llvmir-debug.mlir (+6-3)
diff --git a/mlir/lib/Target/LLVMIR/DebugTranslation.cpp b/mlir/lib/Target/LLVMIR/DebugTranslation.cpp
index 2de5e372d88c0e..f02939abc5fe18 100644
--- a/mlir/lib/Target/LLVMIR/DebugTranslation.cpp
+++ b/mlir/lib/Target/LLVMIR/DebugTranslation.cpp
@@ -406,6 +406,10 @@ llvm::DILocation *DebugTranslation::translateLoc(Location loc,
   if (auto callLoc = dyn_cast<CallSiteLoc>(loc)) {
     // For callsites, the caller is fed as the inlinedAt for the callee.
     auto *callerLoc = translateLoc(callLoc.getCaller(), scope, inlinedAt);
+    // If the caller scope does not exist, the callsite cannot be represented
+    // in LLVM (the callee scope may not match the function it is in).
+    if (!callerLoc)
+      return nullptr;
     llvmLoc = translateLoc(callLoc.getCallee(), nullptr, callerLoc);
     // Fallback: Ignore callee if it has no debug scope.
     if (!llvmLoc)
diff --git a/mlir/test/Target/LLVMIR/llvmir-debug.mlir b/mlir/test/Target/LLVMIR/llvmir-debug.mlir
index 8ab1a1b290dad3..e8a3d65209afde 100644
--- a/mlir/test/Target/LLVMIR/llvmir-debug.mlir
+++ b/mlir/test/Target/LLVMIR/llvmir-debug.mlir
@@ -108,12 +108,15 @@ llvm.func @func_with_debug(%arg: i64) {
   // CHECK: call void @func_no_debug(), !dbg ![[NAMED_LOC:[0-9]+]]
   llvm.call @func_no_debug() : () -> () loc("named"("foo.mlir":10:10))
 
-  // CHECK: call void @func_no_debug(), !dbg ![[CALLSITE_LOC:[0-9]+]]
+  // CHECK: call void @func_no_debug(), !dbg ![[MY_SOURCE_LOC:[0-9]+]]
   llvm.call @func_no_debug() : () -> () loc(callsite("nodebug.cc":3:4 at "mysource.cc":5:6))
 
-  // CHECK: call void @func_no_debug(), !dbg ![[CALLSITE_LOC:[0-9]+]]
+  // CHECK: call void @func_no_debug(), !dbg ![[MY_SOURCE_LOC]]
   llvm.call @func_no_debug() : () -> () loc(callsite("nodebug.cc":3:4 at fused<#sp0>["mysource.cc":5:6]))
 
+  // CHECK: call void @func_no_debug(), !dbg ![[MY_SOURCE_LOC]]
+  llvm.call @func_no_debug() : () -> () loc(callsite(callsite(fused<#callee>["nodebug.cc":3:4] at "foo.mlir":2:4) at fused<#sp0>["mysource.cc":5:6]))
+
   // CHECK: call void @func_no_debug(), !dbg ![[FUSED_LOC:[0-9]+]]
   llvm.call @func_no_debug() : () -> () loc(fused[callsite(fused<#callee>["mysource.cc":5:6] at "mysource.cc":1:1), "mysource.cc":1:1])
 
@@ -153,7 +156,7 @@ llvm.func @empty_types() {
 // CHECK: ![[BLOCK_LOC]] = distinct !DILexicalBlock(scope: ![[FUNC_LOC]])
 // CHECK: ![[NO_NAME_VAR]] = !DILocalVariable(scope: ![[BLOCK_LOC]])
 
-// CHECK-DAG: ![[CALLSITE_LOC]] = !DILocation(line: 5, column: 6,
+// CHECK-DAG: ![[MY_SOURCE_LOC]] = !DILocation(line: 5, column: 6
 // CHECK-DAG: ![[FILE_LOC]] = !DILocation(line: 1, column: 2,
 // CHECK-DAG: ![[NAMED_LOC]] = !DILocation(line: 10, column: 10
 // CHECK-DAG: ![[FUSED_LOC]] = !DILocation(line: 1, column: 1

Copy link
Contributor

@gysit gysit left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Thanks for fixing!

@zyx-billy zyx-billy merged commit 64f6f90 into llvm:main May 2, 2024
zyx-billy added a commit that referenced this pull request May 3, 2024
…0915)

Followup to #90759.

Instead of just returning null when the caller scope is not
translatable, "jump over" the current caller scope and use the outer
scope as the caller if that is available. This means that in an inlined
call stack if there are frames without debug scope, those frames are
skipped to preserve what is available. In the original example where
```
func A {
  foo loc(fused<#A>["a":1:1])
}
func B {
  call @A loc("b":1:1)
}
func C {
  call @b loc(fused<#C>["c":1:1])
}
```
is inlined into
```
func C {
  foo loc(callsite(
            callsite(fused<#A>["a":1:1] at loc("b":1:1))
           at
            fused<#C>["c":1:1]))
}
```
The translated result would be `!1`:
```
!0 = !DILocation(line: 1, column: 1, scope: !C)
!1 = !DILocation(line: 1, column: 1, scope: !A, inlinedAt: !0)
```

This has a neat benefit in maintaining callsite associativity: No matter
if we have `callsite(callsite(A at B) at C)` or `callsite(A at
callsite(B at C))`, the translation now is the same. The previous
solution did not provide this guarantee, which meant the callsite
construction would somehow impact this translation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants