Skip to content

Commit

Permalink
[Sema] Define availability via compiler flag
Browse files Browse the repository at this point in the history
Introduce availability macros defined by a frontend flag.
This feature makes it possible to set the availability
versions at the moment of compilation instead of having
it hard coded in the sources. It can be used by projects
with a need to change the availability depending on the
compilation context while using the same sources.

The availability macro is defined with the `-define-availability` flag:

swift MyLib.swift -define-availability "_iOS8Aligned:macOS 10.10, iOS 8.0" ..

The macro can be used in code instead of a platform name and version:
@available(_iOS8Aligned, *)
public func foo() {}

rdar://problem/65612624
  • Loading branch information
xymus committed Oct 6, 2020
1 parent 327f4f8 commit c6fc53e
Show file tree
Hide file tree
Showing 10 changed files with 379 additions and 16 deletions.
16 changes: 16 additions & 0 deletions include/swift/AST/DiagnosticsParse.def
Original file line number Diff line number Diff line change
Expand Up @@ -1431,6 +1431,22 @@ WARNING(attr_availability_nonspecific_platform_unexpected_version,none,
"unexpected version number in '%0' attribute for non-specific platform "
"'*'", (StringRef))

// availability macro
ERROR(attr_availability_wildcard_in_macro, none,
"future platforms identified by '*' cannot be used in "
"an availability macro definition", ())
ERROR(attr_availability_missing_macro_name,none,
"expected an identifier to begin an availability macro definition", ())
ERROR(attr_availability_expected_colon_macro,none,
"expected ':' after '%0' in availability macro definition",
(StringRef))
ERROR(attr_availability_unknown_version,none,
"reference to undefined version '%0' for availability macro '%1'",
(StringRef, StringRef))
ERROR(attr_availability_duplicate,none,
"duplicate definition of availability macro '%0' for version '%1'",
(StringRef, StringRef))

