Skip to content

[5.5][Parse] Postfix '#if' expression #37149

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions include/swift/AST/DiagnosticsParse.def
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,8 @@ ERROR(statement_begins_with_closure,none,
"top-level statement cannot begin with a closure expression", ())
ERROR(statement_same_line_without_semi,none,
"consecutive statements on a line must be separated by ';'", ())
ERROR(statement_same_line_without_newline,none,
"consecutive statements on a line must be separated by a newline", ())
ERROR(invalid_label_on_stmt,none,
"labels are only valid on loops, if, and switch statements", ())
ERROR(labeled_block_needs_do,none,
Expand Down Expand Up @@ -1330,6 +1332,7 @@ ERROR(expr_selector_expected_rparen,PointsToFirstBadToken,
ERROR(expr_dynamictype_deprecated,PointsToFirstBadToken,
"'.dynamicType' is deprecated. Use 'type(of: ...)' instead", ())

// '#assert'
ERROR(pound_assert_disabled,PointsToFirstBadToken,
"#assert is an experimental feature that is currently disabled", ())
ERROR(pound_assert_expected_lparen,PointsToFirstBadToken,
Expand All @@ -1341,6 +1344,10 @@ ERROR(pound_assert_expected_expression,PointsToFirstBadToken,
ERROR(pound_assert_expected_string_literal,PointsToFirstBadToken,
"expected a string literal", ())

// Postfix '#if' expressions.
ERROR(expr_postfix_ifconfig_unexpectedtoken,none,
"unexpected tokens in '#if' expression body", ())

//------------------------------------------------------------------------------
// MARK: Attribute-parsing diagnostics
//------------------------------------------------------------------------------
Expand Down
4 changes: 4 additions & 0 deletions include/swift/Parse/ParserResult.h
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,10 @@ class ParserStatus {
IsError = true;
}

void setHasCodeCompletion() {
IsCodeCompletion = true;
}

void clearIsError() {
IsError = false;
}
Expand Down
92 changes: 92 additions & 0 deletions lib/Parse/ParseExpr.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1087,6 +1087,9 @@ Parser::parseExprPostfixSuffix(ParserResult<Expr> Result, bool isExprBasic,
if (Result.isNull())
return Result;

if (InPoundIfEnvironment && Tok.isAtStartOfLine())
return Result;

if (Result.hasCodeCompletion() &&
SourceMgr.getCodeCompletionLoc() == PreviousLoc) {
// Don't parse suffixes if the expression ended with code completion
Expand Down Expand Up @@ -1305,6 +1308,95 @@ Parser::parseExprPostfixSuffix(ParserResult<Expr> Result, bool isExprBasic,
continue;
}

if (Tok.is(tok::pound_if)) {

// Helper function to see if we can parse member reference like suffixes
// inside '#if'.
auto isAtStartOfPostfixExprSuffix = [&]() {
if (!Tok.isAny(tok::period, tok::period_prefix)) {
return false;
}
if (!peekToken().isAny(tok::identifier, tok::kw_Self, tok::kw_self,
tok::integer_literal, tok::code_complete) &&
!peekToken().isKeyword()) {
return false;
}
return true;
};

// Check if the first '#if' body starts with '.' <identifier>, and parse
// it as a "postfix ifconfig expression".
bool isPostfixIfConfigExpr = false;
{
llvm::SaveAndRestore<Optional<StableHasher>> H(CurrentTokenHash, None);
Parser::BacktrackingScope Backtrack(*this);
// Skip to the first body. We may need to skip multiple '#if' directives
// since we support nested '#if's. e.g.
// baseExpr
// #if CONDITION_1
// #if CONDITION_2
// .someMember
do {
consumeToken(tok::pound_if);
skipUntilTokenOrEndOfLine(tok::NUM_TOKENS);
} while (Tok.is(tok::pound_if));
isPostfixIfConfigExpr = isAtStartOfPostfixExprSuffix();
}
if (!isPostfixIfConfigExpr)
break;

if (!Tok.isAtStartOfLine()) {
diagnose(Tok, diag::statement_same_line_without_newline)
.fixItInsert(getEndOfPreviousLoc(), "\n");
}

llvm::SmallPtrSet<Expr *, 4> exprsWithBindOptional;
auto ICD =
parseIfConfig([&](SmallVectorImpl<ASTNode> &elements, bool isActive) {
SyntaxParsingContext postfixCtx(SyntaxContext,
SyntaxContextKind::Expr);
// Although we know the '#if' body starts with period,
// '#elseif'/'#else' bodies might start with invalid tokens.
if (isAtStartOfPostfixExprSuffix() || Tok.is(tok::pound_if)) {
bool exprHasBindOptional = false;
auto expr = parseExprPostfixSuffix(Result, isExprBasic,
periodHasKeyPathBehavior,
exprHasBindOptional);
if (exprHasBindOptional)
exprsWithBindOptional.insert(expr.get());
elements.push_back(expr.get());
}

// Don't allow any character other than the postfix expression.
if (!Tok.isAny(tok::pound_elseif, tok::pound_else, tok::pound_endif,
tok::eof)) {
diagnose(Tok, diag::expr_postfix_ifconfig_unexpectedtoken);
skipUntilConditionalBlockClose();
}
});
if (ICD.isNull())
break;

SyntaxContext->createNodeInPlace(SyntaxKind::PostfixIfConfigExpr);

auto activeElements = ICD.get()->getActiveClauseElements();
if (activeElements.empty())
// There's no active clause, or it was empty. Keep the current result.
continue;

// Extract the parsed expression as the result.
assert(activeElements.size() == 1 && activeElements[0].is<Expr *>());
auto expr = activeElements[0].get<Expr *>();
ParserStatus status(ICD);
if (SourceMgr.getCodeCompletionLoc().isValid() &&
SourceMgr.rangeContainsTokenLoc(expr->getSourceRange(),
SourceMgr.getCodeCompletionLoc()))
status.setHasCodeCompletion();
hasBindOptional |= exprsWithBindOptional.contains(expr);
Result = makeParserResult(status, expr);
continue;
}

if (Tok.is(tok::code_complete)) {
if (InSwiftKeyPath)
return Result;
Expand Down
2 changes: 2 additions & 0 deletions lib/Parse/ParseIfConfig.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,8 @@ ParserResult<IfConfigDecl> Parser::parseIfConfig(
// clause unless we're doing a parse-only pass.
if (isElse) {
isActive = !foundActive && shouldEvaluate;
if (SyntaxContext->isEnabled())
SyntaxContext->addRawSyntax(ParsedRawSyntaxNode());
} else {
llvm::SaveAndRestore<bool> S(InPoundIfEnvironment, true);
ParserResult<Expr> Result = parseExprSequence(diag::expected_expr,
Expand Down
1 change: 1 addition & 0 deletions lib/Parse/SyntaxParsingContext.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ void SyntaxParsingContext::createNodeInPlace(SyntaxKind Kind,
case SyntaxKind::MemberTypeIdentifier:
case SyntaxKind::FunctionCallExpr:
case SyntaxKind::SubscriptExpr:
case SyntaxKind::PostfixIfConfigExpr:
case SyntaxKind::ExprList: {
createNodeInPlace(Kind, getParts().size(), nodeCreateK);
break;
Expand Down
4 changes: 2 additions & 2 deletions test/Parse/ConditionalCompilation/basicParseErrors.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ func h() {}
#else /* aaa */
#endif /* bbb */

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

#endif

Expand Down
127 changes: 127 additions & 0 deletions test/Parse/ifconfig_expr.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// RUN: %target-typecheck-verify-swift -D CONDITION_1

postfix operator ++
postfix func ++ (_: Int) -> Int { 0 }

struct OneResult {}
struct TwoResult {}

protocol MyProto {
func optionalMethod() -> [Int]?
}
struct MyStruct {
var optionalMember: MyProto? { nil }
func methodOne() -> OneResult { OneResult() }
func methodTwo() -> TwoResult { TwoResult() }
}

func globalFunc<T>(_ arg: T) -> T { arg }

func testBasic(baseExpr: MyStruct) {
baseExpr
#if CONDITION_1
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
#else
.methodTwo()
#endif
}

MyStruct()
#if CONDITION_1
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
#else
.methodTwo()
#endif


func testInvalidContent(baseExpr: MyStruct, otherExpr: Int) {
baseExpr // expected-warning {{expression of type 'MyStruct' is unused}}
#if CONDITION_1
{ $0 + 1 } // expected-error {{closure expression is unused}}
#endif

baseExpr // expected-warning {{expression of type 'MyStruct' is unused}}
#if CONDITION_1
+ otherExpr // expected-error {{unary operator cannot be separated from its operand}}
// expected-warning@-1 {{result of operator '+' is unused}}
#endif

baseExpr
#if CONDITION_1
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}

print("debug") // expected-error {{unexpected tokens in '#if' expression body}}
#endif
}

func testExprKind(baseExpr: MyStruct, idx: Int) {
baseExpr
#if CONDITION_1
.optionalMember?.optionalMethod()![idx]++ // expected-warning {{result of operator '++' is unused}}
#else
.otherMethod(arg) {
//...
}
#endif

baseExpr
#if CONDITION_1
.methodOne() + 12 // expected-error {{unexpected tokens in '#if' expression body}}
// expected-warning@-1 {{result of call to 'methodOne()' is unused}}
#endif
}

func emptyElse(baseExpr: MyStruct) {
baseExpr
#if CONDITION_1
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
#elseif CONDITION_2
// OK. Do nothing.
#endif

baseExpr
#if CONDITION_1
.methodOne() // expected-warning {{result of call to 'methodOne()' is unused}}
#elseif CONDITION_2
return // expected-error {{unexpected tokens in '#if' expression body}}
#endif
}

func consecutiveIfConfig(baseExpr: MyStruct) {
baseExpr
#if CONDITION_1
.methodOne()
#endif
#if CONDITION_2
.methodTwo()
#endif
.unknownMethod() // expected-error {{value of type 'OneResult' has no member 'unknownMethod'}}
}

func nestedIfConfig(baseExpr: MyStruct) {
baseExpr
#if CONDITION_1
#if CONDITION_2
.methodOne()
#endif
#if CONDITION_1
.methodTwo() // expected-warning {{result of call to 'methodTwo()' is unused}}
#endif
#else
.unknownMethod1()
#if CONDITION_2
.unknownMethod2()
#endif
#endif
}

func ifconfigExprInExpr(baseExpr: MyStruct) {
globalFunc( // expected-warning {{result of call to 'globalFunc' is unused}}
baseExpr
#if CONDITION_1
.methodOne()
#else
.methodTwo()
#endif
)
}
13 changes: 13 additions & 0 deletions test/Parse/omit_return_ifdecl.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2511,3 +2511,16 @@ var mismatched_implicit_return_ifdecl: Int {
return 1
#endif
}

enum VerticalDirection {
case up, down
}
func implicitMember() -> VerticalDirection {
#if false
.up
#elseif true
.down
#else
.unknown
#endif
}
9 changes: 8 additions & 1 deletion test/Syntax/Outputs/round_trip_parse_gen.swift.withkinds
Original file line number Diff line number Diff line change
Expand Up @@ -596,4 +596,11 @@ func foo<FunctionSignature><ParameterClause>() </ParameterClause></FunctionSigna
#"<StringSegment>abc</StringSegment>"#</StringLiteralExpr><StringLiteralExpr>
#"<StringSegment>abc </StringSegment><ExpressionSegment>\#(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"#</StringLiteralExpr><StringLiteralExpr>
##"<StringSegment>abc</StringSegment>"##</StringLiteralExpr><StringLiteralExpr>
##"<StringSegment>abc </StringSegment><ExpressionSegment>\##(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"##</StringLiteralExpr>
##"<StringSegment>abc </StringSegment><ExpressionSegment>\##(<TupleExprElement><IdentifierExpr>foo</IdentifierExpr></TupleExprElement>)</ExpressionSegment><StringSegment></StringSegment>"##</StringLiteralExpr><PostfixIfConfigExpr><FunctionCallExpr><IdentifierExpr>

foo</IdentifierExpr>()</FunctionCallExpr><IfConfigDecl><IfConfigClause>
#if <BooleanLiteralExpr>true</BooleanLiteralExpr><ForcedValueExpr><FunctionCallExpr><OptionalChainingExpr><MemberAccessExpr>
.bar</MemberAccessExpr>?</OptionalChainingExpr>()</FunctionCallExpr>!</ForcedValueExpr></IfConfigClause><IfConfigClause>
#else<FunctionCallExpr><MemberAccessExpr>
.baz</MemberAccessExpr>() <ClosureExpr>{}</ClosureExpr></FunctionCallExpr></IfConfigClause>
#endif</IfConfigDecl></PostfixIfConfigExpr>
7 changes: 7 additions & 0 deletions test/Syntax/round_trip_parse_gen.swift
Original file line number Diff line number Diff line change
Expand Up @@ -597,3 +597,10 @@ func foo() {}
#"abc \#(foo)"#
##"abc"##
##"abc \##(foo)"##

foo()
#if true
.bar?()!
#else
.baz() {}
#endif
1 change: 1 addition & 0 deletions utils/gyb_syntax_support/DeclNodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@
Child('Statements', kind='CodeBlockItemList'),
Child('SwitchCases', kind='SwitchCaseList'),
Child('Decls', kind='MemberDeclList'),
Child('PostfixExpression', kind='Expr'),
]),
]),

Expand Down
7 changes: 7 additions & 0 deletions utils/gyb_syntax_support/ExprNodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,13 @@
Child('RightParen', kind='RightParenToken'),
]),

# postfix '#if' expession
Node('PostfixIfConfigExpr', kind='Expr',
children=[
Child('Base', kind='Expr'),
Child('Config', kind='IfConfigDecl'),
]),

# <#content#>
Node('EditorPlaceholderExpr', kind='Expr',
children=[
Expand Down
1 change: 1 addition & 0 deletions utils/gyb_syntax_support/NodeSerializationCodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@
'MultipleTrailingClosureElement': 246,
'PoundFileIDExpr': 247,
'TargetFunctionEntry': 248,
'PostfixIfConfigExpr': 250,
}


Expand Down
2 changes: 2 additions & 0 deletions utils/gyb_syntax_support/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ def check_parsed_child_condition_raw(child):
"""
result = '[](const ParsedRawSyntaxNode &Raw) {\n'
result += ' // check %s\n' % child.name
if child.is_optional:
result += 'if (Raw.isNull()) return true;\n'
if child.token_choices:
result += 'if (!Raw.isToken()) return false;\n'
result += 'auto TokKind = Raw.getTokenKind();\n'
Expand Down