[DebugInfo] Salvage more in -O builds

Specifically, improved debug info retention in:
* tryReplaceRedundantInstructionPair,
* splitAggregateLoad,
* TempLValueElimination,
* Mem2Reg,
* ConstantFolding.

The changes to Mem2Reg allow debug info to be retained in the case tested by
self-nostorage.swift in -O builds, so we have just enabled -O in that file
instead of writing a new test for it.

We attempted to add a case to salvageDebugInfo for unchecked_enum_data, but it
caused crashes in Linux CI that we were not able to reproduce.
This commit is contained in:
Aidan Hall
2025-10-23 17:38:25 +01:00
parent e62153187a
commit a95d2979f9
13 changed files with 170 additions and 8 deletions

View File

@@ -180,6 +180,12 @@ private func tryEliminate(copy: CopyLikeInstruction, _ context: FunctionPassCont
use.set(to: copy.destinationAddress, context)
}
}
// Salvage the debug variable attribute, if present.
if let debugVariable = allocStack.debugVariable {
let builder = Builder(before: firstUseOfAllocStack, location: allocStack.location, context)
builder.createDebugValue(value: copy.destinationAddress, debugVariable: debugVariable)
}
context.erase(instruction: allocStack)
context.erase(instructionIncludingAllUsers: copy.loadingInstruction)
}

View File

@@ -842,6 +842,11 @@ extension SimplifyContext {
second.replace(with: replacement, self)
if canEraseFirst {
// We only need to salvage first's debug info when !preserveDebugInfo and
// it has no non-debug uses.
if !preserveDebugInfo {
first.salvageDebugInfo(self)
}
erase(instructionIncludingDebugUses: first)
}
}

View File