// originallyDefinedIn
ERROR(originally_defined_in_missing_rparen,none,
"expected ')' in @_originallyDefinedIn argument list", ())
Expand Down
3 changes: 3 additions & 0 deletions include/swift/Basic/LangOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,9 @@ namespace swift {
/// when using RequireExplicitAvailability.
std::string RequireExplicitAvailabilityTarget;

// Availability macros definitions to be expanded at parsing.
SmallVector<StringRef, 4> AvailabilityMacros;

/// If false, '#file' evaluates to the full path rather than a
/// human-readable string.
bool EnableConcisePoundFile = false;
Expand Down
5 changes: 5 additions & 0 deletions include/swift/Option/Options.td
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,11 @@ def require_explicit_availability_target : Separate<["-"], "require-explicit-ava
HelpText<"Suggest fix-its adding @available(<target>, *) to public declarations without availability">,
MetaVarName<"<target>">;

def define_availability : Separate<["-"], "define-availability">,
Flags<[FrontendOption, NoInteractiveOption]>,
HelpText<"Define an availability macro in the format 'macroName : iOS 13.0, macOS 10.15'">,
MetaVarName<"<macro>">;

def module_name : Separate<["-"], "module-name">,
Flags<[FrontendOption, ModuleInterfaceOption]>,
HelpText<"Name of the module to build">;
Expand Down
49 changes: 47 additions & 2 deletions include/swift/Parse/Parser.h
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,22 @@ class Parser {
/// Current syntax parsing context where call backs should be directed to.
SyntaxParsingContext *SyntaxContext;

/// Maps of macro name and version to availability specifications.
typedef llvm::DenseMap<llvm::VersionTuple,
SmallVector<AvailabilitySpec *, 4>>
AvailabilityMacroVersionMap;
typedef llvm::DenseMap<StringRef, AvailabilityMacroVersionMap>
AvailabilityMacroMap;

/// Cache of the availability macros parsed from the command line arguments.
/// Organized as two nested \c DenseMap keyed first on the macro name then
/// the macro version. This structure allows to peek at macro names before
/// parsing a version tuple.
AvailabilityMacroMap AvailabilityMacros;

/// Has \c AvailabilityMacros been computed?
bool AvailabilityMacrosComputed = false;

public:
Parser(unsigned BufferID, SourceFile &SF, DiagnosticEngine* LexerDiags,
SILParserStateBase *SIL, PersistentParserState *PersistentState,
Expand Down Expand Up @@ -1682,9 +1698,38 @@ class Parser {
//===--------------------------------------------------------------------===//
// Availability Specification Parsing

/// Parse a comma-separated list of availability specifications.
/// Parse a comma-separated list of availability specifications. Try to
/// expand availability macros when /p ParsingMacroDefinition is false.
ParserStatus
parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs,
bool ParsingMacroDefinition = false);

/// Does the current matches an argument macro name? Parsing compiler
/// arguments as required without consuming tokens from the source file
/// parser.
bool peekAvailabilityMacroName();

/// Try to parse a reference to an availability macro and append its result
/// to \p Specs. If the current token doesn't match a macro name, return
/// a success without appending anything to \c Specs.
ParserStatus
parseAvailabilityMacro(SmallVectorImpl<AvailabilitySpec *> &Specs);

/// Parse the availability macros definitions passed as arguments.
void parseAllAvailabilityMacroArguments();

/// Result of parsing an availability macro definition.
struct AvailabilityMacroDefinition {
StringRef Name;
llvm::VersionTuple Version;
SmallVector<AvailabilitySpec *, 4> Specs;
};

/// Parse an availability macro definition from a command line argument.
/// This function should be called on a Parser set up on the command line
/// argument code.
ParserStatus
parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs);
parseAvailabilityMacroDefinition(AvailabilityMacroDefinition &Result);

ParserResult<AvailabilitySpec> parseAvailabilitySpec();
ParserResult<PlatformVersionConstraintAvailabilitySpec>
Expand Down
4 changes: 4 additions & 0 deletions lib/Frontend/CompilerInvocation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,10 @@ static bool ParseLangArgs(LangOptions &Opts, ArgList &Args,
}
}

for (const Arg *A : Args.filtered(OPT_define_availability)) {
Opts.AvailabilityMacros.push_back(A->getValue());
}

if (const Arg *A = Args.getLastArg(OPT_value_recursion_threshold)) {
unsigned threshold;
if (StringRef(A->getValue()).getAsInteger(10, threshold)) {
Expand Down
112 changes: 111 additions & 1 deletion lib/Parse/ParseDecl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1372,6 +1372,115 @@ void Parser::parseObjCSelector(SmallVector<Identifier, 4> &Names,
}
}

bool Parser::peekAvailabilityMacroName() {
parseAllAvailabilityMacroArguments();
AvailabilityMacroMap Map = AvailabilityMacros;

StringRef MacroName = Tok.getText();
return Map.find(MacroName) != Map.end();
}

ParserStatus
Parser::parseAvailabilityMacro(SmallVectorImpl<AvailabilitySpec *> &Specs) {
// Get the macros from the compiler arguments.
parseAllAvailabilityMacroArguments();
AvailabilityMacroMap Map = AvailabilityMacros;

StringRef MacroName = Tok.getText();
auto NameMatch = Map.find(MacroName);
if (NameMatch == Map.end())
return makeParserSuccess(); // No match, it could be a standard platform.

consumeToken();

llvm::VersionTuple Version;
SourceRange VersionRange;
if (Tok.isAny(tok::integer_literal, tok::floating_literal)) {
if (parseVersionTuple(Version, VersionRange,
diag::avail_query_expected_version_number))
return makeParserError();
}

auto VersionMatch = NameMatch->getSecond().find(Version);
if (VersionMatch == NameMatch->getSecond().end()) {
diagnose(PreviousLoc, diag::attr_availability_unknown_version,
Version.getAsString(), MacroName);
return makeParserError(); // Failed to match the version, that's an error.
}

Specs.append(VersionMatch->getSecond().begin(),
VersionMatch->getSecond().end());

return makeParserSuccess();
}

void Parser::parseAllAvailabilityMacroArguments() {

if (AvailabilityMacrosComputed) return;

AvailabilityMacroMap Map;

SourceManager &SM = Context.SourceMgr;
const LangOptions &LangOpts = Context.LangOpts;

for (StringRef macro: LangOpts.AvailabilityMacros) {

// Create temporary parser.
int bufferID = SM.addMemBufferCopy(macro,
"-define-availability argument");
swift::ParserUnit PU(SM,
SourceFileKind::Main, bufferID,
LangOpts,
TypeCheckerOptions(), "unknown");

ForwardingDiagnosticConsumer PDC(Context.Diags);
PU.getDiagnosticEngine().addConsumer(PDC);

// Parse the argument.
AvailabilityMacroDefinition ParsedMacro;
ParserStatus Status =
PU.getParser().parseAvailabilityMacroDefinition(ParsedMacro);
if (Status.isError())
continue;

// Copy the Specs to the requesting ASTContext from the temporary context
// that parsed the argument.
auto SpecsCopy = SmallVector<AvailabilitySpec*, 4>();
for (auto *Spec : ParsedMacro.Specs)
if (auto *PlatformVersionSpec =
dyn_cast<PlatformVersionConstraintAvailabilitySpec>(Spec)) {
auto SpecCopy =
new (Context) PlatformVersionConstraintAvailabilitySpec(
*PlatformVersionSpec);
SpecsCopy.push_back(SpecCopy);
}

ParsedMacro.Specs = SpecsCopy;

// Find the macro info by name.
AvailabilityMacroVersionMap MacroDefinition;
auto NameMatch = Map.find(ParsedMacro.Name);
if (NameMatch != Map.end()) {
MacroDefinition = NameMatch->getSecond();
}

// Set the macro info by version.
auto PreviousEntry =
MacroDefinition.insert({ParsedMacro.Version, ParsedMacro.Specs});
if (!PreviousEntry.second) {
diagnose(PU.getParser().PreviousLoc, diag::attr_availability_duplicate,
ParsedMacro.Name, ParsedMacro.Version.getAsString());
}

// Save back the macro spec.
Map.erase(ParsedMacro.Name);
Map.insert({ParsedMacro.Name, MacroDefinition});
}

AvailabilityMacros = Map;
AvailabilityMacrosComputed = true;
}

bool Parser::parseNewDeclAttribute(DeclAttributes &Attributes, SourceLoc AtLoc,
DeclAttrKind DK) {
// Ok, it is a valid attribute, eat it, and then process it.
Expand Down Expand Up @@ -1975,7 +2084,8 @@ bool Parser::parseNewDeclAttribute(DeclAttributes &Attributes, SourceLoc AtLoc,
StringRef Platform = Tok.getText();

if (Platform != "*" &&
peekToken().isAny(tok::integer_literal, tok::floating_literal)) {
(peekToken().isAny(tok::integer_literal, tok::floating_literal) ||
peekAvailabilityMacroName())) {
// We have the short form of available: @available(iOS 8.0.1, *)
SmallVector<AvailabilitySpec *, 5> Specs;
ParserStatus Status = parseAvailabilitySpecList(Specs);
Expand Down
86 changes: 73 additions & 13 deletions lib/Parse/ParseStmt.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1183,8 +1183,10 @@ static void parseGuardedPattern(Parser &P, GuardedPattern &result,

/// Validate availability spec list, emitting diagnostics if necessary and removing
/// specs for unrecognized platforms.
static void validateAvailabilitySpecList(Parser &P,
SmallVectorImpl<AvailabilitySpec *> &Specs) {
static void
validateAvailabilitySpecList(Parser &P,
SmallVectorImpl<AvailabilitySpec *> &Specs,
bool ParsingMacroDefinition) {
llvm::SmallSet<PlatformKind, 4> Platforms;
bool HasOtherPlatformSpec = false;

Expand Down Expand Up @@ -1232,8 +1234,13 @@ static void validateAvailabilitySpecList(Parser &P,
}
}

if (!HasOtherPlatformSpec) {
SourceLoc InsertWildcardLoc = Specs.back()->getSourceRange().End;
if (ParsingMacroDefinition) {
if (HasOtherPlatformSpec) {
SourceLoc InsertWildcardLoc = P.PreviousLoc;
P.diagnose(InsertWildcardLoc, diag::attr_availability_wildcard_in_macro);
}
} else if (!HasOtherPlatformSpec) {
SourceLoc InsertWildcardLoc = P.PreviousLoc;
P.diagnose(InsertWildcardLoc, diag::availability_query_wildcard_required)
.fixItInsertAfter(InsertWildcardLoc, ", *");
}
Expand Down Expand Up @@ -1285,7 +1292,40 @@ ParserResult<PoundAvailableInfo> Parser::parseStmtConditionPoundAvailable() {
}

ParserStatus
Parser::parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs) {
Parser::parseAvailabilityMacroDefinition(AvailabilityMacroDefinition &Result) {

// Prime the lexer.
if (Tok.is(tok::NUM_TOKENS))
consumeTokenWithoutFeedingReceiver();

if (!Tok.isIdentifierOrUnderscore()) {
diagnose(Tok, diag::attr_availability_missing_macro_name);
return makeParserError();
}

Result.Name = Tok.getText();
consumeToken();

if (Tok.isAny(tok::integer_literal, tok::floating_literal)) {
SourceRange VersionRange;
if (parseVersionTuple(Result.Version, VersionRange,
diag::avail_query_expected_version_number)) {
return makeParserError();
}
}

if (!consumeIf(tok::colon)) {
diagnose(Tok, diag::attr_availability_expected_colon_macro, Result.Name);
return makeParserError();
}

return parseAvailabilitySpecList(Result.Specs,
/*ParsingMacroDefinition=*/true);
}

ParserStatus
Parser::parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs,
bool ParsingMacroDefinition) {
SyntaxParsingContext AvailabilitySpecContext(
SyntaxContext, SyntaxKind::AvailabilitySpecList);
ParserStatus Status = makeParserSuccess();
Expand All @@ -1295,14 +1335,34 @@ Parser::parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs) {
while (1) {
SyntaxParsingContext AvailabilityEntryContext(
SyntaxContext, SyntaxKind::AvailabilityArgument);
auto SpecResult = parseAvailabilitySpec();
if (auto *Spec = SpecResult.getPtrOrNull()) {
Specs.push_back(Spec);
} else {
if (SpecResult.hasCodeCompletion()) {
return makeParserCodeCompletionStatus();

// First look for a macro as we need Specs for the expansion.
bool MatchedAMacro = false;
if (!ParsingMacroDefinition && Tok.is(tok::identifier)) {
SmallVector<AvailabilitySpec *, 4> MacroSpecs;
ParserStatus MacroStatus = parseAvailabilityMacro(MacroSpecs);

if (MacroStatus.isError()) {
// There's a parsing error if the platform name matches a macro
// but something goes wrong after.
Status.setIsParseError();
MatchedAMacro = true;
} else {
MatchedAMacro = !MacroSpecs.empty();
Specs.append(MacroSpecs.begin(), MacroSpecs.end());
}
}

if (!MatchedAMacro) {
auto SpecResult = parseAvailabilitySpec();
if (auto *Spec = SpecResult.getPtrOrNull()) {
Specs.push_back(Spec);
} else {
if (SpecResult.hasCodeCompletion()) {
return makeParserCodeCompletionStatus();
}
Status.setIsParseError();
}
Status.setIsParseError();
}

// We don't allow binary operators to combine specs.
Expand Down Expand Up @@ -1362,7 +1422,7 @@ Parser::parseAvailabilitySpecList(SmallVectorImpl<AvailabilitySpec *> &Specs) {
}

if (Status.isSuccess() && !Status.hasCodeCompletion())
validateAvailabilitySpecList(*this, Specs);
validateAvailabilitySpecList(*this, Specs, ParsingMacroDefinition);

return Status;
}
Expand Down
29 changes: 29 additions & 0 deletions test/ModuleInterface/availability-expansion.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// RUN: %empty-directory(%t)

// RUN: %target-swift-frontend -swift-version 5 -enable-library-evolution -typecheck -module-name Test -emit-module-interface-path %t/Test.swiftinterface %s -define-availability "_iOS8Aligned:macOS 10.10, iOS 8.0" -define-availability "_iOS9Aligned:macOS 10.11, iOS 9.0" -define-availability "_iOS9:iOS 9.0" -define-availability "_macOS10_11:macOS 10.11" -define-availability "_myProject 1.0:macOS 10.11" -define-availability "_myProject 2.5:macOS 10.12"
// RUN: %FileCheck %s < %t/Test.swiftinterface

@available(_iOS8Aligned, *)
public func onMacOS10_10() {}
// CHECK: @available(macOS 10.10, iOS 8.0, *)
// CHECK-NEXT: public func onMacOS10_10

@available(_iOS9Aligned, *)
public func onMacOS10_11() {}
// CHECK: @available(macOS 10.11, iOS 9.0, *)
// CHECK-NEXT: public func onMacOS10_11()

@available(_iOS9, _macOS10_11, tvOS 11.0, *)
public func composed() {}
// CHECK: @available(iOS 9.0, macOS 10.11, tvOS 11.0, *)
// CHECK-NEXT: public func composed()

@available(_myProject 1.0, *)
public func onMyProjectV1() {}
// CHECK: @available(macOS 10.11, *)
// CHECK-NEXT: public func onMyProjectV1

@available(_myProject 2.5, *)
public func onMyProjectV2_5() {}
// CHECK: @available(macOS 10.12, *)
// CHECK-NEXT: public func onMyProjectV2_5
Loading

0 comments on commit c6fc53e

Please sign in to comment.