Skip to content

Conversation

rintaro
Copy link
Member

@rintaro rintaro commented Sep 26, 2025

Consider the following code:

}
@attr1 func foo() {}

Previously, this was parsed as a FunctionDeclSyntax with the unexpected code } @attr attached as unexpectedBeforeFuncKeyword. This was problematic because @attr was effectively ignored. The TokenPrecedence-based recovery strategy couldn't handle this well, since @ is not a declaration keyword and doesn't by itself signal the start of a declaration.

One option would be to check atStartOfDeclaration() after each skipSingle() and recognize @ as the start of a declaration. However, then the preceding } would need to be attached to the first attribute in the attribute list, which would require threading recovery logic into parseAttributeList() and only applying it to the first parseAttribute(). This would make the code more complex and harder to maintain.

This PR introduces a new syntax node UnexpectedCodeDeclSyntax

  • It represents unexpected code at the statement/declaration level.
  • All statement-level recovery is now handled by parseUnexpectedCodeDeclaration().
    This simplifies recovery handling significantly.

Also:

  • Improve #if related recovery:
    • orphan #endif et al at top-level is now UnexpectedCodeDecl instead of making everything after it "unexpected"
      #endif
      func foo() {}
    • Unbalanced #endif et al closes code/member block with missing }.
      struct S {
        #if FOO
        func foo() {
        #endif
      }
      
    • Simplified parsePoundDirective(). It now receives only one closure instead of 3.
  • Don't attach ';' to compiler directives. For example:
    #sourceLocation(file: "<file>", line: 100)
    ;
    ; is now diagnosed as ;-only statement, or unexpected ; separator depending on the position.
  • Adjust TokenPrecedence for decl/statement keyword so. Decl keyword recovery won't go beyond } or statements. E.g.
    func foo() {
      @attr
    }
    struct S {}
    func foo() {
      @attr
      if true {}
      struct S {}
    }
  • cont...

@rintaro rintaro force-pushed the parser-recover-stmtlevel branch from c706d4c to 2ca55fe Compare September 26, 2025 23:27
_ = subparser.consumeAttributeList()
hasAttribute = true
} else if subparser.at(.poundIf) && subparser.consumeIfConfigOfAttributes() {
subparser.skipSingle()
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

subparser.consumeIfConfigOfAttributes() already skipped #if .... #endif

@rintaro rintaro force-pushed the parser-recover-stmtlevel branch from ef1bf91 to dc4e479 Compare September 27, 2025 03:58
base: .syntax,
nameForDiagnostics: "declaration",
parserFunction: "parseDeclaration"
parserFunction: "parseDeclarationOrIfConfig"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseDeclaration() don't parse #if ... #endif. Newly introduced parseDeclarationOrIfConfig() just for DeclSyntax.parse(from:)

mutating func atStartOfDeclaration(
isAtTopLevel: Bool = false,
allowInitDecl: Bool = true,
allowRecovery: Bool = false
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed allowRecovery from atStartOfDeclaration()/atStartOfStatement()/atStartOfSwitchCase() because all statement-level recoveries are now done by parseUnexpectedCodeDecl()

mutating func parseDeclaration(in context: DeclarationParseContext = .topLevelOrCodeBlock) -> RawDeclSyntax {
// If we are at a `#if` of attributes, the `#if` directive should be
// parsed when we're parsing the attributes.
if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This block is basically hoisted to parseMemberBlockItem()

  • To align with parseCodeBlockItem() scheme
  • Don't want to attach ; to IfConfigDeclSyntax

Comment on lines -383 to -388
let isProbablyVarDecl = self.at(.identifier, .wildcard) && self.peek(isAt: .colon, .equal, .comma)
let isProbablyTupleDecl = self.at(.leftParen) && self.peek(isAt: .identifier, .wildcard)

if isProbablyVarDecl || isProbablyTupleDecl {
return RawDeclSyntax(self.parseBindingDeclaration(attrs, .missing(.keyword(.var)), in: context))
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Factored out to atBindingDeclarationWithoutVarKeyword() and move the handling to recoveryResult above, aligning with atFunctionDeclarationWithoutFuncKeyword().

Comment on lines -401 to -404

if atFunctionDeclarationWithoutFuncKeyword() {
return RawDeclSyntax(self.parseFuncDeclaration(attrs, .missing(.keyword(.func))))
}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already handled in recoveryResult above.

if self.atStartOfStatement(preferExpr: true) || self.atStartOfDeclaration() {
return false
}
if self.atStartOfLine && self.withLookahead({ $0.atStartOfSwitchCase() }) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously case was a start of statement, but not anymore. This prevents the second case below is parsed as the returning expression.

case .foo:
  return
case bar:
  ...


let item: RawCodeBlockItemSyntax.Item
let attachSemi: Bool
if self.at(.poundIf) && !self.withLookahead({ $0.consumeIfConfigOfAttributes() }) {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inlined parseItem() because we want to choose whether to attach ; (attachSemi) depending on what we parse.

Comment on lines +206 to +213
} else {
// Otherwise, eat the unexpected tokens into an "decl".
item = .decl(
RawDeclSyntax(
self.parseUnexpectedCodeDeclaration(allowInitDecl: allowInitDecl, requiresDecl: false, until: stopCondition)
)
)
attachSemi = true
Copy link
Member Author

@rintaro rintaro Sep 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Previously in parseItem(), we tried atStartOfStatement(allowRecovery: true) and atStartOfDeclaration(allowRecovery: true) for recoveries.
Now unexpected tokens at this position are always parsed as UnexpectedCodeDeclSyntax until we find a decl, statement, or expression.

1 | func o() {
2 | }👨‍👩‍👧‍👦}
| |`- error: extraneous braces at top level
| |`- error: unexpected braces in source file
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This message change is because previously the UnexpectedNodesSyntax was stored as SourceFileSyntax.unexpectedBetweenStatementsAndEndOfFileToken, but now it's UnexpectedCodeDeclSyntax in SourceFileSyntax.statements.

DiagnosticSpec(
locationMarker: "2️⃣",
message: "unexpected code '))' before macro"
message: "unexpected code '))' in class"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This and similar changes are because the unexpected code are now separate UnexpectedCodeDeclSyntax in the statements.

@rintaro
Copy link
Member Author

rintaro commented Sep 27, 2025

@swift-ci Please test

@rintaro
Copy link
Member Author

rintaro commented Sep 27, 2025

swiftlang/swift#84553
@swift-ci Please test

@rintaro rintaro force-pushed the parser-recover-stmtlevel branch from 5ceb6e8 to f9a97cf Compare September 28, 2025 03:08
@rintaro
Copy link
Member Author

rintaro commented Sep 28, 2025

swiftlang/swift#84553
@swift-ci Please test

@rintaro
Copy link
Member Author

rintaro commented Sep 28, 2025

swiftlang/swift#84553
@swift-ci Please test

@rintaro
Copy link
Member Author

rintaro commented Sep 28, 2025

swiftlang/swift#84553
@swift-ci Please test Windows

2 similar comments
@rintaro
Copy link
Member Author

rintaro commented Sep 28, 2025

swiftlang/swift#84553
@swift-ci Please test Windows

@rintaro
Copy link
Member Author

rintaro commented Sep 28, 2025

swiftlang/swift#84553
@swift-ci Please test Windows

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant