Skip to content

Commit 4a76c04

Browse files
committed
SILGen: Fix if #unavailable mis-compile for zippered libraries.
Inverted availability queries were mis-compiled for zippered libraries because the code that emits calls to `isOSVersionAtLeastOrVariantVersionAtLeast()` was not updated when the `if #unavailable` syntax was introduced (at that time support for zippered libraries had not yet been upstreamed). The result of these calls is now inverted when appropriate. To make it easier to manage the growing complexity of supporting availability queries, Sema now models the relevant information about an availability query with the new `AvailabilityQuery` type. It encapsulates the domain for the query, the result if it is known at compile time, and the version tuple arguments to pass to a runtime invocation if applicable. Resolves rdar://147929876.
1 parent b493b56 commit 4a76c04

13 files changed

+392
-155
lines changed

include/swift/AST/AvailabilityQuery.h

Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
//===--- AvailabilityQuery.h - Swift Availability Query ASTs ----*- C++ -*-===//
2+
//
3+
// This source file is part of the Swift.org open source project
4+
//
5+
// Copyright (c) 2025 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
// See https://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
10+
//
11+
//===----------------------------------------------------------------------===//
12+
//
13+
// This file defines the availability query AST classes.
14+
//
15+
//===----------------------------------------------------------------------===//
16+
17+
#ifndef SWIFT_AST_AVAILABILITY_QUERY_H
18+
#define SWIFT_AST_AVAILABILITY_QUERY_H
19+
20+
#include "swift/AST/AvailabilityDomain.h"
21+
#include "swift/AST/AvailabilityRange.h"
22+
#include "llvm/ADT/SmallVector.h"
23+
#include "llvm/Support/VersionTuple.h"
24+
25+
namespace swift {
26+
/// Represents the information needed to evaluate an `#if available` query
27+
/// (either at runtime or compile-time).
28+
class AvailabilityQuery final {
29+
AvailabilityDomain domain;
30+
std::optional<AvailabilityRange> primaryRange;
31+
std::optional<AvailabilityRange> variantRange;
32+
33+
enum class ResultKind : uint8_t {
34+
/// The result of the query is true at compile-time.
35+
ConstTrue = 0,
36+
/// The result of the query is false at compile-time.
37+
ConstFalse = 1,
38+
/// The result of the query must be determined at runtime.
39+
Dynamic = 2,
40+
};
41+
ResultKind kind;
42+
43+
bool unavailable;
44+
45+
AvailabilityQuery(AvailabilityDomain domain, ResultKind kind,
46+
bool isUnavailable,
47+
const std::optional<AvailabilityRange> &primaryRange,
48+
const std::optional<AvailabilityRange> &variantRange)
49+
: domain(domain), primaryRange(primaryRange), variantRange(variantRange),
50+
kind(kind), unavailable(isUnavailable) {};
51+
52+
public:
53+
/// Returns an `AvailabilityQuery` for a query that evaluates to true or
54+
/// false at compile-time.
55+
static AvailabilityQuery constant(AvailabilityDomain domain,
56+
bool isUnavailable, bool value) {
57+
return AvailabilityQuery(
58+
domain, value ? ResultKind::ConstTrue : ResultKind::ConstFalse,
59+
isUnavailable, std::nullopt, std::nullopt);
60+
}
61+
62+
/// Returns an `AvailabilityQuery` for a query that must be evaluated at
63+
/// runtime with the given arguments, which may be zero, one, or two version
64+
/// tuples that should be passed to the query function.
65+
static AvailabilityQuery
66+
dynamic(AvailabilityDomain domain, bool isUnavailable,
67+
const std::optional<AvailabilityRange> &primaryRange,
68+
const std::optional<AvailabilityRange> &variantRange) {
69+
return AvailabilityQuery(domain, ResultKind::Dynamic, isUnavailable,
70+
primaryRange, variantRange);
71+
}
72+
73+
/// Returns the domain that the query applies to.
74+
AvailabilityDomain getDomain() const { return domain; }
75+
76+
/// Returns true if the query's result is determined at compile-time.
77+
bool isConstant() const { return kind != ResultKind::Dynamic; }
78+
79+
/// Returns true if the query was spelled `#unavailable`.
80+
bool isUnavailability() const { return unavailable; }
81+
82+
/// Returns the boolean result of the query if it is known at compile-time, or
83+
/// `std::nullopt` otherwise. The returned value accounts for whether the
84+
/// query was spelled `#unavailable`.
85+
std::optional<bool> getConstantResult() const {
86+
switch (kind) {
87+
case ResultKind::ConstTrue:
88+
return !unavailable;
89+
case ResultKind::ConstFalse:
90+
return unavailable;
91+
case ResultKind::Dynamic:
92+
return std::nullopt;
93+
}
94+
}
95+
96+
/// Returns the availability range that is the first argument to query
97+
/// function.
98+
std::optional<AvailabilityRange> getPrimaryRange() const {
99+
return primaryRange;
100+
}
101+
102+
/// Returns the version tuple that is the first argument to query function.
103+
std::optional<llvm::VersionTuple> getPrimaryArgument() const {
104+
if (!primaryRange)
105+
return std::nullopt;
106+
return primaryRange->getRawMinimumVersion();
107+
}
108+
109+
/// Returns the availability range that is the second argument to query
110+
/// function. This represents the `-target-variant` version when compiling a
111+
/// zippered library.
112+
std::optional<AvailabilityRange> getVariantRange() const {
113+
return variantRange;
114+
}
115+
116+
/// Returns the version tuple that is the second argument to query function.
117+
/// This represents the `-target-variant` version when compiling a zippered
118+
/// library.
119+
std::optional<llvm::VersionTuple> getVariantArgument() const {
120+
if (!variantRange)
121+
return std::nullopt;
122+
return variantRange->getRawMinimumVersion();
123+
}
124+
};
125+
126+
} // end namespace swift
127+
128+
#endif

