//===--- Devirtualization.swift -------------------------------------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2014 - 2023 Apple Inc. and the Swift project authors
// Licensed under Apache License v2.0 with Runtime Library Exception
//
// See https://swift.org/LICENSE.txt for license information
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
//
//===----------------------------------------------------------------------===//

import AST
import SIL

/// Devirtualizes all value-type deinitializers of a `destroy_value`.
///
/// This may be a no-op if the destroy doesn't call any deinitializers.
/// Returns true if all deinitializers could be devirtualized.
func devirtualizeDeinits(of destroy: DestroyValueInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
  if destroy.isDeadEnd {
    // It doesn't make sense to de-virtualize `destroy_value [dead_end]` because such operations
    // are no-ops anyway. This is especially important for Embedded Swift, because introducing
    // calls to generic deinit functions after the mandatory pipeline can result in IRGen crashes.
    return true
  }
  return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}

/// Devirtualizes all value-type deinitializers of a `destroy_addr`.
///
/// This may be a no-op if the destroy doesn't call any deinitializers.
/// Returns true if all deinitializers could be devirtualized.
func devirtualizeDeinits(of destroy: DestroyAddrInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
  return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}

func devirtualizeDeinits(of builtin: BuiltinInst, isMandatory: Bool, _ context: some MutatingContext) -> Bool {
  switch builtin.id {
  case .DestroyArray:
    return devirtualize(builtinDestroyArray: builtin, isMandatory: isMandatory, context)
  default:
    return true
  }
}

private func devirtualize(destroy: some DevirtualizableDestroy,
                          isMandatory: Bool,
                          _ context: some MutatingContext
) -> Bool {
  let type = destroy.type
  if !type.isMoveOnly {
    return true
  }

  if type.isBuiltinFixedArray {
    return devirtualizeFixedSizeArray(destroy: destroy, isMandatory: isMandatory, context)
  }

  guard let nominal = type.nominal else {
    // E.g. a non-copyable generic function parameter
    return true
  }

  // We cannot de-virtualize C++ destructor calls of C++ move-only types because we cannot get
  // its destructor (`nominal.valueTypeDestructor` is nil).
  if nominal.hasClangNode {
    return false
  }

  if nominal.valueTypeDestructor != nil && !destroy.shouldDropDeinit {
    guard let deinitFunc = context.lookupDeinit(ofNominal: nominal) else {
      return false
    }
    // In mandatory mode, de-virtualization might create function references to functions with wrong linkage.
    // MandatoryPerformanceOptimization fixes this later.
    if !isMandatory {
      // In non-mandatory mode, we don't allow this.
      let serialized = destroy.parentFunction.serializedKind
      guard serialized == .notSerialized || deinitFunc.hasValidLinkageForFragileRef(serialized) else {
        return false
      }
    }

    if deinitFunc.linkage == .shared && !deinitFunc.isDefinition {
      // Make sure to not have an external shared function, which is illegal in SIL.
      _ = context.loadFunction(function: deinitFunc, loadCalleesRecursively: false)
    }
    destroy.createDeinitCall(to: deinitFunc, context)
    context.erase(instruction: destroy)
    return true
  }
  // If there is no deinit to be called for the original type we have to recursively visit
  // the struct fields or enum cases.
  if type.isStruct {
    return destroy.devirtualizeStructFields(isMandatory: isMandatory, context)
  }
  if type.isEnum {
    return destroy.devirtualizeEnumPayloads(isMandatory: isMandatory, context)
  }
  precondition(type.isClass, "unknown non-copyable type")
  // A class reference cannot be further de-composed.
  return true
}

private func devirtualizeFixedSizeArray(destroy: some DevirtualizableDestroy,
                                        isMandatory: Bool,
                                        _ context: some MutatingContext
) -> Bool {
  guard let arraySize = destroy.type.builtinFixedArraySizeType.valueOfInteger else {
    return false
  }
  let elementType = destroy.type.builtinFixedArrayElementType(in: destroy.parentFunction)

  let destroyAddr = destroy.materializeAsDestroyAddr(context)

  let builder = Builder(before: destroyAddr, context)
  let base = builder.createVectorBaseAddr(vector: destroyAddr.destroyedAddress)
  let count = builder.createIntegerLiteral(arraySize, type: context.getBuiltinWordType())

  let success = createArrayDestroyLoop(baseAddress: base,
                                       arrayCount: count,
                                       elementType: elementType,
                                       insertionPoint: destroyAddr,
                                       isMandatory: isMandatory,
                                       context)

  context.erase(instruction: destroyAddr)
  return success
}

// Used to dispatch devirtualization tasks to `destroy_value` and `destroy_addr`.
private protocol DevirtualizableDestroy : UnaryInstruction {
  var shouldDropDeinit: Bool { get }
  func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext)
  func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool
  func devirtualizeEnumPayload(enumCase: EnumCase,
                               in block: BasicBlock,
                               isMandatory: Bool,
                               _ context: some MutatingContext) -> Bool
  func createSwitchEnum(atEndOf block: BasicBlock, cases: [(Int, BasicBlock)], _ context: some MutatingContext)
  func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst
}

private extension DevirtualizableDestroy {
  var type: Type { operand.value.type }

  func devirtualizeEnumPayloads(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
    guard let cases = type.getEnumCases(in: parentFunction) else {
      return false
    }
    defer {
      context.erase(instruction: self)
    }

    if cases.allPayloadsAreTrivial(in: parentFunction) {
      let builder = Builder(before: self, context)
      builder.createEndLifetime(of: operand.value)
      return true
    }

    var caseBlocks: [(caseIndex: Int, targetBlock: BasicBlock)] = []
    let switchBlock = parentBlock
    let endBlock = context.splitBlock(before: self)
    var result = true

    for enumCase in cases {
      let caseBlock = context.createBlock(after: switchBlock)
      caseBlocks.append((enumCase.index, caseBlock))
      let builder = Builder(atEndOf: caseBlock, location: location, context)
      builder.createBranch(to: endBlock)
      if !devirtualizeEnumPayload(enumCase: enumCase, in: caseBlock, isMandatory: isMandatory, context) {
        result = false
      }
    }
    createSwitchEnum(atEndOf: switchBlock, cases: caseBlocks, context)
    return result
  }
}

extension DestroyValueInst : DevirtualizableDestroy {
  fileprivate var shouldDropDeinit: Bool { operand.value.lookThoughOwnershipInstructions is DropDeinitInst }

  fileprivate func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext) {
    let builder = Builder(before: self, context)
    let subs = deinitializer.isGeneric ? context.getContextSubstitutionMap(for: type) : SubstitutionMap()
    let deinitRef = builder.createFunctionRef(deinitializer)
    if deinitializer.argumentConventions[deinitializer.selfArgumentIndex!].isIndirect {
      let allocStack = builder.createAllocStack(type)
      builder.createStore(source: destroyedValue, destination: allocStack, ownership: .initialize)
      builder.createApply(function: deinitRef, subs, arguments: [allocStack])
      builder.createDeallocStack(allocStack)
    } else {
      builder.createApply(function: deinitRef, subs, arguments: [destroyedValue])
    }
  }

  fileprivate func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
    guard let fields = type.getNominalFields(in: parentFunction) else {
      return false
    }

    defer {
      context.erase(instruction: self)
    }

    let builder = Builder(before: self, context)
    if fields.allFieldsAreTrivial(in: parentFunction) {
      builder.createEndLifetime(of: operand.value)
      return true
    }
    let destructure = builder.createDestructureStruct(struct: destroyedValue)
    var result = true

    for fieldValue in destructure.results where !fieldValue.type.isTrivial(in: parentFunction) {
      let destroyField = builder.createDestroyValue(operand: fieldValue)
      if !devirtualizeDeinits(of: destroyField, isMandatory: isMandatory, context) {
        result = false
      }
    }
    return result
  }

  fileprivate func devirtualizeEnumPayload(
    enumCase: EnumCase,
    in block: BasicBlock,
    isMandatory: Bool,
    _ context: some MutatingContext
  ) -> Bool {
    let builder = Builder(atBeginOf: block, location: location, context)
    if let payloadTy = enumCase.payload {
      let payload = block.addArgument(type: payloadTy, ownership: .owned, context)
      if !payloadTy.isTrivial(in: parentFunction) {
        let destroyPayload = builder.createDestroyValue(operand: payload)
        return devirtualizeDeinits(of: destroyPayload, isMandatory: isMandatory, context)
      }
    }
    return true
  }

  fileprivate func createSwitchEnum(
    atEndOf block: BasicBlock,
    cases: [(Int, BasicBlock)],
    _ context: some MutatingContext
  ) {
    let builder = Builder(atEndOf: block, location: location, context)
    builder.createSwitchEnum(enum: destroyedValue, cases: cases)
  }

  fileprivate func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst {
    let builder = Builder(before: self, context)
    let allocStack = builder.createAllocStack(destroyedValue.type)
    builder.createStore(source: destroyedValue, destination: allocStack, ownership: .initialize)
    let destroyAddr = builder.createDestroyAddr(address: allocStack)
    builder.createDeallocStack(allocStack)
    context.erase(instruction: self)
    return destroyAddr
  }
}

