Skip to content

Fix DestroyHoisting: InteriorLiveness for noescape closures #79985

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
Mar 18, 2025

Conversation

atrick
Copy link
Contributor

@atrick atrick commented Mar 13, 2025

InteriorLiveness has a new "visitInnerUses" mode used by DestroyHoisting. That mode may visit dependent values, which was not valid for noescape closures. ClosureLifetimeFixup inserts destroys of noescape closures after the destroys of the captures. So following such dependent value could result in an apparent use-after-destroy. This causes DestroyHoisting to insert redundant destroys.

Fix: InteriorUses will conservatively only follow dependent values if they are escapable. Non-escapable values, like noescape closures are now considered escapes of the original value that the non-escapable value depends on. This can be improved in the future, but we may want to rewrite ClosureLifetimeFixup first.

Fixes the root cause of: rdar://146142041 (PR #79841)

@atrick atrick requested a review from meg-gupta March 13, 2025 07:46
@atrick atrick requested review from eeckstein and removed request for eeckstein and jckarter March 13, 2025 07:47
@atrick
Copy link
Contributor Author

atrick commented Mar 13, 2025

@swift-ci test

Copy link
Contributor

@eeckstein eeckstein left a comment

Choose a reason for hiding this comment

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

lgtm

type.objectType.isEscapable(in: parentFunction)
}

public var mayEscape: Bool {
Copy link
Contributor

@eeckstein eeckstein Mar 13, 2025

Choose a reason for hiding this comment

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

Can you add a comment also here? E.g. "Like isEscapable, but also returns true for not-escaping functions.".
And ideally also for Type.mayEscape?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

extension Type {
  /// Return true if this type conforms to Escapable.
  ///
  /// Note: invalid types are non-Escapable, so take care not to make assumptions about non-Escapable types.
  public func isEscapable(in function: Function) -> Bool {
    bridged.isEscapable(function.bridged)
  }

  /// Return true if this type conforms to Escapable and is not a noescape function type.
  ///
  /// Warning: may return true for (source-level) non-escaping closures. After SILGen, all partial_apply instructions
  /// have an escaping function type. ClosureLifetimeFixup only changes them to noescape function type if they are
  /// promoted to [on_stack]. But regardless of stack promotion, a non-escaping closure value's lifetime is constrained
  /// by the lifetime of its captures. Use Value.mayEscape instead to check for this case.
  public func mayEscape(in function: Function) -> Bool {
    !isNoEscapeFunction && isEscapable(in: function)
  }

extension Value {
  /// Return true if the object type conforms to Escapable.
  ///
  /// Note: noescape function types conform to Escapable, use mayEscape instead to exclude them.
  public var isEscapable: Bool {
    type.objectType.isEscapable(in: parentFunction)
  }

  /// Return true only if this value's lifetime is unconstrained by an outer lifetime. Requires all of the following:
  /// - the object type conforms to Escapable
  /// - the type is not a noescape function
  /// - the value is not the direct result of a partial_apply with a noescape (inout_aliasable) capture.
  public var mayEscape: Bool {
    if !type.objectType.mayEscape(in: parentFunction) {
      return false
    }
    // A noescape partial_apply has an escaping function type if it has not been promoted to on_stack, but it's value
    // still cannot "escape" its captures.
    //
    // TODO: This would be much more robust if pai.hasNoescapeCapture simply implied !pai.type.isEscapable
    if let pai = self as? PartialApplyInst {
      return pai.mayEscape
    }
    return true
  }

InteriorLiveness has a new "visitInnerUses" mode used by DestroyHoisting. That
mode may visit dependent values, which was not valid for noescape
closures. ClosureLifetimeFixup inserts destroys of noescape closures after the
destroys of the captures. So following such dependent value could result in an
apparent use-after-destroy. This causes DestroyHoisting to insert redundant
destroys.

Fix: InteriorUses will conservatively only follow dependent values if they are
escapable. Non-escapable values, like noescape closures are now considered
escapes of the original value that the non-escapable value depends on. This can
be improved in the future, but we may want to rewrite ClosureLifetimeFixup first.

Fixes the root cause of: rdar://146142041
@atrick atrick force-pushed the fix-destroyhoist branch from 444c718 to 995c749 Compare March 13, 2025 16:17
@atrick atrick enabled auto-merge March 13, 2025 16:17
@atrick
Copy link
Contributor Author

atrick commented Mar 13, 2025

@swift-ci smoke test

@meg-gupta
Copy link
Contributor

lgtm

@atrick
Copy link
Contributor Author

atrick commented Mar 15, 2025

@swift-ci smoke test

1 similar comment
@atrick
Copy link
Contributor Author

atrick commented Mar 18, 2025

@swift-ci smoke test

@atrick atrick merged commit ce18a4b into swiftlang:main Mar 18, 2025
3 checks passed
@atrick atrick deleted the fix-destroyhoist branch March 19, 2025 19:07
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.

3 participants