Skip to content

Commit ce87bf7

Browse files
committed
[Parse] Postfix '#if' expression
Implement postfix ifconfig expression which expands '#if' functionality to postfix member reference expressions. rdar://problem/51690082
1 parent 605a923 commit ce87bf7

File tree

14 files changed

+274
-3
lines changed

14 files changed

+274
-3
lines changed

include/swift/AST/DiagnosticsParse.def

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,8 @@ ERROR(statement_begins_with_closure,none,
966966
"top-level statement cannot begin with a closure expression", ())
967967
ERROR(statement_same_line_without_semi,none,
968968
"consecutive statements on a line must be separated by ';'", ())
969+
ERROR(statement_same_line_without_newline,none,
970+
"consecutive statements on a line must be separated by a newline", ())
969971
ERROR(invalid_label_on_stmt,none,
970972
"labels are only valid on loops, if, and switch statements", ())
971973
ERROR(labeled_block_needs_do,none,
@@ -1330,6 +1332,7 @@ ERROR(expr_selector_expected_rparen,PointsToFirstBadToken,
13301332
ERROR(expr_dynamictype_deprecated,PointsToFirstBadToken,
13311333
"'.dynamicType' is deprecated. Use 'type(of: ...)' instead", ())
13321334

1335+
// '#assert'
13331336
ERROR(pound_assert_disabled,PointsToFirstBadToken,
13341337
"#assert is an experimental feature that is currently disabled", ())
13351338
ERROR(pound_assert_expected_lparen,PointsToFirstBadToken,
@@ -1341,6 +1344,10 @@ ERROR(pound_assert_expected_expression,PointsToFirstBadToken,
13411344
ERROR(pound_assert_expected_string_literal,PointsToFirstBadToken,
13421345
"expected a string literal", ())
13431346

1347+
// Postfix '#if' expressions.
1348+
ERROR(expr_postfix_ifconfig_unexpectedtoken,none,
1349+
"unexpected tokens in '#if' expression body", ())
1350+
13441351
//------------------------------------------------------------------------------
13451352
// MARK: Attribute-parsing diagnostics
13461353
//------------------------------------------------------------------------------

include/swift/Parse/ParserResult.h

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ class ParserStatus {
183183
IsError = true;
184184
}
185185

186+
void setHasCodeCompletion() {
187+
IsCodeCompletion = true;
188+
}
189+
186190
void clearIsError() {
187191
IsError = false;
188192
}

lib/Parse/ParseExpr.cpp

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,6 +1087,9 @@ Parser::parseExprPostfixSuffix(ParserResult<Expr> Result, bool isExprBasic,
10871087
if (Result.isNull())
10881088
return Result;
10891089

1090+
if (InPoundIfEnvironment && Tok.isAtStartOfLine())
1091+
return Result;
1092+
10901093
if (Result.hasCodeCompletion() &&
10911094
SourceMgr.getCodeCompletionLoc() == PreviousLoc) {
10921095
// Don't parse suffixes if the expression ended with code completion
@@ -1305,6 +1308,95 @@ Parser::parseExprPostfixSuffix(ParserResult<Expr> Result, bool isExprBasic,
13051308
continue;
13061309
}
13071310

1311+
if (Tok.is(tok::pound_if)) {
1312+
1313+
// Helper function to see if we can parse member reference like suffixes
1314+
// inside '#if'.
1315+
auto isAtStartOfPostfixExprSuffix = [&]() {
1316+
if (!Tok.isAny(tok::period, tok::period_prefix)) {
1317+
return false;
1318+
}
1319+
if (!peekToken().isAny(tok::identifier, tok::kw_Self, tok::kw_self,
1320+
tok::integer_literal, tok::code_complete) &&
1321+
!peekToken().isKeyword()) {
1322+
return false;
1323+
}
1324+
return true;
1325+
};
1326+
1327+
// Check if the first '#if' body starts with '.' <identifier>, and parse
1328+
// it as a "postfix ifconfig expression".
1329+
bool isPostfixIfConfigExpr = false;
1330+
{
1331+
llvm::SaveAndRestore<Optional<StableHasher>> H(CurrentTokenHash, None);
1332+
Parser::BacktrackingScope Backtrack(*this);
1333+
// Skip to the first body. We may need to skip multiple '#if' directives
1334+
// since we support nested '#if's. e.g.
1335+
// baseExpr
1336+
// #if CONDITION_1
1337+
// #if CONDITION_2
1338+
// .someMember
1339+
do {
1340+
consumeToken(tok::pound_if);
1341+
skipUntilTokenOrEndOfLine(tok::NUM_TOKENS);
1342+
} while (Tok.is(tok::pound_if));
1343+
isPostfixIfConfigExpr = isAtStartOfPostfixExprSuffix();
1344+
}
1345+
if (!isPostfixIfConfigExpr)
1346+
break;
1347+
1348+
if (!Tok.isAtStartOfLine()) {
1349+
diagnose(Tok, diag::statement_same_line_without_newline)
1350+
.fixItInsert(getEndOfPreviousLoc(), "\n");
1351+
}
1352+
1353+
llvm::SmallPtrSet<Expr *, 4> exprsWithBindOptional;
1354+
auto ICD =
1355+
parseIfConfig([&](SmallVectorImpl<ASTNode> &elements, bool isActive) {
1356+
SyntaxParsingContext postfixCtx(SyntaxContext,
1357+
SyntaxContextKind::Expr);
1358+
// Although we know the '#if' body starts with period,
1359+
// '#elseif'/'#else' bodies might start with invalid tokens.
1360+
if (isAtStartOfPostfixExprSuffix() || Tok.is(tok::pound_if)) {
1361+
bool exprHasBindOptional = false;
1362+
auto expr = parseExprPostfixSuffix(Result, isExprBasic,
1363+
periodHasKeyPathBehavior,
1364+
exprHasBindOptional);
1365+
if (exprHasBindOptional)
1366+
exprsWithBindOptional.insert(expr.get());
1367+
elements.push_back(expr.get());
1368+
}
1369+
1370+
// Don't allow any character other than the postfix expression.
1371+
if (!Tok.isAny(tok::pound_elseif, tok::pound_else, tok::pound_endif,
1372+
tok::eof)) {
1373+
diagnose(Tok, diag::expr_postfix_ifconfig_unexpectedtoken);
1374+
skipUntilConditionalBlockClose();
1375+
}
1376+
});
1377+
if (ICD.isNull())
1378+
break;
1379+
1380+
SyntaxContext->createNodeInPlace(SyntaxKind::PostfixIfConfigExpr);
1381+
1382+
auto activeElements = ICD.get()->getActiveClauseElements();
1383+
if (activeElements.empty())
1384+
// There's no active clause, or it was empty. Keep the current result.
1385+
continue;
1386+
1387+
// Extract the parsed expression as the result.
1388+
assert(activeElements.size() == 1 && activeElements[0].is<Expr *>());
1389+
auto expr = activeElements[0].get<Expr *>();
1390+
ParserStatus status(ICD);
1391+
if (SourceMgr.getCodeCompletionLoc().isValid() &&
1392+
SourceMgr.rangeContainsTokenLoc(expr->getSourceRange(),
1393+
SourceMgr.getCodeCompletionLoc()))
1394+
status.setHasCodeCompletion();
1395+
hasBindOptional |= exprsWithBindOptional.contains(expr);
1396+
Result = makeParserResult(status, expr);
1397+
continue;
1398+
}
1399+
13081400
if (Tok.is(tok::code_complete)) {
13091401
if (InSwiftKeyPath)
13101402
return Result;

lib/Parse/ParseIfConfig.cpp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -673,6 +673,8 @@ ParserResult<IfConfigDecl> Parser::parseIfConfig(
673673
// clause unless we're doing a parse-only pass.
674674
if (isElse) {
675675
isActive = !foundActive && shouldEvaluate;
676+
if (SyntaxContext->isEnabled())
677+
SyntaxContext->addRawSyntax(ParsedRawSyntaxNode());
676678
} else {
677679
llvm::SaveAndRestore<bool> S(InPoundIfEnvironment, true);
678680
ParserResult<Expr> Result = parseExprSequence(diag::expected_expr,

lib/Parse/SyntaxParsingContext.cpp

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,7 @@ void SyntaxParsingContext::createNodeInPlace(SyntaxKind Kind,
266266
case SyntaxKind::MemberTypeIdentifier:
267267
case SyntaxKind::FunctionCallExpr:
268268
case SyntaxKind::SubscriptExpr:
269+
case SyntaxKind::PostfixIfConfigExpr:
269270
case SyntaxKind::ExprList: {
270271
createNodeInPlace(Kind, getParts().size(), nodeCreateK);
271272
break;

test/Parse/ConditionalCompilation/basicParseErrors.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ func h() {}
2727
#else /* aaa */
2828
#endif /* bbb */
2929

30-
#if foo.bar()
31-
.baz() // expected-error {{unexpected platform condition (expected 'os', 'arch', or 'swift')}}
30+
#if foo.bar() // expected-error {{unexpected platform condition (expected 'os', 'arch', or 'swift')}}
31+
.baz()
3232

3333
#endif
3434

test/Parse/ifconfig_expr.swift

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// RUN: %target-typecheck-verify-swift -D CONDITION_1
2+
3+
postfix operator ++
4+
postfix func ++ (_: Int) -> Int { 0 }
5+
6+
struct OneResult {}
7+
struct TwoResult {}
8+
9+
protocol MyProto {
10+
func optionalMethod() -> [Int]?
11+
}
12+
struct MyStruct {
13+
var optionalMember: MyProto? { nil }
14+
func methodOne() -> OneResult { OneResult() }
15+
func methodTwo() -> TwoResult { TwoResult() }
16+
}
17+
18+
func globalFunc<T>(_ arg: T) -> T { arg }
19+
20+
func testBasic(baseExpr: MyStruct) {
21+
baseExpr
22+
#if CONDITION_1
23+
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
24+
#else
25+
.methodTwo()
26+
#endif
27+
}
28+
29+
MyStruct()
30+
#if CONDITION_1
31+
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
32+
#else
33+
.methodTwo()
34+
#endif
35+
36+
37+
func testInvalidContent(baseExpr: MyStruct, otherExpr: Int) {
38+
baseExpr // expected-warning {{expression of type 'MyStruct' is unused}}
39+
#if CONDITION_1
40+
{ $0 + 1 } // expected-error {{closure expression is unused}}
41+
#endif
42+
43+
baseExpr // expected-warning {{expression of type 'MyStruct' is unused}}
44+
#if CONDITION_1
45+
+ otherExpr // expected-error {{unary operator cannot be separated from its operand}}
46+
// expected-warning@-1 {{result of operator '+' is unused}}
47+
#endif
48+
49+
baseExpr
50+
#if CONDITION_1
51+
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
52+
53+
print("debug") // expected-error {{unexpected tokens in '#if' expression body}}
54+
#endif
55+
}
56+
57+
func testExprKind(baseExpr: MyStruct, idx: Int) {
58+
baseExpr
59+
#if CONDITION_1
60+
.optionalMember?.optionalMethod()![idx]++ // expected-warning {{result of operator '++' is unused}}
61+
#else
62+
.otherMethod(arg) {
63+
//...
64+
}
65+
#endif
66+
67+
baseExpr
68+
#if CONDITION_1
69+
.methodOne() + 12 // expected-error {{unexpected tokens in '#if' expression body}}
70+
// expected-warning@-1 {{result of call to 'methodOne()' is unused}}
71+
#endif
72+
}
73+
74+
func emptyElse(baseExpr: MyStruct) {
75+
baseExpr
76+
#if CONDITION_1
77+
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
78+
#elseif CONDITION_2
79+
// OK. Do nothing.
80+
#endif
81+
82+
baseExpr
83+
#if CONDITION_1
84+
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
85+
#elseif CONDITION_2
86+
return // expected-error {{unexpected tokens in '#if' expression body}}
87+
#endif
88+
}
89+
90+
func consecutiveIfConfig(baseExpr: MyStruct) {
91+
baseExpr
92+
#if CONDITION_1
93+
.methodOne()
94+
#endif
95+
#if CONDITION_2
96+
.methodTwo()
97+
#endif
98+
.unknownMethod() // expected-error {{value of type 'OneResult' has no member 'unknownMethod'}}
99+
}
100+
101+
func nestedIfConfig(baseExpr: MyStruct) {
102+
baseExpr
103+
#if CONDITION_1
104+
#if CONDITION_2
105+
.methodOne()
106+
#endif
107+
#if CONDITION_1
108+
.methodTwo() // expected-warning {{result of call to 'methodTwo()' is unused}}
109+
#endif
110+
#else
111+
.unknownMethod1()
112+
#if CONDITION_2
113+
.unknownMethod2()
114+
#endif
115+
#endif
116+
}
117+
118+
func ifconfigExprInExpr(baseExpr: MyStruct) {
119+
globalFunc( // expected-warning {{result of call to 'globalFunc' is unused}}
120+
baseExpr
121+
#if CONDITION_1
122+
.methodOne()
123+
#else
124+
.methodTwo()
125+
#endif
126+
)
127+
}

test/Parse/omit_return_ifdecl.swift

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2511,3 +2511,16 @@ var mismatched_implicit_return_ifdecl: Int {
25112511
return 1
25122512
#endif
25132513
}
2514+
2515+
enum VerticalDirection {
2516+
case up, down
2517+
}
2518+
func implicitMember() -> VerticalDirection {
2519+
#if false
2520+
.up
2521+
#elseif true
2522+
.down
2523+
#else
2524+
.unknown
2525+
#endif
2526+
}

test/Syntax/Outputs/round_trip_parse_gen.swift.withkinds

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -596,4 +596,11 @@ func foo<FunctionSignature><ParameterClause>() </ParameterClause></FunctionSigna
596596
#"<StringSegment>abc</StringSegment>"#</StringLiteralExpr><StringLiteralExpr>
597597
#"<StringSegment>abc </StringSegment><ExpressionSegment>\#(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"#</StringLiteralExpr><StringLiteralExpr>
598598
##"<StringSegment>abc</StringSegment>"##</StringLiteralExpr><StringLiteralExpr>
599-
##"<StringSegment>abc </StringSegment><ExpressionSegment>\##(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"##</StringLiteralExpr>
599+
##"<StringSegment>abc </StringSegment><ExpressionSegment>\##(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"##</StringLiteralExpr><PostfixIfConfigExpr><FunctionCallExpr><IdentifierExpr>
600+
601+
foo</IdentifierExpr>()</FunctionCallExpr><IfConfigDecl><IfConfigClause>
602+
#if <BooleanLiteralExpr>true</BooleanLiteralExpr><ForcedValueExpr><FunctionCallExpr><OptionalChainingExpr><MemberAccessExpr>
603+
.bar</MemberAccessExpr>?</OptionalChainingExpr>()</FunctionCallExpr>!</ForcedValueExpr></IfConfigClause><IfConfigClause>
604+
#else<FunctionCallExpr><MemberAccessExpr>
605+
.baz</MemberAccessExpr>() <ClosureExpr>{}</ClosureExpr></FunctionCallExpr></IfConfigClause>
606+
#endif</IfConfigDecl></PostfixIfConfigExpr>

test/Syntax/round_trip_parse_gen.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -597,3 +597,10 @@ func foo() {}
597597
#"abc \#(foo)"#
598598
##"abc"##
599599
##"abc \##(foo)"##
600+
601+
foo()
602+
#if true
603+
.bar?()!
604+
#else
605+
.baz() {}
606+
#endif

utils/gyb_syntax_support/DeclNodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@
106106
Child('Statements', kind='CodeBlockItemList'),
107107
Child('SwitchCases', kind='SwitchCaseList'),
108108
Child('Decls', kind='MemberDeclList'),
109+
Child('PostfixExpression', kind='Expr'),
109110
]),
110111
]),
111112

utils/gyb_syntax_support/ExprNodes.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -592,6 +592,13 @@
592592
Child('RightParen', kind='RightParenToken'),
593593
]),
594594

595+
# postfix '#if' expession
596+
Node('PostfixIfConfigExpr', kind='Expr',
597+
children=[
598+
Child('Base', kind='Expr'),
599+
Child('Config', kind='IfConfigDecl'),
600+
]),
601+
595602
# <#content#>
596603
Node('EditorPlaceholderExpr', kind='Expr',
597604
children=[

utils/gyb_syntax_support/NodeSerializationCodes.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,7 @@
251251
'MultipleTrailingClosureElement': 246,
252252
'PoundFileIDExpr': 247,
253253
'TargetFunctionEntry': 248,
254+
'PostfixIfConfigExpr': 250,
254255
}
255256

256257

utils/gyb_syntax_support/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ def check_parsed_child_condition_raw(child):
8787
"""
8888
result = '[](const ParsedRawSyntaxNode &Raw) {\n'
8989
result += ' // check %s\n' % child.name
90+
if child.is_optional:
91+
result += 'if (Raw.isNull()) return true;\n'
9092
if child.token_choices:
9193
result += 'if (!Raw.isToken()) return false;\n'
9294
result += 'auto TokKind = Raw.getTokenKind();\n'

0 commit comments

Comments
 (0)