extension DestroyAddrInst : DevirtualizableDestroy {
  fileprivate var shouldDropDeinit: Bool {
    // The deinit is always called by a destroy_addr. There must not be a `drop_deinit` as operand.
    false
  }

  fileprivate func createDeinitCall(to deinitializer: Function, _ context: some MutatingContext) {
    let builder = Builder(before: self, context)
    let subs = context.getContextSubstitutionMap(for: destroyedAddress.type)
    let deinitRef = builder.createFunctionRef(deinitializer)
    if !deinitializer.argumentConventions[deinitializer.selfArgumentIndex!].isIndirect {
      let value = builder.createLoad(fromAddress: destroyedAddress, ownership: .take)
      builder.createApply(function: deinitRef, subs, arguments: [value])
    } else {
      builder.createApply(function: deinitRef, subs, arguments: [destroyedAddress])
    }
  }

  fileprivate func devirtualizeStructFields(isMandatory: Bool, _ context: some MutatingContext) -> Bool {
    let builder = Builder(before: self, context)

    guard let fields = type.getNominalFields(in: parentFunction) else {
      return false
    }
    defer {
      context.erase(instruction: self)
    }
    if fields.allFieldsAreTrivial(in: parentFunction) {
      builder.createEndLifetime(of: operand.value)
      return true
    }
    var result = true
    for (fieldIdx, fieldTy) in fields.enumerated()
      where !fieldTy.isTrivial(in: parentFunction)
    {
      let fieldAddr = builder.createStructElementAddr(structAddress: destroyedAddress, fieldIndex: fieldIdx)
      let destroyField = builder.createDestroyAddr(address: fieldAddr)
      if !devirtualizeDeinits(of: destroyField, isMandatory: isMandatory, context) {
        result = false
      }
    }
    return result
  }

  fileprivate func devirtualizeEnumPayload(
    enumCase: EnumCase,
    in block: BasicBlock,
    isMandatory: Bool,
    _ context: some MutatingContext
  ) -> Bool {
    let builder = Builder(atBeginOf: block, location: location, context)
    if let payloadTy = enumCase.payload,
       !payloadTy.isTrivial(in: parentFunction)
    {
      let caseAddr = builder.createUncheckedTakeEnumDataAddr(enumAddress: destroyedAddress, caseIndex: enumCase.index)
      let destroyPayload = builder.createDestroyAddr(address: caseAddr)
      return devirtualizeDeinits(of: destroyPayload, isMandatory: isMandatory, context)
    }
    return true
  }

  fileprivate func createSwitchEnum(
    atEndOf block: BasicBlock,
    cases: [(Int, BasicBlock)],
    _ context: some MutatingContext
  ) {
    let builder = Builder(atEndOf: block, location: location, context)
    builder.createSwitchEnumAddr(enumAddress: destroyedAddress, cases: cases)
  }

  fileprivate func materializeAsDestroyAddr(_ context: some MutatingContext) -> DestroyAddrInst {
    return self
  }
}

private func devirtualize(builtinDestroyArray: BuiltinInst,
                          isMandatory: Bool,
                          _ context: some MutatingContext
) -> Bool {
  let function = builtinDestroyArray.parentFunction
  let elementType = builtinDestroyArray.substitutionMap.replacementType.loweredType(in: function)
  guard elementType.isMoveOnly,
        // This avoids lowering the loop if the element is a non-copyable generic type.
        (elementType.isStruct || elementType.isEnum || elementType.isBuiltinFixedArray)
  else {
    return true
  }

  // Lower the `builtin "destroyArray" to a loop which destroys all elements

  let builder = Builder(before: builtinDestroyArray, context)
  let baseAddress = builder.createPointerToAddress(pointer: builtinDestroyArray.arguments[1],
                                                   addressType: elementType.addressType,
                                                   isStrict: true, isInvariant: false)

  let success = createArrayDestroyLoop(baseAddress: baseAddress,
                                       arrayCount: builtinDestroyArray.arguments[2],
                                       elementType: elementType,
                                       insertionPoint: builtinDestroyArray,
                                       isMandatory: isMandatory,
                                       context)

  context.erase(instruction: builtinDestroyArray)
  return success
}

private func createArrayDestroyLoop(baseAddress: Value,
                                    arrayCount: Value,
                                    elementType: Type,
                                    insertionPoint: Instruction,
                                    isMandatory: Bool,
                                    _ context: some MutatingContext
) -> Bool {
  let indexType = arrayCount.type
  let boolType = context.getBuiltinIntegerType(bitWidth: 1)

  let preheaderBlock = insertionPoint.parentBlock
  let exitBlock = context.splitBlock(after: insertionPoint)
  let headerBlock = context.createBlock(after: preheaderBlock)
  let bodyBlock = context.createBlock(after: headerBlock)

  let preheaderBuilder = Builder(atEndOf: preheaderBlock, location: insertionPoint.location, context)
  let zero = preheaderBuilder.createIntegerLiteral(0, type: indexType)
  let one = preheaderBuilder.createIntegerLiteral(1, type: indexType)
  let falseValue = preheaderBuilder.createBoolLiteral(false);
  preheaderBuilder.createBranch(to: headerBlock, arguments: [zero])

  let inductionVariable = headerBlock.addArgument(type: indexType, ownership: .none, context)
  let headerBuilder = Builder(atEndOf: headerBlock, location: insertionPoint.location, context)
  let cmp = headerBuilder.createBuiltinBinaryFunction(name: "cmp_slt", operandType: indexType, resultType: boolType,
                                                      arguments: [inductionVariable, arrayCount])
  headerBuilder.createCondBranch(condition: cmp, trueBlock: bodyBlock, falseBlock: exitBlock)

  let bodyBuilder = Builder(atEndOf: bodyBlock, location: insertionPoint.location, context)
  let elementAddr = bodyBuilder.createIndexAddr(base: baseAddress, index: inductionVariable,
                                                needStackProtection: false, isProjection: true)
  let destroy = bodyBuilder.createDestroyAddr(address: elementAddr)
  let resultType = context.getTupleType(elements: [indexType, boolType]).loweredType(in: insertionPoint.parentFunction)
  let increment = bodyBuilder.createBuiltinBinaryFunction(name: "sadd_with_overflow", operandType: indexType,
                                                          resultType: resultType,
                                                          arguments: [inductionVariable, one, falseValue])
  let incrResult = bodyBuilder.createTupleExtract(tuple: increment, elementIndex: 0)
  bodyBuilder.createBranch(to: headerBlock, arguments: [incrResult])

  return devirtualize(destroy: destroy, isMandatory: isMandatory, context)
}

private extension EnumCases {
  func allPayloadsAreTrivial(in function: Function) -> Bool {
    allSatisfy({ $0.payload?.isTrivial(in: function) ?? true })
  }
}

private extension NominalFieldsArray {
  func allFieldsAreTrivial(in function: Function) -> Bool {
    allSatisfy({ $0.isTrivial(in: function)})
  }
}