@@ -102,6 +102,13 @@ public class Instruction : CustomStringConvertible, Hashable {
context.notifyInstructionsChanged()
}
/// Transfer debug info associated with (the result of) this instruction to a
/// new `debug_value` instruction before this instruction is deleted.
public final func salvageDebugInfo(_ context: some MutatingContext) {
BridgedContext.salvageDebugInfo(self.bridged)
context.notifyInstructionsChanged()
}
public var mayTrap: Bool { false }
final public var mayHaveSideEffects: Bool {

View File

@@ -1534,6 +1534,7 @@ struct BridgedContext {
BRIDGED_INLINE void eraseBlock(BridgedBasicBlock block) const;
static BRIDGED_INLINE void moveInstructionBefore(BridgedInstruction inst, BridgedInstruction beforeInst);
static BRIDGED_INLINE void copyInstructionBefore(BridgedInstruction inst, BridgedInstruction beforeInst);
static BRIDGED_INLINE void salvageDebugInfo(BridgedInstruction inst);
// SSAUpdater

View File

@@ -41,6 +41,7 @@
#include "swift/SIL/SILVTable.h"
#include "swift/SIL/SILWitnessTable.h"
#include "swift/SILOptimizer/Utils/ConstExpr.h"
#include "swift/SILOptimizer/Utils/DebugOptUtils.h"
#include "swift/SIL/SILConstants.h"
#include <stdbool.h>
#include <stddef.h>
@@ -3129,6 +3130,10 @@ void BridgedContext::copyInstructionBefore(BridgedInstruction inst, BridgedInstr
inst.unbridged()->clone(beforeInst.unbridged());
}
void BridgedContext::salvageDebugInfo(BridgedInstruction inst) {
swift::salvageDebugInfo(inst.unbridged());
}
OptionalBridgedFunction BridgedContext::lookupStdlibFunction(BridgedStringRef name) const {
return {context->lookupStdlibFunction(name.unbridged())};
}

View File

@@ -1968,11 +1968,16 @@ void MemoryToRegisters::removeSingleBlockAllocation(AllocStackInst *asi) {
if (!runningVals) {
// Loading from uninitialized memory is only acceptable if the type is
// empty--an aggregate of types without storage.
const auto initialValue =
createEmptyAndUndefValue(asi->getElementType(), inst, ctx);
runningVals = {
LiveValues::toReplace(asi,
/*replacement=*/createEmptyAndUndefValue(
asi->getElementType(), inst, ctx)),
/*replacement=*/initialValue),
/*isStorageValid=*/!doesLoadInvalidateStorage(inst)};
if (auto varInfo = asi->getVarInfo()) {
SILBuilderWithScope(inst, ctx).createDebugValue(
inst->getLoc(), initialValue, *varInfo);
}
}
auto *loadInst = dyn_cast<LoadInst>(inst);
if (loadInst &&

View File

@@ -321,9 +321,8 @@ splitAggregateLoad(LoadOperation loadInst, CanonicalizeInstruction &pass) {
}
// Preserve the original load's debug information.
if (pass.preserveDebugInfo) {
swift::salvageLoadDebugInfo(loadInst);
}
swift::salvageLoadDebugInfo(loadInst);
// Remove the now unused borrows.
for (auto *borrow : borrows)
nextII = killInstAndIncidentalUses(borrow, nextII, pass);

View File

@@ -20,6 +20,7 @@
#include "swift/SIL/PatternMatch.h"
#include "swift/SIL/SILBuilder.h"
#include "swift/SILOptimizer/Utils/CastOptimizer.h"
#include "swift/SILOptimizer/Utils/DebugOptUtils.h"
#include "swift/SILOptimizer/Utils/InstOptUtils.h"
#include "swift/SILOptimizer/Utils/InstructionDeleter.h"
#include "llvm/ADT/APFloat.h"
@@ -1232,8 +1233,8 @@ static SILValue constantFoldIsConcrete(BuiltinInst *BI) {
return inst;
}
SILValue swift::constantFoldBuiltin(BuiltinInst *BI,
std::optional<bool> &ResultsInError) {
static SILValue constantFoldBuiltinWithoutSalvagingDebugInfo(BuiltinInst *BI,
std::optional<bool> &ResultsInError) {
const IntrinsicInfo &Intrinsic = BI->getIntrinsicInfo();
SILModule &M = BI->getModule();
@@ -1444,6 +1445,20 @@ case BuiltinValueKind::id:
return nullptr;
}
SILValue swift::constantFoldBuiltin(BuiltinInst *BI,
std::optional<bool> &ResultsInError) {
const auto value =
constantFoldBuiltinWithoutSalvagingDebugInfo(BI, ResultsInError);
// Salvage debug info of BI arguments if it was successfully folded.
if (value) {
for (auto arg : BI->getArguments()) {
if (auto *argInst = arg.getDefiningInstruction())
swift::salvageDebugInfo(argInst);
}
}
return value;
}
/// On success this places a new value for each result of Op->getUser() into
/// Results. Results is guaranteed on success to have the same number of entries
/// as results of User. If we could only simplify /some/ of an instruction's

View File

@@ -1,4 +1,4 @@
// RUN: %target-swift-frontend -primary-file %s -emit-ir -g -o - | %FileCheck %s
// RUN: %target-swift-frontend -primary-file %s -emit-ir -g -O -o - | %FileCheck %s
public struct S {
func f() {

View File

@@ -71,3 +71,26 @@ bb3:
// CHECK-IR: ![[DBG_VAR]] = !DILocalVariable(name: "hello"
struct T {
@_hasStorage let x: Builtin.Int32 { get }
init(x: Builtin.Int32)
}
// This test verifies that splitAggregateLoad in CanonicalizeInstruction.cpp
// salvages debug info attached to the loaded aggregate in optimized builds.
//
// CHECK-LABEL: sil @split_aggregate_load : $@convention(thin) (@in T) -> Builtin.Int32 {
// CHECK: bb0([[ARG:%[0-9]+]] : $*T):
// CHECK-NEXT: [[ELEM_ADDR:%[0-9]+]] = struct_element_addr [[ARG]] : $*T, #T.x
// CHECK-NEXT: [[ELEM:%[0-9]+]] = load [[ELEM_ADDR]]
// CHECK-NEXT: debug_value [[ARG]] : $*T, let, name "var", expr op_deref
// CHECK-NEXT: return [[ELEM]] : $Builtin.Int32
// CHECK-LABEL: } // end sil function 'split_aggregate_load'
sil @split_aggregate_load : $@convention(thin) (@in T) -> Builtin.Int32 {
bb0(%0 : $*T):
%1 = load %0
debug_value %1, let, name "var"
%2 = struct_extract %1, #T.x
return %2
}

View File

@@ -0,0 +1,43 @@
// RUN: %target-sil-opt -sil-print-types -enable-sil-verify-all %s -simplification -simplify-instruction=builtin -sil-print-debuginfo | %FileCheck %s
// REQUIRES: swift_in_compiler
import Swift
import Builtin
// This verifies that debug information attached to the arguments of builtin
// instructions is preserved when those instructions are constant-folded.
//
// CHECK-LABEL: sil @preserve_bitwise_operands : $@convention(thin) () -> Builtin.Int32 {
// CHECK: bb0:
// CHECK-NEXT: debug_value undef : $Builtin.Int32, let, name "x", expr op_constu:1
// CHECK-NEXT: debug_value undef : $Builtin.Int32, let, name "y", expr op_constu:2
// CHECK-NEXT: [[THREE:%[0-9]+]] = integer_literal $Builtin.Int32, 3
// CHECK-NEXT: return [[THREE]] : $Builtin.Int32
// CHECK-LABEL: } // end sil function 'preserve_bitwise_operands'
sil @preserve_bitwise_operands : $@convention(thin) () -> Builtin.Int32 {
bb0:
%0 = integer_literal $Builtin.Int32, 1
debug_value %0, let, name "x"
%1 = integer_literal $Builtin.Int32, 2
debug_value %1, let, name "y"
%2 = builtin "or_Int32"(%0, %1) : $Builtin.Int32
return %2
}
// CHECK-LABEL: sil @preserve_add_operands : $@convention(thin) () -> Builtin.Int32 {
// CHECK: bb0:
// CHECK-NEXT: debug_value undef : $Builtin.Int32, let, name "x", expr op_consts:4294967295
// CHECK-NEXT: debug_value undef : $Builtin.Int32, let, name "y", expr op_constu:2
// CHECK-NEXT: [[ONE:%[0-9]+]] = integer_literal $Builtin.Int32, 1
// CHECK-NEXT: return [[ONE]] : $Builtin.Int32
// CHECK-LABEL: } // end sil function 'preserve_add_operands'
sil @preserve_add_operands : $@convention(thin) () -> Builtin.Int32 {
bb0:
%0 = integer_literal $Builtin.Int32, -1
debug_value %0, let, name "x"
%1 = integer_literal $Builtin.Int32, 2
debug_value %1, let, name "y"
%2 = builtin "add_Int32"(%0, %1) : $Builtin.Int32
return %2
}

View File

@@ -0,0 +1,27 @@
// RUN: %target-sil-opt -sil-print-types -enable-sil-verify-all %s -simplification -simplify-instruction=struct_extract -sil-print-debuginfo | %FileCheck %s
// REQUIRES: swift_in_compiler
import Builtin
import Swift
struct T {
var x: Builtin.Int32
}
// This verifies that tryReplaceRedundantInstructionPair in OptUtils.swift
// salvages debug info attached to the first instruction of the pair,
// in this case the struct.
//
// CHECK-LABEL: sil @redundant_pair : $@convention(thin) (Builtin.Int32) -> Builtin.Int32 {
// CHECK: bb0([[VAR:%[0-9]+]] : $Builtin.Int32):
// CHECK-NEXT: debug_value [[VAR]] : $Builtin.Int32, let, name "var", type $T, expr op_fragment:#T.x
// CHECK-NEXT: return [[VAR]]
// CHECK-LABEL: } // end sil function 'redundant_pair'
sil @redundant_pair : $@convention(thin) (Builtin.Int32) -> Builtin.Int32 {
bb0(%0 : $Builtin.Int32):
%1 = struct $T(%0)
debug_value %1, let, name "var"
%2 = struct_extract %1, #T.x
return %2
}

View File

@@ -0,0 +1,26 @@
// RUN: %target-sil-opt -sil-print-types -sil-verify-all -temp-lvalue-elimination %s | %FileCheck %s
sil_stage canonical
// REQUIRES: swift_in_compiler
import Builtin
// This verifies that the TempLValueElimination pass correctly salvages the
// debug variable attribute of alloc_stack instructions.
//
// CHECK-LABEL: sil @stack_var_elimination : $@convention(thin) (Builtin.Int32) -> @out Builtin.Int32 {
// CHECK: bb0([[X:%[0-9]+]] : $*Builtin.Int32, [[VAL:%[0-9]+]] : $Builtin.Int32):
// CHECK-NEXT: debug_value [[X]] : $*Builtin.Int32, var, name "x"
// CHECK-NEXT: store [[VAL]] to [[X]] : $*Builtin.Int32
// CHECK-NEXT: [[E:%[0-9]+]] = tuple ()
// CHECK-NEXT: return [[E]] : $()
// CHECK-LABEL: } // end sil function 'stack_var_elimination'
sil @stack_var_elimination : $@convention(thin) (Builtin.Int32) -> @out Builtin.Int32 {
bb0(%0 : $*Builtin.Int32, %1 : $Builtin.Int32):
%2 = alloc_stack [var_decl] $Builtin.Int32, var, name "x"
store %1 to %2
copy_addr %2 to %0
dealloc_stack %2
%3 = tuple ()
return %3
}