Skip to content

[DebugInfo] Add a verifier pass to find holes in lexical scopes. #13491

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
merged 1 commit into from
Jan 18, 2018

Conversation

dcci
Copy link
Member

@dcci dcci commented Dec 16, 2017

WIP, this can't be merged yet, it's just an experiment I and Adrian discussed yesterday.
OTOH, it apparently already caught a violation I'm investigating. It can't be enabled by default until at least we self-host with this on, but it seems like there's some value in being more strict here.

AlreadySeenScopes.insert(LastSeenScope);
for (SILInstruction &SI : *BB) {
auto *DS = SI.getDebugScope();
if (!AlreadySeenScopes.count(DS)) {
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: if (AlreadySeenScopes.insert(DS).second)?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, definitely. I'll change it :)

@JDevlieghere
Copy link
Contributor

I don't know much (~nothing) about the SILDebugScope representation, but I'm curious about the heuristic. If I read the code correctly, every instruction that doesn't introduce a new scope should have the same scope as the last one. Is it not possible that things are nested and that you re-encounter a previously seen scope?

@dcci
Copy link
Member Author

dcci commented Dec 19, 2017

@JDevlieghere From what I see this is not possible at -Onone, but the scenario you mention is definitely possible at higher optimizations level. @adrian-prantl can confirm/deny.

@adrian-prantl
Copy link
Contributor

@JDevlieghere is correct. We need to add an additional check: if the "re-entered" scope is a (non-inlined) parent of the previous instruction, then this is okay.

@adrian-prantl
Copy link
Contributor

For example:

{
  let i = 0; // scope 0
  if (cond) {
    let j = i // scope 1
  }
  let k = i + 1 // scope 0 and okay.
}

@dcci
Copy link
Member Author

dcci commented Dec 20, 2017

Updated after your comments. Thanks for your review!

It looks like this has found a case where -Onone breaks debug info.
This instruction breaks:

copy_addr %1 to [initialization] %2 : $*S, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:28, scope 2 // id: %5

Here we have something like:

alloc_stack -> scope 2
debug_value -> scope 1
debug_value -> scope 1
debug_value -> scope 2
SIL verification failed: Found an hole in debug lexical scopes: DS == LastSeenScope
In function:
sil_scope 1 { loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":200:13 parent @_T011ArrayAppend17appendFromGenericySay7ElementQzGz5array_x8sequencets8SequenceRzlF : $@convention(thin) <τ_0_0 where τ_0_0 : Sequence> (@inout Array<τ_0_0.Element>, @in τ_0_0) -> () }
sil_scope 2 { loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":204:1 parent 1 }

// appendFromGeneric<A>(array:sequence:)
sil [noinline] @_T011ArrayAppend17appendFromGenericySay7ElementQzGz5array_x8sequencets8SequenceRzlF : $@convention(thin) <S where S : Sequence> (@inout Array<S.Element>, @in S) -> () {
// %0                                             // users: %6, %3
// %1                                             // users: %10, %5, %4
bb0(%0 : $*Array<S.Element>, %1 : $*S):
  %2 = alloc_stack $S, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:28, scope 2 // users: %12, %8, %5
  debug_value_addr %0 : $*Array<S.Element>, var, name "array", argno 1, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":202:3, scope 1 // id: %3
  debug_value_addr %1 : $*S, let, name "sequence", argno 2, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":202:38, scope 1 // id: %4
  copy_addr %1 to [initialization] %2 : $*S, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:28, scope 2 // id: %5
  %6 = begin_access [modify] [static] %0 : $*Array<S.Element>, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:3, scope 2 // users: %9, %8
  // function_ref Array.append<A>(contentsOf:)
  %7 = function_ref @_T0Sa6appendyqd__10contentsOf_t7ElementQyd__Rszs8SequenceRd__lF : $@convention(method) <τ_0_0><τ_1_0 where τ_0_0 == τ_1_0.Element, τ_1_0 : Sequence> (@in τ_1_0, @inout Array<τ_0_0>) -> (), loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:9, scope 2 // user: %8
  %8 = apply %7<S.Element, S>(%2, %6) : $@convention(method) <τ_0_0><τ_1_0 where τ_0_0 == τ_1_0.Element, τ_1_0 : Sequence> (@in τ_1_0, @inout Array<τ_0_0>) -> (), loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:9, scope 2
  end_access %6 : $*Array<S.Element>, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:3, scope 2 // id: %9
  destroy_addr %1 : $*S, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":204:1, scope 2 // id: %10
  %11 = tuple (), loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":204:1, scope 2 // user: %13
  dealloc_stack %2 : $*S, loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":203:28, scope 2 // id: %12
  return %11 : $(), loc "/Users/dcci/work/swift/swift/benchmark/single-source/ArrayAppend.swift":204:1, scope 2 // id: %13
} // end sil function '_T011ArrayAppend17appendFromGenericySay7ElementQzGz5array_x8sequencets8SequenceRzlF'

Is this a valid scenario? From what I see scope 2 is a child of scope 1 so we shouldn't really escape from scope 1 and get to scope 2 in the same SIL Basic block


// Visit the instructions in the BB, to find holes in the lexical scopes.
// We use a fairly simple heuristic here (Explain heuristic).
llvm::DenseSet<const SILDebugScope *> AlreadySeenScopes;
Copy link
Member Author

Choose a reason for hiding this comment

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

This probably is growing enough that can be split in its own function. I've been doing this when I was at 30k feet and I didn't want to lose this snapshot :)

}
if (DS != LastSeenScope) {
llvm::dbgs() << "Broken instruction!\n"; SI.dump();
require(DS == LastSeenScope, "Found an hole in debug lexical scopes");
Copy link
Member Author

Choose a reason for hiding this comment

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

require is printing the wrong instruction (always the last one in the block). We probably need an overload where we pass the instruction to print.

@@ -314,7 +314,7 @@ int main(int argc, char **argv) {
// Setup the SIL Options.
SILOptions &SILOpts = Invocation.getSILOptions();
SILOpts.InlineThreshold = SILInlineThreshold;
SILOpts.VerifyAll = EnableSILVerifyAll;
SILOpts.VerifyAll = true;
Copy link
Member Author

Choose a reason for hiding this comment

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

This (and the other changes to LLVMOpt/CompilerInvocation seem to be required to trigger issues, otherwise we don't run the verifier after each pass). Of course this can't go in as is, but it seems to be valuable to keep this hack around in the PR for testing purposes.


// Visit the instructions in the BB, to find holes in the lexical scopes.
// We use a fairly simple heuristic here (Explain heuristic).
llvm::DenseSet<const SILDebugScope *> AlreadySeenScopes;
Copy link
Contributor

Choose a reason for hiding this comment

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

(nit) I'd move this down until after the additional checks, closer to the for loop.

if (!AlreadySeenScopes.count(DS)) {
AlreadySeenScopes.insert(DS);
LastSeenScope = DS;
continue;
Copy link
Contributor

@JDevlieghere JDevlieghere Dec 20, 2017

Choose a reason for hiding this comment

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

If you always continue in the if-branch you might as well omit the else.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done

Copy link
Contributor

@JDevlieghere JDevlieghere left a comment

Choose a reason for hiding this comment

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

Two minor nits, but the heuristic/algorithm looks sound to me.

@dcci dcci force-pushed the lexicalscopes branch 5 times, most recently from fe2b6e2 to 61097f5 Compare January 17, 2018 19:07
@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

After fixing the issues that this verifier check exposes, this is ready for review/commit.
@adrian-prantl, can I ask you to take another fresh look?

Copy link
Contributor

@vedantk vedantk left a comment

Choose a reason for hiding this comment

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

Looks good to me.

return;
SILInstruction &First = *BB->begin();
const SILDebugScope *LastSeenScope = First.getDebugScope();
AlreadySeenScopes.insert(LastSeenScope);
Copy link
Contributor

Choose a reason for hiding this comment

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

This doesn't yet handle inlined scopes. I think the right way to handle them here is to insert the scope of the inlined call site into the map. This way all inlined instructions belong to the same scope (the scope of the apply instruction that was inlined).

We also need to to restrict this to only run at -ONone. Non-mandatory optimizations may legitimately hoist sink or merge instructions.

Copy link
Member Author

Choose a reason for hiding this comment

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

Can the inlined scope be handled as a follow up I assume?
About -Onone, I think this code should take care of it:

    // This check only makes sense at -Onone. Optimizations,
    // e.g. inlining, can move scopes around.
    llvm::DenseSet<const SILDebugScope *> AlreadySeenScopes;
    if (BB->getParent()->getEffectiveOptimizationMode() !=
        OptimizationMode::NoOptimization)
      return;

LastSeenScope = DS;
continue;
}
require(DS == LastSeenScope, "Found an hole in debug lexical scopes");
Copy link
Contributor

Choose a reason for hiding this comment

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

The check looks good!
perhaps: "Basic block contains a non-contiguous lexical scope at -Onone"

Copy link
Member Author

Choose a reason for hiding this comment

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

Good idea, done!

@@ -64,6 +64,10 @@ class SILDebugScope : public SILAllocated<SILDebugScope> {
/// into.
SILFunction *getParentFunction() const;

PointerUnion<const SILDebugScope *, SILFunction *> getParent() const {
Copy link
Contributor

Choose a reason for hiding this comment

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

I think we should either have an accessor, or make the field public, but not both.

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -4338,6 +4342,76 @@ class SILVerifier : public SILVerifierBase<SILVerifier> {
}
}

// This pass verifies that there are no hole in debug scopes at -Onone.
Copy link
Contributor

Choose a reason for hiding this comment

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

///

Copy link
Member Author

Choose a reason for hiding this comment

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

Done.

@@ -56,6 +56,10 @@ static llvm::cl::opt<bool> AbortOnFailure(
"verify-abort-on-failure",
llvm::cl::init(true));

static llvm::cl::opt<bool> VerifyDIHoles(
Copy link
Contributor

Choose a reason for hiding this comment

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

Is this option really necessary, shouldn't it just always be on?

Copy link
Member Author

Choose a reason for hiding this comment

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

I think this is good while we're transitioning, so that if something breaks, we can just turn the option off and investigate rather than reverting the whole commit.

@dcci dcci changed the title [DebugInfo/WIP] Add a verifier pass to find holes in lexical scopes. [DebugInfo] Add a verifier pass to find holes in lexical scopes. Jan 17, 2018
@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

Addressed comments & replied. Thanks for your reviews, @adrian-prantl/ @vedantk !

@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

@swift-ci please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 61097f52ffd9f65b98389e2fe29f9fc7182c748e

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 61097f52ffd9f65b98389e2fe29f9fc7182c748e

@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

@swift-ci please test

@swift-ci
Copy link
Contributor

Build failed
Swift Test OS X Platform
Git Sha - 0adfd6e79b0f194d4ab8f9c4baf5cc64eca33d49

@swift-ci
Copy link
Contributor

Build failed
Swift Test Linux Platform
Git Sha - 0adfd6e79b0f194d4ab8f9c4baf5cc64eca33d49

@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

Apparently this found a bug in swiftpm :)

1.	While running pass #557 SILModuleTransform "MandatoryInlining".
2.	While verifying SIL function "@$S5Basic25LocalFileOutputByteStreamC_13closeOnDeinitAcA12AbsolutePathV_SbtKcfc".
 for 'init(_:closeOnDeinit:)' at /home/buildnode/jenkins/workspace/swift-PR-Linux/branch-master/swiftpm/Sources/Basic/OutputByteStream.swift:657:12

@vedantk
Copy link
Contributor

vedantk commented Jan 17, 2018

Just a heads-up that you'll want to run "AT swift-ci Please test source compatibility" before checking this in. That contains a large suite of swift projects which we'll want to run the verifier over.

@vedantk
Copy link
Contributor

vedantk commented Jan 17, 2018

In fact
@swift-ci Please test source compatibility

This found a bunch of issues where we didn't properly preserve
debug infos which have been fixed over the course of the past
two weeks.

The basic idea is we want to make sure that within a BB we never
have `holes` in debug scopes, i.e. if we enter a scope, one of
the two holds

1) We never visited that scope before
2) We're re-entering a scope that is an ancestor of the scope
   currently leaving. This is needed to correctly model nested
   scopes, e.g.
    {
      let i = 0; // scope 0
      if (cond) {
        let j = i // scope 1
      }
      let k = i + 1 // scope 0 and okay.
    }

I also checked in a `cl::opt`, so that if this breaks something,
it can be conveniently disabled. This is currently not enabled
as we flesh out the last violations.

<rdar://problem/35816212>
@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

@swift-ci please test and merge

@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

@swift-ci Please test source compatibility

@shahmishal
Copy link
Member

@swift-ci test source compatibility

@dcci
Copy link
Member Author

dcci commented Jan 17, 2018

I'm currently merging this with the support disabled, as this is breaks a lot of code in the wild probably (and unfortunately, I'd add). I plan to write an RFC to send to swift-dev so that people can check & report bugs, and then I'll enable this after a grace period.

@dcci
Copy link
Member Author

dcci commented Jan 18, 2018

@swift-ci please smoke test and merge

@dcci
Copy link
Member Author

dcci commented Jan 18, 2018

@swift-ci test and merge

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants