From 085f8ecca95fdb6f4705169e33ea7b398036fc9d Mon Sep 17 00:00:00 2001 From: Allan Shortlidge Date: Wed, 15 Oct 2025 16:58:27 -0700 Subject: [PATCH] stdlib/SILGen: Emit SIL for Swift runtime availability queries. When emitting SIL for `if #available(Swift ..., *)` queries, call the new `_isSwiftRuntimeVersionAtLeast()` function in the stdlib to check the condition. To support back deployment, the implementation of `_isSwiftRuntimeVersionAtLeast()` is `@_alwaysEmitIntoClient` and performs its comparison against the result of `_SwiftStdlibVersion.current`, which is pre-existing ABI that the stdlib exposes for querying the Swift runtime version. Resolves rdar://162726037. --- include/swift/AST/ASTContext.h | 3 + include/swift/AST/AvailabilityQuery.h | 4 +- lib/AST/ASTContext.cpp | 21 ++++++- lib/AST/AvailabilityQuery.cpp | 59 +++++++++++++++++-- lib/AST/AvailabilityScopeBuilder.cpp | 2 + stdlib/public/core/Availability.swift | 46 ++++++++++++++- test/IRGen/availability_swift_runtime.swift | 46 +++++++++++++++ .../availability_query_swift_runtime.swift | 26 ++++++++ ...y_swift_runtime_maccatalyst_zippered.swift | 27 +++++++++ 9 files changed, 224 insertions(+), 10 deletions(-) create mode 100644 test/IRGen/availability_swift_runtime.swift create mode 100644 test/SILGen/availability_query_swift_runtime.swift create mode 100644 test/SILGen/availability_query_swift_runtime_maccatalyst_zippered.swift diff --git a/include/swift/AST/ASTContext.h b/include/swift/AST/ASTContext.h index 858e28d7dfc29..dfcaadeac7533 100644 --- a/include/swift/AST/ASTContext.h +++ b/include/swift/AST/ASTContext.h @@ -801,6 +801,9 @@ class ASTContext final { // Swift._stdlib_isOSVersionAtLeastOrVariantVersionAtLeast. FuncDecl *getIsOSVersionAtLeastOrVariantVersionAtLeast() const; + /// Retrieve the declaration of Swift._isSwiftRuntimeVersionAtLeast. + FuncDecl *getIsSwiftRuntimeVersionAtLeast() const; + /// Look for the declaration with the given name within the /// passed in module. void lookupInModule(ModuleDecl *M, StringRef name, diff --git a/include/swift/AST/AvailabilityQuery.h b/include/swift/AST/AvailabilityQuery.h index ca9f8af38e753..dbb4f843e3022 100644 --- a/include/swift/AST/AvailabilityQuery.h +++ b/include/swift/AST/AvailabilityQuery.h @@ -47,9 +47,7 @@ class AvailabilityQuery final { AvailabilityQuery(AvailabilityDomain domain, ResultKind kind, const std::optional &primaryRange, - const std::optional &variantRange) - : domain(domain), primaryRange(primaryRange), variantRange(variantRange), - kind(kind), unavailable(false) {}; + const std::optional &variantRange); public: /// Returns an `AvailabilityQuery` for a query that evaluates to true or diff --git a/lib/AST/ASTContext.cpp b/lib/AST/ASTContext.cpp index 99c229c1c4557..2d0b954a1ce01 100644 --- a/lib/AST/ASTContext.cpp +++ b/lib/AST/ASTContext.cpp @@ -425,6 +425,13 @@ struct ASTContext::Implementation { /// -> Builtin.Int1 FuncDecl *IsOSVersionAtLeastOrVariantVersionAtLeastDecl = nullptr; + /// func _isSwiftRuntimeVersionAtLeast( + /// Builtin.Word, + /// Builtin.Word, + /// Builtin.word) + /// -> Builtin.Int1 + FuncDecl *IsSwiftRuntimeVersionAtLeastDecl = nullptr; + /// The set of known protocols, lazily populated as needed. ProtocolDecl *KnownProtocols[NumKnownProtocols] = { }; @@ -1934,7 +1941,7 @@ FuncDecl *ASTContext::getIsVariantOSVersionAtLeastDecl() const { } FuncDecl *ASTContext::getIsOSVersionAtLeastOrVariantVersionAtLeast() const { -if (getImpl().IsOSVersionAtLeastOrVariantVersionAtLeastDecl) + if (getImpl().IsOSVersionAtLeastOrVariantVersionAtLeastDecl) return getImpl().IsOSVersionAtLeastOrVariantVersionAtLeastDecl; auto decl = findLibraryIntrinsic(*this, @@ -1946,6 +1953,18 @@ if (getImpl().IsOSVersionAtLeastOrVariantVersionAtLeastDecl) return decl; } +FuncDecl *ASTContext::getIsSwiftRuntimeVersionAtLeast() const { + if (getImpl().IsSwiftRuntimeVersionAtLeastDecl) + return getImpl().IsSwiftRuntimeVersionAtLeastDecl; + + auto decl = findLibraryIntrinsic(*this, "_isSwiftRuntimeVersionAtLeast"); + if (!decl) + return nullptr; + + getImpl().IsSwiftRuntimeVersionAtLeastDecl = decl; + return decl; +} + static bool isHigherPrecedenceThan(PrecedenceGroupDecl *a, PrecedenceGroupDecl *b) { assert(a != b && "exact match should already have been filtered"); diff --git a/lib/AST/AvailabilityQuery.cpp b/lib/AST/AvailabilityQuery.cpp index 16b3b41c2ae88..ac481c324f17a 100644 --- a/lib/AST/AvailabilityQuery.cpp +++ b/lib/AST/AvailabilityQuery.cpp @@ -17,6 +17,54 @@ using namespace swift; +AvailabilityQuery::AvailabilityQuery( + AvailabilityDomain domain, ResultKind kind, + const std::optional &primaryRange, + const std::optional &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::SwiftRuntime: + // 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 &arguments) { arguments.push_back(version.getMajor()); @@ -133,16 +181,17 @@ FuncDecl *AvailabilityQuery::getDynamicQueryDeclAndArguments( switch (domain.getKind()) { case AvailabilityDomain::Kind::Universal: case AvailabilityDomain::Kind::SwiftLanguageMode: - case AvailabilityDomain::Kind::SwiftRuntime: case AvailabilityDomain::Kind::PackageDescription: case AvailabilityDomain::Kind::Embedded: + // These domains don't support dynamic queries. return nullptr; + + case AvailabilityDomain::Kind::SwiftRuntime: + unpackVersion(getPrimaryArgument().value(), arguments); + return ctx.getIsSwiftRuntimeVersionAtLeast(); case AvailabilityDomain::Kind::Platform: return getOSAvailabilityDeclAndArguments(*this, arguments, ctx); case AvailabilityDomain::Kind::Custom: - auto customDomain = domain.getCustomDomain(); - ASSERT(customDomain); - - return customDomain->getPredicateFunc(); + return domain.getCustomDomain()->getPredicateFunc(); } } diff --git a/lib/AST/AvailabilityScopeBuilder.cpp b/lib/AST/AvailabilityScopeBuilder.cpp index 47273ff7ac710..5238cfff473dd 100644 --- a/lib/AST/AvailabilityScopeBuilder.cpp +++ b/lib/AST/AvailabilityScopeBuilder.cpp @@ -870,6 +870,8 @@ class AvailabilityScopeBuilder : private ASTWalker { variantRange); case AvailabilityDomain::Kind::SwiftRuntime: + return AvailabilityQuery::dynamic(domain, primaryRange, std::nullopt); + case AvailabilityDomain::Kind::Platform: // Platform and Swift runtime checks are always dynamic. The SIL optimizer // is responsible eliminating these checks when it can prove that they can diff --git a/stdlib/public/core/Availability.swift b/stdlib/public/core/Availability.swift index 08880a2ee57d5..e73be8cae5817 100644 --- a/stdlib/public/core/Availability.swift +++ b/stdlib/public/core/Availability.swift @@ -197,6 +197,22 @@ public func _stdlib_isOSVersionAtLeastOrVariantVersionAtLeast( public typealias _SwiftStdlibVersion = SwiftShims._SwiftStdlibVersion +/// This is a magic entry point known to the compiler. It is called in +/// generated code for Swift runtime availability checking, e.g. +/// +/// if #available(Swift 6.2, *) { } +/// +@available(SwiftStdlib 5.7, *) +@_alwaysEmitIntoClient +internal func _isSwiftRuntimeVersionAtLeast( + _ major: Builtin.Word, + _ minor: Builtin.Word, + _ patch: Builtin.Word +) -> Builtin.Int1 { + let version = _SwiftStdlibVersion(major, minor, patch) + return (_SwiftStdlibVersion.current._value <= version._value)._value +} + /// Return true if the main executable was linked with an SDK version /// corresponding to the given Swift Stdlib release, or later. Otherwise, return /// false. @@ -243,8 +259,36 @@ extension _SwiftStdlibVersion { @_alwaysEmitIntoClient public static var v6_3_0: Self { Self(_value: 0x060300) } + private static var _current: Self { .v6_3_0 } + +#if hasFeature(Macros) + @available(SwiftStdlib 5.7, *) + public static var current: Self { + @_noLocks + @_effects(readnone) + get { ._current } + } +#else @available(SwiftStdlib 5.7, *) - public static var current: Self { .v6_3_0 } + public static var current: Self { + @_effects(readnone) + get { ._current } + } +#endif + + @_alwaysEmitIntoClient + internal init( + _ major: Builtin.Word, + _ minor: Builtin.Word, + _ patch: Builtin.Word + ) { + let version = (Int(major), Int(minor), Int(patch)) + var value: UInt32 = 0x0 + value |= ((UInt32(truncatingIfNeeded: version.0) & 0xffff) << 16) + value |= ((UInt32(truncatingIfNeeded: version.1) & 0xff) << 8) + value |= ((UInt32(truncatingIfNeeded: version.2) & 0xff)) + self = Self(_value: value) + } } @available(SwiftStdlib 5.7, *) diff --git a/test/IRGen/availability_swift_runtime.swift b/test/IRGen/availability_swift_runtime.swift new file mode 100644 index 0000000000000..4f9c48416fa86 --- /dev/null +++ b/test/IRGen/availability_swift_runtime.swift @@ -0,0 +1,46 @@ +// RUN: %target-swift-emit-ir %s -min-swift-runtime-version 5.0 -O -enable-experimental-feature SwiftRuntimeAvailability | %FileCheck %s + +// REQUIRES: swift_feature_SwiftRuntimeAvailability + +@_silgen_name("callMeMaybe") +public func callMeMaybe() + +// Verify that optimized IR for "if #available(Swift X.Y, *)" is composed of a +// call to the stdlib ABI that returns the current runtime version and an +// integer comparison of that version and the predicate version. + +// CHECK-LABEL: define {{.*}}swiftcc void @"$s26availability_swift_runtime15testIfAvailableyyF"() +// CHECK: [[CURRENT_VERS:%.*]] = tail call swiftcc i32 @"$sSo19_SwiftStdlibVersionasE7currentABvgZ"() +// CHECK: [[ICMP:%.*]] = icmp {{.*}} +// CHECK: br i1 [[ICMP]], label %[[TRUE_LABEL:.*]], label %[[FALSE_LABEL:.*]] +// CHECK: [[TRUE_LABEL]]: +// CHECK: call {{.*}} @callMeMaybe() +// CHECK: [[FALSE_LABEL]]: +// CHECK: ret void +public func testIfAvailable() { + if #available(Swift 6.2, *) { + callMeMaybe() + } +} + +// In optimized IR multiple "if #available" checks for the same version should +// only generate a single call to the getter for _SwiftStdlibVersion.current. + +// CHECK-LABEL: define {{.*}}swiftcc void @"$s26availability_swift_runtime23testIfAvailableMultipleyyF"() +// CHECK: call {{.*}} @"$sSo19_SwiftStdlibVersionasE7currentABvgZ"() +// CHECK-NOT: call {{.*}} @"$sSo19_SwiftStdlibVersionasE7currentABvgZ"() +// CHECK: icmp +// CHECK: call {{.*}} @callMeMaybe() +// CHECK: call {{.*}} @callMeMaybe() +// CHECK: call {{.*}} @callMeMaybe() +public func testIfAvailableMultiple() { + if #available(Swift 5.10, *) { + callMeMaybe() + } + if #available(Swift 5.10, *) { + callMeMaybe() + } + if #available(Swift 5.10, *) { + callMeMaybe() + } +} diff --git a/test/SILGen/availability_query_swift_runtime.swift b/test/SILGen/availability_query_swift_runtime.swift new file mode 100644 index 0000000000000..500460304a991 --- /dev/null +++ b/test/SILGen/availability_query_swift_runtime.swift @@ -0,0 +1,26 @@ +// RUN: %target-swift-emit-sil %s -min-swift-runtime-version 5.0 -verify -enable-experimental-feature SwiftRuntimeAvailability +// RUN: %target-swift-emit-silgen %s -min-swift-runtime-version 5.0 -enable-experimental-feature SwiftRuntimeAvailability | %FileCheck %s + +// REQUIRES: swift_feature_SwiftRuntimeAvailability + +// CHECK-LABEL: sil [ossa] @$s32availability_query_swift_runtime15testIfAvailableyyF : $@convention(thin) () -> () { +// CHECK: [[MAJOR:%.*]] = integer_literal $Builtin.Word, 6 +// CHECK: [[MINOR:%.*]] = integer_literal $Builtin.Word, 2 +// CHECK: [[PATCH:%.*]] = integer_literal $Builtin.Word, 0 +// CHECK: [[FUNC:%.*]] = function_ref @$ss29_isSwiftRuntimeVersionAtLeastyBi1_Bw_BwBwtF : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[QUERY_RESULT:%.*]] = apply [[FUNC]]([[MAJOR]], [[MINOR]], [[PATCH]]) : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +public func testIfAvailable() { + if #available(Swift 6.2, *) { } +} + +// CHECK-LABEL: sil [ossa] @$s32availability_query_swift_runtime17testIfUnavailableyyF : $@convention(thin) () -> () { +// CHECK: [[MAJOR:%.*]] = integer_literal $Builtin.Word, 5 +// CHECK: [[MINOR:%.*]] = integer_literal $Builtin.Word, 10 +// CHECK: [[PATCH:%.*]] = integer_literal $Builtin.Word, 1 +// CHECK: [[FUNC:%.*]] = function_ref @$ss29_isSwiftRuntimeVersionAtLeastyBi1_Bw_BwBwtF : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[QUERY_RESULT:%.*]] = apply [[FUNC]]([[MAJOR]], [[MINOR]], [[PATCH]]) : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[MINUSONE:%.*]] = integer_literal $Builtin.Int1, -1 +// CHECK: [[QUERY_INVERSION:%.*]] = builtin "xor_Int1"([[QUERY_RESULT]], [[MINUSONE]]) : $Builtin.Int1 +public func testIfUnavailable() { + if #unavailable(Swift 5.10.1) { } +} diff --git a/test/SILGen/availability_query_swift_runtime_maccatalyst_zippered.swift b/test/SILGen/availability_query_swift_runtime_maccatalyst_zippered.swift new file mode 100644 index 0000000000000..3d67cf78c0983 --- /dev/null +++ b/test/SILGen/availability_query_swift_runtime_maccatalyst_zippered.swift @@ -0,0 +1,27 @@ +// RUN: %target-swift-emit-sil %s -target %target-cpu-apple-macosx11 -target-variant %target-cpu-apple-ios14-macabi -min-swift-runtime-version 5.0 -verify -enable-experimental-feature SwiftRuntimeAvailability +// RUN: %target-swift-emit-silgen %s -target %target-cpu-apple-macosx11 -target-variant %target-cpu-apple-ios14-macabi -min-swift-runtime-version 5.0 -enable-experimental-feature SwiftRuntimeAvailability | %FileCheck %s + +// REQUIRES: OS=macosx || OS=maccatalyst +// REQUIRES: swift_feature_SwiftRuntimeAvailability + +// CHECK-LABEL: sil [ossa] @$s53availability_query_swift_runtime_maccatalyst_zippered15testIfAvailableyyF : $@convention(thin) () -> () { +// CHECK: [[MAJOR:%.*]] = integer_literal $Builtin.Word, 6 +// CHECK: [[MINOR:%.*]] = integer_literal $Builtin.Word, 2 +// CHECK: [[PATCH:%.*]] = integer_literal $Builtin.Word, 0 +// CHECK: [[FUNC:%.*]] = function_ref @$ss29_isSwiftRuntimeVersionAtLeastyBi1_Bw_BwBwtF : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[QUERY_RESULT:%.*]] = apply [[FUNC]]([[MAJOR]], [[MINOR]], [[PATCH]]) : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +public func testIfAvailable() { + if #available(Swift 6.2, *) { } +} + +// CHECK-LABEL: sil [ossa] @$s53availability_query_swift_runtime_maccatalyst_zippered17testIfUnavailableyyF : $@convention(thin) () -> () { +// CHECK: [[MAJOR:%.*]] = integer_literal $Builtin.Word, 5 +// CHECK: [[MINOR:%.*]] = integer_literal $Builtin.Word, 10 +// CHECK: [[PATCH:%.*]] = integer_literal $Builtin.Word, 1 +// CHECK: [[FUNC:%.*]] = function_ref @$ss29_isSwiftRuntimeVersionAtLeastyBi1_Bw_BwBwtF : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[QUERY_RESULT:%.*]] = apply [[FUNC]]([[MAJOR]], [[MINOR]], [[PATCH]]) : $@convention(thin) (Builtin.Word, Builtin.Word, Builtin.Word) -> Builtin.Int1 +// CHECK: [[MINUSONE:%.*]] = integer_literal $Builtin.Int1, -1 +// CHECK: [[QUERY_INVERSION:%.*]] = builtin "xor_Int1"([[QUERY_RESULT]], [[MINUSONE]]) : $Builtin.Int1 +public func testIfUnavailable() { + if #unavailable(Swift 5.10.1) { } +}