//===--- AvailabilityQuery.cpp - Swift Availability Queries ---------------===//
//
// This source file is part of the Swift.org open source project
//
// Copyright (c) 2025 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
//
//===----------------------------------------------------------------------===//

#include "swift/AST/AvailabilityQuery.h"
#include "swift/AST/ASTContext.h"
#include "swift/AST/Decl.h"
#include "swift/Basic/Platform.h"

using namespace swift;

AvailabilityQuery::AvailabilityQuery(
    AvailabilityDomain domain, ResultKind kind,
    const std::optional<AvailabilityRange> &primaryRange,
    const std::optional<AvailabilityRange> &variantRange)
    : domain(domain), primaryRange(primaryRange), variantRange(variantRange),
      kind(kind), unavailable(false) {
  // Check invariants.
  switch (domain.getKind()) {
  case AvailabilityDomain::Kind::SwiftLanguageMode:
  case AvailabilityDomain::Kind::PackageDescription:
  case AvailabilityDomain::Kind::Embedded:
    // These domains don't support queries at all.
    DEBUG_ASSERT(false);
    break;

  case AvailabilityDomain::Kind::Universal:
    // The universal domain can only support constant queries.
    DEBUG_ASSERT(kind != ResultKind::Dynamic);
    break;

  case AvailabilityDomain::Kind::StandaloneSwiftRuntime:
    // Dynamic Swift runtime queries take just a primary version argument.
    if (kind == ResultKind::Dynamic) {
      DEBUG_ASSERT(primaryRange);
      DEBUG_ASSERT(!variantRange);
    }
    break;

  case AvailabilityDomain::Kind::Platform:
    // Dynamic platform version queries must have either a primary version
    // argument or a variant version argument (or both).
    if (kind == ResultKind::Dynamic) {
      DEBUG_ASSERT(primaryRange || variantRange);
    }
    break;

  case AvailabilityDomain::Kind::Custom:
    // Custom availability domains do not support versioned queries at all yet.
    DEBUG_ASSERT(!primaryRange);
    DEBUG_ASSERT(!variantRange);

    // A valid custom domain object is required.
    auto customDomain = domain.getCustomDomain();
    ASSERT(customDomain);
    break;
  }
}

static void unpackVersion(const llvm::VersionTuple &version,
                          llvm::SmallVectorImpl<unsigned> &arguments) {
  arguments.push_back(version.getMajor());
  arguments.push_back(version.getMinor().value_or(0));
  arguments.push_back(version.getSubminor().value_or(0));
}

static FuncDecl *
getOSVersionRangeCheck(const llvm::VersionTuple &version,
                       llvm::SmallVectorImpl<unsigned> &arguments,
                       ASTContext &ctx, bool forTargetVariant) {
  unpackVersion(version, arguments);
  return forTargetVariant ? ctx.getIsVariantOSVersionAtLeastDecl()
                          : ctx.getIsOSVersionAtLeastDecl();
}

static FuncDecl *getOSVersionOrVariantVersionRangeCheck(
    const llvm::VersionTuple &targetVersion,
    const llvm::VersionTuple &variantVersion,
    llvm::SmallVectorImpl<unsigned> &arguments, ASTContext &ctx) {
  unpackVersion(targetVersion, arguments);
  unpackVersion(variantVersion, arguments);
  return ctx.getIsOSVersionAtLeastOrVariantVersionAtLeast();
}

static FuncDecl *
getZipperedOSVersionRangeCheck(const AvailabilityQuery &query,
                               llvm::SmallVectorImpl<unsigned> &arguments,
                               ASTContext &ctx) {

  auto targetVersion = query.getPrimaryArgument();
  auto variantVersion = query.getVariantArgument();
  DEBUG_ASSERT(targetVersion || variantVersion);

  // We're building zippered, so we need to pass both macOS and iOS versions to
  // the runtime version range check. At run time that check will determine what
  // kind of process this code is loaded into. In a macOS process it will use
  // the macOS version; in an macCatalyst process it will use the iOS version.
  llvm::Triple targetTriple = ctx.LangOpts.Target;
  llvm::Triple variantTriple = *ctx.LangOpts.TargetVariant;

  // From perspective of the driver and most of the frontend, -target and
  // -target-variant are symmetric. That is, the user can pass either:
  //    -target x86_64-apple-macosx10.15 \
  //    -target-variant x86_64-apple-ios13.1-macabi
  // or:
  //    -target x86_64-apple-ios13.1-macabi \
  //    -target-variant x86_64-apple-macosx10.15
  //
  // However, the runtime availability-checking entry points need to compare
  // against an actual running OS version and so can't be symmetric. Here we
  // standardize on "target" means macOS version and "targetVariant" means iOS
  // version.
  if (tripleIsMacCatalystEnvironment(targetTriple)) {
    DEBUG_ASSERT(variantTriple.isMacOSX());
    // Normalize so that "variant" always means iOS version.
    std::swap(targetVersion, variantVersion);
    std::swap(targetTriple, variantTriple);
  }

  // The variant-only availability-checking entrypoint is not part of the
  // Swift 5.0 ABI. It is only available in macOS 10.15 and above.
  bool isVariantEntrypointAvailable = !targetTriple.isMacOSXVersionLT(10, 15);

  // If there is no check for the target but there is for the variant, then we
  // only need to emit code for the variant check.
  if (isVariantEntrypointAvailable && !targetVersion && variantVersion)
    return getOSVersionRangeCheck(*variantVersion, arguments, ctx,
                                  /*forVariant=*/true);

  // Similarly, if there is a check for the target but not for the target
  // variant then we only to emit code for the target check.
  if (targetVersion && !variantVersion)
    return getOSVersionRangeCheck(*targetVersion, arguments, ctx,
                                  /*forTargetVariant=*/false);

  if (!isVariantEntrypointAvailable || (targetVersion && variantVersion)) {

    // If the variant-only entrypoint isn't available (as is the case
    // pre-macOS 10.15) we need to use the zippered entrypoint (which is part of
    // the Swift 5.0 ABI) even when the macOS version is '*' (all). In this
    // case, use the minimum macOS deployment version from the target triple.
    // This ensures the check always passes on macOS.
    if (!isVariantEntrypointAvailable && !targetVersion) {
      DEBUG_ASSERT(targetTriple.isMacOSX());

      llvm::VersionTuple macosVersion;
      targetTriple.getMacOSXVersion(macosVersion);
      targetVersion = macosVersion;
    }

    return getOSVersionOrVariantVersionRangeCheck(
        *targetVersion, *variantVersion, arguments, ctx);
  }

  llvm_unreachable("Unhandled zippered configuration");
}

static FuncDecl *
getOSAvailabilityDeclAndArguments(const AvailabilityQuery &query,
                                  llvm::SmallVectorImpl<unsigned> &arguments,
                                  ASTContext &ctx) {
  if (ctx.LangOpts.TargetVariant)
    return getZipperedOSVersionRangeCheck(query, arguments, ctx);

  bool isMacCatalyst = tripleIsMacCatalystEnvironment(ctx.LangOpts.Target);
  return getOSVersionRangeCheck(query.getPrimaryArgument().value(), arguments,
                                ctx, isMacCatalyst);
}

FuncDecl *AvailabilityQuery::getDynamicQueryDeclAndArguments(
    llvm::SmallVectorImpl<unsigned> &arguments, ASTContext &ctx) const {
  auto domain = getDomain();
  switch (domain.getKind()) {
  case AvailabilityDomain::Kind::Universal:
  case AvailabilityDomain::Kind::SwiftLanguageMode:
  case AvailabilityDomain::Kind::PackageDescription:
  case AvailabilityDomain::Kind::Embedded:
    // These domains don't support dynamic queries.
    return nullptr;

  case AvailabilityDomain::Kind::StandaloneSwiftRuntime:
    unpackVersion(getPrimaryArgument().value(), arguments);
    return ctx.getIsSwiftRuntimeVersionAtLeast();
  case AvailabilityDomain::Kind::Platform:
    return getOSAvailabilityDeclAndArguments(*this, arguments, ctx);
  case AvailabilityDomain::Kind::Custom:
    return domain.getCustomDomain()->getPredicateFunc();
  }
}