include/swift/AST/AvailabilitySpec.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//===--- AvailabilitySpec.h - Swift Availability Query ASTs -----*- C++ -*-===//
1+
//===--- AvailabilitySpec.h - Swift Availability Spec ASTs ------*- C++ -*-===//
22
//
33
// This source file is part of the Swift.org open source project
44
//

include/swift/AST/Stmt.h

Lines changed: 10 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919

2020
#include "swift/AST/ASTAllocated.h"
2121
#include "swift/AST/ASTNode.h"
22+
#include "swift/AST/AvailabilityQuery.h"
2223
#include "swift/AST/AvailabilityRange.h"
2324
#include "swift/AST/ConcreteDeclRef.h"
2425
#include "swift/AST/IfConfigClause.h"
@@ -477,20 +478,11 @@ class alignas(8) PoundAvailableInfo final :
477478
SourceLoc LParenLoc;
478479
SourceLoc RParenLoc;
479480

480-
// The number of queries tail allocated after this object.
481+
/// The number of queries tail allocated after this object.
481482
unsigned NumQueries;
482-
483-
/// The version range when this query will return true. This value is
484-
/// filled in by Sema.
485-
std::optional<VersionRange> AvailableRange;
486-
487-
/// For zippered builds, this is the version range for the target variant
488-
/// that must hold for the query to return true. For example, when
489-
/// compiling with target x86_64-macosx10.15 and target-variant
490-
/// x86_64-ios13.0 a query of #available(macOS 10.22, iOS 20.0, *) will
491-
/// have a variant range of [20.0, +inf).
492-
/// This is filled in by Sema.
493-
std::optional<VersionRange> VariantAvailableRange;
483+
484+
/// The type-checked availability query information.
485+
std::optional<const AvailabilityQuery> Query;
494486

495487
struct {
496488
unsigned isInvalid : 1;
@@ -504,8 +496,7 @@ class alignas(8) PoundAvailableInfo final :
504496
ArrayRef<AvailabilitySpec *> queries, SourceLoc RParenLoc,
505497
bool isUnavailability)
506498
: PoundLoc(PoundLoc), LParenLoc(LParenLoc), RParenLoc(RParenLoc),
507-
NumQueries(queries.size()), AvailableRange(VersionRange::empty()),
508-
VariantAvailableRange(VersionRange::empty()), Flags() {
499+
NumQueries(queries.size()), Flags() {
509500
Flags.isInvalid = false;
510501
Flags.isUnavailability = isUnavailability;
511502
std::uninitialized_copy(queries.begin(), queries.end(),
@@ -539,16 +530,11 @@ class alignas(8) PoundAvailableInfo final :
539530
SourceRange getSourceRange() const { return SourceRange(getStartLoc(),
540531
getEndLoc()); }
541532

542-
std::optional<VersionRange> getAvailableRange() const {
543-
return AvailableRange;
544-
}
545-
void setAvailableRange(const VersionRange &Range) { AvailableRange = Range; }
546-
547-
std::optional<VersionRange> getVariantAvailableRange() const {
548-
return VariantAvailableRange;
533+
std::optional<const AvailabilityQuery> getAvailabilityQuery() const {
534+
return Query;
549535
}
550-
void setVariantAvailableRange(const VersionRange &Range) {
551-
VariantAvailableRange = Range;
536+
void setAvailabilityQuery(const AvailabilityQuery &query) {
537+
Query.emplace(query);
552538
}
553539

554540
bool isUnavailability() const { return Flags.isUnavailability; }

lib/AST/AvailabilityScopeBuilder.cpp

Lines changed: 78 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -824,6 +824,77 @@ class AvailabilityScopeBuilder : private ASTWalker {
824824
pushContext(fallthroughScope, parentBrace);
825825
}
826826

827+
AvailabilityQuery buildAvailabilityQuery(
828+
const SemanticAvailabilitySpec spec,
829+
const std::optional<SemanticAvailabilitySpec> &variantSpec,
830+
bool isUnavailability) {
831+
auto domain = spec.getDomain();
832+
833+
// Variant availability specfications are only supported for platform
834+
// domains when compiling with a -target-variant.
835+
if (!Context.LangOpts.TargetVariant)
836+
ASSERT(!variantSpec);
837+
838+
auto runtimeRangeForSpec =
839+
[](const std::optional<SemanticAvailabilitySpec> &spec)
840+
-> std::optional<AvailabilityRange> {
841+
if (!spec || spec->isWildcard() || !spec->getDomain().isVersioned())
842+
return std::nullopt;
843+
844+
return AvailabilityRange(spec->getRuntimeVersion());
845+
};
846+
847+
auto primaryRange = runtimeRangeForSpec(spec);
848+
auto variantRange = runtimeRangeForSpec(variantSpec);
849+
850+
switch (domain.getKind()) {
851+
case AvailabilityDomain::Kind::Embedded:
852+
case AvailabilityDomain::Kind::SwiftLanguage:
853+
case AvailabilityDomain::Kind::PackageDescription:
854+
// These domains don't support queries.
855+
llvm::report_fatal_error("unsupported domain");
856+
857+
case AvailabilityDomain::Kind::Universal:
858+
DEBUG_ASSERT(spec.isWildcard());
859+
860+
// If all of the specs that matched are '*', then the query trivially
861+
// evaluates to "true" at compile time.
862+
if (!variantRange)
863+
return AvailabilityQuery::constant(domain, isUnavailability, true);
864+
865+
// Otherwise, generate a dynamic query for the variant spec. For example,
866+
// when compiling zippered for macOS, this should generate a query that
867+
// just checks the iOS version at runtime:
868+
//
869+
// if #available(iOS 18, *) { ... }
870+
//
871+
return AvailabilityQuery::dynamic(variantSpec->getDomain(),
872+
isUnavailability, primaryRange,
873+
variantRange);
874+
875+
case AvailabilityDomain::Kind::Platform:
876+
// Platform checks are always dynamic. The SIL optimizer is responsible
877+
// eliminating these checks when it can prove that they can never fail
878+
// (due to the deployment target). We can't perform that analysis here
879+
// because it may depend on inlining.
880+
return AvailabilityQuery::dynamic(domain, isUnavailability, primaryRange,
881+
variantRange);
882+
case AvailabilityDomain::Kind::Custom:
883+
auto customDomain = domain.getCustomDomain();
884+
ASSERT(customDomain);
885+
886+
switch (customDomain->getKind()) {
887+
case CustomAvailabilityDomain::Kind::Enabled:
888+
return AvailabilityQuery::constant(domain, isUnavailability, true);
889+
case CustomAvailabilityDomain::Kind::Disabled:
890+
return AvailabilityQuery::constant(domain, isUnavailability, false);
891+
case CustomAvailabilityDomain::Kind::Dynamic:
892+
return AvailabilityQuery::dynamic(domain, isUnavailability,
893+
primaryRange, variantRange);
894+
}
895+
}
896+
}
897+
827898
/// Build the availability scopes for a StmtCondition and return a pair of
828899
/// optional availability contexts, the first for the true branch and the
829900
/// second for the false branch. A value of `nullopt` for a given branch
@@ -982,23 +1053,18 @@ class AvailabilityScopeBuilder : private ASTWalker {
9821053
continue;
9831054
}
9841055

985-
auto runtimeQueryRange = runtimeQueryRangeForSpec(*spec);
986-
query->setAvailableRange(runtimeQueryRange.getRawVersionRange());
987-
9881056
// When compiling zippered for macCatalyst, we need to collect both
9891057
// a macOS version (the target version) and an iOS/macCatalyst version
9901058
// (the target-variant). These versions will both be passed to a runtime
9911059
// entrypoint that will check either the macOS version or the iOS
9921060
// version depending on the kind of process this code is loaded into.
993-
if (Context.LangOpts.TargetVariant) {
994-
auto variantSpec =
995-
bestActiveSpecForQuery(query, /*ForTargetVariant*/ true);
996-
if (variantSpec) {
997-
auto variantQueryRange = runtimeQueryRangeForSpec(*variantSpec);
998-
query->setVariantAvailableRange(
999-
variantQueryRange.getRawVersionRange());
1000-
}
1001-
}
1061+
std::optional<SemanticAvailabilitySpec> variantSpec =
1062+
(Context.LangOpts.TargetVariant)
1063+
? bestActiveSpecForQuery(query, /*ForTargetVariant*/ true)
1064+
: std::nullopt;
1065+
1066+
query->setAvailabilityQuery(buildAvailabilityQuery(
1067+
*spec, variantSpec, query->isUnavailability()));
10021068

10031069
// Wildcards are expected to be "useless". There may be other specs in
10041070
// this query that are useful when compiling for other platforms.
@@ -1164,24 +1230,6 @@ class AvailabilityScopeBuilder : private ASTWalker {
11641230
}
11651231
}
11661232

1167-
/// Return the availability context for the given spec.
1168-
AvailabilityRange runtimeQueryRangeForSpec(SemanticAvailabilitySpec spec) {
1169-
if (spec.isWildcard())
1170-
return AvailabilityRange::alwaysAvailable();
1171-
1172-
auto domain = spec.getDomain();
1173-
if (domain.isVersioned())
1174-
return AvailabilityRange(spec.getRuntimeVersion());
1175-
1176-
// If it's not a versioned domain, we must be querying whether a domain is
1177-
// present or absent.
1178-
if (domain.isCustom() && domain.getCustomDomain()->getKind() ==
1179-
CustomAvailabilityDomain::Kind::Disabled)
1180-
return AvailabilityRange::neverAvailable();
1181-
1182-
return AvailabilityRange::alwaysAvailable();
1183-
}
1184-
11851233
/// For the given spec, returns a pair of availability ranges. The first range
11861234
/// is for the if/then flow and the second is for the if/else flow.
11871235
std::pair<std::optional<AvailabilityRange>, std::optional<AvailabilityRange>>

0 commit comments

Comments
 (0)